diff --git a/mobile/ios/.gitattributes b/mobile/ios/.gitattributes new file mode 100644 index 0000000000..f301122286 --- /dev/null +++ b/mobile/ios/.gitattributes @@ -0,0 +1,3 @@ +# Don't include third party files in the Github language stats! +ThirdParty/* linguist-vendored=true +FxA/* linguist-vendored=true \ No newline at end of file diff --git a/mobile/ios/.gitignore b/mobile/ios/.gitignore new file mode 100644 index 0000000000..ad0f13f34d --- /dev/null +++ b/mobile/ios/.gitignore @@ -0,0 +1,77 @@ +# Xcode +build/ +*.pbxuser +!default.pbxuser +*.mode1v3 +!default.mode1v3 +*.mode2v3 +!default.mode2v3 +*.perspectivev3 +!default.perspectivev3 +xcuserdata +*.xccheckout +*.moved-aside +DerivedData +*.hmap +*.ipa +*.xcuserstate +*.xcscmblueprint + +/fastlane/scripts/upload.sh +/fastlane/README.md +/firefox-ios-l10n +# fastlane temporary profiling data +/fastlane/report.xml +# deliver temporary error output +/fastlane/Error*.png +# deliver temporary preview output +/fastlane/Preview.html +# snapshot generated screenshots +/fastlane/screenshots +/fastlane/screenshots/*/*-portrait.png +/fastlane/screenshots/*/*-landscape.png +/fastlane/screenshots/screenshots.html +# frameit generated screenshots +/fastlane/screenshots/*/*-portrait_framed.png +/fastlane/screenshots/*/*-landscape_framed.png +# folders for storing builds and prov profiles +/builds +/provisioning-profiles +/assets +#build tools +/fastlane/Appfile +/fastlane/Snapfile +/fastlane/SnapshotHelper.swift +/fastlane/frames +/fastlane/scripts +/fastlane/templates + +#python environment +python-env/ + +# OS X +.DS_Store + +# Vim +*~ +.*.sw* + +# IDEA +.idea + +Carthage/ + +ThirdParty/google-breakpad + +# Saved Sync credentials for tests. +signedInUser.json + +# Generated config file +MozBuildID.xcconfig + +# Python. +*.pyc + +# SQLite +*.db-shm +*.db-wal diff --git a/mobile/ios/.swiftlint.yml b/mobile/ios/.swiftlint.yml new file mode 100644 index 0000000000..d14570aae3 --- /dev/null +++ b/mobile/ios/.swiftlint.yml @@ -0,0 +1,92 @@ +disabled_rules: # rule identifiers to exclude from running + - variable_name + - todo + - trailing_newline + - type_name + - function_body_length + - missing_docs + - valid_docs + - cyclomatic_complexity + - type_body_length + - function_parameter_count + - file_length + - mark + - unused_closure_parameter + - empty_parentheses_with_trailing_closure + - redundant_string_enum_value + - large_tuple + - class_delegate_protocol + - syntactic_sugar + - implicit_getter + - weak_delegate + - shorthand_operator + - trailing_comma + - unused_optional_binding + - private_over_fileprivate + - empty_enum_arguments + - discarded_notification_center_observer + - block_based_kvo + - nesting + - is_disjoint + - multiple_closures_with_trailing_closure + - fallthrough + - switch_case_alignment + - trailing_whitespace + - leading_whitespace + - operator_whitespace + - legacy_cggeometry_functions + - unneeded_break_in_switch + - closure_parameter_position +opt_in_rules: # some rules are only opt-in + - closing_brace + - opening_brace + - return_arrow_whitespace + - trailing_semicolon + - statement_position + - explicit_init + - shorthand_operator + - file_header + # Find all the available rules by running: + # swiftlint rules +included: # paths to include during linting. `--path` is ignored if present. +excluded: # paths to ignore during linting. Takes precedence over `included`. + - Carthage + - Pods + - Source/ExcludedFolder + - Source/ExcludedFile.swift + - ThirdParty + - FxA + - FxAClient + - build + - UITests/EarlGrey.swift + - Storage/ThirdParty/SwiftData.swift + - UITests/ + - XCUITests/ + - SyncTests/ + - StorageTests/ + - ReadingListTests/ + - ClientTests/ + - AccountTests/ + - fastlane/ + - SharedTests/ + - Client/Assets/Search/get_supported_locales.swift + +# configurable rules can be customized from this configuration file +# binary rules can set their severity level +trailing_semicolon: error +empty_count: error +closing_brace: error +opening_brace: error +return_arrow_whitespace: error +statement_position: error +colon: error +comma: error +force_try: warning +force_cast: warning + + +file_header: + required_string: "/* This Source Code Form is subject to the terms of the Mozilla Public\n * License, v. 2.0. If a copy of the MPL was not distributed with this\n * file, You can obtain one at http://mozilla.org/MPL/2.0/. */" +line_length: 1000 + +reporter: "json" # reporter type (xcode, json, csv, checkstyle) diff --git a/mobile/ios/AUTHORS b/mobile/ios/AUTHORS new file mode 100644 index 0000000000..99c62e8631 --- /dev/null +++ b/mobile/ios/AUTHORS @@ -0,0 +1,25 @@ +This is an (incomplete) list of people who have contributed to the +codebase which lives in this repository. If you make a contribution +here, you may add your name and, optionally, email address in the +appropriate place. + +For a full list of the people who are credited with making a +contribution to Mozilla, see http://www.mozilla.org/credits/. + +Boris Dušek +Brian Nicholson +Bryan Munar +Emily Toop +Farhan Patel +Jacob White +James Hugman +Le Van Nghia +Maurya Talisetti +Nick Alexander +Richard Newman +Sachin Palewar +Sahil Wasan +Stefan Arentz +Steph Leroux +Thomas Bonnin +Wes Johnston diff --git a/mobile/ios/Account/Account-Bridging-Header.h b/mobile/ios/Account/Account-Bridging-Header.h new file mode 100644 index 0000000000..4131450754 --- /dev/null +++ b/mobile/ios/Account/Account-Bridging-Header.h @@ -0,0 +1,7 @@ +#ifndef Client_Account_Bridging_Header_h +#define Client_Account_Bridging_Header_h + +#import +#import "Shared-Bridging-Header.h" + +#endif diff --git a/mobile/ios/Account/FirefoxAccount.swift b/mobile/ios/Account/FirefoxAccount.swift new file mode 100644 index 0000000000..e065501a90 --- /dev/null +++ b/mobile/ios/Account/FirefoxAccount.swift @@ -0,0 +1,425 @@ +/* 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 XCGLogger +import Deferred +import SwiftyJSON +import FxA +import SDWebImage + +private let log = Logger.syncLogger + +// The version of the account schema we persist. +let AccountSchemaVersion = 2 + +/// A FirefoxAccount mediates access to identity attached services. +/// +/// All data maintained as part of the account or its state should be +/// considered sensitive and stored appropriately. Usually, that means +/// storing account data in the iOS keychain. +/// +/// Non-sensitive but persistent data should be maintained outside of +/// the account itself. +open class FirefoxAccount { + /// The email address identifying the account. A Firefox Account is uniquely identified on a particular server + /// (auth endpoint) by its email address. + open let email: String + + /// The auth endpoint user identifier identifying the account. A Firefox Account is uniquely identified on a + /// particular server (auth endpoint) by its assigned uid. + open let uid: String + + open var fxaProfile: FxAProfile? + + open var deviceRegistration: FxADeviceRegistration? + + open var configuration: FirefoxAccountConfiguration + + open var pushRegistration: PushRegistration? + + fileprivate let stateCache: KeychainCache + open var syncAuthState: SyncAuthState! // We can't give a reference to self if this is a let. + + // To prevent advance() consumers racing, we maintain a shared advance() deferred (`advanceDeferred`). If an + // advance() is in progress, the shared deferred will be returned. (Multiple consumers can chain off a single + // deferred safely.) If no advance() is in progress, a new shared deferred will be scheduled and returned. To + // prevent data races against the shared deferred, advance() locks accesses to `advanceDeferred` using + // `advanceLock`. + fileprivate var advanceLock = OSSpinLock() + fileprivate var advanceDeferred: Deferred? + + open var actionNeeded: FxAActionNeeded { + return stateCache.value!.actionNeeded + } + + public convenience init(configuration: FirefoxAccountConfiguration, email: String, uid: String, deviceRegistration: FxADeviceRegistration?, stateKeyLabel: String, state: FxAState) { + self.init(configuration: configuration, email: email, uid: uid, deviceRegistration: deviceRegistration, stateCache: KeychainCache(branch: "account.state", label: stateKeyLabel, value: state)) + } + + public init(configuration: FirefoxAccountConfiguration, email: String, uid: String, deviceRegistration: FxADeviceRegistration?, stateCache: KeychainCache) { + self.email = email + self.uid = uid + self.deviceRegistration = deviceRegistration + self.configuration = configuration + self.stateCache = stateCache + self.stateCache.checkpoint() + self.fxaProfile = nil + self.syncAuthState = FirefoxAccountSyncAuthState(account: self, + cache: KeychainCache.fromBranch("account.syncAuthState", withLabel: self.stateCache.label, factory: syncAuthStateCachefromJSON)) + } + + open class func from(_ configuration: FirefoxAccountConfiguration, andJSON data: JSON) -> FirefoxAccount? { + guard let email = data["email"].string , + let uid = data["uid"].string, + let sessionToken = data["sessionToken"].string?.hexDecodedData, + let keyFetchToken = data["keyFetchToken"].string?.hexDecodedData, + let unwrapkB = data["unwrapBKey"].string?.hexDecodedData else { + return nil + } + + let verified = data["verified"].bool ?? false + return FirefoxAccount.from(configuration: configuration, + andParametersWithEmail: email, uid: uid, deviceRegistration: nil, verified: verified, + sessionToken: sessionToken, keyFetchToken: keyFetchToken, unwrapkB: unwrapkB) + } + + open class func from(_ configuration: FirefoxAccountConfiguration, + andLoginResponse response: FxALoginResponse, + unwrapkB: Data) -> FirefoxAccount { + return FirefoxAccount.from(configuration: configuration, + andParametersWithEmail: response.remoteEmail, uid: response.uid, deviceRegistration: nil, verified: response.verified, + sessionToken: response.sessionToken as Data, keyFetchToken: response.keyFetchToken as Data, unwrapkB: unwrapkB) + } + + fileprivate class func from(configuration: FirefoxAccountConfiguration, + andParametersWithEmail email: String, + uid: String, + deviceRegistration: FxADeviceRegistration?, + verified: Bool, + sessionToken: Data, + keyFetchToken: Data, + unwrapkB: Data) -> FirefoxAccount { + var state: FxAState! = nil + if !verified { + let now = Date.now() + state = EngagedBeforeVerifiedState(knownUnverifiedAt: now, + lastNotifiedUserAt: now, + sessionToken: sessionToken, + keyFetchToken: keyFetchToken, + unwrapkB: unwrapkB + ) + } else { + state = EngagedAfterVerifiedState( + sessionToken: sessionToken, + keyFetchToken: keyFetchToken, + unwrapkB: unwrapkB + ) + } + + let account = FirefoxAccount( + configuration: configuration, + email: email, + uid: uid, + deviceRegistration: deviceRegistration, + stateKeyLabel: Bytes.generateGUID(), + state: state + ) + return account + } + + open func dictionary() -> [String: Any] { + var dict: [String: Any] = [:] + dict["version"] = AccountSchemaVersion + dict["email"] = email + dict["uid"] = uid + dict["deviceRegistration"] = deviceRegistration + dict["pushRegistration"] = pushRegistration + dict["configurationLabel"] = configuration.label.rawValue + dict["stateKeyLabel"] = stateCache.label + return dict + } + + open class func fromDictionary(_ dictionary: [String: Any]) -> FirefoxAccount? { + if let version = dictionary["version"] as? Int { + // As of this writing, the current version, v2, is backward compatible with v1. The only + // field added is pushRegistration, which is ok to be nil. If it is nil, then the app + // will attempt registration when it starts up. + if version <= AccountSchemaVersion { + return FirefoxAccount.fromDictionaryV1(dictionary) + } + } + return nil + } + + fileprivate class func fromDictionaryV1(_ dictionary: [String: Any]) -> FirefoxAccount? { + var configurationLabel: FirefoxAccountConfigurationLabel? = nil + if let rawValue = dictionary["configurationLabel"] as? String { + configurationLabel = FirefoxAccountConfigurationLabel(rawValue: rawValue) + } + if let + configurationLabel = configurationLabel, + let email = dictionary["email"] as? String, + let uid = dictionary["uid"] as? String { + let deviceRegistration = dictionary["deviceRegistration"] as? FxADeviceRegistration + let stateCache = KeychainCache.fromBranch("account.state", withLabel: dictionary["stateKeyLabel"] as? String, withDefault: SeparatedState(), factory: state) + let account = FirefoxAccount( + configuration: configurationLabel.toConfiguration(), + email: email, uid: uid, + deviceRegistration: deviceRegistration, + stateCache: stateCache) + account.pushRegistration = dictionary["pushRegistration"] as? PushRegistration + return account + } + return nil + } + + public enum AccountError: MaybeErrorType { + case notMarried + + public var description: String { + switch self { + case .notMarried: return "Not married." + } + } + } + + public class NotATokenStateError: MaybeErrorType { + let state: FxAState? + init(state: FxAState?) { + self.state = state + } + public var description: String { + return "Not in a Token State: \(state?.label.rawValue ?? "Empty State")" + } + } + + public class FxAProfile { + open var displayName: String? + open let email: String + open let avatar: Avatar + + init(email: String, displayName: String?, avatar: String?) { + self.email = email + self.displayName = displayName + self.avatar = Avatar(url: avatar?.asURL) + } + + enum ImageDownloadState { + case notStarted + case started + case failedCanRetry + case failedCanNotRetry + case succeededMalformed + case succeeded + } + + open class Avatar { + open var image: UIImage? + open let url: URL? + var currentImageState: ImageDownloadState = .notStarted + + init(url: URL?) { + self.image = UIImage(named: "placeholder-avatar") + self.url = url + self.updateAvatarImageState() + } + + func updateAvatarImageState() { + switch currentImageState { + case .notStarted: + self.currentImageState = .started + self.downloadAvatar() + break + case .failedCanRetry: + self.downloadAvatar() + break + default: + break + } + } + + func downloadAvatar() { + SDWebImageManager.shared().loadImage(with: url, options: [.continueInBackground, .lowPriority], progress: nil) { (image, _, error, _, success, _) in + if let error = error { + if (error as NSError).code == 404 || self.currentImageState == .failedCanRetry { + // Image is not found or failed to download a second time + self.currentImageState = .failedCanNotRetry + } else { + // This could have been a transient error, attempt to download the image only once more + self.currentImageState = .failedCanRetry + self.updateAvatarImageState() + } + return + } + + if success == true && image == nil { + self.currentImageState = .succeededMalformed + return + } + + self.image = image + self.currentImageState = .succeeded + NotificationCenter.default.post(name: NotificationFirefoxAccountProfileChanged, object: self) + } + } + } + } + + // Fetch current user's FxA profile. It contains the most updated email, displayName and avatar. This + // emits two `NotificationFirefoxAccountProfileChanged`, once when the profile has been downloaded and + // another when the avatar image has been downloaded. + open func updateProfile() { + guard let session = stateCache.value as? TokenState else { + return + } + + let client = FxAClient10(authEndpoint: self.configuration.authEndpointURL, oauthEndpoint: self.configuration.oauthEndpointURL, profileEndpoint: self.configuration.profileEndpointURL) + client.getProfile(withSessionToken: session.sessionToken as NSData) >>== { result in + self.fxaProfile = FxAProfile(email: result.email, displayName: result.displayName, avatar: result.avatarURL) + NotificationCenter.default.post(name: NotificationFirefoxAccountProfileChanged, object: self) + } + } + + // Fetch the devices list from FxA then replace the current stored remote devices. + open func updateFxADevices(remoteDevices: RemoteDevices) -> Success { + guard let session = stateCache.value as? TokenState else { + return deferMaybe(NotATokenStateError(state: stateCache.value)) + } + let client = FxAClient10(authEndpoint: self.configuration.authEndpointURL) + return client.devices(withSessionToken: session.sessionToken as NSData) >>== { resp in + return remoteDevices.replaceRemoteDevices(resp.devices) + } + } + + public class NotifyError: MaybeErrorType { + public var description = "The server could not notify the clients." + } + + @discardableResult open func notify(deviceIDs: [GUID], collectionsChanged collections: [String], reason: String) -> Success { + guard let session = stateCache.value as? TokenState else { + return deferMaybe(NotATokenStateError(state: stateCache.value)) + } + let client = FxAClient10(authEndpoint: self.configuration.authEndpointURL) + return client.notify(deviceIDs: deviceIDs, collectionsChanged: collections, reason: reason, withSessionToken: session.sessionToken as NSData) >>== { resp in + guard resp.success else { + return deferMaybe(NotifyError()) + } + return succeed() + } + } + + @discardableResult open func notifyAll(collectionsChanged collections: [String], reason: String) -> Success { + guard let session = stateCache.value as? TokenState else { + return deferMaybe(NotATokenStateError(state: stateCache.value)) + } + guard let ownDeviceId = self.deviceRegistration?.id else { + return deferMaybe(FxAClientError.local(NSError())) + } + let client = FxAClient10(authEndpoint: self.configuration.authEndpointURL) + return client.notifyAll(ownDeviceId: ownDeviceId, collectionsChanged: collections, reason: reason, withSessionToken: session.sessionToken as NSData) >>== { resp in + guard resp.success else { + return deferMaybe(NotifyError()) + } + return succeed() + } + } + + @discardableResult open func destroyDevice() -> Success { + guard let session = stateCache.value as? TokenState else { + return deferMaybe(NotATokenStateError(state: stateCache.value)) + } + guard let ownDeviceId = self.deviceRegistration?.id else { + return deferMaybe(FxAClientError.local(NSError())) + } + let client = FxAClient10(authEndpoint: self.configuration.authEndpointURL) + + return client.destroyDevice(ownDeviceId: ownDeviceId, withSessionToken: session.sessionToken as NSData) >>> succeed + } + + @discardableResult open func advance() -> Deferred { + OSSpinLockLock(&advanceLock) + if let deferred = advanceDeferred { + // We already have an advance() in progress. This consumer can chain from it. + log.debug("advance already in progress; returning shared deferred.") + OSSpinLockUnlock(&advanceLock) + return deferred + } + + // Alright, we haven't an advance() in progress. Schedule a new deferred to chain from. + let cachedState = stateCache.value! + var registration = succeed() + if let session = cachedState as? TokenState { + registration = FxADeviceRegistrator.registerOrUpdateDevice(self, sessionToken: session.sessionToken as NSData).bind { result in + if result.successValue != FxADeviceRegistrationResult.alreadyRegistered { + NotificationCenter.default.post(name: NotificationFirefoxAccountDeviceRegistrationUpdated, object: nil) + } + return succeed() + } + } + + let deferred: Deferred = registration.bind { _ in + let client = FxAClient10(authEndpoint: self.configuration.authEndpointURL, oauthEndpoint: self.configuration.oauthEndpointURL, profileEndpoint: self.configuration.profileEndpointURL) + let stateMachine = FxALoginStateMachine(client: client) + let now = Date.now() + return stateMachine.advance(fromState: cachedState, now: now).map { newState in + self.stateCache.value = newState + return newState + } + } + + advanceDeferred = deferred + log.debug("no advance() in progress; setting and returning new shared deferred.") + OSSpinLockUnlock(&advanceLock) + + deferred.upon { _ in + // This advance() is complete. Clear the shared deferred. + OSSpinLockLock(&self.advanceLock) + if let existingDeferred = self.advanceDeferred, existingDeferred === deferred { + // The guard should not be needed, but should prevent trampling racing consumers. + self.advanceDeferred = nil + log.debug("advance() completed and shared deferred is existing deferred; clearing shared deferred.") + } else { + log.warning("advance() completed but shared deferred is not existing deferred; ignoring potential bug!") + } + OSSpinLockUnlock(&self.advanceLock) + } + return deferred + } + + open func marriedState() -> Deferred> { + return advance().map { newState in + if newState.label == FxAStateLabel.married { + if let married = newState as? MarriedState { + return Maybe(success: married) + } + } + return Maybe(failure: AccountError.notMarried) + } + } + + @discardableResult open func makeSeparated() -> Bool { + log.info("Making Account State be Separated.") + self.stateCache.value = SeparatedState() + return true + } + + @discardableResult open func makeDoghouse() -> Bool { + log.info("Making Account State be Doghouse.") + self.stateCache.value = DoghouseState() + return true + } + + open func makeCohabitingWithoutKeyPair() -> Bool { + if let married = self.stateCache.value as? MarriedState { + log.info("Making Account State be CohabitingWithoutKeyPair.") + self.stateCache.value = married.withoutKeyPair() + return true + } + + log.info("Cannot make Account State be CohabitingWithoutKeyPair from state with label \(self.stateCache.value?.label ??? "nil").") + return false + } +} diff --git a/mobile/ios/Account/FirefoxAccountConfiguration.swift b/mobile/ios/Account/FirefoxAccountConfiguration.swift new file mode 100644 index 0000000000..1daca26f6b --- /dev/null +++ b/mobile/ios/Account/FirefoxAccountConfiguration.swift @@ -0,0 +1,266 @@ +/* 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 + +public enum FirefoxAccountConfigurationLabel: String { + case latestDev = "LatestDev" + case stableDev = "StableDev" + case stage = "Stage" + case production = "Production" + case chinaEdition = "ChinaEdition" + case custom = "Custom" + + public func toConfiguration(prefs: Prefs? = nil) -> FirefoxAccountConfiguration { + switch self { + case .latestDev: return LatestDevFirefoxAccountConfiguration() + case .stableDev: return StableDevFirefoxAccountConfiguration() + case .stage: return StageFirefoxAccountConfiguration() + case .production: return ProductionFirefoxAccountConfiguration() + case .chinaEdition: return ChinaEditionFirefoxAccountConfiguration() + case .custom: return CustomFirefoxAccountConfiguration(prefs: prefs) + } + } +} + +/** + * In the URLs below, service=sync ensures that we always get the keys with signin messages, + * and context=fx_ios_v1 opts us in to the Desktop Sync postMessage interface. + */ +public protocol FirefoxAccountConfiguration { + var label: FirefoxAccountConfigurationLabel { get } + + /// A Firefox Account exists on a particular server. The auth endpoint should speak the protocol documented at + /// https://github.com/mozilla/fxa-auth-server/blob/02f88502700b0c5ef5a4768a8adf332f062ad9bf/docs/api.md + var authEndpointURL: URL { get } + + /// The associated oauth server should speak the protocol documented at + /// https://github.com/mozilla/fxa-oauth-server/blob/6cc91e285fc51045a365dbacb3617ef29093dbc3/docs/api.md + var oauthEndpointURL: URL { get } + + var profileEndpointURL: URL { get } + + /// The associated content server should speak the protocol implemented (but not yet documented) at + /// https://github.com/mozilla/fxa-content-server/blob/161bff2d2b50bac86ec46c507e597441c8575189/app/scripts/models/auth_brokers/fx-desktop.js + var signInURL: URL { get } + var settingsURL: URL { get } + var forceAuthURL: URL { get } + + var sync15Configuration: Sync15Configuration { get } + + var pushConfiguration: PushConfiguration { get } +} + +public struct LatestDevFirefoxAccountConfiguration: FirefoxAccountConfiguration { + public init() { + } + + public let label = FirefoxAccountConfigurationLabel.latestDev + + public let authEndpointURL = URL(string: "https://latest.dev.lcip.org/auth/v1")! + public let oauthEndpointURL = URL(string: "https://oauth-latest.dev.lcip.org")! + public let profileEndpointURL = URL(string: "https://latest.dev.lcip.org/profile")! + + public let signInURL = URL(string: "https://latest.dev.lcip.org/signin?service=sync&context=fx_ios_v1")! + public let settingsURL = URL(string: "https://latest.dev.lcip.org/settings?context=fx_ios_v1")! + public let forceAuthURL = URL(string: "https://latest.dev.lcip.org/force_auth?service=sync&context=fx_ios_v1")! + + public let sync15Configuration: Sync15Configuration = StageSync15Configuration() + + public let pushConfiguration: PushConfiguration = FennecPushConfiguration() +} + +public struct StableDevFirefoxAccountConfiguration: FirefoxAccountConfiguration { + public init() { + } + + public let label = FirefoxAccountConfigurationLabel.stableDev + + public let authEndpointURL = URL(string: "https://stable.dev.lcip.org/auth/v1")! + public let oauthEndpointURL = URL(string: "https://oauth-stable.dev.lcip.org")! + public let profileEndpointURL = URL(string: "https://stable.dev.lcip.org/profile")! + + public let signInURL = URL(string: "https://stable.dev.lcip.org/signin?service=sync&context=fx_ios_v1")! + public let settingsURL = URL(string: "https://stable.dev.lcip.org/settings?context=fx_ios_v1")! + public let forceAuthURL = URL(string: "https://stable.dev.lcip.org/force_auth?service=sync&context=fx_ios_v1")! + + public let sync15Configuration: Sync15Configuration = StageSync15Configuration() + + public let pushConfiguration: PushConfiguration = FennecPushConfiguration() +} + +public struct StageFirefoxAccountConfiguration: FirefoxAccountConfiguration { + public init() { + } + + public let label = FirefoxAccountConfigurationLabel.stage + + public let authEndpointURL = URL(string: "https://api-accounts.stage.mozaws.net/v1")! + public let oauthEndpointURL = URL(string: "https://oauth.stage.mozaws.net/v1")! + public let profileEndpointURL = URL(string: "https://profile.stage.mozaws.net/v1")! + + public let signInURL = URL(string: "https://accounts.stage.mozaws.net/signin?service=sync&context=fx_ios_v1")! + public let settingsURL = URL(string: "https://accounts.stage.mozaws.net/settings?context=fx_ios_v1")! + public let forceAuthURL = URL(string: "https://accounts.stage.mozaws.net/force_auth?service=sync&context=fx_ios_v1")! + + public let sync15Configuration: Sync15Configuration = StageSync15Configuration() + + public var pushConfiguration: PushConfiguration { + get { + #if MOZ_CHANNEL_RELEASE + return FirefoxStagingPushConfiguration() + #elseif MOZ_CHANNEL_BETA + return FirefoxBetaStagingPushConfiguration() + #elseif MOZ_CHANNEL_FENNEC + return FennecStagingPushConfiguration() + #endif + } + } +} + +public struct ProductionFirefoxAccountConfiguration: FirefoxAccountConfiguration { + public init() { + } + + public let label = FirefoxAccountConfigurationLabel.production + + public let authEndpointURL = URL(string: "https://api.accounts.firefox.com/v1")! + public let oauthEndpointURL = URL(string: "https://oauth.accounts.firefox.com/v1")! + public let profileEndpointURL = URL(string: "https://profile.accounts.firefox.com/v1")! + + public let signInURL = URL(string: "https://accounts.firefox.com/signin?service=sync&context=fx_ios_v1")! + public let settingsURL = URL(string: "https://accounts.firefox.com/settings?context=fx_ios_v1")! + public let forceAuthURL = URL(string: "https://accounts.firefox.com/force_auth?service=sync&context=fx_ios_v1")! + + public let sync15Configuration: Sync15Configuration = ProductionSync15Configuration() + + public let pushConfiguration: PushConfiguration = FirefoxPushConfiguration() +} + +public struct CustomFirefoxAccountConfiguration: FirefoxAccountConfiguration { + public init(prefs: Prefs? = nil) { + self.prefs = prefs + } + + public var prefs: Prefs? + + public let label = FirefoxAccountConfigurationLabel.custom + + public var authEndpointURL: URL { + get { + if let authServer = self.prefs?.stringForKey(PrefsKeys.KeyCustomSyncAuth), let url = URL(string: authServer + "/v1") { + return url + } + + // If somehow an invalid url was stored, fallback to the production URL + return ProductionFirefoxAccountConfiguration().authEndpointURL + } + } + + public var oauthEndpointURL: URL { + get { + if let oauthServer = self.prefs?.stringForKey(PrefsKeys.KeyCustomSyncOauth), let url = URL(string: oauthServer + "/v1") { + return url + } + return ProductionFirefoxAccountConfiguration().oauthEndpointURL + } + } + + public var profileEndpointURL: URL { + get { + if let profileServer = self.prefs?.stringForKey(PrefsKeys.KeyCustomSyncProfile), let url = URL(string: profileServer + "/v1") { + return url + } + return ProductionFirefoxAccountConfiguration().profileEndpointURL + } + } + + public var signInURL: URL { + get { + if let signIn = self.prefs?.stringForKey(PrefsKeys.KeyCustomSyncWeb), let url = URL(string: signIn + "/signin?service=sync&context=fx_ios_v1") { + return url + } + return ProductionFirefoxAccountConfiguration().signInURL + } + } + + public var forceAuthURL: URL { + get { + if let forceAuth = self.prefs?.stringForKey(PrefsKeys.KeyCustomSyncWeb), let url = URL(string: forceAuth + "/force_auth?service=sync&context=fx_ios_v1") { + return url + } + return ProductionFirefoxAccountConfiguration().forceAuthURL + } + } + + public var settingsURL: URL { + get { + if let settings = self.prefs?.stringForKey(PrefsKeys.KeyCustomSyncWeb), let url = URL(string: settings + "/settings?service=sync&context=fx_ios_v1") { + return url + } + return ProductionFirefoxAccountConfiguration().settingsURL + } + } + + public var sync15Configuration: Sync15Configuration { + get { + return CustomSync15Configuration(prefs: self.prefs) + } + } + + public let pushConfiguration: PushConfiguration = FirefoxPushConfiguration() +} + +public struct ChinaEditionFirefoxAccountConfiguration: FirefoxAccountConfiguration { + public init() { + } + + public let label = FirefoxAccountConfigurationLabel.chinaEdition + + public let authEndpointURL = URL(string: "https://api-accounts.firefox.com.cn/v1")! + public let oauthEndpointURL = URL(string: "https://oauth.firefox.com.cn/v1")! + public let profileEndpointURL = URL(string: "https://profile.firefox.com.cn/v1")! + + public let signInURL = URL(string: "https://accounts.firefox.com.cn/signin?service=sync&context=fx_ios_v1")! + public let settingsURL = URL(string: "https://accounts.firefox.com.cn/settings?context=fx_ios_v1")! + public let forceAuthURL = URL(string: "https://accounts.firefox.com.cn/force_auth?service=sync&context=fx_ios_v1")! + + public let sync15Configuration: Sync15Configuration = ChinaEditionSync15Configuration() + + public let pushConfiguration: PushConfiguration = FirefoxPushConfiguration() +} + +public protocol Sync15Configuration { + var tokenServerEndpointURL: URL { get } +} + +public struct ChinaEditionSync15Configuration: Sync15Configuration { + public let tokenServerEndpointURL = URL(string: "https://sync.firefox.com.cn/token/1.0/sync/1.5")! +} + +public struct ProductionSync15Configuration: Sync15Configuration { + public let tokenServerEndpointURL = URL(string: "https://token.services.mozilla.com/1.0/sync/1.5")! +} + +public struct StageSync15Configuration: Sync15Configuration { + public let tokenServerEndpointURL = URL(string: "https://token.stage.mozaws.net/1.0/sync/1.5")! +} + +public struct CustomSync15Configuration: Sync15Configuration { + public init(prefs: Prefs? = nil) { + self.prefs = prefs + } + + public var prefs: Prefs? + + public var tokenServerEndpointURL: URL { + get { + if let tokenServer = self.prefs?.stringForKey(PrefsKeys.KeyCustomSyncToken), let url = URL(string: tokenServer + "/1.0/sync/1.5") { + return url + } + return ProductionSync15Configuration().tokenServerEndpointURL + } + } +} diff --git a/mobile/ios/Account/FxAClient10.swift b/mobile/ios/Account/FxAClient10.swift new file mode 100644 index 0000000000..08e5edf5b9 --- /dev/null +++ b/mobile/ios/Account/FxAClient10.swift @@ -0,0 +1,562 @@ +/* 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 Alamofire +import Shared +import Foundation +import FxA +import Deferred +import SwiftyJSON + +public let FxAClientErrorDomain = "org.mozilla.fxa.error" +public let FxAClientUnknownError = NSError(domain: FxAClientErrorDomain, code: 999, + userInfo: [NSLocalizedDescriptionKey: "Invalid server response"]) + +let KeyLength: Int = 32 + +public struct FxALoginResponse { + public let remoteEmail: String + public let uid: String + public let verified: Bool + public let sessionToken: Data + public let keyFetchToken: Data +} + +public struct FxAccountRemoteError { + static let AttemptToOperateOnAnUnverifiedAccount: Int32 = 104 + static let InvalidAuthenticationToken: Int32 = 110 + static let EndpointIsNoLongerSupported: Int32 = 116 + static let IncorrectLoginMethodForThisAccount: Int32 = 117 + static let IncorrectKeyRetrievalMethodForThisAccount: Int32 = 118 + static let IncorrectAPIVersionForThisAccount: Int32 = 119 + static let UnknownDevice: Int32 = 123 + static let DeviceSessionConflict: Int32 = 124 + static let UnknownError: Int32 = 999 +} + +public struct FxAKeysResponse { + let kA: Data + let wrapkB: Data +} + +public struct FxASignResponse { + let certificate: String +} + +public struct FxAStatusResponse { + let exists: Bool +} + +public struct FxADevicesResponse { + let devices: [FxADevice] +} + +public struct FxANotifyResponse { + let success: Bool +} + +public struct FxAOAuthResponse { + let accessToken: String +} + +public struct FxAProfileResponse { + let email: String + let uid: String + let avatarURL: String? + let displayName: String? +} + +public struct FxADeviceDestroyResponse { + let success: Bool +} + +// fxa-auth-server produces error details like: +// { +// "code": 400, // matches the HTTP status code +// "errno": 107, // stable application-level error number +// "error": "Bad Request", // string description of the error type +// "message": "the value of salt is not allowed to be undefined", +// "info": "https://docs.dev.lcip.og/errors/1234" // link to more info on the error +// } + +public enum FxAClientError { + case remote(RemoteError) + case local(NSError) +} + +// Be aware that string interpolation doesn't work: rdar://17318018, much good that it will do. +extension FxAClientError: MaybeErrorType { + public var description: String { + switch self { + case let .remote(error): + let errorString = error.error ?? NSLocalizedString("Missing error", comment: "Error for a missing remote error number") + let messageString = error.message ?? NSLocalizedString("Missing message", comment: "Error for a missing remote error message") + return "" + case let .local(error): + return "" + } + } +} + +public struct RemoteError { + let code: Int32 + let errno: Int32 + let error: String? + let message: String? + let info: String? + + var isUpgradeRequired: Bool { + return errno == FxAccountRemoteError.EndpointIsNoLongerSupported + || errno == FxAccountRemoteError.IncorrectLoginMethodForThisAccount + || errno == FxAccountRemoteError.IncorrectKeyRetrievalMethodForThisAccount + || errno == FxAccountRemoteError.IncorrectAPIVersionForThisAccount + } + + var isInvalidAuthentication: Bool { + return code == 401 + } + + var isUnverified: Bool { + return errno == FxAccountRemoteError.AttemptToOperateOnAnUnverifiedAccount + } +} + +open class FxAClient10 { + let authURL: URL + let oauthURL: URL + let profileURL: URL + + public init(authEndpoint: URL? = nil, oauthEndpoint: URL? = nil, profileEndpoint: URL? = nil) { + self.authURL = authEndpoint ?? ProductionFirefoxAccountConfiguration().authEndpointURL as URL + self.oauthURL = oauthEndpoint ?? ProductionFirefoxAccountConfiguration().oauthEndpointURL as URL + self.profileURL = profileEndpoint ?? ProductionFirefoxAccountConfiguration().profileEndpointURL as URL + } + + open class func KW(_ kw: String) -> Data { + return ("identity.mozilla.com/picl/v1/" + kw).utf8EncodedData + } + + /** + * The token server accepts an X-Client-State header, which is the + * lowercase-hex-encoded first 16 bytes of the SHA-256 hash of the + * bytes of kB. + */ + open class func computeClientState(_ kB: Data) -> String? { + if kB.count != 32 { + return nil + } + return kB.sha256.subdata(in: 0..<16).hexEncodedString + } + + open class func quickStretchPW(_ email: Data, password: Data) -> Data { + var salt = KW("quickStretch") + salt.append(":".utf8EncodedData) + salt.append(email) + return (password as NSData).derivePBKDF2HMACSHA256Key(withSalt: salt as Data!, iterations: 1000, length: 32) + } + + open class func computeUnwrapKey(_ stretchedPW: Data) -> Data { + let salt: Data = Data() + let contextInfo: Data = KW("unwrapBkey") + let bytes = (stretchedPW as NSData).deriveHKDFSHA256Key(withSalt: salt, contextInfo: contextInfo, length: UInt(KeyLength)) + return bytes! + } + + fileprivate class func remoteError(fromJSON json: JSON, statusCode: Int) -> RemoteError? { + if json.error != nil || 200 <= statusCode && statusCode <= 299 { + return nil + } + + if let code = json["code"].int32 { + if let errno = json["errno"].int32 { + return RemoteError(code: code, errno: errno, + error: json["error"].string, + message: json["message"].string, + info: json["info"].string) + } + } + return nil + } + + fileprivate class func loginResponse(fromJSON json: JSON) -> FxALoginResponse? { + guard json.error == nil, + let uid = json["uid"].string, + let verified = json["verified"].bool, + let sessionToken = json["sessionToken"].string, + let keyFetchToken = json["keyFetchToken"].string else { + return nil + } + + return FxALoginResponse(remoteEmail: "", uid: uid, verified: verified, + sessionToken: sessionToken.hexDecodedData, keyFetchToken: keyFetchToken.hexDecodedData) + } + + fileprivate class func keysResponse(fromJSON keyRequestKey: Data, json: JSON) -> FxAKeysResponse? { + guard json.error == nil, + let bundle = json["bundle"].string else { + return nil + } + + let data = bundle.hexDecodedData + guard data.count == 3 * KeyLength else { + return nil + } + + let ciphertext = data.subdata(in: 0..<(2 * KeyLength)) + let MAC = data.subdata(in: (2 * KeyLength)..<(3 * KeyLength)) + + let salt: Data = Data() + let contextInfo: Data = KW("account/keys") + let bytes = (keyRequestKey as NSData).deriveHKDFSHA256Key(withSalt: salt, contextInfo: contextInfo, length: UInt(3 * KeyLength)) + let respHMACKey = bytes?.subdata(in: 0.. FxASignResponse? { + guard json.error == nil, + let cert = json["cert"].string else { + return nil + } + + return FxASignResponse(certificate: cert) + } + + fileprivate class func statusResponse(fromJSON json: JSON) -> FxAStatusResponse? { + guard json.error == nil, + let exists = json["exists"].bool else { + return nil + } + + return FxAStatusResponse(exists: exists) + } + + fileprivate class func devicesResponse(fromJSON json: JSON) -> FxADevicesResponse? { + guard json.error == nil, + let jsonDevices = json.array else { + return nil + } + + let devices = jsonDevices.flatMap { (jsonDevice) -> FxADevice? in + return FxADevice.fromJSON(jsonDevice) + } + + return FxADevicesResponse(devices: devices) + } + + fileprivate class func notifyResponse(fromJSON json: JSON) -> FxANotifyResponse { + return FxANotifyResponse(success: json.error == nil) + } + + fileprivate class func deviceDestroyResponse(fromJSON json: JSON) -> FxADeviceDestroyResponse { + return FxADeviceDestroyResponse(success: json.error == nil) + } + + fileprivate class func oauthResponse(fromJSON json: JSON) -> FxAOAuthResponse? { + guard json.error == nil, + let accessToken = json["access_token"].string else { + return nil + } + + return FxAOAuthResponse(accessToken: accessToken) + } + + fileprivate class func profileResponse(fromJSON json: JSON) -> FxAProfileResponse? { + guard json.error == nil, + let uid = json["uid"].string, + let email = json["email"].string else { + return nil + } + + let avatarURL = json["avatar"].string + let displayName = json["displayName"].string + + return FxAProfileResponse(email: email, uid: uid, avatarURL: avatarURL, displayName: displayName) + } + + lazy fileprivate var alamofire: SessionManager = { + let ua = UserAgent.fxaUserAgent + let configuration = URLSessionConfiguration.ephemeral + var defaultHeaders = SessionManager.default.session.configuration.httpAdditionalHeaders ?? [:] + defaultHeaders["User-Agent"] = ua + configuration.httpAdditionalHeaders = defaultHeaders + return SessionManager(configuration: configuration) + }() + + open func login(_ emailUTF8: Data, quickStretchedPW: Data, getKeys: Bool) -> Deferred> { + let authPW = (quickStretchedPW as NSData).deriveHKDFSHA256Key(withSalt: Data(), contextInfo: FxAClient10.KW("authPW"), length: 32) as NSData + + let parameters = [ + "email": NSString(data: emailUTF8, encoding: String.Encoding.utf8.rawValue)!, + "authPW": authPW.base16EncodedString(options: NSDataBase16EncodingOptions.lowerCase) as NSString, + ] + + var URL: URL = self.authURL.appendingPathComponent("/account/login") + if getKeys { + var components = URLComponents(url: URL, resolvingAgainstBaseURL: false)! + components.query = "keys=true" + URL = components.url! + } + var mutableURLRequest = URLRequest(url: URL) + mutableURLRequest.httpMethod = HTTPMethod.post.rawValue + + mutableURLRequest.setValue("application/json", forHTTPHeaderField: "Content-Type") + mutableURLRequest.httpBody = JSON(parameters).stringValue()?.utf8EncodedData + + return makeRequest(mutableURLRequest, responseHandler: FxAClient10.loginResponse) + } + + open func status(forUID uid: String) -> Deferred> { + let statusURL = self.authURL.appendingPathComponent("/account/status").withQueryParam("uid", value: uid) + var mutableURLRequest = URLRequest(url: statusURL) + mutableURLRequest.httpMethod = HTTPMethod.get.rawValue + + mutableURLRequest.setValue("application/json", forHTTPHeaderField: "Content-Type") + + return makeRequest(mutableURLRequest, responseHandler: FxAClient10.statusResponse) + } + + open func devices(withSessionToken sessionToken: NSData) -> Deferred> { + let URL = self.authURL.appendingPathComponent("/account/devices") + var mutableURLRequest = URLRequest(url: URL) + mutableURLRequest.httpMethod = HTTPMethod.get.rawValue + + mutableURLRequest.setValue("application/json", forHTTPHeaderField: "Content-Type") + + let salt: Data = Data() + let contextInfo: Data = FxAClient10.KW("sessionToken") + let key = sessionToken.deriveHKDFSHA256Key(withSalt: salt, contextInfo: contextInfo, length: UInt(2 * KeyLength))! + mutableURLRequest.addAuthorizationHeader(forHKDFSHA256Key: key) + + return makeRequest(mutableURLRequest, responseHandler: FxAClient10.devicesResponse) + } + + open func notify(deviceIDs: [GUID], collectionsChanged collections: [String], reason: String, withSessionToken sessionToken: NSData) -> Deferred> { + let httpBody = JSON([ + "to": deviceIDs, + "payload": [ + "version": 1, + "command": "sync:collection_changed", + "data": [ + "collections": collections, + "reason": reason + ] + ] + ]) + return self.notify(httpBody: httpBody, withSessionToken: sessionToken) + } + + open func notifyAll(ownDeviceId: GUID, collectionsChanged collections: [String], reason: String, withSessionToken sessionToken: NSData) -> Deferred> { + let httpBody = JSON([ + "to": "all", + "excluded": [ownDeviceId], + "payload": [ + "version": 1, + "command": "sync:collection_changed", + "data": [ + "collections": collections, + "reason": reason + ] + ] + ]) + return self.notify(httpBody: httpBody, withSessionToken: sessionToken) + } + + fileprivate func notify(httpBody: JSON, withSessionToken sessionToken: NSData) -> Deferred> { + let URL = self.authURL.appendingPathComponent("/account/devices/notify") + var mutableURLRequest = URLRequest(url: URL) + mutableURLRequest.httpMethod = HTTPMethod.post.rawValue + mutableURLRequest.setValue("application/json", forHTTPHeaderField: "Content-Type") + mutableURLRequest.httpBody = httpBody.stringValue()?.utf8EncodedData + + let salt: Data = Data() + let contextInfo: Data = FxAClient10.KW("sessionToken") + let key = sessionToken.deriveHKDFSHA256Key(withSalt: salt, contextInfo: contextInfo, length: UInt(2 * KeyLength))! + mutableURLRequest.addAuthorizationHeader(forHKDFSHA256Key: key) + + return makeRequest(mutableURLRequest, responseHandler: FxAClient10.notifyResponse) + } + + open func destroyDevice(ownDeviceId: GUID, withSessionToken sessionToken: NSData) -> Deferred> { + let URL = self.authURL.appendingPathComponent("/account/device/destroy") + var mutableURLRequest = URLRequest(url: URL) + let httpBody: JSON = JSON(["id": ownDeviceId]) + mutableURLRequest.httpMethod = HTTPMethod.post.rawValue + mutableURLRequest.setValue("application/json", forHTTPHeaderField: "Content-Type") + mutableURLRequest.httpBody = httpBody.stringValue()?.utf8EncodedData + + let salt: Data = Data() + let contextInfo: Data = FxAClient10.KW("sessionToken") + let key = sessionToken.deriveHKDFSHA256Key(withSalt: salt, contextInfo: contextInfo, length: UInt(2 * KeyLength))! + mutableURLRequest.addAuthorizationHeader(forHKDFSHA256Key: key) + + return makeRequest(mutableURLRequest, responseHandler: FxAClient10.deviceDestroyResponse) + } + + open func registerOrUpdate(device: FxADevice, withSessionToken sessionToken: NSData) -> Deferred> { + let URL = self.authURL.appendingPathComponent("/account/device") + var mutableURLRequest = URLRequest(url: URL) + mutableURLRequest.httpMethod = HTTPMethod.post.rawValue + + mutableURLRequest.setValue("application/json", forHTTPHeaderField: "Content-Type") + mutableURLRequest.httpBody = device.toJSON().stringValue()?.utf8EncodedData + + let salt: Data = Data() + let contextInfo: Data = FxAClient10.KW("sessionToken") + let key = sessionToken.deriveHKDFSHA256Key(withSalt: salt, contextInfo: contextInfo, length: UInt(2 * KeyLength))! + mutableURLRequest.addAuthorizationHeader(forHKDFSHA256Key: key) + + return makeRequest(mutableURLRequest, responseHandler: FxADevice.fromJSON) + } + + open func oauthAuthorize(withSessionToken sessionToken: NSData, keyPair: RSAKeyPair, certificate: String) -> Deferred> { + let audience = self.getAudience(forURL: self.oauthURL) + + let assertion = JSONWebTokenUtils.createAssertionWithPrivateKeyToSign(with: keyPair.privateKey, + certificate: certificate, + audience: audience) + + let oauthAuthorizationURL = self.oauthURL.appendingPathComponent("/authorization") + var mutableURLRequest = URLRequest(url: oauthAuthorizationURL) + mutableURLRequest.httpMethod = HTTPMethod.post.rawValue + mutableURLRequest.setValue("application/json", forHTTPHeaderField: "Content-Type") + + let parameters = [ + "assertion": assertion, + "client_id": AppConstants.FxAiOSClientId, + "response_type": "token", + "scope": "profile", + "ttl": "300" + ] + + let salt: Data = Data() + let contextInfo: Data = FxAClient10.KW("sessionToken") + let key = sessionToken.deriveHKDFSHA256Key(withSalt: salt, contextInfo: contextInfo, length: UInt(2 * KeyLength))! + + guard let httpBody = JSON(parameters as NSDictionary).stringValue()?.utf8EncodedData else { + return deferMaybe(FxAClientError.local(FxAClientUnknownError)) + } + + mutableURLRequest.httpBody = httpBody + mutableURLRequest.addAuthorizationHeader(forHKDFSHA256Key: key) + + return makeRequest(mutableURLRequest, responseHandler: FxAClient10.oauthResponse) + } + + open func getProfile(withSessionToken sessionToken: NSData) -> Deferred> { + let keyPair = RSAKeyPair.generate(withModulusSize: 1024)! + return self.sign(sessionToken as Data, publicKey: keyPair.publicKey) >>== { signResult in + return self.oauthAuthorize(withSessionToken: sessionToken, keyPair: keyPair, certificate: signResult.certificate) >>== { oauthResult in + + let profileURL = self.profileURL.appendingPathComponent("/profile") + var mutableURLRequest = URLRequest(url: profileURL) + mutableURLRequest.httpMethod = HTTPMethod.get.rawValue + + mutableURLRequest.setValue("application/json", forHTTPHeaderField: "Content-Type") + mutableURLRequest.setValue("Bearer " + oauthResult.accessToken, forHTTPHeaderField: "Authorization") + + return self.makeRequest(mutableURLRequest, responseHandler: FxAClient10.profileResponse) + } + } + } + + open func getAudience(forURL URL: URL) -> String { + if let port = URL.port { + return "\(URL.scheme!)://\(URL.host!):\(port)" + } else { + return "\(URL.scheme!)://\(URL.host!)" + } + } + + fileprivate func makeRequest(_ request: URLRequest, responseHandler: @escaping (JSON) -> T?) -> Deferred> { + let deferred = Deferred>() + + alamofire.request(request) + .validate(contentType: ["application/json"]) + .responseJSON { response in + withExtendedLifetime(self.alamofire) { + if let error = response.result.error { + deferred.fill(Maybe(failure: FxAClientError.local(error as NSError))) + return + } + + if let data = response.result.value { + let json = JSON(data) + if let remoteError = FxAClient10.remoteError(fromJSON: json, statusCode: response.response!.statusCode) { + deferred.fill(Maybe(failure: FxAClientError.remote(remoteError))) + return + } + + if let response = responseHandler(json) { + deferred.fill(Maybe(success: response)) + return + } + } + + deferred.fill(Maybe(failure: FxAClientError.local(FxAClientUnknownError))) + } + } + + return deferred + } +} + +extension FxAClient10: FxALoginClient { + + func keyPair() -> Deferred> { + let result = RSAKeyPair.generate(withModulusSize: 2048)! // TODO: debate key size and extract this constant. + return Deferred(value: Maybe(success: result)) + } + + open func keys(_ keyFetchToken: Data) -> Deferred> { + let URL = self.authURL.appendingPathComponent("/account/keys") + var mutableURLRequest = URLRequest(url: URL) + mutableURLRequest.httpMethod = HTTPMethod.get.rawValue + + let salt: Data = Data() + let contextInfo: Data = FxAClient10.KW("keyFetchToken") + let key = (keyFetchToken as NSData).deriveHKDFSHA256Key(withSalt: salt, contextInfo: contextInfo, length: UInt(3 * KeyLength))! + mutableURLRequest.addAuthorizationHeader(forHKDFSHA256Key: key) + + let rangeStart = 2 * KeyLength + let keyRequestKey = key.subdata(in: rangeStart..<(rangeStart + KeyLength)) + + return makeRequest(mutableURLRequest) { FxAClient10.keysResponse(fromJSON: keyRequestKey, json: $0) } + } + + open func sign(_ sessionToken: Data, publicKey: PublicKey) -> Deferred> { + let parameters = [ + "publicKey": publicKey.jsonRepresentation() as NSDictionary, + "duration": NSNumber(value: OneDayInMilliseconds), // The maximum the server will allow. + ] + + let url = self.authURL.appendingPathComponent("/certificate/sign") + var mutableURLRequest = URLRequest(url: url) + mutableURLRequest.httpMethod = HTTPMethod.post.rawValue + + mutableURLRequest.setValue("application/json", forHTTPHeaderField: "Content-Type") + mutableURLRequest.httpBody = JSON(parameters as NSDictionary).stringValue()?.utf8EncodedData + + let salt: Data = Data() + let contextInfo: Data = FxAClient10.KW("sessionToken") + let key = (sessionToken as NSData).deriveHKDFSHA256Key(withSalt: salt, contextInfo: contextInfo, length: UInt(2 * KeyLength))! + mutableURLRequest.addAuthorizationHeader(forHKDFSHA256Key: key) + + return makeRequest(mutableURLRequest, responseHandler: FxAClient10.signResponse) + } +} diff --git a/mobile/ios/Account/FxADevice.swift b/mobile/ios/Account/FxADevice.swift new file mode 100644 index 0000000000..40ec27170c --- /dev/null +++ b/mobile/ios/Account/FxADevice.swift @@ -0,0 +1,66 @@ +/* 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 SwiftyJSON + +public struct FxADevicePushParams { + let callback: String + let publicKey: String + let authKey: String +} + +public class FxADevice: RemoteDevice { + let pushParams: FxADevicePushParams? + + fileprivate init(name: String, id: String?, type: String?, isCurrentDevice: Bool = false, push: FxADevicePushParams?, lastAccessTime: Timestamp?) { + self.pushParams = push + super.init(id: id, name: name, type: type, isCurrentDevice: isCurrentDevice, lastAccessTime: lastAccessTime) + } + + static func forRegister(_ name: String, type: String, push: FxADevicePushParams?) -> FxADevice { + return FxADevice(name: name, id: nil, type: type, push: push, lastAccessTime: nil) + } + + static func forUpdate(_ name: String, id: String, push: FxADevicePushParams?) -> FxADevice { + return FxADevice(name: name, id: id, type: nil, push: push, lastAccessTime: nil) + } + + func toJSON() -> JSON { + var parameters = [String: String]() + parameters["name"] = name + parameters["id"] = id + parameters["type"] = type + if let push = self.pushParams { + parameters["pushCallback"] = push.callback + parameters["pushPublicKey"] = push.publicKey + parameters["pushAuthKey"] = push.authKey + } + return JSON(parameters as NSDictionary) + } + + static func fromJSON(_ json: JSON) -> FxADevice? { + guard json.error == nil, + let id = json["id"].string, + let name = json["name"].string else { + return nil + } + + let isCurrentDevice = json["isCurrentDevice"].bool ?? false + let lastAccessTime = json["lastAccessTime"].uInt64 + let type = json["type"].string + + let push: FxADevicePushParams? + if let pushCallback = json["pushCallback"].stringValue(), + let publicKey = json["pushPublicKey"].stringValue(), publicKey != "", + let authKey = json["pushAuthKey"].stringValue(), authKey != "" { + push = FxADevicePushParams(callback: pushCallback, publicKey: publicKey, authKey: authKey) + } else { + push = nil + } + + return FxADevice(name: name, id: id, type: type, isCurrentDevice: isCurrentDevice, push: push, lastAccessTime: lastAccessTime) + } +} diff --git a/mobile/ios/Account/FxADeviceRegistration.swift b/mobile/ios/Account/FxADeviceRegistration.swift new file mode 100644 index 0000000000..ac809cf70a --- /dev/null +++ b/mobile/ios/Account/FxADeviceRegistration.swift @@ -0,0 +1,184 @@ +/* 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 Deferred +import Shared +import SwiftyJSON + +private let log = Logger.syncLogger + +/// The current version of the device registration. We use this to re-register +/// devices after we update what we send on device registration. +private let DeviceRegistrationVersion = 2 + +public enum FxADeviceRegistrationResult { + case registered + case updated + case alreadyRegistered +} + +public enum FxADeviceRegistratorError: MaybeErrorType { + case accountDeleted + case currentDeviceNotFound + case invalidSession + case unknownDevice + + public var description: String { + switch self { + case .accountDeleted: return "Account no longer exists." + case .currentDeviceNotFound: return "Current device not found." + case .invalidSession: return "Session token was invalid." + case .unknownDevice: return "Unknown device." + } + } +} + +open class FxADeviceRegistration: NSObject, NSCoding { + /// The device identifier identifying this device. A device is uniquely identified + /// across the lifetime of a Firefox Account. + public let id: String + + /// The version of the device registration. We use this to re-register + /// devices after we update what we send on device registration. + let version: Int + + /// The last time we successfully (re-)registered with the server. + let lastRegistered: Timestamp + + init(id: String, version: Int, lastRegistered: Timestamp) { + self.id = id + self.version = version + self.lastRegistered = lastRegistered + } + + public convenience required init(coder: NSCoder) { + let id = coder.decodeObject(forKey: "id") as! String + let version = coder.decodeAsInt(forKey: "version") + let lastRegistered = coder.decodeAsUInt64(forKey: "lastRegistered") + self.init(id: id, version: version, lastRegistered: lastRegistered) + } + + open func encode(with aCoder: NSCoder) { + aCoder.encode(id, forKey: "id") + aCoder.encode(version, forKey: "version") + aCoder.encode(NSNumber(value: lastRegistered), forKey: "lastRegistered") + } + + open func toJSON() -> JSON { + return JSON(object: [ + "id": id, + "version": version, + "lastRegistered": lastRegistered, + ]) + } +} + +open class FxADeviceRegistrator { + open static func registerOrUpdateDevice(_ account: FirefoxAccount, sessionToken: NSData, client: FxAClient10? = nil) -> Deferred> { + // If we've already registered, the registration version is up-to-date, *and* we've (re-)registered + // within the last week, do nothing. We re-register weekly as a sanity check. + if let registration = account.deviceRegistration, registration.version == DeviceRegistrationVersion && + Date.now() < registration.lastRegistered + OneWeekInMilliseconds { + return deferMaybe(FxADeviceRegistrationResult.alreadyRegistered) + } + + let pushParams: FxADevicePushParams? + if AppConstants.MOZ_FXA_PUSH, let pushRegistration = account.pushRegistration { + let subscription = pushRegistration.defaultSubscription + pushParams = FxADevicePushParams(callback: subscription.endpoint.absoluteString, publicKey: subscription.p256dhPublicKey, authKey: subscription.authKey) + } else { + pushParams = nil + } + + let client = client ?? FxAClient10(authEndpoint: account.configuration.authEndpointURL, oauthEndpoint: account.configuration.oauthEndpointURL, profileEndpoint: account.configuration.profileEndpointURL) + let name = DeviceInfo.defaultClientName() + let device: FxADevice + let registrationResult: FxADeviceRegistrationResult + if let registration = account.deviceRegistration { + device = FxADevice.forUpdate(name, id: registration.id, push: pushParams) + registrationResult = FxADeviceRegistrationResult.updated + } else { + device = FxADevice.forRegister(name, type: "mobile", push: pushParams) + registrationResult = FxADeviceRegistrationResult.registered + } + + let registeredDevice = client.registerOrUpdate(device: device, withSessionToken: sessionToken) + let registration: Deferred> = registeredDevice.bind { result in + if let device = result.successValue { + return deferMaybe(FxADeviceRegistration(id: device.id!, version: DeviceRegistrationVersion, lastRegistered: Date.now())) + } + + // Recover from the error -- if we can. + if let error = result.failureValue as? FxAClientError, + case .remote(let remoteError) = error { + switch remoteError.code { + case FxAccountRemoteError.DeviceSessionConflict: + return recoverFromDeviceSessionConflict(account, client: client, sessionToken: sessionToken) + case FxAccountRemoteError.InvalidAuthenticationToken: + return recoverFromTokenError(account, client: client) + case FxAccountRemoteError.UnknownDevice: + return recoverFromUnknownDevice(account) + default: break + } + } + + // Not an error we can recover from. Rethrow it and fall back to the failure handler. + return deferMaybe(result.failureValue!) + } + + // Post-recovery. We either registered or we didn't, but update the account either way. + return registration.bind { result in + switch result { + case .success(let registration): + account.deviceRegistration = registration.value + return deferMaybe(registrationResult) + case .failure(let error): + log.error("Device registration failed: \(error.description)") + if let registration = account.deviceRegistration { + account.deviceRegistration = FxADeviceRegistration(id: registration.id, version: 0, lastRegistered: registration.lastRegistered) + } + return deferMaybe(error) + } + } + } + + fileprivate static func recoverFromDeviceSessionConflict(_ account: FirefoxAccount, client: FxAClient10, sessionToken: NSData) -> Deferred> { + // FxA has already associated this session with a different device id. + // Perhaps we were beaten in a race to register. Handle the conflict: + // 1. Fetch the list of devices for the current user from FxA. + // 2. Look for ourselves in the list. + // 3. If we find a match, set the correct device id and device registration + // version on the account data and return the correct device id. At next + // sync or next sign-in, registration is retried and should succeed. + log.warning("Device session conflict. Attempting to find the current device ID…") + return client.devices(withSessionToken: sessionToken) >>== { response in + guard let currentDevice = response.devices.find({ $0.isCurrentDevice }) else { + return deferMaybe(FxADeviceRegistratorError.currentDeviceNotFound) + } + + return deferMaybe(FxADeviceRegistration(id: currentDevice.id!, version: 0, lastRegistered: Date.now())) + } + } + + fileprivate static func recoverFromTokenError(_ account: FirefoxAccount, client: FxAClient10) -> Deferred> { + return client.status(forUID: account.uid) >>== { status in + _ = account.makeDoghouse() + if !status.exists { + // TODO: Should be in an "I have an iOS account, but the FxA is gone." state. + // This will do for now... + return deferMaybe(FxADeviceRegistratorError.accountDeleted) + } + return deferMaybe(FxADeviceRegistratorError.invalidSession) + } + } + + fileprivate static func recoverFromUnknownDevice(_ account: FirefoxAccount) -> Deferred> { + // FxA did not recognize the device ID. Handle it by clearing the registration on the account data. + // At next sync or next sign-in, registration is retried and should succeed. + log.warning("Unknown device ID. Clearing the local device data.") + account.deviceRegistration = nil + return deferMaybe(FxADeviceRegistratorError.unknownDevice) + } +} diff --git a/mobile/ios/Account/FxALoginStateMachine.swift b/mobile/ios/Account/FxALoginStateMachine.swift new file mode 100644 index 0000000000..a10c058822 --- /dev/null +++ b/mobile/ios/Account/FxALoginStateMachine.swift @@ -0,0 +1,182 @@ +/* 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 FxA +import Shared +import XCGLogger +import Deferred + +// TODO: log to an FxA-only, persistent log file. +private let log = Logger.syncLogger + +// TODO: fill this in! +private let KeyUnwrappingError = NSError(domain: "org.mozilla", code: 1, userInfo: nil) + +protocol FxALoginClient { + func keyPair() -> Deferred> + func keys(_ keyFetchToken: Data) -> Deferred> + func sign(_ sessionToken: Data, publicKey: PublicKey) -> Deferred> +} + +class FxALoginStateMachine { + let client: FxALoginClient + + // The keys are used as a set, to prevent cycles in the state machine. + var stateLabelsSeen = [FxAStateLabel: Bool]() + + init(client: FxALoginClient) { + self.client = client + } + + func advance(fromState state: FxAState, now: Timestamp) -> Deferred { + stateLabelsSeen.updateValue(true, forKey: state.label) + return self.advanceOne(fromState: state, now: now).bind { (newState: FxAState) in + let labelAlreadySeen = self.stateLabelsSeen.updateValue(true, forKey: newState.label) != nil + if labelAlreadySeen { + // Last stop! + return Deferred(value: newState) + } + return self.advance(fromState: newState, now: now) + } + } + + fileprivate func advanceOne(fromState state: FxAState, now: Timestamp) -> Deferred { + // For convenience. Without type annotation, Swift complains about types not being exact. + let separated: Deferred = Deferred(value: SeparatedState()) + let doghouse: Deferred = Deferred(value: DoghouseState()) + let same: Deferred = Deferred(value: state) + + log.info("Advancing from state: \(state.label.rawValue)") + switch state.label { + case .married: + let state = state as! MarriedState + log.debug("Checking key pair freshness.") + if state.isKeyPairExpired(now) { + log.info("Key pair has expired; transitioning to CohabitingBeforeKeyPair.") + return advanceOne(fromState: state.withoutKeyPair(), now: now) + } + log.debug("Checking certificate freshness.") + if state.isCertificateExpired(now) { + log.info("Certificate has expired; transitioning to CohabitingAfterKeyPair.") + return advanceOne(fromState: state.withoutCertificate(), now: now) + } + log.info("Key pair and certificate are fresh; staying Married.") + return same + + case .cohabitingBeforeKeyPair: + let state = state as! CohabitingBeforeKeyPairState + log.debug("Generating key pair.") + return self.client.keyPair().bind { result in + if let keyPair = result.successValue { + log.info("Generated key pair! Transitioning to CohabitingAfterKeyPair.") + let newState = CohabitingAfterKeyPairState(sessionToken: state.sessionToken, + kA: state.kA, kB: state.kB, + keyPair: keyPair, keyPairExpiresAt: now + OneMonthInMilliseconds) + return Deferred(value: newState) + } else { + log.error("Failed to generate key pair! Something is horribly wrong; transitioning to Separated in the hope that the error is transient.") + return separated + } + } + + case .cohabitingAfterKeyPair: + let state = state as! CohabitingAfterKeyPairState + log.debug("Signing public key.") + return client.sign(state.sessionToken, publicKey: state.keyPair.publicKey).bind { result in + if let response = result.successValue { + log.info("Signed public key! Transitioning to Married.") + let newState = MarriedState(sessionToken: state.sessionToken, + kA: state.kA, kB: state.kB, + keyPair: state.keyPair, keyPairExpiresAt: state.keyPairExpiresAt, + certificate: response.certificate, certificateExpiresAt: now + OneDayInMilliseconds) + return Deferred(value: newState) + } else { + if let error = result.failureValue as? FxAClientError { + switch error { + case let .remote(remoteError): + if remoteError.isUpgradeRequired { + log.error("Upgrade required: \(error.description)! Transitioning to Doghouse.") + return doghouse + } else if remoteError.isInvalidAuthentication { + log.error("Invalid authentication: \(error.description)! Transitioning to Separated.") + return separated + } else if remoteError.code < 200 || remoteError.code >= 300 { + log.error("Unsuccessful HTTP request: \(error.description)! Assuming error is transient and not transitioning.") + return same + } else { + log.error("Unknown error: \(error.description). Transitioning to Separated.") + return separated + } + case let .local(localError) where localError.domain == NSURLErrorDomain: + log.warning("Local networking error: \(result.failureValue!). Assuming transient and not transitioning.") + return same + default: + break + } + } + log.error("Unknown error: \(result.failureValue!). Transitioning to Separated.") + return separated + } + } + + case .engagedBeforeVerified, .engagedAfterVerified: + let state = state as! ReadyForKeys + log.debug("Fetching keys.") + return client.keys(state.keyFetchToken).bind { result in + if let response = result.successValue { + if let kB = response.wrapkB.xoredWith(state.unwrapkB) { + log.info("Unwrapped keys response. Transition to CohabitingBeforeKeyPair.") + self.notifyAccountVerified() + let newState = CohabitingBeforeKeyPairState(sessionToken: state.sessionToken, + kA: response.kA, kB: kB) + return Deferred(value: newState) + } else { + log.error("Failed to unwrap keys response! Transitioning to Separated in order to fetch new initial datum.") + return separated + } + } else { + if let error = result.failureValue as? FxAClientError { + log.error("Error \(error.description) \(error.description)") + switch error { + case let .remote(remoteError): + if remoteError.isUpgradeRequired { + log.error("Upgrade required: \(error.description)! Transitioning to Doghouse.") + return doghouse + } else if remoteError.isInvalidAuthentication { + log.error("Invalid authentication: \(error.description)! Transitioning to Separated in order to fetch new initial datum.") + return separated + } else if remoteError.isUnverified { + log.warning("Account is not yet verified; not transitioning.") + return same + } else if remoteError.code < 200 || remoteError.code >= 300 { + log.error("Unsuccessful HTTP request: \(error.description)! Assuming error is transient and not transitioning.") + return same + } else { + log.error("Unknown error: \(error.description). Transitioning to Separated.") + return separated + } + case let .local(localError) where localError.domain == NSURLErrorDomain: + log.warning("Local networking error: \(result.failureValue!). Assuming transient and not transitioning.") + return same + default: + break + } + } + log.error("Unknown error: \(result.failureValue!). Transitioning to Separated.") + return separated + } + } + + case .separated, .doghouse: + // We can't advance from the separated state (we need user input) or the doghouse (we need a client upgrade). + log.warning("User interaction required; not transitioning.") + return same + } + } + + fileprivate func notifyAccountVerified() { + NotificationCenter.default.post(name: NotificationFirefoxAccountVerified, object: nil, userInfo: nil) + } +} diff --git a/mobile/ios/Account/FxAPushMessageHandler.swift b/mobile/ios/Account/FxAPushMessageHandler.swift new file mode 100644 index 0000000000..0c209eff91 --- /dev/null +++ b/mobile/ios/Account/FxAPushMessageHandler.swift @@ -0,0 +1,294 @@ +/* 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 Deferred +import Shared +import SwiftyJSON + +let PendingAccountDisconnectedKey = "PendingAccountDisconnect" + +/// This class provides handles push messages from FxA. +/// For reference, the [message schema][0] and [Android implementation][1] are both useful resources. +/// [0]: https://github.com/mozilla/fxa-auth-server/blob/master/docs/pushpayloads.schema.json#L26 +/// [1]: https://dxr.mozilla.org/mozilla-central/source/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/FxAccountPushHandler.java +/// The main entry points are `handle` methods, to accept the raw APNS `userInfo` and then to process the resulting JSON. +class FxAPushMessageHandler { + let profile: Profile + + init(with profile: Profile) { + self.profile = profile + } +} + +extension FxAPushMessageHandler { + /// Accepts the raw Push message from Autopush. + /// This method then decrypts it according to the content-encoding (aes128gcm or aesgcm) + /// and then effects changes on the logged in account. + @discardableResult func handle(userInfo: [AnyHashable: Any]) -> PushMessageResult { + guard let subscription = profile.getAccount()?.pushRegistration?.defaultSubscription else { + return deferMaybe(PushMessageError.notDecrypted) + } + + guard let encoding = userInfo["con"] as? String, // content-encoding + let payload = userInfo["body"] as? String else { + return handleVerification() + } + // ver == endpointURL path, chid == channel id, aps == alert text and content_available. + + let plaintext: String? + if let cryptoKeyHeader = userInfo["cryptokey"] as? String, // crypto-key + let encryptionHeader = userInfo["enc"] as? String, // encryption + encoding == "aesgcm" { + plaintext = subscription.aesgcm(payload: payload, encryptionHeader: encryptionHeader, cryptoHeader: cryptoKeyHeader) + } else if encoding == "aes128gcm" { + plaintext = subscription.aes128gcm(payload: payload) + } else { + plaintext = nil + } + + guard let string = plaintext else { + return deferMaybe(PushMessageError.notDecrypted) + } + + return handle(plaintext: string) + } + + func handle(plaintext: String) -> PushMessageResult { + return handle(message: JSON(parseJSON: plaintext)) + } + + /// The main entry point to the handler for decrypted messages. + func handle(message json: JSON) -> PushMessageResult { + if !json.isDictionary() || json.isEmpty { + return handleVerification() + } + + let rawValue = json["command"].stringValue + guard let command = PushMessageType(rawValue: rawValue) else { + print("Command \(rawValue) received but not recognized") + return deferMaybe(PushMessageError.messageIncomplete) + } + + let result: PushMessageResult + switch command { + case .deviceConnected: + result = handleDeviceConnected(json["data"]) + case .deviceDisconnected: + result = handleDeviceDisconnected(json["data"]) + case .profileUpdated: + result = handleProfileUpdated() + case .passwordChanged: + result = handlePasswordChanged() + case .passwordReset: + result = handlePasswordReset() + case .collectionChanged: + result = handleCollectionChanged(json["data"]) + case .accountVerified: + result = handleVerification() + } + return result + } +} + +extension FxAPushMessageHandler { + func handleVerification() -> PushMessageResult { + // What we'd really like to be able to start syncing immediately we receive this + // message, but this method is run by the extension, so we can't do it here. + return deferMaybe(.accountVerified) + } + + // This will be executed by the app, not the extension. + // This isn't guaranteed to be run (when the app is backgrounded, and the user + // doesn't tap on the notification), but that's okay because: + // We'll naturally be syncing shortly after startup. + func postVerification() -> Success { + if let account = profile.getAccount(), + let syncManager = profile.syncManager { + return account.advance().bind { _ in + return syncManager.syncEverything(why: .didLogin) + } >>> succeed + } + return succeed() + } +} + +/// An extension to handle each of the messages. +extension FxAPushMessageHandler { + func handleDeviceConnected(_ data: JSON?) -> PushMessageResult { + guard let deviceName = data?["deviceName"].string else { + return messageIncomplete(.deviceConnected) + } + let message = PushMessage.deviceConnected(deviceName) + return deferMaybe(message) + } +} + +extension FxAPushMessageHandler { + func handleDeviceDisconnected(_ data: JSON?) -> PushMessageResult { + guard let deviceId = data?["id"].string else { + return messageIncomplete(.deviceDisconnected) + } + + if let ourDeviceId = self.getOurDeviceId(), deviceId == ourDeviceId { + // We can't disconnect the device from the account until we have + // access to the application, so we'll handle this properly in the AppDelegate, + // by calling the FxALoginHelper.applicationDidDisonnect(application). + profile.prefs.setBool(true, forKey: PendingAccountDisconnectedKey) + return deferMaybe(PushMessage.thisDeviceDisconnected) + } + + guard let profile = self.profile as? BrowserProfile else { + // We can't look up a name in testing, so this is the same as + // not knowing about it. + return deferMaybe(PushMessage.deviceDisconnected(nil)) + } + + let clients = profile.remoteClientsAndTabs + let getClient = clients.getClient(fxaDeviceId: deviceId) + + return getClient >>== { device in + let message = PushMessage.deviceDisconnected(device?.name) + if let id = device?.guid { + return clients.deleteClient(guid: id) >>== { _ in deferMaybe(message) } + } + + return deferMaybe(message) + } + } + + fileprivate func getOurDeviceId() -> String? { + return profile.getAccount()?.deviceRegistration?.id + } +} + +extension FxAPushMessageHandler { + func handleProfileUpdated() -> PushMessageResult { + return unimplemented(.profileUpdated) + } +} + +extension FxAPushMessageHandler { + func handlePasswordChanged() -> PushMessageResult { + return unimplemented(.passwordChanged) + } +} + +extension FxAPushMessageHandler { + func handlePasswordReset() -> PushMessageResult { + return unimplemented(.passwordReset) + } +} + +extension FxAPushMessageHandler { + func handleCollectionChanged(_ data: JSON?) -> PushMessageResult { + guard let collections = data?["collections"].arrayObject as? [String] else { + print("collections_changed received but incomplete: \(data ?? "nil")") + return deferMaybe(PushMessageError.messageIncomplete) + } + // Possible values: "addons", "bookmarks", "history", "forms", "prefs", "tabs", "passwords", "clients" + + // syncManager will only do a subset; others will be ignored. + return profile.syncManager.syncNamedCollections(why: .push, names: collections) >>== { deferMaybe(.collectionChanged(collections: collections)) } + } +} + +/// Some utility methods +fileprivate extension FxAPushMessageHandler { + func unimplemented(_ messageType: PushMessageType, with param: String? = nil) -> PushMessageResult { + if let param = param { + print("\(messageType) message received with parameter = \(param), but unimplemented") + } else { + print("\(messageType) message received, but unimplemented") + } + return deferMaybe(PushMessageError.unimplemented(messageType)) + } + + func messageIncomplete(_ messageType: PushMessageType) -> PushMessageResult { + print("\(messageType) message received, but incomplete") + return deferMaybe(PushMessageError.messageIncomplete) + } +} + +enum PushMessageType: String { + case deviceConnected = "fxaccounts:device_connected" + case deviceDisconnected = "fxaccounts:device_disconnected" + case profileUpdated = "fxaccounts:profile_updated" + case passwordChanged = "fxaccounts:password_changed" + case passwordReset = "fxaccounts:password_reset" + case collectionChanged = "sync:collection_changed" + + // This isn't a real message type, just the absence of one. + case accountVerified = "account_verified" +} + +enum PushMessage: Equatable { + case deviceConnected(String) + case deviceDisconnected(String?) + case profileUpdated + case passwordChanged + case passwordReset + case collectionChanged(collections: [String]) + case accountVerified + + // This is returned when we detect that it is us that has been disconnected. + case thisDeviceDisconnected + + var messageType: PushMessageType { + switch self { + case .deviceConnected(_): + return .deviceConnected + case .deviceDisconnected(_): + return .deviceDisconnected + case .thisDeviceDisconnected: + return .deviceDisconnected + case .profileUpdated: + return .profileUpdated + case .passwordChanged: + return .passwordChanged + case .passwordReset: + return .passwordReset + case .collectionChanged(collections: _): + return .collectionChanged + case .accountVerified: + return .accountVerified + } + } + + public static func ==(lhs: PushMessage, rhs: PushMessage) -> Bool { + guard lhs.messageType == rhs.messageType else { + return false + } + + switch (lhs, rhs) { + case (.deviceConnected(let lName), .deviceConnected(let rName)): + return lName == rName + case (.collectionChanged(let lList), .collectionChanged(let rList)): + return lList == rList + default: + return true + } + } +} + +typealias PushMessageResult = Deferred> + +enum PushMessageError: MaybeErrorType { + case notDecrypted + case messageIncomplete + case unimplemented(PushMessageType) + case timeout + case accountError + case noProfile + + public var description: String { + switch self { + case .notDecrypted: return "notDecrypted" + case .messageIncomplete: return "messageIncomplete" + case .unimplemented(let what): return "unimplemented=\(what)" + case .timeout: return "timeout" + case .accountError: return "accountError" + case .noProfile: return "noProfile" + } + } +} diff --git a/mobile/ios/Account/FxAState.swift b/mobile/ios/Account/FxAState.swift new file mode 100644 index 0000000000..dbd865049c --- /dev/null +++ b/mobile/ios/Account/FxAState.swift @@ -0,0 +1,338 @@ +/* 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 FxA +import Shared +import SwiftyJSON + +// The version of the state schema we persist. +let StateSchemaVersion = 1 + +// We want an enum because the set of states is closed. However, each state has state-specific +// behaviour, and the state's behaviour accumulates, so each state is a class. Switch on the +// label to get exhaustive cases. +public enum FxAStateLabel: String { + case engagedBeforeVerified = "engagedBeforeVerified" + case engagedAfterVerified = "engagedAfterVerified" + case cohabitingBeforeKeyPair = "cohabitingBeforeKeyPair" + case cohabitingAfterKeyPair = "cohabitingAfterKeyPair" + case married = "married" + case separated = "separated" + case doghouse = "doghouse" + + // See http://stackoverflow.com/a/24137319 + static let allValues: [FxAStateLabel] = [ + engagedBeforeVerified, + engagedAfterVerified, + cohabitingBeforeKeyPair, + cohabitingAfterKeyPair, + married, + separated, + doghouse, + ] +} + +public enum FxAActionNeeded { + case none + case needsVerification + case needsPassword + case needsUpgrade +} + +func state(fromJSON json: JSON) -> FxAState? { + if json.error != nil { + return nil + } + if let version = json["version"].int { + if version == StateSchemaVersion { + return stateV1(fromJSON: json) + } + } + return nil +} + +func stateV1(fromJSON json: JSON) -> FxAState? { + if let labelString = json["label"].string { + if let label = FxAStateLabel(rawValue: labelString) { + switch label { + case .engagedBeforeVerified: + if let + sessionToken = json["sessionToken"].string?.hexDecodedData, + let keyFetchToken = json["keyFetchToken"].string?.hexDecodedData, + let unwrapkB = json["unwrapkB"].string?.hexDecodedData, + let knownUnverifiedAt = json["knownUnverifiedAt"].int64, + let lastNotifiedUserAt = json["lastNotifiedUserAt"].int64 { + return EngagedBeforeVerifiedState( + knownUnverifiedAt: UInt64(knownUnverifiedAt), lastNotifiedUserAt: UInt64(lastNotifiedUserAt), + sessionToken: sessionToken, keyFetchToken: keyFetchToken, unwrapkB: unwrapkB) + } + + case .engagedAfterVerified: + if let + sessionToken = json["sessionToken"].string?.hexDecodedData, + let keyFetchToken = json["keyFetchToken"].string?.hexDecodedData, + let unwrapkB = json["unwrapkB"].string?.hexDecodedData { + return EngagedAfterVerifiedState(sessionToken: sessionToken, keyFetchToken: keyFetchToken, unwrapkB: unwrapkB) + } + + case .cohabitingBeforeKeyPair: + if let + sessionToken = json["sessionToken"].string?.hexDecodedData, + let kA = json["kA"].string?.hexDecodedData, + let kB = json["kB"].string?.hexDecodedData { + return CohabitingBeforeKeyPairState(sessionToken: sessionToken, kA: kA, kB: kB) + } + + case .cohabitingAfterKeyPair: + if let + sessionToken = json["sessionToken"].string?.hexDecodedData, + let kA = json["kA"].string?.hexDecodedData, + let kB = json["kB"].string?.hexDecodedData, + let keyPairJSON = json["keyPair"].dictionaryObject, + let keyPair = RSAKeyPair(jsonRepresentation: keyPairJSON), + let keyPairExpiresAt = json["keyPairExpiresAt"].int64 { + return CohabitingAfterKeyPairState(sessionToken: sessionToken, kA: kA, kB: kB, + keyPair: keyPair, keyPairExpiresAt: UInt64(keyPairExpiresAt)) + } + + case .married: + if let + sessionToken = json["sessionToken"].string?.hexDecodedData, + let kA = json["kA"].string?.hexDecodedData, + let kB = json["kB"].string?.hexDecodedData, + let keyPairJSON = json["keyPair"].dictionaryObject, + let keyPair = RSAKeyPair(jsonRepresentation: keyPairJSON), + let keyPairExpiresAt = json["keyPairExpiresAt"].int64, + let certificate = json["certificate"].string, + let certificateExpiresAt = json["certificateExpiresAt"].int64 { + return MarriedState(sessionToken: sessionToken, kA: kA, kB: kB, + keyPair: keyPair, keyPairExpiresAt: UInt64(keyPairExpiresAt), + certificate: certificate, certificateExpiresAt: UInt64(certificateExpiresAt)) + } + + case .separated: + return SeparatedState() + + case .doghouse: + return DoghouseState() + } + } + } + return nil +} + +// Not an externally facing state! +open class FxAState: JSONLiteralConvertible { + open var label: FxAStateLabel { return FxAStateLabel.separated } // This is bogus, but we have to do something! + + open var actionNeeded: FxAActionNeeded { + // Kind of nice to have this in one place. + switch label { + case .engagedBeforeVerified: return .needsVerification + case .engagedAfterVerified: return .none + case .cohabitingBeforeKeyPair: return .none + case .cohabitingAfterKeyPair: return .none + case .married: return .none + case .separated: return .needsPassword + case .doghouse: return .needsUpgrade + } + } + + open func asJSON() -> JSON { + return JSON([ + "version": StateSchemaVersion, + "label": self.label.rawValue, + ]) + } +} + +open class SeparatedState: FxAState { + override open var label: FxAStateLabel { return FxAStateLabel.separated } + + override public init() { + super.init() + } +} + +// Not an externally facing state! +open class TokenState: FxAState { + let sessionToken: Data + + init(sessionToken: Data) { + self.sessionToken = sessionToken + super.init() + } + + open override func asJSON() -> JSON { + var d: [String: JSON] = super.asJSON().dictionary! + d["sessionToken"] = JSON(sessionToken.hexEncodedString as NSString) + return JSON(d) + } +} + +// Not an externally facing state! +open class ReadyForKeys: TokenState { + let keyFetchToken: Data + let unwrapkB: Data + + init(sessionToken: Data, keyFetchToken: Data, unwrapkB: Data) { + self.keyFetchToken = keyFetchToken + self.unwrapkB = unwrapkB + super.init(sessionToken: sessionToken) + } + + open override func asJSON() -> JSON { + var d: [String: JSON] = super.asJSON().dictionary! + d["keyFetchToken"] = JSON(keyFetchToken.hexEncodedString as NSString) + d["unwrapkB"] = JSON(unwrapkB.hexEncodedString as NSString) + return JSON(d) + } +} + +open class EngagedBeforeVerifiedState: ReadyForKeys { + override open var label: FxAStateLabel { return FxAStateLabel.engagedBeforeVerified } + + // Timestamp, in milliseconds after the epoch, when we first knew the account was unverified. + // Use this to avoid nagging the user to verify her account immediately after connecting. + let knownUnverifiedAt: Timestamp + let lastNotifiedUserAt: Timestamp + + public init(knownUnverifiedAt: Timestamp, lastNotifiedUserAt: Timestamp, sessionToken: Data, keyFetchToken: Data, unwrapkB: Data) { + self.knownUnverifiedAt = knownUnverifiedAt + self.lastNotifiedUserAt = lastNotifiedUserAt + super.init(sessionToken: sessionToken, keyFetchToken: keyFetchToken, unwrapkB: unwrapkB) + } + + open override func asJSON() -> JSON { + var d = super.asJSON().dictionary! + d["knownUnverifiedAt"] = JSON(NSNumber(value: knownUnverifiedAt)) + d["lastNotifiedUserAt"] = JSON(NSNumber(value: lastNotifiedUserAt)) + return JSON(d) + } + + func withUnwrapKey(_ unwrapkB: Data) -> EngagedBeforeVerifiedState { + return EngagedBeforeVerifiedState( + knownUnverifiedAt: knownUnverifiedAt, lastNotifiedUserAt: lastNotifiedUserAt, + sessionToken: sessionToken, keyFetchToken: keyFetchToken, unwrapkB: unwrapkB) + } +} + +open class EngagedAfterVerifiedState: ReadyForKeys { + override open var label: FxAStateLabel { return FxAStateLabel.engagedAfterVerified } + + override public init(sessionToken: Data, keyFetchToken: Data, unwrapkB: Data) { + super.init(sessionToken: sessionToken, keyFetchToken: keyFetchToken, unwrapkB: unwrapkB) + } + + func withUnwrapKey(_ unwrapkB: Data) -> EngagedAfterVerifiedState { + return EngagedAfterVerifiedState(sessionToken: sessionToken, keyFetchToken: keyFetchToken, unwrapkB: unwrapkB) + } +} + +// Not an externally facing state! +open class TokenAndKeys: TokenState { + open let kA: Data + open let kB: Data + + init(sessionToken: Data, kA: Data, kB: Data) { + self.kA = kA + self.kB = kB + super.init(sessionToken: sessionToken) + } + + open override func asJSON() -> JSON { + var d = super.asJSON().dictionary! + d["kA"] = JSON(kA.hexEncodedString as NSString) + d["kB"] = JSON(kB.hexEncodedString as NSString) + return JSON(d) + } +} + +open class CohabitingBeforeKeyPairState: TokenAndKeys { + override open var label: FxAStateLabel { return FxAStateLabel.cohabitingBeforeKeyPair } +} + +// Not an externally facing state! +open class TokenKeysAndKeyPair: TokenAndKeys { + let keyPair: KeyPair + // Timestamp, in milliseconds after the epoch, when keyPair expires. After this time, generate a new keyPair. + let keyPairExpiresAt: Timestamp + + init(sessionToken: Data, kA: Data, kB: Data, keyPair: KeyPair, keyPairExpiresAt: Timestamp) { + self.keyPair = keyPair + self.keyPairExpiresAt = keyPairExpiresAt + super.init(sessionToken: sessionToken, kA: kA, kB: kB) + } + + open override func asJSON() -> JSON { + var d = super.asJSON().dictionary! + d["keyPair"] = JSON(keyPair.jsonRepresentation()) + d["keyPairExpiresAt"] = JSON(NSNumber(value: keyPairExpiresAt)) + return JSON(d) + } + + func isKeyPairExpired(_ now: Timestamp) -> Bool { + return keyPairExpiresAt < now + } +} + +open class CohabitingAfterKeyPairState: TokenKeysAndKeyPair { + override open var label: FxAStateLabel { return FxAStateLabel.cohabitingAfterKeyPair } +} + +open class MarriedState: TokenKeysAndKeyPair { + override open var label: FxAStateLabel { return FxAStateLabel.married } + + let certificate: String + let certificateExpiresAt: Timestamp + + init(sessionToken: Data, kA: Data, kB: Data, keyPair: KeyPair, keyPairExpiresAt: Timestamp, certificate: String, certificateExpiresAt: Timestamp) { + self.certificate = certificate + self.certificateExpiresAt = certificateExpiresAt + super.init(sessionToken: sessionToken, kA: kA, kB: kB, keyPair: keyPair, keyPairExpiresAt: keyPairExpiresAt) + } + + open override func asJSON() -> JSON { + var d = super.asJSON().dictionary! + d["certificate"] = JSON(certificate as NSString) + d["certificateExpiresAt"] = JSON(NSNumber(value: certificateExpiresAt)) + return JSON(d) + } + + func isCertificateExpired(_ now: Timestamp) -> Bool { + return certificateExpiresAt < now + } + + func withoutKeyPair() -> CohabitingBeforeKeyPairState { + let newState = CohabitingBeforeKeyPairState(sessionToken: sessionToken, + kA: kA, kB: kB) + return newState + } + + func withoutCertificate() -> CohabitingAfterKeyPairState { + let newState = CohabitingAfterKeyPairState(sessionToken: sessionToken, + kA: kA, kB: kB, + keyPair: keyPair, keyPairExpiresAt: keyPairExpiresAt) + return newState + } + + open func generateAssertionForAudience(_ audience: String, now: Timestamp) -> String { + let assertion = JSONWebTokenUtils.createAssertionWithPrivateKeyToSign(with: keyPair.privateKey, + certificate: certificate, + audience: audience, + issuer: "127.0.0.1", + issuedAt: now, + duration: OneHourInMilliseconds) + return assertion! + } +} + +open class DoghouseState: FxAState { + override open var label: FxAStateLabel { return FxAStateLabel.doghouse } + + override public init() { + super.init() + } +} diff --git a/mobile/ios/Account/HawkHelper.swift b/mobile/ios/Account/HawkHelper.swift new file mode 100644 index 0000000000..55cc23a468 --- /dev/null +++ b/mobile/ios/Account/HawkHelper.swift @@ -0,0 +1,150 @@ +/* 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 FxA +import Shared + +open class HawkHelper { + fileprivate let nonceLengthInBytes: UInt = 8 + + let id: String + let key: Data + + public init(id: String, key: Data) { + self.id = id + self.key = key + } + + // Produce a HAWK value suitable for an "Authorization: value" header, timestamped now. + open func getAuthorizationValueFor(_ request: URLRequest) -> String { + let timestampInSeconds: Int64 = Int64(Date().timeIntervalSince1970) + return getAuthorizationValueFor(request, at: timestampInSeconds) + } + + // Produce a HAWK value suitable for an "Authorization: value" header. + func getAuthorizationValueFor(_ request: URLRequest, at timestampInSeconds: Int64) -> String { + let nonce = Data.randomOfLength(nonceLengthInBytes)!.base64EncodedString + let extra = "" + return getAuthorizationValueFor(request, at: timestampInSeconds, nonce: nonce, extra: extra) + } + + func getAuthorizationValueFor(_ request: URLRequest, at timestampInSeconds: Int64, nonce: String, extra: String) -> String { + let timestampString = String(timestampInSeconds) + let hashString = HawkHelper.getPayloadHashFor(request) + let requestString = HawkHelper.getRequestStringFor(request, timestampString: timestampString, nonce: nonce, hash: hashString, extra: extra) + let macString = HawkHelper.getSignatureFor(requestString.utf8EncodedData, key: self.key) + + let s = NSMutableString(string: "Hawk ") + func append(_ key: String, value: String) { + s.append(key) + s.append("=\"") + s.append(value) + s.append("\", ") + } + append("id", value: id) + append("ts", value: timestampString) + append("nonce", value: nonce) + if !hashString.isEmpty { + append("hash", value: hashString) + } + if !extra.isEmpty { + append("ext", value: HawkHelper.escapeExtraHeaderAttribute(extra)) + } + append("mac", value: macString) + // Drop the trailing "\",". + return s.substring(to: s.length - 2) + } + + class func getSignatureFor(_ input: Data, key: Data) -> String { + return input.hmacSha256WithKey(key).base64EncodedString + } + + class func getRequestStringFor(_ request: URLRequest, timestampString: String, nonce: String, hash: String, extra: String) -> String { + let s = NSMutableString(string: "hawk.1.header\n") + func append(_ line: String) { + s.append(line) + s.append("\n") + } + append(timestampString) + append(nonce) + append((request as NSURLRequest).httpMethod?.uppercased() ?? "GET") + let url = request.url! + s.append(url.path) + if let query = url.query { + s.append("?") + s.append(query) + } + if let fragment = url.fragment { + s.append("#") + s.append(fragment) + } + s.append("\n") + append(url.host!) + if let port = (url as NSURL).port { + append(String(describing: port)) + } else { + if url.scheme?.lowercased() == "https" { + append("443") + } else { + append("80") + } + } + append(hash) + if !extra.isEmpty { + append(HawkHelper.escapeExtraString(extra)) + } else { + append("") + } + return s as String + } + + class func getPayloadHashFor(_ request: URLRequest) -> String { + if let body = request.httpBody { + var d = Data() + func append(_ s: String) { + let data = s.utf8EncodedData + d.append(data) + } + append("hawk.1.payload\n") + append(getBaseContentTypeFor(request.value(forHTTPHeaderField: "Content-Type"))) + append("\n") // Trailing newline is specified by Hawk. + d.append(body) + append("\n") // Trailing newline is specified by Hawk. + return d.sha256.base64EncodedString + } else { + return "" + } + } + + class func getBaseContentTypeFor(_ contentType: String?) -> String { + if let contentType = contentType { + if let index = contentType.characters.index(of: ";") { + return contentType.substring(to: index).trimmingCharacters(in: CharacterSet.whitespaces) + } else { + return contentType.trimmingCharacters(in: CharacterSet.whitespaces) + } + } else { + return "text/plain" + } + } + + class func escapeExtraHeaderAttribute(_ extra: String) -> String { + return extra.replacingOccurrences(of: "\\", with: "\\\\").replacingOccurrences(of: "\"", with: "\\\"") + } + + class func escapeExtraString(_ extra: String) -> String { + return extra.replacingOccurrences(of: "\\", with: "\\\\").replacingOccurrences(of: "\n", with: "\\n") + } +} + +extension URLRequest { + mutating func addAuthorizationHeader(forHKDFSHA256Key bytes: Data) { + let tokenId = bytes.subdata(in: 0.. + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + FMWK + CFBundleShortVersionString + 10.6 + CFBundleSignature + ???? + CFBundleVersion + 1 + NSPrincipalClass + + + diff --git a/mobile/ios/Account/SyncAuthState.swift b/mobile/ios/Account/SyncAuthState.swift new file mode 100644 index 0000000000..40fef5b570 --- /dev/null +++ b/mobile/ios/Account/SyncAuthState.swift @@ -0,0 +1,149 @@ +/* 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 XCGLogger +import Deferred +import SwiftyJSON + +private let CurrentSyncAuthStateCacheVersion = 1 + +private let log = Logger.syncLogger + +public struct SyncAuthStateCache { + let token: TokenServerToken + let forKey: Data + let expiresAt: Timestamp +} + +public protocol SyncAuthState { + func invalidate() + func token(_ now: Timestamp, canBeExpired: Bool) -> Deferred> + var deviceID: String? { get } +} + +public func syncAuthStateCachefromJSON(_ json: JSON) -> SyncAuthStateCache? { + if let version = json["version"].int { + if version != CurrentSyncAuthStateCacheVersion { + log.warning("Sync Auth State Cache is wrong version; dropping.") + return nil + } + if let + token = TokenServerToken.fromJSON(json["token"]), + let forKey = json["forKey"].string?.hexDecodedData, + let expiresAt = json["expiresAt"].int64 { + return SyncAuthStateCache(token: token, forKey: forKey, expiresAt: Timestamp(expiresAt)) + } + } + return nil +} + +extension SyncAuthStateCache: JSONLiteralConvertible { + public func asJSON() -> JSON { + return JSON([ + "version": CurrentSyncAuthStateCacheVersion, + "token": token.asJSON(), + "forKey": forKey.hexEncodedString, + "expiresAt": NSNumber(value: expiresAt), + ] as NSDictionary) + } +} + +open class FirefoxAccountSyncAuthState: SyncAuthState { + fileprivate let account: FirefoxAccount + fileprivate let cache: KeychainCache + public var deviceID: String? { + return account.deviceRegistration?.id + } + + init(account: FirefoxAccount, cache: KeychainCache) { + self.account = account + self.cache = cache + } + + // If a token gives you a 401, invalidate it and request a new one. + open func invalidate() { + log.info("Invalidating cached token server token.") + self.cache.value = nil + } + + // Generate an assertion and try to fetch a token server token, retrying at most a fixed number + // of times. + // + // It's tricky to get Swift to recurse into a closure that captures from the environment without + // segfaulting the compiler, so we pass everything around, like barbarians. + fileprivate func generateAssertionAndFetchTokenAt(_ audience: String, + client: TokenServerClient, + clientState: String?, + married: MarriedState, + now: Timestamp, + retryCount: Int) -> Deferred> { + let assertion = married.generateAssertionForAudience(audience, now: now) + return client.token(assertion, clientState: clientState).bind { result in + if retryCount > 0 { + if let tokenServerError = result.failureValue as? TokenServerError { + switch tokenServerError { + case let .remote(code, status, remoteTimestamp) where code == 401 && status == "invalid-timestamp": + if let remoteTimestamp = remoteTimestamp { + let skew = Int64(remoteTimestamp) - Int64(now) // Without casts, runtime crash due to overflow. + log.info("Token server responded with 401/invalid-timestamp: retrying with remote timestamp \(remoteTimestamp), which is local timestamp + skew = \(now) + \(skew).") + return self.generateAssertionAndFetchTokenAt(audience, client: client, clientState: clientState, married: married, now: remoteTimestamp, retryCount: retryCount - 1) + } + default: + break + } + } + } + // Fall-through. + return Deferred(value: result) + } + } + + open func token(_ now: Timestamp, canBeExpired: Bool) -> Deferred> { + if let value = cache.value { + // Give ourselves some room to do work. + let isExpired = value.expiresAt < now + 5 * OneMinuteInMilliseconds + if canBeExpired { + if isExpired { + log.info("Returning cached expired token.") + } else { + log.info("Returning cached token, which should be valid.") + } + return deferMaybe((token: value.token, forKey: value.forKey)) + } + + if !isExpired { + log.info("Returning cached token, which should be valid.") + return deferMaybe((token: value.token, forKey: value.forKey)) + } + } + + log.debug("Advancing Account state.") + return account.marriedState().bind { result in + if let married = result.successValue { + log.info("Account is in Married state; generating assertion.") + let tokenServerEndpointURL = self.account.configuration.sync15Configuration.tokenServerEndpointURL + let audience = TokenServerClient.getAudience(forURL: tokenServerEndpointURL) + let client = TokenServerClient(URL: tokenServerEndpointURL) + let clientState = FxAClient10.computeClientState(married.kB) + log.debug("Fetching token server token.") + let deferred = self.generateAssertionAndFetchTokenAt(audience, client: client, clientState: clientState, married: married, now: now, retryCount: 1) + deferred.upon { result in + // This could race to update the cache with multiple token results. + // One racer will win -- that's fine, presumably she has the freshest token. + // If not, that's okay, 'cuz the slightly dated token is still a valid token. + if let token = result.successValue { + let newCache = SyncAuthStateCache(token: token, forKey: married.kB, + expiresAt: now + 1000 * token.durationInSeconds) + log.debug("Fetched token server token! Token expires at \(newCache.expiresAt).") + self.cache.value = newCache + } + } + return chain(deferred, f: { (token: $0, forKey: married.kB) }) + } + return deferMaybe(result.failureValue!) + } + } +} diff --git a/mobile/ios/Account/TokenServerClient.swift b/mobile/ios/Account/TokenServerClient.swift new file mode 100644 index 0000000000..7ce70a92cf --- /dev/null +++ b/mobile/ios/Account/TokenServerClient.swift @@ -0,0 +1,189 @@ +/* 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 Alamofire +import Shared +import Foundation +import Deferred +import SwiftyJSON + +let TokenServerClientErrorDomain = "org.mozilla.token.error" +let TokenServerClientUnknownError = TokenServerError.local( + NSError(domain: TokenServerClientErrorDomain, code: 999, + userInfo: [NSLocalizedDescriptionKey: "Invalid server response"])) + +public struct TokenServerToken { + public let id: String + public let key: String + public let api_endpoint: String + public let uid: UInt64 + public let hashedFxAUID: String + public let durationInSeconds: UInt64 + // A healthy token server reports its timestamp. + public let remoteTimestamp: Timestamp + + /** + * Return true if this token points to the same place as the other token. + */ + public func sameDestination(_ other: TokenServerToken) -> Bool { + return self.uid == other.uid && + self.api_endpoint == other.api_endpoint + } + + public static func fromJSON(_ json: JSON) -> TokenServerToken? { + if let + id = json["id"].string, + let key = json["key"].string, + let api_endpoint = json["api_endpoint"].string, + let uid = json["uid"].int64, + let hashedFxAUID = json["hashed_fxa_uid"].string, + let durationInSeconds = json["duration"].int64, + let remoteTimestamp = json["remoteTimestamp"].int64 { + return TokenServerToken(id: id, key: key, api_endpoint: api_endpoint, uid: UInt64(uid), + hashedFxAUID: hashedFxAUID, durationInSeconds: UInt64(durationInSeconds), + remoteTimestamp: Timestamp(remoteTimestamp)) + } + return nil + } + + public func asJSON() -> JSON { + let D: [String: AnyObject] = [ + "id": id as AnyObject, + "key": key as AnyObject, + "api_endpoint": api_endpoint as AnyObject, + "uid": NSNumber(value: uid as UInt64), + "hashed_fxa_uid": hashedFxAUID as AnyObject, + "duration": NSNumber(value: durationInSeconds as UInt64), + "remoteTimestamp": NSNumber(value: remoteTimestamp), + ] + return JSON(D as NSDictionary) + } +} + +enum TokenServerError { + // A Remote error definitely has a status code, but we may not have a well-formed JSON response + // with a status; and we could have an unhealthy server that is not reporting its timestamp. + case remote(code: Int32, status: String?, remoteTimestamp: Timestamp?) + case local(NSError) +} + +extension TokenServerError: MaybeErrorType { + var description: String { + switch self { + case let .remote(code: code, status: status, remoteTimestamp: _): + if let status = status { + return "" + } else { + return "" + } + case let .local(error): + return "" + } + } +} + +open class TokenServerClient { + let URL: URL + + public init(URL: URL? = nil) { + self.URL = URL ?? ProductionSync15Configuration().tokenServerEndpointURL + } + + open class func getAudience(forURL URL: URL) -> String { + if let port = URL.port { + return "\(URL.scheme!)://\(URL.host!):\(port)" + } else { + return "\(URL.scheme!)://\(URL.host!)" + } + } + + fileprivate class func parseTimestampHeader(_ header: String?) -> Timestamp? { + if let timestampString = header { + return decimalSecondsStringToTimestamp(timestampString) + } else { + return nil + } + } + + fileprivate class func remoteError(fromJSON json: JSON, statusCode: Int, remoteTimestampHeader: String?) -> TokenServerError? { + if json.error != nil { + return nil + } + if 200 <= statusCode && statusCode <= 299 { + return nil + } + return TokenServerError.remote(code: Int32(statusCode), status: json["status"].string, + remoteTimestamp: parseTimestampHeader(remoteTimestampHeader)) + } + + fileprivate class func token(fromJSON json: JSON, remoteTimestampHeader: String?) -> TokenServerToken? { + if json.error != nil { + return nil + } + if let + remoteTimestamp = parseTimestampHeader(remoteTimestampHeader), // A token server that is not providing its timestamp is not healthy. + let id = json["id"].string, + let key = json["key"].string, + let api_endpoint = json["api_endpoint"].string, + let uid = json["uid"].int, + let hashedFxAUID = json["hashed_fxa_uid"].string, + let durationInSeconds = json["duration"].int64, durationInSeconds > 0 { + return TokenServerToken(id: id, key: key, api_endpoint: api_endpoint, uid: UInt64(uid), + hashedFxAUID: hashedFxAUID, durationInSeconds: UInt64(durationInSeconds), + remoteTimestamp: remoteTimestamp) + } + return nil + } + + lazy fileprivate var alamofire: SessionManager = { + let ua = UserAgent.tokenServerClientUserAgent + let configuration = URLSessionConfiguration.ephemeral + var defaultHeaders = SessionManager.default.session.configuration.httpAdditionalHeaders ?? [:] + defaultHeaders["User-Agent"] = ua + configuration.httpAdditionalHeaders = defaultHeaders + return SessionManager(configuration: configuration) + }() + + open func token(_ assertion: String, clientState: String? = nil) -> Deferred> { + let deferred = Deferred>() + + var mutableURLRequest = URLRequest(url: URL) + mutableURLRequest.setValue("BrowserID " + assertion, forHTTPHeaderField: "Authorization") + if let clientState = clientState { + mutableURLRequest.setValue(clientState, forHTTPHeaderField: "X-Client-State") + } + + alamofire.request(mutableURLRequest) + .validate(contentType: ["application/json"]) + .responseJSON { response in + + // Don't cancel requests just because our Manager is deallocated. + withExtendedLifetime(self.alamofire) { + if let error = response.result.error { + deferred.fill(Maybe(failure: TokenServerError.local(error as NSError))) + return + } + + if let data = response.result.value as AnyObject? { // Declaring the type quiets a Swift warning about inferring AnyObject. + let json = JSON(data) + let remoteTimestampHeader = response.response?.allHeaderFields["X-Timestamp"] as? String + + if let remoteError = TokenServerClient.remoteError(fromJSON: json, statusCode: response.response!.statusCode, + remoteTimestampHeader: remoteTimestampHeader) { + deferred.fill(Maybe(failure: remoteError)) + return + } + + if let token = TokenServerClient.token(fromJSON: json, remoteTimestampHeader: remoteTimestampHeader) { + deferred.fill(Maybe(success: token)) + return + } + } + + deferred.fill(Maybe(failure: TokenServerClientUnknownError)) + } + } + return deferred + } +} diff --git a/mobile/ios/AccountTests/FirefoxAccountTests.swift b/mobile/ios/AccountTests/FirefoxAccountTests.swift new file mode 100644 index 0000000000..1622d80a92 --- /dev/null +++ b/mobile/ios/AccountTests/FirefoxAccountTests.swift @@ -0,0 +1,63 @@ +/* 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/. */ + +@testable import Account +import Shared +import UIKit + +import XCTest + +class FirefoxAccountTests: XCTestCase { + override func setUp() { + super.setUp() + continueAfterFailure = false + } + + func testSerialization() { + let ogPushSub = PushSubscription(channelID: "channel-id", + endpoint: URL(string: "https://mozilla.com")!, + p256dhPrivateKey: "private-key", + p256dhPublicKey: "public-key", + authKey: "auth-key") + + let d: [String: Any] = [ + "version": 1, + "configurationLabel": FirefoxAccountConfigurationLabel.production.rawValue, + "email": "testtest@test.com", + "uid": "uid", + "deviceRegistration": FxADeviceRegistration(id: "bogus-device", version: 0, lastRegistered: Date.now()), + "pushRegistration": PushRegistration(uaid: "bogus-device-uaid", secret: "secret", subscription: ogPushSub), + ] + + let account1 = FirefoxAccount( + configuration: FirefoxAccountConfigurationLabel.production.toConfiguration(), + email: d["email"] as! String, + uid: d["uid"] as! String, + deviceRegistration: (d["deviceRegistration"] as! FxADeviceRegistration), + stateKeyLabel: Bytes.generateGUID(), + state: SeparatedState()) + + account1.pushRegistration = d["pushRegistration"] as? PushRegistration + + let d1 = account1.dictionary() + + let account2 = FirefoxAccount.fromDictionary(d1) + XCTAssertNotNil(account2) + let d2 = account2!.dictionary() + + for (k, v) in d { + // Skip version, which is an Int. + if let s = v as? String { + XCTAssertEqual(s, d1[k] as? String, "Value for '\(k)' does not agree for manually created account.") + XCTAssertEqual(s, d2[k] as? String, "Value for '\(k)' does not agree for deserialized account.") + } + } + + if let pubSub = account2?.pushRegistration?.defaultSubscription { + XCTAssertEqual(pubSub, ogPushSub) + } else { + XCTFail("PushSubscription did not get decoded") + } + } +} diff --git a/mobile/ios/AccountTests/FxAClient10Tests.swift b/mobile/ios/AccountTests/FxAClient10Tests.swift new file mode 100644 index 0000000000..3250c76ad7 --- /dev/null +++ b/mobile/ios/AccountTests/FxAClient10Tests.swift @@ -0,0 +1,165 @@ +/* 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/. */ + +@testable import Account +import FxA +import Shared +import UIKit +import Deferred + +import XCTest + +class FxAClient10Tests: LiveAccountTest { + func testUnwrapKey() { + let stretchedPW = "e4e8889bd8bd61ad6de6b95c059d56e7b50dacdaf62bd84644af7e2add84345d".hexDecodedData + let unwrapKey = FxAClient10.computeUnwrapKey(stretchedPW) + XCTAssertEqual(unwrapKey.hexEncodedString, "de6a2648b78284fcb9ffa81ba95803309cfba7af583c01a8a1a63e567234dd28") + } + + func testClientState() { + let kB = "fd5c747806c07ce0b9d69dcfea144663e630b65ec4963596a22f24910d7dd15d".hexDecodedData + let clientState = FxAClient10.computeClientState(kB)! + XCTAssertEqual(clientState, "6ae94683571c7a7c54dab4700aa3995f") + } + + func testErrorOutput() { + // Make sure we don't hide error details. + let error = NSError(domain: "test", code: 123, userInfo: nil) + + let localError = FxAClientError.local(error) + XCTAssertEqual( + localError.description, + "") + XCTAssertEqual( + "\(localError)", + "") + + let remoteError = FxAClientError.remote(RemoteError(code: 401, errno: 104, + error: "error", message: "message", info: "info")) + XCTAssertEqual( + remoteError.description, + "") + XCTAssertEqual( + "\(remoteError)", + "") + } + + func testLoginSuccess() { + withVerifiedAccount { emailUTF8, quickStretchedPW in + let e = self.expectation(description: "") + + let client = FxAClient10() + let result = client.login(emailUTF8, quickStretchedPW: quickStretchedPW, getKeys: true) + result.upon { result in + if let response = result.successValue { + XCTAssertNotNil(response.uid) + XCTAssertEqual(response.verified, true) + XCTAssertNotNil(response.sessionToken) + XCTAssertNotNil(response.keyFetchToken) + } else { + XCTAssertEqual(result.failureValue!.description, "") + } + e.fulfill() + } + } + self.waitForExpectations(timeout: 10, handler: nil) + } + + func testLoginFailure() { + withVerifiedAccount { emailUTF8, _ in + let e = self.expectation(description: "") + + let badPassword = FxAClient10.quickStretchPW(emailUTF8, password: "BAD PASSWORD".utf8EncodedData) + + let client = FxAClient10() + let result = client.login(emailUTF8, quickStretchedPW: badPassword, getKeys: true) + result.upon { result in + if let response = result.successValue { + XCTFail("Got response: \(response)") + } else { + if let error = result.failureValue as? FxAClientError { + switch error { + case let .remote(remoteError): + XCTAssertEqual(remoteError.code, Int32(400)) // Bad auth. + XCTAssertEqual(remoteError.errno, Int32(103)) // Incorrect password. + case let .local(error): + XCTAssertEqual(error.description, "") + } + } else { + XCTAssertEqual(result.failureValue!.description, "") + } + } + e.fulfill() + } + } + self.waitForExpectations(timeout: 10, handler: nil) + } + + func testKeysSuccess() { + withVerifiedAccount { emailUTF8, quickStretchedPW in + let e = self.expectation(description: "") + + let client = FxAClient10() + let login: Deferred> = client.login(emailUTF8, quickStretchedPW: quickStretchedPW, getKeys: true) + let keys: Deferred> = login.bind { (result: Maybe) in + switch result { + case let .failure(error): + return Deferred(value: .failure(error)) + case let .success(loginResponse): + return client.keys(loginResponse.value.keyFetchToken) + } + } + keys.upon { result in + if let response = result.successValue { + XCTAssertEqual(32, response.kA.count) + XCTAssertEqual(32, response.wrapkB.count) + } else { + XCTAssertEqual(result.failureValue!.description, "") + } + e.fulfill() + } + } + self.waitForExpectations(timeout: 10, handler: nil) + } + + func testSignSuccess() { + withVerifiedAccount { emailUTF8, quickStretchedPW in + let e = self.expectation(description: "") + + let client = FxAClient10() + let login: Deferred> = client.login(emailUTF8, quickStretchedPW: quickStretchedPW, getKeys: true) + let sign: Deferred> = login.bind { (result: Maybe) in + switch result { + case let .failure(error): + return Deferred(value: .failure(error)) + case let .success(loginResponse): + let keyPair = RSAKeyPair.generate(withModulusSize: 1024)! + return client.sign(loginResponse.value.sessionToken, publicKey: keyPair.publicKey) + } + } + sign.upon { result in + if let response = result.successValue { + XCTAssertNotNil(response.certificate) + // A simple test that we got a reasonable certificate back. + XCTAssertEqual(3, response.certificate.components(separatedBy: ".").count) + } else { + XCTAssertEqual(result.failureValue!.description, "") + } + e.fulfill() + } + } + self.waitForExpectations(timeout: 10, handler: nil) + } + + func testProfileSuccess() { + withVerifiedAccountNoExpectations { emailUTF8, quickStretchedPW in + let stageConfiguration = StageFirefoxAccountConfiguration() + let client = FxAClient10(authEndpoint: stageConfiguration.authEndpointURL, oauthEndpoint: stageConfiguration.oauthEndpointURL, profileEndpoint: stageConfiguration.profileEndpointURL) + let response = (client.login(emailUTF8, quickStretchedPW: quickStretchedPW, getKeys: true) >>== { login in + return client.getProfile(withSessionToken: login.sessionToken as NSData) + }).value.successValue + XCTAssertNotNil(response?.uid) + } + } +} diff --git a/mobile/ios/AccountTests/FxALoginStateMachineTests.swift b/mobile/ios/AccountTests/FxALoginStateMachineTests.swift new file mode 100644 index 0000000000..3a20e0fba5 --- /dev/null +++ b/mobile/ios/AccountTests/FxALoginStateMachineTests.swift @@ -0,0 +1,249 @@ +/* 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/. */ + +@testable import Account +import Foundation +import FxA +import Shared +import Deferred + +import XCTest + +class MockFxALoginClient: FxALoginClient { + // Fixed per mock client, for testing. + let kA = Data.randomOfLength(UInt(KeyLength))! + let wrapkB = Data.randomOfLength(UInt(KeyLength))! + + func keyPair() -> Deferred> { + let keyPair: KeyPair = RSAKeyPair.generate(withModulusSize: 512) + return Deferred(value: Maybe(success: keyPair)) + } + + func keys(_ keyFetchToken: Data) -> Deferred> { + let response = FxAKeysResponse(kA: kA, wrapkB: wrapkB) + return Deferred(value: Maybe(success: response)) + } + + func sign(_ sessionToken: Data, publicKey: PublicKey) -> Deferred> { + let response = FxASignResponse(certificate: "certificate") + return Deferred(value: Maybe(success: response)) + } +} + +// A mock client that fails locally (i.e., cannot connect to the network). +class MockFxALoginClientWithoutNetwork: MockFxALoginClient { + override func keys(_ keyFetchToken: Data) -> Deferred> { + // Fail! + return Deferred(value: Maybe(failure: FxAClientError.local(NSError(domain: NSURLErrorDomain, code: -1000, userInfo: nil)))) + } + + override func sign(_ sessionToken: Data, publicKey: PublicKey) -> Deferred> { + // Fail! + return Deferred(value: Maybe(failure: FxAClientError.local(NSError(domain: NSURLErrorDomain, code: -1000, userInfo: nil)))) + } +} + +// A mock client that responds to keys and sign with 401 errors. +class MockFxALoginClientAfterPasswordChange: MockFxALoginClient { + override func keys(_ keyFetchToken: Data) -> Deferred> { + let response = FxAClientError.remote(RemoteError(code: 401, errno: 103, error: "Bad auth", message: "Bad auth message", info: "Bad auth info")) + return Deferred(value: Maybe(failure: response)) + } + + override func sign(_ sessionToken: Data, publicKey: PublicKey) -> Deferred> { + let response = FxAClientError.remote(RemoteError(code: 401, errno: 103, error: "Bad auth", message: "Bad auth message", info: "Bad auth info")) + return Deferred(value: Maybe(failure: response)) + } +} + +// A mock client that responds to keys with 400/104 (needs verification responses). +class MockFxALoginClientBeforeVerification: MockFxALoginClient { + override func keys(_ keyFetchToken: Data) -> Deferred> { + let response = FxAClientError.remote(RemoteError(code: 400, errno: 104, + error: "Unverified", message: "Unverified message", info: "Unverified info")) + return Deferred(value: Maybe(failure: response)) + } +} + +// A mock client that responds to sign with 503/999 (unknown server error). +class MockFxALoginClientDuringOutage: MockFxALoginClient { + override func sign(_ sessionToken: Data, publicKey: PublicKey) -> Deferred> { + let response = FxAClientError.remote(RemoteError(code: 503, errno: 999, + error: "Unknown", message: "Unknown error", info: "Unknown err info")) + return Deferred(value: Maybe(failure: response)) + } +} + +class FxALoginStateMachineTests: XCTestCase { + let marriedState = FxAStateTests.stateForLabel(FxAStateLabel.married) as! MarriedState + + override func setUp() { + super.setUp() + self.continueAfterFailure = false + } + + func withMachine(_ client: FxALoginClient, callback: (FxALoginStateMachine) -> Void) { + let stateMachine = FxALoginStateMachine(client: client) + callback(stateMachine) + } + + func withMachineAndClient(_ callback: (FxALoginStateMachine, MockFxALoginClient) -> Void) { + let client = MockFxALoginClient() + withMachine(client) { stateMachine in + callback(stateMachine, client) + } + } + + func testAdvanceWhenInteractionRequired() { + // The simple cases are when we get to Separated and Doghouse. There's nothing to do! + // We just have to wait for user interaction. + for stateLabel in [FxAStateLabel.separated, FxAStateLabel.doghouse] { + let e = expectation(description: "Wait for login state machine.") + let state = FxAStateTests.stateForLabel(stateLabel) + withMachineAndClient { stateMachine, _ in + stateMachine.advance(fromState: state, now: 0).upon { newState in + XCTAssertEqual(newState.label, stateLabel) + e.fulfill() + } + } + } + self.waitForExpectations(timeout: 10, handler: nil) + } + + func testAdvanceFromEngagedBeforeVerified() { + // Advancing from engaged before verified stays put. + let e = self.expectation(description: "Wait for login state machine.") + let engagedState = (FxAStateTests.stateForLabel(.engagedBeforeVerified) as! EngagedBeforeVerifiedState) + withMachine(MockFxALoginClientBeforeVerification()) { stateMachine in + stateMachine.advance(fromState: engagedState, now: engagedState.knownUnverifiedAt).upon { newState in + XCTAssertEqual(newState.label.rawValue, engagedState.label.rawValue) + e.fulfill() + } + } + self.waitForExpectations(timeout: 10, handler: nil) + } + + func testAdvanceFromEngagedAfterVerified() { + // Advancing from an Engaged state correctly XORs the keys. + withMachineAndClient { stateMachine, client in + // let unwrapkB = Bytes.generateRandomBytes(UInt(KeyLength)) + let unwrapkB = client.wrapkB // This way we get all 0s, which is easy to test. + let engagedState = (FxAStateTests.stateForLabel(.engagedAfterVerified) as! EngagedAfterVerifiedState).withUnwrapKey(unwrapkB) + + let e = self.expectation(description: "Wait for login state machine.") + stateMachine.advance(fromState: engagedState, now: 0).upon { newState in + XCTAssertEqual(newState.label.rawValue, FxAStateLabel.married.rawValue) + if let newState = newState as? MarriedState { + // We get kA from the client directly. + XCTAssertEqual(newState.kA.hexEncodedString, client.kA.hexEncodedString) + // We unwrap kB by XORing. The result is KeyLength (32) 0s. + XCTAssertEqual(newState.kB.hexEncodedString, "0000000000000000000000000000000000000000000000000000000000000000") + } + e.fulfill() + } + } + self.waitForExpectations(timeout: 10, handler: nil) + } + + func testAdvanceFromEngagedAfterVerifiedWithoutNetwork() { + // Advancing from engaged after verified, but during outage, stays put. + withMachine(MockFxALoginClientWithoutNetwork()) { stateMachine in + let engagedState = FxAStateTests.stateForLabel(.engagedAfterVerified) + + let e = self.expectation(description: "Wait for login state machine.") + stateMachine.advance(fromState: engagedState, now: 0).upon { newState in + XCTAssertEqual(newState.label.rawValue, engagedState.label.rawValue) + e.fulfill() + } + } + self.waitForExpectations(timeout: 10, handler: nil) + } + + func testAdvanceFromCohabitingAfterVerifiedDuringOutage() { + // Advancing from engaged after verified, but during outage, stays put. + let e = self.expectation(description: "Wait for login state machine.") + let state = (FxAStateTests.stateForLabel(.cohabitingAfterKeyPair) as! CohabitingAfterKeyPairState) + withMachine(MockFxALoginClientDuringOutage()) { stateMachine in + stateMachine.advance(fromState: state, now: 0).upon { newState in + XCTAssertEqual(newState.label.rawValue, state.label.rawValue) + e.fulfill() + } + } + self.waitForExpectations(timeout: 10, handler: nil) + } + + func testAdvanceFromCohabitingAfterVerifiedWithoutNetwork() { + // Advancing from cohabiting after verified, but when the network is not available, stays put. + let e = self.expectation(description: "Wait for login state machine.") + let state = (FxAStateTests.stateForLabel(.cohabitingAfterKeyPair) as! CohabitingAfterKeyPairState) + withMachine(MockFxALoginClientWithoutNetwork()) { stateMachine in + stateMachine.advance(fromState: state, now: 0).upon { newState in + XCTAssertEqual(newState.label.rawValue, state.label.rawValue) + e.fulfill() + } + } + self.waitForExpectations(timeout: 10, handler: nil) + } + + func testAdvanceFromMarried() { + // Advancing from a healthy Married state is easy. + let e = self.expectation(description: "Wait for login state machine.") + withMachineAndClient { stateMachine, _ in + stateMachine.advance(fromState: self.marriedState, now: 0).upon { newState in + XCTAssertEqual(newState.label, FxAStateLabel.married) + e.fulfill() + } + } + self.waitForExpectations(timeout: 10, handler: nil) + } + + func testAdvanceFromMarriedWithExpiredCertificate() { + // Advancing from a Married state with an expired certificate gets back to Married. + let e = self.expectation(description: "Wait for login state machine.") + let now = self.marriedState.certificateExpiresAt + OneWeekInMilliseconds + 1 + withMachineAndClient { stateMachine, _ in + stateMachine.advance(fromState: self.marriedState, now: now).upon { newState in + XCTAssertEqual(newState.label.rawValue, FxAStateLabel.married.rawValue) + if let newState = newState as? MarriedState { + // We should have a fresh certificate. + XCTAssertLessThan(self.marriedState.certificateExpiresAt, now) + XCTAssertGreaterThan(newState.certificateExpiresAt, now) + } + e.fulfill() + } + } + self.waitForExpectations(timeout: 10, handler: nil) + } + + func testAdvanceFromMarriedWithExpiredKeyPair() { + // Advancing from a Married state with an expired keypair gets back to Married too. + let e = self.expectation(description: "Wait for login state machine.") + let now = self.marriedState.certificateExpiresAt + OneMonthInMilliseconds + 1 + withMachineAndClient { stateMachine, _ in + stateMachine.advance(fromState: self.marriedState, now: now).upon { newState in + XCTAssertEqual(newState.label.rawValue, FxAStateLabel.married.rawValue) + if let newState = newState as? MarriedState { + // We should have a fresh key pair (and certificate, but we don't verify that). + XCTAssertLessThan(self.marriedState.keyPairExpiresAt, now) + XCTAssertGreaterThan(newState.keyPairExpiresAt, now) + } + e.fulfill() + } + } + self.waitForExpectations(timeout: 10, handler: nil) + } + + func testAdvanceFromMarriedAfterPasswordChange() { + // Advancing from a Married state with a 401 goes to Separated if it needs a new certificate. + let e = self.expectation(description: "Wait for login state machine.") + let now = self.marriedState.certificateExpiresAt + OneDayInMilliseconds + 1 + withMachine(MockFxALoginClientAfterPasswordChange()) { stateMachine in + stateMachine.advance(fromState: self.marriedState, now: now).upon { newState in + XCTAssertEqual(newState.label.rawValue, FxAStateLabel.separated.rawValue) + e.fulfill() + } + } + self.waitForExpectations(timeout: 10, handler: nil) + } +} diff --git a/mobile/ios/AccountTests/FxAStateTests.swift b/mobile/ios/AccountTests/FxAStateTests.swift new file mode 100644 index 0000000000..06f7d2428f --- /dev/null +++ b/mobile/ios/AccountTests/FxAStateTests.swift @@ -0,0 +1,81 @@ +/* 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/. */ + +@testable import Account +import FxA +import Shared +import SwiftyJSON + +import XCTest + +class FxAStateTests: XCTestCase { + class func stateForLabel(_ label: FxAStateLabel) -> FxAState { + let keyLength = UInt(KeyLength) // Ah, Swift. + let now = Date.now() + + switch label { + case .engagedBeforeVerified: + return EngagedBeforeVerifiedState( + knownUnverifiedAt: now + 1, lastNotifiedUserAt: now + 2, + sessionToken: Data.randomOfLength(keyLength)!, + keyFetchToken: Data.randomOfLength(keyLength)!, + unwrapkB: Data.randomOfLength(keyLength)!) + + case .engagedAfterVerified: + return EngagedAfterVerifiedState( + sessionToken: Data.randomOfLength(keyLength)!, + keyFetchToken: Data.randomOfLength(keyLength)!, + unwrapkB: Data.randomOfLength(keyLength)!) + + case .cohabitingBeforeKeyPair: + return CohabitingBeforeKeyPairState(sessionToken: Data.randomOfLength(keyLength)!, + kA: Data.randomOfLength(keyLength)!, kB: Data.randomOfLength(keyLength)!) + + case .cohabitingAfterKeyPair: + let keyPair = RSAKeyPair.generate(withModulusSize: 512)! + return CohabitingAfterKeyPairState(sessionToken: Data.randomOfLength(keyLength)!, + kA: Data.randomOfLength(keyLength)!, kB: Data.randomOfLength(keyLength)!, + keyPair: keyPair, keyPairExpiresAt: now + 1) + + case .married: + let keyPair = RSAKeyPair.generate(withModulusSize: 512)! + return MarriedState(sessionToken: Data.randomOfLength(keyLength)!, + kA: Data.randomOfLength(keyLength)!, kB: Data.randomOfLength(keyLength)!, + keyPair: keyPair, keyPairExpiresAt: now + 1, + certificate: "certificate", certificateExpiresAt: now + 2) + + case .separated: + return SeparatedState() + + case .doghouse: + return DoghouseState() + } + } + + func testSerialization() { + // Journal of Negative Results: make sure we aren't *always* succeeding. + // This Married state will have an earlier timestamp than the one generated after the loop. + let state1 = FxAStateTests.stateForLabel(.married) as! MarriedState + + for stateLabel in FxAStateLabel.allValues { + let stateFromLabel = FxAStateTests.stateForLabel(stateLabel) + let d = stateFromLabel.asJSON() + if let e = state(fromJSON:d)?.asJSON() { + // We can't compare arbitrary Swift Dictionary instances directly, but the following appears to work. + XCTAssertEqual( + NSDictionary(dictionary: d.dictionaryObject!), + NSDictionary(dictionary: e.dictionaryObject!)) + } else { + XCTFail("Expected to create state.") + } + } + + // This Married state will have a later timestamp than the one generated before the loop. + let state2 = FxAStateTests.stateForLabel(.married) as! MarriedState + // We can't compare arbitrary Swift Dictionary instances directly, but the following appears to work. + XCTAssertNotEqual( + NSDictionary(dictionary: state1.asJSON().dictionaryObject!), + NSDictionary(dictionary: state2.asJSON().dictionaryObject!)) + } +} diff --git a/mobile/ios/AccountTests/HawkHelperTests.swift b/mobile/ios/AccountTests/HawkHelperTests.swift new file mode 100644 index 0000000000..db0a9b3b89 --- /dev/null +++ b/mobile/ios/AccountTests/HawkHelperTests.swift @@ -0,0 +1,90 @@ +/* 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/. */ + +@testable import Account +import Alamofire +import Foundation +import FxA + +import XCTest + +class HawkHelperTests: XCTestCase { + func testSpecSignatureExample() { + let input = "hawk.1.header\n" + + "1353832234\n" + + "j4h3g2\n" + + "GET\n" + + "/resource/1?b=1&a=2\n" + + "example.com\n" + + "8000\n" + + "\n" + + "some-app-ext-data\n" + + let expected = HawkHelper.getSignatureFor(input.utf8EncodedData, key: "werxhqb98rpaxn39848xrunpaw3489ruxnpa98w4rxn".utf8EncodedData) + XCTAssertEqual("6R4rV5iE+NPoym+WwjeHzjAGXUtLNIxmo1vpMofpLAE=", expected) + } + + func testSpecRequestString() { + let timestamp = Int64(1353832234) + let nonce = "j4h3g2" + let extra = "some-app-ext-data" + + let req = Alamofire.request(URL(string: "http://example.com:8000/resource/1?b=1&a=2")!) + let expected = "hawk.1.header\n" + + "1353832234\n" + + "j4h3g2\n" + + "GET\n" + + "/resource/1?b=1&a=2\n" + + "example.com\n" + + "8000\n" + + "\n" + + "some-app-ext-data\n" + XCTAssertEqual(HawkHelper.getRequestStringFor(req.request!, timestampString: String(timestamp), nonce: nonce, hash: "", extra: extra).components(separatedBy: "\n"), + expected.components(separatedBy: "\n")) + } + + func testSpecWithoutPayloadExample() { + let helper = HawkHelper(id: "dh37fgj492je", + key: "werxhqb98rpaxn39848xrunpaw3489ruxnpa98w4rxn".utf8EncodedData) + let req = Alamofire.request(URL(string: "http://example.com:8000/resource/1?b=1&a=2")!) + let timestamp = Int64(1353832234) + let nonce = "j4h3g2" + let extra = "some-app-ext-data" + let value = helper.getAuthorizationValueFor(req.request!, at: timestamp, nonce: nonce, extra: extra) + let expected = "Hawk id=\"dh37fgj492je\", ts=\"1353832234\", nonce=\"j4h3g2\", ext=\"some-app-ext-data\", mac=\"6R4rV5iE+NPoym+WwjeHzjAGXUtLNIxmo1vpMofpLAE=\"" + XCTAssertEqual(value, expected) + } + + func testSpecWithPayloadExample() { + let helper = HawkHelper(id: "dh37fgj492je", + key: "werxhqb98rpaxn39848xrunpaw3489ruxnpa98w4rxn".utf8EncodedData) + let body = "Thank you for flying Hawk" + + let req = Alamofire.request("http://example.com:8000/resource/1?b=1&a=2", method: .post, parameters: [:], encoding: URF8BodyEncoding(body: body)) + let timestamp = Int64(1353832234) + let nonce = "j4h3g2" + let extra = "some-app-ext-data" + let value = helper.getAuthorizationValueFor(req.request!, at: timestamp, nonce: nonce, extra: extra) + let expected = "Hawk id=\"dh37fgj492je\", ts=\"1353832234\", nonce=\"j4h3g2\", hash=\"Yi9LfIIFRtBEPt74PVmbTF/xVAwPn7ub15ePICfgnuY=\", ext=\"some-app-ext-data\", mac=\"aSe1DERmZuRl3pI36/9BdZmnErTw3sNzOOAUlfeKjVw=\"" + XCTAssertEqual(value, expected) + } + + func testGetBaseContentType() { + XCTAssertEqual("text/plain", HawkHelper.getBaseContentTypeFor("text/plain")) + XCTAssertEqual("text/plain", HawkHelper.getBaseContentTypeFor("text/plain;one")) + XCTAssertEqual("text/plain", HawkHelper.getBaseContentTypeFor("text/plain;one;two")) + XCTAssertEqual("text/html", HawkHelper.getBaseContentTypeFor("text/html;charset=UTF-8")) + XCTAssertEqual("text/html", HawkHelper.getBaseContentTypeFor("text/html; charset=UTF-8")) + XCTAssertEqual("text/html", HawkHelper.getBaseContentTypeFor("text/html ;charset=UTF-8")) + } +} + +struct URF8BodyEncoding: ParameterEncoding { + var body: String + func encode(_ urlRequest: URLRequestConvertible, with parameters: Parameters?) throws -> URLRequest { + var mutableRequest = urlRequest.urlRequest + mutableRequest?.httpBody = body.utf8EncodedData + return mutableRequest! + } +} diff --git a/mobile/ios/AccountTests/Info.plist b/mobile/ios/AccountTests/Info.plist new file mode 100644 index 0000000000..9411e4a06e --- /dev/null +++ b/mobile/ios/AccountTests/Info.plist @@ -0,0 +1,24 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + BNDL + CFBundleShortVersionString + 10.6 + CFBundleSignature + ???? + CFBundleVersion + 1 + + diff --git a/mobile/ios/AccountTests/LiveAccountTest.swift b/mobile/ios/AccountTests/LiveAccountTest.swift new file mode 100644 index 0000000000..a1d389a8a2 --- /dev/null +++ b/mobile/ios/AccountTests/LiveAccountTest.swift @@ -0,0 +1,158 @@ +/* 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/. */ + +@testable import Account +import Foundation +import FxA +import Shared +import Deferred +import SwiftyJSON + +import XCTest + +// Note: All live account tests have been disabled. Please see https://bugzilla.mozilla.org/show_bug.cgi?id=1332028. + +/* + * A base test type for tests that need a live Firefox Account. + */ +open class LiveAccountTest: XCTestCase { + lazy var signedInUser: JSON? = { + if let path = Bundle(for: type(of: self)).path(forResource: "signedInUser.json", ofType: nil) { + if let contents = try? String(contentsOfFile: path, encoding: String.Encoding.utf8) { + let json = JSON(parseJSON: contents) + if json.isError() { + return nil + } + if let email = json["email"].string { + return json + } else { + // This is the standard case: signedInUser.json is {}. + return nil + } + } + } + XCTFail("Expected to read signedInUser.json!") + return nil + }() + + // It's not easy to have an optional resource, so we always include signedInUser.json in the test bundle. + // If signedInUser.json contains an email address, we use that email address. + // Since there's no way to get the corresponding password (from any client!), we assume that any + // test account has password identical to its email address. + fileprivate func withExistingAccount(_ mustBeVerified: Bool, completion: (Data, Data) -> Void) { + // If we don't create at least one expectation, waitForExpectations fails. + // So we unconditionally create one, even though the callback may not execute. + self.expectation(description: "withExistingAccount").fulfill() + if let json = self.signedInUser { + if mustBeVerified { + XCTAssertTrue(json["verified"].bool ?? false) + } + let email = json["email"].stringValue + let password = json["password"].stringValue + let emailUTF8 = email.utf8EncodedData + let passwordUT8 = password.utf8EncodedData + let stretchedPW = FxAClient10.quickStretchPW(emailUTF8, password: passwordUT8) + completion(emailUTF8, stretchedPW) + } else { + // This is the standard case: signedInUser.json is {}. + NSLog("Skipping test because signedInUser.json does not include email address.") + } + } + + func withVerifiedAccount(_ completion: (Data, Data) -> Void) { + withExistingAccount(true, completion: completion) + } + + // Helper function that waits for expectations to clear + func withVerifiedAccountNoExpectations(_ completion: (Data, Data) -> Void) { + withExistingAccount(true, completion: completion) + self.waitForExpectations(timeout: 10, handler: nil) + } + + func withCertificate(_ completion: @escaping (XCTestExpectation, Data, KeyPair, String) -> Void) { + withVerifiedAccount { emailUTF8, quickStretchedPW in + let expectation = self.expectation(description: "withCertificate") + + let keyPair = RSAKeyPair.generate(withModulusSize: 1024)! + let client = FxAClient10() + let login: Deferred> = client.login(emailUTF8, quickStretchedPW: quickStretchedPW, getKeys: true) + let sign: Deferred> = login.bind { (result: Maybe) in + switch result { + case let .failure(error): + expectation.fulfill() + return Deferred(value: .failure(error)) + case let .success(loginResponse): + return client.sign(loginResponse.value.sessionToken, publicKey: keyPair.publicKey) + } + } + sign.upon { result in + if let response = result.successValue { + XCTAssertNotNil(response.certificate) + completion(expectation, emailUTF8, keyPair, response.certificate) + } else { + XCTAssertEqual(result.failureValue!.description, "") + expectation.fulfill() + } + } + } + } + + public enum AccountError: MaybeErrorType { + case badParameters + case noSignedInUser + case unverifiedSignedInUser + + public var description: String { + switch self { + case .badParameters: return "Bad account parameters (email, password, or a derivative thereof)." + case .noSignedInUser: return "No signedInUser.json (missing, no email, etc)." + case .unverifiedSignedInUser: return "signedInUser.json describes an unverified account." + } + } + } + + // Internal helper. + func account(_ email: String, password: String, configuration: FirefoxAccountConfiguration) -> Deferred> { + let client = FxAClient10(authEndpoint: configuration.authEndpointURL) + let emailUTF8 = email.utf8EncodedData + let passwordUTF8 = password.utf8EncodedData + let quickStretchedPW = FxAClient10.quickStretchPW(emailUTF8, password: passwordUTF8) + let login = client.login(emailUTF8, quickStretchedPW: quickStretchedPW, getKeys: true) + return login.bind { result in + if let response = result.successValue { + let unwrapkB = FxAClient10.computeUnwrapKey(quickStretchedPW) + return Deferred(value: Maybe(success: FirefoxAccount.from(configuration, andLoginResponse: response, unwrapkB: unwrapkB))) + } else { + return Deferred(value: Maybe(failure: result.failureValue!)) + } + } + } + + func getTestAccount() -> Deferred> { + // TODO: Use signedInUser.json here. It's hard to include the same resource file in two Xcode targets. + return self.account("998797987.sync@restmail.net", password: "998797987.sync@restmail.net", + configuration: ProductionFirefoxAccountConfiguration()) + } + + open func getAuthState(_ now: Timestamp) -> Deferred> { + let account = self.getTestAccount() + print("Got test account.") + return account.map { result in + print("Result was successful? \(result.isSuccess)") + if let account = result.successValue { + return Maybe(success: account.syncAuthState) + } + return Maybe(failure: result.failureValue!) + } + } + + open func syncAuthState(_ now: Timestamp) -> Deferred> { + return getAuthState(now).bind { result in + if let authState = result.successValue { + return authState.token(now, canBeExpired: false) + } + return Deferred(value: Maybe(failure: result.failureValue!)) + } + } +} diff --git a/mobile/ios/AccountTests/SyncAuthStateTests.swift b/mobile/ios/AccountTests/SyncAuthStateTests.swift new file mode 100644 index 0000000000..a441c98a03 --- /dev/null +++ b/mobile/ios/AccountTests/SyncAuthStateTests.swift @@ -0,0 +1,32 @@ +/* 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/. */ + +@testable import Account +import Foundation +import FxA +import Shared +import UIKit + +import XCTest + +class SyncAuthStateTests: LiveAccountTest { + func testLive() { + let e = self.expectation(description: "Wait for token.") + syncAuthState(Date.now()).upon { result in + if let (token, forKey) = result.successValue { + let uidString = NSNumber(value: token.uid).stringValue + XCTAssertTrue(token.api_endpoint.endsWith(uidString)) + XCTAssertNotNil(forKey) + } else { + if let error = result.failureValue as? AccountError { + XCTAssertEqual(error, AccountError.noSignedInUser) + } else { + XCTAssertEqual(result.failureValue!.description, "") + } + } + e.fulfill() + } + self.waitForExpectations(timeout: 10, handler: nil) + } +} diff --git a/mobile/ios/AccountTests/TokenServerClientTests.swift b/mobile/ios/AccountTests/TokenServerClientTests.swift new file mode 100644 index 0000000000..846758d2af --- /dev/null +++ b/mobile/ios/AccountTests/TokenServerClientTests.swift @@ -0,0 +1,112 @@ +/* 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/. */ + +@testable import Account +import Foundation +import FxA +import Shared +import UIKit + +import XCTest + +// Testing client state is so delicate that I'm not going to test this. The test below does two +// requests; we would need a third, and a guarantee of the server state, to test this completely. +// The rule is: if you turn up with a never-before-seen client state; you win. If you turn up with +// a seen-before client state, you lose. +class TokenServerClientTests: LiveAccountTest { + func testErrorOutput() { + // Make sure we don't hide error details. + let error = NSError(domain: "test", code: 123, userInfo: nil) + XCTAssertEqual( + "", + TokenServerError.local(error).description) + } + + func testAudienceForEndpoint() { + func audienceFor(_ endpoint: String) -> String { + return TokenServerClient.getAudience(forURL: URL(string: endpoint)!) + } + + // Sub-domains and path components. + XCTAssertEqual("http://sub.test.com", audienceFor("http://sub.test.com")) + XCTAssertEqual("http://test.com", audienceFor("http://test.com/")) + XCTAssertEqual("http://test.com", audienceFor("http://test.com/path/component")) + XCTAssertEqual("http://test.com", audienceFor("http://test.com/path/component/")) + + // No port and default port. + XCTAssertEqual("http://test.com", audienceFor("http://test.com")) + XCTAssertEqual("http://test.com:80", audienceFor("http://test.com:80")) + + XCTAssertEqual("https://test.com", audienceFor("https://test.com")) + XCTAssertEqual("https://test.com:443", audienceFor("https://test.com:443")) + + // Ports that are the default ports for a different scheme. + XCTAssertEqual("https://test.com:80", audienceFor("https://test.com:80")) + XCTAssertEqual("http://test.com:443", audienceFor("http://test.com:443")) + + // Arbitrary ports. + XCTAssertEqual("http://test.com:8080", audienceFor("http://test.com:8080")) + XCTAssertEqual("https://test.com:4430", audienceFor("https://test.com:4430")) + } + + func testTokenSuccess() { + let audience = TokenServerClient.getAudience(forURL: ProductionSync15Configuration().tokenServerEndpointURL) + + withCertificate { expectation, emailUTF8, keyPair, certificate in + let assertion = JSONWebTokenUtils.createAssertionWithPrivateKeyToSign(with: keyPair.privateKey, + certificate: certificate, audience: audience) + + let client = TokenServerClient() + client.token(assertion!).upon { result in + if let token = result.successValue { + XCTAssertNotNil(token.id) + XCTAssertNotNil(token.key) + XCTAssertNotNil(token.api_endpoint) + XCTAssertNotNil(token.hashedFxAUID) + XCTAssertTrue(token.uid >= 0) + XCTAssertTrue(token.api_endpoint.hasSuffix(String(token.uid))) + let expectedRemoteTimestamp: Timestamp = 1429121686000 + XCTAssertTrue(token.remoteTimestamp >= expectedRemoteTimestamp) // Not a special timestamp; just a sanity check. + } else { + XCTAssertEqual(result.failureValue!.description, "") + } + expectation.fulfill() + } + } + self.waitForExpectations(timeout: 100, handler: nil) + } + + func testTokenFailure() { + withVerifiedAccount { _, _ in + // Account details aren't used, but we want to skip when we're not running live tests. + let e = self.expectation(description: "") + + let assertion = "BAD ASSERTION" + + let client = TokenServerClient() + client.token(assertion).upon { result in + if let token = result.successValue { + XCTFail("Got token: \(token)") + } else { + if let error = result.failureValue as? TokenServerError { + switch error { + case let .remote(code, status, remoteTimestamp): + XCTAssertEqual(code, Int32(401)) // Bad auth. + XCTAssertEqual(status!, "error") + XCTAssertFalse(remoteTimestamp == nil) + let expectedRemoteTimestamp: Timestamp = 1429121686000 + XCTAssertTrue(remoteTimestamp! >= expectedRemoteTimestamp) // Not a special timestamp; just a sanity check. + case let .local(error): + XCTAssertNil(error) + } + } else { + XCTFail("Expected TokenServerError") + } + } + e.fulfill() + } + } + self.waitForExpectations(timeout: 10, handler: nil) + } +} diff --git a/mobile/ios/Cartfile b/mobile/ios/Cartfile new file mode 100644 index 0000000000..92985f159f --- /dev/null +++ b/mobile/ios/Cartfile @@ -0,0 +1,18 @@ +github "getsentry/sentry-cocoa" "3.11.1" +github "Alamofire/Alamofire" ~> 4.0 +github "sleroux/Deferred" "Swift3.0" +github "SnapKit/SnapKit" "3.1.2" +github "rs/SDWebImage" "4.1.0" +github "swisspol/GCDWebServer" "3.3.2" +github "kif-framework/KIF" "v3.6.0" +github "adjust/ios_sdk" "v4.11.5" +github "AgileBits/onepassword-extension" "new/carthage+ios10" +github "mozilla/readability" "master" +github "jrendel/SwiftKeychainWrapper" "3.0.1" +github "DaveWoodCom/XCGLogger" "Version_4.0.0" +github "cezheng/Fuzi" "1.0.1" +github "SwiftyJSON/SwiftyJSON" "3.1.4" +github "farhanpatel/JSONSchema.swift" "master" +github "google/EarlGrey" "1.12.1" +github "jhugman/SwiftRouter" "master" +github "mozilla-mobile/telemetry-ios" "v1.0.10" diff --git a/mobile/ios/Cartfile.resolved b/mobile/ios/Cartfile.resolved new file mode 100644 index 0000000000..abc624f3de --- /dev/null +++ b/mobile/ios/Cartfile.resolved @@ -0,0 +1,18 @@ +github "getsentry/sentry-cocoa" "3.11.1" +github "Alamofire/Alamofire" "4.3.0" +github "sleroux/Deferred" "35b8927c1b94ce074e10793c57e1f80d0e2227fa" +github "cezheng/Fuzi" "1.0.1" +github "swisspol/GCDWebServer" "3.3.2" +github "kif-framework/KIF" "v3.6.0" +github "SnapKit/SnapKit" "3.1.2" +github "jrendel/SwiftKeychainWrapper" "3.0.1" +github "DaveWoodCom/XCGLogger" "Version_4.0.0" +github "adjust/ios_sdk" "v4.11.5" +github "AgileBits/onepassword-extension" "a614e290396346e3cb69e4951656eb8033390f8c" +github "mozilla/readability" "ccc8e9bf4c5400814d9b7a3ea83c21540da4c76f" +github "SwiftyJSON/SwiftyJSON" "3.1.4" +github "farhanpatel/JSONSchema.swift" "1c052b83baa8c497e12cde6a8afca0f54574612f" +github "google/EarlGrey" "1.12.1" +github "jhugman/SwiftRouter" "7b446fd063846ce2961b7518bb2386ffcbcc94d8" +github "rs/SDWebImage" "4.1.0" +github "mozilla-mobile/telemetry-ios" "v1.0.10" diff --git a/mobile/ios/Client-Bridging-Header.h b/mobile/ios/Client-Bridging-Header.h new file mode 100644 index 0000000000..485e3abdca --- /dev/null +++ b/mobile/ios/Client-Bridging-Header.h @@ -0,0 +1,18 @@ +#ifndef Client_Client_Bridging_Header_h +#define Client_Client_Bridging_Header_h + +#import +#import + +#import "FSReadingList.h" +#import "Try.h" + +#import "ThirdParty/UIImageViewAligned/UIImageViewAligned/UIImageViewAligned.h" +#import "ThirdParty/Apple/UIImage+ImageEffects.h" + +#import + +#import "Shared-Bridging-Header.h" +#import "Storage-Bridging-Header.h" + +#endif diff --git a/mobile/ios/Client.xcodeproj/project.pbxproj b/mobile/ios/Client.xcodeproj/project.pbxproj new file mode 100644 index 0000000000..f27661167a --- /dev/null +++ b/mobile/ios/Client.xcodeproj/project.pbxproj @@ -0,0 +1,9468 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + 03CCC9181AF05E7300DBF30D /* RelativeDatesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03CCC9171AF05E7300DBF30D /* RelativeDatesTests.swift */; }; + 0B1C05D71A798B1F004C78B0 /* UIImageViewAligned.m in Sources */ = {isa = PBXBuildFile; fileRef = 0B1C05D61A798B1F004C78B0 /* UIImageViewAligned.m */; }; + 0B21E8061E26CCB7000C8779 /* EarlGrey.framework in CopyFiles */ = {isa = PBXBuildFile; fileRef = 0B21E8051E26CCB7000C8779 /* EarlGrey.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; + 0B305E1B1E3A98A900BE0767 /* BookmarkingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B305E1A1E3A98A900BE0767 /* BookmarkingTests.swift */; }; + 0B3D670E1E09B90B00C1EFC7 /* AuthenticationTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B3D670D1E09B90B00C1EFC7 /* AuthenticationTest.swift */; }; + 0B3E7D951B27A7CE00E2E84D /* AboutHomeHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B3E7D931B27A7CE00E2E84D /* AboutHomeHandler.swift */; }; + 0B54BD191B698B7C004C822C /* SuggestedSites.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B54BD181B698B7C004C822C /* SuggestedSites.swift */; }; + 0B5A93221B1EB4C8004F47A2 /* ReadingListTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B5A93211B1EB4C8004F47A2 /* ReadingListTest.swift */; }; + 0B5A93421B1EB572004F47A2 /* readablePage.html in Resources */ = {isa = PBXBuildFile; fileRef = 0B5A93411B1EB572004F47A2 /* readablePage.html */; }; + 0B62EFD21AD63CD100ACB9CD /* Clearables.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B62EFD11AD63CD100ACB9CD /* Clearables.swift */; }; + 0B6FBAB21AC1F830007EC669 /* numberedPage.html in Resources */ = {isa = PBXBuildFile; fileRef = 0B6FBAB11AC1F830007EC669 /* numberedPage.html */; }; + 0B729D371E047D6A008E6859 /* HomePageSettingsTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B729D361E047D6A008E6859 /* HomePageSettingsTest.swift */; }; + 0B742CCF1B32493E00EE9264 /* libsqlcipher.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 0B742CCC1B32491400EE9264 /* libsqlcipher.a */; }; + 0B75AEA91AC20FB20015E5DC /* ImageIO.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0B8E0FF31A932BD500161DC3 /* ImageIO.framework */; }; + 0B75AEAA1AC20FBC0015E5DC /* ImageIO.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0B8E0FF31A932BD500161DC3 /* ImageIO.framework */; }; + 0B7C1E951F6097AD006A8869 /* TrackingProtectionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B7C1E941F6097AD006A8869 /* TrackingProtectionTests.swift */; }; + 0B8E0FF41A932BD500161DC3 /* ImageIO.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0B8E0FF31A932BD500161DC3 /* ImageIO.framework */; }; + 0BA1E00E1B03FB0B007675AF /* NetError.html in Resources */ = {isa = PBXBuildFile; fileRef = 0BA1E00D1B03FB0B007675AF /* NetError.html */; }; + 0BA1E02E1B046F1E007675AF /* ErrorPageHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0BA1E02D1B046F1E007675AF /* ErrorPageHelper.swift */; }; + 0BA1E0301B051A07007675AF /* NetError.css in Resources */ = {isa = PBXBuildFile; fileRef = 0BA1E02F1B051A07007675AF /* NetError.css */; }; + 0BA8964B1A250E6500C1010C /* ProfileTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0BA896491A250E6500C1010C /* ProfileTest.swift */; }; + 0BA8964C1A250E6500C1010C /* TestBookmarks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0BA8964A1A250E6500C1010C /* TestBookmarks.swift */; }; + 0BB5B2881AC0A2B90052877D /* SnackBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0BB5B2861AC0A2B90052877D /* SnackBar.swift */; }; + 0BB5B2891AC0A2B90052877D /* Toolbar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0BB5B2871AC0A2B90052877D /* Toolbar.swift */; }; + 0BB5B30B1AC0AD1F0052877D /* LoginsHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0BB5B30A1AC0AD1F0052877D /* LoginsHelper.swift */; }; + 0BC9C9C41F26F54D000E8AB5 /* SiteLoadTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0BC9C9C31F26F54D000E8AB5 /* SiteLoadTest.swift */; }; + 0BD19A671A25309B0084FBA7 /* NSUserDefaultsPrefs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0BD19A661A25309B0084FBA7 /* NSUserDefaultsPrefs.swift */; }; + 0BDA568B1B26B1BE008C9B96 /* LoginsHelper.js in Resources */ = {isa = PBXBuildFile; fileRef = 0BDA568A1B26B1BE008C9B96 /* LoginsHelper.js */; }; + 0BDA56B01B26B1D5008C9B96 /* TestLogins.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0BDA56AE1B26B1D5008C9B96 /* TestLogins.swift */; }; + 0BDA56B21B26B1E4008C9B96 /* Logins.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0BDA56B11B26B1E4008C9B96 /* Logins.swift */; }; + 0BDA56B41B26B203008C9B96 /* SQLiteLogins.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0BDA56B31B26B203008C9B96 /* SQLiteLogins.swift */; }; + 0BEF44631E31165700187C32 /* EarlGrey.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0BEF44621E31165700187C32 /* EarlGrey.swift */; }; + 0BF0DB4A1E57B05E009172B0 /* LaunchArguments.swift in Sources */ = {isa = PBXBuildFile; fileRef = E65075861E37F7AB006961AC /* LaunchArguments.swift */; }; + 0BF0DB941A8545800039F300 /* URLBarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0BF0DB931A8545800039F300 /* URLBarView.swift */; }; + 0BF1B7E31AC60DEA00A7B407 /* InsetButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0BF1B7E21AC60DEA00A7B407 /* InsetButton.swift */; }; + 0BF42D371A7C0B8E00889E28 /* FaviconManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0BF42D361A7C0B8E00889E28 /* FaviconManager.swift */; }; + 0BF42D391A7C0E8900889E28 /* Favicons.js in Resources */ = {isa = PBXBuildFile; fileRef = 0BF42D381A7C0E8900889E28 /* Favicons.js */; }; + 0BF42D4F1A7CD09600889E28 /* TestFavicons.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0BF42D4E1A7CD09600889E28 /* TestFavicons.swift */; }; + 0BF8F8DA1AEFF1C900E90BC2 /* noTitle.html in Resources */ = {isa = PBXBuildFile; fileRef = 0BF8F8D91AEFF1C900E90BC2 /* noTitle.html */; }; + 19DE1F671EC13B6400428B8C /* LeanplumIntegration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19DE1F661EC13B6400428B8C /* LeanplumIntegration.swift */; }; + 2805F5A61B8BCA7A00268FD1 /* ReadingList.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E4D567181ADECE2700F1EFE7 /* ReadingList.framework */; }; + 28078A471B1507EF0053B46A /* ReadingList.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E4D567181ADECE2700F1EFE7 /* ReadingList.framework */; }; + 280CB1371C20E0A3005AF147 /* BookmarksSynchronizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 280CB1361C20E0A3005AF147 /* BookmarksSynchronizer.swift */; }; + 28126F481C2F948E006466CC /* SQLiteBookmarksHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28126F471C2F948E006466CC /* SQLiteBookmarksHelpers.swift */; }; + 28126F6E1C2F94F9006466CC /* SQLiteBookmarksModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28126F6D1C2F94F9006466CC /* SQLiteBookmarksModel.swift */; }; + 28126F741C2F96F1006466CC /* SQLiteBookmarksResetting.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28126F731C2F96F1006466CC /* SQLiteBookmarksResetting.swift */; }; + 28126F771C2F9833006466CC /* SQLiteBookmarksBase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28126F761C2F9833006466CC /* SQLiteBookmarksBase.swift */; }; + 2816F0001B33E05400522243 /* UIConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2816EFFF1B33E05400522243 /* UIConstants.swift */; }; + 281B029A1C037C1F005202C3 /* TestBrowserDB.swift in Sources */ = {isa = PBXBuildFile; fileRef = 281B02991C037C1F005202C3 /* TestBrowserDB.swift */; }; + 281B2BEA1ADF4D90002917DC /* MockProfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 281B2BE91ADF4D90002917DC /* MockProfile.swift */; }; + 282731631ABC9BE600AA1954 /* Sync-Bridging-Header.h in Headers */ = {isa = PBXBuildFile; fileRef = 282731621ABC9BE600AA1954 /* Sync-Bridging-Header.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 282731691ABC9BE700AA1954 /* Sync.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2827315E1ABC9BE600AA1954 /* Sync.framework */; }; + 282731751ABC9BE700AA1954 /* Sync.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2827315E1ABC9BE600AA1954 /* Sync.framework */; }; + 282731991ABC9C2F00AA1954 /* ClientPayload.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28CE83BB1A1D1D3200576538 /* ClientPayload.swift */; }; + 2827319A1ABC9C2F00AA1954 /* KeyBundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28CE83BC1A1D1D3200576538 /* KeyBundle.swift */; }; + 2827319B1ABC9C2F00AA1954 /* KeysPayload.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28CE83BD1A1D1D3200576538 /* KeysPayload.swift */; }; + 2827319C1ABC9C2F00AA1954 /* Record.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28CE83BE1A1D1D3200576538 /* Record.swift */; }; + 2827319D1ABC9C2F00AA1954 /* SyncMeta.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28CE83BF1A1D1D3200576538 /* SyncMeta.swift */; }; + 2827319E1ABC9C5900AA1954 /* RecordTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28C0779D1A3B066000834FE5 /* RecordTests.swift */; }; + 28293BB51B333EA2004A65EC /* ReadingList.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E4D567181ADECE2700F1EFE7 /* ReadingList.framework */; }; + 2829D37A1C2F0A7F00DCF931 /* BookmarksModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2829D3791C2F0A7F00DCF931 /* BookmarksModel.swift */; }; + 2829D3A01C2F0AD400DCF931 /* Sharing.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2829D39F1C2F0AD400DCF931 /* Sharing.swift */; }; + 282DA4731A68C1E700A406E2 /* OpenSearch.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3FA77831A43B2CE0010CD32 /* OpenSearch.swift */; }; + 28302E401AF0747800521E2E /* DatabaseError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28302E3F1AF0747800521E2E /* DatabaseError.swift */; }; + 283586FD1C73F18E00A55435 /* CachingItemSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 283586FC1C73F18E00A55435 /* CachingItemSource.swift */; }; + 2852B8441C51996B00591EAC /* Trees.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2852B8431C51996B00591EAC /* Trees.swift */; }; + 28532BE91C471FFB000072D9 /* ResultTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FEBABAE1AB3659000DB5728 /* ResultTests.swift */; }; + 28532BEA1C472008000072D9 /* DeferredTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28786E541AB0F5FA009EA9EF /* DeferredTests.swift */; }; + 28532BEB1C472015000072D9 /* UtilsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28A6CE891AC082E200C1A2D4 /* UtilsTests.swift */; }; + 28532CC11C473977000072D9 /* MockFiles.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FCAE2791ABB533A00877008 /* MockFiles.swift */; }; + 28532CE61C48098D000072D9 /* ThreeWayTreeMerger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28532CE51C48098D000072D9 /* ThreeWayTreeMerger.swift */; }; + 28532D321C483E3D000072D9 /* CompletionOps.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28532D311C483E3D000072D9 /* CompletionOps.swift */; }; + 2853C5411AD84C6800C4F31D /* TabsPayload.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2853C5401AD84C6800C4F31D /* TabsPayload.swift */; }; + 2855611F1AEFFA1C00D5ED5B /* HistorySynchronizerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2855611E1AEFFA1C00D5ED5B /* HistorySynchronizerTests.swift */; }; + 285D37E31ABCA69E000E1CF9 /* CryptoTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28C077971A3B064000834FE5 /* CryptoTests.swift */; }; + 285D3B681B4380B70035FD22 /* Queue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 285D3B671B4380B70035FD22 /* Queue.swift */; }; + 285D3B901B4386520035FD22 /* SQLiteQueue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 285D3B8F1B4386520035FD22 /* SQLiteQueue.swift */; }; + 285F2DC11AF80B4600211843 /* SQLiteBookmarksSyncing.swift in Sources */ = {isa = PBXBuildFile; fileRef = 285F2DC01AF80B4600211843 /* SQLiteBookmarksSyncing.swift */; }; + 2868FA061ADF7B69000D9B1D /* Sync.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2827315E1ABC9BE600AA1954 /* Sync.framework */; }; + 2868FA231ADF7B70000D9B1D /* Sync.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2827315E1ABC9BE600AA1954 /* Sync.framework */; }; + 287AC8661AF4776D00101515 /* TestSQLiteHistory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FCAE27D1ABB533A00877008 /* TestSQLiteHistory.swift */; }; + 288501DE1AC0F61D00E7F670 /* RequestExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 288501DD1AC0F61D00E7F670 /* RequestExtensions.swift */; }; + 2885023F1AC117A500E7F670 /* SyncStateMachine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2885023E1AC117A500E7F670 /* SyncStateMachine.swift */; }; + 288A2D9D1AB8B3260023ABC3 /* Shared.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 288A2D861AB8B3260023ABC3 /* Shared.framework */; }; + 288A2DAA1AB8B3700023ABC3 /* Box.swift in Sources */ = {isa = PBXBuildFile; fileRef = 288A2DA71AB8B3700023ABC3 /* Box.swift */; }; + 288A2DAB1AB8B3700023ABC3 /* BoxType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 288A2DA81AB8B3700023ABC3 /* BoxType.swift */; }; + 288A2DAC1AB8B3700023ABC3 /* MutableBox.swift in Sources */ = {isa = PBXBuildFile; fileRef = 288A2DA91AB8B3700023ABC3 /* MutableBox.swift */; }; + 288A2DB51AB8B38D0023ABC3 /* Error.swift in Sources */ = {isa = PBXBuildFile; fileRef = 288A2DB31AB8B38D0023ABC3 /* Error.swift */; }; + 288A2DB61AB8B38D0023ABC3 /* Result.swift in Sources */ = {isa = PBXBuildFile; fileRef = 288A2DB41AB8B38D0023ABC3 /* Result.swift */; }; + 288E67C21B9E730900AE2C6A /* BookmarksDownloader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 288E67C11B9E730900AE2C6A /* BookmarksDownloader.swift */; }; + 2891F2CB1F991185001B105E /* v33.db in Resources */ = {isa = PBXBuildFile; fileRef = 2891F2BA1F991185001B105E /* v33.db */; }; + 28926B1A1AC0F074009C0B1D /* EncryptedJSON.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28926B191AC0F074009C0B1D /* EncryptedJSON.swift */; }; + 28926B371AC0F0DD009C0B1D /* EnvelopeJSON.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28926B361AC0F0DD009C0B1D /* EnvelopeJSON.swift */; }; + 28926B3A1AC0F128009C0B1D /* CleartextPayloadJSON.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28926B391AC0F128009C0B1D /* CleartextPayloadJSON.swift */; }; + 2894C1431AE89D8900F1F92F /* HistorySynchronizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2894C1421AE89D8900F1F92F /* HistorySynchronizer.swift */; }; + 2894C1661AE89DD200F1F92F /* TabsSynchronizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2894C1651AE89DD200F1F92F /* TabsSynchronizer.swift */; }; + 2894C1691AE89DDC00F1F92F /* ClientsSynchronizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2894C1681AE89DDC00F1F92F /* ClientsSynchronizer.swift */; }; + 2894C16C1AE89FD500F1F92F /* HistoryPayload.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2894C16B1AE89FD500F1F92F /* HistoryPayload.swift */; }; + 289A4C131C4EB90600A460E3 /* StorageTestUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 289A4C121C4EB90600A460E3 /* StorageTestUtils.swift */; }; + 289A4C141C4EB90600A460E3 /* StorageTestUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 289A4C121C4EB90600A460E3 /* StorageTestUtils.swift */; }; + 28A17B671BEC727500BC14ED /* Downloader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28A17B661BEC727500BC14ED /* Downloader.swift */; }; + 28AA941D1B97DCA800703DC6 /* BookmarkPayload.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28AA941C1B97DCA800703DC6 /* BookmarkPayload.swift */; }; + 28B62ACE1BC745E7004A585A /* Syncable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28B62ACD1BC745E7004A585A /* Syncable.swift */; }; + 28C28BFD1C51A3B900D5460E /* Merging.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28C28BFC1C51A3B900D5460E /* Merging.swift */; }; + 28C4AB721AD42D4300D9ACE3 /* Clients.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28C4AB711AD42D4300D9ACE3 /* Clients.swift */; }; + 28C8B7851C852535006D8318 /* BookmarksPanelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28C8B7841C852535006D8318 /* BookmarksPanelTests.swift */; }; + 28C8D11D1AD4CE8900F62011 /* Storage.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2FCAE21A1ABB51F800877008 /* Storage.framework */; }; + 28C8D1391AD4CE9100F62011 /* Shared.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 288A2D861AB8B3260023ABC3 /* Shared.framework */; }; + 28C8D13B1AD4CEB100F62011 /* Shared.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 288A2D861AB8B3260023ABC3 /* Shared.framework */; }; + 28C8D13C1AD4CEB400F62011 /* Storage.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2FCAE21A1ABB51F800877008 /* Storage.framework */; }; + 28CDA55C1A43C37C005C318C /* NSUserDefaultsPrefs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0BD19A661A25309B0084FBA7 /* NSUserDefaultsPrefs.swift */; }; + 28CDA5641A43C37D005C318C /* NSUserDefaultsPrefs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0BD19A661A25309B0084FBA7 /* NSUserDefaultsPrefs.swift */; }; + 28D158AD1AFD90E500F9C065 /* TestSQLiteBookmarks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28D158AC1AFD90E500F9C065 /* TestSQLiteBookmarks.swift */; }; + 28D52E2F1BCDF53900187A1D /* ResetTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28D52E081BCDF44100187A1D /* ResetTests.swift */; }; + 28D980231C47149000277055 /* TestBookmarkTreeMerging.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28D980221C47149000277055 /* TestBookmarkTreeMerging.swift */; }; + 28E08C991AF44EF9009BA2FA /* SQLiteHistory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FCAE2551ABB531100877008 /* SQLiteHistory.swift */; }; + 28E08C9A1AF44F00009BA2FA /* BrowserSchema.swift in Sources */ = {isa = PBXBuildFile; fileRef = 282915E51AF1A7920006EEB5 /* BrowserSchema.swift */; }; + 28E23C121AC5A5EE00F5AC85 /* State.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28E23C111AC5A5EE00F5AC85 /* State.swift */; }; + 28E91E751B443AD5009DF274 /* SyncConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28E91E741B443AD5009DF274 /* SyncConstants.swift */; }; + 28EADE5D1AFC3A78007FB2FB /* UIImageViewExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28EADE381AFC3898007FB2FB /* UIImageViewExtensions.swift */; }; + 28ECD97A1BA1EA2200D829DA /* MockSyncServer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28ECD9791BA1EA2200D829DA /* MockSyncServer.swift */; }; + 28ECD9BF1BA1F19900D829DA /* libz.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = E6231C001B90A44F005ABB0D /* libz.tbd */; }; + 28ECD9F41BA1F59800D829DA /* DownloadTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28ECD9F31BA1F59800D829DA /* DownloadTests.swift */; }; + 28ED02021B26123E003948B2 /* LoginPayload.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28ED02011B26123E003948B2 /* LoginPayload.swift */; }; + 28ED02261B262B5E003948B2 /* LoginsSynchronizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28ED02251B262B5E003948B2 /* LoginsSynchronizer.swift */; }; + 28ED02291B262E0A003948B2 /* IndependentRecordSynchronizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28ED02281B262E0A003948B2 /* IndependentRecordSynchronizer.swift */; }; + 28F596A11ACA13CA0071DDCC /* InfoTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28F596A01ACA13CA0071DDCC /* InfoTests.swift */; }; + 28F657EA1ABFCA7A00A608BD /* LiveAccountTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FA4363B1ABB8448008031D1 /* LiveAccountTest.swift */; }; + 28FDFF0C1C1F725800840F86 /* SeparatorTableCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28FDFF0B1C1F725800840F86 /* SeparatorTableCell.swift */; }; + 2C2A5EF41E68469500F02659 /* PrivateBrowsingTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C2A5EF31E68469500F02659 /* PrivateBrowsingTest.swift */; }; + 2C2A91291FA2410D002E36BD /* HistoryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C2A91281FA2410D002E36BD /* HistoryTests.swift */; }; + 2C31A7A91E8BFB2200DAC646 /* ReaderViewUITest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C31A7A81E8BFB2200DAC646 /* ReaderViewUITest.swift */; }; + 2C31A8471E8D447F00DAC646 /* HomePageSettingsUITest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C31A8461E8D447F00DAC646 /* HomePageSettingsUITest.swift */; }; + 2C3406C81E719F00000FD889 /* SettingsTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C3406C71E719F00000FD889 /* SettingsTest.swift */; }; + 2C8C07771E7800EA00DC1237 /* FindInPageTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C8C07761E7800EA00DC1237 /* FindInPageTest.swift */; }; + 2C97EC711E72C80E0092EC18 /* TopTabsTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C97EC701E72C80E0092EC18 /* TopTabsTest.swift */; }; + 2CA16FDE1E5F089100332277 /* SearchTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CA16FDD1E5F089100332277 /* SearchTest.swift */; }; + 2CB56E3F1E926BFB00AF7586 /* ToolbarTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CB56E3E1E926BFB00AF7586 /* ToolbarTest.swift */; }; + 2CC1B3F01E9B861400814EEC /* DomainAutocompleteTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CC1B3EF1E9B861400814EEC /* DomainAutocompleteTest.swift */; }; + 2CEA6F791E93E3A600D4100E /* SearchSettingsUITest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CEA6F781E93E3A600D4100E /* SearchSettingsUITest.swift */; }; + 2CF449A51E7BFE2C00FD7595 /* NavigationTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CF449A41E7BFE2C00FD7595 /* NavigationTest.swift */; }; + 2F13E79B1AC0C02700D75081 /* StringExtensionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F13E79A1AC0C02700D75081 /* StringExtensionsTests.swift */; }; + 2F14E13A1ABB890800FF98DB /* Account-Bridging-Header.h in Headers */ = {isa = PBXBuildFile; fileRef = 2F14E1391ABB890800FF98DB /* Account-Bridging-Header.h */; }; + 2F1A3DE11ABE3C90002F1E15 /* FxALoginStateMachine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F1A3DE01ABE3C90002F1E15 /* FxALoginStateMachine.swift */; }; + 2F1A3DFE1ABE3C9C002F1E15 /* FxALoginStateMachineTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F1A3DFD1ABE3C9C002F1E15 /* FxALoginStateMachineTests.swift */; }; + 2F3724C61ABF3C01007607FA /* LiveStorageClientTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F3724C41ABF3C01007607FA /* LiveStorageClientTests.swift */; }; + 2F3724C71ABF3C01007607FA /* StorageClientTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F3724C51ABF3C01007607FA /* StorageClientTests.swift */; }; + 2F3724E51ABF3C19007607FA /* Info.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F3724E21ABF3C19007607FA /* Info.swift */; }; + 2F3724E71ABF3C19007607FA /* StorageClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F3724E31ABF3C19007607FA /* StorageClient.swift */; }; + 2F3724E91ABF3C19007607FA /* Synchronizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F3724E41ABF3C19007607FA /* Synchronizer.swift */; }; + 2F44FA1B1A9D426A00FD20CC /* TestHashExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F44FA1A1A9D426A00FD20CC /* TestHashExtensions.swift */; }; + 2F44FB2C1A9D5D8500FD20CC /* Home.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = F84B22261A09127C00AAB793 /* Home.xcassets */; }; + 2F44FB2D1A9D5D8500FD20CC /* FiraSans-BoldItalic.ttf in Resources */ = {isa = PBXBuildFile; fileRef = E4B7B7421A793CF20022C5E0 /* FiraSans-BoldItalic.ttf */; }; + 2F44FC721A9E840300FD20CC /* SettingsNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F44FC711A9E840300FD20CC /* SettingsNavigationController.swift */; }; + 2F44FCC51A9E85E900FD20CC /* SettingsTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F44FCC41A9E85E900FD20CC /* SettingsTableViewController.swift */; }; + 2F44FCC71A9E8CF500FD20CC /* SearchSettingsTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F44FCC61A9E8CF500FD20CC /* SearchSettingsTableViewController.swift */; }; + 2F44FCCB1A9E972E00FD20CC /* SearchEnginePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F44FCCA1A9E972E00FD20CC /* SearchEnginePicker.swift */; }; + 2F67C5261BB0CB4E00E7B73A /* MetaGlobalTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F67C5251BB0CB4E00E7B73A /* MetaGlobalTests.swift */; }; + 2F697F7E1A9FD22D009E03AE /* SearchEnginesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F697F7D1A9FD22D009E03AE /* SearchEnginesTests.swift */; }; + 2F8C76571BC32F3C00D5E4E0 /* MockSyncServerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F8C76561BC32F3C00D5E4E0 /* MockSyncServerTests.swift */; }; + 2FA436061ABB83B4008031D1 /* Account.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2FA435FB1ABB83B4008031D1 /* Account.framework */; }; + 2FA436121ABB83B4008031D1 /* Account.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2FA435FB1ABB83B4008031D1 /* Account.framework */; }; + 2FA436291ABB8436008031D1 /* FirefoxAccount.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FA436221ABB8436008031D1 /* FirefoxAccount.swift */; }; + 2FA4362B1ABB8436008031D1 /* FirefoxAccountConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FA436231ABB8436008031D1 /* FirefoxAccountConfiguration.swift */; }; + 2FA4362F1ABB8436008031D1 /* FxAClient10.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FA436251ABB8436008031D1 /* FxAClient10.swift */; }; + 2FA436331ABB8436008031D1 /* HawkHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FA436271ABB8436008031D1 /* HawkHelper.swift */; }; + 2FA436351ABB8436008031D1 /* TokenServerClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FA436281ABB8436008031D1 /* TokenServerClient.swift */; }; + 2FA4363D1ABB8448008031D1 /* FirefoxAccountTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FA436371ABB8448008031D1 /* FirefoxAccountTests.swift */; }; + 2FA4363E1ABB8448008031D1 /* FxAClient10Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FA436381ABB8448008031D1 /* FxAClient10Tests.swift */; }; + 2FA436401ABB8448008031D1 /* HawkHelperTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FA4363A1ABB8448008031D1 /* HawkHelperTests.swift */; }; + 2FA436411ABB8448008031D1 /* LiveAccountTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FA4363B1ABB8448008031D1 /* LiveAccountTest.swift */; }; + 2FA436421ABB8448008031D1 /* TokenServerClientTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FA4363C1ABB8448008031D1 /* TokenServerClientTests.swift */; }; + 2FC244B31A855E05007CE41D /* FxA.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 28CE83D01A1D1D5100576538 /* FxA.framework */; }; + 2FC244B81A855E0D007CE41D /* FxA.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 28CE83D01A1D1D5100576538 /* FxA.framework */; }; + 2FCAE2251ABB51F800877008 /* Storage.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2FCAE21A1ABB51F800877008 /* Storage.framework */; }; + 2FCAE2311ABB51F800877008 /* Storage.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2FCAE21A1ABB51F800877008 /* Storage.framework */; }; + 2FCAE25D1ABB531100877008 /* Bookmarks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FCAE23F1ABB531100877008 /* Bookmarks.swift */; }; + 2FCAE25F1ABB531100877008 /* Cursor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FCAE2411ABB531100877008 /* Cursor.swift */; }; + 2FCAE2601ABB531100877008 /* Favicons.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FCAE2421ABB531100877008 /* Favicons.swift */; }; + 2FCAE2611ABB531100877008 /* FileAccessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FCAE2431ABB531100877008 /* FileAccessor.swift */; }; + 2FCAE2621ABB531100877008 /* History.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FCAE2441ABB531100877008 /* History.swift */; }; + 2FCAE2651ABB531100877008 /* RemoteTabs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FCAE2471ABB531100877008 /* RemoteTabs.swift */; }; + 2FCAE2661ABB531100877008 /* Site.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FCAE2481ABB531100877008 /* Site.swift */; }; + 2FCAE2681ABB531100877008 /* BrowserDB.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FCAE24B1ABB531100877008 /* BrowserDB.swift */; }; + 2FCAE2751ABB531100877008 /* SQLiteRemoteClientsAndTabs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FCAE2581ABB531100877008 /* SQLiteRemoteClientsAndTabs.swift */; }; + 2FCAE2771ABB531100877008 /* SwiftData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FCAE25B1ABB531100877008 /* SwiftData.swift */; }; + 2FCAE2781ABB531100877008 /* Visit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FCAE25C1ABB531100877008 /* Visit.swift */; }; + 2FCAE2841ABB533A00877008 /* MockFiles.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FCAE2791ABB533A00877008 /* MockFiles.swift */; }; + 2FCAE2851ABB533A00877008 /* TestSQLiteRemoteClientsAndTabs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FCAE27A1ABB533A00877008 /* TestSQLiteRemoteClientsAndTabs.swift */; }; + 2FCAE33E1ABB5F1800877008 /* Storage-Bridging-Header.h in Headers */ = {isa = PBXBuildFile; fileRef = 2FCAE33D1ABB5F1800877008 /* Storage-Bridging-Header.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 2FDB10931A9FBEC5006CF312 /* PrefsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FDB10921A9FBEC5006CF312 /* PrefsTests.swift */; }; + 2FDBCF611ABFC9DE00AFF7F0 /* SyncAuthState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FDBCF601ABFC9DE00AFF7F0 /* SyncAuthState.swift */; }; + 2FDBCF9B1AC0ADB500AFF7F0 /* SyncAuthStateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FDBCF9A1AC0ADB500AFF7F0 /* SyncAuthStateTests.swift */; }; + 2FDE87FE1ABB3817005317B1 /* RemoteTabsPanel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FDE87FD1ABB3817005317B1 /* RemoteTabsPanel.swift */; }; + 2FFC4D1B1ABE3C360081D675 /* FxAState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FFC4D1A1ABE3C360081D675 /* FxAState.swift */; }; + 2FFC4D381ABE3C420081D675 /* FxAStateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FFC4D371ABE3C420081D675 /* FxAStateTests.swift */; }; + 315D05561E58DD60001F349B /* UIPasteboardExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = A83E5AB61C1D993D0026D912 /* UIPasteboardExtensions.swift */; }; + 318FB6EB1DB5600D0004E40F /* SQLiteHistoryFactories.swift in Sources */ = {isa = PBXBuildFile; fileRef = 318FB6EA1DB5600D0004E40F /* SQLiteHistoryFactories.swift */; }; + 31ADB5DA1E58CEC300E87909 /* ClipboardBarDisplayHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31ADB5D91E58CEC300E87909 /* ClipboardBarDisplayHandler.swift */; }; + 3905274C1C874D35007E0BB7 /* NotificationCenter.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3905274B1C874D35007E0BB7 /* NotificationCenter.framework */; }; + 3905274F1C874D35007E0BB7 /* TodayViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3905274E1C874D35007E0BB7 /* TodayViewController.swift */; }; + 390527561C874D35007E0BB7 /* Today.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 3905274A1C874D35007E0BB7 /* Today.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; + 39098DC41CAD5ACB00AE87F3 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 391AEFD11C8F11ED00691F84 /* Images.xcassets */; }; + 391B4FFF1F9767F50094F841 /* FxScreenGraph.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39EB46981E26DDB4006346E8 /* FxScreenGraph.swift */; }; + 391B500E1F9768EA0094F841 /* ScreenGraph.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39EB46971E26DDB4006346E8 /* ScreenGraph.swift */; }; + 39236E721FCC600200A38F1B /* TabEventHandlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39236E711FCC600200A38F1B /* TabEventHandlerTests.swift */; }; + 392ED6B71D06E85E009D9B62 /* NewTabChoiceViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 392ED6B61D06E85E009D9B62 /* NewTabChoiceViewController.swift */; }; + 392ED7E41D0AEF56009D9B62 /* NewTabAccessors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 392ED7E31D0AEF56009D9B62 /* NewTabAccessors.swift */; }; + 392ED7E61D0AEFEF009D9B62 /* HomePageAccessors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 392ED7E51D0AEFEF009D9B62 /* HomePageAccessors.swift */; }; + 39409A3F1C90E68300DAE683 /* Shared.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 288A2D861AB8B3260023ABC3 /* Shared.framework */; }; + 3943A81D1E9807C700D4F6DC /* FxAPushMessageTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3943A81C1E9807C700D4F6DC /* FxAPushMessageTest.swift */; }; + 39455F771FC83F430088A22C /* TabEventHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39455F761FC83F430088A22C /* TabEventHandler.swift */; }; + 394CF6CF1BAA493C00906917 /* DefaultSuggestedSites.swift in Sources */ = {isa = PBXBuildFile; fileRef = 394CF6CE1BAA493C00906917 /* DefaultSuggestedSites.swift */; }; + 3964B09A1EA8F06F00F2EEF4 /* FeatureSwitch.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3964B0991EA8F06F00F2EEF4 /* FeatureSwitch.swift */; }; + 3964B09C1EA8F32C00F2EEF4 /* FeatureSwitchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3964B09B1EA8F32C00F2EEF4 /* FeatureSwitchTests.swift */; }; + 396E38CC1EE0816C00CC180F /* Profile.swift in Sources */ = {isa = PBXBuildFile; fileRef = D34DC84D1A16C40C00D49B7B /* Profile.swift */; }; + 396E38DD1EE081DA00CC180F /* SyncStatusResolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = E60D03171D511398002FE3F6 /* SyncStatusResolver.swift */; }; + 396E38E01EE0821B00CC180F /* NSUserDefaultsPrefs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0BD19A661A25309B0084FBA7 /* NSUserDefaultsPrefs.swift */; }; + 396E38E31EE083A400CC180F /* Shared.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 288A2D861AB8B3260023ABC3 /* Shared.framework */; settings = {ATTRIBUTES = (Weak, ); }; }; + 396E38E61EE0843500CC180F /* Storage.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2FCAE21A1ABB51F800877008 /* Storage.framework */; settings = {ATTRIBUTES = (Weak, ); }; }; + 396E38ED1EE0C63500CC180F /* Sync.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2827315E1ABC9BE600AA1954 /* Sync.framework */; settings = {ATTRIBUTES = (Weak, ); }; }; + 396E38EE1EE0C6ED00CC180F /* ExtensionProfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 396E38DB1EE0818800CC180F /* ExtensionProfile.swift */; }; + 396E38F11EE0C8EC00CC180F /* FxAPushMessageHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3905B4D41E8E7A6B0027D953 /* FxAPushMessageHandler.swift */; }; + 396E38F21EE0C8ED00CC180F /* FxAPushMessageHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3905B4D41E8E7A6B0027D953 /* FxAPushMessageHandler.swift */; }; + 396E38F31EE0C90D00CC180F /* FxALoginHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39F99FED1E3A71F800F353B4 /* FxALoginHelper.swift */; }; + 397848DE1ED86605004C0C0B /* NotificationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 397848DD1ED86605004C0C0B /* NotificationService.swift */; }; + 397848E21ED86605004C0C0B /* NotificationService.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 397848DB1ED86605004C0C0B /* NotificationService.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; + 39A359E41BFCCE94006B9E87 /* UserActivityHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39A359E31BFCCE94006B9E87 /* UserActivityHandler.swift */; }; + 39A35AED1C0662A3006B9E87 /* SpotlightHelper.js in Resources */ = {isa = PBXBuildFile; fileRef = 39A35AEC1C0662A3006B9E87 /* SpotlightHelper.js */; }; + 39AC591A1CC574AB0042C2F5 /* HomePageSettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39AC59191CC574AA0042C2F5 /* HomePageSettingsViewController.swift */; }; + 39C22C291E89791C000C0E56 /* PushCrypto.swift in Sources */ = {isa = PBXBuildFile; fileRef = 395C8F201E796AD600A68E8C /* PushCrypto.swift */; }; + 39C22C2A1E897926000C0E56 /* PushCryptoTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39B0647C1E7ADAC2000BE173 /* PushCryptoTests.swift */; }; + 39C22C2F1E897BA7000C0E56 /* LivePushClientTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39C22C2C1E897B9A000C0E56 /* LivePushClientTests.swift */; }; + 39D9E6851C89E9690071FADA /* SnapKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7B604FA11C495268006EEEC3 /* SnapKit.framework */; }; + 39DD030D1CD53E1900BC09B3 /* HomePageHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39DD030C1CD53E1900BC09B3 /* HomePageHelper.swift */; }; + 39E65D191CA455A900C63CE3 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 391AEFD11C8F11ED00691F84 /* Images.xcassets */; }; + 39E65D271CA5B92000C63CE3 /* AsyncReducerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39E65D261CA5B92000C63CE3 /* AsyncReducerTests.swift */; }; + 39EB46991E26DDB4006346E8 /* ScreenGraph.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39EB46971E26DDB4006346E8 /* ScreenGraph.swift */; }; + 39EB469A1E26DDB4006346E8 /* FxScreenGraph.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39EB46981E26DDB4006346E8 /* FxScreenGraph.swift */; }; + 39F819C61FD70F5D009E31E4 /* TabEventHandlers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39F819C51FD70F5D009E31E4 /* TabEventHandlers.swift */; }; + 39F99FE41E3A6F1700F353B4 /* PushClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39F99FD91E3A6DE300F353B4 /* PushClient.swift */; }; + 39F99FE51E3A6F1700F353B4 /* PushConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39F99FDA1E3A6DE300F353B4 /* PushConfiguration.swift */; }; + 39F99FE61E3A6F1700F353B4 /* PushRegistration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39F99FDB1E3A6DE300F353B4 /* PushRegistration.swift */; }; + 3B0943811D6CC4FC004F24E1 /* FilledPageControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B0943801D6CC4FC004F24E1 /* FilledPageControl.swift */; }; + 3B39EDBA1E16E18900EF029F /* CustomSearchEnginesTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B39EDB91E16E18900EF029F /* CustomSearchEnginesTest.swift */; }; + 3B39EDCB1E16E1AA00EF029F /* CustomSearchViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B39EDCA1E16E1AA00EF029F /* CustomSearchViewController.swift */; }; + 3B4262E51E538EC400CA681C /* MetadataHelper.js in Resources */ = {isa = PBXBuildFile; fileRef = 3B4262E21E538EC400CA681C /* MetadataHelper.js */; }; + 3B4262E61E538EC400CA681C /* MetadataParserHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B4262E31E538EC400CA681C /* MetadataParserHelper.swift */; }; + 3B4262E71E538EC400CA681C /* page-metadata-parser.js in Resources */ = {isa = PBXBuildFile; fileRef = 3B4262E41E538EC400CA681C /* page-metadata-parser.js */; }; + 3B43E3D31D95C48D00BBA9DB /* StoragePerfTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B43E3D21D95C48D00BBA9DB /* StoragePerfTests.swift */; }; + 3B4988CE1E42B01800A12FDA /* SwiftyJSON.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3B4988CD1E42B01800A12FDA /* SwiftyJSON.framework */; }; + 3B4AA24B1D8B8C4C00A2E008 /* ArrayExtensionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B4AA24A1D8B8C4C00A2E008 /* ArrayExtensionTests.swift */; }; + 3B546EC01D95ECAE00BDBE36 /* ActivityStreamTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B546EBF1D95ECAE00BDBE36 /* ActivityStreamTest.swift */; }; + 3B61CD491F2A74EF00D38DE1 /* PocketFeed.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B61CD481F2A74EF00D38DE1 /* PocketFeed.swift */; }; + 3B61CD591F2A750800D38DE1 /* PocketFeedTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B61CD581F2A750800D38DE1 /* PocketFeedTests.swift */; }; + 3B61CD631F2A769D00D38DE1 /* pocketglobalfeed.json in Resources */ = {isa = PBXBuildFile; fileRef = 3B61CD621F2A769D00D38DE1 /* pocketglobalfeed.json */; }; + 3B6889C51D66950E002AC85E /* UIImageColors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B6889C41D66950E002AC85E /* UIImageColors.swift */; }; + 3B6F40181DC7849C00656CC6 /* ActivityStreamTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B6F40171DC7849C00656CC6 /* ActivityStreamTests.swift */; }; + 3BA9A0231D2C208C00BD418C /* Fuzi.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3BA9A0221D2C208C00BD418C /* Fuzi.framework */; }; + 3BA9A0311D2C2C0400BD418C /* Fuzi.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3BA9A0221D2C208C00BD418C /* Fuzi.framework */; }; + 3BA9A0321D2C2C0500BD418C /* Fuzi.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3BA9A0221D2C208C00BD418C /* Fuzi.framework */; }; + 3BB50E111D6274CD004B33DF /* ActivityStreamTopSitesCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BB50E101D6274CD004B33DF /* ActivityStreamTopSitesCell.swift */; }; + 3BB50E201D627539004B33DF /* ActivityStreamPanel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BB50E1F1D627539004B33DF /* ActivityStreamPanel.swift */; }; + 3BB54B311E68EB2B0021DAC4 /* AuthenticationKeychainInfoTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BB54B301E68EB2B0021DAC4 /* AuthenticationKeychainInfoTests.swift */; }; + 3BC659491E5BA4AE006D560F /* TopSites in Resources */ = {isa = PBXBuildFile; fileRef = 3BC659481E5BA4AE006D560F /* TopSites */; }; + 3BC659591E5BA505006D560F /* top_sites.json in Resources */ = {isa = PBXBuildFile; fileRef = 3BC659581E5BA505006D560F /* top_sites.json */; }; + 3BCE6D3C1CEB9E4D0080928C /* ThirdPartySearchAlerts.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BCE6D3B1CEB9E4D0080928C /* ThirdPartySearchAlerts.swift */; }; + 3BE7274E1CCFE8A30099189F /* CustomSearchHelper.js in Resources */ = {isa = PBXBuildFile; fileRef = 3BE7274D1CCFE8A30099189F /* CustomSearchHelper.js */; }; + 3BE7275D1CCFE8B60099189F /* CustomSearchHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BE7275C1CCFE8B60099189F /* CustomSearchHandler.swift */; }; + 3BF4B8E91D38497A00493393 /* BaseTestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BF4B8E81D38497A00493393 /* BaseTestCase.swift */; }; + 3BF56D271CDBBE1F00AC4D75 /* SimpleToast.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BF56D261CDBBE1F00AC4D75 /* SimpleToast.swift */; }; + 3BFCBF201E04B1C50070C042 /* UIImageViewExtensionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BFCBF1F1E04B1C50070C042 /* UIImageViewExtensionsTests.swift */; }; + 3BFE4B501D34673D00DDF53F /* ThirdPartySearchTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BFE4B4F1D34673D00DDF53F /* ThirdPartySearchTest.swift */; }; + 3D71C89E1F5703A1008D8646 /* CopiedLinksTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D71C89D1F5703A1008D8646 /* CopiedLinksTests.swift */; }; + 3D9CA9841EF456A8002434DD /* NightModeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D9CA9831EF456A8002434DD /* NightModeTests.swift */; }; + 3D9CA9A81EF84D04002434DD /* NoImageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D9CA9A71EF84D04002434DD /* NoImageTests.swift */; }; + 3D9CAA1C1EFCD655002434DD /* ClipBoardTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D9CAA1B1EFCD655002434DD /* ClipBoardTests.swift */; }; + 3DEFED081F55EBE300F8620C /* TrackingProtectionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DEFED071F55EBE300F8620C /* TrackingProtectionTests.swift */; }; + 4A59B58AD11B5EE1F80BBDEB /* TestHistory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A59BF410BBD9B3BE71F4C7C /* TestHistory.swift */; }; + 4F514FD41ACD8F2C0022D7EA /* HistoryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F514FD31ACD8F2C0022D7EA /* HistoryTests.swift */; }; + 4F97573B1AFA6F37006ECC24 /* readerContent.html in Resources */ = {isa = PBXBuildFile; fileRef = 4F9757391AFA6F37006ECC24 /* readerContent.html */; }; + 5002717C41BC7C50F67F1CAD /* StateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50027345B49B967409DDA348 /* StateTests.swift */; }; + 554867231DC3935A00183DAA /* HomePageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 554867221DC3935A00183DAA /* HomePageTests.swift */; }; + 55A747171DC46FC400CE1B57 /* HomePageUITest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55A747161DC46FC400CE1B57 /* HomePageUITest.swift */; }; + 59A681BDFC95A19F05E07223 /* SearchViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59A68CCB63E2A565CB03F832 /* SearchViewController.swift */; }; + 59A68B280D62462B85CF57A4 /* HistoryPanel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59A6825233896FC846499289 /* HistoryPanel.swift */; }; + 59A68D66379CFA85C4EAF00B /* TwoLineCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59A68B1F857A8638598A63A0 /* TwoLineCell.swift */; }; + 59A68E0B4ABBF55E14819668 /* BookmarksPanel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59A6839879D615FC1C0D71CE /* BookmarksPanel.swift */; }; + 59A68FD5260B8D520F890F4A /* ReaderPanel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59A685F4EAD19EDEC854BCA4 /* ReaderPanel.swift */; }; + 742A56391D80B54A00BDB803 /* PhotonActionSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 742A56381D80B54A00BDB803 /* PhotonActionSheet.swift */; }; + 744B0FFE1B4F172E00100422 /* ToolbarTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 744B0FFD1B4F172E00100422 /* ToolbarTests.swift */; }; + 744ED5611DBFEB8D00A2B5BE /* MailtoLinkHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 744ED5601DBFEB8D00A2B5BE /* MailtoLinkHandler.swift */; }; + 745DAB301CDAAFAA00D44181 /* RecentlyClosedTabsPanel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 745DAB2F1CDAAFAA00D44181 /* RecentlyClosedTabsPanel.swift */; }; + 745DAB3F1CDAB09E00D44181 /* HistoryBackButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 745DAB3E1CDAB09E00D44181 /* HistoryBackButton.swift */; }; + 7479B4EF1C5306A200DF000B /* Reachability.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7479B4ED1C5306A200DF000B /* Reachability.swift */; }; + 74821F8E1DAD8F1400EEEA72 /* ActivityStreamHighlightCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74821F8D1DAD8F1300EEEA72 /* ActivityStreamHighlightCell.swift */; }; + 74821FC51DB56A2500EEEA72 /* OpenWithSettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74821FC41DB56A2500EEEA72 /* OpenWithSettingsViewController.swift */; }; + 74821FFE1DB6D3AC00EEEA72 /* MailSchemes.plist in Resources */ = {isa = PBXBuildFile; fileRef = 74821FFD1DB6D3AC00EEEA72 /* MailSchemes.plist */; }; + 7482205C1DBAB56300EEEA72 /* MailProviders.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7482205B1DBAB56300EEEA72 /* MailProviders.swift */; }; + 74B195441CF503FC007F36EF /* RecentlyClosedTabs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74B195431CF503FC007F36EF /* RecentlyClosedTabs.swift */; }; + 74C027451B2A348C001B1E88 /* SessionData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74C027441B2A348C001B1E88 /* SessionData.swift */; }; + 74E36D781B71323500D69DA1 /* SettingsContentViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74E36D771B71323500D69DA1 /* SettingsContentViewController.swift */; }; + 7B0B1B791C1B69C900DF4AB5 /* ClientPickerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4BCAAD81B05380B00855D82 /* ClientPickerViewController.swift */; }; + 7B0B1B9D1C1B6A3F00DF4AB5 /* InstructionsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4BCAAB81B0537E300855D82 /* InstructionsViewController.swift */; }; + 7B10AA9F1E3A15020002DD08 /* DataExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B10AA9E1E3A15020002DD08 /* DataExtensions.swift */; }; + 7B10AABB1E3A1F650002DD08 /* URLRequestExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B10AABA1E3A1F650002DD08 /* URLRequestExtensions.swift */; }; + 7B2142FE1E5E055000CDD3FC /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 7B2142FC1E5E055000CDD3FC /* InfoPlist.strings */; }; + 7B2142FF1E5E059900CDD3FC /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 7B2142F31E5E03F800CDD3FC /* InfoPlist.strings */; }; + 7B2143001E5E05A000CDD3FC /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 7B2142F61E5E040200CDD3FC /* InfoPlist.strings */; }; + 7B24DC9C1B67B3590005766B /* ClearPrivateDataTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B24DC9B1B67B3590005766B /* ClearPrivateDataTests.swift */; }; + 7B3631EA1C244FEE00D12AF9 /* Theme.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B3631E91C244FEE00D12AF9 /* Theme.swift */; }; + 7B3D9E651E4CBFDB007A50DA /* NSCoderExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B3D9E641E4CBFDB007A50DA /* NSCoderExtensions.swift */; }; + 7B42406E1CA04CAC009B5C28 /* Menu.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 7B42406D1CA04CAC009B5C28 /* Menu.xcassets */; }; + 7B4980A81CE363ED0017547C /* Settings.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 7B4980A71CE363ED0017547C /* Settings.xcassets */; }; + 7B604F861C494983006EEEC3 /* Alamofire.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7B604F851C494983006EEEC3 /* Alamofire.framework */; }; + 7B604F871C494995006EEEC3 /* Alamofire.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7B604F851C494983006EEEC3 /* Alamofire.framework */; }; + 7B604F891C4949B1006EEEC3 /* Alamofire.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7B604F851C494983006EEEC3 /* Alamofire.framework */; }; + 7B604F8A1C4949C1006EEEC3 /* Alamofire.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7B604F851C494983006EEEC3 /* Alamofire.framework */; }; + 7B604F901C494B0F006EEEC3 /* Alamofire.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7B604F851C494983006EEEC3 /* Alamofire.framework */; }; + 7B604F911C494B3C006EEEC3 /* Alamofire.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7B604F851C494983006EEEC3 /* Alamofire.framework */; }; + 7B604F991C494F74006EEEC3 /* KIF.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7B604F981C494F74006EEEC3 /* KIF.framework */; }; + 7B604F9B1C4950F2006EEEC3 /* SDWebImage.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7B604F9A1C4950F2006EEEC3 /* SDWebImage.framework */; }; + 7B604F9C1C495135006EEEC3 /* SDWebImage.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7B604F9A1C4950F2006EEEC3 /* SDWebImage.framework */; }; + 7B604F9D1C495143006EEEC3 /* SDWebImage.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7B604F9A1C4950F2006EEEC3 /* SDWebImage.framework */; }; + 7B604F9E1C49514A006EEEC3 /* SDWebImage.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7B604F9A1C4950F2006EEEC3 /* SDWebImage.framework */; }; + 7B604FA21C495268006EEEC3 /* SnapKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7B604FA11C495268006EEEC3 /* SnapKit.framework */; }; + 7B604FA31C4952A4006EEEC3 /* SnapKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7B604FA11C495268006EEEC3 /* SnapKit.framework */; }; + 7B604FBB1C495D8A006EEEC3 /* XCGLogger.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7B604FBA1C495D8A006EEEC3 /* XCGLogger.framework */; }; + 7B604FBC1C495E1E006EEEC3 /* XCGLogger.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7B604FBA1C495D8A006EEEC3 /* XCGLogger.framework */; settings = {ATTRIBUTES = (Weak, ); }; }; + 7B604FBD1C495E31006EEEC3 /* XCGLogger.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7B604FBA1C495D8A006EEEC3 /* XCGLogger.framework */; }; + 7B604FBE1C495E4E006EEEC3 /* XCGLogger.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7B604FBA1C495D8A006EEEC3 /* XCGLogger.framework */; }; + 7B604FBF1C495E60006EEEC3 /* XCGLogger.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7B604FBA1C495D8A006EEEC3 /* XCGLogger.framework */; }; + 7B604FC01C495FD1006EEEC3 /* XCGLogger.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7B604FBA1C495D8A006EEEC3 /* XCGLogger.framework */; }; + 7B6050011C49653B006EEEC3 /* AdjustSdk.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7B6050001C49653B006EEEC3 /* AdjustSdk.framework */; }; + 7B844E3D1BBDDB9D00E733A2 /* ChevronView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B844E3C1BBDDB9D00E733A2 /* ChevronView.swift */; }; + 7B8A47F61D01D3B400C07734 /* PassKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7B8A47F51D01D3B400C07734 /* PassKit.framework */; }; + 7B9BF91C1E43472E00CB24F4 /* JSONExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B9BF91B1E43472E00CB24F4 /* JSONExtensions.swift */; }; + 7B9BF9301E449D2500CB24F4 /* MockLogins.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B3E7DB91B27AB4C00E2E84D /* MockLogins.swift */; }; + 7BA0601B1C0F4DE200DFADB6 /* TabPeekViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BA0601A1C0F4DE200DFADB6 /* TabPeekViewController.swift */; }; + 7BA4A9471C4CED900091D032 /* GCDWebServers.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7BA4A9451C4CED780091D032 /* GCDWebServers.framework */; }; + 7BA4A94A1C4CEFC70091D032 /* OnePasswordExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7BA4A9491C4CEFC70091D032 /* OnePasswordExtension.framework */; }; + 7BA4A94C1C4CF03B0091D032 /* SwiftKeychainWrapper.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7BA4A94B1C4CF03B0091D032 /* SwiftKeychainWrapper.framework */; settings = {ATTRIBUTES = (Weak, ); }; }; + 7BA4A9641C4CFE840091D032 /* Deferred.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7BA4A9631C4CFE840091D032 /* Deferred.framework */; settings = {ATTRIBUTES = (Weak, ); }; }; + 7BA4A9651C4D007B0091D032 /* Deferred.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7BA4A9631C4CFE840091D032 /* Deferred.framework */; }; + 7BA4A9661C4D008C0091D032 /* Deferred.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7BA4A9631C4CFE840091D032 /* Deferred.framework */; }; + 7BA8D1C71BA037F500C8AE9E /* OpenInHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BA8D1C61BA037F500C8AE9E /* OpenInHelper.swift */; }; + 7BBFEE741BB405D900A305AA /* TabManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BBFEE731BB405D900A305AA /* TabManagerTests.swift */; }; + 7BC68D091CC153020043562A /* RoundedToolbar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BC68D081CC153020043562A /* RoundedToolbar.swift */; }; + 7BEB64441C7345600092C02E /* L10nSnapshotTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B3632D31C2983F000D12AF9 /* L10nSnapshotTests.swift */; }; + 7BEB64451C7345600092C02E /* SnapshotHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B60B0071BDE3AE10090C984 /* SnapshotHelper.swift */; }; + 7BEB64511C7345990092C02E /* MarketingUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B95CD191C3AB2EE00638E31 /* MarketingUITests.swift */; }; + 7BEB64521C7345990092C02E /* SnapshotHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B60B0071BDE3AE10090C984 /* SnapshotHelper.swift */; }; + 7BEDD29B1CB7CB24004B51F3 /* JumpAndSpinAnimator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BEDD29A1CB7CB24004B51F3 /* JumpAndSpinAnimator.swift */; }; + 7BEDD2B21CB7CC0B004B51F3 /* Animatable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BEDD2B11CB7CC0B004B51F3 /* Animatable.swift */; }; + 7BEFC6801BFF68C30059C952 /* QuickActions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BEFC67F1BFF68C30059C952 /* QuickActions.swift */; }; + 7BF5A1EA1B41640500EA9DD8 /* SyncQueue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BF5A1E91B41640500EA9DD8 /* SyncQueue.swift */; }; + 7BF5A1EE1B429B3100EA9DD8 /* SyncCommandsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BF5A1ED1B429B3100EA9DD8 /* SyncCommandsTests.swift */; }; + 8D8251811F4DE67F00780643 /* AdvanceAccountSettingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8D8251721F4DE67E00780643 /* AdvanceAccountSettingViewController.swift */; }; + 8DCD3BCD1ED5B7FA00446D38 /* FxADeepLinkingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8DCD3BCC1ED5B7FA00446D38 /* FxADeepLinkingTests.swift */; }; + A83E5AB71C1D993D0026D912 /* UIPasteboardExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = A83E5AB61C1D993D0026D912 /* UIPasteboardExtensions.swift */; }; + A83E5B1A1C1DA8BF0026D912 /* image.gif in Resources */ = {isa = PBXBuildFile; fileRef = A83E5B181C1DA8BF0026D912 /* image.gif */; }; + A83E5B1B1C1DA8BF0026D912 /* image.png in Resources */ = {isa = PBXBuildFile; fileRef = A83E5B191C1DA8BF0026D912 /* image.png */; }; + A83E5B1D1C1DA8D80026D912 /* UIPasteboardExtensionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A83E5B1C1C1DA8D80026D912 /* UIPasteboardExtensionsTests.swift */; }; + A83E5B1E1C1DAAAA0026D912 /* UIPasteboardExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = A83E5AB61C1D993D0026D912 /* UIPasteboardExtensions.swift */; }; + A9072B801D07B34100459960 /* NoImageModeHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9072B7F1D07B34100459960 /* NoImageModeHelper.swift */; }; + A91528D21D0E637600CD0425 /* NoImageModeHelper.js in Resources */ = {isa = PBXBuildFile; fileRef = A91528D11D0E637600CD0425 /* NoImageModeHelper.js */; }; + A93067D91D0FE15E00C49C6E /* NightModeHelper.js in Resources */ = {isa = PBXBuildFile; fileRef = A93067D81D0FE15E00C49C6E /* NightModeHelper.js */; }; + A93067E81D0FE18E00C49C6E /* NightModeHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = A93067E71D0FE18E00C49C6E /* NightModeHelper.swift */; }; + C400467C1CF4E43E00B08303 /* BackForwardListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C400467B1CF4E43E00B08303 /* BackForwardListViewController.swift */; }; + C40046FA1CF8E0B200B08303 /* BackForwardListAnimator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C40046F91CF8E0B200B08303 /* BackForwardListAnimator.swift */; }; + C45F44691D087DB600CB7EF0 /* TopTabsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C45F44681D087DB600CB7EF0 /* TopTabsViewController.swift */; }; + C4E3983D1D21F1E7004E89BA /* TopTabsViews.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4E3983C1D21F1E7004E89BA /* TopTabsViews.swift */; }; + C4E3984C1D21F2FD004E89BA /* TabTrayButtonExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4E3984B1D21F2FD004E89BA /* TabTrayButtonExtensions.swift */; }; + C4E398601D22C409004E89BA /* TopTabsLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4E3985F1D22C409004E89BA /* TopTabsLayout.swift */; }; + C4EFEECF1CEBB6F2009762A4 /* BackForwardTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4EFEECE1CEBB6F2009762A4 /* BackForwardTableViewCell.swift */; }; + C4F3B29A1CFCF93A00966259 /* ButtonToast.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4F3B2991CFCF93A00966259 /* ButtonToast.swift */; }; + C817B34D1FC609500086018E /* UIScrollViewSwizzled.swift in Sources */ = {isa = PBXBuildFile; fileRef = C817B34C1FC609500086018E /* UIScrollViewSwizzled.swift */; }; + C8611C8E1F71904C00C3DE7D /* DiskImageStoreTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3BF8CBC1B7472FA0007AFE6 /* DiskImageStoreTests.swift */; }; + C8611CB01F71AEBA00C3DE7D /* NoImageModeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C8611CA11F71AEB900C3DE7D /* NoImageModeTests.swift */; }; + C88601C61F4228AD00BBDE4F /* ContentBlockerSettingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C88601B71F4228AD00BBDE4F /* ContentBlockerSettingViewController.swift */; }; + C8EB60C41F1FB12500F9B5B3 /* navigationDelegate.html in Resources */ = {isa = PBXBuildFile; fileRef = C8EB60C31F1FB12500F9B5B3 /* navigationDelegate.html */; }; + C8EB60DC1F1FB9AD00F9B5B3 /* NavigationDelegateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C8EB60DB1F1FB9AD00F9B5B3 /* NavigationDelegateTests.swift */; }; + C8F457A81F1FD75A000CB895 /* BrowserViewController+WKNavigationDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C8F457A71F1FD75A000CB895 /* BrowserViewController+WKNavigationDelegate.swift */; }; + C8F457AA1F1FDD9B000CB895 /* BrowserViewController+KeyCommands.swift in Sources */ = {isa = PBXBuildFile; fileRef = C8F457A91F1FDD9B000CB895 /* BrowserViewController+KeyCommands.swift */; }; + CE339EE61F2507C0009BE0E6 /* TestBookmarksSynchronizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE339ED61F2507A1009BE0E6 /* TestBookmarksSynchronizer.swift */; }; + CE564D8E1EB7BD7700BEDDDC /* BookmarksRepairRequestor.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE564D8D1EB7BD7700BEDDDC /* BookmarksRepairRequestor.swift */; }; + CE7F11941F3CEEC800ABFC0B /* RemoteDevices.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE7F115E1F3CCEF900ABFC0B /* RemoteDevices.swift */; }; + CEFC984B1EC0DCF5008A3E48 /* TestBookmarksRepairRequestor.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEFC983B1EC0DC60008A3E48 /* TestBookmarksRepairRequestor.swift */; }; + D0131B4D1F3CF7D8000CDE86 /* SQLiteFavicons.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0131B4C1F3CF7D8000CDE86 /* SQLiteFavicons.swift */; }; + D018F93E1F44A71A0098F8CA /* Schema.swift in Sources */ = {isa = PBXBuildFile; fileRef = D018F93D1F44A7190098F8CA /* Schema.swift */; }; + D02816C21ECA5E2A00240CAA /* HistoryStateHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = D02816C11ECA5E2A00240CAA /* HistoryStateHelper.swift */; }; + D02816D21ECA60DF00240CAA /* HistoryStateHelper.js in Resources */ = {isa = PBXBuildFile; fileRef = D02816D11ECA60DF00240CAA /* HistoryStateHelper.js */; }; + D02818611EF056C800240CAA /* SentryIntegration.swift in Sources */ = {isa = PBXBuildFile; fileRef = D02818601EF056C800240CAA /* SentryIntegration.swift */; }; + D09A0CD81FAA23F6009A0273 /* Shared.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 288A2D861AB8B3260023ABC3 /* Shared.framework */; }; + D09A0CD91FAA2413009A0273 /* Shared.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 288A2D861AB8B3260023ABC3 /* Shared.framework */; }; + D09A0CDA1FAA24C2009A0273 /* Storage.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2FCAE21A1ABB51F800877008 /* Storage.framework */; }; + D09A0CDB1FAA24C8009A0273 /* Shared.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 288A2D861AB8B3260023ABC3 /* Shared.framework */; }; + D09A0CDC1FAA24CC009A0273 /* Account.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2FA435FB1ABB83B4008031D1 /* Account.framework */; }; + D09A0CDD1FAA24D0009A0273 /* FxA.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 28CE83D01A1D1D5100576538 /* FxA.framework */; }; + D09A0D301FAB8AF3009A0273 /* Sentry.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E46175F21EBB73A10021AE8A /* Sentry.framework */; settings = {ATTRIBUTES = (Weak, ); }; }; + D0B29EE01F460BDF00C7CEFC /* LoginsSchema.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0B29EDF1F460BDF00C7CEFC /* LoginsSchema.swift */; }; + D301AAEE1A3A55B70078DD1D /* TabTrayController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D301AAED1A3A55B70078DD1D /* TabTrayController.swift */; }; + D30684F11C84F12A002D8D82 /* SearchPlugins in Resources */ = {isa = PBXBuildFile; fileRef = D30684F01C84F12A002D8D82 /* SearchPlugins */; }; + D308E4E41A5306F500842685 /* SearchEngines.swift in Sources */ = {isa = PBXBuildFile; fileRef = D308E4E31A5306F500842685 /* SearchEngines.swift */; }; + D308EE561CBF0BF5006843F2 /* CertError.css in Resources */ = {isa = PBXBuildFile; fileRef = D308EE551CBF0BF5006843F2 /* CertError.css */; }; + D30B101E1AA7F9C600C01CA3 /* HomePanels.swift in Sources */ = {isa = PBXBuildFile; fileRef = D30B101D1AA7F9C600C01CA3 /* HomePanels.swift */; }; + D313BE981B2F5096009EF241 /* DomainAutocompleteTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D313BE971B2F5096009EF241 /* DomainAutocompleteTests.swift */; }; + D314E7F71A37B98700426A76 /* TabToolbar.swift in Sources */ = {isa = PBXBuildFile; fileRef = D314E7F51A37B98700426A76 /* TabToolbar.swift */; }; + D31A0FC71A65D6D000DC8C7E /* SearchSuggestClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = D31A0FC61A65D6D000DC8C7E /* SearchSuggestClient.swift */; }; + D31CF65C1CC1959A001D0BD0 /* PrivilegedRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = D31CF65B1CC1959A001D0BD0 /* PrivilegedRequest.swift */; }; + D31EC05F1CC57ED80096F4AB /* localhostLoad.html in Resources */ = {isa = PBXBuildFile; fileRef = D31EC05E1CC57ED80096F4AB /* localhostLoad.html */; }; + D31F95E91AC226CB005C9F3B /* ScreenshotHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = D31F95E81AC226CB005C9F3B /* ScreenshotHelper.swift */; }; + D32A350E1D6530D80066DAE9 /* FxADevice.swift in Sources */ = {isa = PBXBuildFile; fileRef = D32A350D1D6530D80066DAE9 /* FxADevice.swift */; }; + D32CACED1AE04DA1000658EB /* TestSwiftData.swift in Sources */ = {isa = PBXBuildFile; fileRef = D32CACEC1AE04DA1000658EB /* TestSwiftData.swift */; }; + D331DFCA1CB6E9EE009B5DA2 /* OldStrings.swift in Sources */ = {isa = PBXBuildFile; fileRef = D331DFC91CB6E9EE009B5DA2 /* OldStrings.swift */; }; + D343DCFE1C446BDB00D7EEE8 /* findPage.html in Resources */ = {isa = PBXBuildFile; fileRef = D343DCFD1C446BDB00D7EEE8 /* findPage.html */; }; + D34510881ACF415700EC27F0 /* SearchLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = D34510871ACF415700EC27F0 /* SearchLoader.swift */; }; + D34DC8531A16C40C00D49B7B /* Profile.swift in Sources */ = {isa = PBXBuildFile; fileRef = D34DC84D1A16C40C00D49B7B /* Profile.swift */; }; + D34E33031BA793C2006135F0 /* loginForm.html in Resources */ = {isa = PBXBuildFile; fileRef = D34E33021BA793C2006135F0 /* loginForm.html */; }; + D36998891AD70A0A00650C6C /* IOKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D36998881AD70A0A00650C6C /* IOKit.framework */; }; + D37524871C6E8B5A00A5F6C2 /* topdomains.txt in Resources */ = {isa = PBXBuildFile; fileRef = D37524861C6E8B5A00A5F6C2 /* topdomains.txt */; }; + D375A9201AE71675001B30D5 /* ViewMemoryLeakTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D375A91F1AE71675001B30D5 /* ViewMemoryLeakTests.swift */; }; + D37DE2831CA2047500A5EC69 /* CertStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = D37DE2821CA2047500A5EC69 /* CertStore.swift */; }; + D37DE2C71CA356D800A5EC69 /* CertTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D37DE2C61CA356D800A5EC69 /* CertTests.swift */; }; + D37DE2CA1CA356F900A5EC69 /* testcert1.pem in Resources */ = {isa = PBXBuildFile; fileRef = D37DE2C81CA356F900A5EC69 /* testcert1.pem */; }; + D37DE2CB1CA356F900A5EC69 /* testcert2.pem in Resources */ = {isa = PBXBuildFile; fileRef = D37DE2C91CA356F900A5EC69 /* testcert2.pem */; }; + D38A1BEE1A9FA2CA00F6A386 /* SiteTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D38A1BEC1A9FA2CA00F6A386 /* SiteTableViewController.swift */; }; + D38A1EE01CB458EC0080C842 /* CertError.html in Resources */ = {isa = PBXBuildFile; fileRef = D38A1EDF1CB458EC0080C842 /* CertError.html */; }; + D38B2D8A1A8D98D00040E6B5 /* SearchEngines.swift in Sources */ = {isa = PBXBuildFile; fileRef = D308E4E31A5306F500842685 /* SearchEngines.swift */; }; + D38B2D8B1A8D98D10040E6B5 /* SearchEngines.swift in Sources */ = {isa = PBXBuildFile; fileRef = D308E4E31A5306F500842685 /* SearchEngines.swift */; }; + D38B2D8C1A8D98D90040E6B5 /* OpenSearch.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3FA77831A43B2CE0010CD32 /* OpenSearch.swift */; }; + D38B2D8D1A8D98DA0040E6B5 /* OpenSearch.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3FA77831A43B2CE0010CD32 /* OpenSearch.swift */; }; + D38F02D11C05127100175932 /* Authenticator.swift in Sources */ = {isa = PBXBuildFile; fileRef = D38F02D01C05127100175932 /* Authenticator.swift */; }; + D38F03701C06387900175932 /* AuthenticationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D38F036F1C06387900175932 /* AuthenticationTests.swift */; }; + D3968F251A38FE8500CEFD3B /* TabManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3968F241A38FE8500CEFD3B /* TabManager.swift */; }; + D3972BF31C22412B00035B87 /* ShareExtensionHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3972BF11C22412B00035B87 /* ShareExtensionHelper.swift */; }; + D3972BF41C22412B00035B87 /* TitleActivityItemProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3972BF21C22412B00035B87 /* TitleActivityItemProvider.swift */; }; + D39FA16C1A83E17800EE869C /* CoreGraphics.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D39FA16B1A83E17800EE869C /* CoreGraphics.framework */; }; + D39FA1811A83E84900EE869C /* Global.swift in Sources */ = {isa = PBXBuildFile; fileRef = D39FA1801A83E84900EE869C /* Global.swift */; }; + D3A14C221CB3145E00253BC6 /* Strings.swift in Sources */ = {isa = PBXBuildFile; fileRef = D35210E01CB2F16600FC5DCB /* Strings.swift */; }; + D3A9949C1A3686BD008AD1AC /* BrowserViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3A994951A3686BD008AD1AC /* BrowserViewController.swift */; }; + D3A9949D1A3686BD008AD1AC /* Tab.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3A994961A3686BD008AD1AC /* Tab.swift */; }; + D3B6923D1B9F9444004B87A4 /* FindInPageBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3B6923C1B9F9444004B87A4 /* FindInPageBar.swift */; }; + D3B6923F1B9F9A58004B87A4 /* FindInPageHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3B6923E1B9F9A58004B87A4 /* FindInPageHelper.swift */; }; + D3B692411B9F9CC8004B87A4 /* FindInPage.js in Resources */ = {isa = PBXBuildFile; fileRef = D3B692401B9F9CC8004B87A4 /* FindInPage.js */; }; + D3BA41681BD82F2200DA5457 /* XCTestCaseExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3BA41671BD82F2200DA5457 /* XCTestCaseExtensions.swift */; }; + D3BA7E0C1B0E902A00153782 /* ContextMenu.js in Resources */ = {isa = PBXBuildFile; fileRef = D3BA7E0B1B0E902A00153782 /* ContextMenu.js */; }; + D3BA7E0E1B0E934F00153782 /* ContextMenuHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3BA7E0D1B0E934F00153782 /* ContextMenuHelper.swift */; }; + D3BE7B261B054D4400641031 /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3BE7B251B054D4400641031 /* main.swift */; }; + D3BE7B461B054F8600641031 /* TestAppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3BE7B451B054F8600641031 /* TestAppDelegate.swift */; }; + D3BF8CBB1B7425570007AFE6 /* DiskImageStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3BF8CBA1B7425570007AFE6 /* DiskImageStore.swift */; }; + D3C3696E1CC6B78800348A61 /* LocalRequestHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3C3696D1CC6B78800348A61 /* LocalRequestHelper.swift */; }; + D3C3EB651B6FF44000388E9A /* SessionRestoreTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3C3EB641B6FF44000388E9A /* SessionRestoreTests.swift */; }; + D3C744CD1A687D6C004CE85D /* URIFixup.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3C744CC1A687D6C004CE85D /* URIFixup.swift */; }; + D3C744CF1A687D6C004CE85D /* URIFixup.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3C744CC1A687D6C004CE85D /* URIFixup.swift */; }; + D3C744D01A687D6C004CE85D /* URIFixup.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3C744CC1A687D6C004CE85D /* URIFixup.swift */; }; + D3CFD3641CC5605B0064AB4A /* SecurityTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3CFD3631CC5605B0064AB4A /* SecurityTests.swift */; }; + D3D488591ABB54CD00A93597 /* FileAccessorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3D488581ABB54CD00A93597 /* FileAccessorTests.swift */; }; + D3DBE6E51D6516FE00033FFF /* FxADeviceRegistration.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3DBE6E41D6516FE00033FFF /* FxADeviceRegistration.swift */; }; + D3E171C21A841EAD00AB44CD /* KIFHelper.js in Resources */ = {isa = PBXBuildFile; fileRef = D3E171C11A841EAD00AB44CD /* KIFHelper.js */; }; + D3E8EF101B97BE69001900FB /* ClearPrivateDataTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3E8EEE71B97A87A001900FB /* ClearPrivateDataTableViewController.swift */; }; + D3FA777B1A43B2990010CD32 /* SearchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3FA777A1A43B2990010CD32 /* SearchTests.swift */; }; + D3FEC38D1AC4B42F00494F45 /* AutocompleteTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3FEC38C1AC4B42F00494F45 /* AutocompleteTextField.swift */; }; + D81127D81F84023B0050841D /* PhotonActionSheetTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = D81127D71F84023B0050841D /* PhotonActionSheetTest.swift */; }; + D81E45131F82C56D004EFFBA /* NewTabContentSettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D81E45121F82C56C004EFFBA /* NewTabContentSettingsViewController.swift */; }; + D83822001FC7961D00303C12 /* DispatchQueueExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D83821FF1FC7961D00303C12 /* DispatchQueueExtensions.swift */; }; + D863C8F21F68BFC20058D95F /* GradientProgressBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = D863C8E31F68BFC20058D95F /* GradientProgressBar.swift */; }; + D88FDA9F1F4E2B9200FD9709 /* PhotonActionSheetProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = D88FDA9E1F4E2B9200FD9709 /* PhotonActionSheetProtocol.swift */; }; + D88FDAAF1F4E2BA000FD9709 /* PhotonActionSheetAnimator.swift in Sources */ = {isa = PBXBuildFile; fileRef = D88FDAAE1F4E2BA000FD9709 /* PhotonActionSheetAnimator.swift */; }; + DD31E0FB1B382B520077078A /* TabPrintPageRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD31E0FA1B382B520077078A /* TabPrintPageRenderer.swift */; }; + E402000A1E6493C800B45AFF /* LaunchArguments.swift in Sources */ = {isa = PBXBuildFile; fileRef = E65075861E37F7AB006961AC /* LaunchArguments.swift */; }; + E40AFC541DD0E93300DA5651 /* L10nPermissionStringsSnapshotTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E40AFC531DD0E93300DA5651 /* L10nPermissionStringsSnapshotTests.swift */; }; + E40AFC651DD0F25500DA5651 /* L10nBaseSnapshotTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E40AFC641DD0F25500DA5651 /* L10nBaseSnapshotTests.swift */; }; + E40AFC6C1DD128DA00DA5651 /* L10nIntroSnapshotTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E40AFC6B1DD128DA00DA5651 /* L10nIntroSnapshotTests.swift */; }; + E40FAB0C1A7ABB77009CB80D /* WebServer.swift in Sources */ = {isa = PBXBuildFile; fileRef = E40FAB0B1A7ABB77009CB80D /* WebServer.swift */; }; + E418D0D91A251B3200CAE47A /* Profile.swift in Sources */ = {isa = PBXBuildFile; fileRef = D34DC84D1A16C40C00D49B7B /* Profile.swift */; }; + E41A7D4B1A1BE04500245963 /* InitialViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E41A7D4A1A1BE04500245963 /* InitialViewController.swift */; }; + E41F0D981ADFFB5400FC7387 /* ReadingListService.swift in Sources */ = {isa = PBXBuildFile; fileRef = E41F0D971ADFFB5400FC7387 /* ReadingListService.swift */; }; + E41F0D9B1ADFFC5600FC7387 /* ReadingListError.swift in Sources */ = {isa = PBXBuildFile; fileRef = E41F0D9A1ADFFC5600FC7387 /* ReadingListError.swift */; }; + E42736071EA858CF009C428E /* TabsPayloadTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E42736061EA858CF009C428E /* TabsPayloadTests.swift */; }; + E42CCDE81A23A73D00B794D3 /* Profile.swift in Sources */ = {isa = PBXBuildFile; fileRef = D34DC84D1A16C40C00D49B7B /* Profile.swift */; }; + E4424B3C1AC71FB400F44C38 /* FiraSans-Book.ttf in Resources */ = {isa = PBXBuildFile; fileRef = E4424B3B1AC71FB400F44C38 /* FiraSans-Book.ttf */; }; + E444F7DC1B4B1B7E00FEB19B /* Shared.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 288A2D861AB8B3260023ABC3 /* Shared.framework */; }; + E444F7DF1B4B1C1200FEB19B /* Profile.swift in Sources */ = {isa = PBXBuildFile; fileRef = D34DC84D1A16C40C00D49B7B /* Profile.swift */; }; + E444F7E01B4B1C1A00FEB19B /* NSUserDefaultsPrefs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0BD19A661A25309B0084FBA7 /* NSUserDefaultsPrefs.swift */; }; + E444F7E21B4B1C6800FEB19B /* SearchEngines.swift in Sources */ = {isa = PBXBuildFile; fileRef = D308E4E31A5306F500842685 /* SearchEngines.swift */; }; + E444F7E31B4B1C8000FEB19B /* OpenSearch.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3FA77831A43B2CE0010CD32 /* OpenSearch.swift */; }; + E444F7E51B4B1CB600FEB19B /* ImageIO.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0B8E0FF31A932BD500161DC3 /* ImageIO.framework */; }; + E444F8271B4C1E8800FEB19B /* Sync.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2827315E1ABC9BE600AA1954 /* Sync.framework */; }; + E4483B5A1ADED63300B485A7 /* Shared.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 288A2D861AB8B3260023ABC3 /* Shared.framework */; }; + E47536ED1C85412C0022CC69 /* PrintHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = E47536EC1C85412C0022CC69 /* PrintHelper.swift */; }; + E47536FC1C85415F0022CC69 /* PrintHelper.js in Resources */ = {isa = PBXBuildFile; fileRef = E47536FB1C85415E0022CC69 /* PrintHelper.js */; }; + E47616C71AB74CA600E7DD25 /* ReaderModeBarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E47616C61AB74CA600E7DD25 /* ReaderModeBarView.swift */; }; + E4791B9C1CC035FD00C6D77B /* testcert1.pem in Resources */ = {isa = PBXBuildFile; fileRef = D37DE2C81CA356F900A5EC69 /* testcert1.pem */; }; + E4791BAA1CC0360200C6D77B /* testcert2.pem in Resources */ = {isa = PBXBuildFile; fileRef = D37DE2C91CA356F900A5EC69 /* testcert2.pem */; }; + E487B2331AC1C64300F3E86F /* FiraSans-Regular.ttf in Resources */ = {isa = PBXBuildFile; fileRef = E4B7B7561A793CF20022C5E0 /* FiraSans-Regular.ttf */; }; + E487B24E1AC1C66400F3E86F /* FiraSans-SemiBold.ttf in Resources */ = {isa = PBXBuildFile; fileRef = E4B7B7571A793CF20022C5E0 /* FiraSans-SemiBold.ttf */; }; + E487B24F1AC1CC9200F3E86F /* FiraSans-Medium.ttf in Resources */ = {isa = PBXBuildFile; fileRef = E4ECCDAD1AB131770005E717 /* FiraSans-Medium.ttf */; }; + E487B2501AC1CC9800F3E86F /* FiraSans-Light.ttf in Resources */ = {isa = PBXBuildFile; fileRef = E4B7B7521A793CF20022C5E0 /* FiraSans-Light.ttf */; }; + E49943F51AE6879C00BF9DE4 /* IntroViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E49943F41AE6879C00BF9DE4 /* IntroViewController.swift */; }; + E49943F71AE69EDD00BF9DE4 /* Intro.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = E49943F61AE69EDD00BF9DE4 /* Intro.xcassets */; }; + E49BFF591B1F48C3009368CC /* Readability.js in Resources */ = {isa = PBXBuildFile; fileRef = E49BFF581B1F48C3009368CC /* Readability.js */; }; + E4A888161A95679500CDC337 /* FxA.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 28CE83D01A1D1D5100576538 /* FxA.framework */; }; + E4A960061ABB9C450069AD6F /* ReaderModeUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4A960051ABB9C450069AD6F /* ReaderModeUtils.swift */; }; + E4A961181AC041C40069AD6F /* ReadabilityService.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4A961171AC041C40069AD6F /* ReadabilityService.swift */; }; + E4A961341AC051360069AD6F /* ReadabilityTabHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4A961331AC051360069AD6F /* ReadabilityTabHelper.swift */; }; + E4A961361AC052DE0069AD6F /* ReadabilityTabHelper.js in Resources */ = {isa = PBXBuildFile; fileRef = E4A961351AC052DE0069AD6F /* ReadabilityTabHelper.js */; }; + E4A961381AC06FA50069AD6F /* ReaderViewLoading.html in Resources */ = {isa = PBXBuildFile; fileRef = E4A961371AC06FA50069AD6F /* ReaderViewLoading.html */; }; + E4B334881BBF23F3004E2BFF /* iAd.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E4B334871BBF23F3004E2BFF /* iAd.framework */; }; + E4B3348A1BBF23F9004E2BFF /* AdSupport.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E4B334891BBF23F9004E2BFF /* AdSupport.framework */; }; + E4B3348C1BC01D8F004E2BFF /* AdjustIntegration.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4B3348B1BC01D8F004E2BFF /* AdjustIntegration.swift */; }; + E4B423BE1AB9FE6A007E66C8 /* ReaderModeCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4B423BD1AB9FE6A007E66C8 /* ReaderModeCache.swift */; }; + E4B423DD1ABA0318007E66C8 /* ReaderModeHandlers.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4B423DC1ABA0318007E66C8 /* ReaderModeHandlers.swift */; }; + E4B7B7611A793CF20022C5E0 /* CharisSILB.ttf in Resources */ = {isa = PBXBuildFile; fileRef = E4B7B73A1A793CF20022C5E0 /* CharisSILB.ttf */; }; + E4B7B7621A793CF20022C5E0 /* CharisSILBI.ttf in Resources */ = {isa = PBXBuildFile; fileRef = E4B7B73B1A793CF20022C5E0 /* CharisSILBI.ttf */; }; + E4B7B7631A793CF20022C5E0 /* CharisSILI.ttf in Resources */ = {isa = PBXBuildFile; fileRef = E4B7B73C1A793CF20022C5E0 /* CharisSILI.ttf */; }; + E4B7B7641A793CF20022C5E0 /* CharisSILR.ttf in Resources */ = {isa = PBXBuildFile; fileRef = E4B7B73D1A793CF20022C5E0 /* CharisSILR.ttf */; }; + E4B7B7681A793CF20022C5E0 /* FiraSans-Bold.ttf in Resources */ = {isa = PBXBuildFile; fileRef = E4B7B7411A793CF20022C5E0 /* FiraSans-Bold.ttf */; }; + E4B7B7781A793CF20022C5E0 /* FiraSans-Italic.ttf in Resources */ = {isa = PBXBuildFile; fileRef = E4B7B7511A793CF20022C5E0 /* FiraSans-Italic.ttf */; }; + E4B7B7791A793CF20022C5E0 /* FiraSans-Light.ttf in Resources */ = {isa = PBXBuildFile; fileRef = E4B7B7521A793CF20022C5E0 /* FiraSans-Light.ttf */; }; + E4B7B77D1A793CF20022C5E0 /* FiraSans-Regular.ttf in Resources */ = {isa = PBXBuildFile; fileRef = E4B7B7561A793CF20022C5E0 /* FiraSans-Regular.ttf */; }; + E4B7B77E1A793CF20022C5E0 /* FiraSans-SemiBold.ttf in Resources */ = {isa = PBXBuildFile; fileRef = E4B7B7571A793CF20022C5E0 /* FiraSans-SemiBold.ttf */; }; + E4B7B7861A793CF20022C5E0 /* FiraSans-UltraLight.ttf in Resources */ = {isa = PBXBuildFile; fileRef = E4B7B75F1A793CF20022C5E0 /* FiraSans-UltraLight.ttf */; }; + E4BA8A351B4B0A1700BC2E95 /* ViewLater.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = E4BA8A2B1B4B0A1600BC2E95 /* ViewLater.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; + E4BA8A8A1B4B10F400BC2E95 /* Storage.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2FCAE21A1ABB51F800877008 /* Storage.framework */; }; + E4BA8AA81B4B15EF00BC2E95 /* ActionRequestHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4BA8AA11B4B15EF00BC2E95 /* ActionRequestHandler.swift */; }; + E4BA8AAD1B4B15EF00BC2E95 /* ViewLater.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = E4BA8AA61B4B15EF00BC2E95 /* ViewLater.xcassets */; }; + E4BCAAB91B0537E300855D82 /* InstructionsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4BCAAB81B0537E300855D82 /* InstructionsViewController.swift */; }; + E4BCAAD91B05380B00855D82 /* ClientPickerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4BCAAD81B05380B00855D82 /* ClientPickerViewController.swift */; }; + E4C358551AF144BA00299F7E /* FSReadingList.m in Sources */ = {isa = PBXBuildFile; fileRef = E4C358541AF144BA00299F7E /* FSReadingList.m */; }; + E4CD9E911A6897FB00318571 /* ReaderMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4CD9E901A6897FB00318571 /* ReaderMode.swift */; }; + E4CD9E9A1A68980A00318571 /* ReaderMode.js in Resources */ = {isa = PBXBuildFile; fileRef = E4CD9E991A68980A00318571 /* ReaderMode.js */; }; + E4CD9F1D1A6D9C2800318571 /* WebServerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4CD9F1C1A6D9C2800318571 /* WebServerTests.swift */; }; + E4CD9F2D1A6DC91200318571 /* TabLocationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4CD9F2C1A6DC91200318571 /* TabLocationView.swift */; }; + E4CD9F541A71506400318571 /* Reader.html in Resources */ = {isa = PBXBuildFile; fileRef = E4CD9F531A71506400318571 /* Reader.html */; }; + E4CD9F5B1A71506C00318571 /* Reader.css in Resources */ = {isa = PBXBuildFile; fileRef = E4CD9F5A1A71506C00318571 /* Reader.css */; }; + E4CD9F6D1A77DD2800318571 /* ReaderModeStyleViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4CD9F6C1A77DD2800318571 /* ReaderModeStyleViewController.swift */; }; + E4D567231ADECE2700F1EFE7 /* ReadingList.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E4D567181ADECE2700F1EFE7 /* ReadingList.framework */; }; + E4D5672F1ADECE2800F1EFE7 /* ReadingList.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E4D567181ADECE2700F1EFE7 /* ReadingList.framework */; }; + E4D5676A1ADECE8100F1EFE7 /* ReadingListAuthenticator.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4D567551ADECE8100F1EFE7 /* ReadingListAuthenticator.swift */; }; + E4D5676C1ADECE8100F1EFE7 /* ReadingListBasicAuthAuthenticator.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4D567561ADECE8100F1EFE7 /* ReadingListBasicAuthAuthenticator.swift */; }; + E4D5676E1ADECE8100F1EFE7 /* ReadingListBatchRecordResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4D567571ADECE8100F1EFE7 /* ReadingListBatchRecordResponse.swift */; }; + E4D567701ADECE8100F1EFE7 /* ReadingListChangeAccumulator.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4D567581ADECE8100F1EFE7 /* ReadingListChangeAccumulator.swift */; }; + E4D567721ADECE8100F1EFE7 /* ReadingListClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4D567591ADECE8100F1EFE7 /* ReadingListClient.swift */; }; + E4D567741ADECE8100F1EFE7 /* ReadingListClientMetadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4D5675A1ADECE8100F1EFE7 /* ReadingListClientMetadata.swift */; }; + E4D567761ADECE8100F1EFE7 /* ReadingListClientRecord.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4D5675B1ADECE8100F1EFE7 /* ReadingListClientRecord.swift */; }; + E4D5677A1ADECE8100F1EFE7 /* ReadingListFetchSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4D5675D1ADECE8100F1EFE7 /* ReadingListFetchSpec.swift */; }; + E4D5677C1ADECE8100F1EFE7 /* ReadingListOAuthAuthenticator.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4D5675E1ADECE8100F1EFE7 /* ReadingListOAuthAuthenticator.swift */; }; + E4D5677E1ADECE8100F1EFE7 /* ReadingListRecord.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4D5675F1ADECE8100F1EFE7 /* ReadingListRecord.swift */; }; + E4D567801ADECE8100F1EFE7 /* ReadingListRecordResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4D567601ADECE8100F1EFE7 /* ReadingListRecordResponse.swift */; }; + E4D567821ADECE8100F1EFE7 /* ReadingListRecordsResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4D567611ADECE8100F1EFE7 /* ReadingListRecordsResponse.swift */; }; + E4D567841ADECE8100F1EFE7 /* ReadingListResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4D567621ADECE8100F1EFE7 /* ReadingListResponse.swift */; }; + E4D567861ADECE8100F1EFE7 /* ReadingListServerMetadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4D567631ADECE8100F1EFE7 /* ReadingListServerMetadata.swift */; }; + E4D567881ADECE8100F1EFE7 /* ReadingListServerRecord.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4D567641ADECE8100F1EFE7 /* ReadingListServerRecord.swift */; }; + E4D5678A1ADECE8100F1EFE7 /* ReadingListSQLStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4D567651ADECE8100F1EFE7 /* ReadingListSQLStorage.swift */; }; + E4D5678C1ADECE8100F1EFE7 /* ReadingListStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4D567661ADECE8100F1EFE7 /* ReadingListStorage.swift */; }; + E4D5678E1ADECE8100F1EFE7 /* ReadingListSynchronizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4D567671ADECE8100F1EFE7 /* ReadingListSynchronizer.swift */; }; + E4D567901ADECE8100F1EFE7 /* ReadingListSyncMetadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4D567681ADECE8100F1EFE7 /* ReadingListSyncMetadata.swift */; }; + E4D567921ADECE8100F1EFE7 /* ReadingListUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4D567691ADECE8100F1EFE7 /* ReadingListUtils.swift */; }; + E4D5679F1ADECEB800F1EFE7 /* ReadingListClientRecordTestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4D567951ADECEB800F1EFE7 /* ReadingListClientRecordTestCase.swift */; }; + E4D567A51ADECEB800F1EFE7 /* ReadingListStorageTestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4D5679B1ADECEB800F1EFE7 /* ReadingListStorageTestCase.swift */; }; + E4D6BEB91A0930EC00F538BD /* LaunchScreen.xib in Resources */ = {isa = PBXBuildFile; fileRef = E4D6BEB81A0930EC00F538BD /* LaunchScreen.xib */; }; + E4E25CCB1CA99E7400D0F088 /* HexExtensionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4E25CCA1CA99E7400D0F088 /* HexExtensionsTests.swift */; }; + E4E7EB6D1C4AED5E0094275D /* SupportUtilsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4E7EB6C1C4AED5E0094275D /* SupportUtilsTests.swift */; }; + E4ECCDAE1AB131770005E717 /* FiraSans-Medium.ttf in Resources */ = {isa = PBXBuildFile; fileRef = E4ECCDAD1AB131770005E717 /* FiraSans-Medium.ttf */; }; + E4F21AA61A13C4A300B0FAAA /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = E4F21AA51A13C4A300B0FAAA /* Images.xcassets */; }; + E4F2DAC01F620C0300A556CD /* Leanplum.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E4F2DABF1F620C0200A556CD /* Leanplum.framework */; }; + E4F2DAC11F62C73900A556CD /* Leanplum.framework in Copy Frameworks */ = {isa = PBXBuildFile; fileRef = E4F2DABF1F620C0200A556CD /* Leanplum.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + E60138651C89EB7600DF9756 /* Shared.framework in Copy Frameworks */ = {isa = PBXBuildFile; fileRef = 288A2D861AB8B3260023ABC3 /* Shared.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + E60138661C89EB7D00DF9756 /* Storage.framework in Copy Frameworks */ = {isa = PBXBuildFile; fileRef = 2FCAE21A1ABB51F800877008 /* Storage.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + E60138671C89EB8300DF9756 /* Account.framework in Copy Frameworks */ = {isa = PBXBuildFile; fileRef = 2FA435FB1ABB83B4008031D1 /* Account.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + E60138681C89EB8800DF9756 /* Sync.framework in Copy Frameworks */ = {isa = PBXBuildFile; fileRef = 2827315E1ABC9BE600AA1954 /* Sync.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + E60138691C89EB8E00DF9756 /* ReadingList.framework in Copy Frameworks */ = {isa = PBXBuildFile; fileRef = E4D567181ADECE2700F1EFE7 /* ReadingList.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + E601386A1C89EB9800DF9756 /* SQLite.framework in Copy Frameworks */ = {isa = PBXBuildFile; fileRef = 7B9A62DB1C4002B1005C83DC /* SQLite.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + E60222DC1C6E55610061C436 /* FxA.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 28CE83D01A1D1D5100576538 /* FxA.framework */; }; + E60222E31C6E57B20061C436 /* SQLite.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7B9A62DB1C4002B1005C83DC /* SQLite.framework */; }; + E60D03181D511398002FE3F6 /* SyncStatusResolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = E60D03171D511398002FE3F6 /* SyncStatusResolver.swift */; }; + E60D03261D511554002FE3F6 /* SyncStatusResolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = E60D03171D511398002FE3F6 /* SyncStatusResolver.swift */; }; + E60D03271D511554002FE3F6 /* SyncStatusResolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = E60D03171D511398002FE3F6 /* SyncStatusResolver.swift */; }; + E60D03281D511555002FE3F6 /* SyncStatusResolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = E60D03171D511398002FE3F6 /* SyncStatusResolver.swift */; }; + E60D032A1D5118DB002FE3F6 /* SyncStatusResolverTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E60D03291D5118DB002FE3F6 /* SyncStatusResolverTests.swift */; }; + E6108FF91C84E91C005D25E8 /* BasePasscodeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6108FF81C84E91C005D25E8 /* BasePasscodeViewController.swift */; }; + E610B0191EDC68FC00DC9372 /* EventTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E610B0181EDC68FC00DC9372 /* EventTests.swift */; }; + E61453BE1B750A1700C3F9D7 /* RollingFileLoggerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E61453BD1B750A1700C3F9D7 /* RollingFileLoggerTests.swift */; }; + E61D11681EAF8F43008A305B /* PanelDataObserversTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E61D11671EAF8F43008A305B /* PanelDataObserversTests.swift */; }; + E6231C011B90A44F005ABB0D /* libz.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = E6231C001B90A44F005ABB0D /* libz.tbd */; }; + E6231C031B90A466005ABB0D /* libstdc++.6.0.9.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = E6231C021B90A466005ABB0D /* libstdc++.6.0.9.tbd */; }; + E6231C051B90A472005ABB0D /* libxml2.2.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = E6231C041B90A472005ABB0D /* libxml2.2.tbd */; }; + E6231C071B90A712005ABB0D /* libz.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = E6231C001B90A44F005ABB0D /* libz.tbd */; }; + E6231C081B90A71E005ABB0D /* libz.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = E6231C001B90A44F005ABB0D /* libz.tbd */; }; + E62CFB3D1C89EFF700C01324 /* FxA.framework in Copy Frameworks */ = {isa = PBXBuildFile; fileRef = 28CE83D01A1D1D5100576538 /* FxA.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + E6327A641BF6438E008D12E0 /* DebugSettingsBundleOptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6327A631BF6438E008D12E0 /* DebugSettingsBundleOptions.swift */; }; + E633E2DA1C21EAF8001FFF6C /* LoginDetailViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E633E2D91C21EAF8001FFF6C /* LoginDetailViewController.swift */; }; + E633E37A1C2204BE001FFF6C /* LoginManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E633E3791C2204BE001FFF6C /* LoginManagerTests.swift */; }; + E63ED7D81BFCD9990097D08E /* LoginTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = E63ED7D71BFCD9990097D08E /* LoginTableViewCell.swift */; }; + E63ED8E11BFD25580097D08E /* LoginListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E63ED8E01BFD25580097D08E /* LoginListViewController.swift */; }; + E63F71881DB7FBE200A995C9 /* TestSQLiteMetadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = E63F71871DB7FBE200A995C9 /* TestSQLiteMetadata.swift */; }; + E640E85E1C73A45A00C5F072 /* PasscodeEntryViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E640E85D1C73A45A00C5F072 /* PasscodeEntryViewController.swift */; }; + E640E86A1C73A47C00C5F072 /* PasscodeViews.swift in Sources */ = {isa = PBXBuildFile; fileRef = E640E8691C73A47C00C5F072 /* PasscodeViews.swift */; }; + E64ED8FA1BC55AE300DAF864 /* UIAlertControllerExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E64ED8F91BC55AE300DAF864 /* UIAlertControllerExtensions.swift */; }; + E650754E1E37F6AE006961AC /* GeometryExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E650754D1E37F6AE006961AC /* GeometryExtensions.swift */; }; + E65075511E37F6D1006961AC /* NSURLExtensionsMailTo.swift in Sources */ = {isa = PBXBuildFile; fileRef = E650754F1E37F6D1006961AC /* NSURLExtensionsMailTo.swift */; }; + E65075521E37F6D1006961AC /* UIViewExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E65075501E37F6D1006961AC /* UIViewExtensions.swift */; }; + E65075541E37F6FC006961AC /* DynamicFontHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = E65075531E37F6FC006961AC /* DynamicFontHelper.swift */; }; + E65075571E37F714006961AC /* FaviconFetcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = E65075561E37F714006961AC /* FaviconFetcher.swift */; }; + E650755C1E37F747006961AC /* Swizzling.m in Sources */ = {isa = PBXBuildFile; fileRef = E650755B1E37F747006961AC /* Swizzling.m */; }; + E650755F1E37F756006961AC /* Try.m in Sources */ = {isa = PBXBuildFile; fileRef = E650755E1E37F756006961AC /* Try.m */; }; + E65075611E37F77D006961AC /* MenuHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = E65075601E37F77D006961AC /* MenuHelper.swift */; }; + E65075921E37F7AB006961AC /* Accessibility.swift in Sources */ = {isa = PBXBuildFile; fileRef = E65075621E37F7AB006961AC /* Accessibility.swift */; }; + E65075931E37F7AB006961AC /* AppConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = E65075631E37F7AB006961AC /* AppConstants.swift */; }; + E65075941E37F7AB006961AC /* AppInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = E65075641E37F7AB006961AC /* AppInfo.swift */; }; + E65075951E37F7AB006961AC /* AssertionUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = E65075651E37F7AB006961AC /* AssertionUtils.swift */; }; + E65075961E37F7AB006961AC /* AsyncReducer.swift in Sources */ = {isa = PBXBuildFile; fileRef = E65075661E37F7AB006961AC /* AsyncReducer.swift */; }; + E65075971E37F7AB006961AC /* AuthenticationKeychainInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = E65075671E37F7AB006961AC /* AuthenticationKeychainInfo.swift */; }; + E65075981E37F7AB006961AC /* Bytes.swift in Sources */ = {isa = PBXBuildFile; fileRef = E65075681E37F7AB006961AC /* Bytes.swift */; }; + E65075991E37F7AB006961AC /* Cancellable.swift in Sources */ = {isa = PBXBuildFile; fileRef = E65075691E37F7AB006961AC /* Cancellable.swift */; }; + E650759A1E37F7AB006961AC /* CrashSimulator.h in Headers */ = {isa = PBXBuildFile; fileRef = E650756A1E37F7AB006961AC /* CrashSimulator.h */; }; + E650759B1E37F7AB006961AC /* CrashSimulator.m in Sources */ = {isa = PBXBuildFile; fileRef = E650756B1E37F7AB006961AC /* CrashSimulator.m */; }; + E650759C1E37F7AB006961AC /* DeferredUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = E650756C1E37F7AB006961AC /* DeferredUtils.swift */; }; + E650759D1E37F7AB006961AC /* DeviceInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = E650756D1E37F7AB006961AC /* DeviceInfo.swift */; }; + E650759E1E37F7AB006961AC /* effective_tld_names.dat in Resources */ = {isa = PBXBuildFile; fileRef = E650756E1E37F7AB006961AC /* effective_tld_names.dat */; }; + E65075A01E37F7AB006961AC /* ArrayExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E65075711E37F7AB006961AC /* ArrayExtensions.swift */; }; + E65075A11E37F7AB006961AC /* HashExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E65075721E37F7AB006961AC /* HashExtensions.swift */; }; + E65075A21E37F7AB006961AC /* HexExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E65075731E37F7AB006961AC /* HexExtensions.swift */; }; + E65075A31E37F7AB006961AC /* KeychainWrapperExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E65075741E37F7AB006961AC /* KeychainWrapperExtensions.swift */; }; + E65075A41E37F7AB006961AC /* NSCharacterSetExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E65075751E37F7AB006961AC /* NSCharacterSetExtensions.swift */; }; + E65075A51E37F7AB006961AC /* NSFileManagerExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E65075761E37F7AB006961AC /* NSFileManagerExtensions.swift */; }; + E65075A61E37F7AB006961AC /* NSMutableAttributedStringExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E65075771E37F7AB006961AC /* NSMutableAttributedStringExtensions.swift */; }; + E65075A71E37F7AB006961AC /* NSScannerExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E65075781E37F7AB006961AC /* NSScannerExtensions.swift */; }; + E65075A81E37F7AB006961AC /* NSStringExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E65075791E37F7AB006961AC /* NSStringExtensions.swift */; }; + E65075A91E37F7AB006961AC /* NSURLExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E650757A1E37F7AB006961AC /* NSURLExtensions.swift */; }; + E65075AA1E37F7AB006961AC /* NSURLProtectionSpaceExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E650757B1E37F7AB006961AC /* NSURLProtectionSpaceExtensions.swift */; }; + E65075AB1E37F7AB006961AC /* SetExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E650757C1E37F7AB006961AC /* SetExtensions.swift */; }; + E65075AC1E37F7AB006961AC /* StringExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E650757D1E37F7AB006961AC /* StringExtensions.swift */; }; + E65075AD1E37F7AB006961AC /* UIColorExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E650757E1E37F7AB006961AC /* UIColorExtensions.swift */; }; + E65075AE1E37F7AB006961AC /* UIImageExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E650757F1E37F7AB006961AC /* UIImageExtensions.swift */; }; + E65075B01E37F7AB006961AC /* FSUtils.h in Headers */ = {isa = PBXBuildFile; fileRef = E65075811E37F7AB006961AC /* FSUtils.h */; }; + E65075B11E37F7AB006961AC /* FSUtils.m in Sources */ = {isa = PBXBuildFile; fileRef = E65075821E37F7AB006961AC /* FSUtils.m */; }; + E65075B21E37F7AB006961AC /* Functions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E65075831E37F7AB006961AC /* Functions.swift */; }; + E65075B31E37F7AB006961AC /* KeyboardHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = E65075841E37F7AB006961AC /* KeyboardHelper.swift */; }; + E65075B41E37F7AB006961AC /* KeychainCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = E65075851E37F7AB006961AC /* KeychainCache.swift */; }; + E65075B51E37F7AB006961AC /* LaunchArguments.swift in Sources */ = {isa = PBXBuildFile; fileRef = E65075861E37F7AB006961AC /* LaunchArguments.swift */; }; + E65075B61E37F7AB006961AC /* Loader.swift in Sources */ = {isa = PBXBuildFile; fileRef = E65075871E37F7AB006961AC /* Loader.swift */; }; + E65075B71E37F7AB006961AC /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = E65075881E37F7AB006961AC /* Logger.swift */; }; + E65075B81E37F7AB006961AC /* NotificationConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = E65075891E37F7AB006961AC /* NotificationConstants.swift */; }; + E65075BA1E37F7AB006961AC /* Prefs.swift in Sources */ = {isa = PBXBuildFile; fileRef = E650758B1E37F7AB006961AC /* Prefs.swift */; }; + E65075BB1E37F7AB006961AC /* RollingFileLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = E650758C1E37F7AB006961AC /* RollingFileLogger.swift */; }; + E65075BC1E37F7AB006961AC /* SupportUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = E650758D1E37F7AB006961AC /* SupportUtils.swift */; }; + E65075BD1E37F7AB006961AC /* SystemUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = E650758E1E37F7AB006961AC /* SystemUtils.swift */; }; + E65075BE1E37F7AB006961AC /* TimeConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = E650758F1E37F7AB006961AC /* TimeConstants.swift */; }; + E65075BF1E37F7AB006961AC /* UserAgent.swift in Sources */ = {isa = PBXBuildFile; fileRef = E65075901E37F7AB006961AC /* UserAgent.swift */; }; + E65075C01E37F7AB006961AC /* WeakList.swift in Sources */ = {isa = PBXBuildFile; fileRef = E65075911E37F7AB006961AC /* WeakList.swift */; }; + E65075C21E37F956006961AC /* ExtensionUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = E65075C11E37F956006961AC /* ExtensionUtils.swift */; }; + E653422D1C5944F90039DD9E /* BrowserPrompts.swift in Sources */ = {isa = PBXBuildFile; fileRef = E653422C1C5944F90039DD9E /* BrowserPrompts.swift */; }; + E65607611C08B4E200534B02 /* SearchInputView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E65607601C08B4E200534B02 /* SearchInputView.swift */; }; + E65D89181C8647420006EA35 /* AppAuthenticator.swift in Sources */ = {isa = PBXBuildFile; fileRef = E65D89171C8647420006EA35 /* AppAuthenticator.swift */; }; + E660BDD91BB06521009AC090 /* TabsButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = E660BDD81BB06521009AC090 /* TabsButton.swift */; }; + E660BE061BB0666D009AC090 /* InnerStrokedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E660BE051BB0666D009AC090 /* InnerStrokedView.swift */; }; + E663D5781BB341C4001EF30E /* ToggleButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = E663D5771BB341C4001EF30E /* ToggleButton.swift */; }; + E66C5B481BDA81050051AA93 /* UIImage+ImageEffects.m in Sources */ = {isa = PBXBuildFile; fileRef = E66C5B471BDA81050051AA93 /* UIImage+ImageEffects.m */; }; + E67035EE1E538B500037AB83 /* Mocking.swift in Sources */ = {isa = PBXBuildFile; fileRef = E67035ED1E538B500037AB83 /* Mocking.swift */; }; + E67422C51CFF2D39009E8373 /* youtube.ico in Resources */ = {isa = PBXBuildFile; fileRef = E67422C41CFF2D39009E8373 /* youtube.ico */; }; + E677F0451D9423FB00ECF1FB /* SQLiteMetadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = E677F0441D9423FB00ECF1FB /* SQLiteMetadata.swift */; }; + E677F0541D94247300ECF1FB /* Metadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = E677F0531D94247300ECF1FB /* Metadata.swift */; }; + E67D57031D527449003917B1 /* BatchingClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = E67D57021D527449003917B1 /* BatchingClient.swift */; }; + E683F0A61E92E0820035D990 /* MockableHistory.swift in Sources */ = {isa = PBXBuildFile; fileRef = E683F0A51E92E0820035D990 /* MockableHistory.swift */; }; + E683F0C21E93D4E90035D990 /* DictionaryExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E683F0C11E93D4E90035D990 /* DictionaryExtensions.swift */; }; + E689C6F81E0C6E72008BAADB /* FxASignIn.js in Resources */ = {isa = PBXBuildFile; fileRef = E689C6F71E0C6E72008BAADB /* FxASignIn.js */; }; + E689C6FA1E0C6E98008BAADB /* FxAContentViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E689C6F91E0C6E98008BAADB /* FxAContentViewController.swift */; }; + E689C7301E0C7617008BAADB /* NSAttributedStringExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E689C72F1E0C7617008BAADB /* NSAttributedStringExtensions.swift */; }; + E68AEDB01B18F81A00133D99 /* SwipeAnimator.swift in Sources */ = {isa = PBXBuildFile; fileRef = E68AEDAF1B18F81A00133D99 /* SwipeAnimator.swift */; }; + E68E39BE1C46F42000B85F42 /* AppSettingsTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E68E39BD1C46F42000B85F42 /* AppSettingsTableViewController.swift */; }; + E68E7ACB1CAC1D4500FDCA76 /* PagingPasscodeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E68E7ACA1CAC1D4500FDCA76 /* PagingPasscodeViewController.swift */; }; + E68E7ADA1CAC207400FDCA76 /* ChangePasscodeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E68E7AD91CAC207400FDCA76 /* ChangePasscodeViewController.swift */; }; + E68E7ADC1CAC208200FDCA76 /* SetupPasscodeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E68E7ADB1CAC208200FDCA76 /* SetupPasscodeViewController.swift */; }; + E68E7ADE1CAC208A00FDCA76 /* RemovePasscodeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E68E7ADD1CAC208A00FDCA76 /* RemovePasscodeViewController.swift */; }; + E68F36981EA694000048CF44 /* PanelDataObservers.swift in Sources */ = {isa = PBXBuildFile; fileRef = E68F36971EA694000048CF44 /* PanelDataObservers.swift */; }; + E68F36AB1EA698610048CF44 /* PanelDataObservers.swift in Sources */ = {isa = PBXBuildFile; fileRef = E68F36971EA694000048CF44 /* PanelDataObservers.swift */; }; + E68F36AC1EA698610048CF44 /* PanelDataObservers.swift in Sources */ = {isa = PBXBuildFile; fileRef = E68F36971EA694000048CF44 /* PanelDataObservers.swift */; }; + E68F36AD1EA698650048CF44 /* PanelDataObservers.swift in Sources */ = {isa = PBXBuildFile; fileRef = E68F36971EA694000048CF44 /* PanelDataObservers.swift */; }; + E6927EC01C7B6FB800D03F75 /* ErrorToast.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6927EBF1C7B6FB800D03F75 /* ErrorToast.swift */; }; + E692E3291C46E62D009D1240 /* AuthenticationSettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E692E3281C46E62D009D1240 /* AuthenticationSettingsViewController.swift */; }; + E692E3371C46E86A009D1240 /* AppSettingsOptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E692E3361C46E86A009D1240 /* AppSettingsOptions.swift */; }; + E693F0D91E9D64BD0086DC17 /* OptionalExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E693F0D81E9D64BD0086DC17 /* OptionalExtensions.swift */; }; + E696FE511C47F86E00EC007C /* AuthenticatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E696FE501C47F86E00EC007C /* AuthenticatorTests.swift */; }; + E698FFDA1B4AADF40001F623 /* TabScrollController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E698FFD91B4AADF40001F623 /* TabScrollController.swift */; }; + E69922171B94E3EF007C480D /* Licenses.html in Resources */ = {isa = PBXBuildFile; fileRef = E69922121B94E3EF007C480D /* Licenses.html */; }; + E69DB07E1E97DEAA008A67E6 /* SyncTelemetry.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E69DB0751E97DEA9008A67E6 /* SyncTelemetry.framework */; }; + E69DB0871E97DEAA008A67E6 /* SyncTelemetry.h in Headers */ = {isa = PBXBuildFile; fileRef = E69DB0771E97DEA9008A67E6 /* SyncTelemetry.h */; settings = {ATTRIBUTES = (Public, ); }; }; + E69DB08A1E97DEAA008A67E6 /* SyncTelemetry.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E69DB0751E97DEA9008A67E6 /* SyncTelemetry.framework */; }; + E69DB08B1E97DEAA008A67E6 /* SyncTelemetry.framework in Copy Frameworks */ = {isa = PBXBuildFile; fileRef = E69DB0751E97DEA9008A67E6 /* SyncTelemetry.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + E69DB0A81E97DF22008A67E6 /* SyncTelemetry.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E69DB0751E97DEA9008A67E6 /* SyncTelemetry.framework */; }; + E69DB0A91E97DF73008A67E6 /* Shared.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 288A2D861AB8B3260023ABC3 /* Shared.framework */; }; + E69DB0B71E97E2AC008A67E6 /* SyncTelemetry.swift in Sources */ = {isa = PBXBuildFile; fileRef = E69DB0B51E97E2AC008A67E6 /* SyncTelemetry.swift */; }; + E69DB0BD1E97E301008A67E6 /* ActivityStreamTopics.swift in Sources */ = {isa = PBXBuildFile; fileRef = E69DB0BA1E97E301008A67E6 /* ActivityStreamTopics.swift */; }; + E69DB0C31E97E320008A67E6 /* BookmarkTelemetryPing.swift in Sources */ = {isa = PBXBuildFile; fileRef = E69DB0C01E97E320008A67E6 /* BookmarkTelemetryPing.swift */; }; + E69E06BA1C76173D00D0F926 /* RequirePasscodeIntervalViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E69E06B91C76173D00D0F926 /* RequirePasscodeIntervalViewController.swift */; }; + E69E06C91C76198000D0F926 /* AuthenticationManagerConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = E69E06C81C76198000D0F926 /* AuthenticationManagerConstants.swift */; }; + E6A92ADB1C52A8DA00743291 /* LoginInputTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6A92ADA1C52A8DA00743291 /* LoginInputTests.swift */; }; + E6B4C3D81C68F55C001F97E8 /* JSPrompt.html in Resources */ = {isa = PBXBuildFile; fileRef = E6B4C3D71C68F55C001F97E8 /* JSPrompt.html */; }; + E6B4C4031C68F58B001F97E8 /* BrowserTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6B4C4021C68F58B001F97E8 /* BrowserTests.swift */; }; + E6BA20211E52170E00697F9C /* SyncTelemetryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6BA20111E52165800697F9C /* SyncTelemetryTests.swift */; }; + E6BE53CD1D9177B10074909A /* TestSQLiteHistoryRecommendations.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6BE53CC1D9177B10074909A /* TestSQLiteHistoryRecommendations.swift */; }; + E6C70E821E28314700F8DB57 /* PingCentreTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6C70E811E28314700F8DB57 /* PingCentreTests.swift */; }; + E6C9EB6B1E2FBFC300D5CE80 /* signedInUser.json in Resources */ = {isa = PBXBuildFile; fileRef = E6C9EB6A1E2FBFC300D5CE80 /* signedInUser.json */; }; + E6CF28E71CB43B7900151AB3 /* SensitiveViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6CF28E61CB43B7900151AB3 /* SensitiveViewController.swift */; }; + E6D7C32B1CF4E86C00E746BA /* TestBookmarkModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6D7C31C1CF4E68D00E746BA /* TestBookmarkModel.swift */; }; + E6D8D5E71B569D70009E5A58 /* BrowserTrayAnimators.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6D8D5E61B569D70009E5A58 /* BrowserTrayAnimators.swift */; }; + E6EAC5961B29CB3A00E1DE1E /* scrollablePage.html in Resources */ = {isa = PBXBuildFile; fileRef = E6EAC5951B29CB3A00E1DE1E /* scrollablePage.html */; }; + E6EC6EED1E53548A0067985D /* EarlGrey.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0B21E8051E26CCB7000C8779 /* EarlGrey.framework */; }; + E6ECF2381C974E0F00B0DC93 /* KIF.framework in CopyFiles */ = {isa = PBXBuildFile; fileRef = D30EBB6A1C75503800105AE9 /* KIF.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + E6EDE82C1D5244AF007A0732 /* BatchingClientTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6EDE81D1D524475007A0732 /* BatchingClientTests.swift */; }; + E6F368291D7F594F008CDD67 /* SQLiteHistoryRecommendations.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6F368281D7F594F008CDD67 /* SQLiteHistoryRecommendations.swift */; }; + E6F6D6A31E9E6B2A0012072B /* DeferredTestUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6F6D6A11E9E6AF90012072B /* DeferredTestUtils.swift */; }; + E6F6D6A41E9E6B2B0012072B /* DeferredTestUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6F6D6A11E9E6AF90012072B /* DeferredTestUtils.swift */; }; + E6F6D6A51E9E6B2B0012072B /* DeferredTestUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6F6D6A11E9E6AF90012072B /* DeferredTestUtils.swift */; }; + E6F6D6A61E9E6B2C0012072B /* DeferredTestUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6F6D6A11E9E6AF90012072B /* DeferredTestUtils.swift */; }; + E6F6D6A71E9E6B2D0012072B /* DeferredTestUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6F6D6A11E9E6AF90012072B /* DeferredTestUtils.swift */; }; + E6F6D6AA1E9E6B300012072B /* DeferredTestUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6F6D6A11E9E6AF90012072B /* DeferredTestUtils.swift */; }; + E6F6D6AB1E9E6B330012072B /* DeferredTestUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6F6D6A11E9E6AF90012072B /* DeferredTestUtils.swift */; }; + E6F965121B2F1CF20034B023 /* Shared.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 288A2D861AB8B3260023ABC3 /* Shared.framework */; }; + E6F9653C1B2F1D5D0034B023 /* NSURLExtensionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6F9653B1B2F1D5D0034B023 /* NSURLExtensionsTests.swift */; }; + E6F965421B2F25110034B023 /* NSMutableAttributedStringExtensionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6F965411B2F25110034B023 /* NSMutableAttributedStringExtensionsTests.swift */; }; + E6FF6ACA1D873CFF0070C294 /* PageMetadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6FF6AC91D873CFF0070C294 /* PageMetadata.swift */; }; + EB2A63341F3B49A7004EF8B0 /* ContentBlockerHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB2A63251F3B49A7004EF8B0 /* ContentBlockerHelper.swift */; }; + EB2A633B1F3B513B004EF8B0 /* disconnect-advertising.json in Resources */ = {isa = PBXBuildFile; fileRef = EB2A63381F3B5135004EF8B0 /* disconnect-advertising.json */; }; + EB2A633C1F3B513E004EF8B0 /* disconnect-analytics.json in Resources */ = {isa = PBXBuildFile; fileRef = EB2A63361F3B5134004EF8B0 /* disconnect-analytics.json */; }; + EB2A633E1F3B514A004EF8B0 /* disconnect-content.json in Resources */ = {isa = PBXBuildFile; fileRef = EB2A63371F3B5135004EF8B0 /* disconnect-content.json */; }; + EB2A633F1F3B514B004EF8B0 /* disconnect-social.json in Resources */ = {isa = PBXBuildFile; fileRef = EB2A63391F3B5135004EF8B0 /* disconnect-social.json */; }; + EBA31D791F7999030055463D /* SyncPingCentre.swift in Sources */ = {isa = PBXBuildFile; fileRef = EBA31D781F7999030055463D /* SyncPingCentre.swift */; }; + EBA31D7B1F79990C0055463D /* SyncTelemetryEvents.swift in Sources */ = {isa = PBXBuildFile; fileRef = EBA31D7A1F79990C0055463D /* SyncTelemetryEvents.swift */; }; + EBA31D7D1F79996E0055463D /* SyncTelemetryUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = EBA31D7C1F79996E0055463D /* SyncTelemetryUtils.swift */; }; + EBA31D861F799BE20055463D /* Telemetry.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = EBA31D7E1F799BE20055463D /* Telemetry.framework */; }; + EBF47E701F7979DF00899189 /* UnifiedTelemetry.swift in Sources */ = {isa = PBXBuildFile; fileRef = EBF47E6F1F7979DF00899189 /* UnifiedTelemetry.swift */; }; + F35B8D2B1D6380EA008E3D61 /* SessionRestore.html in Resources */ = {isa = PBXBuildFile; fileRef = F35B8D2A1D6380EA008E3D61 /* SessionRestore.html */; }; + F35B8D2D1D6383E9008E3D61 /* SessionRestoreHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = F35B8D2C1D6383E9008E3D61 /* SessionRestoreHelper.swift */; }; + F35B8D2F1D638408008E3D61 /* SessionRestoreHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = F35B8D2E1D638408008E3D61 /* SessionRestoreHandler.swift */; }; + F84B21DA1A090F8100AAB793 /* ClientTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F84B21D91A090F8100AAB793 /* ClientTests.swift */; }; + F84B22041A0910F600AAB793 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = F84B21E51A0910F600AAB793 /* AppDelegate.swift */; }; + F84B220B1A0910F600AAB793 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = F84B21EF1A0910F600AAB793 /* Images.xcassets */; }; + F84B22241A09122500AAB793 /* HomePanelViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F84B22221A09122500AAB793 /* HomePanelViewController.swift */; }; + F84B22541A0920C600AAB793 /* ShareTo.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = F84B22491A0920C600AAB793 /* ShareTo.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; + F84B22661A09210A00AAB793 /* SendTo.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = F84B225C1A09210A00AAB793 /* SendTo.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; + F8708D2A1A0970B40051AB07 /* ActionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8708D201A0970990051AB07 /* ActionViewController.swift */; }; + F8708D2C1A0970B40051AB07 /* MainInterface.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = F8708D221A0970990051AB07 /* MainInterface.storyboard */; }; + F8708D2E1A0970B70051AB07 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = F8708D251A0970990051AB07 /* Images.xcassets */; }; + F8708D321A0970B70051AB07 /* ShareViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8708D291A0970990051AB07 /* ShareViewController.swift */; }; + FA6B2AC21D41F02D00429414 /* Punycode.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA6B2AC11D41F02D00429414 /* Punycode.swift */; }; + FA6B2AC31D41F02D00429414 /* Punycode.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA6B2AC11D41F02D00429414 /* Punycode.swift */; }; + FA6B2AC41D41F02D00429414 /* Punycode.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA6B2AC11D41F02D00429414 /* Punycode.swift */; }; + FA9293D41D6580E100AC8D33 /* QRCodeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA9293D31D6580E100AC8D33 /* QRCodeViewController.swift */; }; + FA9294011D6584A200AC8D33 /* QRCode.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = FA9294001D6584A200AC8D33 /* QRCode.xcassets */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 0B742CCB1B32491400EE9264 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 0B742CC61B32491400EE9264 /* sqlcipher.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = D2AAC046055464E500DB518D; + remoteInfo = sqlcipher; + }; + 0B742CCD1B32493800EE9264 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 0B742CC61B32491400EE9264 /* sqlcipher.xcodeproj */; + proxyType = 1; + remoteGlobalIDString = D2AAC045055464E500DB518D; + remoteInfo = sqlcipher; + }; + 2827316A1ABC9BE700AA1954 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = F84B21B61A090F8100AAB793 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 2827315D1ABC9BE600AA1954; + remoteInfo = Sync; + }; + 282731731ABC9BE700AA1954 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = F84B21B61A090F8100AAB793 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 2827315D1ABC9BE600AA1954; + remoteInfo = Sync; + }; + 288A2D9B1AB8B3260023ABC3 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = F84B21B61A090F8100AAB793 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 288A2D851AB8B3260023ABC3; + remoteInfo = Shared; + }; + 28CE83CF1A1D1D5100576538 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 28CE83CA1A1D1D5100576538 /* FxA.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 28F951FB19D0F9FA00DCE892; + remoteInfo = FxA; + }; + 28CE83D11A1D1D5100576538 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 28CE83CA1A1D1D5100576538 /* FxA.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 28F9520619D0F9FB00DCE892; + remoteInfo = FxATests; + }; + 28ECD9BE1BA1F12B00D829DA /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = F84B21B61A090F8100AAB793 /* Project object */; + proxyType = 1; + remoteGlobalIDString = F84B21BD1A090F8100AAB793; + remoteInfo = Client; + }; + 2F11EE4F1ABCAE910083902D /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = F84B21B61A090F8100AAB793 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 288A2D851AB8B3260023ABC3; + remoteInfo = Shared; + }; + 2F14E1171ABB88CB00FF98DB /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 28CE83CA1A1D1D5100576538 /* FxA.xcodeproj */; + proxyType = 1; + remoteGlobalIDString = 28F951FA19D0F9FA00DCE892; + remoteInfo = FxA; + }; + 2F14E1331ABB88CF00FF98DB /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = F84B21B61A090F8100AAB793 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 288A2D851AB8B3260023ABC3; + remoteInfo = Shared; + }; + 2F77F69C1ABCAEFE00484F3A /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 28CE83CA1A1D1D5100576538 /* FxA.xcodeproj */; + proxyType = 1; + remoteGlobalIDString = 28F951FA19D0F9FA00DCE892; + remoteInfo = FxA; + }; + 2F77F6B81ABCAF0700484F3A /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = F84B21B61A090F8100AAB793 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 2FA435FA1ABB83B4008031D1; + remoteInfo = Account; + }; + 2F9A72311ABB856100F9F05D /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = F84B21B61A090F8100AAB793 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 2FCAE2191ABB51F800877008; + remoteInfo = Storage; + }; + 2F9A72331ABB856A00F9F05D /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = F84B21B61A090F8100AAB793 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 2FCAE2191ABB51F800877008; + remoteInfo = Storage; + }; + 2FA436101ABB83B4008031D1 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = F84B21B61A090F8100AAB793 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 2FA435FA1ABB83B4008031D1; + remoteInfo = Account; + }; + 2FA4361C1ABB83DD008031D1 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = F84B21B61A090F8100AAB793 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 288A2D851AB8B3260023ABC3; + remoteInfo = Shared; + }; + 2FA436431ABB84CB008031D1 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 28CE83CA1A1D1D5100576538 /* FxA.xcodeproj */; + proxyType = 1; + remoteGlobalIDString = 28F951FA19D0F9FA00DCE892; + remoteInfo = FxA; + }; + 2FC87C3C1BC45E67001BAE77 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = F84B21B61A090F8100AAB793 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 2FA435FA1ABB83B4008031D1; + remoteInfo = Account; + }; + 2FCAE2261ABB51F800877008 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = F84B21B61A090F8100AAB793 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 2FCAE2191ABB51F800877008; + remoteInfo = Storage; + }; + 2FCAE2281ABB51F800877008 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = F84B21B61A090F8100AAB793 /* Project object */; + proxyType = 1; + remoteGlobalIDString = F84B21BD1A090F8100AAB793; + remoteInfo = Client; + }; + 2FCAE22F1ABB51F800877008 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = F84B21B61A090F8100AAB793 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 2FCAE2191ABB51F800877008; + remoteInfo = Storage; + }; + 2FCAE23B1ABB520700877008 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = F84B21B61A090F8100AAB793 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 288A2D851AB8B3260023ABC3; + remoteInfo = Shared; + }; + 390527541C874D35007E0BB7 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = F84B21B61A090F8100AAB793 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 390527491C874D35007E0BB7; + remoteInfo = Today; + }; + 397848E01ED86605004C0C0B /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = F84B21B61A090F8100AAB793 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 397848DA1ED86605004C0C0B; + remoteInfo = NotificationService; + }; + 3B43E3D51D95C48D00BBA9DB /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = F84B21B61A090F8100AAB793 /* Project object */; + proxyType = 1; + remoteGlobalIDString = F84B21BD1A090F8100AAB793; + remoteInfo = Client; + }; + 3BFE4B0C1D342FB900DDF53F /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = F84B21B61A090F8100AAB793 /* Project object */; + proxyType = 1; + remoteGlobalIDString = F84B21BD1A090F8100AAB793; + remoteInfo = Client; + }; + 7B10AAE51E3A57F90002DD08 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 7B9A62B91C4002B1005C83DC /* SQLite.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = EE247ADD1C3F04ED00AE3E12; + remoteInfo = "SQLiteTests iOS"; + }; + 7B10AAE71E3A57F90002DD08 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 7B9A62B91C4002B1005C83DC /* SQLite.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = EE247B451C3F3ED000AE3E12; + remoteInfo = "SQLiteTests Mac"; + }; + 7B10AAE91E3A57F90002DD08 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 7B9A62B91C4002B1005C83DC /* SQLite.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 03A65E5A1C6BB0F50062603F; + remoteInfo = "SQLite tvOS"; + }; + 7B10AAEB1E3A57F90002DD08 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 7B9A62B91C4002B1005C83DC /* SQLite.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 03A65E631C6BB0F60062603F; + remoteInfo = "SQLiteTests tvOS"; + }; + 7B10AAED1E3A57F90002DD08 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 7B9A62B91C4002B1005C83DC /* SQLite.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = A121AC451CA35C79005A31D1; + remoteInfo = "SQLite watchOS"; + }; + 7B10AAEF1E3A58C80002DD08 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = F84B21B61A090F8100AAB793 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 288A2D851AB8B3260023ABC3; + remoteInfo = Shared; + }; + 7B9A62DA1C4002B1005C83DC /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 7B9A62B91C4002B1005C83DC /* SQLite.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = DC2B29E21B26F718001C60EA; + remoteInfo = "SQLite iOS"; + }; + 7B9A62DE1C4002B1005C83DC /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 7B9A62B91C4002B1005C83DC /* SQLite.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = DC7253AC1B52ADEB009B38B1; + remoteInfo = "SQLite Mac"; + }; + 7B9A62F81C400303005C83DC /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 7B9A62B91C4002B1005C83DC /* SQLite.xcodeproj */; + proxyType = 1; + remoteGlobalIDString = EE247AD21C3F04ED00AE3E12; + remoteInfo = "SQLite iOS"; + }; + 7B9BF92E1E435DE400CB24F4 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = F84B21B61A090F8100AAB793 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 2FCAE2231ABB51F800877008; + remoteInfo = StorageTests; + }; + 7BEB64421C7345600092C02E /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = F84B21B61A090F8100AAB793 /* Project object */; + proxyType = 1; + remoteGlobalIDString = F84B21BD1A090F8100AAB793; + remoteInfo = Client; + }; + 7BEB645C1C7346100092C02E /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = F84B21B61A090F8100AAB793 /* Project object */; + proxyType = 1; + remoteGlobalIDString = F84B21BD1A090F8100AAB793; + remoteInfo = Client; + }; + D09A0CDE1FAA2577009A0273 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = F84B21B61A090F8100AAB793 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 288A2D851AB8B3260023ABC3; + remoteInfo = Shared; + }; + D09A0CE01FAA25C5009A0273 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = F84B21B61A090F8100AAB793 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 288A2D851AB8B3260023ABC3; + remoteInfo = Shared; + }; + D09A0CE41FAA2C18009A0273 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = F84B21B61A090F8100AAB793 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 288A2D851AB8B3260023ABC3; + remoteInfo = Shared; + }; + D09A0CE61FAA2C1B009A0273 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = F84B21B61A090F8100AAB793 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 2FCAE2191ABB51F800877008; + remoteInfo = Storage; + }; + D09A0CE81FAA2C1E009A0273 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = F84B21B61A090F8100AAB793 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 2827315D1ABC9BE600AA1954; + remoteInfo = Sync; + }; + D09A0CEA1FAA2C38009A0273 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = F84B21B61A090F8100AAB793 /* Project object */; + proxyType = 1; + remoteGlobalIDString = E4D567171ADECE2700F1EFE7; + remoteInfo = ReadingList; + }; + D09A0CEC1FAA2C4C009A0273 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = F84B21B61A090F8100AAB793 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 288A2D851AB8B3260023ABC3; + remoteInfo = Shared; + }; + D09A0CEE1FAA2C66009A0273 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 28CE83CA1A1D1D5100576538 /* FxA.xcodeproj */; + proxyType = 1; + remoteGlobalIDString = 28F951FA19D0F9FA00DCE892; + remoteInfo = FxA; + }; + D09A0CF01FAA2C7E009A0273 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = F84B21B61A090F8100AAB793 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 288A2D851AB8B3260023ABC3; + remoteInfo = Shared; + }; + D09A0CF21FAA2C81009A0273 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = F84B21B61A090F8100AAB793 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 2827315D1ABC9BE600AA1954; + remoteInfo = Sync; + }; + D09A0CF41FAA2C99009A0273 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = F84B21B61A090F8100AAB793 /* Project object */; + proxyType = 1; + remoteGlobalIDString = E4D567171ADECE2700F1EFE7; + remoteInfo = ReadingList; + }; + D09A0CF61FAA2CB2009A0273 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 28CE83CA1A1D1D5100576538 /* FxA.xcodeproj */; + proxyType = 1; + remoteGlobalIDString = 28F951FA19D0F9FA00DCE892; + remoteInfo = FxA; + }; + D09A0CF81FAA2CBF009A0273 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = F84B21B61A090F8100AAB793 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 288A2D851AB8B3260023ABC3; + remoteInfo = Shared; + }; + D09A0CFA1FAA2CC2009A0273 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = F84B21B61A090F8100AAB793 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 2827315D1ABC9BE600AA1954; + remoteInfo = Sync; + }; + D09A0CFC1FAA2CC5009A0273 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = F84B21B61A090F8100AAB793 /* Project object */; + proxyType = 1; + remoteGlobalIDString = E4D567171ADECE2700F1EFE7; + remoteInfo = ReadingList; + }; + D0EA58121FA836CA00D03ED1 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = F84B21B61A090F8100AAB793 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 2827315D1ABC9BE600AA1954; + remoteInfo = Sync; + }; + D0EA58141FA836CE00D03ED1 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = F84B21B61A090F8100AAB793 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 2FCAE2191ABB51F800877008; + remoteInfo = Storage; + }; + D0EA58161FA836D200D03ED1 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = F84B21B61A090F8100AAB793 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 288A2D851AB8B3260023ABC3; + remoteInfo = Shared; + }; + D30EBB631C75503800105AE9 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = D30EBB5A1C75503800105AE9 /* KIF.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = EABD46AA1857A0C700A5F081; + remoteInfo = KIF; + }; + D30EBB651C75503800105AE9 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = D30EBB5A1C75503800105AE9 /* KIF.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = EB60ECC1177F8C83005A041A; + remoteInfo = "Test Host"; + }; + D30EBB671C75503800105AE9 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = D30EBB5A1C75503800105AE9 /* KIF.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = EABD46CD1857A0F300A5F081; + remoteInfo = "KIF Tests"; + }; + D30EBB691C75503800105AE9 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = D30EBB5A1C75503800105AE9 /* KIF.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 9CC9673B1AD4B1B600576D13; + remoteInfo = KIFFramework; + }; + D39FA1651A83E0EC00EE869C /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = F84B21B61A090F8100AAB793 /* Project object */; + proxyType = 1; + remoteGlobalIDString = F84B21BD1A090F8100AAB793; + remoteInfo = Client; + }; + E4A888181A95679500CDC337 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 28CE83CA1A1D1D5100576538 /* FxA.xcodeproj */; + proxyType = 1; + remoteGlobalIDString = 28F951FA19D0F9FA00DCE892; + remoteInfo = FxA; + }; + E4BA8A331B4B0A1700BC2E95 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = F84B21B61A090F8100AAB793 /* Project object */; + proxyType = 1; + remoteGlobalIDString = E4BA8A2A1B4B0A1600BC2E95; + remoteInfo = ViewLater; + }; + E4D567241ADECE2700F1EFE7 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = F84B21B61A090F8100AAB793 /* Project object */; + proxyType = 1; + remoteGlobalIDString = E4D567171ADECE2700F1EFE7; + remoteInfo = ReadingList; + }; + E4D5672D1ADECE2800F1EFE7 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = F84B21B61A090F8100AAB793 /* Project object */; + proxyType = 1; + remoteGlobalIDString = E4D567171ADECE2700F1EFE7; + remoteInfo = ReadingList; + }; + E4EE05AF1BA3A0A10021B3A7 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = F84B21B61A090F8100AAB793 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 2FCAE2191ABB51F800877008; + remoteInfo = Storage; + }; + E60222E51C6E5A770061C436 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 28CE83CA1A1D1D5100576538 /* FxA.xcodeproj */; + proxyType = 1; + remoteGlobalIDString = 28F951FA19D0F9FA00DCE892; + remoteInfo = FxA; + }; + E63CD1B11B31B66400A63AFF /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = F84B21B61A090F8100AAB793 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 288A2D851AB8B3260023ABC3; + remoteInfo = Shared; + }; + E63CD1B21B31B66400A63AFF /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = F84B21B61A090F8100AAB793 /* Project object */; + proxyType = 1; + remoteGlobalIDString = F84B21BD1A090F8100AAB793; + remoteInfo = Client; + }; + E69DB07F1E97DEAA008A67E6 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = F84B21B61A090F8100AAB793 /* Project object */; + proxyType = 1; + remoteGlobalIDString = E69DB0741E97DEA9008A67E6; + remoteInfo = SyncTelemetry; + }; + E69DB0811E97DEAA008A67E6 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = F84B21B61A090F8100AAB793 /* Project object */; + proxyType = 1; + remoteGlobalIDString = F84B21BD1A090F8100AAB793; + remoteInfo = Client; + }; + E69DB0881E97DEAA008A67E6 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = F84B21B61A090F8100AAB793 /* Project object */; + proxyType = 1; + remoteGlobalIDString = E69DB0741E97DEA9008A67E6; + remoteInfo = SyncTelemetry; + }; + E69DB0A61E97DEF6008A67E6 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = F84B21B61A090F8100AAB793 /* Project object */; + proxyType = 1; + remoteGlobalIDString = E69DB0741E97DEA9008A67E6; + remoteInfo = SyncTelemetry; + }; + E6CB64CB1DA42C2900887098 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = D30EBB5A1C75503800105AE9 /* KIF.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = FAB89FFC1CAC546900C6DFC1; + remoteInfo = KIFFrameworkConsumer; + }; + E6CB64CD1DA42C2900887098 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = D30EBB5A1C75503800105AE9 /* KIF.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = FAB8A0141CAC546A00C6DFC1; + remoteInfo = KIFFrameworkConsumerTests; + }; + F84B21D41A090F8100AAB793 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = F84B21B61A090F8100AAB793 /* Project object */; + proxyType = 1; + remoteGlobalIDString = F84B21BD1A090F8100AAB793; + remoteInfo = Client; + }; + F84B22511A0920C600AAB793 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = F84B21B61A090F8100AAB793 /* Project object */; + proxyType = 1; + remoteGlobalIDString = F84B22481A0920C600AAB793; + remoteInfo = ShareToFirefox; + }; + F84B22641A09210A00AAB793 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = F84B21B61A090F8100AAB793 /* Project object */; + proxyType = 1; + remoteGlobalIDString = F84B225B1A09210A00AAB793; + remoteInfo = SendToDevice; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 0B21E8011E26C5D3000C8779 /* CopyFiles */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = "$(TEST_HOST)/.."; + dstSubfolderSpec = 0; + files = ( + 0B21E8061E26CCB7000C8779 /* EarlGrey.framework in CopyFiles */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 2F3444EC1AB2378200FD9731 /* Copy Files */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 7; + files = ( + ); + name = "Copy Files"; + runOnlyForDeploymentPostprocessing = 0; + }; + E6B09CD31C74EEDB00C63FA1 /* Copy Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + E4F2DAC11F62C73900A556CD /* Leanplum.framework in Copy Frameworks */, + E62CFB3D1C89EFF700C01324 /* FxA.framework in Copy Frameworks */, + E601386A1C89EB9800DF9756 /* SQLite.framework in Copy Frameworks */, + E69DB08B1E97DEAA008A67E6 /* SyncTelemetry.framework in Copy Frameworks */, + E60138691C89EB8E00DF9756 /* ReadingList.framework in Copy Frameworks */, + E60138681C89EB8800DF9756 /* Sync.framework in Copy Frameworks */, + E60138671C89EB8300DF9756 /* Account.framework in Copy Frameworks */, + E60138661C89EB7D00DF9756 /* Storage.framework in Copy Frameworks */, + E60138651C89EB7600DF9756 /* Shared.framework in Copy Frameworks */, + ); + name = "Copy Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; + E6ECF2371C974E0600B0DC93 /* CopyFiles */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + E6ECF2381C974E0F00B0DC93 /* KIF.framework in CopyFiles */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + F84B22531A0920C600AAB793 /* Embed App Extensions */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 13; + files = ( + 390527561C874D35007E0BB7 /* Today.appex in Embed App Extensions */, + 397848E21ED86605004C0C0B /* NotificationService.appex in Embed App Extensions */, + F84B22541A0920C600AAB793 /* ShareTo.appex in Embed App Extensions */, + F84B22661A09210A00AAB793 /* SendTo.appex in Embed App Extensions */, + E4BA8A351B4B0A1700BC2E95 /* ViewLater.appex in Embed App Extensions */, + ); + name = "Embed App Extensions"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 03CCC9171AF05E7300DBF30D /* RelativeDatesTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RelativeDatesTests.swift; sourceTree = ""; }; + 0B1C05D51A798B1F004C78B0 /* UIImageViewAligned.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = UIImageViewAligned.h; sourceTree = ""; }; + 0B1C05D61A798B1F004C78B0 /* UIImageViewAligned.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = UIImageViewAligned.m; sourceTree = ""; }; + 0B21E8051E26CCB7000C8779 /* EarlGrey.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = EarlGrey.framework; path = Carthage/Build/iOS/EarlGrey.framework; sourceTree = ""; }; + 0B305E1A1E3A98A900BE0767 /* BookmarkingTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BookmarkingTests.swift; sourceTree = ""; }; + 0B3D670D1E09B90B00C1EFC7 /* AuthenticationTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AuthenticationTest.swift; sourceTree = ""; }; + 0B3E7D931B27A7CE00E2E84D /* AboutHomeHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AboutHomeHandler.swift; sourceTree = ""; }; + 0B3E7DB91B27AB4C00E2E84D /* MockLogins.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = MockLogins.swift; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; + 0B54BD181B698B7C004C822C /* SuggestedSites.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SuggestedSites.swift; sourceTree = ""; }; + 0B5A93211B1EB4C8004F47A2 /* ReadingListTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReadingListTest.swift; sourceTree = ""; }; + 0B5A93411B1EB572004F47A2 /* readablePage.html */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.html; path = readablePage.html; sourceTree = ""; }; + 0B62EFD11AD63CD100ACB9CD /* Clearables.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Clearables.swift; sourceTree = ""; }; + 0B6FBAB11AC1F830007EC669 /* numberedPage.html */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.html; path = numberedPage.html; sourceTree = ""; }; + 0B729D361E047D6A008E6859 /* HomePageSettingsTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HomePageSettingsTest.swift; sourceTree = ""; }; + 0B742CC61B32491400EE9264 /* sqlcipher.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = sqlcipher.xcodeproj; path = ThirdParty/sqlcipher/sqlcipher.xcodeproj; sourceTree = ""; }; + 0B7C1E941F6097AD006A8869 /* TrackingProtectionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrackingProtectionTests.swift; sourceTree = ""; }; + 0B8E0FF31A932BD500161DC3 /* ImageIO.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = ImageIO.framework; path = System/Library/Frameworks/ImageIO.framework; sourceTree = SDKROOT; }; + 0B9D40781E8D5AC80059E664 /* XCUITests-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "XCUITests-Bridging-Header.h"; sourceTree = ""; }; + 0BA1E00D1B03FB0B007675AF /* NetError.html */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.html; path = NetError.html; sourceTree = ""; }; + 0BA1E02D1B046F1E007675AF /* ErrorPageHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ErrorPageHelper.swift; sourceTree = ""; }; + 0BA1E02F1B051A07007675AF /* NetError.css */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.css; path = NetError.css; sourceTree = ""; }; + 0BA896491A250E6500C1010C /* ProfileTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProfileTest.swift; sourceTree = ""; }; + 0BA8964A1A250E6500C1010C /* TestBookmarks.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestBookmarks.swift; sourceTree = ""; }; + 0BB5B2861AC0A2B90052877D /* SnackBar.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SnackBar.swift; sourceTree = ""; }; + 0BB5B2871AC0A2B90052877D /* Toolbar.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Toolbar.swift; sourceTree = ""; }; + 0BB5B30A1AC0AD1F0052877D /* LoginsHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = LoginsHelper.swift; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; + 0BC9C9C31F26F54D000E8AB5 /* SiteLoadTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SiteLoadTest.swift; sourceTree = ""; }; + 0BD19A661A25309B0084FBA7 /* NSUserDefaultsPrefs.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NSUserDefaultsPrefs.swift; sourceTree = ""; }; + 0BDA568A1B26B1BE008C9B96 /* LoginsHelper.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; path = LoginsHelper.js; sourceTree = ""; }; + 0BDA56AE1B26B1D5008C9B96 /* TestLogins.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestLogins.swift; sourceTree = ""; }; + 0BDA56B11B26B1E4008C9B96 /* Logins.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = Logins.swift; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; + 0BDA56B31B26B203008C9B96 /* SQLiteLogins.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SQLiteLogins.swift; sourceTree = ""; }; + 0BEF44621E31165700187C32 /* EarlGrey.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EarlGrey.swift; sourceTree = ""; }; + 0BF0DB931A8545800039F300 /* URLBarView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = URLBarView.swift; sourceTree = ""; }; + 0BF1B7E21AC60DEA00A7B407 /* InsetButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InsetButton.swift; sourceTree = ""; }; + 0BF42D361A7C0B8E00889E28 /* FaviconManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FaviconManager.swift; sourceTree = ""; }; + 0BF42D381A7C0E8900889E28 /* Favicons.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; path = Favicons.js; sourceTree = ""; }; + 0BF42D4E1A7CD09600889E28 /* TestFavicons.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestFavicons.swift; sourceTree = ""; }; + 0BF8F8D91AEFF1C900E90BC2 /* noTitle.html */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.html; path = noTitle.html; sourceTree = ""; }; + 19DE1F661EC13B6400428B8C /* LeanplumIntegration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LeanplumIntegration.swift; sourceTree = ""; }; + 280CB1361C20E0A3005AF147 /* BookmarksSynchronizer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = BookmarksSynchronizer.swift; path = Sync/Synchronizers/Bookmarks/BookmarksSynchronizer.swift; sourceTree = SOURCE_ROOT; }; + 28126F471C2F948E006466CC /* SQLiteBookmarksHelpers.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SQLiteBookmarksHelpers.swift; sourceTree = ""; }; + 28126F6D1C2F94F9006466CC /* SQLiteBookmarksModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SQLiteBookmarksModel.swift; sourceTree = ""; }; + 28126F731C2F96F1006466CC /* SQLiteBookmarksResetting.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SQLiteBookmarksResetting.swift; sourceTree = ""; }; + 28126F761C2F9833006466CC /* SQLiteBookmarksBase.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SQLiteBookmarksBase.swift; sourceTree = ""; }; + 2816EFFF1B33E05400522243 /* UIConstants.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIConstants.swift; sourceTree = ""; }; + 281B02991C037C1F005202C3 /* TestBrowserDB.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestBrowserDB.swift; sourceTree = ""; }; + 281B2BE91ADF4D90002917DC /* MockProfile.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = MockProfile.swift; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; + 2827315E1ABC9BE600AA1954 /* Sync.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Sync.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 282731611ABC9BE600AA1954 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 282731621ABC9BE600AA1954 /* Sync-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Sync-Bridging-Header.h"; sourceTree = ""; }; + 282731681ABC9BE700AA1954 /* SyncTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SyncTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 282731701ABC9BE700AA1954 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 282915E51AF1A7920006EEB5 /* BrowserSchema.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BrowserSchema.swift; sourceTree = ""; }; + 2829D3791C2F0A7F00DCF931 /* BookmarksModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = BookmarksModel.swift; path = Bookmarks/BookmarksModel.swift; sourceTree = ""; }; + 2829D39F1C2F0AD400DCF931 /* Sharing.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Sharing.swift; sourceTree = ""; }; + 28302E3F1AF0747800521E2E /* DatabaseError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatabaseError.swift; sourceTree = ""; }; + 283586FC1C73F18E00A55435 /* CachingItemSource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = CachingItemSource.swift; path = Bookmarks/CachingItemSource.swift; sourceTree = ""; }; + 2852B8431C51996B00591EAC /* Trees.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Trees.swift; path = Bookmarks/Trees.swift; sourceTree = ""; }; + 28532CE51C48098D000072D9 /* ThreeWayTreeMerger.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ThreeWayTreeMerger.swift; sourceTree = ""; }; + 28532D311C483E3D000072D9 /* CompletionOps.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CompletionOps.swift; sourceTree = ""; }; + 2853C5401AD84C6800C4F31D /* TabsPayload.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TabsPayload.swift; sourceTree = ""; }; + 2855611E1AEFFA1C00D5ED5B /* HistorySynchronizerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; name = HistorySynchronizerTests.swift; path = SyncTests/HistorySynchronizerTests.swift; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; + 285D3B671B4380B70035FD22 /* Queue.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = Queue.swift; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; + 285D3B8F1B4386520035FD22 /* SQLiteQueue.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SQLiteQueue.swift; sourceTree = ""; }; + 285F2DC01AF80B4600211843 /* SQLiteBookmarksSyncing.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SQLiteBookmarksSyncing.swift; sourceTree = ""; }; + 28786E541AB0F5FA009EA9EF /* DeferredTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DeferredTests.swift; sourceTree = ""; }; + 288501DD1AC0F61D00E7F670 /* RequestExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RequestExtensions.swift; sourceTree = ""; }; + 2885023E1AC117A500E7F670 /* SyncStateMachine.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncStateMachine.swift; sourceTree = ""; }; + 288A2D861AB8B3260023ABC3 /* Shared.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Shared.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 288A2DA71AB8B3700023ABC3 /* Box.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Box.swift; path = ThirdParty/Box/Box.swift; sourceTree = ""; }; + 288A2DA81AB8B3700023ABC3 /* BoxType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = BoxType.swift; path = ThirdParty/Box/BoxType.swift; sourceTree = ""; }; + 288A2DA91AB8B3700023ABC3 /* MutableBox.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = MutableBox.swift; path = ThirdParty/Box/MutableBox.swift; sourceTree = ""; }; + 288A2DB31AB8B38D0023ABC3 /* Error.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Error.swift; path = ThirdParty/Result/Error.swift; sourceTree = ""; }; + 288A2DB41AB8B38D0023ABC3 /* Result.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Result.swift; path = ThirdParty/Result/Result.swift; sourceTree = ""; }; + 288E67C11B9E730900AE2C6A /* BookmarksDownloader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = BookmarksDownloader.swift; path = Sync/Synchronizers/Bookmarks/BookmarksDownloader.swift; sourceTree = SOURCE_ROOT; }; + 2891F2BA1F991185001B105E /* v33.db */ = {isa = PBXFileReference; lastKnownFileType = file; path = v33.db; sourceTree = ""; }; + 28926B191AC0F074009C0B1D /* EncryptedJSON.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EncryptedJSON.swift; sourceTree = ""; }; + 28926B361AC0F0DD009C0B1D /* EnvelopeJSON.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EnvelopeJSON.swift; sourceTree = ""; }; + 28926B391AC0F128009C0B1D /* CleartextPayloadJSON.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CleartextPayloadJSON.swift; sourceTree = ""; }; + 2894C1421AE89D8900F1F92F /* HistorySynchronizer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = HistorySynchronizer.swift; path = Synchronizers/HistorySynchronizer.swift; sourceTree = ""; }; + 2894C1651AE89DD200F1F92F /* TabsSynchronizer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = TabsSynchronizer.swift; path = Synchronizers/TabsSynchronizer.swift; sourceTree = ""; }; + 2894C1681AE89DDC00F1F92F /* ClientsSynchronizer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ClientsSynchronizer.swift; path = Synchronizers/ClientsSynchronizer.swift; sourceTree = ""; }; + 2894C16B1AE89FD500F1F92F /* HistoryPayload.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HistoryPayload.swift; sourceTree = ""; }; + 289A4C121C4EB90600A460E3 /* StorageTestUtils.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StorageTestUtils.swift; sourceTree = ""; }; + 28A17B661BEC727500BC14ED /* Downloader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Downloader.swift; path = Synchronizers/Downloader.swift; sourceTree = ""; }; + 28A6CE891AC082E200C1A2D4 /* UtilsTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UtilsTests.swift; sourceTree = ""; }; + 28AA941C1B97DCA800703DC6 /* BookmarkPayload.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BookmarkPayload.swift; sourceTree = ""; }; + 28B62ACD1BC745E7004A585A /* Syncable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Syncable.swift; sourceTree = ""; }; + 28C077971A3B064000834FE5 /* CryptoTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = CryptoTests.swift; path = SyncTests/CryptoTests.swift; sourceTree = ""; }; + 28C0779D1A3B066000834FE5 /* RecordTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = RecordTests.swift; path = SyncTests/RecordTests.swift; sourceTree = ""; }; + 28C28BFC1C51A3B900D5460E /* Merging.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Merging.swift; sourceTree = ""; }; + 28C4AB711AD42D4300D9ACE3 /* Clients.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Clients.swift; sourceTree = ""; }; + 28C8B7841C852535006D8318 /* BookmarksPanelTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BookmarksPanelTests.swift; sourceTree = ""; }; + 28CE83BB1A1D1D3200576538 /* ClientPayload.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ClientPayload.swift; sourceTree = ""; }; + 28CE83BC1A1D1D3200576538 /* KeyBundle.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KeyBundle.swift; sourceTree = ""; }; + 28CE83BD1A1D1D3200576538 /* KeysPayload.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KeysPayload.swift; sourceTree = ""; }; + 28CE83BE1A1D1D3200576538 /* Record.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Record.swift; sourceTree = ""; }; + 28CE83BF1A1D1D3200576538 /* SyncMeta.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncMeta.swift; sourceTree = ""; }; + 28CE83CA1A1D1D5100576538 /* FxA.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = FxA.xcodeproj; path = FxA/FxA.xcodeproj; sourceTree = ""; }; + 28CE83E81A1D206D00576538 /* Client-Bridging-Header.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "Client-Bridging-Header.h"; path = "../../Client-Bridging-Header.h"; sourceTree = ""; }; + 28D158AC1AFD90E500F9C065 /* TestSQLiteBookmarks.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestSQLiteBookmarks.swift; sourceTree = ""; }; + 28D52E081BCDF44100187A1D /* ResetTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ResetTests.swift; sourceTree = ""; }; + 28D980221C47149000277055 /* TestBookmarkTreeMerging.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = TestBookmarkTreeMerging.swift; path = SyncTests/TestBookmarkTreeMerging.swift; sourceTree = ""; }; + 28E23C111AC5A5EE00F5AC85 /* State.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = State.swift; sourceTree = ""; }; + 28E91E741B443AD5009DF274 /* SyncConstants.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncConstants.swift; sourceTree = ""; }; + 28EADE381AFC3898007FB2FB /* UIImageViewExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = UIImageViewExtensions.swift; path = Extensions/UIImageViewExtensions.swift; sourceTree = ""; }; + 28ECD9791BA1EA2200D829DA /* MockSyncServer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = MockSyncServer.swift; path = SyncTests/MockSyncServer.swift; sourceTree = ""; }; + 28ECD9A21BA1EBA800D829DA /* SyncTests-Bridging-Header.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "SyncTests-Bridging-Header.h"; sourceTree = ""; }; + 28ECD9F31BA1F59800D829DA /* DownloadTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = DownloadTests.swift; path = SyncTests/DownloadTests.swift; sourceTree = ""; }; + 28ED02011B26123E003948B2 /* LoginPayload.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LoginPayload.swift; sourceTree = ""; }; + 28ED02251B262B5E003948B2 /* LoginsSynchronizer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = LoginsSynchronizer.swift; path = Synchronizers/LoginsSynchronizer.swift; sourceTree = ""; }; + 28ED02281B262E0A003948B2 /* IndependentRecordSynchronizer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = IndependentRecordSynchronizer.swift; path = Synchronizers/IndependentRecordSynchronizer.swift; sourceTree = ""; }; + 28F596A01ACA13CA0071DDCC /* InfoTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = InfoTests.swift; path = SyncTests/InfoTests.swift; sourceTree = ""; }; + 28FDFF0B1C1F725800840F86 /* SeparatorTableCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SeparatorTableCell.swift; sourceTree = ""; }; + 2C2A5EF31E68469500F02659 /* PrivateBrowsingTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PrivateBrowsingTest.swift; sourceTree = ""; }; + 2C2A91281FA2410D002E36BD /* HistoryTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryTests.swift; sourceTree = ""; }; + 2C31A7A81E8BFB2200DAC646 /* ReaderViewUITest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReaderViewUITest.swift; sourceTree = ""; }; + 2C31A8461E8D447F00DAC646 /* HomePageSettingsUITest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HomePageSettingsUITest.swift; sourceTree = ""; }; + 2C3406C71E719F00000FD889 /* SettingsTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsTest.swift; sourceTree = ""; }; + 2C8C07761E7800EA00DC1237 /* FindInPageTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FindInPageTest.swift; sourceTree = ""; }; + 2C97EC701E72C80E0092EC18 /* TopTabsTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TopTabsTest.swift; sourceTree = ""; }; + 2CA16FDD1E5F089100332277 /* SearchTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SearchTest.swift; sourceTree = ""; }; + 2CB56E3E1E926BFB00AF7586 /* ToolbarTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ToolbarTest.swift; sourceTree = ""; }; + 2CC1B3EF1E9B861400814EEC /* DomainAutocompleteTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DomainAutocompleteTest.swift; sourceTree = ""; }; + 2CEA6F781E93E3A600D4100E /* SearchSettingsUITest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SearchSettingsUITest.swift; sourceTree = ""; }; + 2CF449A41E7BFE2C00FD7595 /* NavigationTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NavigationTest.swift; sourceTree = ""; }; + 2F13E79A1AC0C02700D75081 /* StringExtensionsTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StringExtensionsTests.swift; sourceTree = ""; }; + 2F14E1391ABB890800FF98DB /* Account-Bridging-Header.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "Account-Bridging-Header.h"; sourceTree = ""; }; + 2F1A3DE01ABE3C90002F1E15 /* FxALoginStateMachine.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = FxALoginStateMachine.swift; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; + 2F1A3DFD1ABE3C9C002F1E15 /* FxALoginStateMachineTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = FxALoginStateMachineTests.swift; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; + 2F3724C41ABF3C01007607FA /* LiveStorageClientTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; name = LiveStorageClientTests.swift; path = SyncTests/LiveStorageClientTests.swift; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; + 2F3724C51ABF3C01007607FA /* StorageClientTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = StorageClientTests.swift; path = SyncTests/StorageClientTests.swift; sourceTree = ""; }; + 2F3724E21ABF3C19007607FA /* Info.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Info.swift; sourceTree = ""; }; + 2F3724E31ABF3C19007607FA /* StorageClient.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StorageClient.swift; sourceTree = ""; }; + 2F3724E41ABF3C19007607FA /* Synchronizer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Synchronizer.swift; path = Synchronizers/Synchronizer.swift; sourceTree = ""; }; + 2F44FA1A1A9D426A00FD20CC /* TestHashExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestHashExtensions.swift; sourceTree = ""; }; + 2F44FC711A9E840300FD20CC /* SettingsNavigationController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsNavigationController.swift; sourceTree = ""; }; + 2F44FCC41A9E85E900FD20CC /* SettingsTableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsTableViewController.swift; sourceTree = ""; }; + 2F44FCC61A9E8CF500FD20CC /* SearchSettingsTableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SearchSettingsTableViewController.swift; sourceTree = ""; }; + 2F44FCCA1A9E972E00FD20CC /* SearchEnginePicker.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SearchEnginePicker.swift; sourceTree = ""; }; + 2F67C5251BB0CB4E00E7B73A /* MetaGlobalTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = MetaGlobalTests.swift; path = SyncTests/MetaGlobalTests.swift; sourceTree = ""; }; + 2F697F7D1A9FD22D009E03AE /* SearchEnginesTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SearchEnginesTests.swift; sourceTree = ""; }; + 2F8C76561BC32F3C00D5E4E0 /* MockSyncServerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = MockSyncServerTests.swift; path = SyncTests/MockSyncServerTests.swift; sourceTree = ""; }; + 2FA435FB1ABB83B4008031D1 /* Account.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Account.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 2FA435FE1ABB83B4008031D1 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 2FA436051ABB83B4008031D1 /* AccountTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = AccountTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 2FA4360D1ABB83B4008031D1 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 2FA436221ABB8436008031D1 /* FirefoxAccount.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FirefoxAccount.swift; sourceTree = ""; }; + 2FA436231ABB8436008031D1 /* FirefoxAccountConfiguration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FirefoxAccountConfiguration.swift; sourceTree = ""; }; + 2FA436251ABB8436008031D1 /* FxAClient10.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FxAClient10.swift; sourceTree = ""; }; + 2FA436271ABB8436008031D1 /* HawkHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HawkHelper.swift; sourceTree = ""; }; + 2FA436281ABB8436008031D1 /* TokenServerClient.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TokenServerClient.swift; sourceTree = ""; }; + 2FA436371ABB8448008031D1 /* FirefoxAccountTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FirefoxAccountTests.swift; sourceTree = ""; }; + 2FA436381ABB8448008031D1 /* FxAClient10Tests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FxAClient10Tests.swift; sourceTree = ""; }; + 2FA4363A1ABB8448008031D1 /* HawkHelperTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HawkHelperTests.swift; sourceTree = ""; }; + 2FA4363B1ABB8448008031D1 /* LiveAccountTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = LiveAccountTest.swift; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; + 2FA4363C1ABB8448008031D1 /* TokenServerClientTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TokenServerClientTests.swift; sourceTree = ""; }; + 2FCAE21A1ABB51F800877008 /* Storage.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Storage.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 2FCAE2241ABB51F800877008 /* StorageTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = StorageTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 2FCAE22C1ABB51F800877008 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 2FCAE23F1ABB531100877008 /* Bookmarks.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Bookmarks.swift; path = Bookmarks/Bookmarks.swift; sourceTree = ""; }; + 2FCAE2411ABB531100877008 /* Cursor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Cursor.swift; sourceTree = ""; }; + 2FCAE2421ABB531100877008 /* Favicons.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Favicons.swift; sourceTree = ""; }; + 2FCAE2431ABB531100877008 /* FileAccessor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FileAccessor.swift; sourceTree = ""; }; + 2FCAE2441ABB531100877008 /* History.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = History.swift; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; + 2FCAE2471ABB531100877008 /* RemoteTabs.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RemoteTabs.swift; sourceTree = ""; }; + 2FCAE2481ABB531100877008 /* Site.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Site.swift; sourceTree = ""; }; + 2FCAE24B1ABB531100877008 /* BrowserDB.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BrowserDB.swift; sourceTree = ""; }; + 2FCAE2551ABB531100877008 /* SQLiteHistory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SQLiteHistory.swift; sourceTree = ""; }; + 2FCAE2581ABB531100877008 /* SQLiteRemoteClientsAndTabs.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SQLiteRemoteClientsAndTabs.swift; sourceTree = ""; }; + 2FCAE25B1ABB531100877008 /* SwiftData.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwiftData.swift; sourceTree = ""; }; + 2FCAE25C1ABB531100877008 /* Visit.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Visit.swift; sourceTree = ""; }; + 2FCAE2791ABB533A00877008 /* MockFiles.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockFiles.swift; sourceTree = ""; }; + 2FCAE27A1ABB533A00877008 /* TestSQLiteRemoteClientsAndTabs.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestSQLiteRemoteClientsAndTabs.swift; sourceTree = ""; }; + 2FCAE27D1ABB533A00877008 /* TestSQLiteHistory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestSQLiteHistory.swift; sourceTree = ""; }; + 2FCAE33D1ABB5F1800877008 /* Storage-Bridging-Header.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "Storage-Bridging-Header.h"; sourceTree = ""; }; + 2FDB10921A9FBEC5006CF312 /* PrefsTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PrefsTests.swift; sourceTree = ""; }; + 2FDBCF601ABFC9DE00AFF7F0 /* SyncAuthState.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncAuthState.swift; sourceTree = ""; }; + 2FDBCF9A1AC0ADB500AFF7F0 /* SyncAuthStateTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncAuthStateTests.swift; sourceTree = ""; }; + 2FDE87FD1ABB3817005317B1 /* RemoteTabsPanel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RemoteTabsPanel.swift; sourceTree = ""; }; + 2FEBABAE1AB3659000DB5728 /* ResultTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ResultTests.swift; sourceTree = ""; }; + 2FFC4D1A1ABE3C360081D675 /* FxAState.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FxAState.swift; sourceTree = ""; }; + 2FFC4D371ABE3C420081D675 /* FxAStateTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FxAStateTests.swift; sourceTree = ""; }; + 318FB6EA1DB5600D0004E40F /* SQLiteHistoryFactories.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SQLiteHistoryFactories.swift; sourceTree = ""; }; + 31ADB5D91E58CEC300E87909 /* ClipboardBarDisplayHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ClipboardBarDisplayHandler.swift; sourceTree = ""; }; + 3905274A1C874D35007E0BB7 /* Today.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = Today.appex; sourceTree = BUILT_PRODUCTS_DIR; }; + 3905274B1C874D35007E0BB7 /* NotificationCenter.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = NotificationCenter.framework; path = System/Library/Frameworks/NotificationCenter.framework; sourceTree = SDKROOT; }; + 3905274E1C874D35007E0BB7 /* TodayViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TodayViewController.swift; sourceTree = ""; }; + 390527531C874D35007E0BB7 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 3905B4D41E8E7A6B0027D953 /* FxAPushMessageHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FxAPushMessageHandler.swift; sourceTree = ""; }; + 391AEFD11C8F11ED00691F84 /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = ""; }; + 39236E711FCC600200A38F1B /* TabEventHandlerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabEventHandlerTests.swift; sourceTree = ""; }; + 392ED6B61D06E85E009D9B62 /* NewTabChoiceViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NewTabChoiceViewController.swift; sourceTree = ""; }; + 392ED7E31D0AEF56009D9B62 /* NewTabAccessors.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = NewTabAccessors.swift; path = Accessors/NewTabAccessors.swift; sourceTree = ""; }; + 392ED7E51D0AEFEF009D9B62 /* HomePageAccessors.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = HomePageAccessors.swift; path = Accessors/HomePageAccessors.swift; sourceTree = ""; }; + 3943A81C1E9807C700D4F6DC /* FxAPushMessageTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FxAPushMessageTest.swift; sourceTree = ""; }; + 39455F761FC83F430088A22C /* TabEventHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = TabEventHandler.swift; path = Helpers/TabEventHandler.swift; sourceTree = ""; }; + 394CF6CE1BAA493C00906917 /* DefaultSuggestedSites.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DefaultSuggestedSites.swift; sourceTree = ""; }; + 395C8F201E796AD600A68E8C /* PushCrypto.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PushCrypto.swift; sourceTree = ""; }; + 3964B0991EA8F06F00F2EEF4 /* FeatureSwitch.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeatureSwitch.swift; sourceTree = ""; }; + 3964B09B1EA8F32C00F2EEF4 /* FeatureSwitchTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeatureSwitchTests.swift; sourceTree = ""; }; + 396E38DB1EE0818800CC180F /* ExtensionProfile.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ExtensionProfile.swift; sourceTree = ""; }; + 397848DB1ED86605004C0C0B /* NotificationService.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = NotificationService.appex; sourceTree = BUILT_PRODUCTS_DIR; }; + 397848DD1ED86605004C0C0B /* NotificationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationService.swift; sourceTree = ""; }; + 397848DF1ED86605004C0C0B /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 39A359E31BFCCE94006B9E87 /* UserActivityHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = UserActivityHandler.swift; path = Helpers/UserActivityHandler.swift; sourceTree = ""; }; + 39A35AEC1C0662A3006B9E87 /* SpotlightHelper.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; path = SpotlightHelper.js; sourceTree = ""; }; + 39AC59191CC574AA0042C2F5 /* HomePageSettingsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HomePageSettingsViewController.swift; sourceTree = ""; }; + 39B0647C1E7ADAC2000BE173 /* PushCryptoTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = PushCryptoTests.swift; path = PushTests/PushCryptoTests.swift; sourceTree = ""; }; + 39C22C2C1E897B9A000C0E56 /* LivePushClientTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = LivePushClientTests.swift; path = PushTests/LivePushClientTests.swift; sourceTree = ""; }; + 39DD030C1CD53E1900BC09B3 /* HomePageHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HomePageHelper.swift; sourceTree = ""; }; + 39E65D261CA5B92000C63CE3 /* AsyncReducerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AsyncReducerTests.swift; sourceTree = ""; }; + 39EB46971E26DDB4006346E8 /* ScreenGraph.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ScreenGraph.swift; sourceTree = ""; }; + 39EB46981E26DDB4006346E8 /* FxScreenGraph.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FxScreenGraph.swift; sourceTree = ""; }; + 39F819C51FD70F5D009E31E4 /* TabEventHandlers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabEventHandlers.swift; sourceTree = ""; }; + 39F99FD91E3A6DE300F353B4 /* PushClient.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PushClient.swift; sourceTree = ""; }; + 39F99FDA1E3A6DE300F353B4 /* PushConfiguration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PushConfiguration.swift; sourceTree = ""; }; + 39F99FDB1E3A6DE300F353B4 /* PushRegistration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PushRegistration.swift; sourceTree = ""; }; + 39F99FED1E3A71F800F353B4 /* FxALoginHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = FxALoginHelper.swift; path = Helpers/FxALoginHelper.swift; sourceTree = ""; }; + 3B0943801D6CC4FC004F24E1 /* FilledPageControl.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = FilledPageControl.swift; path = ThirdParty/FilledPageControl.swift; sourceTree = ""; }; + 3B39EDB91E16E18900EF029F /* CustomSearchEnginesTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomSearchEnginesTest.swift; sourceTree = ""; }; + 3B39EDCA1E16E1AA00EF029F /* CustomSearchViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomSearchViewController.swift; sourceTree = ""; }; + 3B4262E21E538EC400CA681C /* MetadataHelper.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; name = MetadataHelper.js; path = "Metadata Parsing/MetadataHelper.js"; sourceTree = ""; }; + 3B4262E31E538EC400CA681C /* MetadataParserHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = MetadataParserHelper.swift; path = "Metadata Parsing/MetadataParserHelper.swift"; sourceTree = ""; }; + 3B4262E41E538EC400CA681C /* page-metadata-parser.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; name = "page-metadata-parser.js"; path = "Metadata Parsing/page-metadata-parser.js"; sourceTree = ""; }; + 3B43E3D01D95C48D00BBA9DB /* StoragePerfTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = StoragePerfTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 3B43E3D21D95C48D00BBA9DB /* StoragePerfTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoragePerfTests.swift; sourceTree = ""; }; + 3B43E3D41D95C48D00BBA9DB /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 3B4988CD1E42B01800A12FDA /* SwiftyJSON.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SwiftyJSON.framework; path = Carthage/Build/iOS/SwiftyJSON.framework; sourceTree = ""; }; + 3B4AA24A1D8B8C4C00A2E008 /* ArrayExtensionTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ArrayExtensionTests.swift; sourceTree = ""; }; + 3B546EBF1D95ECAE00BDBE36 /* ActivityStreamTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ActivityStreamTest.swift; sourceTree = ""; }; + 3B61CD481F2A74EF00D38DE1 /* PocketFeed.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PocketFeed.swift; sourceTree = ""; }; + 3B61CD581F2A750800D38DE1 /* PocketFeedTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PocketFeedTests.swift; sourceTree = ""; }; + 3B61CD621F2A769D00D38DE1 /* pocketglobalfeed.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = pocketglobalfeed.json; sourceTree = ""; }; + 3B6889C41D66950E002AC85E /* UIImageColors.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = UIImageColors.swift; path = ThirdParty/UIImageColors.swift; sourceTree = ""; }; + 3B6F40171DC7849C00656CC6 /* ActivityStreamTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ActivityStreamTests.swift; sourceTree = ""; }; + 3BA9A0221D2C208C00BD418C /* Fuzi.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Fuzi.framework; path = Carthage/Build/iOS/Fuzi.framework; sourceTree = ""; }; + 3BB50E101D6274CD004B33DF /* ActivityStreamTopSitesCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ActivityStreamTopSitesCell.swift; sourceTree = ""; }; + 3BB50E1F1D627539004B33DF /* ActivityStreamPanel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ActivityStreamPanel.swift; sourceTree = ""; }; + 3BB54B301E68EB2B0021DAC4 /* AuthenticationKeychainInfoTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AuthenticationKeychainInfoTests.swift; sourceTree = ""; }; + 3BC659481E5BA4AE006D560F /* TopSites */ = {isa = PBXFileReference; lastKnownFileType = folder; path = TopSites; sourceTree = ""; }; + 3BC659581E5BA505006D560F /* top_sites.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = top_sites.json; sourceTree = ""; }; + 3BCE6D3B1CEB9E4D0080928C /* ThirdPartySearchAlerts.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ThirdPartySearchAlerts.swift; sourceTree = ""; }; + 3BE7274D1CCFE8A30099189F /* CustomSearchHelper.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; path = CustomSearchHelper.js; sourceTree = ""; }; + 3BE7275C1CCFE8B60099189F /* CustomSearchHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomSearchHandler.swift; sourceTree = ""; }; + 3BF4B8E81D38497A00493393 /* BaseTestCase.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BaseTestCase.swift; sourceTree = ""; }; + 3BF56D261CDBBE1F00AC4D75 /* SimpleToast.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SimpleToast.swift; sourceTree = ""; }; + 3BFCBF1F1E04B1C50070C042 /* UIImageViewExtensionsTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIImageViewExtensionsTests.swift; sourceTree = ""; }; + 3BFE4B071D342FB800DDF53F /* XCUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = XCUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 3BFE4B0B1D342FB900DDF53F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 3BFE4B4F1D34673D00DDF53F /* ThirdPartySearchTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ThirdPartySearchTest.swift; sourceTree = ""; }; + 3D71C89D1F5703A1008D8646 /* CopiedLinksTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CopiedLinksTests.swift; sourceTree = ""; }; + 3D9CA9831EF456A8002434DD /* NightModeTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NightModeTests.swift; sourceTree = ""; }; + 3D9CA9A71EF84D04002434DD /* NoImageTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NoImageTests.swift; sourceTree = ""; }; + 3D9CAA1B1EFCD655002434DD /* ClipBoardTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ClipBoardTests.swift; sourceTree = ""; }; + 3DEFED071F55EBE300F8620C /* TrackingProtectionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrackingProtectionTests.swift; sourceTree = ""; }; + 4A59BF410BBD9B3BE71F4C7C /* TestHistory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestHistory.swift; sourceTree = ""; }; + 4F514FD31ACD8F2C0022D7EA /* HistoryTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HistoryTests.swift; sourceTree = ""; }; + 4F9757391AFA6F37006ECC24 /* readerContent.html */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.html; path = readerContent.html; sourceTree = ""; }; + 50027345B49B967409DDA348 /* StateTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = StateTests.swift; path = SyncTests/StateTests.swift; sourceTree = ""; }; + 554867221DC3935A00183DAA /* HomePageTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HomePageTests.swift; sourceTree = ""; }; + 55A747161DC46FC400CE1B57 /* HomePageUITest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HomePageUITest.swift; sourceTree = ""; }; + 59A6825233896FC846499289 /* HistoryPanel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = HistoryPanel.swift; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; + 59A6839879D615FC1C0D71CE /* BookmarksPanel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BookmarksPanel.swift; sourceTree = ""; }; + 59A685F4EAD19EDEC854BCA4 /* ReaderPanel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReaderPanel.swift; sourceTree = ""; }; + 59A68B1F857A8638598A63A0 /* TwoLineCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TwoLineCell.swift; sourceTree = ""; }; + 59A68CCB63E2A565CB03F832 /* SearchViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SearchViewController.swift; sourceTree = ""; }; + 742A56381D80B54A00BDB803 /* PhotonActionSheet.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhotonActionSheet.swift; sourceTree = ""; }; + 744B0FFD1B4F172E00100422 /* ToolbarTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ToolbarTests.swift; sourceTree = ""; }; + 744ED5601DBFEB8D00A2B5BE /* MailtoLinkHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MailtoLinkHandler.swift; sourceTree = ""; }; + 745DAB2F1CDAAFAA00D44181 /* RecentlyClosedTabsPanel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RecentlyClosedTabsPanel.swift; sourceTree = ""; }; + 745DAB3E1CDAB09E00D44181 /* HistoryBackButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HistoryBackButton.swift; sourceTree = ""; }; + 7479B4ED1C5306A200DF000B /* Reachability.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Reachability.swift; path = ThirdParty/Reachability.swift; sourceTree = ""; }; + 74821F8D1DAD8F1300EEEA72 /* ActivityStreamHighlightCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ActivityStreamHighlightCell.swift; path = ../Widgets/ActivityStreamHighlightCell.swift; sourceTree = ""; }; + 74821FC41DB56A2500EEEA72 /* OpenWithSettingsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OpenWithSettingsViewController.swift; sourceTree = ""; }; + 74821FFD1DB6D3AC00EEEA72 /* MailSchemes.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = MailSchemes.plist; sourceTree = ""; }; + 7482205B1DBAB56300EEEA72 /* MailProviders.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MailProviders.swift; sourceTree = ""; }; + 74B195431CF503FC007F36EF /* RecentlyClosedTabs.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RecentlyClosedTabs.swift; sourceTree = ""; }; + 74C027441B2A348C001B1E88 /* SessionData.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SessionData.swift; sourceTree = ""; }; + 74E36D771B71323500D69DA1 /* SettingsContentViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsContentViewController.swift; sourceTree = ""; }; + 7B10AA9E1E3A15020002DD08 /* DataExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DataExtensions.swift; sourceTree = ""; }; + 7B10AABA1E3A1F650002DD08 /* URLRequestExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = URLRequestExtensions.swift; sourceTree = ""; }; + 7B2142F41E5E03F800CDD3FC /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = ""; }; + 7B2142F71E5E040200CDD3FC /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = ""; }; + 7B2142FD1E5E055000CDD3FC /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = ""; }; + 7B2143021E5F224A00CDD3FC /* Base */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = Base; path = Base.lproj/InfoPlist.strings; sourceTree = ""; }; + 7B2143031E5F225200CDD3FC /* Base */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = Base; path = Base.lproj/InfoPlist.strings; sourceTree = ""; }; + 7B24DC9B1B67B3590005766B /* ClearPrivateDataTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ClearPrivateDataTests.swift; sourceTree = ""; }; + 7B3631E91C244FEE00D12AF9 /* Theme.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Theme.swift; sourceTree = ""; }; + 7B3632D31C2983F000D12AF9 /* L10nSnapshotTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = L10nSnapshotTests.swift; sourceTree = ""; }; + 7B3D9E641E4CBFDB007A50DA /* NSCoderExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NSCoderExtensions.swift; sourceTree = ""; }; + 7B42406D1CA04CAC009B5C28 /* Menu.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Menu.xcassets; sourceTree = ""; }; + 7B4980A71CE363ED0017547C /* Settings.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Settings.xcassets; sourceTree = ""; }; + 7B604F851C494983006EEEC3 /* Alamofire.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Alamofire.framework; path = Carthage/Build/iOS/Alamofire.framework; sourceTree = ""; }; + 7B604F981C494F74006EEEC3 /* KIF.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = KIF.framework; path = Carthage/Build/iOS/KIF.framework; sourceTree = ""; }; + 7B604F9A1C4950F2006EEEC3 /* SDWebImage.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SDWebImage.framework; path = Carthage/Build/iOS/SDWebImage.framework; sourceTree = ""; }; + 7B604FA11C495268006EEEC3 /* SnapKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SnapKit.framework; path = Carthage/Build/iOS/SnapKit.framework; sourceTree = ""; }; + 7B604FBA1C495D8A006EEEC3 /* XCGLogger.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = XCGLogger.framework; path = Carthage/Build/iOS/XCGLogger.framework; sourceTree = ""; }; + 7B6050001C49653B006EEEC3 /* AdjustSdk.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AdjustSdk.framework; path = Carthage/Build/iOS/AdjustSdk.framework; sourceTree = ""; }; + 7B60B0071BDE3AE10090C984 /* SnapshotHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = SnapshotHelper.swift; path = fastlane/SnapshotHelper.swift; sourceTree = SOURCE_ROOT; }; + 7B844E3C1BBDDB9D00E733A2 /* ChevronView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChevronView.swift; sourceTree = ""; }; + 7B8A47F51D01D3B400C07734 /* PassKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = PassKit.framework; path = System/Library/Frameworks/PassKit.framework; sourceTree = SDKROOT; }; + 7B95CD191C3AB2EE00638E31 /* MarketingUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarketingUITests.swift; sourceTree = ""; }; + 7B9A62B91C4002B1005C83DC /* SQLite.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = SQLite.xcodeproj; path = ThirdParty/SQLite.swift/SQLite.xcodeproj; sourceTree = ""; }; + 7B9BF91B1E43472E00CB24F4 /* JSONExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JSONExtensions.swift; sourceTree = ""; }; + 7BA0601A1C0F4DE200DFADB6 /* TabPeekViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TabPeekViewController.swift; sourceTree = ""; }; + 7BA4A9451C4CED780091D032 /* GCDWebServers.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = GCDWebServers.framework; path = Carthage/Build/iOS/GCDWebServers.framework; sourceTree = ""; }; + 7BA4A9491C4CEFC70091D032 /* OnePasswordExtension.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = OnePasswordExtension.framework; path = Carthage/Build/iOS/OnePasswordExtension.framework; sourceTree = ""; }; + 7BA4A94B1C4CF03B0091D032 /* SwiftKeychainWrapper.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SwiftKeychainWrapper.framework; path = Carthage/Build/iOS/SwiftKeychainWrapper.framework; sourceTree = ""; }; + 7BA4A9631C4CFE840091D032 /* Deferred.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Deferred.framework; path = Carthage/Build/iOS/Deferred.framework; sourceTree = ""; }; + 7BA8D1C61BA037F500C8AE9E /* OpenInHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OpenInHelper.swift; sourceTree = ""; }; + 7BBFEE731BB405D900A305AA /* TabManagerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TabManagerTests.swift; sourceTree = ""; }; + 7BC68D081CC153020043562A /* RoundedToolbar.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RoundedToolbar.swift; sourceTree = ""; }; + 7BEB644D1C7345600092C02E /* L10nSnapshotTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = L10nSnapshotTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 7BEB644E1C7345600092C02E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = L10nSnapshotTests/Info.plist; sourceTree = SOURCE_ROOT; }; + 7BEB645A1C7345990092C02E /* MarketingUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = MarketingUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 7BEB645B1C7345990092C02E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = MarketingUITests/Info.plist; sourceTree = SOURCE_ROOT; }; + 7BEDD29A1CB7CB24004B51F3 /* JumpAndSpinAnimator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JumpAndSpinAnimator.swift; sourceTree = ""; }; + 7BEDD2B11CB7CC0B004B51F3 /* Animatable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Animatable.swift; sourceTree = ""; }; + 7BEFC67F1BFF68C30059C952 /* QuickActions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = QuickActions.swift; sourceTree = ""; }; + 7BF5A1E91B41640500EA9DD8 /* SyncQueue.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncQueue.swift; sourceTree = ""; }; + 7BF5A1ED1B429B3100EA9DD8 /* SyncCommandsTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncCommandsTests.swift; sourceTree = ""; }; + 8D8251721F4DE67E00780643 /* AdvanceAccountSettingViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AdvanceAccountSettingViewController.swift; sourceTree = ""; }; + 8DCD3BCC1ED5B7FA00446D38 /* FxADeepLinkingTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FxADeepLinkingTests.swift; sourceTree = ""; }; + A83E5AB61C1D993D0026D912 /* UIPasteboardExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = UIPasteboardExtensions.swift; path = Extensions/UIPasteboardExtensions.swift; sourceTree = ""; }; + A83E5B181C1DA8BF0026D912 /* image.gif */ = {isa = PBXFileReference; lastKnownFileType = image.gif; path = image.gif; sourceTree = ""; }; + A83E5B191C1DA8BF0026D912 /* image.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = image.png; sourceTree = ""; }; + A83E5B1C1C1DA8D80026D912 /* UIPasteboardExtensionsTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIPasteboardExtensionsTests.swift; sourceTree = ""; }; + A9072B7F1D07B34100459960 /* NoImageModeHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NoImageModeHelper.swift; sourceTree = ""; }; + A91528D11D0E637600CD0425 /* NoImageModeHelper.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; path = NoImageModeHelper.js; sourceTree = ""; }; + A93067D81D0FE15E00C49C6E /* NightModeHelper.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; path = NightModeHelper.js; sourceTree = ""; }; + A93067E71D0FE18E00C49C6E /* NightModeHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NightModeHelper.swift; sourceTree = ""; }; + C400467B1CF4E43E00B08303 /* BackForwardListViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BackForwardListViewController.swift; sourceTree = ""; }; + C40046F91CF8E0B200B08303 /* BackForwardListAnimator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BackForwardListAnimator.swift; sourceTree = ""; }; + C45F44681D087DB600CB7EF0 /* TopTabsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TopTabsViewController.swift; sourceTree = ""; }; + C4E3983C1D21F1E7004E89BA /* TopTabsViews.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TopTabsViews.swift; sourceTree = ""; }; + C4E3984B1D21F2FD004E89BA /* TabTrayButtonExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = TabTrayButtonExtensions.swift; path = ../Browser/TabTrayButtonExtensions.swift; sourceTree = ""; }; + C4E3985F1D22C409004E89BA /* TopTabsLayout.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TopTabsLayout.swift; sourceTree = ""; }; + C4EFEECE1CEBB6F2009762A4 /* BackForwardTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BackForwardTableViewCell.swift; sourceTree = ""; }; + C4F3B2991CFCF93A00966259 /* ButtonToast.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ButtonToast.swift; sourceTree = ""; }; + C817B34C1FC609500086018E /* UIScrollViewSwizzled.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIScrollViewSwizzled.swift; sourceTree = ""; }; + C8611CA11F71AEB900C3DE7D /* NoImageModeTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NoImageModeTests.swift; sourceTree = ""; }; + C88601B71F4228AD00BBDE4F /* ContentBlockerSettingViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContentBlockerSettingViewController.swift; sourceTree = ""; }; + C8EB60C31F1FB12500F9B5B3 /* navigationDelegate.html */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.html; path = navigationDelegate.html; sourceTree = ""; }; + C8EB60DB1F1FB9AD00F9B5B3 /* NavigationDelegateTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NavigationDelegateTests.swift; sourceTree = ""; }; + C8F457A71F1FD75A000CB895 /* BrowserViewController+WKNavigationDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "BrowserViewController+WKNavigationDelegate.swift"; sourceTree = ""; }; + C8F457A91F1FDD9B000CB895 /* BrowserViewController+KeyCommands.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "BrowserViewController+KeyCommands.swift"; sourceTree = ""; }; + CE339ED61F2507A1009BE0E6 /* TestBookmarksSynchronizer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = TestBookmarksSynchronizer.swift; path = SyncTests/TestBookmarksSynchronizer.swift; sourceTree = ""; }; + CE564D8D1EB7BD7700BEDDDC /* BookmarksRepairRequestor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BookmarksRepairRequestor.swift; sourceTree = ""; }; + CE7F115E1F3CCEF900ABFC0B /* RemoteDevices.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RemoteDevices.swift; sourceTree = ""; }; + CEFC983B1EC0DC60008A3E48 /* TestBookmarksRepairRequestor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = TestBookmarksRepairRequestor.swift; path = SyncTests/TestBookmarksRepairRequestor.swift; sourceTree = ""; }; + D0131B4C1F3CF7D8000CDE86 /* SQLiteFavicons.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SQLiteFavicons.swift; sourceTree = ""; }; + D018F93D1F44A7190098F8CA /* Schema.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Schema.swift; sourceTree = ""; }; + D02816C11ECA5E2A00240CAA /* HistoryStateHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HistoryStateHelper.swift; sourceTree = ""; }; + D02816D11ECA60DF00240CAA /* HistoryStateHelper.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; path = HistoryStateHelper.js; sourceTree = ""; }; + D02818601EF056C800240CAA /* SentryIntegration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SentryIntegration.swift; sourceTree = ""; }; + D0B29EDF1F460BDF00C7CEFC /* LoginsSchema.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginsSchema.swift; sourceTree = ""; }; + D301AAED1A3A55B70078DD1D /* TabTrayController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TabTrayController.swift; sourceTree = ""; }; + D30684F01C84F12A002D8D82 /* SearchPlugins */ = {isa = PBXFileReference; lastKnownFileType = folder; name = SearchPlugins; path = Search/SearchPlugins; sourceTree = ""; }; + D308E4E31A5306F500842685 /* SearchEngines.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SearchEngines.swift; sourceTree = ""; }; + D308EE551CBF0BF5006843F2 /* CertError.css */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.css; path = CertError.css; sourceTree = ""; }; + D30B101D1AA7F9C600C01CA3 /* HomePanels.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HomePanels.swift; sourceTree = ""; }; + D30EBB5A1C75503800105AE9 /* KIF.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = KIF.xcodeproj; path = Carthage/Checkouts/KIF/KIF.xcodeproj; sourceTree = ""; }; + D313BE971B2F5096009EF241 /* DomainAutocompleteTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DomainAutocompleteTests.swift; sourceTree = ""; }; + D314E7F51A37B98700426A76 /* TabToolbar.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TabToolbar.swift; sourceTree = ""; }; + D31A0FC61A65D6D000DC8C7E /* SearchSuggestClient.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SearchSuggestClient.swift; sourceTree = ""; }; + D31CF65B1CC1959A001D0BD0 /* PrivilegedRequest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PrivilegedRequest.swift; sourceTree = ""; }; + D31EC05E1CC57ED80096F4AB /* localhostLoad.html */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.html; path = localhostLoad.html; sourceTree = ""; }; + D31F95E81AC226CB005C9F3B /* ScreenshotHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ScreenshotHelper.swift; sourceTree = ""; }; + D32A350D1D6530D80066DAE9 /* FxADevice.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FxADevice.swift; sourceTree = ""; }; + D32CACEC1AE04DA1000658EB /* TestSwiftData.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestSwiftData.swift; sourceTree = ""; }; + D331DFC91CB6E9EE009B5DA2 /* OldStrings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OldStrings.swift; sourceTree = ""; }; + D343DCFD1C446BDB00D7EEE8 /* findPage.html */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.html; path = findPage.html; sourceTree = ""; }; + D34510871ACF415700EC27F0 /* SearchLoader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SearchLoader.swift; sourceTree = ""; }; + D34DC84D1A16C40C00D49B7B /* Profile.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = Profile.swift; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; + D34E33021BA793C2006135F0 /* loginForm.html */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.html; path = loginForm.html; sourceTree = ""; }; + D35210E01CB2F16600FC5DCB /* Strings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Strings.swift; path = ../Client/Frontend/Strings.swift; sourceTree = ""; }; + D36998881AD70A0A00650C6C /* IOKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = IOKit.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk/System/Library/Frameworks/IOKit.framework; sourceTree = DEVELOPER_DIR; }; + D37524861C6E8B5A00A5F6C2 /* topdomains.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = topdomains.txt; sourceTree = ""; }; + D375A91F1AE71675001B30D5 /* ViewMemoryLeakTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ViewMemoryLeakTests.swift; sourceTree = ""; }; + D37DE2821CA2047500A5EC69 /* CertStore.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CertStore.swift; sourceTree = ""; }; + D37DE2C61CA356D800A5EC69 /* CertTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CertTests.swift; sourceTree = ""; }; + D37DE2C81CA356F900A5EC69 /* testcert1.pem */ = {isa = PBXFileReference; lastKnownFileType = file; path = testcert1.pem; sourceTree = ""; }; + D37DE2C91CA356F900A5EC69 /* testcert2.pem */ = {isa = PBXFileReference; lastKnownFileType = file; path = testcert2.pem; sourceTree = ""; }; + D38A1BEC1A9FA2CA00F6A386 /* SiteTableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SiteTableViewController.swift; sourceTree = ""; }; + D38A1EDF1CB458EC0080C842 /* CertError.html */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.html; path = CertError.html; sourceTree = ""; }; + D38F02D01C05127100175932 /* Authenticator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Authenticator.swift; sourceTree = ""; }; + D38F036F1C06387900175932 /* AuthenticationTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AuthenticationTests.swift; sourceTree = ""; }; + D3968F241A38FE8500CEFD3B /* TabManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TabManager.swift; sourceTree = ""; }; + D3972BF11C22412B00035B87 /* ShareExtensionHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ShareExtensionHelper.swift; sourceTree = ""; }; + D3972BF21C22412B00035B87 /* TitleActivityItemProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TitleActivityItemProvider.swift; sourceTree = ""; }; + D39FA15F1A83E0EC00EE869C /* UITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = UITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + D39FA1621A83E0EC00EE869C /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + D39FA16B1A83E17800EE869C /* CoreGraphics.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreGraphics.framework; path = System/Library/Frameworks/CoreGraphics.framework; sourceTree = SDKROOT; }; + D39FA16F1A83E62600EE869C /* UITests-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "UITests-Bridging-Header.h"; sourceTree = ""; }; + D39FA1801A83E84900EE869C /* Global.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Global.swift; sourceTree = ""; }; + D3A994951A3686BD008AD1AC /* BrowserViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BrowserViewController.swift; sourceTree = ""; }; + D3A994961A3686BD008AD1AC /* Tab.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Tab.swift; sourceTree = ""; }; + D3B6923C1B9F9444004B87A4 /* FindInPageBar.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FindInPageBar.swift; sourceTree = ""; }; + D3B6923E1B9F9A58004B87A4 /* FindInPageHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FindInPageHelper.swift; sourceTree = ""; }; + D3B692401B9F9CC8004B87A4 /* FindInPage.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; path = FindInPage.js; sourceTree = ""; }; + D3BA41671BD82F2200DA5457 /* XCTestCaseExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = XCTestCaseExtensions.swift; sourceTree = ""; }; + D3BA7E0B1B0E902A00153782 /* ContextMenu.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; path = ContextMenu.js; sourceTree = ""; }; + D3BA7E0D1B0E934F00153782 /* ContextMenuHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContextMenuHelper.swift; sourceTree = ""; }; + D3BE7B251B054D4400641031 /* main.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = ""; }; + D3BE7B451B054F8600641031 /* TestAppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestAppDelegate.swift; sourceTree = ""; }; + D3BF8CBA1B7425570007AFE6 /* DiskImageStore.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DiskImageStore.swift; sourceTree = ""; }; + D3BF8CBC1B7472FA0007AFE6 /* DiskImageStoreTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DiskImageStoreTests.swift; sourceTree = ""; }; + D3C3696D1CC6B78800348A61 /* LocalRequestHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LocalRequestHelper.swift; sourceTree = ""; }; + D3C3EB641B6FF44000388E9A /* SessionRestoreTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SessionRestoreTests.swift; sourceTree = ""; }; + D3C744CC1A687D6C004CE85D /* URIFixup.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = URIFixup.swift; sourceTree = ""; }; + D3CFD3631CC5605B0064AB4A /* SecurityTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SecurityTests.swift; sourceTree = ""; }; + D3D488581ABB54CD00A93597 /* FileAccessorTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FileAccessorTests.swift; sourceTree = ""; }; + D3DBE6E41D6516FE00033FFF /* FxADeviceRegistration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FxADeviceRegistration.swift; sourceTree = ""; }; + D3E171C11A841EAD00AB44CD /* KIFHelper.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; path = KIFHelper.js; sourceTree = ""; }; + D3E8EEE71B97A87A001900FB /* ClearPrivateDataTableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ClearPrivateDataTableViewController.swift; sourceTree = ""; }; + D3FA777A1A43B2990010CD32 /* SearchTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SearchTests.swift; sourceTree = ""; }; + D3FA77831A43B2CE0010CD32 /* OpenSearch.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OpenSearch.swift; sourceTree = ""; }; + D3FEC38C1AC4B42F00494F45 /* AutocompleteTextField.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AutocompleteTextField.swift; sourceTree = ""; }; + D81127D71F84023B0050841D /* PhotonActionSheetTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotonActionSheetTest.swift; sourceTree = ""; }; + D81E45121F82C56C004EFFBA /* NewTabContentSettingsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabContentSettingsViewController.swift; sourceTree = ""; }; + D83821FF1FC7961D00303C12 /* DispatchQueueExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DispatchQueueExtensions.swift; sourceTree = ""; }; + D863C8E31F68BFC20058D95F /* GradientProgressBar.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GradientProgressBar.swift; sourceTree = ""; }; + D88FDA9E1F4E2B9200FD9709 /* PhotonActionSheetProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotonActionSheetProtocol.swift; sourceTree = ""; }; + D88FDAAE1F4E2BA000FD9709 /* PhotonActionSheetAnimator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotonActionSheetAnimator.swift; sourceTree = ""; }; + DD31E0FA1B382B520077078A /* TabPrintPageRenderer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TabPrintPageRenderer.swift; sourceTree = ""; }; + E40A18F71EDC73D5006B7F28 /* Fennec.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Fennec.entitlements; sourceTree = ""; }; + E40A18F81EDC73D5006B7F28 /* FennecEnterprise.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = FennecEnterprise.entitlements; sourceTree = ""; }; + E40A18F91EDC73D5006B7F28 /* Firefox.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Firefox.entitlements; sourceTree = ""; }; + E40A18FA1EDC73D5006B7F28 /* FirefoxBeta.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = FirefoxBeta.entitlements; sourceTree = ""; }; + E40AFC531DD0E93300DA5651 /* L10nPermissionStringsSnapshotTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = L10nPermissionStringsSnapshotTests.swift; sourceTree = ""; }; + E40AFC641DD0F25500DA5651 /* L10nBaseSnapshotTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = L10nBaseSnapshotTests.swift; sourceTree = ""; }; + E40AFC6B1DD128DA00DA5651 /* L10nIntroSnapshotTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = L10nIntroSnapshotTests.swift; sourceTree = ""; }; + E40FAB0B1A7ABB77009CB80D /* WebServer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WebServer.swift; sourceTree = ""; }; + E41A7D4A1A1BE04500245963 /* InitialViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InitialViewController.swift; sourceTree = ""; }; + E41F0D971ADFFB5400FC7387 /* ReadingListService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = ReadingListService.swift; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; + E41F0D9A1ADFFC5600FC7387 /* ReadingListError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReadingListError.swift; sourceTree = ""; }; + E42736061EA858CF009C428E /* TabsPayloadTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = TabsPayloadTests.swift; path = SyncTests/TabsPayloadTests.swift; sourceTree = ""; }; + E4424B3B1AC71FB400F44C38 /* FiraSans-Book.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "FiraSans-Book.ttf"; sourceTree = ""; }; + E46175F21EBB73A10021AE8A /* Sentry.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Sentry.framework; path = Carthage/Build/iOS/Sentry.framework; sourceTree = ""; }; + E47536EC1C85412C0022CC69 /* PrintHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PrintHelper.swift; sourceTree = ""; }; + E47536FB1C85415E0022CC69 /* PrintHelper.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; path = PrintHelper.js; sourceTree = ""; }; + E47616C61AB74CA600E7DD25 /* ReaderModeBarView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReaderModeBarView.swift; sourceTree = ""; }; + E49943F41AE6879C00BF9DE4 /* IntroViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IntroViewController.swift; sourceTree = ""; }; + E49943F61AE69EDD00BF9DE4 /* Intro.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Intro.xcassets; sourceTree = ""; }; + E49BFF581B1F48C3009368CC /* Readability.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; name = Readability.js; path = Carthage/Checkouts/readability/Readability.js; sourceTree = SOURCE_ROOT; }; + E4A960051ABB9C450069AD6F /* ReaderModeUtils.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReaderModeUtils.swift; sourceTree = ""; }; + E4A961171AC041C40069AD6F /* ReadabilityService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReadabilityService.swift; sourceTree = ""; }; + E4A961331AC051360069AD6F /* ReadabilityTabHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReadabilityTabHelper.swift; sourceTree = ""; }; + E4A961351AC052DE0069AD6F /* ReadabilityTabHelper.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; path = ReadabilityTabHelper.js; sourceTree = ""; }; + E4A961371AC06FA50069AD6F /* ReaderViewLoading.html */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.html; path = ReaderViewLoading.html; sourceTree = ""; }; + E4B334871BBF23F3004E2BFF /* iAd.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = iAd.framework; path = System/Library/Frameworks/iAd.framework; sourceTree = SDKROOT; }; + E4B334891BBF23F9004E2BFF /* AdSupport.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AdSupport.framework; path = System/Library/Frameworks/AdSupport.framework; sourceTree = SDKROOT; }; + E4B3348B1BC01D8F004E2BFF /* AdjustIntegration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AdjustIntegration.swift; sourceTree = ""; }; + E4B423BD1AB9FE6A007E66C8 /* ReaderModeCache.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReaderModeCache.swift; sourceTree = ""; }; + E4B423DC1ABA0318007E66C8 /* ReaderModeHandlers.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReaderModeHandlers.swift; sourceTree = ""; }; + E4B7B73A1A793CF20022C5E0 /* CharisSILB.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = CharisSILB.ttf; sourceTree = ""; }; + E4B7B73B1A793CF20022C5E0 /* CharisSILBI.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = CharisSILBI.ttf; sourceTree = ""; }; + E4B7B73C1A793CF20022C5E0 /* CharisSILI.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = CharisSILI.ttf; sourceTree = ""; }; + E4B7B73D1A793CF20022C5E0 /* CharisSILR.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = CharisSILR.ttf; sourceTree = ""; }; + E4B7B7411A793CF20022C5E0 /* FiraSans-Bold.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "FiraSans-Bold.ttf"; sourceTree = ""; }; + E4B7B7421A793CF20022C5E0 /* FiraSans-BoldItalic.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "FiraSans-BoldItalic.ttf"; sourceTree = ""; }; + E4B7B7511A793CF20022C5E0 /* FiraSans-Italic.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "FiraSans-Italic.ttf"; sourceTree = ""; }; + E4B7B7521A793CF20022C5E0 /* FiraSans-Light.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "FiraSans-Light.ttf"; sourceTree = ""; }; + E4B7B7561A793CF20022C5E0 /* FiraSans-Regular.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "FiraSans-Regular.ttf"; sourceTree = ""; }; + E4B7B7571A793CF20022C5E0 /* FiraSans-SemiBold.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "FiraSans-SemiBold.ttf"; sourceTree = ""; }; + E4B7B75F1A793CF20022C5E0 /* FiraSans-UltraLight.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "FiraSans-UltraLight.ttf"; sourceTree = ""; }; + E4BA8A2B1B4B0A1600BC2E95 /* ViewLater.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = ViewLater.appex; sourceTree = BUILT_PRODUCTS_DIR; }; + E4BA8AA11B4B15EF00BC2E95 /* ActionRequestHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ActionRequestHandler.swift; sourceTree = ""; }; + E4BA8AA61B4B15EF00BC2E95 /* ViewLater.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = ViewLater.xcassets; sourceTree = ""; }; + E4BA8AA71B4B15EF00BC2E95 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + E4BCAAB81B0537E300855D82 /* InstructionsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = InstructionsViewController.swift; path = ../../Extensions/SendTo/InstructionsViewController.swift; sourceTree = ""; }; + E4BCAAD81B05380B00855D82 /* ClientPickerViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ClientPickerViewController.swift; path = ../../Extensions/SendTo/ClientPickerViewController.swift; sourceTree = ""; }; + E4C358541AF144BA00299F7E /* FSReadingList.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FSReadingList.m; sourceTree = ""; }; + E4C358561AF1467A00299F7E /* FSReadingList.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FSReadingList.h; sourceTree = ""; }; + E4CD9E901A6897FB00318571 /* ReaderMode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReaderMode.swift; sourceTree = ""; }; + E4CD9E991A68980A00318571 /* ReaderMode.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; path = ReaderMode.js; sourceTree = ""; }; + E4CD9F1C1A6D9C2800318571 /* WebServerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WebServerTests.swift; sourceTree = ""; }; + E4CD9F2C1A6DC91200318571 /* TabLocationView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TabLocationView.swift; sourceTree = ""; }; + E4CD9F531A71506400318571 /* Reader.html */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.html; path = Reader.html; sourceTree = ""; }; + E4CD9F5A1A71506C00318571 /* Reader.css */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.css; path = Reader.css; sourceTree = ""; }; + E4CD9F6C1A77DD2800318571 /* ReaderModeStyleViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReaderModeStyleViewController.swift; sourceTree = ""; }; + E4D567181ADECE2700F1EFE7 /* ReadingList.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = ReadingList.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + E4D5671B1ADECE2700F1EFE7 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + E4D567221ADECE2700F1EFE7 /* ReadingListTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ReadingListTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + E4D5672A1ADECE2800F1EFE7 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + E4D567551ADECE8100F1EFE7 /* ReadingListAuthenticator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReadingListAuthenticator.swift; sourceTree = ""; }; + E4D567561ADECE8100F1EFE7 /* ReadingListBasicAuthAuthenticator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReadingListBasicAuthAuthenticator.swift; sourceTree = ""; }; + E4D567571ADECE8100F1EFE7 /* ReadingListBatchRecordResponse.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReadingListBatchRecordResponse.swift; sourceTree = ""; }; + E4D567581ADECE8100F1EFE7 /* ReadingListChangeAccumulator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReadingListChangeAccumulator.swift; sourceTree = ""; }; + E4D567591ADECE8100F1EFE7 /* ReadingListClient.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReadingListClient.swift; sourceTree = ""; }; + E4D5675A1ADECE8100F1EFE7 /* ReadingListClientMetadata.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReadingListClientMetadata.swift; sourceTree = ""; }; + E4D5675B1ADECE8100F1EFE7 /* ReadingListClientRecord.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReadingListClientRecord.swift; sourceTree = ""; }; + E4D5675D1ADECE8100F1EFE7 /* ReadingListFetchSpec.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReadingListFetchSpec.swift; sourceTree = ""; }; + E4D5675E1ADECE8100F1EFE7 /* ReadingListOAuthAuthenticator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReadingListOAuthAuthenticator.swift; sourceTree = ""; }; + E4D5675F1ADECE8100F1EFE7 /* ReadingListRecord.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReadingListRecord.swift; sourceTree = ""; }; + E4D567601ADECE8100F1EFE7 /* ReadingListRecordResponse.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReadingListRecordResponse.swift; sourceTree = ""; }; + E4D567611ADECE8100F1EFE7 /* ReadingListRecordsResponse.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReadingListRecordsResponse.swift; sourceTree = ""; }; + E4D567621ADECE8100F1EFE7 /* ReadingListResponse.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReadingListResponse.swift; sourceTree = ""; }; + E4D567631ADECE8100F1EFE7 /* ReadingListServerMetadata.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReadingListServerMetadata.swift; sourceTree = ""; }; + E4D567641ADECE8100F1EFE7 /* ReadingListServerRecord.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReadingListServerRecord.swift; sourceTree = ""; }; + E4D567651ADECE8100F1EFE7 /* ReadingListSQLStorage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = ReadingListSQLStorage.swift; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; + E4D567661ADECE8100F1EFE7 /* ReadingListStorage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = ReadingListStorage.swift; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; + E4D567671ADECE8100F1EFE7 /* ReadingListSynchronizer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReadingListSynchronizer.swift; sourceTree = ""; }; + E4D567681ADECE8100F1EFE7 /* ReadingListSyncMetadata.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReadingListSyncMetadata.swift; sourceTree = ""; }; + E4D567691ADECE8100F1EFE7 /* ReadingListUtils.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReadingListUtils.swift; sourceTree = ""; }; + E4D567951ADECEB800F1EFE7 /* ReadingListClientRecordTestCase.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReadingListClientRecordTestCase.swift; sourceTree = ""; }; + E4D5679B1ADECEB800F1EFE7 /* ReadingListStorageTestCase.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReadingListStorageTestCase.swift; sourceTree = ""; }; + E4D6BEB81A0930EC00F538BD /* LaunchScreen.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = LaunchScreen.xib; sourceTree = ""; }; + E4E0BB151AFBC9E4008D6260 /* Shared-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "Shared-Bridging-Header.h"; path = "../Shared-Bridging-Header.h"; sourceTree = ""; }; + E4E0BB171AFBC9E4008D6260 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + E4E25CCA1CA99E7400D0F088 /* HexExtensionsTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HexExtensionsTests.swift; sourceTree = ""; }; + E4E7EB6C1C4AED5E0094275D /* SupportUtilsTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SupportUtilsTests.swift; sourceTree = ""; }; + E4ECCDAD1AB131770005E717 /* FiraSans-Medium.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "FiraSans-Medium.ttf"; sourceTree = ""; }; + E4F21AA51A13C4A300B0FAAA /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = ""; }; + E4F2DABF1F620C0200A556CD /* Leanplum.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Leanplum.framework; path = ThirdParty/Leanplum/Leanplum.framework; sourceTree = ""; }; + E60961861B62B8A700DD640F /* Fennec.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Fennec.xcconfig; path = Configuration/Fennec.xcconfig; sourceTree = ""; }; + E60961891B62B8C800DD640F /* Firefox.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Firefox.xcconfig; path = Configuration/Firefox.xcconfig; sourceTree = ""; }; + E60D03171D511398002FE3F6 /* SyncStatusResolver.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncStatusResolver.swift; sourceTree = ""; }; + E60D03291D5118DB002FE3F6 /* SyncStatusResolverTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncStatusResolverTests.swift; sourceTree = ""; }; + E6108FF81C84E91C005D25E8 /* BasePasscodeViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BasePasscodeViewController.swift; sourceTree = ""; }; + E610B0181EDC68FC00DC9372 /* EventTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EventTests.swift; sourceTree = ""; }; + E61453BD1B750A1700C3F9D7 /* RollingFileLoggerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RollingFileLoggerTests.swift; sourceTree = ""; }; + E619FB2F1E292BE100882B20 /* signedInUser.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = signedInUser.json; sourceTree = ""; }; + E61D11671EAF8F43008A305B /* PanelDataObserversTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PanelDataObserversTests.swift; sourceTree = ""; }; + E6231C001B90A44F005ABB0D /* libz.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libz.tbd; path = usr/lib/libz.tbd; sourceTree = SDKROOT; }; + E6231C021B90A466005ABB0D /* libstdc++.6.0.9.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = "libstdc++.6.0.9.tbd"; path = "usr/lib/libstdc++.6.0.9.tbd"; sourceTree = SDKROOT; }; + E6231C041B90A472005ABB0D /* libxml2.2.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libxml2.2.tbd; path = usr/lib/libxml2.2.tbd; sourceTree = SDKROOT; }; + E62AC15F1E956AFC00843532 /* FennecEnterpriseApplication.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = FennecEnterpriseApplication.entitlements; sourceTree = ""; }; + E6327A631BF6438E008D12E0 /* DebugSettingsBundleOptions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DebugSettingsBundleOptions.swift; sourceTree = ""; }; + E633E2D91C21EAF8001FFF6C /* LoginDetailViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = LoginDetailViewController.swift; path = ../Settings/LoginDetailViewController.swift; sourceTree = ""; }; + E633E3791C2204BE001FFF6C /* LoginManagerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LoginManagerTests.swift; sourceTree = ""; }; + E63ED7D71BFCD9990097D08E /* LoginTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LoginTableViewCell.swift; sourceTree = ""; }; + E63ED8E01BFD25580097D08E /* LoginListViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LoginListViewController.swift; sourceTree = ""; }; + E63F71871DB7FBE200A995C9 /* TestSQLiteMetadata.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestSQLiteMetadata.swift; sourceTree = ""; }; + E640E85D1C73A45A00C5F072 /* PasscodeEntryViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PasscodeEntryViewController.swift; sourceTree = ""; }; + E640E8691C73A47C00C5F072 /* PasscodeViews.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PasscodeViews.swift; sourceTree = ""; }; + E64ED8F91BC55AE300DAF864 /* UIAlertControllerExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = UIAlertControllerExtensions.swift; path = Extensions/UIAlertControllerExtensions.swift; sourceTree = ""; }; + E650754D1E37F6AE006961AC /* GeometryExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = GeometryExtensions.swift; path = Extensions/GeometryExtensions.swift; sourceTree = ""; }; + E650754F1E37F6D1006961AC /* NSURLExtensionsMailTo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = NSURLExtensionsMailTo.swift; path = Extensions/NSURLExtensionsMailTo.swift; sourceTree = ""; }; + E65075501E37F6D1006961AC /* UIViewExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = UIViewExtensions.swift; path = Extensions/UIViewExtensions.swift; sourceTree = ""; }; + E65075531E37F6FC006961AC /* DynamicFontHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = DynamicFontHelper.swift; path = Helpers/DynamicFontHelper.swift; sourceTree = ""; }; + E65075561E37F714006961AC /* FaviconFetcher.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FaviconFetcher.swift; sourceTree = ""; }; + E650755A1E37F747006961AC /* Swizzling.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Swizzling.h; sourceTree = ""; }; + E650755B1E37F747006961AC /* Swizzling.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = Swizzling.m; sourceTree = ""; }; + E650755D1E37F756006961AC /* Try.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Try.h; sourceTree = ""; }; + E650755E1E37F756006961AC /* Try.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = Try.m; sourceTree = ""; }; + E65075601E37F77D006961AC /* MenuHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = MenuHelper.swift; path = Helpers/MenuHelper.swift; sourceTree = ""; }; + E65075621E37F7AB006961AC /* Accessibility.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Accessibility.swift; sourceTree = ""; }; + E65075631E37F7AB006961AC /* AppConstants.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppConstants.swift; sourceTree = ""; }; + E65075641E37F7AB006961AC /* AppInfo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppInfo.swift; sourceTree = ""; }; + E65075651E37F7AB006961AC /* AssertionUtils.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AssertionUtils.swift; sourceTree = ""; }; + E65075661E37F7AB006961AC /* AsyncReducer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AsyncReducer.swift; sourceTree = ""; }; + E65075671E37F7AB006961AC /* AuthenticationKeychainInfo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AuthenticationKeychainInfo.swift; sourceTree = ""; }; + E65075681E37F7AB006961AC /* Bytes.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Bytes.swift; sourceTree = ""; }; + E65075691E37F7AB006961AC /* Cancellable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Cancellable.swift; sourceTree = ""; }; + E650756A1E37F7AB006961AC /* CrashSimulator.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CrashSimulator.h; sourceTree = ""; }; + E650756B1E37F7AB006961AC /* CrashSimulator.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CrashSimulator.m; sourceTree = ""; }; + E650756C1E37F7AB006961AC /* DeferredUtils.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DeferredUtils.swift; sourceTree = ""; }; + E650756D1E37F7AB006961AC /* DeviceInfo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DeviceInfo.swift; sourceTree = ""; }; + E650756E1E37F7AB006961AC /* effective_tld_names.dat */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = effective_tld_names.dat; sourceTree = ""; }; + E65075711E37F7AB006961AC /* ArrayExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ArrayExtensions.swift; sourceTree = ""; }; + E65075721E37F7AB006961AC /* HashExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HashExtensions.swift; sourceTree = ""; }; + E65075731E37F7AB006961AC /* HexExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HexExtensions.swift; sourceTree = ""; }; + E65075741E37F7AB006961AC /* KeychainWrapperExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KeychainWrapperExtensions.swift; sourceTree = ""; }; + E65075751E37F7AB006961AC /* NSCharacterSetExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NSCharacterSetExtensions.swift; sourceTree = ""; }; + E65075761E37F7AB006961AC /* NSFileManagerExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NSFileManagerExtensions.swift; sourceTree = ""; }; + E65075771E37F7AB006961AC /* NSMutableAttributedStringExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NSMutableAttributedStringExtensions.swift; sourceTree = ""; }; + E65075781E37F7AB006961AC /* NSScannerExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NSScannerExtensions.swift; sourceTree = ""; }; + E65075791E37F7AB006961AC /* NSStringExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NSStringExtensions.swift; sourceTree = ""; }; + E650757A1E37F7AB006961AC /* NSURLExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NSURLExtensions.swift; sourceTree = ""; }; + E650757B1E37F7AB006961AC /* NSURLProtectionSpaceExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NSURLProtectionSpaceExtensions.swift; sourceTree = ""; }; + E650757C1E37F7AB006961AC /* SetExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SetExtensions.swift; sourceTree = ""; }; + E650757D1E37F7AB006961AC /* StringExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StringExtensions.swift; sourceTree = ""; }; + E650757E1E37F7AB006961AC /* UIColorExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIColorExtensions.swift; sourceTree = ""; }; + E650757F1E37F7AB006961AC /* UIImageExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIImageExtensions.swift; sourceTree = ""; }; + E65075811E37F7AB006961AC /* FSUtils.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FSUtils.h; sourceTree = ""; }; + E65075821E37F7AB006961AC /* FSUtils.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FSUtils.m; sourceTree = ""; }; + E65075831E37F7AB006961AC /* Functions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Functions.swift; sourceTree = ""; }; + E65075841E37F7AB006961AC /* KeyboardHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KeyboardHelper.swift; sourceTree = ""; }; + E65075851E37F7AB006961AC /* KeychainCache.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KeychainCache.swift; sourceTree = ""; }; + E65075861E37F7AB006961AC /* LaunchArguments.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LaunchArguments.swift; sourceTree = ""; }; + E65075871E37F7AB006961AC /* Loader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Loader.swift; sourceTree = ""; }; + E65075881E37F7AB006961AC /* Logger.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Logger.swift; sourceTree = ""; }; + E65075891E37F7AB006961AC /* NotificationConstants.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NotificationConstants.swift; sourceTree = ""; }; + E650758B1E37F7AB006961AC /* Prefs.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Prefs.swift; sourceTree = ""; }; + E650758C1E37F7AB006961AC /* RollingFileLogger.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RollingFileLogger.swift; sourceTree = ""; }; + E650758D1E37F7AB006961AC /* SupportUtils.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SupportUtils.swift; sourceTree = ""; }; + E650758E1E37F7AB006961AC /* SystemUtils.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SystemUtils.swift; sourceTree = ""; }; + E650758F1E37F7AB006961AC /* TimeConstants.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TimeConstants.swift; sourceTree = ""; }; + E65075901E37F7AB006961AC /* UserAgent.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UserAgent.swift; sourceTree = ""; }; + E65075911E37F7AB006961AC /* WeakList.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WeakList.swift; sourceTree = ""; }; + E65075C11E37F956006961AC /* ExtensionUtils.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ExtensionUtils.swift; sourceTree = ""; }; + E653422C1C5944F90039DD9E /* BrowserPrompts.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BrowserPrompts.swift; sourceTree = ""; }; + E65607601C08B4E200534B02 /* SearchInputView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SearchInputView.swift; sourceTree = ""; }; + E659A60A1EB7C1D300D7A6AD /* BuddyBuildSDK.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = BuddyBuildSDK.framework; sourceTree = ""; }; + E65D89171C8647420006EA35 /* AppAuthenticator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppAuthenticator.swift; sourceTree = ""; }; + E660BDD81BB06521009AC090 /* TabsButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TabsButton.swift; sourceTree = ""; }; + E660BE051BB0666D009AC090 /* InnerStrokedView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InnerStrokedView.swift; sourceTree = ""; }; + E6639F171BF11E17002D0853 /* Settings.bundle */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.plug-in"; path = Settings.bundle; sourceTree = ""; }; + E663D5771BB341C4001EF30E /* ToggleButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ToggleButton.swift; sourceTree = ""; }; + E66C5B461BDA81050051AA93 /* UIImage+ImageEffects.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UIImage+ImageEffects.h"; sourceTree = ""; }; + E66C5B471BDA81050051AA93 /* UIImage+ImageEffects.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UIImage+ImageEffects.m"; sourceTree = ""; }; + E67035ED1E538B500037AB83 /* Mocking.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Mocking.swift; sourceTree = ""; }; + E67422C41CFF2D39009E8373 /* youtube.ico */ = {isa = PBXFileReference; lastKnownFileType = image.ico; path = youtube.ico; sourceTree = ""; }; + E677F0441D9423FB00ECF1FB /* SQLiteMetadata.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SQLiteMetadata.swift; sourceTree = ""; }; + E677F0531D94247300ECF1FB /* Metadata.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Metadata.swift; sourceTree = ""; }; + E67D57021D527449003917B1 /* BatchingClient.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BatchingClient.swift; sourceTree = ""; }; + E683F0A51E92E0820035D990 /* MockableHistory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockableHistory.swift; sourceTree = ""; }; + E683F0C11E93D4E90035D990 /* DictionaryExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DictionaryExtensions.swift; sourceTree = ""; }; + E689C6F71E0C6E72008BAADB /* FxASignIn.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; path = FxASignIn.js; sourceTree = ""; }; + E689C6F91E0C6E98008BAADB /* FxAContentViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FxAContentViewController.swift; sourceTree = ""; }; + E689C72F1E0C7617008BAADB /* NSAttributedStringExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = NSAttributedStringExtensions.swift; path = Extensions/NSAttributedStringExtensions.swift; sourceTree = ""; }; + E68AEDAF1B18F81A00133D99 /* SwipeAnimator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwipeAnimator.swift; sourceTree = ""; }; + E68E39BD1C46F42000B85F42 /* AppSettingsTableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppSettingsTableViewController.swift; sourceTree = ""; }; + E68E7ACA1CAC1D4500FDCA76 /* PagingPasscodeViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PagingPasscodeViewController.swift; sourceTree = ""; }; + E68E7AD91CAC207400FDCA76 /* ChangePasscodeViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChangePasscodeViewController.swift; sourceTree = ""; }; + E68E7ADB1CAC208200FDCA76 /* SetupPasscodeViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SetupPasscodeViewController.swift; sourceTree = ""; }; + E68E7ADD1CAC208A00FDCA76 /* RemovePasscodeViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RemovePasscodeViewController.swift; sourceTree = ""; }; + E68F36971EA694000048CF44 /* PanelDataObservers.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = PanelDataObservers.swift; path = ../Client/Frontend/Home/PanelDataObservers.swift; sourceTree = ""; }; + E6927EBF1C7B6FB800D03F75 /* ErrorToast.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ErrorToast.swift; sourceTree = ""; }; + E692E3281C46E62D009D1240 /* AuthenticationSettingsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AuthenticationSettingsViewController.swift; sourceTree = ""; }; + E692E3361C46E86A009D1240 /* AppSettingsOptions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppSettingsOptions.swift; sourceTree = ""; }; + E693F0D81E9D64BD0086DC17 /* OptionalExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OptionalExtensions.swift; sourceTree = ""; }; + E696FE501C47F86E00EC007C /* AuthenticatorTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AuthenticatorTests.swift; sourceTree = ""; }; + E698FFD91B4AADF40001F623 /* TabScrollController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TabScrollController.swift; sourceTree = ""; }; + E69922121B94E3EF007C480D /* Licenses.html */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.html; path = Licenses.html; sourceTree = ""; }; + E69DB0751E97DEA9008A67E6 /* SyncTelemetry.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = SyncTelemetry.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + E69DB0771E97DEA9008A67E6 /* SyncTelemetry.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SyncTelemetry.h; sourceTree = ""; }; + E69DB0781E97DEA9008A67E6 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + E69DB07D1E97DEA9008A67E6 /* SyncTelemetryTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SyncTelemetryTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + E69DB0861E97DEAA008A67E6 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + E69DB0B51E97E2AC008A67E6 /* SyncTelemetry.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncTelemetry.swift; sourceTree = ""; }; + E69DB0BA1E97E301008A67E6 /* ActivityStreamTopics.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ActivityStreamTopics.swift; sourceTree = ""; }; + E69DB0C01E97E320008A67E6 /* BookmarkTelemetryPing.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BookmarkTelemetryPing.swift; sourceTree = ""; }; + E69E06B91C76173D00D0F926 /* RequirePasscodeIntervalViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RequirePasscodeIntervalViewController.swift; sourceTree = ""; }; + E69E06C81C76198000D0F926 /* AuthenticationManagerConstants.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AuthenticationManagerConstants.swift; sourceTree = ""; }; + E6A92ADA1C52A8DA00743291 /* LoginInputTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LoginInputTests.swift; sourceTree = ""; }; + E6B4C3D71C68F55C001F97E8 /* JSPrompt.html */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.html; path = JSPrompt.html; sourceTree = ""; }; + E6B4C4021C68F58B001F97E8 /* BrowserTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BrowserTests.swift; sourceTree = ""; }; + E6BA20111E52165800697F9C /* SyncTelemetryTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = SyncTelemetryTests.swift; path = StorageTests/SyncTelemetryTests.swift; sourceTree = ""; }; + E6BE53CC1D9177B10074909A /* TestSQLiteHistoryRecommendations.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestSQLiteHistoryRecommendations.swift; sourceTree = ""; }; + E6C191D41E38F7B7000A213B /* Cartfile */ = {isa = PBXFileReference; lastKnownFileType = text; path = Cartfile; sourceTree = ""; }; + E6C191D51E38F7B7000A213B /* Cartfile.resolved */ = {isa = PBXFileReference; lastKnownFileType = text; path = Cartfile.resolved; sourceTree = ""; }; + E6C70E811E28314700F8DB57 /* PingCentreTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PingCentreTests.swift; sourceTree = ""; }; + E6C9EB6A1E2FBFC300D5CE80 /* signedInUser.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = signedInUser.json; sourceTree = ""; }; + E6CF28E61CB43B7900151AB3 /* SensitiveViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SensitiveViewController.swift; sourceTree = ""; }; + E6D7C31C1CF4E68D00E746BA /* TestBookmarkModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = TestBookmarkModel.swift; path = SyncTests/TestBookmarkModel.swift; sourceTree = ""; }; + E6D8D5E61B569D70009E5A58 /* BrowserTrayAnimators.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BrowserTrayAnimators.swift; sourceTree = ""; }; + E6DCC1ED1DCBB6AA00CEC4B7 /* Fennec.enterprise.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Fennec.enterprise.xcconfig; path = Configuration/Fennec.enterprise.xcconfig; sourceTree = ""; }; + E6EAC5951B29CB3A00E1DE1E /* scrollablePage.html */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.html; path = scrollablePage.html; sourceTree = ""; }; + E6EDE81D1D524475007A0732 /* BatchingClientTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = BatchingClientTests.swift; path = SyncTests/BatchingClientTests.swift; sourceTree = ""; }; + E6F368281D7F594F008CDD67 /* SQLiteHistoryRecommendations.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SQLiteHistoryRecommendations.swift; sourceTree = ""; }; + E6F6D6A11E9E6AF90012072B /* DeferredTestUtils.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DeferredTestUtils.swift; sourceTree = ""; }; + E6F738741EB7A8D300B50143 /* FennecApplication.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = FennecApplication.entitlements; sourceTree = ""; }; + E6F738751EB7A97100B50143 /* FirefoxBetaApplication.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = FirefoxBetaApplication.entitlements; sourceTree = ""; }; + E6F738761EB7A97500B50143 /* FirefoxApplication.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = FirefoxApplication.entitlements; sourceTree = ""; }; + E6F9650C1B2F1CF20034B023 /* SharedTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SharedTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + E6F9650F1B2F1CF20034B023 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + E6F9653B1B2F1D5D0034B023 /* NSURLExtensionsTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NSURLExtensionsTests.swift; sourceTree = ""; }; + E6F965411B2F25110034B023 /* NSMutableAttributedStringExtensionsTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NSMutableAttributedStringExtensionsTests.swift; sourceTree = ""; }; + E6FCC43C1C40565200DF6113 /* FirefoxBeta.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = FirefoxBeta.xcconfig; path = Configuration/FirefoxBeta.xcconfig; sourceTree = ""; }; + E6FF6AC91D873CFF0070C294 /* PageMetadata.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PageMetadata.swift; sourceTree = ""; }; + EB2A63251F3B49A7004EF8B0 /* ContentBlockerHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContentBlockerHelper.swift; sourceTree = ""; }; + EB2A63361F3B5134004EF8B0 /* disconnect-analytics.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "disconnect-analytics.json"; sourceTree = ""; }; + EB2A63371F3B5135004EF8B0 /* disconnect-content.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "disconnect-content.json"; sourceTree = ""; }; + EB2A63381F3B5135004EF8B0 /* disconnect-advertising.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "disconnect-advertising.json"; sourceTree = ""; }; + EB2A63391F3B5135004EF8B0 /* disconnect-social.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "disconnect-social.json"; sourceTree = ""; }; + EBA31D781F7999030055463D /* SyncPingCentre.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncPingCentre.swift; sourceTree = ""; }; + EBA31D7A1F79990C0055463D /* SyncTelemetryEvents.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncTelemetryEvents.swift; sourceTree = ""; }; + EBA31D7C1F79996E0055463D /* SyncTelemetryUtils.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncTelemetryUtils.swift; sourceTree = ""; }; + EBA31D7E1F799BE20055463D /* Telemetry.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Telemetry.framework; path = Carthage/Build/iOS/Telemetry.framework; sourceTree = ""; }; + EBF47E6F1F7979DF00899189 /* UnifiedTelemetry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnifiedTelemetry.swift; sourceTree = ""; }; + F35B8D2A1D6380EA008E3D61 /* SessionRestore.html */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.html; path = SessionRestore.html; sourceTree = ""; }; + F35B8D2C1D6383E9008E3D61 /* SessionRestoreHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SessionRestoreHelper.swift; sourceTree = ""; }; + F35B8D2E1D638408008E3D61 /* SessionRestoreHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SessionRestoreHandler.swift; sourceTree = ""; }; + F84B21BE1A090F8100AAB793 /* Client.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Client.app; sourceTree = BUILT_PRODUCTS_DIR; }; + F84B21D31A090F8100AAB793 /* ClientTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ClientTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + F84B21D81A090F8100AAB793 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + F84B21D91A090F8100AAB793 /* ClientTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClientTests.swift; sourceTree = ""; }; + F84B21E51A0910F600AAB793 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = AppDelegate.swift; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; + F84B21EF1A0910F600AAB793 /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = ""; }; + F84B22221A09122500AAB793 /* HomePanelViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HomePanelViewController.swift; sourceTree = ""; }; + F84B22261A09127C00AAB793 /* Home.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Home.xcassets; sourceTree = ""; }; + F84B22431A09165600AAB793 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + F84B22491A0920C600AAB793 /* ShareTo.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = ShareTo.appex; sourceTree = BUILT_PRODUCTS_DIR; }; + F84B225C1A09210A00AAB793 /* SendTo.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = SendTo.appex; sourceTree = BUILT_PRODUCTS_DIR; }; + F8708D201A0970990051AB07 /* ActionViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActionViewController.swift; sourceTree = ""; }; + F8708D211A0970990051AB07 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + F8708D221A0970990051AB07 /* MainInterface.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = MainInterface.storyboard; sourceTree = ""; }; + F8708D251A0970990051AB07 /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = ""; }; + F8708D261A0970990051AB07 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + F8708D291A0970990051AB07 /* ShareViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareViewController.swift; sourceTree = ""; }; + FA6B2AC11D41F02D00429414 /* Punycode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Punycode.swift; sourceTree = ""; }; + FA9293D31D6580E100AC8D33 /* QRCodeViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = QRCodeViewController.swift; sourceTree = ""; }; + FA9294001D6584A200AC8D33 /* QRCode.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = QRCode.xcassets; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 2827315A1ABC9BE600AA1954 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + D09A0CDC1FAA24CC009A0273 /* Account.framework in Frameworks */, + D09A0CDD1FAA24D0009A0273 /* FxA.framework in Frameworks */, + D09A0CDB1FAA24C8009A0273 /* Shared.framework in Frameworks */, + D09A0CDA1FAA24C2009A0273 /* Storage.framework in Frameworks */, + E69DB0A81E97DF22008A67E6 /* SyncTelemetry.framework in Frameworks */, + 7B604F901C494B0F006EEEC3 /* Alamofire.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 282731651ABC9BE700AA1954 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 7B604FC01C495FD1006EEEC3 /* XCGLogger.framework in Frameworks */, + 28ECD9BF1BA1F19900D829DA /* libz.tbd in Frameworks */, + 282731691ABC9BE700AA1954 /* Sync.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 288A2D821AB8B3260023ABC3 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 7BA4A9641C4CFE840091D032 /* Deferred.framework in Frameworks */, + D09A0D301FAB8AF3009A0273 /* Sentry.framework in Frameworks */, + 7BA4A94C1C4CF03B0091D032 /* SwiftKeychainWrapper.framework in Frameworks */, + 7B604FBC1C495E1E006EEEC3 /* XCGLogger.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 28CE83DE1A1D1E7C00576538 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + E6231C031B90A466005ABB0D /* libstdc++.6.0.9.tbd in Frameworks */, + E6231C051B90A472005ABB0D /* libxml2.2.tbd in Frameworks */, + E6231C011B90A44F005ABB0D /* libz.tbd in Frameworks */, + E4A888161A95679500CDC337 /* FxA.framework in Frameworks */, + 2FA436121ABB83B4008031D1 /* Account.framework in Frameworks */, + E4D5672F1ADECE2800F1EFE7 /* ReadingList.framework in Frameworks */, + 288A2D9D1AB8B3260023ABC3 /* Shared.framework in Frameworks */, + 2FCAE2311ABB51F800877008 /* Storage.framework in Frameworks */, + 282731751ABC9BE700AA1954 /* Sync.framework in Frameworks */, + E69DB08A1E97DEAA008A67E6 /* SyncTelemetry.framework in Frameworks */, + 7B6050011C49653B006EEEC3 /* AdjustSdk.framework in Frameworks */, + E4B3348A1BBF23F9004E2BFF /* AdSupport.framework in Frameworks */, + 7B604F861C494983006EEEC3 /* Alamofire.framework in Frameworks */, + 3BA9A0231D2C208C00BD418C /* Fuzi.framework in Frameworks */, + 7BA4A9471C4CED900091D032 /* GCDWebServers.framework in Frameworks */, + E4B334881BBF23F3004E2BFF /* iAd.framework in Frameworks */, + 0B8E0FF41A932BD500161DC3 /* ImageIO.framework in Frameworks */, + E4F2DAC01F620C0300A556CD /* Leanplum.framework in Frameworks */, + 7BA4A94A1C4CEFC70091D032 /* OnePasswordExtension.framework in Frameworks */, + 7B8A47F61D01D3B400C07734 /* PassKit.framework in Frameworks */, + 7B604F9B1C4950F2006EEEC3 /* SDWebImage.framework in Frameworks */, + 7B604FA21C495268006EEEC3 /* SnapKit.framework in Frameworks */, + 3B4988CE1E42B01800A12FDA /* SwiftyJSON.framework in Frameworks */, + EBA31D861F799BE20055463D /* Telemetry.framework in Frameworks */, + 7B604FBB1C495D8A006EEEC3 /* XCGLogger.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 2FA435F71ABB83B4008031D1 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + E60222DC1C6E55610061C436 /* FxA.framework in Frameworks */, + D09A0CD91FAA2413009A0273 /* Shared.framework in Frameworks */, + 7B604F891C4949B1006EEEC3 /* Alamofire.framework in Frameworks */, + 7B604FBE1C495E4E006EEEC3 /* XCGLogger.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 2FA436021ABB83B4008031D1 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 7B604F8A1C4949C1006EEEC3 /* Alamofire.framework in Frameworks */, + 2FA436061ABB83B4008031D1 /* Account.framework in Frameworks */, + 7B604FBF1C495E60006EEEC3 /* XCGLogger.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 2FCAE2161ABB51F800877008 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 0B742CCF1B32493E00EE9264 /* libsqlcipher.a in Frameworks */, + D09A0CD81FAA23F6009A0273 /* Shared.framework in Frameworks */, + 7B604FBD1C495E31006EEEC3 /* XCGLogger.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 2FCAE2211ABB51F800877008 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 2FCAE2251ABB51F800877008 /* Storage.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 390527471C874D35007E0BB7 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 39409A3F1C90E68300DAE683 /* Shared.framework in Frameworks */, + 3905274C1C874D35007E0BB7 /* NotificationCenter.framework in Frameworks */, + 39D9E6851C89E9690071FADA /* SnapKit.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 397848D81ED86605004C0C0B /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 396E38ED1EE0C63500CC180F /* Sync.framework in Frameworks */, + 396E38E61EE0843500CC180F /* Storage.framework in Frameworks */, + 396E38E31EE083A400CC180F /* Shared.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 3B43E3CD1D95C48D00BBA9DB /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 3BFE4B041D342FB800DDF53F /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + E6EC6EED1E53548A0067985D /* EarlGrey.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 7BEB64461C7345600092C02E /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 7BEB64531C7345990092C02E /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + D39FA15C1A83E0EC00EE869C /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 7BA4A9661C4D008C0091D032 /* Deferred.framework in Frameworks */, + E6231C071B90A712005ABB0D /* libz.tbd in Frameworks */, + 7B604F991C494F74006EEEC3 /* KIF.framework in Frameworks */, + D36998891AD70A0A00650C6C /* IOKit.framework in Frameworks */, + D39FA16C1A83E17800EE869C /* CoreGraphics.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + E4BA8A281B4B0A1600BC2E95 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 2805F5A61B8BCA7A00268FD1 /* ReadingList.framework in Frameworks */, + E444F7DC1B4B1B7E00FEB19B /* Shared.framework in Frameworks */, + E4BA8A8A1B4B10F400BC2E95 /* Storage.framework in Frameworks */, + E444F8271B4C1E8800FEB19B /* Sync.framework in Frameworks */, + E444F7E51B4B1CB600FEB19B /* ImageIO.framework in Frameworks */, + 7B604F9E1C49514A006EEEC3 /* SDWebImage.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + E4D567141ADECE2700F1EFE7 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + E4483B5A1ADED63300B485A7 /* Shared.framework in Frameworks */, + E60222E31C6E57B20061C436 /* SQLite.framework in Frameworks */, + 7B604F911C494B3C006EEEC3 /* Alamofire.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + E4D5671F1ADECE2700F1EFE7 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + E4D567231ADECE2700F1EFE7 /* ReadingList.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + E69DB0711E97DEA9008A67E6 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + E69DB0A91E97DF73008A67E6 /* Shared.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + E69DB07A1E97DEA9008A67E6 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + E69DB07E1E97DEAA008A67E6 /* SyncTelemetry.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + E6F965091B2F1CF20034B023 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + E6F965121B2F1CF20034B023 /* Shared.framework in Frameworks */, + 7BA4A9651C4D007B0091D032 /* Deferred.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + F84B21D01A090F8100AAB793 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + E6231C081B90A71E005ABB0D /* libz.tbd in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + F84B22461A0920C600AAB793 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 2FC244B31A855E05007CE41D /* FxA.framework in Frameworks */, + 28078A471B1507EF0053B46A /* ReadingList.framework in Frameworks */, + 28C8D1391AD4CE9100F62011 /* Shared.framework in Frameworks */, + 28C8D11D1AD4CE8900F62011 /* Storage.framework in Frameworks */, + 2868FA061ADF7B69000D9B1D /* Sync.framework in Frameworks */, + 7B604F871C494995006EEEC3 /* Alamofire.framework in Frameworks */, + 3BA9A0321D2C2C0500BD418C /* Fuzi.framework in Frameworks */, + 0B75AEA91AC20FB20015E5DC /* ImageIO.framework in Frameworks */, + 7B604F9D1C495143006EEEC3 /* SDWebImage.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + F84B22591A09210A00AAB793 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 2FC244B81A855E0D007CE41D /* FxA.framework in Frameworks */, + 28293BB51B333EA2004A65EC /* ReadingList.framework in Frameworks */, + 28C8D13B1AD4CEB100F62011 /* Shared.framework in Frameworks */, + 28C8D13C1AD4CEB400F62011 /* Storage.framework in Frameworks */, + 2868FA231ADF7B70000D9B1D /* Sync.framework in Frameworks */, + 3BA9A0311D2C2C0400BD418C /* Fuzi.framework in Frameworks */, + 0B75AEAA1AC20FBC0015E5DC /* ImageIO.framework in Frameworks */, + 7B604F9C1C495135006EEEC3 /* SDWebImage.framework in Frameworks */, + 7B604FA31C4952A4006EEEC3 /* SnapKit.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 0B1C05D41A798B1F004C78B0 /* UIImageViewAligned */ = { + isa = PBXGroup; + children = ( + 0B1C05D51A798B1F004C78B0 /* UIImageViewAligned.h */, + 0B1C05D61A798B1F004C78B0 /* UIImageViewAligned.m */, + ); + name = UIImageViewAligned; + path = ThirdParty/UIImageViewAligned/UIImageViewAligned; + sourceTree = ""; + }; + 0B742CC71B32491400EE9264 /* Products */ = { + isa = PBXGroup; + children = ( + 0B742CCC1B32491400EE9264 /* libsqlcipher.a */, + ); + name = Products; + sourceTree = ""; + }; + 282731601ABC9BE600AA1954 /* Supporting Files */ = { + isa = PBXGroup; + children = ( + 282731621ABC9BE600AA1954 /* Sync-Bridging-Header.h */, + 282731611ABC9BE600AA1954 /* Info.plist */, + ); + name = "Supporting Files"; + sourceTree = ""; + }; + 2827316F1ABC9BE700AA1954 /* Supporting Files */ = { + isa = PBXGroup; + children = ( + 28ECD9A21BA1EBA800D829DA /* SyncTests-Bridging-Header.h */, + 282731701ABC9BE700AA1954 /* Info.plist */, + ); + name = "Supporting Files"; + path = SyncTests; + sourceTree = ""; + }; + 284AA38F1AD4C115009041C5 /* Interfaces */ = { + isa = PBXGroup; + children = ( + ); + name = Interfaces; + sourceTree = ""; + }; + 28532D301C483DEB000072D9 /* Bookmarks */ = { + isa = PBXGroup; + children = ( + 2FCAE23F1ABB531100877008 /* Bookmarks.swift */, + 2829D3791C2F0A7F00DCF931 /* BookmarksModel.swift */, + 283586FC1C73F18E00A55435 /* CachingItemSource.swift */, + 28532D311C483E3D000072D9 /* CompletionOps.swift */, + 2852B8431C51996B00591EAC /* Trees.swift */, + ); + name = Bookmarks; + sourceTree = ""; + }; + 288A2D7E1AB8B2AD0023ABC3 /* Result */ = { + isa = PBXGroup; + children = ( + 288A2DB31AB8B38D0023ABC3 /* Error.swift */, + 288A2DB41AB8B38D0023ABC3 /* Result.swift */, + ); + name = Result; + sourceTree = ""; + }; + 288A2D7F1AB8B2B20023ABC3 /* Box */ = { + isa = PBXGroup; + children = ( + 288A2DA71AB8B3700023ABC3 /* Box.swift */, + 288A2DA81AB8B3700023ABC3 /* BoxType.swift */, + 288A2DA91AB8B3700023ABC3 /* MutableBox.swift */, + ); + name = Box; + sourceTree = ""; + }; + 2891F2B91F991185001B105E /* fixtures */ = { + isa = PBXGroup; + children = ( + 2891F2BA1F991185001B105E /* v33.db */, + ); + path = fixtures; + sourceTree = ""; + }; + 28926B3C1AC0F176009C0B1D /* Records */ = { + isa = PBXGroup; + children = ( + 28926B361AC0F0DD009C0B1D /* EnvelopeJSON.swift */, + 28926B191AC0F074009C0B1D /* EncryptedJSON.swift */, + 28926B391AC0F128009C0B1D /* CleartextPayloadJSON.swift */, + 28CE83BD1A1D1D3200576538 /* KeysPayload.swift */, + 28AA941C1B97DCA800703DC6 /* BookmarkPayload.swift */, + 28CE83BB1A1D1D3200576538 /* ClientPayload.swift */, + 2894C16B1AE89FD500F1F92F /* HistoryPayload.swift */, + 28ED02011B26123E003948B2 /* LoginPayload.swift */, + 2853C5401AD84C6800C4F31D /* TabsPayload.swift */, + 28CE83BE1A1D1D3200576538 /* Record.swift */, + ); + name = Records; + sourceTree = ""; + }; + 28926B3D1AC0F1A6009C0B1D /* Meta */ = { + isa = PBXGroup; + children = ( + 2F3724E21ABF3C19007607FA /* Info.swift */, + 28CE83BF1A1D1D3200576538 /* SyncMeta.swift */, + 28E23C111AC5A5EE00F5AC85 /* State.swift */, + ); + name = Meta; + sourceTree = ""; + }; + 2894C1641AE89DBC00F1F92F /* Synchronizers */ = { + isa = PBXGroup; + children = ( + 28C28BD81C51A2FA00D5460E /* Bookmarks */, + 2894C1681AE89DDC00F1F92F /* ClientsSynchronizer.swift */, + 28A17B661BEC727500BC14ED /* Downloader.swift */, + 2894C1421AE89D8900F1F92F /* HistorySynchronizer.swift */, + 28ED02281B262E0A003948B2 /* IndependentRecordSynchronizer.swift */, + 28ED02251B262B5E003948B2 /* LoginsSynchronizer.swift */, + 2F3724E41ABF3C19007607FA /* Synchronizer.swift */, + 2894C1651AE89DD200F1F92F /* TabsSynchronizer.swift */, + ); + name = Synchronizers; + sourceTree = ""; + }; + 28C077911A3B05C200834FE5 /* SyncTests */ = { + isa = PBXGroup; + children = ( + 2827316F1ABC9BE700AA1954 /* Supporting Files */, + CE339ED61F2507A1009BE0E6 /* TestBookmarksSynchronizer.swift */, + 28D980221C47149000277055 /* TestBookmarkTreeMerging.swift */, + E6D7C31C1CF4E68D00E746BA /* TestBookmarkModel.swift */, + CEFC983B1EC0DC60008A3E48 /* TestBookmarksRepairRequestor.swift */, + 28ECD9791BA1EA2200D829DA /* MockSyncServer.swift */, + 28C077971A3B064000834FE5 /* CryptoTests.swift */, + 28ECD9F31BA1F59800D829DA /* DownloadTests.swift */, + 2855611E1AEFFA1C00D5ED5B /* HistorySynchronizerTests.swift */, + 28F596A01ACA13CA0071DDCC /* InfoTests.swift */, + 2F3724C41ABF3C01007607FA /* LiveStorageClientTests.swift */, + 2F67C5251BB0CB4E00E7B73A /* MetaGlobalTests.swift */, + 2F8C76561BC32F3C00D5E4E0 /* MockSyncServerTests.swift */, + E67035ED1E538B500037AB83 /* Mocking.swift */, + 28C0779D1A3B066000834FE5 /* RecordTests.swift */, + 50027345B49B967409DDA348 /* StateTests.swift */, + E6EDE81D1D524475007A0732 /* BatchingClientTests.swift */, + 2F3724C51ABF3C01007607FA /* StorageClientTests.swift */, + E6BA20111E52165800697F9C /* SyncTelemetryTests.swift */, + E42736061EA858CF009C428E /* TabsPayloadTests.swift */, + ); + name = SyncTests; + sourceTree = ""; + }; + 28C28BD81C51A2FA00D5460E /* Bookmarks */ = { + isa = PBXGroup; + children = ( + CE564D8D1EB7BD7700BEDDDC /* BookmarksRepairRequestor.swift */, + 288E67C11B9E730900AE2C6A /* BookmarksDownloader.swift */, + 280CB1361C20E0A3005AF147 /* BookmarksSynchronizer.swift */, + 28C28BFC1C51A3B900D5460E /* Merging.swift */, + 28532CE51C48098D000072D9 /* ThreeWayTreeMerger.swift */, + ); + name = Bookmarks; + path = Synchronizers/Bookmarks; + sourceTree = ""; + }; + 28CE83B91A1D1D3200576538 /* Sync */ = { + isa = PBXGroup; + children = ( + 2894C1641AE89DBC00F1F92F /* Synchronizers */, + 284AA38F1AD4C115009041C5 /* Interfaces */, + 282731601ABC9BE600AA1954 /* Supporting Files */, + 28926B3D1AC0F1A6009C0B1D /* Meta */, + 28926B3C1AC0F176009C0B1D /* Records */, + E69DB0B31E97E0E4008A67E6 /* Telemetry */, + E67D57021D527449003917B1 /* BatchingClient.swift */, + 28CE83BC1A1D1D3200576538 /* KeyBundle.swift */, + 2F3724E31ABF3C19007607FA /* StorageClient.swift */, + 28E91E741B443AD5009DF274 /* SyncConstants.swift */, + 2885023E1AC117A500E7F670 /* SyncStateMachine.swift */, + 288501DD1AC0F61D00E7F670 /* RequestExtensions.swift */, + ); + path = Sync; + sourceTree = ""; + }; + 28CE83CB1A1D1D5100576538 /* Products */ = { + isa = PBXGroup; + children = ( + 28CE83D01A1D1D5100576538 /* FxA.framework */, + 28CE83D21A1D1D5100576538 /* FxATests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 28CE83EF1A1D246900576538 /* Third-Party Source */ = { + isa = PBXGroup; + children = ( + E66C5B451BDA81050051AA93 /* Apple */, + E659A6091EB7C1D300D7A6AD /* BuddyBuild */, + 288A2D7F1AB8B2B20023ABC3 /* Box */, + 28CE83CA1A1D1D5100576538 /* FxA.xcodeproj */, + D30EBB5A1C75503800105AE9 /* KIF.xcodeproj */, + 288A2D7E1AB8B2AD0023ABC3 /* Result */, + 0B742CC61B32491400EE9264 /* sqlcipher.xcodeproj */, + 7B9A62B91C4002B1005C83DC /* SQLite.xcodeproj */, + 0B1C05D41A798B1F004C78B0 /* UIImageViewAligned */, + 3B6889C41D66950E002AC85E /* UIImageColors.swift */, + 3B0943801D6CC4FC004F24E1 /* FilledPageControl.swift */, + 7479B4ED1C5306A200DF000B /* Reachability.swift */, + ); + name = "Third-Party Source"; + sourceTree = ""; + }; + 28EADE5C1AFC3A6D007FB2FB /* Extensions */ = { + isa = PBXGroup; + children = ( + E650754D1E37F6AE006961AC /* GeometryExtensions.swift */, + D83821FF1FC7961D00303C12 /* DispatchQueueExtensions.swift */, + E689C72F1E0C7617008BAADB /* NSAttributedStringExtensions.swift */, + 28EADE381AFC3898007FB2FB /* UIImageViewExtensions.swift */, + E64ED8F91BC55AE300DAF864 /* UIAlertControllerExtensions.swift */, + A83E5AB61C1D993D0026D912 /* UIPasteboardExtensions.swift */, + E650754F1E37F6D1006961AC /* NSURLExtensionsMailTo.swift */, + E65075501E37F6D1006961AC /* UIViewExtensions.swift */, + C817B34C1FC609500086018E /* UIScrollViewSwizzled.swift */, + ); + name = Extensions; + sourceTree = ""; + }; + 2F44FC551A9E83E200FD20CC /* Settings */ = { + isa = PBXGroup; + children = ( + 8D8251721F4DE67E00780643 /* AdvanceAccountSettingViewController.swift */, + 3B39EDCA1E16E1AA00EF029F /* CustomSearchViewController.swift */, + E692E3361C46E86A009D1240 /* AppSettingsOptions.swift */, + E68E39BD1C46F42000B85F42 /* AppSettingsTableViewController.swift */, + 0B62EFD11AD63CD100ACB9CD /* Clearables.swift */, + D3E8EEE71B97A87A001900FB /* ClearPrivateDataTableViewController.swift */, + E689C6F91E0C6E98008BAADB /* FxAContentViewController.swift */, + 39AC59191CC574AA0042C2F5 /* HomePageSettingsViewController.swift */, + 392ED6B61D06E85E009D9B62 /* NewTabChoiceViewController.swift */, + D81E45121F82C56C004EFFBA /* NewTabContentSettingsViewController.swift */, + 2F44FCCA1A9E972E00FD20CC /* SearchEnginePicker.swift */, + 2F44FCC61A9E8CF500FD20CC /* SearchSettingsTableViewController.swift */, + 74E36D771B71323500D69DA1 /* SettingsContentViewController.swift */, + 2F44FC711A9E840300FD20CC /* SettingsNavigationController.swift */, + 2F44FCC41A9E85E900FD20CC /* SettingsTableViewController.swift */, + 7B4980A71CE363ED0017547C /* Settings.xcassets */, + ); + path = Settings; + sourceTree = ""; + }; + 2FA435FC1ABB83B4008031D1 /* Account */ = { + isa = PBXGroup; + children = ( + 2F14E1391ABB890800FF98DB /* Account-Bridging-Header.h */, + 2FA436221ABB8436008031D1 /* FirefoxAccount.swift */, + 2FA436231ABB8436008031D1 /* FirefoxAccountConfiguration.swift */, + 2FA436251ABB8436008031D1 /* FxAClient10.swift */, + D32A350D1D6530D80066DAE9 /* FxADevice.swift */, + D3DBE6E41D6516FE00033FFF /* FxADeviceRegistration.swift */, + 2F1A3DE01ABE3C90002F1E15 /* FxALoginStateMachine.swift */, + 3905B4D41E8E7A6B0027D953 /* FxAPushMessageHandler.swift */, + 2FFC4D1A1ABE3C360081D675 /* FxAState.swift */, + 2FA436271ABB8436008031D1 /* HawkHelper.swift */, + 2FDBCF601ABFC9DE00AFF7F0 /* SyncAuthState.swift */, + 2FA436281ABB8436008031D1 /* TokenServerClient.swift */, + 2FA435FD1ABB83B4008031D1 /* Supporting Files */, + ); + path = Account; + sourceTree = ""; + }; + 2FA435FD1ABB83B4008031D1 /* Supporting Files */ = { + isa = PBXGroup; + children = ( + 2FA435FE1ABB83B4008031D1 /* Info.plist */, + ); + name = "Supporting Files"; + sourceTree = ""; + }; + 2FA4360B1ABB83B4008031D1 /* AccountTests */ = { + isa = PBXGroup; + children = ( + 2FA436371ABB8448008031D1 /* FirefoxAccountTests.swift */, + 2FA436381ABB8448008031D1 /* FxAClient10Tests.swift */, + 2F1A3DFD1ABE3C9C002F1E15 /* FxALoginStateMachineTests.swift */, + 2FFC4D371ABE3C420081D675 /* FxAStateTests.swift */, + 2FA4363A1ABB8448008031D1 /* HawkHelperTests.swift */, + 2FA4363B1ABB8448008031D1 /* LiveAccountTest.swift */, + 2FDBCF9A1AC0ADB500AFF7F0 /* SyncAuthStateTests.swift */, + 2FA4363C1ABB8448008031D1 /* TokenServerClientTests.swift */, + E6C9EB6A1E2FBFC300D5CE80 /* signedInUser.json */, + 2FA4360C1ABB83B4008031D1 /* Supporting Files */, + ); + path = AccountTests; + sourceTree = ""; + }; + 2FA4360C1ABB83B4008031D1 /* Supporting Files */ = { + isa = PBXGroup; + children = ( + E619FB2F1E292BE100882B20 /* signedInUser.json */, + 2FA4360D1ABB83B4008031D1 /* Info.plist */, + ); + name = "Supporting Files"; + sourceTree = ""; + }; + 2FCAE21B1ABB51F800877008 /* Storage */ = { + isa = PBXGroup; + children = ( + 74B195431CF503FC007F36EF /* RecentlyClosedTabs.swift */, + 2FCAE33D1ABB5F1800877008 /* Storage-Bridging-Header.h */, + D37DE2821CA2047500A5EC69 /* CertStore.swift */, + 28C4AB711AD42D4300D9ACE3 /* Clients.swift */, + 2FCAE2411ABB531100877008 /* Cursor.swift */, + 28302E3F1AF0747800521E2E /* DatabaseError.swift */, + 394CF6CE1BAA493C00906917 /* DefaultSuggestedSites.swift */, + D3BF8CBA1B7425570007AFE6 /* DiskImageStore.swift */, + E65075C11E37F956006961AC /* ExtensionUtils.swift */, + 2FCAE2421ABB531100877008 /* Favicons.swift */, + 2FCAE2431ABB531100877008 /* FileAccessor.swift */, + 2FCAE2441ABB531100877008 /* History.swift */, + 0BDA56B11B26B1E4008C9B96 /* Logins.swift */, + E677F0531D94247300ECF1FB /* Metadata.swift */, + 0B3E7DB91B27AB4C00E2E84D /* MockLogins.swift */, + 285D3B671B4380B70035FD22 /* Queue.swift */, + E6FF6AC91D873CFF0070C294 /* PageMetadata.swift */, + 2FCAE2471ABB531100877008 /* RemoteTabs.swift */, + 2829D39F1C2F0AD400DCF931 /* Sharing.swift */, + 2FCAE2481ABB531100877008 /* Site.swift */, + 0B54BD181B698B7C004C822C /* SuggestedSites.swift */, + 28B62ACD1BC745E7004A585A /* Syncable.swift */, + 7BF5A1E91B41640500EA9DD8 /* SyncQueue.swift */, + 2FCAE25C1ABB531100877008 /* Visit.swift */, + 28532D301C483DEB000072D9 /* Bookmarks */, + 2FCAE2491ABB531100877008 /* SQL */, + 2FCAE21C1ABB51F800877008 /* Supporting Files */, + 2FCAE25A1ABB531100877008 /* ThirdParty */, + ); + path = Storage; + sourceTree = ""; + }; + 2FCAE21C1ABB51F800877008 /* Supporting Files */ = { + isa = PBXGroup; + children = ( + ); + name = "Supporting Files"; + sourceTree = ""; + }; + 2FCAE22A1ABB51F800877008 /* StorageTests */ = { + isa = PBXGroup; + children = ( + D37DE2C81CA356F900A5EC69 /* testcert1.pem */, + D37DE2C91CA356F900A5EC69 /* testcert2.pem */, + D37DE2C61CA356D800A5EC69 /* CertTests.swift */, + D3BF8CBC1B7472FA0007AFE6 /* DiskImageStoreTests.swift */, + 2FCAE2791ABB533A00877008 /* MockFiles.swift */, + 289A4C121C4EB90600A460E3 /* StorageTestUtils.swift */, + 7BF5A1ED1B429B3100EA9DD8 /* SyncCommandsTests.swift */, + 281B02991C037C1F005202C3 /* TestBrowserDB.swift */, + 0BDA56AE1B26B1D5008C9B96 /* TestLogins.swift */, + 28D158AC1AFD90E500F9C065 /* TestSQLiteBookmarks.swift */, + 2FCAE27D1ABB533A00877008 /* TestSQLiteHistory.swift */, + E6BE53CC1D9177B10074909A /* TestSQLiteHistoryRecommendations.swift */, + E63F71871DB7FBE200A995C9 /* TestSQLiteMetadata.swift */, + 2FCAE27A1ABB533A00877008 /* TestSQLiteRemoteClientsAndTabs.swift */, + D32CACEC1AE04DA1000658EB /* TestSwiftData.swift */, + 2FCAE22B1ABB51F800877008 /* Supporting Files */, + ); + path = StorageTests; + sourceTree = ""; + }; + 2FCAE22B1ABB51F800877008 /* Supporting Files */ = { + isa = PBXGroup; + children = ( + 2891F2B91F991185001B105E /* fixtures */, + 2FCAE22C1ABB51F800877008 /* Info.plist */, + ); + name = "Supporting Files"; + sourceTree = ""; + }; + 2FCAE2491ABB531100877008 /* SQL */ = { + isa = PBXGroup; + children = ( + 2FCAE24B1ABB531100877008 /* BrowserDB.swift */, + 282915E51AF1A7920006EEB5 /* BrowserSchema.swift */, + D0B29EDF1F460BDF00C7CEFC /* LoginsSchema.swift */, + D018F93D1F44A7190098F8CA /* Schema.swift */, + 28126F761C2F9833006466CC /* SQLiteBookmarksBase.swift */, + 28126F471C2F948E006466CC /* SQLiteBookmarksHelpers.swift */, + 28126F6D1C2F94F9006466CC /* SQLiteBookmarksModel.swift */, + 28126F731C2F96F1006466CC /* SQLiteBookmarksResetting.swift */, + 285F2DC01AF80B4600211843 /* SQLiteBookmarksSyncing.swift */, + D0131B4C1F3CF7D8000CDE86 /* SQLiteFavicons.swift */, + 2FCAE2551ABB531100877008 /* SQLiteHistory.swift */, + 318FB6EA1DB5600D0004E40F /* SQLiteHistoryFactories.swift */, + E6F368281D7F594F008CDD67 /* SQLiteHistoryRecommendations.swift */, + 0BDA56B31B26B203008C9B96 /* SQLiteLogins.swift */, + E677F0441D9423FB00ECF1FB /* SQLiteMetadata.swift */, + 285D3B8F1B4386520035FD22 /* SQLiteQueue.swift */, + 2FCAE2581ABB531100877008 /* SQLiteRemoteClientsAndTabs.swift */, + ); + path = SQL; + sourceTree = ""; + }; + 2FCAE25A1ABB531100877008 /* ThirdParty */ = { + isa = PBXGroup; + children = ( + 2FCAE25B1ABB531100877008 /* SwiftData.swift */, + ); + path = ThirdParty; + sourceTree = ""; + }; + 3905274D1C874D35007E0BB7 /* Today */ = { + isa = PBXGroup; + children = ( + 391AEFD11C8F11ED00691F84 /* Images.xcassets */, + 390527531C874D35007E0BB7 /* Info.plist */, + 3905274E1C874D35007E0BB7 /* TodayViewController.swift */, + ); + path = Today; + sourceTree = ""; + }; + 392ED7D51D0AEEEE009D9B62 /* Accessors */ = { + isa = PBXGroup; + children = ( + 392ED7E31D0AEF56009D9B62 /* NewTabAccessors.swift */, + 392ED7E51D0AEFEF009D9B62 /* HomePageAccessors.swift */, + ); + name = Accessors; + sourceTree = ""; + }; + 397848DC1ED86605004C0C0B /* NotificationService */ = { + isa = PBXGroup; + children = ( + 397848DF1ED86605004C0C0B /* Info.plist */, + 397848DD1ED86605004C0C0B /* NotificationService.swift */, + 396E38DB1EE0818800CC180F /* ExtensionProfile.swift */, + ); + path = NotificationService; + sourceTree = ""; + }; + 39A359BD1BFCCE7B006B9E87 /* Helpers */ = { + isa = PBXGroup; + children = ( + E65075531E37F6FC006961AC /* DynamicFontHelper.swift */, + E65075601E37F77D006961AC /* MenuHelper.swift */, + 39F99FED1E3A71F800F353B4 /* FxALoginHelper.swift */, + 39455F761FC83F430088A22C /* TabEventHandler.swift */, + 39F819C51FD70F5D009E31E4 /* TabEventHandlers.swift */, + 39A359E31BFCCE94006B9E87 /* UserActivityHandler.swift */, + ); + name = Helpers; + sourceTree = ""; + }; + 39B0646D1E7ADA4B000BE173 /* PushTests */ = { + isa = PBXGroup; + children = ( + 39C22C2C1E897B9A000C0E56 /* LivePushClientTests.swift */, + 39B0647C1E7ADAC2000BE173 /* PushCryptoTests.swift */, + ); + name = PushTests; + sourceTree = ""; + }; + 39F99FC71E3A6DB700F353B4 /* Push */ = { + isa = PBXGroup; + children = ( + 39F99FD91E3A6DE300F353B4 /* PushClient.swift */, + 39F99FDA1E3A6DE300F353B4 /* PushConfiguration.swift */, + 395C8F201E796AD600A68E8C /* PushCrypto.swift */, + 39F99FDB1E3A6DE300F353B4 /* PushRegistration.swift */, + ); + path = Push; + sourceTree = ""; + }; + 3B4262D31E538EB100CA681C /* Metadata Parsing */ = { + isa = PBXGroup; + children = ( + 3B4262E21E538EC400CA681C /* MetadataHelper.js */, + 3B4262E31E538EC400CA681C /* MetadataParserHelper.swift */, + 3B4262E41E538EC400CA681C /* page-metadata-parser.js */, + ); + name = "Metadata Parsing"; + sourceTree = ""; + }; + 3B43E3D11D95C48D00BBA9DB /* StoragePerfTests */ = { + isa = PBXGroup; + children = ( + 3B43E3D21D95C48D00BBA9DB /* StoragePerfTests.swift */, + 3B43E3D41D95C48D00BBA9DB /* Info.plist */, + ); + path = StoragePerfTests; + sourceTree = ""; + }; + 3BF4B8DA1D38493300493393 /* Utils */ = { + isa = PBXGroup; + children = ( + 39EB46971E26DDB4006346E8 /* ScreenGraph.swift */, + 39EB46981E26DDB4006346E8 /* FxScreenGraph.swift */, + 3BF4B8E81D38497A00493393 /* BaseTestCase.swift */, + ); + name = Utils; + sourceTree = ""; + }; + 3BFE4B081D342FB900DDF53F /* XCUITests */ = { + isa = PBXGroup; + children = ( + 0BC9C9C31F26F54D000E8AB5 /* SiteLoadTest.swift */, + 3D71C89D1F5703A1008D8646 /* CopiedLinksTests.swift */, + 3DEFED071F55EBE300F8620C /* TrackingProtectionTests.swift */, + 3D9CA9A71EF84D04002434DD /* NoImageTests.swift */, + 3D9CAA1B1EFCD655002434DD /* ClipBoardTests.swift */, + 3D9CA9831EF456A8002434DD /* NightModeTests.swift */, + 0B305E1A1E3A98A900BE0767 /* BookmarkingTests.swift */, + 2C3406C71E719F00000FD889 /* SettingsTest.swift */, + 0B3D670D1E09B90B00C1EFC7 /* AuthenticationTest.swift */, + 0B729D361E047D6A008E6859 /* HomePageSettingsTest.swift */, + 3BF4B8DA1D38493300493393 /* Utils */, + 3BFE4B4F1D34673D00DDF53F /* ThirdPartySearchTest.swift */, + 3B546EBF1D95ECAE00BDBE36 /* ActivityStreamTest.swift */, + 3BFE4B0B1D342FB900DDF53F /* Info.plist */, + 55A747161DC46FC400CE1B57 /* HomePageUITest.swift */, + 2C2A5EF31E68469500F02659 /* PrivateBrowsingTest.swift */, + 2CA16FDD1E5F089100332277 /* SearchTest.swift */, + 2C97EC701E72C80E0092EC18 /* TopTabsTest.swift */, + 2CF449A41E7BFE2C00FD7595 /* NavigationTest.swift */, + 2C8C07761E7800EA00DC1237 /* FindInPageTest.swift */, + 2C31A7A81E8BFB2200DAC646 /* ReaderViewUITest.swift */, + 2C31A8461E8D447F00DAC646 /* HomePageSettingsUITest.swift */, + 2CB56E3E1E926BFB00AF7586 /* ToolbarTest.swift */, + 2CC1B3EF1E9B861400814EEC /* DomainAutocompleteTest.swift */, + 2CEA6F781E93E3A600D4100E /* SearchSettingsUITest.swift */, + 0B9D40781E8D5AC80059E664 /* XCUITests-Bridging-Header.h */, + D81127D71F84023B0050841D /* PhotonActionSheetTest.swift */, + 2C2A91281FA2410D002E36BD /* HistoryTests.swift */, + ); + path = XCUITests; + sourceTree = ""; + }; + 7B0B1B9C1C1B69F500DF4AB5 /* Extensions */ = { + isa = PBXGroup; + children = ( + E4BCAAD81B05380B00855D82 /* ClientPickerViewController.swift */, + E4BCAAB81B0537E300855D82 /* InstructionsViewController.swift */, + ); + name = Extensions; + sourceTree = ""; + }; + 7B3632D21C2983F000D12AF9 /* L10nSnapshotTests */ = { + isa = PBXGroup; + children = ( + 7BEB644E1C7345600092C02E /* Info.plist */, + E40AFC641DD0F25500DA5651 /* L10nBaseSnapshotTests.swift */, + E40AFC6B1DD128DA00DA5651 /* L10nIntroSnapshotTests.swift */, + E40AFC531DD0E93300DA5651 /* L10nPermissionStringsSnapshotTests.swift */, + 7B3632D31C2983F000D12AF9 /* L10nSnapshotTests.swift */, + ); + path = L10nSnapshotTests; + sourceTree = ""; + }; + 7B3632E71C29879300D12AF9 /* Snapshot */ = { + isa = PBXGroup; + children = ( + 7B60B0071BDE3AE10090C984 /* SnapshotHelper.swift */, + 7B95CD181C3AB2EE00638E31 /* MarketingUITests */, + 7B3632D21C2983F000D12AF9 /* L10nSnapshotTests */, + ); + name = Snapshot; + sourceTree = ""; + }; + 7B604FC11C496005006EEEC3 /* Frameworks */ = { + isa = PBXGroup; + children = ( + EBA31D7E1F799BE20055463D /* Telemetry.framework */, + E4F2DABF1F620C0200A556CD /* Leanplum.framework */, + E46175F21EBB73A10021AE8A /* Sentry.framework */, + 3B4988CD1E42B01800A12FDA /* SwiftyJSON.framework */, + E6231C041B90A472005ABB0D /* libxml2.2.tbd */, + E6231C021B90A466005ABB0D /* libstdc++.6.0.9.tbd */, + E6231C001B90A44F005ABB0D /* libz.tbd */, + 0B21E8051E26CCB7000C8779 /* EarlGrey.framework */, + D39FA16B1A83E17800EE869C /* CoreGraphics.framework */, + 0B8E0FF31A932BD500161DC3 /* ImageIO.framework */, + D36998881AD70A0A00650C6C /* IOKit.framework */, + 3BA9A0221D2C208C00BD418C /* Fuzi.framework */, + 7B8A47F51D01D3B400C07734 /* PassKit.framework */, + 7BA4A9631C4CFE840091D032 /* Deferred.framework */, + 7BA4A94B1C4CF03B0091D032 /* SwiftKeychainWrapper.framework */, + 7BA4A9491C4CEFC70091D032 /* OnePasswordExtension.framework */, + 7BA4A9451C4CED780091D032 /* GCDWebServers.framework */, + 7B6050001C49653B006EEEC3 /* AdjustSdk.framework */, + 7B604FBA1C495D8A006EEEC3 /* XCGLogger.framework */, + 7B604FA11C495268006EEEC3 /* SnapKit.framework */, + 7B604F9A1C4950F2006EEEC3 /* SDWebImage.framework */, + 7B604F981C494F74006EEEC3 /* KIF.framework */, + 7B604F851C494983006EEEC3 /* Alamofire.framework */, + E4B334891BBF23F9004E2BFF /* AdSupport.framework */, + E4B334871BBF23F3004E2BFF /* iAd.framework */, + 3905274B1C874D35007E0BB7 /* NotificationCenter.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + 7B95CD181C3AB2EE00638E31 /* MarketingUITests */ = { + isa = PBXGroup; + children = ( + 7B95CD191C3AB2EE00638E31 /* MarketingUITests.swift */, + 7BEB645B1C7345990092C02E /* Info.plist */, + ); + path = MarketingUITests; + sourceTree = ""; + }; + 7B9A62BA1C4002B1005C83DC /* Products */ = { + isa = PBXGroup; + children = ( + 7B9A62DB1C4002B1005C83DC /* SQLite.framework */, + 7B10AAE61E3A57F90002DD08 /* SQLiteTests iOS.xctest */, + 7B9A62DF1C4002B1005C83DC /* SQLite.framework */, + 7B10AAE81E3A57F90002DD08 /* SQLiteTests Mac.xctest */, + 7B10AAEA1E3A57F90002DD08 /* SQLite.framework */, + 7B10AAEC1E3A57F90002DD08 /* SQLiteTests tvOS.xctest */, + 7B10AAEE1E3A57F90002DD08 /* SQLite.framework */, + ); + name = Products; + sourceTree = ""; + }; + 7BC7B4571C903A6A0046E9D2 /* Menu */ = { + isa = PBXGroup; + children = ( + 7B42406D1CA04CAC009B5C28 /* Menu.xcassets */, + ); + path = Menu; + sourceTree = ""; + }; + 7BEDD28C1CB7CB12004B51F3 /* Animators */ = { + isa = PBXGroup; + children = ( + 7BEDD29A1CB7CB24004B51F3 /* JumpAndSpinAnimator.swift */, + 7BEDD2B11CB7CC0B004B51F3 /* Animatable.swift */, + ); + path = Animators; + sourceTree = ""; + }; + C8F457A61F1FD75A000CB895 /* BrowserViewController */ = { + isa = PBXGroup; + children = ( + C8F457A71F1FD75A000CB895 /* BrowserViewController+WKNavigationDelegate.swift */, + C8F457A91F1FDD9B000CB895 /* BrowserViewController+KeyCommands.swift */, + ); + path = BrowserViewController; + sourceTree = ""; + }; + D30EBB5B1C75503800105AE9 /* Products */ = { + isa = PBXGroup; + children = ( + D30EBB641C75503800105AE9 /* libKIF.a */, + D30EBB661C75503800105AE9 /* Test Host.app */, + D30EBB681C75503800105AE9 /* KIF Tests - XCTest.xctest */, + D30EBB6A1C75503800105AE9 /* KIF.framework */, + E6CB64CC1DA42C2900887098 /* KIFFrameworkConsumer.app */, + E6CB64CE1DA42C2900887098 /* KIFFrameworkConsumerTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + D34DC84C1A16C40C00D49B7B /* Providers */ = { + isa = PBXGroup; + children = ( + E68F36971EA694000048CF44 /* PanelDataObservers.swift */, + 3B61CD481F2A74EF00D38DE1 /* PocketFeed.swift */, + E60D03171D511398002FE3F6 /* SyncStatusResolver.swift */, + D34DC84D1A16C40C00D49B7B /* Profile.swift */, + 0BD19A661A25309B0084FBA7 /* NSUserDefaultsPrefs.swift */, + ); + path = Providers; + sourceTree = ""; + }; + D38A1BEB1A9FA2CA00F6A386 /* Widgets */ = { + isa = PBXGroup; + children = ( + D3FEC38C1AC4B42F00494F45 /* AutocompleteTextField.swift */, + E6927EBF1C7B6FB800D03F75 /* ErrorToast.swift */, + E660BE051BB0666D009AC090 /* InnerStrokedView.swift */, + 0BF1B7E21AC60DEA00A7B407 /* InsetButton.swift */, + 7BC68D081CC153020043562A /* RoundedToolbar.swift */, + 28FDFF0B1C1F725800840F86 /* SeparatorTableCell.swift */, + D38A1BEC1A9FA2CA00F6A386 /* SiteTableViewController.swift */, + 0BB5B2861AC0A2B90052877D /* SnackBar.swift */, + E660BDD81BB06521009AC090 /* TabsButton.swift */, + E663D5771BB341C4001EF30E /* ToggleButton.swift */, + 7B844E3C1BBDDB9D00E733A2 /* ChevronView.swift */, + E65607601C08B4E200534B02 /* SearchInputView.swift */, + E63ED7D71BFCD9990097D08E /* LoginTableViewCell.swift */, + C4E3984B1D21F2FD004E89BA /* TabTrayButtonExtensions.swift */, + 7B3631E91C244FEE00D12AF9 /* Theme.swift */, + 0BB5B2871AC0A2B90052877D /* Toolbar.swift */, + 59A68B1F857A8638598A63A0 /* TwoLineCell.swift */, + 745DAB3E1CDAB09E00D44181 /* HistoryBackButton.swift */, + 742A56381D80B54A00BDB803 /* PhotonActionSheet.swift */, + D88FDA9E1F4E2B9200FD9709 /* PhotonActionSheetProtocol.swift */, + D88FDAAE1F4E2BA000FD9709 /* PhotonActionSheetAnimator.swift */, + D863C8E31F68BFC20058D95F /* GradientProgressBar.swift */, + ); + path = Widgets; + sourceTree = ""; + }; + D3972BF01C22412B00035B87 /* Share */ = { + isa = PBXGroup; + children = ( + D3972BF11C22412B00035B87 /* ShareExtensionHelper.swift */, + D3972BF21C22412B00035B87 /* TitleActivityItemProvider.swift */, + ); + path = Share; + sourceTree = ""; + }; + D39FA1601A83E0EC00EE869C /* UITests */ = { + isa = PBXGroup; + children = ( + C8611CA11F71AEB900C3DE7D /* NoImageModeTests.swift */, + 0B7C1E941F6097AD006A8869 /* TrackingProtectionTests.swift */, + 0BEF44621E31165700187C32 /* EarlGrey.swift */, + D39FA16F1A83E62600EE869C /* UITests-Bridging-Header.h */, + D343DCFD1C446BDB00D7EEE8 /* findPage.html */, + E6B4C3D71C68F55C001F97E8 /* JSPrompt.html */, + D31EC05E1CC57ED80096F4AB /* localhostLoad.html */, + D34E33021BA793C2006135F0 /* loginForm.html */, + C8EB60C31F1FB12500F9B5B3 /* navigationDelegate.html */, + 0BF8F8D91AEFF1C900E90BC2 /* noTitle.html */, + 0B6FBAB11AC1F830007EC669 /* numberedPage.html */, + 0B5A93411B1EB572004F47A2 /* readablePage.html */, + 4F9757391AFA6F37006ECC24 /* readerContent.html */, + E6EAC5951B29CB3A00E1DE1E /* scrollablePage.html */, + E67422C41CFF2D39009E8373 /* youtube.ico */, + D3E171C11A841EAD00AB44CD /* KIFHelper.js */, + D38F036F1C06387900175932 /* AuthenticationTests.swift */, + 28C8B7841C852535006D8318 /* BookmarksPanelTests.swift */, + E6B4C4021C68F58B001F97E8 /* BrowserTests.swift */, + 7B24DC9B1B67B3590005766B /* ClearPrivateDataTests.swift */, + D313BE971B2F5096009EF241 /* DomainAutocompleteTests.swift */, + D39FA1801A83E84900EE869C /* Global.swift */, + 4F514FD31ACD8F2C0022D7EA /* HistoryTests.swift */, + E6A92ADA1C52A8DA00743291 /* LoginInputTests.swift */, + E633E3791C2204BE001FFF6C /* LoginManagerTests.swift */, + C8EB60DB1F1FB9AD00F9B5B3 /* NavigationDelegateTests.swift */, + 0B5A93211B1EB4C8004F47A2 /* ReadingListTest.swift */, + D3CFD3631CC5605B0064AB4A /* SecurityTests.swift */, + D3C3EB641B6FF44000388E9A /* SessionRestoreTests.swift */, + 744B0FFD1B4F172E00100422 /* ToolbarTests.swift */, + D375A91F1AE71675001B30D5 /* ViewMemoryLeakTests.swift */, + D39FA1611A83E0EC00EE869C /* Supporting Files */, + ); + path = UITests; + sourceTree = ""; + }; + D39FA1611A83E0EC00EE869C /* Supporting Files */ = { + isa = PBXGroup; + children = ( + D39FA1621A83E0EC00EE869C /* Info.plist */, + ); + name = "Supporting Files"; + sourceTree = ""; + }; + D3A994941A368691008AD1AC /* Browser */ = { + isa = PBXGroup; + children = ( + 0B3E7D931B27A7CE00E2E84D /* AboutHomeHandler.swift */, + D38F02D01C05127100175932 /* Authenticator.swift */, + C40046F91CF8E0B200B08303 /* BackForwardListAnimator.swift */, + C400467B1CF4E43E00B08303 /* BackForwardListViewController.swift */, + C4EFEECE1CEBB6F2009762A4 /* BackForwardTableViewCell.swift */, + E653422C1C5944F90039DD9E /* BrowserPrompts.swift */, + E6D8D5E61B569D70009E5A58 /* BrowserTrayAnimators.swift */, + C8F457A61F1FD75A000CB895 /* BrowserViewController */, + D3A994951A3686BD008AD1AC /* BrowserViewController.swift */, + C4F3B2991CFCF93A00966259 /* ButtonToast.swift */, + 31ADB5D91E58CEC300E87909 /* ClipboardBarDisplayHandler.swift */, + D3BA7E0D1B0E934F00153782 /* ContextMenuHelper.swift */, + 3BE7275C1CCFE8B60099189F /* CustomSearchHandler.swift */, + 0BA1E02D1B046F1E007675AF /* ErrorPageHelper.swift */, + 0BF42D361A7C0B8E00889E28 /* FaviconManager.swift */, + D3B6923C1B9F9444004B87A4 /* FindInPageBar.swift */, + D3B6923E1B9F9A58004B87A4 /* FindInPageHelper.swift */, + D02816C11ECA5E2A00240CAA /* HistoryStateHelper.swift */, + 39DD030C1CD53E1900BC09B3 /* HomePageHelper.swift */, + D3C3696D1CC6B78800348A61 /* LocalRequestHelper.swift */, + 0BB5B30A1AC0AD1F0052877D /* LoginsHelper.swift */, + 7482205B1DBAB56300EEEA72 /* MailProviders.swift */, + 744ED5601DBFEB8D00A2B5BE /* MailtoLinkHandler.swift */, + A93067E71D0FE18E00C49C6E /* NightModeHelper.swift */, + A9072B7F1D07B34100459960 /* NoImageModeHelper.swift */, + 7BA8D1C61BA037F500C8AE9E /* OpenInHelper.swift */, + D3FA77831A43B2CE0010CD32 /* OpenSearch.swift */, + 74821FC41DB56A2500EEEA72 /* OpenWithSettingsViewController.swift */, + E47536EC1C85412C0022CC69 /* PrintHelper.swift */, + D31CF65B1CC1959A001D0BD0 /* PrivilegedRequest.swift */, + FA6B2AC11D41F02D00429414 /* Punycode.swift */, + FA9294001D6584A200AC8D33 /* QRCode.xcassets */, + FA9293D31D6580E100AC8D33 /* QRCodeViewController.swift */, + E47616C61AB74CA600E7DD25 /* ReaderModeBarView.swift */, + D31F95E81AC226CB005C9F3B /* ScreenshotHelper.swift */, + D308E4E31A5306F500842685 /* SearchEngines.swift */, + D34510871ACF415700EC27F0 /* SearchLoader.swift */, + D31A0FC61A65D6D000DC8C7E /* SearchSuggestClient.swift */, + 59A68CCB63E2A565CB03F832 /* SearchViewController.swift */, + 74C027441B2A348C001B1E88 /* SessionData.swift */, + F35B8D2E1D638408008E3D61 /* SessionRestoreHandler.swift */, + F35B8D2C1D6383E9008E3D61 /* SessionRestoreHelper.swift */, + 3BF56D261CDBBE1F00AC4D75 /* SimpleToast.swift */, + E68AEDAF1B18F81A00133D99 /* SwipeAnimator.swift */, + D3A994961A3686BD008AD1AC /* Tab.swift */, + E4CD9F2C1A6DC91200318571 /* TabLocationView.swift */, + D3968F241A38FE8500CEFD3B /* TabManager.swift */, + 7BA0601A1C0F4DE200DFADB6 /* TabPeekViewController.swift */, + DD31E0FA1B382B520077078A /* TabPrintPageRenderer.swift */, + E698FFD91B4AADF40001F623 /* TabScrollController.swift */, + D314E7F51A37B98700426A76 /* TabToolbar.swift */, + D301AAED1A3A55B70078DD1D /* TabTrayController.swift */, + 3BCE6D3B1CEB9E4D0080928C /* ThirdPartySearchAlerts.swift */, + C4E3985F1D22C409004E89BA /* TopTabsLayout.swift */, + C45F44681D087DB600CB7EF0 /* TopTabsViewController.swift */, + C4E3983C1D21F1E7004E89BA /* TopTabsViews.swift */, + D3C744CC1A687D6C004CE85D /* URIFixup.swift */, + 0BF0DB931A8545800039F300 /* URLBarView.swift */, + ); + path = Browser; + sourceTree = ""; + }; + E40A18F61EDC73D5006B7F28 /* Entitlements */ = { + isa = PBXGroup; + children = ( + E40A18F71EDC73D5006B7F28 /* Fennec.entitlements */, + E40A18F81EDC73D5006B7F28 /* FennecEnterprise.entitlements */, + E40A18F91EDC73D5006B7F28 /* Firefox.entitlements */, + E40A18FA1EDC73D5006B7F28 /* FirefoxBeta.entitlements */, + ); + path = Entitlements; + sourceTree = ""; + }; + E49943F31AE6879C00BF9DE4 /* Intro */ = { + isa = PBXGroup; + children = ( + E49943F41AE6879C00BF9DE4 /* IntroViewController.swift */, + E49943F61AE69EDD00BF9DE4 /* Intro.xcassets */, + ); + path = Intro; + sourceTree = ""; + }; + E4BA8AA01B4B15EF00BC2E95 /* ViewLater */ = { + isa = PBXGroup; + children = ( + E4BA8AA11B4B15EF00BC2E95 /* ActionRequestHandler.swift */, + E4BA8AA71B4B15EF00BC2E95 /* Info.plist */, + 7B2142F31E5E03F800CDD3FC /* InfoPlist.strings */, + E4BA8AA61B4B15EF00BC2E95 /* ViewLater.xcassets */, + ); + path = ViewLater; + sourceTree = ""; + }; + E4D567191ADECE2700F1EFE7 /* ReadingList */ = { + isa = PBXGroup; + children = ( + E4D567551ADECE8100F1EFE7 /* ReadingListAuthenticator.swift */, + E4D567561ADECE8100F1EFE7 /* ReadingListBasicAuthAuthenticator.swift */, + E4D567571ADECE8100F1EFE7 /* ReadingListBatchRecordResponse.swift */, + E4D567581ADECE8100F1EFE7 /* ReadingListChangeAccumulator.swift */, + E4D567591ADECE8100F1EFE7 /* ReadingListClient.swift */, + E4D5675A1ADECE8100F1EFE7 /* ReadingListClientMetadata.swift */, + E4D5675B1ADECE8100F1EFE7 /* ReadingListClientRecord.swift */, + E41F0D9A1ADFFC5600FC7387 /* ReadingListError.swift */, + E4D5675D1ADECE8100F1EFE7 /* ReadingListFetchSpec.swift */, + E4D5675E1ADECE8100F1EFE7 /* ReadingListOAuthAuthenticator.swift */, + E4D5675F1ADECE8100F1EFE7 /* ReadingListRecord.swift */, + E4D567601ADECE8100F1EFE7 /* ReadingListRecordResponse.swift */, + E4D567611ADECE8100F1EFE7 /* ReadingListRecordsResponse.swift */, + E4D567621ADECE8100F1EFE7 /* ReadingListResponse.swift */, + E4D567631ADECE8100F1EFE7 /* ReadingListServerMetadata.swift */, + E4D567641ADECE8100F1EFE7 /* ReadingListServerRecord.swift */, + E41F0D971ADFFB5400FC7387 /* ReadingListService.swift */, + E4D567651ADECE8100F1EFE7 /* ReadingListSQLStorage.swift */, + E4D567661ADECE8100F1EFE7 /* ReadingListStorage.swift */, + E4D567671ADECE8100F1EFE7 /* ReadingListSynchronizer.swift */, + E4D567681ADECE8100F1EFE7 /* ReadingListSyncMetadata.swift */, + E4D567691ADECE8100F1EFE7 /* ReadingListUtils.swift */, + E4D5671A1ADECE2700F1EFE7 /* Supporting Files */, + ); + path = ReadingList; + sourceTree = ""; + }; + E4D5671A1ADECE2700F1EFE7 /* Supporting Files */ = { + isa = PBXGroup; + children = ( + E4D5671B1ADECE2700F1EFE7 /* Info.plist */, + ); + name = "Supporting Files"; + sourceTree = ""; + }; + E4D567281ADECE2800F1EFE7 /* ReadingListTests */ = { + isa = PBXGroup; + children = ( + E4D567951ADECEB800F1EFE7 /* ReadingListClientRecordTestCase.swift */, + E4D5679B1ADECEB800F1EFE7 /* ReadingListStorageTestCase.swift */, + E4D567291ADECE2800F1EFE7 /* Supporting Files */, + ); + path = ReadingListTests; + sourceTree = ""; + }; + E4D567291ADECE2800F1EFE7 /* Supporting Files */ = { + isa = PBXGroup; + children = ( + E4D5672A1ADECE2800F1EFE7 /* Info.plist */, + ); + name = "Supporting Files"; + sourceTree = ""; + }; + E4E0BB141AFBC9E4008D6260 /* Shared */ = { + isa = PBXGroup; + children = ( + D35210E01CB2F16600FC5DCB /* Strings.swift */, + E650756F1E37F7AB006961AC /* Extensions */, + E4E0BB161AFBC9E4008D6260 /* Supporting Files */, + E65075621E37F7AB006961AC /* Accessibility.swift */, + E65075631E37F7AB006961AC /* AppConstants.swift */, + E65075641E37F7AB006961AC /* AppInfo.swift */, + E65075651E37F7AB006961AC /* AssertionUtils.swift */, + E65075661E37F7AB006961AC /* AsyncReducer.swift */, + E65075671E37F7AB006961AC /* AuthenticationKeychainInfo.swift */, + E65075681E37F7AB006961AC /* Bytes.swift */, + E65075691E37F7AB006961AC /* Cancellable.swift */, + E650756A1E37F7AB006961AC /* CrashSimulator.h */, + E650756B1E37F7AB006961AC /* CrashSimulator.m */, + E650756C1E37F7AB006961AC /* DeferredUtils.swift */, + E650756D1E37F7AB006961AC /* DeviceInfo.swift */, + E650756E1E37F7AB006961AC /* effective_tld_names.dat */, + 3964B0991EA8F06F00F2EEF4 /* FeatureSwitch.swift */, + E65075811E37F7AB006961AC /* FSUtils.h */, + E65075821E37F7AB006961AC /* FSUtils.m */, + E65075831E37F7AB006961AC /* Functions.swift */, + E65075841E37F7AB006961AC /* KeyboardHelper.swift */, + E65075851E37F7AB006961AC /* KeychainCache.swift */, + E65075861E37F7AB006961AC /* LaunchArguments.swift */, + E65075871E37F7AB006961AC /* Loader.swift */, + E65075881E37F7AB006961AC /* Logger.swift */, + E65075891E37F7AB006961AC /* NotificationConstants.swift */, + E650758B1E37F7AB006961AC /* Prefs.swift */, + CE7F115E1F3CCEF900ABFC0B /* RemoteDevices.swift */, + E650758C1E37F7AB006961AC /* RollingFileLogger.swift */, + D02818601EF056C800240CAA /* SentryIntegration.swift */, + E650758D1E37F7AB006961AC /* SupportUtils.swift */, + E650758E1E37F7AB006961AC /* SystemUtils.swift */, + E650758F1E37F7AB006961AC /* TimeConstants.swift */, + E65075901E37F7AB006961AC /* UserAgent.swift */, + E65075911E37F7AB006961AC /* WeakList.swift */, + ); + path = Shared; + sourceTree = ""; + }; + E4E0BB161AFBC9E4008D6260 /* Supporting Files */ = { + isa = PBXGroup; + children = ( + E4E0BB151AFBC9E4008D6260 /* Shared-Bridging-Header.h */, + E4E0BB171AFBC9E4008D6260 /* Info.plist */, + ); + path = "Supporting Files"; + sourceTree = ""; + }; + E60961841B62B7E100DD640F /* Configuration */ = { + isa = PBXGroup; + children = ( + E60961861B62B8A700DD640F /* Fennec.xcconfig */, + E6DCC1ED1DCBB6AA00CEC4B7 /* Fennec.enterprise.xcconfig */, + E6FCC43C1C40565200DF6113 /* FirefoxBeta.xcconfig */, + E60961891B62B8C800DD640F /* Firefox.xcconfig */, + ); + name = Configuration; + sourceTree = ""; + }; + E63ED8DF1BFD254E0097D08E /* Login Management */ = { + isa = PBXGroup; + children = ( + E63ED8E01BFD25580097D08E /* LoginListViewController.swift */, + E633E2D91C21EAF8001FFF6C /* LoginDetailViewController.swift */, + ); + path = "Login Management"; + sourceTree = ""; + }; + E65075551E37F714006961AC /* Utils */ = { + isa = PBXGroup; + children = ( + E650755A1E37F747006961AC /* Swizzling.h */, + E650755B1E37F747006961AC /* Swizzling.m */, + E65075561E37F714006961AC /* FaviconFetcher.swift */, + E650755D1E37F756006961AC /* Try.h */, + E650755E1E37F756006961AC /* Try.m */, + ); + path = Utils; + sourceTree = ""; + }; + E650756F1E37F7AB006961AC /* Extensions */ = { + isa = PBXGroup; + children = ( + E65075711E37F7AB006961AC /* ArrayExtensions.swift */, + E683F0C11E93D4E90035D990 /* DictionaryExtensions.swift */, + E65075721E37F7AB006961AC /* HashExtensions.swift */, + E65075731E37F7AB006961AC /* HexExtensions.swift */, + E65075741E37F7AB006961AC /* KeychainWrapperExtensions.swift */, + E65075751E37F7AB006961AC /* NSCharacterSetExtensions.swift */, + E65075761E37F7AB006961AC /* NSFileManagerExtensions.swift */, + E65075771E37F7AB006961AC /* NSMutableAttributedStringExtensions.swift */, + E65075781E37F7AB006961AC /* NSScannerExtensions.swift */, + E65075791E37F7AB006961AC /* NSStringExtensions.swift */, + E650757A1E37F7AB006961AC /* NSURLExtensions.swift */, + E650757B1E37F7AB006961AC /* NSURLProtectionSpaceExtensions.swift */, + E650757C1E37F7AB006961AC /* SetExtensions.swift */, + E650757D1E37F7AB006961AC /* StringExtensions.swift */, + E650757E1E37F7AB006961AC /* UIColorExtensions.swift */, + E650757F1E37F7AB006961AC /* UIImageExtensions.swift */, + 7B10AA9E1E3A15020002DD08 /* DataExtensions.swift */, + 7B10AABA1E3A1F650002DD08 /* URLRequestExtensions.swift */, + 7B9BF91B1E43472E00CB24F4 /* JSONExtensions.swift */, + 7B3D9E641E4CBFDB007A50DA /* NSCoderExtensions.swift */, + E693F0D81E9D64BD0086DC17 /* OptionalExtensions.swift */, + ); + path = Extensions; + sourceTree = ""; + }; + E652F6F91BF66A79007FFDD6 /* Delegates */ = { + isa = PBXGroup; + children = ( + F84B21E51A0910F600AAB793 /* AppDelegate.swift */, + D3BE7B451B054F8600641031 /* TestAppDelegate.swift */, + ); + name = Delegates; + sourceTree = ""; + }; + E659A6091EB7C1D300D7A6AD /* BuddyBuild */ = { + isa = PBXGroup; + children = ( + E659A60A1EB7C1D300D7A6AD /* BuddyBuildSDK.framework */, + ); + name = BuddyBuild; + path = ThirdParty/BuddyBuild; + sourceTree = ""; + }; + E66C5B451BDA81050051AA93 /* Apple */ = { + isa = PBXGroup; + children = ( + E66C5B461BDA81050051AA93 /* UIImage+ImageEffects.h */, + E66C5B471BDA81050051AA93 /* UIImage+ImageEffects.m */, + ); + name = Apple; + path = ThirdParty/Apple; + sourceTree = SOURCE_ROOT; + }; + E689C6FF1E0C716F008BAADB /* Entitlements */ = { + isa = PBXGroup; + children = ( + E6F738761EB7A97500B50143 /* FirefoxApplication.entitlements */, + E6F738751EB7A97100B50143 /* FirefoxBetaApplication.entitlements */, + E6F738741EB7A8D300B50143 /* FennecApplication.entitlements */, + E62AC15F1E956AFC00843532 /* FennecEnterpriseApplication.entitlements */, + ); + path = Entitlements; + sourceTree = ""; + }; + E68E7ADF1CAC209000FDCA76 /* PasscodeConfiguration */ = { + isa = PBXGroup; + children = ( + E640E8691C73A47C00C5F072 /* PasscodeViews.swift */, + E6108FF81C84E91C005D25E8 /* BasePasscodeViewController.swift */, + E68E7ACA1CAC1D4500FDCA76 /* PagingPasscodeViewController.swift */, + E640E85D1C73A45A00C5F072 /* PasscodeEntryViewController.swift */, + E69E06B91C76173D00D0F926 /* RequirePasscodeIntervalViewController.swift */, + E68E7AD91CAC207400FDCA76 /* ChangePasscodeViewController.swift */, + E68E7ADB1CAC208200FDCA76 /* SetupPasscodeViewController.swift */, + E68E7ADD1CAC208A00FDCA76 /* RemovePasscodeViewController.swift */, + ); + name = PasscodeConfiguration; + sourceTree = ""; + }; + E692E3271C46E62D009D1240 /* AuthenticationManager */ = { + isa = PBXGroup; + children = ( + E68E7ADF1CAC209000FDCA76 /* PasscodeConfiguration */, + E69E06C81C76198000D0F926 /* AuthenticationManagerConstants.swift */, + E65D89171C8647420006EA35 /* AppAuthenticator.swift */, + E692E3281C46E62D009D1240 /* AuthenticationSettingsViewController.swift */, + E6CF28E61CB43B7900151AB3 /* SensitiveViewController.swift */, + ); + path = AuthenticationManager; + sourceTree = ""; + }; + E699220D1B94E3EF007C480D /* About */ = { + isa = PBXGroup; + children = ( + E69922121B94E3EF007C480D /* Licenses.html */, + ); + path = About; + sourceTree = ""; + }; + E69DB0761E97DEA9008A67E6 /* SyncTelemetry */ = { + isa = PBXGroup; + children = ( + EBA31D7A1F79990C0055463D /* SyncTelemetryEvents.swift */, + EBA31D781F7999030055463D /* SyncPingCentre.swift */, + E69DB0B81E97E2B5008A67E6 /* Supporting Files */, + E69DB0B51E97E2AC008A67E6 /* SyncTelemetry.swift */, + ); + path = SyncTelemetry; + sourceTree = ""; + }; + E69DB0831E97DEAA008A67E6 /* SyncTelemetryTests */ = { + isa = PBXGroup; + children = ( + E69DB0861E97DEAA008A67E6 /* Info.plist */, + E610B0181EDC68FC00DC9372 /* EventTests.swift */, + ); + path = SyncTelemetryTests; + sourceTree = ""; + }; + E69DB0B31E97E0E4008A67E6 /* Telemetry */ = { + isa = PBXGroup; + children = ( + EBA31D7C1F79996E0055463D /* SyncTelemetryUtils.swift */, + E69DB0C01E97E320008A67E6 /* BookmarkTelemetryPing.swift */, + ); + name = Telemetry; + sourceTree = ""; + }; + E69DB0B81E97E2B5008A67E6 /* Supporting Files */ = { + isa = PBXGroup; + children = ( + E69DB0771E97DEA9008A67E6 /* SyncTelemetry.h */, + E69DB0781E97DEA9008A67E6 /* Info.plist */, + ); + name = "Supporting Files"; + sourceTree = ""; + }; + E69DB0B91E97E301008A67E6 /* Telemetry */ = { + isa = PBXGroup; + children = ( + E69DB0BA1E97E301008A67E6 /* ActivityStreamTopics.swift */, + EBF47E6F1F7979DF00899189 /* UnifiedTelemetry.swift */, + ); + path = Telemetry; + sourceTree = ""; + }; + E6F6D6921E9E6AC20012072B /* TestUtils */ = { + isa = PBXGroup; + children = ( + E6F6D6A11E9E6AF90012072B /* DeferredTestUtils.swift */, + ); + name = TestUtils; + sourceTree = ""; + }; + E6F9650D1B2F1CF20034B023 /* SharedTests */ = { + isa = PBXGroup; + children = ( + E6F9650E1B2F1CF20034B023 /* Supporting Files */, + 3B4AA24A1D8B8C4C00A2E008 /* ArrayExtensionTests.swift */, + 39E65D261CA5B92000C63CE3 /* AsyncReducerTests.swift */, + 28786E541AB0F5FA009EA9EF /* DeferredTests.swift */, + 3964B09B1EA8F32C00F2EEF4 /* FeatureSwitchTests.swift */, + E4E25CCA1CA99E7400D0F088 /* HexExtensionsTests.swift */, + E6F965411B2F25110034B023 /* NSMutableAttributedStringExtensionsTests.swift */, + E6F9653B1B2F1D5D0034B023 /* NSURLExtensionsTests.swift */, + 2FEBABAE1AB3659000DB5728 /* ResultTests.swift */, + E61453BD1B750A1700C3F9D7 /* RollingFileLoggerTests.swift */, + E4E7EB6C1C4AED5E0094275D /* SupportUtilsTests.swift */, + 28A6CE891AC082E200C1A2D4 /* UtilsTests.swift */, + 3BB54B301E68EB2B0021DAC4 /* AuthenticationKeychainInfoTests.swift */, + ); + path = SharedTests; + sourceTree = ""; + }; + E6F9650E1B2F1CF20034B023 /* Supporting Files */ = { + isa = PBXGroup; + children = ( + E6F9650F1B2F1CF20034B023 /* Info.plist */, + ); + name = "Supporting Files"; + sourceTree = ""; + }; + EB2A63241F3B49A7004EF8B0 /* ContentBlocker */ = { + isa = PBXGroup; + children = ( + EB2A63251F3B49A7004EF8B0 /* ContentBlockerHelper.swift */, + C88601B71F4228AD00BBDE4F /* ContentBlockerSettingViewController.swift */, + EB2A63381F3B5135004EF8B0 /* disconnect-advertising.json */, + EB2A63361F3B5134004EF8B0 /* disconnect-analytics.json */, + EB2A63371F3B5135004EF8B0 /* disconnect-content.json */, + EB2A63391F3B5135004EF8B0 /* disconnect-social.json */, + ); + path = ContentBlocker; + sourceTree = ""; + }; + F84B21B51A090F8100AAB793 = { + isa = PBXGroup; + children = ( + E6C191D41E38F7B7000A213B /* Cartfile */, + E6C191D51E38F7B7000A213B /* Cartfile.resolved */, + 2FA435FC1ABB83B4008031D1 /* Account */, + 2FA4360B1ABB83B4008031D1 /* AccountTests */, + F84B21C01A090F8100AAB793 /* Client */, + F84B21D61A090F8100AAB793 /* ClientTests */, + F8708D1E1A0970990051AB07 /* Extensions */, + 7B604FC11C496005006EEEC3 /* Frameworks */, + F84B21BF1A090F8100AAB793 /* Products */, + D34DC84C1A16C40C00D49B7B /* Providers */, + 39F99FC71E3A6DB700F353B4 /* Push */, + 39B0646D1E7ADA4B000BE173 /* PushTests */, + E4D567191ADECE2700F1EFE7 /* ReadingList */, + E4D567281ADECE2800F1EFE7 /* ReadingListTests */, + E4E0BB141AFBC9E4008D6260 /* Shared */, + E6F9650D1B2F1CF20034B023 /* SharedTests */, + 7B3632E71C29879300D12AF9 /* Snapshot */, + 2FCAE21B1ABB51F800877008 /* Storage */, + 3B43E3D11D95C48D00BBA9DB /* StoragePerfTests */, + 2FCAE22A1ABB51F800877008 /* StorageTests */, + 28CE83B91A1D1D3200576538 /* Sync */, + 28C077911A3B05C200834FE5 /* SyncTests */, + E6F6D6921E9E6AC20012072B /* TestUtils */, + E69DB0761E97DEA9008A67E6 /* SyncTelemetry */, + E69DB0831E97DEAA008A67E6 /* SyncTelemetryTests */, + 28CE83EF1A1D246900576538 /* Third-Party Source */, + D39FA1601A83E0EC00EE869C /* UITests */, + 3BFE4B081D342FB900DDF53F /* XCUITests */, + ); + sourceTree = ""; + }; + F84B21BF1A090F8100AAB793 /* Products */ = { + isa = PBXGroup; + children = ( + F84B21BE1A090F8100AAB793 /* Client.app */, + F84B21D31A090F8100AAB793 /* ClientTests.xctest */, + F84B22491A0920C600AAB793 /* ShareTo.appex */, + F84B225C1A09210A00AAB793 /* SendTo.appex */, + D39FA15F1A83E0EC00EE869C /* UITests.xctest */, + 288A2D861AB8B3260023ABC3 /* Shared.framework */, + 2FCAE21A1ABB51F800877008 /* Storage.framework */, + 2FCAE2241ABB51F800877008 /* StorageTests.xctest */, + 2FA435FB1ABB83B4008031D1 /* Account.framework */, + 2FA436051ABB83B4008031D1 /* AccountTests.xctest */, + 2827315E1ABC9BE600AA1954 /* Sync.framework */, + 282731681ABC9BE700AA1954 /* SyncTests.xctest */, + E4D567181ADECE2700F1EFE7 /* ReadingList.framework */, + E4D567221ADECE2700F1EFE7 /* ReadingListTests.xctest */, + E6F9650C1B2F1CF20034B023 /* SharedTests.xctest */, + E4BA8A2B1B4B0A1600BC2E95 /* ViewLater.appex */, + 7BEB644D1C7345600092C02E /* L10nSnapshotTests.xctest */, + 7BEB645A1C7345990092C02E /* MarketingUITests.xctest */, + 3905274A1C874D35007E0BB7 /* Today.appex */, + 3BFE4B071D342FB800DDF53F /* XCUITests.xctest */, + 3B43E3D01D95C48D00BBA9DB /* StoragePerfTests.xctest */, + E69DB0751E97DEA9008A67E6 /* SyncTelemetry.framework */, + E69DB07D1E97DEA9008A67E6 /* SyncTelemetryTests.xctest */, + 397848DB1ED86605004C0C0B /* NotificationService.appex */, + ); + name = Products; + sourceTree = ""; + }; + F84B21C01A090F8100AAB793 /* Client */ = { + isa = PBXGroup; + children = ( + 7B2142FC1E5E055000CDD3FC /* InfoPlist.strings */, + F84B22431A09165600AAB793 /* Info.plist */, + F84B21EB1A0910F600AAB793 /* Assets */, + F84B21E41A0910F600AAB793 /* Application */, + E60961841B62B7E100DD640F /* Configuration */, + E689C6FF1E0C716F008BAADB /* Entitlements */, + 28EADE5C1AFC3A6D007FB2FB /* Extensions */, + F84B21F11A0910F600AAB793 /* Frontend */, + 39A359BD1BFCCE7B006B9E87 /* Helpers */, + E69DB0B91E97E301008A67E6 /* Telemetry */, + E65075551E37F714006961AC /* Utils */, + 74821FFD1DB6D3AC00EEEA72 /* MailSchemes.plist */, + ); + path = Client; + sourceTree = ""; + }; + F84B21D61A090F8100AAB793 /* ClientTests */ = { + isa = PBXGroup; + children = ( + 8DCD3BCC1ED5B7FA00446D38 /* FxADeepLinkingTests.swift */, + 3B6F40171DC7849C00656CC6 /* ActivityStreamTests.swift */, + E696FE501C47F86E00EC007C /* AuthenticatorTests.swift */, + F84B21D91A090F8100AAB793 /* ClientTests.swift */, + 3B39EDB91E16E18900EF029F /* CustomSearchEnginesTest.swift */, + D3D488581ABB54CD00A93597 /* FileAccessorTests.swift */, + 3943A81C1E9807C700D4F6DC /* FxAPushMessageTest.swift */, + 281B2BE91ADF4D90002917DC /* MockProfile.swift */, + E683F0A51E92E0820035D990 /* MockableHistory.swift */, + E61D11671EAF8F43008A305B /* PanelDataObserversTests.swift */, + E6C70E811E28314700F8DB57 /* PingCentreTests.swift */, + 3B61CD581F2A750800D38DE1 /* PocketFeedTests.swift */, + 2FDB10921A9FBEC5006CF312 /* PrefsTests.swift */, + 0BA896491A250E6500C1010C /* ProfileTest.swift */, + 03CCC9171AF05E7300DBF30D /* RelativeDatesTests.swift */, + 28D52E081BCDF44100187A1D /* ResetTests.swift */, + 2F697F7D1A9FD22D009E03AE /* SearchEnginesTests.swift */, + D3FA777A1A43B2990010CD32 /* SearchTests.swift */, + 2F13E79A1AC0C02700D75081 /* StringExtensionsTests.swift */, + E60D03291D5118DB002FE3F6 /* SyncStatusResolverTests.swift */, + 7BBFEE731BB405D900A305AA /* TabManagerTests.swift */, + 0BA8964A1A250E6500C1010C /* TestBookmarks.swift */, + 0BF42D4E1A7CD09600889E28 /* TestFavicons.swift */, + 2F44FA1A1A9D426A00FD20CC /* TestHashExtensions.swift */, + 4A59BF410BBD9B3BE71F4C7C /* TestHistory.swift */, + A83E5B1C1C1DA8D80026D912 /* UIPasteboardExtensionsTests.swift */, + 3BFCBF1F1E04B1C50070C042 /* UIImageViewExtensionsTests.swift */, + E4CD9F1C1A6D9C2800318571 /* WebServerTests.swift */, + D3BA41671BD82F2200DA5457 /* XCTestCaseExtensions.swift */, + F84B21D71A090F8100AAB793 /* Supporting Files */, + 554867221DC3935A00183DAA /* HomePageTests.swift */, + 39236E711FCC600200A38F1B /* TabEventHandlerTests.swift */, + ); + path = ClientTests; + sourceTree = ""; + }; + F84B21D71A090F8100AAB793 /* Supporting Files */ = { + isa = PBXGroup; + children = ( + A83E5B181C1DA8BF0026D912 /* image.gif */, + F84B21D81A090F8100AAB793 /* Info.plist */, + A83E5B191C1DA8BF0026D912 /* image.png */, + 3B61CD621F2A769D00D38DE1 /* pocketglobalfeed.json */, + ); + name = "Supporting Files"; + sourceTree = ""; + }; + F84B21E41A0910F600AAB793 /* Application */ = { + isa = PBXGroup; + children = ( + 19DE1F661EC13B6400428B8C /* LeanplumIntegration.swift */, + E652F6F91BF66A79007FFDD6 /* Delegates */, + 28CE83E81A1D206D00576538 /* Client-Bridging-Header.h */, + E40FAB0B1A7ABB77009CB80D /* WebServer.swift */, + E4D6BEB81A0930EC00F538BD /* LaunchScreen.xib */, + D3BE7B251B054D4400641031 /* main.swift */, + E4B3348B1BC01D8F004E2BFF /* AdjustIntegration.swift */, + E6327A631BF6438E008D12E0 /* DebugSettingsBundleOptions.swift */, + E6639F171BF11E17002D0853 /* Settings.bundle */, + 7BEFC67F1BFF68C30059C952 /* QuickActions.swift */, + ); + path = Application; + sourceTree = ""; + }; + F84B21EB1A0910F600AAB793 /* Assets */ = { + isa = PBXGroup; + children = ( + 3BC659581E5BA505006D560F /* top_sites.json */, + 3BC659481E5BA4AE006D560F /* TopSites */, + D30684F01C84F12A002D8D82 /* SearchPlugins */, + D308EE551CBF0BF5006843F2 /* CertError.css */, + 0BA1E02F1B051A07007675AF /* NetError.css */, + D38A1EDF1CB458EC0080C842 /* CertError.html */, + 0BA1E00D1B03FB0B007675AF /* NetError.html */, + D3BA7E0B1B0E902A00153782 /* ContextMenu.js */, + 0BF42D381A7C0E8900889E28 /* Favicons.js */, + D3B692401B9F9CC8004B87A4 /* FindInPage.js */, + E689C6F71E0C6E72008BAADB /* FxASignIn.js */, + D02816D11ECA60DF00240CAA /* HistoryStateHelper.js */, + 0BDA568A1B26B1BE008C9B96 /* LoginsHelper.js */, + A91528D11D0E637600CD0425 /* NoImageModeHelper.js */, + A93067D81D0FE15E00C49C6E /* NightModeHelper.js */, + 3BE7274D1CCFE8A30099189F /* CustomSearchHelper.js */, + E47536FB1C85415E0022CC69 /* PrintHelper.js */, + 39A35AEC1C0662A3006B9E87 /* SpotlightHelper.js */, + D37524861C6E8B5A00A5F6C2 /* topdomains.txt */, + F84B21EF1A0910F600AAB793 /* Images.xcassets */, + E699220D1B94E3EF007C480D /* About */, + F84B22391A0914A300AAB793 /* Fonts */, + F35B8D2A1D6380EA008E3D61 /* SessionRestore.html */, + ); + path = Assets; + sourceTree = ""; + }; + F84B21F11A0910F600AAB793 /* Frontend */ = { + isa = PBXGroup; + children = ( + EB2A63241F3B49A7004EF8B0 /* ContentBlocker */, + D331DFC91CB6E9EE009B5DA2 /* OldStrings.swift */, + 2816EFFF1B33E05400522243 /* UIConstants.swift */, + 392ED7D51D0AEEEE009D9B62 /* Accessors */, + 7BEDD28C1CB7CB12004B51F3 /* Animators */, + E692E3271C46E62D009D1240 /* AuthenticationManager */, + D3A994941A368691008AD1AC /* Browser */, + 7B0B1B9C1C1B69F500DF4AB5 /* Extensions */, + F84B22211A09122500AAB793 /* Home */, + E49943F31AE6879C00BF9DE4 /* Intro */, + E63ED8DF1BFD254E0097D08E /* Login Management */, + 3B4262D31E538EB100CA681C /* Metadata Parsing */, + 7BC7B4571C903A6A0046E9D2 /* Menu */, + F84B21F51A0910F600AAB793 /* Reader */, + 2F44FC551A9E83E200FD20CC /* Settings */, + D3972BF01C22412B00035B87 /* Share */, + D38A1BEB1A9FA2CA00F6A386 /* Widgets */, + ); + path = Frontend; + sourceTree = ""; + }; + F84B21F51A0910F600AAB793 /* Reader */ = { + isa = PBXGroup; + children = ( + E49BFF581B1F48C3009368CC /* Readability.js */, + E4CD9E901A6897FB00318571 /* ReaderMode.swift */, + E4A960051ABB9C450069AD6F /* ReaderModeUtils.swift */, + E4B423BD1AB9FE6A007E66C8 /* ReaderModeCache.swift */, + E4B423DC1ABA0318007E66C8 /* ReaderModeHandlers.swift */, + E4CD9E991A68980A00318571 /* ReaderMode.js */, + E4CD9F531A71506400318571 /* Reader.html */, + E4CD9F5A1A71506C00318571 /* Reader.css */, + E4CD9F6C1A77DD2800318571 /* ReaderModeStyleViewController.swift */, + E4A961171AC041C40069AD6F /* ReadabilityService.swift */, + E4A961331AC051360069AD6F /* ReadabilityTabHelper.swift */, + E4A961351AC052DE0069AD6F /* ReadabilityTabHelper.js */, + E4A961371AC06FA50069AD6F /* ReaderViewLoading.html */, + E4C358541AF144BA00299F7E /* FSReadingList.m */, + E4C358561AF1467A00299F7E /* FSReadingList.h */, + ); + path = Reader; + sourceTree = ""; + }; + F84B22211A09122500AAB793 /* Home */ = { + isa = PBXGroup; + children = ( + F84B22261A09127C00AAB793 /* Home.xcassets */, + 3BB50E1F1D627539004B33DF /* ActivityStreamPanel.swift */, + 3BB50E101D6274CD004B33DF /* ActivityStreamTopSitesCell.swift */, + 74821F8D1DAD8F1300EEEA72 /* ActivityStreamHighlightCell.swift */, + F84B22221A09122500AAB793 /* HomePanelViewController.swift */, + D30B101D1AA7F9C600C01CA3 /* HomePanels.swift */, + 59A6825233896FC846499289 /* HistoryPanel.swift */, + 59A6839879D615FC1C0D71CE /* BookmarksPanel.swift */, + 59A685F4EAD19EDEC854BCA4 /* ReaderPanel.swift */, + 2FDE87FD1ABB3817005317B1 /* RemoteTabsPanel.swift */, + 745DAB2F1CDAAFAA00D44181 /* RecentlyClosedTabsPanel.swift */, + ); + path = Home; + sourceTree = ""; + }; + F84B22391A0914A300AAB793 /* Fonts */ = { + isa = PBXGroup; + children = ( + E4B7B73A1A793CF20022C5E0 /* CharisSILB.ttf */, + E4B7B73B1A793CF20022C5E0 /* CharisSILBI.ttf */, + E4B7B73C1A793CF20022C5E0 /* CharisSILI.ttf */, + E4B7B73D1A793CF20022C5E0 /* CharisSILR.ttf */, + E4B7B7411A793CF20022C5E0 /* FiraSans-Bold.ttf */, + E4B7B7421A793CF20022C5E0 /* FiraSans-BoldItalic.ttf */, + E4424B3B1AC71FB400F44C38 /* FiraSans-Book.ttf */, + E4B7B7511A793CF20022C5E0 /* FiraSans-Italic.ttf */, + E4B7B7521A793CF20022C5E0 /* FiraSans-Light.ttf */, + E4ECCDAD1AB131770005E717 /* FiraSans-Medium.ttf */, + E4B7B7561A793CF20022C5E0 /* FiraSans-Regular.ttf */, + E4B7B7571A793CF20022C5E0 /* FiraSans-SemiBold.ttf */, + E4B7B75F1A793CF20022C5E0 /* FiraSans-UltraLight.ttf */, + ); + path = Fonts; + sourceTree = ""; + }; + F8708D1E1A0970990051AB07 /* Extensions */ = { + isa = PBXGroup; + children = ( + E40A18F61EDC73D5006B7F28 /* Entitlements */, + 397848DC1ED86605004C0C0B /* NotificationService */, + 3905274D1C874D35007E0BB7 /* Today */, + E4BA8AA01B4B15EF00BC2E95 /* ViewLater */, + F8708D1F1A0970990051AB07 /* SendTo */, + F8708D241A0970990051AB07 /* ShareTo */, + ); + path = Extensions; + sourceTree = ""; + }; + F8708D1F1A0970990051AB07 /* SendTo */ = { + isa = PBXGroup; + children = ( + F8708D201A0970990051AB07 /* ActionViewController.swift */, + E4F21AA51A13C4A300B0FAAA /* Images.xcassets */, + 7B2142F61E5E040200CDD3FC /* InfoPlist.strings */, + F8708D211A0970990051AB07 /* Info.plist */, + F8708D221A0970990051AB07 /* MainInterface.storyboard */, + ); + path = SendTo; + sourceTree = ""; + }; + F8708D241A0970990051AB07 /* ShareTo */ = { + isa = PBXGroup; + children = ( + F8708D251A0970990051AB07 /* Images.xcassets */, + F8708D261A0970990051AB07 /* Info.plist */, + E41A7D4A1A1BE04500245963 /* InitialViewController.swift */, + F8708D291A0970990051AB07 /* ShareViewController.swift */, + ); + path = ShareTo; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXHeadersBuildPhase section */ + 2827315B1ABC9BE600AA1954 /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + 282731631ABC9BE600AA1954 /* Sync-Bridging-Header.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 288A2D831AB8B3260023ABC3 /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + E650759A1E37F7AB006961AC /* CrashSimulator.h in Headers */, + E65075B01E37F7AB006961AC /* FSUtils.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 2FA435F81ABB83B4008031D1 /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + 2F14E13A1ABB890800FF98DB /* Account-Bridging-Header.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 2FCAE2171ABB51F800877008 /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + 2FCAE33E1ABB5F1800877008 /* Storage-Bridging-Header.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + E4D567151ADECE2700F1EFE7 /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + E69DB0721E97DEA9008A67E6 /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + E69DB0871E97DEAA008A67E6 /* SyncTelemetry.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXHeadersBuildPhase section */ + +/* Begin PBXNativeTarget section */ + 2827315D1ABC9BE600AA1954 /* Sync */ = { + isa = PBXNativeTarget; + buildConfigurationList = 282731971ABC9BE800AA1954 /* Build configuration list for PBXNativeTarget "Sync" */; + buildPhases = ( + 282731591ABC9BE600AA1954 /* Sources */, + 2827315A1ABC9BE600AA1954 /* Frameworks */, + 2827315B1ABC9BE600AA1954 /* Headers */, + 2827315C1ABC9BE600AA1954 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 2F77F6B91ABCAF0700484F3A /* PBXTargetDependency */, + 2F77F69D1ABCAEFE00484F3A /* PBXTargetDependency */, + 2F11EE501ABCAE910083902D /* PBXTargetDependency */, + E4EE05B01BA3A0A10021B3A7 /* PBXTargetDependency */, + E69DB0A71E97DEF6008A67E6 /* PBXTargetDependency */, + ); + name = Sync; + productName = Sync; + productReference = 2827315E1ABC9BE600AA1954 /* Sync.framework */; + productType = "com.apple.product-type.framework"; + }; + 282731671ABC9BE700AA1954 /* SyncTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 282731981ABC9BE800AA1954 /* Build configuration list for PBXNativeTarget "SyncTests" */; + buildPhases = ( + 282731641ABC9BE700AA1954 /* Sources */, + 282731651ABC9BE700AA1954 /* Frameworks */, + 282731661ABC9BE700AA1954 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + E60222E61C6E5A770061C436 /* PBXTargetDependency */, + 2F67C5281BB0D33000E7B73A /* PBXTargetDependency */, + 28ECD9B11BA1F07000D829DA /* PBXTargetDependency */, + 2827316B1ABC9BE700AA1954 /* PBXTargetDependency */, + ); + name = SyncTests; + productName = SyncTests; + productReference = 282731681ABC9BE700AA1954 /* SyncTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 288A2D851AB8B3260023ABC3 /* Shared */ = { + isa = PBXNativeTarget; + buildConfigurationList = 288A2D9F1AB8B3260023ABC3 /* Build configuration list for PBXNativeTarget "Shared" */; + buildPhases = ( + 288A2D811AB8B3260023ABC3 /* Sources */, + 288A2D821AB8B3260023ABC3 /* Frameworks */, + 288A2D831AB8B3260023ABC3 /* Headers */, + 288A2D841AB8B3260023ABC3 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Shared; + productName = Shared; + productReference = 288A2D861AB8B3260023ABC3 /* Shared.framework */; + productType = "com.apple.product-type.framework"; + }; + 2FA435FA1ABB83B4008031D1 /* Account */ = { + isa = PBXNativeTarget; + buildConfigurationList = 2FA436141ABB83B4008031D1 /* Build configuration list for PBXNativeTarget "Account" */; + buildPhases = ( + 2FA435F61ABB83B4008031D1 /* Sources */, + 2FA435F71ABB83B4008031D1 /* Frameworks */, + 2FA435F81ABB83B4008031D1 /* Headers */, + 2FA435F91ABB83B4008031D1 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 2FA436441ABB84CB008031D1 /* PBXTargetDependency */, + 2FA4361D1ABB83DD008031D1 /* PBXTargetDependency */, + ); + name = Account; + productName = Account; + productReference = 2FA435FB1ABB83B4008031D1 /* Account.framework */; + productType = "com.apple.product-type.framework"; + }; + 2FA436041ABB83B4008031D1 /* AccountTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 2FA436181ABB83B4008031D1 /* Build configuration list for PBXNativeTarget "AccountTests" */; + buildPhases = ( + 2FA436011ABB83B4008031D1 /* Sources */, + 2FA436021ABB83B4008031D1 /* Frameworks */, + 2FA436031ABB83B4008031D1 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 2F14E1341ABB88CF00FF98DB /* PBXTargetDependency */, + 2F14E1181ABB88CB00FF98DB /* PBXTargetDependency */, + ); + name = AccountTests; + productName = AccountTests; + productReference = 2FA436051ABB83B4008031D1 /* AccountTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 2FCAE2191ABB51F800877008 /* Storage */ = { + isa = PBXNativeTarget; + buildConfigurationList = 2FCAE2331ABB51F900877008 /* Build configuration list for PBXNativeTarget "Storage" */; + buildPhases = ( + 2FCAE2151ABB51F800877008 /* Sources */, + 2FCAE2161ABB51F800877008 /* Frameworks */, + 2FCAE2171ABB51F800877008 /* Headers */, + 2FCAE2181ABB51F800877008 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 0B742CCE1B32493800EE9264 /* PBXTargetDependency */, + 2FCAE23C1ABB520700877008 /* PBXTargetDependency */, + ); + name = Storage; + productName = Storage; + productReference = 2FCAE21A1ABB51F800877008 /* Storage.framework */; + productType = "com.apple.product-type.framework"; + }; + 2FCAE2231ABB51F800877008 /* StorageTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 2FCAE2371ABB51F900877008 /* Build configuration list for PBXNativeTarget "StorageTests" */; + buildPhases = ( + 2FCAE2201ABB51F800877008 /* Sources */, + 2FCAE2211ABB51F800877008 /* Frameworks */, + 2FCAE2221ABB51F800877008 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 2FCAE2271ABB51F800877008 /* PBXTargetDependency */, + 2FCAE2291ABB51F800877008 /* PBXTargetDependency */, + ); + name = StorageTests; + productName = StorageTests; + productReference = 2FCAE2241ABB51F800877008 /* StorageTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 390527491C874D35007E0BB7 /* Today */ = { + isa = PBXNativeTarget; + buildConfigurationList = 39409A521C90E68300DAE683 /* Build configuration list for PBXNativeTarget "Today" */; + buildPhases = ( + 390527461C874D35007E0BB7 /* Sources */, + 390527471C874D35007E0BB7 /* Frameworks */, + 390527481C874D35007E0BB7 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + D09A0CED1FAA2C4C009A0273 /* PBXTargetDependency */, + ); + name = Today; + productName = Today; + productReference = 3905274A1C874D35007E0BB7 /* Today.appex */; + productType = "com.apple.product-type.app-extension"; + }; + 397848DA1ED86605004C0C0B /* NotificationService */ = { + isa = PBXNativeTarget; + buildConfigurationList = 397848F51ED86605004C0C0B /* Build configuration list for PBXNativeTarget "NotificationService" */; + buildPhases = ( + 397848D71ED86605004C0C0B /* Sources */, + 397848D81ED86605004C0C0B /* Frameworks */, + 397848D91ED86605004C0C0B /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + D0EA58131FA836CA00D03ED1 /* PBXTargetDependency */, + D0EA58151FA836CE00D03ED1 /* PBXTargetDependency */, + D0EA58171FA836D200D03ED1 /* PBXTargetDependency */, + ); + name = NotificationService; + productName = NotificationService; + productReference = 397848DB1ED86605004C0C0B /* NotificationService.appex */; + productType = "com.apple.product-type.app-extension"; + }; + 3B43E3CF1D95C48D00BBA9DB /* StoragePerfTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 3B43E3E91D95C48E00BBA9DB /* Build configuration list for PBXNativeTarget "StoragePerfTests" */; + buildPhases = ( + 3B43E3CC1D95C48D00BBA9DB /* Sources */, + 3B43E3CD1D95C48D00BBA9DB /* Frameworks */, + 3B43E3CE1D95C48D00BBA9DB /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 3B43E3D61D95C48D00BBA9DB /* PBXTargetDependency */, + ); + name = StoragePerfTests; + productName = StoragePerfTests; + productReference = 3B43E3D01D95C48D00BBA9DB /* StoragePerfTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 3BFE4B061D342FB800DDF53F /* XCUITests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 3BFE4B201D342FB900DDF53F /* Build configuration list for PBXNativeTarget "XCUITests" */; + buildPhases = ( + E6EC6EFC1E5354E20067985D /* Copy Carthage Frameworks */, + 3BFE4B031D342FB800DDF53F /* Sources */, + 3BFE4B041D342FB800DDF53F /* Frameworks */, + 3BFE4B051D342FB800DDF53F /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 3BFE4B0D1D342FB900DDF53F /* PBXTargetDependency */, + ); + name = XCUITests; + productName = XCUITests; + productReference = 3BFE4B071D342FB800DDF53F /* XCUITests.xctest */; + productType = "com.apple.product-type.bundle.ui-testing"; + }; + 7BEB64401C7345600092C02E /* L10nSnapshotTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = E60138631C89EAE700DF9756 /* Build configuration list for PBXNativeTarget "L10nSnapshotTests" */; + buildPhases = ( + 7BEB64431C7345600092C02E /* Sources */, + 7BEB64461C7345600092C02E /* Frameworks */, + 7BEB64471C7345600092C02E /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 7BEB64411C7345600092C02E /* PBXTargetDependency */, + ); + name = L10nSnapshotTests; + productName = L10nSnapshotTests; + productReference = 7BEB644D1C7345600092C02E /* L10nSnapshotTests.xctest */; + productType = "com.apple.product-type.bundle.ui-testing"; + }; + 7BEB644F1C7345990092C02E /* MarketingUITests */ = { + isa = PBXNativeTarget; + buildConfigurationList = E60138641C89EAE700DF9756 /* Build configuration list for PBXNativeTarget "MarketingUITests" */; + buildPhases = ( + 7BEB64501C7345990092C02E /* Sources */, + 7BEB64531C7345990092C02E /* Frameworks */, + 7BEB64541C7345990092C02E /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 7BEB645D1C7346100092C02E /* PBXTargetDependency */, + ); + name = MarketingUITests; + productName = MarketingUITests; + productReference = 7BEB645A1C7345990092C02E /* MarketingUITests.xctest */; + productType = "com.apple.product-type.bundle.ui-testing"; + }; + D39FA15E1A83E0EC00EE869C /* UITests */ = { + isa = PBXNativeTarget; + buildConfigurationList = D39FA1671A83E0EC00EE869C /* Build configuration list for PBXNativeTarget "UITests" */; + buildPhases = ( + D39FA15B1A83E0EC00EE869C /* Sources */, + D39FA15C1A83E0EC00EE869C /* Frameworks */, + D39FA15D1A83E0EC00EE869C /* Resources */, + E6ECF2371C974E0600B0DC93 /* CopyFiles */, + 0B21E8011E26C5D3000C8779 /* CopyFiles */, + ); + buildRules = ( + ); + dependencies = ( + D39FA1661A83E0EC00EE869C /* PBXTargetDependency */, + ); + name = UITests; + productName = UITests; + productReference = D39FA15F1A83E0EC00EE869C /* UITests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + E4BA8A2A1B4B0A1600BC2E95 /* ViewLater */ = { + isa = PBXNativeTarget; + buildConfigurationList = E4BA8A581B4B0A1700BC2E95 /* Build configuration list for PBXNativeTarget "ViewLater" */; + buildPhases = ( + E4BA8A271B4B0A1600BC2E95 /* Sources */, + E4BA8A281B4B0A1600BC2E95 /* Frameworks */, + E4BA8A291B4B0A1600BC2E95 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + D09A0CEB1FAA2C38009A0273 /* PBXTargetDependency */, + D09A0CE51FAA2C18009A0273 /* PBXTargetDependency */, + D09A0CE71FAA2C1B009A0273 /* PBXTargetDependency */, + D09A0CE91FAA2C1E009A0273 /* PBXTargetDependency */, + ); + name = ViewLater; + productName = ViewLater; + productReference = E4BA8A2B1B4B0A1600BC2E95 /* ViewLater.appex */; + productType = "com.apple.product-type.app-extension"; + }; + E4D567171ADECE2700F1EFE7 /* ReadingList */ = { + isa = PBXNativeTarget; + buildConfigurationList = E4D567531ADECE2900F1EFE7 /* Build configuration list for PBXNativeTarget "ReadingList" */; + buildPhases = ( + E4D567131ADECE2700F1EFE7 /* Sources */, + E4D567141ADECE2700F1EFE7 /* Frameworks */, + E4D567151ADECE2700F1EFE7 /* Headers */, + E4D567161ADECE2700F1EFE7 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + D09A0CDF1FAA2577009A0273 /* PBXTargetDependency */, + 7B9A62F91C400303005C83DC /* PBXTargetDependency */, + ); + name = ReadingList; + productName = ReadingList; + productReference = E4D567181ADECE2700F1EFE7 /* ReadingList.framework */; + productType = "com.apple.product-type.framework"; + }; + E4D567211ADECE2700F1EFE7 /* ReadingListTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = E4D567541ADECE2900F1EFE7 /* Build configuration list for PBXNativeTarget "ReadingListTests" */; + buildPhases = ( + E4D5671E1ADECE2700F1EFE7 /* Sources */, + E4D5671F1ADECE2700F1EFE7 /* Frameworks */, + E4D567201ADECE2700F1EFE7 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 7B10AAF01E3A58C80002DD08 /* PBXTargetDependency */, + E4D567251ADECE2700F1EFE7 /* PBXTargetDependency */, + ); + name = ReadingListTests; + productName = ReadingListTests; + productReference = E4D567221ADECE2700F1EFE7 /* ReadingListTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + E69DB0741E97DEA9008A67E6 /* SyncTelemetry */ = { + isa = PBXNativeTarget; + buildConfigurationList = E69DB08C1E97DEAA008A67E6 /* Build configuration list for PBXNativeTarget "SyncTelemetry" */; + buildPhases = ( + E69DB0701E97DEA9008A67E6 /* Sources */, + E69DB0711E97DEA9008A67E6 /* Frameworks */, + E69DB0721E97DEA9008A67E6 /* Headers */, + E69DB0731E97DEA9008A67E6 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + D09A0CE11FAA25C5009A0273 /* PBXTargetDependency */, + ); + name = SyncTelemetry; + productName = SyncTelemetry; + productReference = E69DB0751E97DEA9008A67E6 /* SyncTelemetry.framework */; + productType = "com.apple.product-type.framework"; + }; + E69DB07C1E97DEA9008A67E6 /* SyncTelemetryTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = E69DB0921E97DEAA008A67E6 /* Build configuration list for PBXNativeTarget "SyncTelemetryTests" */; + buildPhases = ( + E69DB0791E97DEA9008A67E6 /* Sources */, + E69DB07A1E97DEA9008A67E6 /* Frameworks */, + E69DB07B1E97DEA9008A67E6 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + E69DB0801E97DEAA008A67E6 /* PBXTargetDependency */, + E69DB0821E97DEAA008A67E6 /* PBXTargetDependency */, + ); + name = SyncTelemetryTests; + productName = SyncTelemetryTests; + productReference = E69DB07D1E97DEA9008A67E6 /* SyncTelemetryTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + E6F9650B1B2F1CF20034B023 /* SharedTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = E6F965381B2F1CF20034B023 /* Build configuration list for PBXNativeTarget "SharedTests" */; + buildPhases = ( + E6F965081B2F1CF20034B023 /* Sources */, + E6F965091B2F1CF20034B023 /* Frameworks */, + E6F9650A1B2F1CF20034B023 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + E6F965141B2F1CF20034B023 /* PBXTargetDependency */, + E6F9653A1B2F1D330034B023 /* PBXTargetDependency */, + ); + name = SharedTests; + productName = SharedTests; + productReference = E6F9650C1B2F1CF20034B023 /* SharedTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + F84B21BD1A090F8100AAB793 /* Client */ = { + isa = PBXNativeTarget; + buildConfigurationList = F84B21DD1A090F8100AAB793 /* Build configuration list for PBXNativeTarget "Client" */; + buildPhases = ( + 7B604F8F1C494AAA006EEEC3 /* Copy Carthage Dependencies */, + F84B21BA1A090F8100AAB793 /* Sources */, + F84B21BC1A090F8100AAB793 /* Resources */, + 28CE83DE1A1D1E7C00576538 /* Frameworks */, + F84B22531A0920C600AAB793 /* Embed App Extensions */, + E6639F191BF11E3A002D0853 /* Conditionally Add Settings Bundle */, + E6B09CD31C74EEDB00C63FA1 /* Copy Frameworks */, + E4DAA6741F63027B00FA53AF /* Run Script */, + ); + buildRules = ( + ); + dependencies = ( + E4A888191A95679500CDC337 /* PBXTargetDependency */, + 2FA436111ABB83B4008031D1 /* PBXTargetDependency */, + E4D5672E1ADECE2800F1EFE7 /* PBXTargetDependency */, + 288A2D9C1AB8B3260023ABC3 /* PBXTargetDependency */, + 2FCAE2301ABB51F800877008 /* PBXTargetDependency */, + 282731741ABC9BE700AA1954 /* PBXTargetDependency */, + E69DB0891E97DEAA008A67E6 /* PBXTargetDependency */, + 397848E11ED86605004C0C0B /* PBXTargetDependency */, + F84B22651A09210A00AAB793 /* PBXTargetDependency */, + F84B22521A0920C600AAB793 /* PBXTargetDependency */, + 390527551C874D35007E0BB7 /* PBXTargetDependency */, + E4BA8A341B4B0A1700BC2E95 /* PBXTargetDependency */, + ); + name = Client; + productName = Client; + productReference = F84B21BE1A090F8100AAB793 /* Client.app */; + productType = "com.apple.product-type.application"; + }; + F84B21D21A090F8100AAB793 /* ClientTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = F84B21E01A090F8100AAB793 /* Build configuration list for PBXNativeTarget "ClientTests" */; + buildPhases = ( + F84B21CF1A090F8100AAB793 /* Sources */, + F84B21D01A090F8100AAB793 /* Frameworks */, + F84B21D11A090F8100AAB793 /* Resources */, + 2F3444EC1AB2378200FD9731 /* Copy Files */, + ); + buildRules = ( + ); + dependencies = ( + 7B9BF92F1E435DE400CB24F4 /* PBXTargetDependency */, + F84B21D51A090F8100AAB793 /* PBXTargetDependency */, + ); + name = ClientTests; + productName = ClientTests; + productReference = F84B21D31A090F8100AAB793 /* ClientTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + F84B22481A0920C600AAB793 /* ShareTo */ = { + isa = PBXNativeTarget; + buildConfigurationList = F84B22551A0920C600AAB793 /* Build configuration list for PBXNativeTarget "ShareTo" */; + buildPhases = ( + F84B22451A0920C600AAB793 /* Sources */, + F84B22461A0920C600AAB793 /* Frameworks */, + F84B22471A0920C600AAB793 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + D09A0CEF1FAA2C66009A0273 /* PBXTargetDependency */, + D09A0CF51FAA2C99009A0273 /* PBXTargetDependency */, + D09A0CF11FAA2C7E009A0273 /* PBXTargetDependency */, + 2F9A72321ABB856100F9F05D /* PBXTargetDependency */, + D09A0CF31FAA2C81009A0273 /* PBXTargetDependency */, + ); + name = ShareTo; + productName = ShareToFirefox; + productReference = F84B22491A0920C600AAB793 /* ShareTo.appex */; + productType = "com.apple.product-type.app-extension"; + }; + F84B225B1A09210A00AAB793 /* SendTo */ = { + isa = PBXNativeTarget; + buildConfigurationList = F84B22671A09210A00AAB793 /* Build configuration list for PBXNativeTarget "SendTo" */; + buildPhases = ( + F84B22581A09210A00AAB793 /* Sources */, + F84B22591A09210A00AAB793 /* Frameworks */, + F84B225A1A09210A00AAB793 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + D09A0CF71FAA2CB2009A0273 /* PBXTargetDependency */, + D09A0CFD1FAA2CC5009A0273 /* PBXTargetDependency */, + D09A0CF91FAA2CBF009A0273 /* PBXTargetDependency */, + 2F9A72341ABB856A00F9F05D /* PBXTargetDependency */, + D09A0CFB1FAA2CC2009A0273 /* PBXTargetDependency */, + ); + name = SendTo; + productName = SendToDevice; + productReference = F84B225C1A09210A00AAB793 /* SendTo.appex */; + productType = "com.apple.product-type.app-extension"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + F84B21B61A090F8100AAB793 /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftMigration = 0700; + LastSwiftUpdateCheck = 0830; + LastUpgradeCheck = 0800; + ORGANIZATIONNAME = Mozilla; + TargetAttributes = { + 2827315D1ABC9BE600AA1954 = { + CreatedOnToolsVersion = 6.2; + DevelopmentTeam = 43AQ936H96; + LastSwiftMigration = 0820; + }; + 282731671ABC9BE700AA1954 = { + CreatedOnToolsVersion = 6.2; + DevelopmentTeam = 43AQ936H96; + LastSwiftMigration = 0820; + TestTargetID = F84B21BD1A090F8100AAB793; + }; + 288A2D851AB8B3260023ABC3 = { + CreatedOnToolsVersion = 6.2; + DevelopmentTeam = 43AQ936H96; + LastSwiftMigration = 0820; + }; + 2FA435FA1ABB83B4008031D1 = { + CreatedOnToolsVersion = 6.2; + DevelopmentTeam = 43AQ936H96; + LastSwiftMigration = 0820; + }; + 2FA436041ABB83B4008031D1 = { + CreatedOnToolsVersion = 6.2; + DevelopmentTeam = 43AQ936H96; + LastSwiftMigration = 0820; + TestTargetID = F84B21BD1A090F8100AAB793; + }; + 2FCAE2191ABB51F800877008 = { + CreatedOnToolsVersion = 6.2; + DevelopmentTeam = 43AQ936H96; + LastSwiftMigration = 0820; + }; + 2FCAE2231ABB51F800877008 = { + CreatedOnToolsVersion = 6.2; + DevelopmentTeam = 43AQ936H96; + LastSwiftMigration = 0820; + TestTargetID = F84B21BD1A090F8100AAB793; + }; + 390527491C874D35007E0BB7 = { + CreatedOnToolsVersion = 7.2.1; + DevelopmentTeam = 43AQ936H96; + LastSwiftMigration = 0820; + ProvisioningStyle = Manual; + }; + 397848DA1ED86605004C0C0B = { + CreatedOnToolsVersion = 8.3.2; + DevelopmentTeam = 43AQ936H96; + ProvisioningStyle = Manual; + SystemCapabilities = { + com.apple.Push = { + enabled = 0; + }; + }; + }; + 3B43E3CF1D95C48D00BBA9DB = { + CreatedOnToolsVersion = 8.0; + DevelopmentTeam = 43AQ936H96; + LastSwiftMigration = 0820; + ProvisioningStyle = Automatic; + TestTargetID = F84B21BD1A090F8100AAB793; + }; + 3BFE4B061D342FB800DDF53F = { + CreatedOnToolsVersion = 7.3.1; + DevelopmentTeam = 43AQ936H96; + LastSwiftMigration = 0820; + TestTargetID = F84B21BD1A090F8100AAB793; + }; + 7BEB64401C7345600092C02E = { + DevelopmentTeam = 43AQ936H96; + TestTargetID = F84B21BD1A090F8100AAB793; + }; + 7BEB644F1C7345990092C02E = { + DevelopmentTeam = 43AQ936H96; + TestTargetID = F84B21BD1A090F8100AAB793; + }; + D39FA15E1A83E0EC00EE869C = { + CreatedOnToolsVersion = 6.1.1; + DevelopmentTeam = 43AQ936H96; + LastSwiftMigration = 0820; + TestTargetID = F84B21BD1A090F8100AAB793; + }; + E4BA8A2A1B4B0A1600BC2E95 = { + CreatedOnToolsVersion = 6.4; + DevelopmentTeam = 43AQ936H96; + LastSwiftMigration = 0820; + ProvisioningStyle = Manual; + SystemCapabilities = { + com.apple.ApplicationGroups.iOS = { + enabled = 1; + }; + }; + }; + E4D567171ADECE2700F1EFE7 = { + CreatedOnToolsVersion = 6.3; + DevelopmentTeam = 43AQ936H96; + LastSwiftMigration = 0820; + }; + E4D567211ADECE2700F1EFE7 = { + CreatedOnToolsVersion = 6.3; + DevelopmentTeam = 9G8J6YA743; + LastSwiftMigration = 0820; + TestTargetID = F84B21BD1A090F8100AAB793; + }; + E69DB0741E97DEA9008A67E6 = { + CreatedOnToolsVersion = 8.3; + DevelopmentTeam = 43AQ936H96; + ProvisioningStyle = Manual; + }; + E69DB07C1E97DEA9008A67E6 = { + CreatedOnToolsVersion = 8.3; + DevelopmentTeam = 43AQ936H96; + LastSwiftMigration = 0830; + ProvisioningStyle = Automatic; + TestTargetID = F84B21BD1A090F8100AAB793; + }; + E6F9650B1B2F1CF20034B023 = { + CreatedOnToolsVersion = 6.3.2; + DevelopmentTeam = 43AQ936H96; + LastSwiftMigration = 0820; + }; + F84B21BD1A090F8100AAB793 = { + CreatedOnToolsVersion = 6.1; + DevelopmentTeam = 43AQ936H96; + LastSwiftMigration = 0820; + ProvisioningStyle = Manual; + SystemCapabilities = { + com.apple.ApplicationGroups.iOS = { + enabled = 1; + }; + com.apple.BackgroundModes = { + enabled = 1; + }; + com.apple.Keychain = { + enabled = 1; + }; + com.apple.Push = { + enabled = 1; + }; + com.apple.SafariKeychain = { + enabled = 1; + }; + }; + }; + F84B21D21A090F8100AAB793 = { + CreatedOnToolsVersion = 6.1; + DevelopmentTeam = 43AQ936H96; + LastSwiftMigration = 0820; + TestTargetID = F84B21BD1A090F8100AAB793; + }; + F84B22481A0920C600AAB793 = { + CreatedOnToolsVersion = 6.1; + DevelopmentTeam = 43AQ936H96; + LastSwiftMigration = 0820; + ProvisioningStyle = Manual; + SystemCapabilities = { + com.apple.ApplicationGroups.iOS = { + enabled = 1; + }; + com.apple.Keychain = { + enabled = 1; + }; + com.apple.Push = { + enabled = 0; + }; + }; + }; + F84B225B1A09210A00AAB793 = { + CreatedOnToolsVersion = 6.1; + DevelopmentTeam = 43AQ936H96; + LastSwiftMigration = 0820; + ProvisioningStyle = Manual; + SystemCapabilities = { + com.apple.ApplicationGroups.iOS = { + enabled = 1; + }; + com.apple.Keychain = { + enabled = 1; + }; + com.apple.Push = { + enabled = 0; + }; + }; + }; + }; + }; + buildConfigurationList = F84B21B91A090F8100AAB793 /* Build configuration list for PBXProject "Client" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = F84B21B51A090F8100AAB793; + productRefGroup = F84B21BF1A090F8100AAB793 /* Products */; + projectDirPath = ""; + projectReferences = ( + { + ProductGroup = 28CE83CB1A1D1D5100576538 /* Products */; + ProjectRef = 28CE83CA1A1D1D5100576538 /* FxA.xcodeproj */; + }, + { + ProductGroup = D30EBB5B1C75503800105AE9 /* Products */; + ProjectRef = D30EBB5A1C75503800105AE9 /* KIF.xcodeproj */; + }, + { + ProductGroup = 0B742CC71B32491400EE9264 /* Products */; + ProjectRef = 0B742CC61B32491400EE9264 /* sqlcipher.xcodeproj */; + }, + { + ProductGroup = 7B9A62BA1C4002B1005C83DC /* Products */; + ProjectRef = 7B9A62B91C4002B1005C83DC /* SQLite.xcodeproj */; + }, + ); + projectRoot = ""; + targets = ( + F84B21BD1A090F8100AAB793 /* Client */, + 397848DA1ED86605004C0C0B /* NotificationService */, + F84B225B1A09210A00AAB793 /* SendTo */, + F84B22481A0920C600AAB793 /* ShareTo */, + 390527491C874D35007E0BB7 /* Today */, + E4BA8A2A1B4B0A1600BC2E95 /* ViewLater */, + 288A2D851AB8B3260023ABC3 /* Shared */, + 2FCAE2191ABB51F800877008 /* Storage */, + 2FA435FA1ABB83B4008031D1 /* Account */, + 2827315D1ABC9BE600AA1954 /* Sync */, + E4D567171ADECE2700F1EFE7 /* ReadingList */, + E69DB0741E97DEA9008A67E6 /* SyncTelemetry */, + F84B21D21A090F8100AAB793 /* ClientTests */, + D39FA15E1A83E0EC00EE869C /* UITests */, + 2FCAE2231ABB51F800877008 /* StorageTests */, + 2FA436041ABB83B4008031D1 /* AccountTests */, + 282731671ABC9BE700AA1954 /* SyncTests */, + E4D567211ADECE2700F1EFE7 /* ReadingListTests */, + E6F9650B1B2F1CF20034B023 /* SharedTests */, + 7BEB64401C7345600092C02E /* L10nSnapshotTests */, + 7BEB644F1C7345990092C02E /* MarketingUITests */, + 3BFE4B061D342FB800DDF53F /* XCUITests */, + 3B43E3CF1D95C48D00BBA9DB /* StoragePerfTests */, + E69DB07C1E97DEA9008A67E6 /* SyncTelemetryTests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXReferenceProxy section */ + 0B742CCC1B32491400EE9264 /* libsqlcipher.a */ = { + isa = PBXReferenceProxy; + fileType = archive.ar; + path = libsqlcipher.a; + remoteRef = 0B742CCB1B32491400EE9264 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + 28CE83D01A1D1D5100576538 /* FxA.framework */ = { + isa = PBXReferenceProxy; + fileType = wrapper.framework; + path = FxA.framework; + remoteRef = 28CE83CF1A1D1D5100576538 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + 28CE83D21A1D1D5100576538 /* FxATests.xctest */ = { + isa = PBXReferenceProxy; + fileType = wrapper.cfbundle; + path = FxATests.xctest; + remoteRef = 28CE83D11A1D1D5100576538 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + 7B10AAE61E3A57F90002DD08 /* SQLiteTests iOS.xctest */ = { + isa = PBXReferenceProxy; + fileType = wrapper.cfbundle; + path = "SQLiteTests iOS.xctest"; + remoteRef = 7B10AAE51E3A57F90002DD08 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + 7B10AAE81E3A57F90002DD08 /* SQLiteTests Mac.xctest */ = { + isa = PBXReferenceProxy; + fileType = wrapper.cfbundle; + path = "SQLiteTests Mac.xctest"; + remoteRef = 7B10AAE71E3A57F90002DD08 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + 7B10AAEA1E3A57F90002DD08 /* SQLite.framework */ = { + isa = PBXReferenceProxy; + fileType = wrapper.framework; + path = SQLite.framework; + remoteRef = 7B10AAE91E3A57F90002DD08 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + 7B10AAEC1E3A57F90002DD08 /* SQLiteTests tvOS.xctest */ = { + isa = PBXReferenceProxy; + fileType = wrapper.cfbundle; + path = "SQLiteTests tvOS.xctest"; + remoteRef = 7B10AAEB1E3A57F90002DD08 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + 7B10AAEE1E3A57F90002DD08 /* SQLite.framework */ = { + isa = PBXReferenceProxy; + fileType = wrapper.framework; + path = SQLite.framework; + remoteRef = 7B10AAED1E3A57F90002DD08 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + 7B9A62DB1C4002B1005C83DC /* SQLite.framework */ = { + isa = PBXReferenceProxy; + fileType = wrapper.framework; + path = SQLite.framework; + remoteRef = 7B9A62DA1C4002B1005C83DC /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + 7B9A62DF1C4002B1005C83DC /* SQLite.framework */ = { + isa = PBXReferenceProxy; + fileType = wrapper.framework; + path = SQLite.framework; + remoteRef = 7B9A62DE1C4002B1005C83DC /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + D30EBB641C75503800105AE9 /* libKIF.a */ = { + isa = PBXReferenceProxy; + fileType = archive.ar; + path = libKIF.a; + remoteRef = D30EBB631C75503800105AE9 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + D30EBB661C75503800105AE9 /* Test Host.app */ = { + isa = PBXReferenceProxy; + fileType = wrapper.application; + path = "Test Host.app"; + remoteRef = D30EBB651C75503800105AE9 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + D30EBB681C75503800105AE9 /* KIF Tests - XCTest.xctest */ = { + isa = PBXReferenceProxy; + fileType = wrapper.cfbundle; + path = "KIF Tests - XCTest.xctest"; + remoteRef = D30EBB671C75503800105AE9 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + D30EBB6A1C75503800105AE9 /* KIF.framework */ = { + isa = PBXReferenceProxy; + fileType = wrapper.framework; + path = KIF.framework; + remoteRef = D30EBB691C75503800105AE9 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + E6CB64CC1DA42C2900887098 /* KIFFrameworkConsumer.app */ = { + isa = PBXReferenceProxy; + fileType = wrapper.application; + path = KIFFrameworkConsumer.app; + remoteRef = E6CB64CB1DA42C2900887098 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + E6CB64CE1DA42C2900887098 /* KIFFrameworkConsumerTests.xctest */ = { + isa = PBXReferenceProxy; + fileType = wrapper.cfbundle; + path = KIFFrameworkConsumerTests.xctest; + remoteRef = E6CB64CD1DA42C2900887098 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; +/* End PBXReferenceProxy section */ + +/* Begin PBXResourcesBuildPhase section */ + 2827315C1ABC9BE600AA1954 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + D37DE2CA1CA356F900A5EC69 /* testcert1.pem in Resources */, + D37DE2CB1CA356F900A5EC69 /* testcert2.pem in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 282731661ABC9BE700AA1954 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 288A2D841AB8B3260023ABC3 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + E650759E1E37F7AB006961AC /* effective_tld_names.dat in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 2FA435F91ABB83B4008031D1 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 2FA436031ABB83B4008031D1 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + E6C9EB6B1E2FBFC300D5CE80 /* signedInUser.json in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 2FCAE2181ABB51F800877008 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 2FCAE2221ABB51F800877008 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + E4791B9C1CC035FD00C6D77B /* testcert1.pem in Resources */, + 39E65D191CA455A900C63CE3 /* Images.xcassets in Resources */, + 2891F2CB1F991185001B105E /* v33.db in Resources */, + E4791BAA1CC0360200C6D77B /* testcert2.pem in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 390527481C874D35007E0BB7 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 39098DC41CAD5ACB00AE87F3 /* Images.xcassets in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 397848D91ED86605004C0C0B /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 3B43E3CE1D95C48D00BBA9DB /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 3BFE4B051D342FB800DDF53F /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 7BEB64471C7345600092C02E /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 7BEB64541C7345990092C02E /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + D39FA15D1A83E0EC00EE869C /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + E6EAC5961B29CB3A00E1DE1E /* scrollablePage.html in Resources */, + D34E33031BA793C2006135F0 /* loginForm.html in Resources */, + 0B6FBAB21AC1F830007EC669 /* numberedPage.html in Resources */, + 0B5A93421B1EB572004F47A2 /* readablePage.html in Resources */, + E6B4C3D81C68F55C001F97E8 /* JSPrompt.html in Resources */, + D343DCFE1C446BDB00D7EEE8 /* findPage.html in Resources */, + 0BF8F8DA1AEFF1C900E90BC2 /* noTitle.html in Resources */, + E67422C51CFF2D39009E8373 /* youtube.ico in Resources */, + D3E171C21A841EAD00AB44CD /* KIFHelper.js in Resources */, + C8EB60C41F1FB12500F9B5B3 /* navigationDelegate.html in Resources */, + D31EC05F1CC57ED80096F4AB /* localhostLoad.html in Resources */, + 4F97573B1AFA6F37006ECC24 /* readerContent.html in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + E4BA8A291B4B0A1600BC2E95 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + E4BA8AAD1B4B15EF00BC2E95 /* ViewLater.xcassets in Resources */, + 7B2142FF1E5E059900CDD3FC /* InfoPlist.strings in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + E4D567161ADECE2700F1EFE7 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + E4D567201ADECE2700F1EFE7 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + E69DB0731E97DEA9008A67E6 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + E69DB07B1E97DEA9008A67E6 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + E6F9650A1B2F1CF20034B023 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + F84B21BC1A090F8100AAB793 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + D38A1EE01CB458EC0080C842 /* CertError.html in Resources */, + 0BA1E0301B051A07007675AF /* NetError.css in Resources */, + 3BC659491E5BA4AE006D560F /* TopSites in Resources */, + E4B7B77E1A793CF20022C5E0 /* FiraSans-SemiBold.ttf in Resources */, + EB2A633C1F3B513E004EF8B0 /* disconnect-analytics.json in Resources */, + F84B220B1A0910F600AAB793 /* Images.xcassets in Resources */, + F35B8D2B1D6380EA008E3D61 /* SessionRestore.html in Resources */, + 7B42406E1CA04CAC009B5C28 /* Menu.xcassets in Resources */, + E49943F71AE69EDD00BF9DE4 /* Intro.xcassets in Resources */, + 3BC659591E5BA505006D560F /* top_sites.json in Resources */, + D02816D21ECA60DF00240CAA /* HistoryStateHelper.js in Resources */, + E4B7B7631A793CF20022C5E0 /* CharisSILI.ttf in Resources */, + E4CD9F541A71506400318571 /* Reader.html in Resources */, + EB2A633E1F3B514A004EF8B0 /* disconnect-content.json in Resources */, + 7B2142FE1E5E055000CDD3FC /* InfoPlist.strings in Resources */, + E69922171B94E3EF007C480D /* Licenses.html in Resources */, + D3B692411B9F9CC8004B87A4 /* FindInPage.js in Resources */, + E4CD9F5B1A71506C00318571 /* Reader.css in Resources */, + E4B7B7611A793CF20022C5E0 /* CharisSILB.ttf in Resources */, + D37524871C6E8B5A00A5F6C2 /* topdomains.txt in Resources */, + E4B7B7621A793CF20022C5E0 /* CharisSILBI.ttf in Resources */, + 7B4980A81CE363ED0017547C /* Settings.xcassets in Resources */, + 3B4262E71E538EC400CA681C /* page-metadata-parser.js in Resources */, + E4A961361AC052DE0069AD6F /* ReadabilityTabHelper.js in Resources */, + E4B7B7861A793CF20022C5E0 /* FiraSans-UltraLight.ttf in Resources */, + 2F44FB2C1A9D5D8500FD20CC /* Home.xcassets in Resources */, + E4B7B77D1A793CF20022C5E0 /* FiraSans-Regular.ttf in Resources */, + EB2A633B1F3B513B004EF8B0 /* disconnect-advertising.json in Resources */, + 0BDA568B1B26B1BE008C9B96 /* LoginsHelper.js in Resources */, + E4B7B7791A793CF20022C5E0 /* FiraSans-Light.ttf in Resources */, + E49BFF591B1F48C3009368CC /* Readability.js in Resources */, + E4CD9E9A1A68980A00318571 /* ReaderMode.js in Resources */, + D30684F11C84F12A002D8D82 /* SearchPlugins in Resources */, + 3B4262E51E538EC400CA681C /* MetadataHelper.js in Resources */, + E47536FC1C85415F0022CC69 /* PrintHelper.js in Resources */, + 74821FFE1DB6D3AC00EEEA72 /* MailSchemes.plist in Resources */, + FA9294011D6584A200AC8D33 /* QRCode.xcassets in Resources */, + D308EE561CBF0BF5006843F2 /* CertError.css in Resources */, + E4B7B7641A793CF20022C5E0 /* CharisSILR.ttf in Resources */, + D3BA7E0C1B0E902A00153782 /* ContextMenu.js in Resources */, + 3BE7274E1CCFE8A30099189F /* CustomSearchHelper.js in Resources */, + E4B7B7681A793CF20022C5E0 /* FiraSans-Bold.ttf in Resources */, + E4B7B7781A793CF20022C5E0 /* FiraSans-Italic.ttf in Resources */, + A91528D21D0E637600CD0425 /* NoImageModeHelper.js in Resources */, + 0BA1E00E1B03FB0B007675AF /* NetError.html in Resources */, + E4A961381AC06FA50069AD6F /* ReaderViewLoading.html in Resources */, + EB2A633F1F3B514B004EF8B0 /* disconnect-social.json in Resources */, + E4ECCDAE1AB131770005E717 /* FiraSans-Medium.ttf in Resources */, + E4424B3C1AC71FB400F44C38 /* FiraSans-Book.ttf in Resources */, + 0BF42D391A7C0E8900889E28 /* Favicons.js in Resources */, + 39A35AED1C0662A3006B9E87 /* SpotlightHelper.js in Resources */, + E689C6F81E0C6E72008BAADB /* FxASignIn.js in Resources */, + E4D6BEB91A0930EC00F538BD /* LaunchScreen.xib in Resources */, + A93067D91D0FE15E00C49C6E /* NightModeHelper.js in Resources */, + 2F44FB2D1A9D5D8500FD20CC /* FiraSans-BoldItalic.ttf in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + F84B21D11A090F8100AAB793 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 3B61CD631F2A769D00D38DE1 /* pocketglobalfeed.json in Resources */, + A83E5B1A1C1DA8BF0026D912 /* image.gif in Resources */, + A83E5B1B1C1DA8BF0026D912 /* image.png in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + F84B22471A0920C600AAB793 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + E487B24F1AC1CC9200F3E86F /* FiraSans-Medium.ttf in Resources */, + E487B2331AC1C64300F3E86F /* FiraSans-Regular.ttf in Resources */, + F8708D2E1A0970B70051AB07 /* Images.xcassets in Resources */, + E487B2501AC1CC9800F3E86F /* FiraSans-Light.ttf in Resources */, + E487B24E1AC1C66400F3E86F /* FiraSans-SemiBold.ttf in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + F84B225A1A09210A00AAB793 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + E4F21AA61A13C4A300B0FAAA /* Images.xcassets in Resources */, + 7B2143001E5E05A000CDD3FC /* InfoPlist.strings in Resources */, + F8708D2C1A0970B40051AB07 /* MainInterface.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 7B604F8F1C494AAA006EEEC3 /* Copy Carthage Dependencies */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "$(SRCROOT)/Carthage/Build/iOS/Alamofire.framework", + "$(SRCROOT)/Carthage/Build/iOS/SDWebImage.framework", + "$(SRCROOT)/Carthage/Build/iOS/SnapKit.framework", + "$(SRCROOT)/Carthage/Build/iOS/XCGLogger.framework", + "$(SRCROOT)/Carthage/Build/iOS/AdjustSdk.framework", + "$(SRCROOT)/Carthage/Build/iOS/GCDWebServers.framework", + "$(SRCROOT)/Carthage/Build/iOS/OnePasswordExtension.framework", + "$(SRCROOT)/Carthage/Build/iOS/Deferred.framework", + "$(SRCROOT)/Carthage/Build/iOS/SwiftKeychainWrapper.framework", + "$(SRCROOT)/Carthage/Build/iOS/Fuzi.framework", + "$(SRCROOT)/Carthage/Build/iOS/JSONSchema.framework", + "$(SRCROOT)/Carthage/Build/iOS/SwiftyJSON.framework", + "$(SRCROOT)/Carthage/Build/iOS/Sentry.framework", + "$(SRCROOT)/Carthage/Build/iOS/SwiftRouter.framework", + "$(SRCROOT)/Carthage/Build/iOS/Telemetry.framework", + ); + name = "Copy Carthage Dependencies"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/usr/local/bin/carthage copy-frameworks"; + }; + E4DAA6741F63027B00FA53AF /* Run Script */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 8; + files = ( + ); + inputPaths = ( + ); + name = "Run Script"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 1; + shellPath = /bin/sh; + shellScript = "cd \"${BUILT_PRODUCTS_DIR}/${FRAMEWORKS_FOLDER_PATH}/Leanplum.framework/\"\nlipo -remove i386 -remove x86_64 Leanplum -output Leanplum"; + }; + E6639F191BF11E3A002D0853 /* Conditionally Add Settings Bundle */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Conditionally Add Settings Bundle"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "if [ \"${INCLUDE_SETTINGS_BUNDLE}\" = \"YES\" ]\nthen\n cp -r \"${PROJECT_DIR}/${TARGET_NAME}/Application/Settings.bundle\" \"${BUILT_PRODUCTS_DIR}/${PRODUCT_NAME}.app\"\nfi"; + }; + E6EC6EFC1E5354E20067985D /* Copy Carthage Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "$(SRCROOT)/Carthage/Build/iOS/EarlGrey.framework", + ); + name = "Copy Carthage Frameworks"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/usr/local/bin/carthage copy-frameworks"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 282731591ABC9BE600AA1954 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 2853C5411AD84C6800C4F31D /* TabsPayload.swift in Sources */, + 28926B3A1AC0F128009C0B1D /* CleartextPayloadJSON.swift in Sources */, + 280CB1371C20E0A3005AF147 /* BookmarksSynchronizer.swift in Sources */, + 2827319D1ABC9C2F00AA1954 /* SyncMeta.swift in Sources */, + 2894C1661AE89DD200F1F92F /* TabsSynchronizer.swift in Sources */, + 282731991ABC9C2F00AA1954 /* ClientPayload.swift in Sources */, + E67D57031D527449003917B1 /* BatchingClient.swift in Sources */, + 2894C1431AE89D8900F1F92F /* HistorySynchronizer.swift in Sources */, + 28E23C121AC5A5EE00F5AC85 /* State.swift in Sources */, + 28ED02291B262E0A003948B2 /* IndependentRecordSynchronizer.swift in Sources */, + CE564D8E1EB7BD7700BEDDDC /* BookmarksRepairRequestor.swift in Sources */, + 28A17B671BEC727500BC14ED /* Downloader.swift in Sources */, + 2F3724E71ABF3C19007607FA /* StorageClient.swift in Sources */, + 28926B1A1AC0F074009C0B1D /* EncryptedJSON.swift in Sources */, + 2894C16C1AE89FD500F1F92F /* HistoryPayload.swift in Sources */, + 2885023F1AC117A500E7F670 /* SyncStateMachine.swift in Sources */, + EBA31D7D1F79996E0055463D /* SyncTelemetryUtils.swift in Sources */, + 28ED02261B262B5E003948B2 /* LoginsSynchronizer.swift in Sources */, + 2894C1691AE89DDC00F1F92F /* ClientsSynchronizer.swift in Sources */, + 288E67C21B9E730900AE2C6A /* BookmarksDownloader.swift in Sources */, + 28E91E751B443AD5009DF274 /* SyncConstants.swift in Sources */, + 28532CE61C48098D000072D9 /* ThreeWayTreeMerger.swift in Sources */, + 28C28BFD1C51A3B900D5460E /* Merging.swift in Sources */, + 2F3724E91ABF3C19007607FA /* Synchronizer.swift in Sources */, + 28AA941D1B97DCA800703DC6 /* BookmarkPayload.swift in Sources */, + 28ED02021B26123E003948B2 /* LoginPayload.swift in Sources */, + 28926B371AC0F0DD009C0B1D /* EnvelopeJSON.swift in Sources */, + 2827319A1ABC9C2F00AA1954 /* KeyBundle.swift in Sources */, + 2F3724E51ABF3C19007607FA /* Info.swift in Sources */, + 2827319C1ABC9C2F00AA1954 /* Record.swift in Sources */, + E69DB0C31E97E320008A67E6 /* BookmarkTelemetryPing.swift in Sources */, + 2827319B1ABC9C2F00AA1954 /* KeysPayload.swift in Sources */, + 288501DE1AC0F61D00E7F670 /* RequestExtensions.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 282731641ABC9BE700AA1954 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + E6BA20211E52170E00697F9C /* SyncTelemetryTests.swift in Sources */, + 28ECD97A1BA1EA2200D829DA /* MockSyncServer.swift in Sources */, + 285D37E31ABCA69E000E1CF9 /* CryptoTests.swift in Sources */, + 28F596A11ACA13CA0071DDCC /* InfoTests.swift in Sources */, + 2F3724C71ABF3C01007607FA /* StorageClientTests.swift in Sources */, + 28ECD9F41BA1F59800D829DA /* DownloadTests.swift in Sources */, + 2F3724C61ABF3C01007607FA /* LiveStorageClientTests.swift in Sources */, + E6EDE82C1D5244AF007A0732 /* BatchingClientTests.swift in Sources */, + E42736071EA858CF009C428E /* TabsPayloadTests.swift in Sources */, + E6F6D6A51E9E6B2B0012072B /* DeferredTestUtils.swift in Sources */, + 28D980231C47149000277055 /* TestBookmarkTreeMerging.swift in Sources */, + CE339EE61F2507C0009BE0E6 /* TestBookmarksSynchronizer.swift in Sources */, + E67035EE1E538B500037AB83 /* Mocking.swift in Sources */, + 289A4C141C4EB90600A460E3 /* StorageTestUtils.swift in Sources */, + 28532CC11C473977000072D9 /* MockFiles.swift in Sources */, + E6D7C32B1CF4E86C00E746BA /* TestBookmarkModel.swift in Sources */, + CEFC984B1EC0DCF5008A3E48 /* TestBookmarksRepairRequestor.swift in Sources */, + 2F8C76571BC32F3C00D5E4E0 /* MockSyncServerTests.swift in Sources */, + 28F657EA1ABFCA7A00A608BD /* LiveAccountTest.swift in Sources */, + 2F67C5261BB0CB4E00E7B73A /* MetaGlobalTests.swift in Sources */, + 2827319E1ABC9C5900AA1954 /* RecordTests.swift in Sources */, + 2855611F1AEFFA1C00D5ED5B /* HistorySynchronizerTests.swift in Sources */, + 5002717C41BC7C50F67F1CAD /* StateTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 288A2D811AB8B3260023ABC3 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + E65075991E37F7AB006961AC /* Cancellable.swift in Sources */, + E65075BE1E37F7AB006961AC /* TimeConstants.swift in Sources */, + E65075B21E37F7AB006961AC /* Functions.swift in Sources */, + E65075B41E37F7AB006961AC /* KeychainCache.swift in Sources */, + E65075BA1E37F7AB006961AC /* Prefs.swift in Sources */, + 7B10AABB1E3A1F650002DD08 /* URLRequestExtensions.swift in Sources */, + D02818611EF056C800240CAA /* SentryIntegration.swift in Sources */, + E693F0D91E9D64BD0086DC17 /* OptionalExtensions.swift in Sources */, + E65075A01E37F7AB006961AC /* ArrayExtensions.swift in Sources */, + 288A2DAA1AB8B3700023ABC3 /* Box.swift in Sources */, + E65075A31E37F7AB006961AC /* KeychainWrapperExtensions.swift in Sources */, + E65075921E37F7AB006961AC /* Accessibility.swift in Sources */, + E65075B51E37F7AB006961AC /* LaunchArguments.swift in Sources */, + 288A2DAB1AB8B3700023ABC3 /* BoxType.swift in Sources */, + E65075BB1E37F7AB006961AC /* RollingFileLogger.swift in Sources */, + E650759D1E37F7AB006961AC /* DeviceInfo.swift in Sources */, + E65075971E37F7AB006961AC /* AuthenticationKeychainInfo.swift in Sources */, + E65075A51E37F7AB006961AC /* NSFileManagerExtensions.swift in Sources */, + E65075A11E37F7AB006961AC /* HashExtensions.swift in Sources */, + 3964B09A1EA8F06F00F2EEF4 /* FeatureSwitch.swift in Sources */, + E65075981E37F7AB006961AC /* Bytes.swift in Sources */, + E65075B11E37F7AB006961AC /* FSUtils.m in Sources */, + CE7F11941F3CEEC800ABFC0B /* RemoteDevices.swift in Sources */, + D3A14C221CB3145E00253BC6 /* Strings.swift in Sources */, + 7B10AA9F1E3A15020002DD08 /* DataExtensions.swift in Sources */, + 7B3D9E651E4CBFDB007A50DA /* NSCoderExtensions.swift in Sources */, + E65075931E37F7AB006961AC /* AppConstants.swift in Sources */, + E65075B81E37F7AB006961AC /* NotificationConstants.swift in Sources */, + E65075AC1E37F7AB006961AC /* StringExtensions.swift in Sources */, + E65075951E37F7AB006961AC /* AssertionUtils.swift in Sources */, + E683F0C21E93D4E90035D990 /* DictionaryExtensions.swift in Sources */, + E650759B1E37F7AB006961AC /* CrashSimulator.m in Sources */, + 288A2DB51AB8B38D0023ABC3 /* Error.swift in Sources */, + E65075A91E37F7AB006961AC /* NSURLExtensions.swift in Sources */, + 7479B4EF1C5306A200DF000B /* Reachability.swift in Sources */, + E65075B31E37F7AB006961AC /* KeyboardHelper.swift in Sources */, + E65075AA1E37F7AB006961AC /* NSURLProtectionSpaceExtensions.swift in Sources */, + E65075BC1E37F7AB006961AC /* SupportUtils.swift in Sources */, + E65075B61E37F7AB006961AC /* Loader.swift in Sources */, + E65075BF1E37F7AB006961AC /* UserAgent.swift in Sources */, + E65075C01E37F7AB006961AC /* WeakList.swift in Sources */, + E65075AE1E37F7AB006961AC /* UIImageExtensions.swift in Sources */, + E65075A71E37F7AB006961AC /* NSScannerExtensions.swift in Sources */, + E65075A21E37F7AB006961AC /* HexExtensions.swift in Sources */, + 288A2DB61AB8B38D0023ABC3 /* Result.swift in Sources */, + E65075AD1E37F7AB006961AC /* UIColorExtensions.swift in Sources */, + E650759C1E37F7AB006961AC /* DeferredUtils.swift in Sources */, + E65075941E37F7AB006961AC /* AppInfo.swift in Sources */, + 288A2DAC1AB8B3700023ABC3 /* MutableBox.swift in Sources */, + E65075A41E37F7AB006961AC /* NSCharacterSetExtensions.swift in Sources */, + E65075B71E37F7AB006961AC /* Logger.swift in Sources */, + E65075961E37F7AB006961AC /* AsyncReducer.swift in Sources */, + 7B9BF91C1E43472E00CB24F4 /* JSONExtensions.swift in Sources */, + E65075A61E37F7AB006961AC /* NSMutableAttributedStringExtensions.swift in Sources */, + E65075BD1E37F7AB006961AC /* SystemUtils.swift in Sources */, + E65075AB1E37F7AB006961AC /* SetExtensions.swift in Sources */, + E65075A81E37F7AB006961AC /* NSStringExtensions.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 2FA435F61ABB83B4008031D1 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 2FA4362B1ABB8436008031D1 /* FirefoxAccountConfiguration.swift in Sources */, + 39F99FE41E3A6F1700F353B4 /* PushClient.swift in Sources */, + 39F99FE51E3A6F1700F353B4 /* PushConfiguration.swift in Sources */, + D32A350E1D6530D80066DAE9 /* FxADevice.swift in Sources */, + 39F99FE61E3A6F1700F353B4 /* PushRegistration.swift in Sources */, + 2F1A3DE11ABE3C90002F1E15 /* FxALoginStateMachine.swift in Sources */, + D3DBE6E51D6516FE00033FFF /* FxADeviceRegistration.swift in Sources */, + 2FA436291ABB8436008031D1 /* FirefoxAccount.swift in Sources */, + 2FFC4D1B1ABE3C360081D675 /* FxAState.swift in Sources */, + 2FDBCF611ABFC9DE00AFF7F0 /* SyncAuthState.swift in Sources */, + 39C22C291E89791C000C0E56 /* PushCrypto.swift in Sources */, + 2FA436351ABB8436008031D1 /* TokenServerClient.swift in Sources */, + 2FA436331ABB8436008031D1 /* HawkHelper.swift in Sources */, + 2FA4362F1ABB8436008031D1 /* FxAClient10.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 2FA436011ABB83B4008031D1 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + E6F6D6A41E9E6B2B0012072B /* DeferredTestUtils.swift in Sources */, + 2FA4363E1ABB8448008031D1 /* FxAClient10Tests.swift in Sources */, + 2FDBCF9B1AC0ADB500AFF7F0 /* SyncAuthStateTests.swift in Sources */, + 2FA436421ABB8448008031D1 /* TokenServerClientTests.swift in Sources */, + 39C22C2A1E897926000C0E56 /* PushCryptoTests.swift in Sources */, + 39C22C2F1E897BA7000C0E56 /* LivePushClientTests.swift in Sources */, + 2FFC4D381ABE3C420081D675 /* FxAStateTests.swift in Sources */, + 2FA436411ABB8448008031D1 /* LiveAccountTest.swift in Sources */, + 2F1A3DFE1ABE3C9C002F1E15 /* FxALoginStateMachineTests.swift in Sources */, + 2FA4363D1ABB8448008031D1 /* FirefoxAccountTests.swift in Sources */, + 2FA436401ABB8448008031D1 /* HawkHelperTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 2FCAE2151ABB51F800877008 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 2FCAE2621ABB531100877008 /* History.swift in Sources */, + 28126F6E1C2F94F9006466CC /* SQLiteBookmarksModel.swift in Sources */, + 7B9BF9301E449D2500CB24F4 /* MockLogins.swift in Sources */, + 0BDA56B21B26B1E4008C9B96 /* Logins.swift in Sources */, + 0BDA56B41B26B203008C9B96 /* SQLiteLogins.swift in Sources */, + E6F368291D7F594F008CDD67 /* SQLiteHistoryRecommendations.swift in Sources */, + 2829D3A01C2F0AD400DCF931 /* Sharing.swift in Sources */, + 2FCAE2751ABB531100877008 /* SQLiteRemoteClientsAndTabs.swift in Sources */, + 285F2DC11AF80B4600211843 /* SQLiteBookmarksSyncing.swift in Sources */, + 28C4AB721AD42D4300D9ACE3 /* Clients.swift in Sources */, + D0131B4D1F3CF7D8000CDE86 /* SQLiteFavicons.swift in Sources */, + 28126F741C2F96F1006466CC /* SQLiteBookmarksResetting.swift in Sources */, + 28B62ACE1BC745E7004A585A /* Syncable.swift in Sources */, + 74B195441CF503FC007F36EF /* RecentlyClosedTabs.swift in Sources */, + E65075C21E37F956006961AC /* ExtensionUtils.swift in Sources */, + 7BF5A1EA1B41640500EA9DD8 /* SyncQueue.swift in Sources */, + 2852B8441C51996B00591EAC /* Trees.swift in Sources */, + E677F0541D94247300ECF1FB /* Metadata.swift in Sources */, + 28E08C991AF44EF9009BA2FA /* SQLiteHistory.swift in Sources */, + 285D3B901B4386520035FD22 /* SQLiteQueue.swift in Sources */, + 28532D321C483E3D000072D9 /* CompletionOps.swift in Sources */, + 2FCAE25D1ABB531100877008 /* Bookmarks.swift in Sources */, + 394CF6CF1BAA493C00906917 /* DefaultSuggestedSites.swift in Sources */, + 2FCAE2781ABB531100877008 /* Visit.swift in Sources */, + D3BF8CBB1B7425570007AFE6 /* DiskImageStore.swift in Sources */, + E6FF6ACA1D873CFF0070C294 /* PageMetadata.swift in Sources */, + 2FCAE2611ABB531100877008 /* FileAccessor.swift in Sources */, + 28126F481C2F948E006466CC /* SQLiteBookmarksHelpers.swift in Sources */, + E677F0451D9423FB00ECF1FB /* SQLiteMetadata.swift in Sources */, + D37DE2831CA2047500A5EC69 /* CertStore.swift in Sources */, + D018F93E1F44A71A0098F8CA /* Schema.swift in Sources */, + 2829D37A1C2F0A7F00DCF931 /* BookmarksModel.swift in Sources */, + 2FCAE2661ABB531100877008 /* Site.swift in Sources */, + 0B54BD191B698B7C004C822C /* SuggestedSites.swift in Sources */, + 318FB6EB1DB5600D0004E40F /* SQLiteHistoryFactories.swift in Sources */, + 285D3B681B4380B70035FD22 /* Queue.swift in Sources */, + 28E08C9A1AF44F00009BA2FA /* BrowserSchema.swift in Sources */, + 2FCAE2681ABB531100877008 /* BrowserDB.swift in Sources */, + 283586FD1C73F18E00A55435 /* CachingItemSource.swift in Sources */, + D0B29EE01F460BDF00C7CEFC /* LoginsSchema.swift in Sources */, + 2FCAE2651ABB531100877008 /* RemoteTabs.swift in Sources */, + 2FCAE2601ABB531100877008 /* Favicons.swift in Sources */, + 28126F771C2F9833006466CC /* SQLiteBookmarksBase.swift in Sources */, + 2FCAE2771ABB531100877008 /* SwiftData.swift in Sources */, + 2FCAE25F1ABB531100877008 /* Cursor.swift in Sources */, + 28302E401AF0747800521E2E /* DatabaseError.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 2FCAE2201ABB51F800877008 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 28D158AD1AFD90E500F9C065 /* TestSQLiteBookmarks.swift in Sources */, + C8611C8E1F71904C00C3DE7D /* DiskImageStoreTests.swift in Sources */, + E63F71881DB7FBE200A995C9 /* TestSQLiteMetadata.swift in Sources */, + 2FCAE2851ABB533A00877008 /* TestSQLiteRemoteClientsAndTabs.swift in Sources */, + E6F6D6A31E9E6B2A0012072B /* DeferredTestUtils.swift in Sources */, + E6BE53CD1D9177B10074909A /* TestSQLiteHistoryRecommendations.swift in Sources */, + 289A4C131C4EB90600A460E3 /* StorageTestUtils.swift in Sources */, + D37DE2C71CA356D800A5EC69 /* CertTests.swift in Sources */, + 281B029A1C037C1F005202C3 /* TestBrowserDB.swift in Sources */, + D32CACED1AE04DA1000658EB /* TestSwiftData.swift in Sources */, + 2FCAE2841ABB533A00877008 /* MockFiles.swift in Sources */, + 7BF5A1EE1B429B3100EA9DD8 /* SyncCommandsTests.swift in Sources */, + 287AC8661AF4776D00101515 /* TestSQLiteHistory.swift in Sources */, + 0BDA56B01B26B1D5008C9B96 /* TestLogins.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 390527461C874D35007E0BB7 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 315D05561E58DD60001F349B /* UIPasteboardExtensions.swift in Sources */, + 3905274F1C874D35007E0BB7 /* TodayViewController.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 397848D71ED86605004C0C0B /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 397848DE1ED86605004C0C0B /* NotificationService.swift in Sources */, + 396E38EE1EE0C6ED00CC180F /* ExtensionProfile.swift in Sources */, + 396E38CC1EE0816C00CC180F /* Profile.swift in Sources */, + 396E38E01EE0821B00CC180F /* NSUserDefaultsPrefs.swift in Sources */, + 396E38DD1EE081DA00CC180F /* SyncStatusResolver.swift in Sources */, + 396E38F21EE0C8ED00CC180F /* FxAPushMessageHandler.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 3B43E3CC1D95C48D00BBA9DB /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + E6F6D6AA1E9E6B300012072B /* DeferredTestUtils.swift in Sources */, + 3B43E3D31D95C48D00BBA9DB /* StoragePerfTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 3BFE4B031D342FB800DDF53F /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 2CA16FDE1E5F089100332277 /* SearchTest.swift in Sources */, + 0BF0DB4A1E57B05E009172B0 /* LaunchArguments.swift in Sources */, + 3B546EC01D95ECAE00BDBE36 /* ActivityStreamTest.swift in Sources */, + 3D9CA9841EF456A8002434DD /* NightModeTests.swift in Sources */, + 39EB46991E26DDB4006346E8 /* ScreenGraph.swift in Sources */, + 0B305E1B1E3A98A900BE0767 /* BookmarkingTests.swift in Sources */, + D81127D81F84023B0050841D /* PhotonActionSheetTest.swift in Sources */, + 3BFE4B501D34673D00DDF53F /* ThirdPartySearchTest.swift in Sources */, + 0BC9C9C41F26F54D000E8AB5 /* SiteLoadTest.swift in Sources */, + 2C2A5EF41E68469500F02659 /* PrivateBrowsingTest.swift in Sources */, + 3D71C89E1F5703A1008D8646 /* CopiedLinksTests.swift in Sources */, + 2CF449A51E7BFE2C00FD7595 /* NavigationTest.swift in Sources */, + 2C2A91291FA2410D002E36BD /* HistoryTests.swift in Sources */, + 3BF4B8E91D38497A00493393 /* BaseTestCase.swift in Sources */, + 3D9CAA1C1EFCD655002434DD /* ClipBoardTests.swift in Sources */, + 0B3D670E1E09B90B00C1EFC7 /* AuthenticationTest.swift in Sources */, + 2C97EC711E72C80E0092EC18 /* TopTabsTest.swift in Sources */, + 39EB469A1E26DDB4006346E8 /* FxScreenGraph.swift in Sources */, + 2CC1B3F01E9B861400814EEC /* DomainAutocompleteTest.swift in Sources */, + 0B729D371E047D6A008E6859 /* HomePageSettingsTest.swift in Sources */, + 3DEFED081F55EBE300F8620C /* TrackingProtectionTests.swift in Sources */, + 3D9CA9A81EF84D04002434DD /* NoImageTests.swift in Sources */, + 2C31A8471E8D447F00DAC646 /* HomePageSettingsUITest.swift in Sources */, + 2CEA6F791E93E3A600D4100E /* SearchSettingsUITest.swift in Sources */, + 55A747171DC46FC400CE1B57 /* HomePageUITest.swift in Sources */, + 2C31A7A91E8BFB2200DAC646 /* ReaderViewUITest.swift in Sources */, + 2C8C07771E7800EA00DC1237 /* FindInPageTest.swift in Sources */, + 2CB56E3F1E926BFB00AF7586 /* ToolbarTest.swift in Sources */, + 2C3406C81E719F00000FD889 /* SettingsTest.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 7BEB64431C7345600092C02E /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 7BEB64441C7345600092C02E /* L10nSnapshotTests.swift in Sources */, + 7BEB64451C7345600092C02E /* SnapshotHelper.swift in Sources */, + E40AFC541DD0E93300DA5651 /* L10nPermissionStringsSnapshotTests.swift in Sources */, + E40AFC6C1DD128DA00DA5651 /* L10nIntroSnapshotTests.swift in Sources */, + E40AFC651DD0F25500DA5651 /* L10nBaseSnapshotTests.swift in Sources */, + E402000A1E6493C800B45AFF /* LaunchArguments.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 7BEB64501C7345990092C02E /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 7BEB64511C7345990092C02E /* MarketingUITests.swift in Sources */, + 7BEB64521C7345990092C02E /* SnapshotHelper.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + D39FA15B1A83E0EC00EE869C /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 0B5A93221B1EB4C8004F47A2 /* ReadingListTest.swift in Sources */, + D38F03701C06387900175932 /* AuthenticationTests.swift in Sources */, + 28C8B7851C852535006D8318 /* BookmarksPanelTests.swift in Sources */, + C8611CB01F71AEBA00C3DE7D /* NoImageModeTests.swift in Sources */, + E6B4C4031C68F58B001F97E8 /* BrowserTests.swift in Sources */, + D39FA1811A83E84900EE869C /* Global.swift in Sources */, + 0B7C1E951F6097AD006A8869 /* TrackingProtectionTests.swift in Sources */, + 4F514FD41ACD8F2C0022D7EA /* HistoryTests.swift in Sources */, + C8EB60DC1F1FB9AD00F9B5B3 /* NavigationDelegateTests.swift in Sources */, + E6A92ADB1C52A8DA00743291 /* LoginInputTests.swift in Sources */, + D375A9201AE71675001B30D5 /* ViewMemoryLeakTests.swift in Sources */, + D313BE981B2F5096009EF241 /* DomainAutocompleteTests.swift in Sources */, + D3CFD3641CC5605B0064AB4A /* SecurityTests.swift in Sources */, + D3C3EB651B6FF44000388E9A /* SessionRestoreTests.swift in Sources */, + 7B24DC9C1B67B3590005766B /* ClearPrivateDataTests.swift in Sources */, + E633E37A1C2204BE001FFF6C /* LoginManagerTests.swift in Sources */, + 744B0FFE1B4F172E00100422 /* ToolbarTests.swift in Sources */, + 0BEF44631E31165700187C32 /* EarlGrey.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + E4BA8A271B4B0A1600BC2E95 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + E444F7E01B4B1C1A00FEB19B /* NSUserDefaultsPrefs.swift in Sources */, + E4BA8AA81B4B15EF00BC2E95 /* ActionRequestHandler.swift in Sources */, + E60D03281D511555002FE3F6 /* SyncStatusResolver.swift in Sources */, + E68F36AB1EA698610048CF44 /* PanelDataObservers.swift in Sources */, + E444F7E21B4B1C6800FEB19B /* SearchEngines.swift in Sources */, + E444F7DF1B4B1C1200FEB19B /* Profile.swift in Sources */, + E444F7E31B4B1C8000FEB19B /* OpenSearch.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + E4D567131ADECE2700F1EFE7 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + E4D5677C1ADECE8100F1EFE7 /* ReadingListOAuthAuthenticator.swift in Sources */, + E4D567841ADECE8100F1EFE7 /* ReadingListResponse.swift in Sources */, + E4D567921ADECE8100F1EFE7 /* ReadingListUtils.swift in Sources */, + E41F0D9B1ADFFC5600FC7387 /* ReadingListError.swift in Sources */, + E4D567761ADECE8100F1EFE7 /* ReadingListClientRecord.swift in Sources */, + E4D5678E1ADECE8100F1EFE7 /* ReadingListSynchronizer.swift in Sources */, + E4D567801ADECE8100F1EFE7 /* ReadingListRecordResponse.swift in Sources */, + E41F0D981ADFFB5400FC7387 /* ReadingListService.swift in Sources */, + E4D5678A1ADECE8100F1EFE7 /* ReadingListSQLStorage.swift in Sources */, + E4D567881ADECE8100F1EFE7 /* ReadingListServerRecord.swift in Sources */, + E4D567861ADECE8100F1EFE7 /* ReadingListServerMetadata.swift in Sources */, + E4D567901ADECE8100F1EFE7 /* ReadingListSyncMetadata.swift in Sources */, + E4D5677A1ADECE8100F1EFE7 /* ReadingListFetchSpec.swift in Sources */, + E4D567821ADECE8100F1EFE7 /* ReadingListRecordsResponse.swift in Sources */, + E4D567721ADECE8100F1EFE7 /* ReadingListClient.swift in Sources */, + E4D5678C1ADECE8100F1EFE7 /* ReadingListStorage.swift in Sources */, + E4D5676A1ADECE8100F1EFE7 /* ReadingListAuthenticator.swift in Sources */, + E4D567741ADECE8100F1EFE7 /* ReadingListClientMetadata.swift in Sources */, + E4D567701ADECE8100F1EFE7 /* ReadingListChangeAccumulator.swift in Sources */, + E4D5676E1ADECE8100F1EFE7 /* ReadingListBatchRecordResponse.swift in Sources */, + E4D5676C1ADECE8100F1EFE7 /* ReadingListBasicAuthAuthenticator.swift in Sources */, + E4D5677E1ADECE8100F1EFE7 /* ReadingListRecord.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + E4D5671E1ADECE2700F1EFE7 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + E4D567A51ADECEB800F1EFE7 /* ReadingListStorageTestCase.swift in Sources */, + E6F6D6A61E9E6B2C0012072B /* DeferredTestUtils.swift in Sources */, + E4D5679F1ADECEB800F1EFE7 /* ReadingListClientRecordTestCase.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + E69DB0701E97DEA9008A67E6 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + E69DB0B71E97E2AC008A67E6 /* SyncTelemetry.swift in Sources */, + EBA31D791F7999030055463D /* SyncPingCentre.swift in Sources */, + EBA31D7B1F79990C0055463D /* SyncTelemetryEvents.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + E69DB0791E97DEA9008A67E6 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + E610B0191EDC68FC00DC9372 /* EventTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + E6F965081B2F1CF20034B023 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + E6F6D6A71E9E6B2D0012072B /* DeferredTestUtils.swift in Sources */, + 28532BEB1C472015000072D9 /* UtilsTests.swift in Sources */, + 28532BEA1C472008000072D9 /* DeferredTests.swift in Sources */, + E61453BE1B750A1700C3F9D7 /* RollingFileLoggerTests.swift in Sources */, + 3BB54B311E68EB2B0021DAC4 /* AuthenticationKeychainInfoTests.swift in Sources */, + 39E65D271CA5B92000C63CE3 /* AsyncReducerTests.swift in Sources */, + E4E7EB6D1C4AED5E0094275D /* SupportUtilsTests.swift in Sources */, + 3964B09C1EA8F32C00F2EEF4 /* FeatureSwitchTests.swift in Sources */, + E4E25CCB1CA99E7400D0F088 /* HexExtensionsTests.swift in Sources */, + E6F965421B2F25110034B023 /* NSMutableAttributedStringExtensionsTests.swift in Sources */, + 28532BE91C471FFB000072D9 /* ResultTests.swift in Sources */, + E6F9653C1B2F1D5D0034B023 /* NSURLExtensionsTests.swift in Sources */, + 3B4AA24B1D8B8C4C00A2E008 /* ArrayExtensionTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + F84B21BA1A090F8100AAB793 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + E68E39BE1C46F42000B85F42 /* AppSettingsTableViewController.swift in Sources */, + E640E86A1C73A47C00C5F072 /* PasscodeViews.swift in Sources */, + E692E3371C46E86A009D1240 /* AppSettingsOptions.swift in Sources */, + D38F02D11C05127100175932 /* Authenticator.swift in Sources */, + E68E7ADC1CAC208200FDCA76 /* SetupPasscodeViewController.swift in Sources */, + 7B3631EA1C244FEE00D12AF9 /* Theme.swift in Sources */, + 3B61CD491F2A74EF00D38DE1 /* PocketFeed.swift in Sources */, + 396E38F11EE0C8EC00CC180F /* FxAPushMessageHandler.swift in Sources */, + E66C5B481BDA81050051AA93 /* UIImage+ImageEffects.m in Sources */, + E4CD9F6D1A77DD2800318571 /* ReaderModeStyleViewController.swift in Sources */, + E4A960061ABB9C450069AD6F /* ReaderModeUtils.swift in Sources */, + EBF47E701F7979DF00899189 /* UnifiedTelemetry.swift in Sources */, + E68F36981EA694000048CF44 /* PanelDataObservers.swift in Sources */, + 31ADB5DA1E58CEC300E87909 /* ClipboardBarDisplayHandler.swift in Sources */, + 745DAB3F1CDAB09E00D44181 /* HistoryBackButton.swift in Sources */, + 396E38F31EE0C90D00CC180F /* FxALoginHelper.swift in Sources */, + D3B6923F1B9F9A58004B87A4 /* FindInPageHelper.swift in Sources */, + D3C3696E1CC6B78800348A61 /* LocalRequestHelper.swift in Sources */, + E4B423DD1ABA0318007E66C8 /* ReaderModeHandlers.swift in Sources */, + 7B0B1B9D1C1B6A3F00DF4AB5 /* InstructionsViewController.swift in Sources */, + D308E4E41A5306F500842685 /* SearchEngines.swift in Sources */, + 3BCE6D3C1CEB9E4D0080928C /* ThirdPartySearchAlerts.swift in Sources */, + 745DAB301CDAAFAA00D44181 /* RecentlyClosedTabsPanel.swift in Sources */, + C45F44691D087DB600CB7EF0 /* TopTabsViewController.swift in Sources */, + 0BF0DB941A8545800039F300 /* URLBarView.swift in Sources */, + C817B34D1FC609500086018E /* UIScrollViewSwizzled.swift in Sources */, + 39F819C61FD70F5D009E31E4 /* TabEventHandlers.swift in Sources */, + E65607611C08B4E200534B02 /* SearchInputView.swift in Sources */, + EB2A63341F3B49A7004EF8B0 /* ContentBlockerHelper.swift in Sources */, + FA6B2AC21D41F02D00429414 /* Punycode.swift in Sources */, + D301AAEE1A3A55B70078DD1D /* TabTrayController.swift in Sources */, + 0B3E7D951B27A7CE00E2E84D /* AboutHomeHandler.swift in Sources */, + D331DFCA1CB6E9EE009B5DA2 /* OldStrings.swift in Sources */, + D3BE7B461B054F8600641031 /* TestAppDelegate.swift in Sources */, + 3BB50E111D6274CD004B33DF /* ActivityStreamTopSitesCell.swift in Sources */, + 0B62EFD21AD63CD100ACB9CD /* Clearables.swift in Sources */, + 7482205C1DBAB56300EEEA72 /* MailProviders.swift in Sources */, + C40046FA1CF8E0B200B08303 /* BackForwardListAnimator.swift in Sources */, + DD31E0FB1B382B520077078A /* TabPrintPageRenderer.swift in Sources */, + D81E45131F82C56D004EFFBA /* NewTabContentSettingsViewController.swift in Sources */, + E4CD9E911A6897FB00318571 /* ReaderMode.swift in Sources */, + E68E7ADE1CAC208A00FDCA76 /* RemovePasscodeViewController.swift in Sources */, + 74C027451B2A348C001B1E88 /* SessionData.swift in Sources */, + D314E7F71A37B98700426A76 /* TabToolbar.swift in Sources */, + E60D03181D511398002FE3F6 /* SyncStatusResolver.swift in Sources */, + E689C6FA1E0C6E98008BAADB /* FxAContentViewController.swift in Sources */, + C4E3983D1D21F1E7004E89BA /* TopTabsViews.swift in Sources */, + 3B0943811D6CC4FC004F24E1 /* FilledPageControl.swift in Sources */, + FA9293D41D6580E100AC8D33 /* QRCodeViewController.swift in Sources */, + 0BB5B2891AC0A2B90052877D /* Toolbar.swift in Sources */, + E6108FF91C84E91C005D25E8 /* BasePasscodeViewController.swift in Sources */, + E4CD9F2D1A6DC91200318571 /* TabLocationView.swift in Sources */, + 0BB5B2881AC0A2B90052877D /* SnackBar.swift in Sources */, + 7BEFC6801BFF68C30059C952 /* QuickActions.swift in Sources */, + 0BF1B7E31AC60DEA00A7B407 /* InsetButton.swift in Sources */, + D31CF65C1CC1959A001D0BD0 /* PrivilegedRequest.swift in Sources */, + E650755C1E37F747006961AC /* Swizzling.m in Sources */, + 74821FC51DB56A2500EEEA72 /* OpenWithSettingsViewController.swift in Sources */, + D3B6923D1B9F9444004B87A4 /* FindInPageBar.swift in Sources */, + 2F44FC721A9E840300FD20CC /* SettingsNavigationController.swift in Sources */, + 74821F8E1DAD8F1400EEEA72 /* ActivityStreamHighlightCell.swift in Sources */, + E69E06BA1C76173D00D0F926 /* RequirePasscodeIntervalViewController.swift in Sources */, + D3BA7E0E1B0E934F00153782 /* ContextMenuHelper.swift in Sources */, + E660BE061BB0666D009AC090 /* InnerStrokedView.swift in Sources */, + 0B1C05D71A798B1F004C78B0 /* UIImageViewAligned.m in Sources */, + 0BB5B30B1AC0AD1F0052877D /* LoginsHelper.swift in Sources */, + E69E06C91C76198000D0F926 /* AuthenticationManagerConstants.swift in Sources */, + E47536ED1C85412C0022CC69 /* PrintHelper.swift in Sources */, + 392ED7E61D0AEFEF009D9B62 /* HomePageAccessors.swift in Sources */, + 7BA8D1C71BA037F500C8AE9E /* OpenInHelper.swift in Sources */, + E4B423BE1AB9FE6A007E66C8 /* ReaderModeCache.swift in Sources */, + 74E36D781B71323500D69DA1 /* SettingsContentViewController.swift in Sources */, + 742A56391D80B54A00BDB803 /* PhotonActionSheet.swift in Sources */, + C4EFEECF1CEBB6F2009762A4 /* BackForwardTableViewCell.swift in Sources */, + E689C7301E0C7617008BAADB /* NSAttributedStringExtensions.swift in Sources */, + E64ED8FA1BC55AE300DAF864 /* UIAlertControllerExtensions.swift in Sources */, + 282DA4731A68C1E700A406E2 /* OpenSearch.swift in Sources */, + F35B8D2F1D638408008E3D61 /* SessionRestoreHandler.swift in Sources */, + E63ED8E11BFD25580097D08E /* LoginListViewController.swift in Sources */, + 2F44FCC71A9E8CF500FD20CC /* SearchSettingsTableViewController.swift in Sources */, + 0BF42D371A7C0B8E00889E28 /* FaviconManager.swift in Sources */, + 39A359E41BFCCE94006B9E87 /* UserActivityHandler.swift in Sources */, + D88FDAAF1F4E2BA000FD9709 /* PhotonActionSheetAnimator.swift in Sources */, + E698FFDA1B4AADF40001F623 /* TabScrollController.swift in Sources */, + 7BEDD29B1CB7CB24004B51F3 /* JumpAndSpinAnimator.swift in Sources */, + D34510881ACF415700EC27F0 /* SearchLoader.swift in Sources */, + E65075521E37F6D1006961AC /* UIViewExtensions.swift in Sources */, + C8F457A81F1FD75A000CB895 /* BrowserViewController+WKNavigationDelegate.swift in Sources */, + E6927EC01C7B6FB800D03F75 /* ErrorToast.swift in Sources */, + D30B101E1AA7F9C600C01CA3 /* HomePanels.swift in Sources */, + F84B22041A0910F600AAB793 /* AppDelegate.swift in Sources */, + E653422D1C5944F90039DD9E /* BrowserPrompts.swift in Sources */, + 2FDE87FE1ABB3817005317B1 /* RemoteTabsPanel.swift in Sources */, + 39DD030D1CD53E1900BC09B3 /* HomePageHelper.swift in Sources */, + C4F3B29A1CFCF93A00966259 /* ButtonToast.swift in Sources */, + E4A961341AC051360069AD6F /* ReadabilityTabHelper.swift in Sources */, + D31A0FC71A65D6D000DC8C7E /* SearchSuggestClient.swift in Sources */, + A83E5AB71C1D993D0026D912 /* UIPasteboardExtensions.swift in Sources */, + E650754E1E37F6AE006961AC /* GeometryExtensions.swift in Sources */, + D3972BF41C22412B00035B87 /* TitleActivityItemProvider.swift in Sources */, + D38A1BEE1A9FA2CA00F6A386 /* SiteTableViewController.swift in Sources */, + 7BA0601B1C0F4DE200DFADB6 /* TabPeekViewController.swift in Sources */, + E6D8D5E71B569D70009E5A58 /* BrowserTrayAnimators.swift in Sources */, + E65075611E37F77D006961AC /* MenuHelper.swift in Sources */, + E63ED7D81BFCD9990097D08E /* LoginTableViewCell.swift in Sources */, + 3BB50E201D627539004B33DF /* ActivityStreamPanel.swift in Sources */, + F84B22241A09122500AAB793 /* HomePanelViewController.swift in Sources */, + 39455F771FC83F430088A22C /* TabEventHandler.swift in Sources */, + E47616C71AB74CA600E7DD25 /* ReaderModeBarView.swift in Sources */, + D88FDA9F1F4E2B9200FD9709 /* PhotonActionSheetProtocol.swift in Sources */, + E65075511E37F6D1006961AC /* NSURLExtensionsMailTo.swift in Sources */, + D83822001FC7961D00303C12 /* DispatchQueueExtensions.swift in Sources */, + 2F44FCC51A9E85E900FD20CC /* SettingsTableViewController.swift in Sources */, + A9072B801D07B34100459960 /* NoImageModeHelper.swift in Sources */, + E660BDD91BB06521009AC090 /* TabsButton.swift in Sources */, + 2F44FCCB1A9E972E00FD20CC /* SearchEnginePicker.swift in Sources */, + D02816C21ECA5E2A00240CAA /* HistoryStateHelper.swift in Sources */, + E68E7ACB1CAC1D4500FDCA76 /* PagingPasscodeViewController.swift in Sources */, + D34DC8531A16C40C00D49B7B /* Profile.swift in Sources */, + 392ED6B71D06E85E009D9B62 /* NewTabChoiceViewController.swift in Sources */, + E4B3348C1BC01D8F004E2BFF /* AdjustIntegration.swift in Sources */, + E65075541E37F6FC006961AC /* DynamicFontHelper.swift in Sources */, + C4E3984C1D21F2FD004E89BA /* TabTrayButtonExtensions.swift in Sources */, + D3FEC38D1AC4B42F00494F45 /* AutocompleteTextField.swift in Sources */, + 0BD19A671A25309B0084FBA7 /* NSUserDefaultsPrefs.swift in Sources */, + E68AEDB01B18F81A00133D99 /* SwipeAnimator.swift in Sources */, + 3BF56D271CDBBE1F00AC4D75 /* SimpleToast.swift in Sources */, + D31F95E91AC226CB005C9F3B /* ScreenshotHelper.swift in Sources */, + 28EADE5D1AFC3A78007FB2FB /* UIImageViewExtensions.swift in Sources */, + D3968F251A38FE8500CEFD3B /* TabManager.swift in Sources */, + C4E398601D22C409004E89BA /* TopTabsLayout.swift in Sources */, + 2816F0001B33E05400522243 /* UIConstants.swift in Sources */, + E650755F1E37F756006961AC /* Try.m in Sources */, + 3B6889C51D66950E002AC85E /* UIImageColors.swift in Sources */, + 392ED7E41D0AEF56009D9B62 /* NewTabAccessors.swift in Sources */, + D3A9949D1A3686BD008AD1AC /* Tab.swift in Sources */, + A93067E81D0FE18E00C49C6E /* NightModeHelper.swift in Sources */, + 3B39EDCB1E16E1AA00EF029F /* CustomSearchViewController.swift in Sources */, + E65075571E37F714006961AC /* FaviconFetcher.swift in Sources */, + D863C8F21F68BFC20058D95F /* GradientProgressBar.swift in Sources */, + D3C744CD1A687D6C004CE85D /* URIFixup.swift in Sources */, + 7BC68D091CC153020043562A /* RoundedToolbar.swift in Sources */, + 39AC591A1CC574AB0042C2F5 /* HomePageSettingsViewController.swift in Sources */, + E4A961181AC041C40069AD6F /* ReadabilityService.swift in Sources */, + D3BE7B261B054D4400641031 /* main.swift in Sources */, + 0BA1E02E1B046F1E007675AF /* ErrorPageHelper.swift in Sources */, + D3A9949C1A3686BD008AD1AC /* BrowserViewController.swift in Sources */, + 59A681BDFC95A19F05E07223 /* SearchViewController.swift in Sources */, + 7BEDD2B21CB7CC0B004B51F3 /* Animatable.swift in Sources */, + E6CF28E71CB43B7900151AB3 /* SensitiveViewController.swift in Sources */, + E68E7ADA1CAC207400FDCA76 /* ChangePasscodeViewController.swift in Sources */, + E640E85E1C73A45A00C5F072 /* PasscodeEntryViewController.swift in Sources */, + 8D8251811F4DE67F00780643 /* AdvanceAccountSettingViewController.swift in Sources */, + E633E2DA1C21EAF8001FFF6C /* LoginDetailViewController.swift in Sources */, + 59A68B280D62462B85CF57A4 /* HistoryPanel.swift in Sources */, + 19DE1F671EC13B6400428B8C /* LeanplumIntegration.swift in Sources */, + E65D89181C8647420006EA35 /* AppAuthenticator.swift in Sources */, + 28FDFF0C1C1F725800840F86 /* SeparatorTableCell.swift in Sources */, + C400467C1CF4E43E00B08303 /* BackForwardListViewController.swift in Sources */, + D3972BF31C22412B00035B87 /* ShareExtensionHelper.swift in Sources */, + F35B8D2D1D6383E9008E3D61 /* SessionRestoreHelper.swift in Sources */, + 744ED5611DBFEB8D00A2B5BE /* MailtoLinkHandler.swift in Sources */, + 59A68E0B4ABBF55E14819668 /* BookmarksPanel.swift in Sources */, + 7B0B1B791C1B69C900DF4AB5 /* ClientPickerViewController.swift in Sources */, + 7B844E3D1BBDDB9D00E733A2 /* ChevronView.swift in Sources */, + E4C358551AF144BA00299F7E /* FSReadingList.m in Sources */, + 3BE7275D1CCFE8B60099189F /* CustomSearchHandler.swift in Sources */, + E692E3291C46E62D009D1240 /* AuthenticationSettingsViewController.swift in Sources */, + E663D5781BB341C4001EF30E /* ToggleButton.swift in Sources */, + C88601C61F4228AD00BBDE4F /* ContentBlockerSettingViewController.swift in Sources */, + E6327A641BF6438E008D12E0 /* DebugSettingsBundleOptions.swift in Sources */, + D3E8EF101B97BE69001900FB /* ClearPrivateDataTableViewController.swift in Sources */, + C8F457AA1F1FDD9B000CB895 /* BrowserViewController+KeyCommands.swift in Sources */, + E69DB0BD1E97E301008A67E6 /* ActivityStreamTopics.swift in Sources */, + 59A68FD5260B8D520F890F4A /* ReaderPanel.swift in Sources */, + E40FAB0C1A7ABB77009CB80D /* WebServer.swift in Sources */, + 59A68D66379CFA85C4EAF00B /* TwoLineCell.swift in Sources */, + E49943F51AE6879C00BF9DE4 /* IntroViewController.swift in Sources */, + 3B4262E61E538EC400CA681C /* MetadataParserHelper.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + F84B21CF1A090F8100AAB793 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + E4CD9F1D1A6D9C2800318571 /* WebServerTests.swift in Sources */, + D3D488591ABB54CD00A93597 /* FileAccessorTests.swift in Sources */, + E6C70E821E28314700F8DB57 /* PingCentreTests.swift in Sources */, + 0BA8964B1A250E6500C1010C /* ProfileTest.swift in Sources */, + 03CCC9181AF05E7300DBF30D /* RelativeDatesTests.swift in Sources */, + F84B21DA1A090F8100AAB793 /* ClientTests.swift in Sources */, + 281B2BEA1ADF4D90002917DC /* MockProfile.swift in Sources */, + E6F6D6AB1E9E6B330012072B /* DeferredTestUtils.swift in Sources */, + 2F697F7E1A9FD22D009E03AE /* SearchEnginesTests.swift in Sources */, + 2F44FA1B1A9D426A00FD20CC /* TestHashExtensions.swift in Sources */, + 3B39EDBA1E16E18900EF029F /* CustomSearchEnginesTest.swift in Sources */, + D3FA777B1A43B2990010CD32 /* SearchTests.swift in Sources */, + D3BA41681BD82F2200DA5457 /* XCTestCaseExtensions.swift in Sources */, + 554867231DC3935A00183DAA /* HomePageTests.swift in Sources */, + 3BFCBF201E04B1C50070C042 /* UIImageViewExtensionsTests.swift in Sources */, + 8DCD3BCD1ED5B7FA00446D38 /* FxADeepLinkingTests.swift in Sources */, + 3943A81D1E9807C700D4F6DC /* FxAPushMessageTest.swift in Sources */, + E60D032A1D5118DB002FE3F6 /* SyncStatusResolverTests.swift in Sources */, + E683F0A61E92E0820035D990 /* MockableHistory.swift in Sources */, + A83E5B1E1C1DAAAA0026D912 /* UIPasteboardExtensions.swift in Sources */, + 0BF42D4F1A7CD09600889E28 /* TestFavicons.swift in Sources */, + 7BBFEE741BB405D900A305AA /* TabManagerTests.swift in Sources */, + 39236E721FCC600200A38F1B /* TabEventHandlerTests.swift in Sources */, + 0BA8964C1A250E6500C1010C /* TestBookmarks.swift in Sources */, + 2F13E79B1AC0C02700D75081 /* StringExtensionsTests.swift in Sources */, + 2FDB10931A9FBEC5006CF312 /* PrefsTests.swift in Sources */, + 4A59B58AD11B5EE1F80BBDEB /* TestHistory.swift in Sources */, + A83E5B1D1C1DA8D80026D912 /* UIPasteboardExtensionsTests.swift in Sources */, + E61D11681EAF8F43008A305B /* PanelDataObserversTests.swift in Sources */, + 28D52E2F1BCDF53900187A1D /* ResetTests.swift in Sources */, + 3B61CD591F2A750800D38DE1 /* PocketFeedTests.swift in Sources */, + E696FE511C47F86E00EC007C /* AuthenticatorTests.swift in Sources */, + 3B6F40181DC7849C00656CC6 /* ActivityStreamTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + F84B22451A0920C600AAB793 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + FA6B2AC41D41F02D00429414 /* Punycode.swift in Sources */, + D38B2D8A1A8D98D00040E6B5 /* SearchEngines.swift in Sources */, + D3C744CF1A687D6C004CE85D /* URIFixup.swift in Sources */, + D38B2D8C1A8D98D90040E6B5 /* OpenSearch.swift in Sources */, + E418D0D91A251B3200CAE47A /* Profile.swift in Sources */, + F8708D321A0970B70051AB07 /* ShareViewController.swift in Sources */, + E68F36AD1EA698650048CF44 /* PanelDataObservers.swift in Sources */, + 28CDA55C1A43C37C005C318C /* NSUserDefaultsPrefs.swift in Sources */, + E60D03271D511554002FE3F6 /* SyncStatusResolver.swift in Sources */, + E41A7D4B1A1BE04500245963 /* InitialViewController.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + F84B22581A09210A00AAB793 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + D38B2D8B1A8D98D10040E6B5 /* SearchEngines.swift in Sources */, + D3C744D01A687D6C004CE85D /* URIFixup.swift in Sources */, + E60D03261D511554002FE3F6 /* SyncStatusResolver.swift in Sources */, + D38B2D8D1A8D98DA0040E6B5 /* OpenSearch.swift in Sources */, + E4BCAAB91B0537E300855D82 /* InstructionsViewController.swift in Sources */, + E42CCDE81A23A73D00B794D3 /* Profile.swift in Sources */, + E68F36AC1EA698610048CF44 /* PanelDataObservers.swift in Sources */, + E4BCAAD91B05380B00855D82 /* ClientPickerViewController.swift in Sources */, + FA6B2AC31D41F02D00429414 /* Punycode.swift in Sources */, + F8708D2A1A0970B40051AB07 /* ActionViewController.swift in Sources */, + 28CDA5641A43C37D005C318C /* NSUserDefaultsPrefs.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 0B742CCE1B32493800EE9264 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + name = sqlcipher; + targetProxy = 0B742CCD1B32493800EE9264 /* PBXContainerItemProxy */; + }; + 2827316B1ABC9BE700AA1954 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 2827315D1ABC9BE600AA1954 /* Sync */; + targetProxy = 2827316A1ABC9BE700AA1954 /* PBXContainerItemProxy */; + }; + 282731741ABC9BE700AA1954 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 2827315D1ABC9BE600AA1954 /* Sync */; + targetProxy = 282731731ABC9BE700AA1954 /* PBXContainerItemProxy */; + }; + 288A2D9C1AB8B3260023ABC3 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 288A2D851AB8B3260023ABC3 /* Shared */; + targetProxy = 288A2D9B1AB8B3260023ABC3 /* PBXContainerItemProxy */; + }; + 28ECD9B11BA1F07000D829DA /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = F84B21BD1A090F8100AAB793 /* Client */; + targetProxy = 28ECD9BE1BA1F12B00D829DA /* PBXContainerItemProxy */; + }; + 2F11EE501ABCAE910083902D /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 288A2D851AB8B3260023ABC3 /* Shared */; + targetProxy = 2F11EE4F1ABCAE910083902D /* PBXContainerItemProxy */; + }; + 2F14E1181ABB88CB00FF98DB /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + name = FxA; + targetProxy = 2F14E1171ABB88CB00FF98DB /* PBXContainerItemProxy */; + }; + 2F14E1341ABB88CF00FF98DB /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 288A2D851AB8B3260023ABC3 /* Shared */; + targetProxy = 2F14E1331ABB88CF00FF98DB /* PBXContainerItemProxy */; + }; + 2F67C5281BB0D33000E7B73A /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 2FA435FA1ABB83B4008031D1 /* Account */; + targetProxy = 2FC87C3C1BC45E67001BAE77 /* PBXContainerItemProxy */; + }; + 2F77F69D1ABCAEFE00484F3A /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + name = FxA; + targetProxy = 2F77F69C1ABCAEFE00484F3A /* PBXContainerItemProxy */; + }; + 2F77F6B91ABCAF0700484F3A /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 2FA435FA1ABB83B4008031D1 /* Account */; + targetProxy = 2F77F6B81ABCAF0700484F3A /* PBXContainerItemProxy */; + }; + 2F9A72321ABB856100F9F05D /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 2FCAE2191ABB51F800877008 /* Storage */; + targetProxy = 2F9A72311ABB856100F9F05D /* PBXContainerItemProxy */; + }; + 2F9A72341ABB856A00F9F05D /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 2FCAE2191ABB51F800877008 /* Storage */; + targetProxy = 2F9A72331ABB856A00F9F05D /* PBXContainerItemProxy */; + }; + 2FA436111ABB83B4008031D1 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 2FA435FA1ABB83B4008031D1 /* Account */; + targetProxy = 2FA436101ABB83B4008031D1 /* PBXContainerItemProxy */; + }; + 2FA4361D1ABB83DD008031D1 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 288A2D851AB8B3260023ABC3 /* Shared */; + targetProxy = 2FA4361C1ABB83DD008031D1 /* PBXContainerItemProxy */; + }; + 2FA436441ABB84CB008031D1 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + name = FxA; + targetProxy = 2FA436431ABB84CB008031D1 /* PBXContainerItemProxy */; + }; + 2FCAE2271ABB51F800877008 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 2FCAE2191ABB51F800877008 /* Storage */; + targetProxy = 2FCAE2261ABB51F800877008 /* PBXContainerItemProxy */; + }; + 2FCAE2291ABB51F800877008 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = F84B21BD1A090F8100AAB793 /* Client */; + targetProxy = 2FCAE2281ABB51F800877008 /* PBXContainerItemProxy */; + }; + 2FCAE2301ABB51F800877008 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 2FCAE2191ABB51F800877008 /* Storage */; + targetProxy = 2FCAE22F1ABB51F800877008 /* PBXContainerItemProxy */; + }; + 2FCAE23C1ABB520700877008 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 288A2D851AB8B3260023ABC3 /* Shared */; + targetProxy = 2FCAE23B1ABB520700877008 /* PBXContainerItemProxy */; + }; + 390527551C874D35007E0BB7 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 390527491C874D35007E0BB7 /* Today */; + targetProxy = 390527541C874D35007E0BB7 /* PBXContainerItemProxy */; + }; + 397848E11ED86605004C0C0B /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 397848DA1ED86605004C0C0B /* NotificationService */; + targetProxy = 397848E01ED86605004C0C0B /* PBXContainerItemProxy */; + }; + 3B43E3D61D95C48D00BBA9DB /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = F84B21BD1A090F8100AAB793 /* Client */; + targetProxy = 3B43E3D51D95C48D00BBA9DB /* PBXContainerItemProxy */; + }; + 3BFE4B0D1D342FB900DDF53F /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = F84B21BD1A090F8100AAB793 /* Client */; + targetProxy = 3BFE4B0C1D342FB900DDF53F /* PBXContainerItemProxy */; + }; + 7B10AAF01E3A58C80002DD08 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 288A2D851AB8B3260023ABC3 /* Shared */; + targetProxy = 7B10AAEF1E3A58C80002DD08 /* PBXContainerItemProxy */; + }; + 7B9A62F91C400303005C83DC /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + name = "SQLite iOS"; + targetProxy = 7B9A62F81C400303005C83DC /* PBXContainerItemProxy */; + }; + 7B9BF92F1E435DE400CB24F4 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 2FCAE2231ABB51F800877008 /* StorageTests */; + targetProxy = 7B9BF92E1E435DE400CB24F4 /* PBXContainerItemProxy */; + }; + 7BEB64411C7345600092C02E /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = F84B21BD1A090F8100AAB793 /* Client */; + targetProxy = 7BEB64421C7345600092C02E /* PBXContainerItemProxy */; + }; + 7BEB645D1C7346100092C02E /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = F84B21BD1A090F8100AAB793 /* Client */; + targetProxy = 7BEB645C1C7346100092C02E /* PBXContainerItemProxy */; + }; + D09A0CDF1FAA2577009A0273 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 288A2D851AB8B3260023ABC3 /* Shared */; + targetProxy = D09A0CDE1FAA2577009A0273 /* PBXContainerItemProxy */; + }; + D09A0CE11FAA25C5009A0273 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 288A2D851AB8B3260023ABC3 /* Shared */; + targetProxy = D09A0CE01FAA25C5009A0273 /* PBXContainerItemProxy */; + }; + D09A0CE51FAA2C18009A0273 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 288A2D851AB8B3260023ABC3 /* Shared */; + targetProxy = D09A0CE41FAA2C18009A0273 /* PBXContainerItemProxy */; + }; + D09A0CE71FAA2C1B009A0273 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 2FCAE2191ABB51F800877008 /* Storage */; + targetProxy = D09A0CE61FAA2C1B009A0273 /* PBXContainerItemProxy */; + }; + D09A0CE91FAA2C1E009A0273 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 2827315D1ABC9BE600AA1954 /* Sync */; + targetProxy = D09A0CE81FAA2C1E009A0273 /* PBXContainerItemProxy */; + }; + D09A0CEB1FAA2C38009A0273 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = E4D567171ADECE2700F1EFE7 /* ReadingList */; + targetProxy = D09A0CEA1FAA2C38009A0273 /* PBXContainerItemProxy */; + }; + D09A0CED1FAA2C4C009A0273 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 288A2D851AB8B3260023ABC3 /* Shared */; + targetProxy = D09A0CEC1FAA2C4C009A0273 /* PBXContainerItemProxy */; + }; + D09A0CEF1FAA2C66009A0273 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + name = FxA; + targetProxy = D09A0CEE1FAA2C66009A0273 /* PBXContainerItemProxy */; + }; + D09A0CF11FAA2C7E009A0273 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 288A2D851AB8B3260023ABC3 /* Shared */; + targetProxy = D09A0CF01FAA2C7E009A0273 /* PBXContainerItemProxy */; + }; + D09A0CF31FAA2C81009A0273 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 2827315D1ABC9BE600AA1954 /* Sync */; + targetProxy = D09A0CF21FAA2C81009A0273 /* PBXContainerItemProxy */; + }; + D09A0CF51FAA2C99009A0273 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = E4D567171ADECE2700F1EFE7 /* ReadingList */; + targetProxy = D09A0CF41FAA2C99009A0273 /* PBXContainerItemProxy */; + }; + D09A0CF71FAA2CB2009A0273 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + name = FxA; + targetProxy = D09A0CF61FAA2CB2009A0273 /* PBXContainerItemProxy */; + }; + D09A0CF91FAA2CBF009A0273 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 288A2D851AB8B3260023ABC3 /* Shared */; + targetProxy = D09A0CF81FAA2CBF009A0273 /* PBXContainerItemProxy */; + }; + D09A0CFB1FAA2CC2009A0273 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 2827315D1ABC9BE600AA1954 /* Sync */; + targetProxy = D09A0CFA1FAA2CC2009A0273 /* PBXContainerItemProxy */; + }; + D09A0CFD1FAA2CC5009A0273 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = E4D567171ADECE2700F1EFE7 /* ReadingList */; + targetProxy = D09A0CFC1FAA2CC5009A0273 /* PBXContainerItemProxy */; + }; + D0EA58131FA836CA00D03ED1 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 2827315D1ABC9BE600AA1954 /* Sync */; + targetProxy = D0EA58121FA836CA00D03ED1 /* PBXContainerItemProxy */; + }; + D0EA58151FA836CE00D03ED1 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 2FCAE2191ABB51F800877008 /* Storage */; + targetProxy = D0EA58141FA836CE00D03ED1 /* PBXContainerItemProxy */; + }; + D0EA58171FA836D200D03ED1 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 288A2D851AB8B3260023ABC3 /* Shared */; + targetProxy = D0EA58161FA836D200D03ED1 /* PBXContainerItemProxy */; + }; + D39FA1661A83E0EC00EE869C /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = F84B21BD1A090F8100AAB793 /* Client */; + targetProxy = D39FA1651A83E0EC00EE869C /* PBXContainerItemProxy */; + }; + E4A888191A95679500CDC337 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + name = FxA; + targetProxy = E4A888181A95679500CDC337 /* PBXContainerItemProxy */; + }; + E4BA8A341B4B0A1700BC2E95 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = E4BA8A2A1B4B0A1600BC2E95 /* ViewLater */; + targetProxy = E4BA8A331B4B0A1700BC2E95 /* PBXContainerItemProxy */; + }; + E4D567251ADECE2700F1EFE7 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = E4D567171ADECE2700F1EFE7 /* ReadingList */; + targetProxy = E4D567241ADECE2700F1EFE7 /* PBXContainerItemProxy */; + }; + E4D5672E1ADECE2800F1EFE7 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = E4D567171ADECE2700F1EFE7 /* ReadingList */; + targetProxy = E4D5672D1ADECE2800F1EFE7 /* PBXContainerItemProxy */; + }; + E4EE05B01BA3A0A10021B3A7 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 2FCAE2191ABB51F800877008 /* Storage */; + targetProxy = E4EE05AF1BA3A0A10021B3A7 /* PBXContainerItemProxy */; + }; + E60222E61C6E5A770061C436 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + name = FxA; + targetProxy = E60222E51C6E5A770061C436 /* PBXContainerItemProxy */; + }; + E69DB0801E97DEAA008A67E6 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = E69DB0741E97DEA9008A67E6 /* SyncTelemetry */; + targetProxy = E69DB07F1E97DEAA008A67E6 /* PBXContainerItemProxy */; + }; + E69DB0821E97DEAA008A67E6 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = F84B21BD1A090F8100AAB793 /* Client */; + targetProxy = E69DB0811E97DEAA008A67E6 /* PBXContainerItemProxy */; + }; + E69DB0891E97DEAA008A67E6 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = E69DB0741E97DEA9008A67E6 /* SyncTelemetry */; + targetProxy = E69DB0881E97DEAA008A67E6 /* PBXContainerItemProxy */; + }; + E69DB0A71E97DEF6008A67E6 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = E69DB0741E97DEA9008A67E6 /* SyncTelemetry */; + targetProxy = E69DB0A61E97DEF6008A67E6 /* PBXContainerItemProxy */; + }; + E6F965141B2F1CF20034B023 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 288A2D851AB8B3260023ABC3 /* Shared */; + targetProxy = E63CD1B11B31B66400A63AFF /* PBXContainerItemProxy */; + }; + E6F9653A1B2F1D330034B023 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = F84B21BD1A090F8100AAB793 /* Client */; + targetProxy = E63CD1B21B31B66400A63AFF /* PBXContainerItemProxy */; + }; + F84B21D51A090F8100AAB793 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = F84B21BD1A090F8100AAB793 /* Client */; + targetProxy = F84B21D41A090F8100AAB793 /* PBXContainerItemProxy */; + }; + F84B22521A0920C600AAB793 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = F84B22481A0920C600AAB793 /* ShareTo */; + targetProxy = F84B22511A0920C600AAB793 /* PBXContainerItemProxy */; + }; + F84B22651A09210A00AAB793 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = F84B225B1A09210A00AAB793 /* SendTo */; + targetProxy = F84B22641A09210A00AAB793 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 7B2142F31E5E03F800CDD3FC /* InfoPlist.strings */ = { + isa = PBXVariantGroup; + children = ( + 7B2142F41E5E03F800CDD3FC /* en */, + 7B2143021E5F224A00CDD3FC /* Base */, + ); + name = InfoPlist.strings; + sourceTree = ""; + }; + 7B2142F61E5E040200CDD3FC /* InfoPlist.strings */ = { + isa = PBXVariantGroup; + children = ( + 7B2142F71E5E040200CDD3FC /* en */, + 7B2143031E5F225200CDD3FC /* Base */, + ); + name = InfoPlist.strings; + sourceTree = ""; + }; + 7B2142FC1E5E055000CDD3FC /* InfoPlist.strings */ = { + isa = PBXVariantGroup; + children = ( + 7B2142FD1E5E055000CDD3FC /* en */, + ); + name = InfoPlist.strings; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 282731771ABC9BE800AA1954 /* Fennec */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = NO; + APPLICATION_EXTENSION_API_ONLY = YES; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_BITCODE = NO; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Carthage/Build/iOS", + "$(BUILD_DIR)/Release$(EFFECTIVE_PLATFORM_NAME)", + ); + INFOPLIST_FILE = Sync/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "org.mozilla.$(PRODUCT_NAME:rfc1034identifier)"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SWIFT_OBJC_BRIDGING_HEADER = "$SRCROOT/Sync/Sync-Bridging-Header.h"; + SWIFT_VERSION = 3.0; + }; + name = Fennec; + }; + 2827317A1ABC9BE800AA1954 /* Fennec */ = { + isa = XCBuildConfiguration; + buildSettings = { + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Carthage/Build/iOS", + "$(BUILD_DIR)/Release$(EFFECTIVE_PLATFORM_NAME)", + ); + INFOPLIST_FILE = SyncTests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "org.mozilla.$(PRODUCT_NAME:rfc1034identifier)"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "$SRCROOT/SyncTests/SyncTests-Bridging-Header.h"; + SWIFT_VERSION = 3.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Client.app/Client"; + }; + name = Fennec; + }; + 288A2DA01AB8B3260023ABC3 /* Fennec */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = NO; + APPLICATION_EXTENSION_API_ONLY = YES; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + INFOPLIST_FILE = "Shared/Supporting Files/Info.plist"; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "org.mozilla.$(PRODUCT_NAME:rfc1034identifier)"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SWIFT_OBJC_BRIDGING_HEADER = "$SRCROOT/Shared/Shared-Bridging-Header.h"; + SWIFT_VERSION = 3.0; + }; + name = Fennec; + }; + 2FA436151ABB83B4008031D1 /* Fennec */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = NO; + APPLICATION_EXTENSION_API_ONLY = YES; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_BITCODE = NO; + FRAMEWORK_SEARCH_PATHS = ( + "$(PROJECT_DIR)/Carthage/Build/iOS", + "$(BUILD_DIR)/Release$(EFFECTIVE_PLATFORM_NAME)", + ); + HEADER_SEARCH_PATHS = ( + /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, + "$(SRCROOT)", + "$(SDKROOT)/usr/include/libxml2", + "$(BUILD_DIR)/Release$(EFFECTIVE_PLATFORM_NAME)/include/**", + ); + INFOPLIST_FILE = Account/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "org.mozilla.$(PRODUCT_NAME:rfc1034identifier)"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SWIFT_OBJC_BRIDGING_HEADER = "$SRCROOT/Account/Account-Bridging-Header.h"; + SWIFT_VERSION = 3.0; + }; + name = Fennec; + }; + 2FA436191ABB83B4008031D1 /* Fennec */ = { + isa = XCBuildConfiguration; + buildSettings = { + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Carthage/Build/iOS", + "$(BUILD_DIR)/Release$(EFFECTIVE_PLATFORM_NAME)", + ); + INFOPLIST_FILE = AccountTests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "org.mozilla.$(PRODUCT_NAME:rfc1034identifier)"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "$SRCROOT/Account/Account-Bridging-Header.h"; + SWIFT_VERSION = 3.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Client.app/Client"; + }; + name = Fennec; + }; + 2FCAE2341ABB51F900877008 /* Fennec */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = NO; + APPLICATION_EXTENSION_API_ONLY = YES; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + INFOPLIST_FILE = Storage/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + OTHER_CFLAGS = "-DSQLITE_HAS_CODEC"; + PRODUCT_BUNDLE_IDENTIFIER = "org.mozilla.$(PRODUCT_NAME:rfc1034identifier)"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SWIFT_OBJC_BRIDGING_HEADER = "$SRCROOT/Storage/Storage-Bridging-Header.h"; + SWIFT_VERSION = 3.0; + }; + name = Fennec; + }; + 2FCAE2381ABB51F900877008 /* Fennec */ = { + isa = XCBuildConfiguration; + buildSettings = { + INFOPLIST_FILE = StorageTests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "org.mozilla.$(PRODUCT_NAME:rfc1034identifier)"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "$SRCROOT/Storage/Storage-Bridging-Header.h"; + SWIFT_VERSION = 3.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Client.app/Client"; + }; + name = Fennec; + }; + 39409A401C90E68300DAE683 /* Fennec */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = NO; + APPLICATION_EXTENSION_API_ONLY = YES; + CODE_SIGN_ENTITLEMENTS = "$(SRCROOT)/Extensions/Entitlements/Fennec.entitlements"; + CODE_SIGN_IDENTITY = "iPhone Developer"; + DEVELOPMENT_TEAM = 43AQ936H96; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Extensions/Today/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 10.3; + LD_RUNPATH_SEARCH_PATHS = "@executable_path/../../Frameworks"; + OTHER_SWIFT_FLAGS = "-DMOZ_CHANNEL_FENNEC -DMOZ_TARGET_TODAY"; + PRODUCT_BUNDLE_IDENTIFIER = "$(MOZ_BUNDLE_ID).$(PRODUCT_NAME:rfc1034identifier)"; + PRODUCT_NAME = Today; + PROVISIONING_PROFILE_SPECIFIER = "Fennec Today Development"; + SWIFT_VERSION = 3.0; + }; + name = Fennec; + }; + 39409A411C90E68300DAE683 /* Firefox */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = NO; + APPLICATION_EXTENSION_API_ONLY = YES; + CODE_SIGN_ENTITLEMENTS = "$(SRCROOT)/Extensions/Entitlements/Firefox.entitlements"; + CODE_SIGN_IDENTITY = "iPhone Developer"; + DEVELOPMENT_TEAM = ""; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Extensions/Today/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 10.3; + LD_RUNPATH_SEARCH_PATHS = "@executable_path/../../Frameworks"; + OTHER_SWIFT_FLAGS = "-DMOZ_CHANNEL_RELEASE -DMOZ_TARGET_TODAY"; + PRODUCT_BUNDLE_IDENTIFIER = "$(MOZ_BUNDLE_ID).$(PRODUCT_NAME:rfc1034identifier)"; + PRODUCT_NAME = Today; + PROVISIONING_PROFILE_SPECIFIER = "Firefox Today Development"; + SWIFT_VERSION = 3.0; + }; + name = Firefox; + }; + 39409A421C90E68300DAE683 /* FirefoxBeta */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = NO; + APPLICATION_EXTENSION_API_ONLY = YES; + CODE_SIGN_ENTITLEMENTS = "$(SRCROOT)/Extensions/Entitlements/FirefoxBeta.entitlements"; + CODE_SIGN_IDENTITY = "iPhone Developer"; + DEVELOPMENT_TEAM = ""; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Extensions/Today/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 10.3; + LD_RUNPATH_SEARCH_PATHS = "@executable_path/../../Frameworks"; + OTHER_SWIFT_FLAGS = "-DMOZ_CHANNEL_BETA -DMOZ_TARGET_TODAY"; + PRODUCT_BUNDLE_IDENTIFIER = "$(MOZ_BUNDLE_ID).$(PRODUCT_NAME:rfc1034identifier)"; + PRODUCT_NAME = Today; + PROVISIONING_PROFILE_SPECIFIER = "Firefox Beta Today Development"; + SWIFT_VERSION = 3.0; + }; + name = FirefoxBeta; + }; + 3958DAB01ED98DCB0054AA27 /* Fennec_Enterprise */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = NO; + CODE_SIGN_ENTITLEMENTS = "$(SRCROOT)/Extensions/Entitlements/FennecEnterprise.entitlements"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + DEVELOPMENT_TEAM = 9G8J6YA743; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Extensions/NotificationService/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 10.3; + LD_RUNPATH_SEARCH_PATHS = "@executable_path/../../Frameworks"; + OTHER_LDFLAGS = ""; + OTHER_SWIFT_FLAGS = "-DMOZ_CHANNEL_FENNEC -DMOZ_TARGET_NOTIFICATIONSERVICE"; + PRODUCT_BUNDLE_IDENTIFIER = org.mozilla.ios.FennecEnterprise.NotificationService; + PRODUCT_NAME = NotificationService; + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 3.0; + }; + name = Fennec_Enterprise; + }; + 397848E31ED86605004C0C0B /* Fennec */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = NO; + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_ENTITLEMENTS = "$(SRCROOT)/Extensions/Entitlements/Fennec.entitlements"; + CODE_SIGN_IDENTITY = "iPhone Developer"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + DEVELOPMENT_TEAM = 43AQ936H96; + ENABLE_BITCODE = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + INFOPLIST_FILE = Extensions/NotificationService/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 10.3; + LD_RUNPATH_SEARCH_PATHS = "@executable_path/../../Frameworks"; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + OTHER_LDFLAGS = ""; + OTHER_SWIFT_FLAGS = "-DMOZ_CHANNEL_FENNEC -DMOZ_TARGET_NOTIFICATIONSERVICE"; + PRODUCT_BUNDLE_IDENTIFIER = org.mozilla.ios.Fennec.NotificationService; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE = "2ca221e2-03db-45b5-ad98-41b0caa46000"; + PROVISIONING_PROFILE_SPECIFIER = "Fennec NotificationService Development"; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 3.0; + }; + name = Fennec; + }; + 397848E51ED86605004C0C0B /* Firefox */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = NO; + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_ENTITLEMENTS = "$(SRCROOT)/Extensions/Entitlements/Firefox.entitlements"; + CODE_SIGN_IDENTITY = "iPhone Developer"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEVELOPMENT_TEAM = ""; + ENABLE_BITCODE = NO; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + INFOPLIST_FILE = Extensions/NotificationService/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 10.3; + LD_RUNPATH_SEARCH_PATHS = "@executable_path/../../Frameworks"; + MTL_ENABLE_DEBUG_INFO = NO; + OTHER_LDFLAGS = ""; + OTHER_SWIFT_FLAGS = "-DMOZ_CHANNEL_RELEASE -DMOZ_TARGET_NOTIFICATIONSERVICE"; + PRODUCT_BUNDLE_IDENTIFIER = org.mozilla.ios.Firefox.NotificationService; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + SWIFT_VERSION = 3.0; + VALIDATE_PRODUCT = YES; + }; + name = Firefox; + }; + 397848E61ED86605004C0C0B /* FirefoxBeta */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = NO; + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_ENTITLEMENTS = "$(SRCROOT)/Extensions/Entitlements/FirefoxBeta.entitlements"; + CODE_SIGN_IDENTITY = "iPhone Developer"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEVELOPMENT_TEAM = ""; + ENABLE_BITCODE = NO; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + INFOPLIST_FILE = Extensions/NotificationService/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 10.3; + LD_RUNPATH_SEARCH_PATHS = "@executable_path/../../Frameworks"; + MTL_ENABLE_DEBUG_INFO = NO; + OTHER_LDFLAGS = ""; + OTHER_SWIFT_FLAGS = "-DMOZ_CHANNEL_BETA -DMOZ_TARGET_NOTIFICATIONSERVICE"; + PRODUCT_BUNDLE_IDENTIFIER = org.mozilla.ios.FirefoxBeta.NotificationService2; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + SWIFT_VERSION = 3.0; + VALIDATE_PRODUCT = YES; + }; + name = FirefoxBeta; + }; + 3B43E3D71D95C48E00BBA9DB /* Fennec */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + BUNDLE_LOADER = "$(TEST_HOST)"; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_SUSPICIOUS_MOVES = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + INFOPLIST_FILE = StoragePerfTests/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 10.3; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + PRODUCT_BUNDLE_IDENTIFIER = com.mozilla.StoragePerfTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 3.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Client.app/Client"; + }; + name = Fennec; + }; + 3B43E3D81D95C48E00BBA9DB /* Firefox */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + BUNDLE_LOADER = "$(TEST_HOST)"; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_SUSPICIOUS_MOVES = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + INFOPLIST_FILE = StoragePerfTests/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 10.3; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + MTL_ENABLE_DEBUG_INFO = NO; + PRODUCT_BUNDLE_IDENTIFIER = com.mozilla.StoragePerfTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + SWIFT_VERSION = 3.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Client.app/Client"; + VALIDATE_PRODUCT = YES; + }; + name = Firefox; + }; + 3B43E3D91D95C48E00BBA9DB /* FirefoxBeta */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + BUNDLE_LOADER = "$(TEST_HOST)"; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_SUSPICIOUS_MOVES = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + INFOPLIST_FILE = StoragePerfTests/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 10.3; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + MTL_ENABLE_DEBUG_INFO = NO; + PRODUCT_BUNDLE_IDENTIFIER = com.mozilla.StoragePerfTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + SWIFT_VERSION = 3.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Client.app/Client"; + VALIDATE_PRODUCT = YES; + }; + name = FirefoxBeta; + }; + 3BFE4B0E1D342FB900DDF53F /* Fennec */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + INFOPLIST_FILE = XCUITests/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 10.3; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + PRODUCT_BUNDLE_IDENTIFIER = com.mozilla.XCUITests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + SWIFT_OBJC_BRIDGING_HEADER = "$(SRCROOT)/XCUITests/XCUITests-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 3.0; + TEST_TARGET_NAME = Client; + }; + name = Fennec; + }; + 3BFE4B0F1D342FB900DDF53F /* Firefox */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + INFOPLIST_FILE = XCUITests/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 10.3; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + MTL_ENABLE_DEBUG_INFO = NO; + PRODUCT_BUNDLE_IDENTIFIER = com.mozilla.XCUITests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + SWIFT_VERSION = 3.0; + TEST_TARGET_NAME = Client; + VALIDATE_PRODUCT = YES; + }; + name = Firefox; + }; + 3BFE4B101D342FB900DDF53F /* FirefoxBeta */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + INFOPLIST_FILE = XCUITests/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 10.3; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + MTL_ENABLE_DEBUG_INFO = NO; + PRODUCT_BUNDLE_IDENTIFIER = com.mozilla.XCUITests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + SWIFT_VERSION = 3.0; + TEST_TARGET_NAME = Client; + VALIDATE_PRODUCT = YES; + }; + name = FirefoxBeta; + }; + D39FA1681A83E0EC00EE869C /* Fennec */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + INFOPLIST_FILE = UITests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + OTHER_LDFLAGS = "-ObjC"; + PRODUCT_BUNDLE_IDENTIFIER = "org.allizom.$(PRODUCT_NAME:rfc1034identifier)"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "$(SRCROOT)/UITests/UITests-Bridging-Header.h"; + SWIFT_VERSION = 3.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Client.app/Client"; + }; + name = Fennec; + }; + D39FA1691A83E0EC00EE869C /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + INFOPLIST_FILE = UITests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + OTHER_LDFLAGS = "-ObjC"; + PRODUCT_BUNDLE_IDENTIFIER = "org.allizom.$(PRODUCT_NAME:rfc1034identifier)"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 3.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Client.app/Client"; + }; + name = Release; + }; + E448FC9D1AEE7A6000869B6C /* Firefox */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = E60961891B62B8C800DD640F /* Firefox.xcconfig */; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + ALWAYS_SEARCH_USER_PATHS = NO; + APPLICATION_EXTENSION_API_ONLY = NO; + CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_ACTIVITY_MODE = ""; + "DEBUG_ACTIVITY_MODE[sdk=iphonesimulator*]" = disable; + DEVELOPMENT_TEAM = 43AQ936H96; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Carthage/Build/iOS", + "$(BUILD_DIR)/Release$(EFFECTIVE_PLATFORM_NAME)", + ); + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + HEADER_SEARCH_PATHS = ( + "$(inherited)", + /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, + "$(SRCROOT)", + "$(SDKROOT)/usr/include/libxml2", + "$(BUILD_DIR)/Release$(EFFECTIVE_PLATFORM_NAME)/include/**", + ); + IPHONEOS_DEPLOYMENT_TARGET = 10.3; + LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks"; + LIBRARY_SEARCH_PATHS = "$(BUILD_DIR)/Release$(EFFECTIVE_PLATFORM_NAME)"; + MOZ_INTERNAL_URL_SCHEME = "firefox-internal"; + MTL_ENABLE_DEBUG_INFO = NO; + OTHER_LDFLAGS = ( + "-ObjC", + "-lxml2", + ); + OTHER_SWIFT_FLAGS = "-DMOZ_CHANNEL_RELEASE"; + PRODUCT_BUNDLE_IDENTIFIER = "$(MOZ_BUNDLE_ID)"; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = ""; + SWIFT_INCLUDE_PATHS = "$(PROJECT_DIR)/Storage/modules"; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Firefox; + }; + E448FC9E1AEE7A6000869B6C /* Firefox */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_ENTITLEMENTS = "$(SRCROOT)/Client/Entitlements/FirefoxApplication.entitlements"; + CODE_SIGN_IDENTITY = "iPhone Developer"; + DEVELOPMENT_TEAM = 43AQ936H96; + ENABLE_BITCODE = NO; + ENABLE_TESTABILITY = YES; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(BUILD_DIR)/Release$(EFFECTIVE_PLATFORM_NAME)", + "$(PROJECT_DIR)/Carthage/Build/iOS", + "$(PROJECT_DIR)/ThirdParty/BuddyBuild", + "$(PROJECT_DIR)/ThirdParty/Leanplum", + ); + HEADER_SEARCH_PATHS = ( + "$(inherited)", + /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, + "$(SRCROOT)", + "$(SDKROOT)/usr/include/libxml2", + "$(BUILD_DIR)/Debug$(EFFECTIVE_PLATFORM_NAME)/include/**", + "$(BUILD_DIR)/Release$(EFFECTIVE_PLATFORM_NAME)/include/**", + ); + INFOPLIST_FILE = Client/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + OTHER_LDFLAGS = ( + "-ObjC", + "-lxml2", + ); + OTHER_SWIFT_FLAGS = "-DMOZ_CHANNEL_RELEASE -DMOZ_TARGET_CLIENT"; + PRODUCT_MODULE_NAME = Client; + PRODUCT_NAME = Client; + PROVISIONING_PROFILE_SPECIFIER = "Firefox Development"; + SWIFT_OBJC_BRIDGING_HEADER = "$(PROJECT_DIR)/Client-Bridging-Header.h"; + SWIFT_VERSION = 3.0; + }; + name = Firefox; + }; + E448FC9F1AEE7A6000869B6C /* Firefox */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Carthage/Build/iOS", + "$(BUILD_DIR)/Release$(EFFECTIVE_PLATFORM_NAME)", + ); + GCC_PREPROCESSOR_DEFINITIONS = "DEBUG=1"; + INFOPLIST_FILE = ClientTests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "org.mozilla.ios.$(PRODUCT_NAME:rfc1034identifier)"; + PRODUCT_NAME = ClientTests; + SWIFT_VERSION = 3.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Client.app/Client"; + }; + name = Firefox; + }; + E448FCA01AEE7A6000869B6C /* Firefox */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = NO; + APPLICATION_EXTENSION_API_ONLY = YES; + CODE_SIGN_ENTITLEMENTS = "$(SRCROOT)/Extensions/Entitlements/Firefox.entitlements"; + CODE_SIGN_IDENTITY = "iPhone Developer"; + DEVELOPMENT_TEAM = ""; + ENABLE_BITCODE = NO; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Carthage/Build/iOS", + "$(BUILD_DIR)/Release$(EFFECTIVE_PLATFORM_NAME)", + ); + INFOPLIST_FILE = Extensions/ShareTo/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "@executable_path/../../Frameworks"; + OTHER_SWIFT_FLAGS = "-DMOZ_CHANNEL_RELEASE -DMOZ_TARGET_SHARETO"; + PRODUCT_BUNDLE_IDENTIFIER = "$(MOZ_BUNDLE_ID).$(PRODUCT_NAME:rfc1034identifier)"; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = "Firefox ShareTo Development"; + SKIP_INSTALL = YES; + SWIFT_VERSION = 3.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Firefox; + }; + E448FCA11AEE7A6000869B6C /* Firefox */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = NO; + APPLICATION_EXTENSION_API_ONLY = YES; + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_ENTITLEMENTS = "$(SRCROOT)/Extensions/Entitlements/Firefox.entitlements"; + CODE_SIGN_IDENTITY = "iPhone Developer"; + DEVELOPMENT_TEAM = ""; + ENABLE_BITCODE = NO; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Carthage/Build/iOS", + "$(BUILD_DIR)/Release$(EFFECTIVE_PLATFORM_NAME)", + ); + INFOPLIST_FILE = Extensions/SendTo/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "@executable_path/../../Frameworks"; + OTHER_SWIFT_FLAGS = "-DMOZ_CHANNEL_RELEASE -DMOZ_TARGET_SENDTO"; + PRODUCT_BUNDLE_IDENTIFIER = "$(MOZ_BUNDLE_ID).$(PRODUCT_NAME:rfc1034identifier)"; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = "Firefox SendTo Development"; + SKIP_INSTALL = YES; + SWIFT_VERSION = 3.0; + }; + name = Firefox; + }; + E448FCA21AEE7A6000869B6C /* Firefox */ = { + isa = XCBuildConfiguration; + buildSettings = { + OTHER_LDFLAGS = "-ObjC"; + PRODUCT_BUNDLE_IDENTIFIER = "org.allizom.$(PRODUCT_NAME:rfc1034identifier)"; + PRODUCT_NAME = UITests; + SWIFT_VERSION = 3.0; + }; + name = Firefox; + }; + E448FCA31AEE7A6000869B6C /* Firefox */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = NO; + APPLICATION_EXTENSION_API_ONLY = YES; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_TESTABILITY = YES; + INFOPLIST_FILE = "Shared/Supporting Files/Info.plist"; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "org.mozilla.$(PRODUCT_NAME:rfc1034identifier)"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SWIFT_OBJC_BRIDGING_HEADER = "$SRCROOT/Shared/Shared-Bridging-Header.h"; + SWIFT_VERSION = 3.0; + }; + name = Firefox; + }; + E448FCA41AEE7A6000869B6C /* Firefox */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = NO; + APPLICATION_EXTENSION_API_ONLY = YES; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_TESTABILITY = YES; + INFOPLIST_FILE = Storage/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + MTL_ENABLE_DEBUG_INFO = NO; + OTHER_CFLAGS = "-DSQLITE_HAS_CODEC"; + PRODUCT_BUNDLE_IDENTIFIER = "org.mozilla.$(PRODUCT_NAME:rfc1034identifier)"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SWIFT_OBJC_BRIDGING_HEADER = "$SRCROOT/Storage/Storage-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + SWIFT_VERSION = 3.0; + }; + name = Firefox; + }; + E448FCA51AEE7A6000869B6C /* Firefox */ = { + isa = XCBuildConfiguration; + buildSettings = { + ENABLE_STRICT_OBJC_MSGSEND = YES; + INFOPLIST_FILE = StorageTests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + MTL_ENABLE_DEBUG_INFO = NO; + PRODUCT_BUNDLE_IDENTIFIER = "org.mozilla.$(PRODUCT_NAME:rfc1034identifier)"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "$SRCROOT/Storage/Storage-Bridging-Header.h"; + SWIFT_VERSION = 3.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Client.app/Client"; + }; + name = Firefox; + }; + E448FCA61AEE7A6000869B6C /* Firefox */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = NO; + APPLICATION_EXTENSION_API_ONLY = YES; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_BITCODE = NO; + ENABLE_TESTABILITY = YES; + FRAMEWORK_SEARCH_PATHS = ( + "$(PROJECT_DIR)/Carthage/Build/iOS", + "$(BUILD_DIR)/Release$(EFFECTIVE_PLATFORM_NAME)", + ); + HEADER_SEARCH_PATHS = ( + /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, + "$(SRCROOT)", + "$(SDKROOT)/usr/include/libxml2", + "$(BUILD_DIR)/Release$(EFFECTIVE_PLATFORM_NAME)/include/**", + ); + INFOPLIST_FILE = Account/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + MTL_ENABLE_DEBUG_INFO = NO; + PRODUCT_BUNDLE_IDENTIFIER = "org.mozilla.$(PRODUCT_NAME:rfc1034identifier)"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SWIFT_OBJC_BRIDGING_HEADER = "$SRCROOT/Account/Account-Bridging-Header.h"; + SWIFT_VERSION = 3.0; + }; + name = Firefox; + }; + E448FCA71AEE7A6000869B6C /* Firefox */ = { + isa = XCBuildConfiguration; + buildSettings = { + ENABLE_STRICT_OBJC_MSGSEND = YES; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Carthage/Build/iOS", + "$(BUILD_DIR)/Release$(EFFECTIVE_PLATFORM_NAME)", + ); + INFOPLIST_FILE = AccountTests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + MTL_ENABLE_DEBUG_INFO = NO; + PRODUCT_BUNDLE_IDENTIFIER = "org.mozilla.$(PRODUCT_NAME:rfc1034identifier)"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "$SRCROOT/Account/Account-Bridging-Header.h"; + SWIFT_VERSION = 3.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Client.app/Client"; + }; + name = Firefox; + }; + E448FCA81AEE7A6000869B6C /* Firefox */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = NO; + APPLICATION_EXTENSION_API_ONLY = YES; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_BITCODE = NO; + ENABLE_TESTABILITY = YES; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Carthage/Build/iOS", + "$(BUILD_DIR)/Release$(EFFECTIVE_PLATFORM_NAME)", + ); + INFOPLIST_FILE = Sync/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + MTL_ENABLE_DEBUG_INFO = NO; + PRODUCT_BUNDLE_IDENTIFIER = "org.mozilla.$(PRODUCT_NAME:rfc1034identifier)"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SWIFT_OBJC_BRIDGING_HEADER = "$SRCROOT/Sync/Sync-Bridging-Header.h"; + SWIFT_VERSION = 3.0; + }; + name = Firefox; + }; + E448FCA91AEE7A6000869B6C /* Firefox */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_ENABLE_MODULES = YES; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Carthage/Build/iOS", + "$(BUILD_DIR)/Release$(EFFECTIVE_PLATFORM_NAME)", + ); + INFOPLIST_FILE = SyncTests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + MTL_ENABLE_DEBUG_INFO = NO; + PRODUCT_BUNDLE_IDENTIFIER = "org.mozilla.$(PRODUCT_NAME:rfc1034identifier)"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "$SRCROOT/SyncTests/SyncTests-Bridging-Header.h"; + SWIFT_VERSION = 3.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Client.app/Client"; + }; + name = Firefox; + }; + E448FCAA1AEE7A6000869B6C /* Firefox */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = NO; + APPLICATION_EXTENSION_API_ONLY = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_TESTABILITY = YES; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Carthage/Build/iOS", + "$(BUILD_DIR)/Release$(EFFECTIVE_PLATFORM_NAME)", + ); + GCC_NO_COMMON_BLOCKS = YES; + INFOPLIST_FILE = ReadingList/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + MTL_ENABLE_DEBUG_INFO = NO; + PRODUCT_BUNDLE_IDENTIFIER = "org.mozilla.ios.$(PRODUCT_NAME:rfc1034identifier)"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SWIFT_OBJC_BRIDGING_HEADER = ""; + SWIFT_VERSION = 3.0; + }; + name = Firefox; + }; + E448FCAB1AEE7A6000869B6C /* Firefox */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + GCC_WARN_UNDECLARED_SELECTOR = YES; + INFOPLIST_FILE = ReadingListTests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + MTL_ENABLE_DEBUG_INFO = NO; + PRODUCT_BUNDLE_IDENTIFIER = "org.mozilla.ios.$(PRODUCT_NAME:rfc1034identifier)"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = ""; + SWIFT_VERSION = 3.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Client.app/Client"; + }; + name = Firefox; + }; + E4BA8A361B4B0A1700BC2E95 /* Fennec */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = NO; + APPLICATION_EXTENSION_API_ONLY = YES; + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_ENTITLEMENTS = "$(SRCROOT)/Extensions/Entitlements/Fennec.entitlements"; + CODE_SIGN_IDENTITY = "iPhone Developer"; + DEVELOPMENT_TEAM = 43AQ936H96; + ENABLE_BITCODE = NO; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Carthage/Build/iOS", + "$(BUILD_DIR)/Release$(EFFECTIVE_PLATFORM_NAME)", + ); + INFOPLIST_FILE = Extensions/ViewLater/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "@executable_path/../../Frameworks"; + OTHER_SWIFT_FLAGS = "-DMOZ_CHANNEL_FENNEC -DMOZ_TARGET_VIEWLATER"; + PRODUCT_BUNDLE_IDENTIFIER = "$(MOZ_BUNDLE_ID).$(PRODUCT_NAME:rfc1034identifier)"; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = "Fennec View Later Development"; + SKIP_INSTALL = YES; + SWIFT_VERSION = 3.0; + }; + name = Fennec; + }; + E4BA8A381B4B0A1700BC2E95 /* Firefox */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = NO; + APPLICATION_EXTENSION_API_ONLY = YES; + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_ENTITLEMENTS = "$(SRCROOT)/Extensions/Entitlements/Firefox.entitlements"; + CODE_SIGN_IDENTITY = "iPhone Developer"; + DEVELOPMENT_TEAM = ""; + ENABLE_BITCODE = NO; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Carthage/Build/iOS", + "$(BUILD_DIR)/Release$(EFFECTIVE_PLATFORM_NAME)", + ); + INFOPLIST_FILE = Extensions/ViewLater/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "@executable_path/../../Frameworks"; + OTHER_SWIFT_FLAGS = "-DMOZ_CHANNEL_RELEASE -DMOZ_TARGET_VIEWLATER"; + PRODUCT_BUNDLE_IDENTIFIER = "$(MOZ_BUNDLE_ID).$(PRODUCT_NAME:rfc1034identifier)"; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = "Firefox View Later Development"; + SKIP_INSTALL = YES; + SWIFT_VERSION = 3.0; + }; + name = Firefox; + }; + E4D567311ADECE2900F1EFE7 /* Fennec */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = NO; + APPLICATION_EXTENSION_API_ONLY = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Carthage/Build/iOS", + "$(BUILD_DIR)/Release$(EFFECTIVE_PLATFORM_NAME)", + ); + GCC_NO_COMMON_BLOCKS = YES; + INFOPLIST_FILE = ReadingList/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "org.mozilla.ios.$(PRODUCT_NAME:rfc1034identifier)"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SWIFT_OBJC_BRIDGING_HEADER = ""; + SWIFT_VERSION = 3.0; + }; + name = Fennec; + }; + E4D567341ADECE2900F1EFE7 /* Fennec */ = { + isa = XCBuildConfiguration; + buildSettings = { + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + INFOPLIST_FILE = ReadingListTests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "org.mozilla.ios.$(PRODUCT_NAME:rfc1034identifier)"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = ""; + SWIFT_VERSION = 3.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Client.app/Client"; + }; + name = Fennec; + }; + E601384C1C89EAE600DF9756 /* Fennec */ = { + isa = XCBuildConfiguration; + buildSettings = { + IPHONEOS_DEPLOYMENT_TARGET = 10.3; + LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_NAME = L10nSnapshotTests; + SWIFT_VERSION = 3.0; + TEST_TARGET_NAME = Client; + USES_XCTRUNNER = YES; + }; + name = Fennec; + }; + E601384D1C89EAE600DF9756 /* Firefox */ = { + isa = XCBuildConfiguration; + buildSettings = { + IPHONEOS_DEPLOYMENT_TARGET = 10.3; + LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_NAME = L10nSnapshotTests; + SWIFT_VERSION = 3.0; + TEST_TARGET_NAME = Client; + USES_XCTRUNNER = YES; + }; + name = Firefox; + }; + E601384E1C89EAE600DF9756 /* FirefoxBeta */ = { + isa = XCBuildConfiguration; + buildSettings = { + IPHONEOS_DEPLOYMENT_TARGET = 10.3; + LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_NAME = L10nSnapshotTests; + SWIFT_VERSION = 3.0; + TEST_TARGET_NAME = Client; + USES_XCTRUNNER = YES; + }; + name = FirefoxBeta; + }; + E60138511C89EAE600DF9756 /* Fennec */ = { + isa = XCBuildConfiguration; + buildSettings = { + IPHONEOS_DEPLOYMENT_TARGET = 10.3; + LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_NAME = MarketingUITests; + }; + name = Fennec; + }; + E60138521C89EAE600DF9756 /* Firefox */ = { + isa = XCBuildConfiguration; + buildSettings = { + IPHONEOS_DEPLOYMENT_TARGET = 10.3; + LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_NAME = MarketingUITests; + }; + name = Firefox; + }; + E60138531C89EAE600DF9756 /* FirefoxBeta */ = { + isa = XCBuildConfiguration; + buildSettings = { + IPHONEOS_DEPLOYMENT_TARGET = 10.3; + LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_NAME = MarketingUITests; + }; + name = FirefoxBeta; + }; + E69DB08D1E97DEAA008A67E6 /* Fennec */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = NO; + ALWAYS_SEARCH_USER_PATHS = NO; + APPLICATION_EXTENSION_API_ONLY = NO; + APP_IDENTIFIER_PREFIX = 9G8J6YA743; + CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_IDENTITY = "iPhone Developer"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = dwarf; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_BITCODE = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + INFOPLIST_FILE = SyncTelemetry/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 10.3; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + PRODUCT_BUNDLE_IDENTIFIER = org.mozilla.ios.SyncTelemetry; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 3.0; + TARGETED_DEVICE_FAMILY = "1,2"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Fennec; + }; + E69DB08E1E97DEAA008A67E6 /* Fennec_Enterprise */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = NO; + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_IDENTITY = "iPhone Developer"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = 43AQ936H96; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_BITCODE = NO; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + INFOPLIST_FILE = SyncTelemetry/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 10.3; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + MTL_ENABLE_DEBUG_INFO = NO; + PRODUCT_BUNDLE_IDENTIFIER = org.mozilla.ios.SyncTelemetry; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + SWIFT_VERSION = 3.0; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Fennec_Enterprise; + }; + E69DB08F1E97DEAA008A67E6 /* Firefox */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = NO; + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_IDENTITY = "iPhone Developer"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_BITCODE = NO; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + INFOPLIST_FILE = SyncTelemetry/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 10.3; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + MTL_ENABLE_DEBUG_INFO = NO; + PRODUCT_BUNDLE_IDENTIFIER = org.mozilla.ios.SyncTelemetry; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + SWIFT_VERSION = 3.0; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Firefox; + }; + E69DB0901E97DEAA008A67E6 /* FirefoxBeta */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = NO; + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_IDENTITY = "iPhone Developer"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_BITCODE = NO; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + INFOPLIST_FILE = SyncTelemetry/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 10.3; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + MTL_ENABLE_DEBUG_INFO = NO; + PRODUCT_BUNDLE_IDENTIFIER = org.mozilla.ios.SyncTelemetry; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + SWIFT_VERSION = 3.0; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = FirefoxBeta; + }; + E69DB0931E97DEAA008A67E6 /* Fennec */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + DEVELOPMENT_TEAM = 43AQ936H96; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + INFOPLIST_FILE = SyncTelemetryTests/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 10.3; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + PRODUCT_BUNDLE_IDENTIFIER = org.mozilla.ios.SyncTelemetryTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 3.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Client.app/Client"; + }; + name = Fennec; + }; + E69DB0941E97DEAA008A67E6 /* Fennec_Enterprise */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEVELOPMENT_TEAM = 43AQ936H96; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + INFOPLIST_FILE = SyncTelemetryTests/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 10.3; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + MTL_ENABLE_DEBUG_INFO = NO; + PRODUCT_BUNDLE_IDENTIFIER = org.mozilla.ios.SyncTelemetryTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + SWIFT_VERSION = 3.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Client.app/Client"; + VALIDATE_PRODUCT = YES; + }; + name = Fennec_Enterprise; + }; + E69DB0951E97DEAA008A67E6 /* Firefox */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEVELOPMENT_TEAM = 43AQ936H96; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + INFOPLIST_FILE = SyncTelemetryTests/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 10.3; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + MTL_ENABLE_DEBUG_INFO = NO; + PRODUCT_BUNDLE_IDENTIFIER = org.mozilla.ios.SyncTelemetryTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + SWIFT_VERSION = 3.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Client.app/Client"; + VALIDATE_PRODUCT = YES; + }; + name = Firefox; + }; + E69DB0961E97DEAA008A67E6 /* FirefoxBeta */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEVELOPMENT_TEAM = 43AQ936H96; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + INFOPLIST_FILE = SyncTelemetryTests/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 10.3; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + MTL_ENABLE_DEBUG_INFO = NO; + PRODUCT_BUNDLE_IDENTIFIER = org.mozilla.ios.SyncTelemetryTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + SWIFT_VERSION = 3.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Client.app/Client"; + VALIDATE_PRODUCT = YES; + }; + name = FirefoxBeta; + }; + E6DCC2051DCBB6F100CEC4B7 /* Fennec_Enterprise */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = E6DCC1ED1DCBB6AA00CEC4B7 /* Fennec.enterprise.xcconfig */; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + ALWAYS_SEARCH_USER_PATHS = NO; + APPLICATION_EXTENSION_API_ONLY = NO; + CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_ACTIVITY_MODE = ""; + "DEBUG_ACTIVITY_MODE[sdk=iphonesimulator*]" = disable; + DEVELOPMENT_TEAM = 9G8J6YA743; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Carthage/Build/iOS", + "$(BUILD_DIR)/Release$(EFFECTIVE_PLATFORM_NAME)", + ); + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = "DEBUG=1"; + GCC_SYMBOLS_PRIVATE_EXTERN = NO; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + HEADER_SEARCH_PATHS = ( + "$(inherited)", + /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, + "$(SRCROOT)", + "$(SDKROOT)/usr/include/libxml2", + "$(BUILD_DIR)/Release$(EFFECTIVE_PLATFORM_NAME)/include/**", + ); + IPHONEOS_DEPLOYMENT_TARGET = 10.3; + LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks"; + LIBRARY_SEARCH_PATHS = "$(BUILD_DIR)/Release$(EFFECTIVE_PLATFORM_NAME)"; + MOZ_INTERNAL_URL_SCHEME = fennec; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + OTHER_LDFLAGS = ( + "-ObjC", + "-lxml2", + ); + OTHER_SWIFT_FLAGS = "-DMOZ_CHANNEL_FENNEC"; + PRODUCT_BUNDLE_IDENTIFIER = "$(MOZ_BUNDLE_ID)"; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = ""; + SWIFT_INCLUDE_PATHS = "$(PROJECT_DIR)/Storage/modules"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + TARGETED_DEVICE_FAMILY = "1,2"; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Fennec_Enterprise; + }; + E6DCC2061DCBB6F100CEC4B7 /* Fennec_Enterprise */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon_Developer; + CODE_SIGN_ENTITLEMENTS = "$(SRCROOT)/Client/Entitlements/FennecEnterpriseApplication.entitlements"; + CODE_SIGN_IDENTITY = "iPhone Developer"; + DEVELOPMENT_TEAM = 43AQ936H96; + ENABLE_BITCODE = NO; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(BUILD_DIR)/Release$(EFFECTIVE_PLATFORM_NAME)", + "$(PROJECT_DIR)/Carthage/Build/iOS", + "$(PROJECT_DIR)/ThirdParty/BuddyBuild", + "$(PROJECT_DIR)/ThirdParty/Leanplum", + ); + HEADER_SEARCH_PATHS = ( + "$(inherited)", + /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, + "$(SRCROOT)", + "$(SDKROOT)/usr/include/libxml2", + "$(BUILD_DIR)/Debug$(EFFECTIVE_PLATFORM_NAME)/include/**", + "$(BUILD_DIR)/Release$(EFFECTIVE_PLATFORM_NAME)/include/**", + ); + INFOPLIST_FILE = Client/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + OTHER_LDFLAGS = ( + "-ObjC", + "-lxml2", + "-framework", + BuddyBuildSDK, + "-framework", + AssetsLibrary, + "-framework", + CoreText, + "-framework", + CoreTelephony, + "-framework", + SystemConfiguration, + "-framework", + QuartzCore, + "-framework", + CoreVideo, + "-framework", + AVFoundation, + "-framework", + CoreMedia, + ); + OTHER_SWIFT_FLAGS = "-DMOZ_CHANNEL_FENNEC -DMOZ_TARGET_CLIENT"; + PRODUCT_MODULE_NAME = Client; + PRODUCT_NAME = Client; + PROVISIONING_PROFILE_SPECIFIER = "FennecEnterprise Development"; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = BUDDYBUILD; + SWIFT_OBJC_BRIDGING_HEADER = "$(PROJECT_DIR)/Client-Bridging-Header.h"; + SWIFT_VERSION = 3.0; + }; + name = Fennec_Enterprise; + }; + E6DCC2071DCBB6F100CEC4B7 /* Fennec_Enterprise */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = NO; + APPLICATION_EXTENSION_API_ONLY = YES; + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_ENTITLEMENTS = "$(SRCROOT)/Extensions/Entitlements/FennecEnterprise.entitlements"; + CODE_SIGN_IDENTITY = "iPhone Developer"; + DEVELOPMENT_TEAM = ""; + ENABLE_BITCODE = NO; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Carthage/Build/iOS", + "$(BUILD_DIR)/Release$(EFFECTIVE_PLATFORM_NAME)", + ); + INFOPLIST_FILE = Extensions/SendTo/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "@executable_path/../../Frameworks"; + OTHER_SWIFT_FLAGS = "-DMOZ_CHANNEL_FENNEC -DMOZ_TARGET_SENDTO"; + PRODUCT_BUNDLE_IDENTIFIER = "$(MOZ_BUNDLE_ID).$(PRODUCT_NAME:rfc1034identifier)"; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = "FennecEnterprise SendTo Development"; + SKIP_INSTALL = YES; + SWIFT_VERSION = 3.0; + }; + name = Fennec_Enterprise; + }; + E6DCC2081DCBB6F100CEC4B7 /* Fennec_Enterprise */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = NO; + APPLICATION_EXTENSION_API_ONLY = YES; + CODE_SIGN_ENTITLEMENTS = "$(SRCROOT)/Extensions/Entitlements/FennecEnterprise.entitlements"; + CODE_SIGN_IDENTITY = "iPhone Developer"; + DEVELOPMENT_TEAM = ""; + ENABLE_BITCODE = NO; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Carthage/Build/iOS", + "$(BUILD_DIR)/Release$(EFFECTIVE_PLATFORM_NAME)", + ); + INFOPLIST_FILE = Extensions/ShareTo/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "@executable_path/../../Frameworks"; + OTHER_SWIFT_FLAGS = "-DMOZ_CHANNEL_FENNEC -DMOZ_TARGET_SHARETO"; + PRODUCT_BUNDLE_IDENTIFIER = "$(MOZ_BUNDLE_ID).$(PRODUCT_NAME:rfc1034identifier)"; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = "FennecEnterprise Share To Development"; + SKIP_INSTALL = YES; + SWIFT_VERSION = 3.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Fennec_Enterprise; + }; + E6DCC2091DCBB6F100CEC4B7 /* Fennec_Enterprise */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = NO; + APPLICATION_EXTENSION_API_ONLY = YES; + CODE_SIGN_ENTITLEMENTS = "$(SRCROOT)/Extensions/Entitlements/FennecEnterprise.entitlements"; + CODE_SIGN_IDENTITY = "iPhone Developer"; + DEVELOPMENT_TEAM = ""; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Extensions/Today/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 10.3; + LD_RUNPATH_SEARCH_PATHS = "@executable_path/../../Frameworks"; + OTHER_SWIFT_FLAGS = "-DMOZ_CHANNEL_FENNEC -DMOZ_TARGET_TODAY"; + PRODUCT_BUNDLE_IDENTIFIER = "$(MOZ_BUNDLE_ID).$(PRODUCT_NAME:rfc1034identifier)"; + PRODUCT_NAME = Today; + PROVISIONING_PROFILE_SPECIFIER = "FennecEnterprise Today Development"; + SWIFT_VERSION = 3.0; + }; + name = Fennec_Enterprise; + }; + E6DCC20A1DCBB6F100CEC4B7 /* Fennec_Enterprise */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = NO; + APPLICATION_EXTENSION_API_ONLY = YES; + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_ENTITLEMENTS = "$(SRCROOT)/Extensions/Entitlements/FennecEnterprise.entitlements"; + CODE_SIGN_IDENTITY = "iPhone Developer"; + DEVELOPMENT_TEAM = ""; + ENABLE_BITCODE = NO; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Carthage/Build/iOS", + "$(BUILD_DIR)/Release$(EFFECTIVE_PLATFORM_NAME)", + ); + INFOPLIST_FILE = Extensions/ViewLater/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "@executable_path/../../Frameworks"; + OTHER_SWIFT_FLAGS = "-DMOZ_CHANNEL_FENNEC -DMOZ_TARGET_VIEWLATER"; + PRODUCT_BUNDLE_IDENTIFIER = "$(MOZ_BUNDLE_ID).$(PRODUCT_NAME:rfc1034identifier)"; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = "FennecEnterprise ViewLater Development"; + SKIP_INSTALL = YES; + SWIFT_VERSION = 3.0; + }; + name = Fennec_Enterprise; + }; + E6DCC20B1DCBB6F100CEC4B7 /* Fennec_Enterprise */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = NO; + APPLICATION_EXTENSION_API_ONLY = YES; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = 43AQ936H96; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + INFOPLIST_FILE = "Shared/Supporting Files/Info.plist"; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "org.mozilla.$(PRODUCT_NAME:rfc1034identifier)"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SWIFT_OBJC_BRIDGING_HEADER = "$SRCROOT/Shared/Shared-Bridging-Header.h"; + SWIFT_VERSION = 3.0; + }; + name = Fennec_Enterprise; + }; + E6DCC20C1DCBB6F100CEC4B7 /* Fennec_Enterprise */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = NO; + APPLICATION_EXTENSION_API_ONLY = YES; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = 43AQ936H96; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + INFOPLIST_FILE = Storage/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + OTHER_CFLAGS = "-DSQLITE_HAS_CODEC"; + PRODUCT_BUNDLE_IDENTIFIER = "org.mozilla.$(PRODUCT_NAME:rfc1034identifier)"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SWIFT_OBJC_BRIDGING_HEADER = "$SRCROOT/Storage/Storage-Bridging-Header.h"; + SWIFT_VERSION = 3.0; + }; + name = Fennec_Enterprise; + }; + E6DCC20D1DCBB6F100CEC4B7 /* Fennec_Enterprise */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = NO; + APPLICATION_EXTENSION_API_ONLY = YES; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = 43AQ936H96; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_BITCODE = NO; + FRAMEWORK_SEARCH_PATHS = ( + "$(PROJECT_DIR)/Carthage/Build/iOS", + "$(BUILD_DIR)/Release$(EFFECTIVE_PLATFORM_NAME)", + ); + HEADER_SEARCH_PATHS = ( + /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, + "$(SRCROOT)", + "$(SDKROOT)/usr/include/libxml2", + "$(BUILD_DIR)/Release$(EFFECTIVE_PLATFORM_NAME)/include/**", + ); + INFOPLIST_FILE = Account/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "org.mozilla.$(PRODUCT_NAME:rfc1034identifier)"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SWIFT_OBJC_BRIDGING_HEADER = "$SRCROOT/Account/Account-Bridging-Header.h"; + SWIFT_VERSION = 3.0; + }; + name = Fennec_Enterprise; + }; + E6DCC20E1DCBB6F100CEC4B7 /* Fennec_Enterprise */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = NO; + APPLICATION_EXTENSION_API_ONLY = YES; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = 43AQ936H96; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_BITCODE = NO; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Carthage/Build/iOS", + "$(BUILD_DIR)/Release$(EFFECTIVE_PLATFORM_NAME)", + ); + INFOPLIST_FILE = Sync/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "org.mozilla.$(PRODUCT_NAME:rfc1034identifier)"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SWIFT_OBJC_BRIDGING_HEADER = "$SRCROOT/Sync/Sync-Bridging-Header.h"; + SWIFT_VERSION = 3.0; + }; + name = Fennec_Enterprise; + }; + E6DCC20F1DCBB6F100CEC4B7 /* Fennec_Enterprise */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = NO; + APPLICATION_EXTENSION_API_ONLY = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = 43AQ936H96; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Carthage/Build/iOS", + "$(BUILD_DIR)/Release$(EFFECTIVE_PLATFORM_NAME)", + ); + GCC_NO_COMMON_BLOCKS = YES; + INFOPLIST_FILE = ReadingList/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "org.mozilla.ios.$(PRODUCT_NAME:rfc1034identifier)"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SWIFT_OBJC_BRIDGING_HEADER = ""; + SWIFT_VERSION = 3.0; + }; + name = Fennec_Enterprise; + }; + E6DCC2101DCBB6F100CEC4B7 /* Fennec_Enterprise */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Carthage/Build/iOS", + "$(BUILD_DIR)/Release$(EFFECTIVE_PLATFORM_NAME)", + ); + INFOPLIST_FILE = ClientTests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "org.mozilla.ios.$(PRODUCT_NAME:rfc1034identifier)"; + PRODUCT_NAME = ClientTests; + SWIFT_VERSION = 3.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Client.app/Client"; + }; + name = Fennec_Enterprise; + }; + E6DCC2111DCBB6F100CEC4B7 /* Fennec_Enterprise */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + INFOPLIST_FILE = UITests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + OTHER_LDFLAGS = "-ObjC"; + PRODUCT_BUNDLE_IDENTIFIER = "org.allizom.$(PRODUCT_NAME:rfc1034identifier)"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "$(SRCROOT)/UITests/UITests-Bridging-Header.h"; + SWIFT_VERSION = 3.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Client.app/Client"; + }; + name = Fennec_Enterprise; + }; + E6DCC2131DCBB6F100CEC4B7 /* Fennec_Enterprise */ = { + isa = XCBuildConfiguration; + buildSettings = { + INFOPLIST_FILE = StorageTests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "org.mozilla.$(PRODUCT_NAME:rfc1034identifier)"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "$SRCROOT/Storage/Storage-Bridging-Header.h"; + SWIFT_VERSION = 3.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Client.app/Client"; + }; + name = Fennec_Enterprise; + }; + E6DCC2141DCBB6F100CEC4B7 /* Fennec_Enterprise */ = { + isa = XCBuildConfiguration; + buildSettings = { + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Carthage/Build/iOS", + "$(BUILD_DIR)/Release$(EFFECTIVE_PLATFORM_NAME)", + ); + INFOPLIST_FILE = AccountTests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "org.mozilla.$(PRODUCT_NAME:rfc1034identifier)"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "$SRCROOT/Account/Account-Bridging-Header.h"; + SWIFT_VERSION = 3.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Client.app/Client"; + }; + name = Fennec_Enterprise; + }; + E6DCC2151DCBB6F100CEC4B7 /* Fennec_Enterprise */ = { + isa = XCBuildConfiguration; + buildSettings = { + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Carthage/Build/iOS", + "$(BUILD_DIR)/Release$(EFFECTIVE_PLATFORM_NAME)", + ); + INFOPLIST_FILE = SyncTests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "org.mozilla.$(PRODUCT_NAME:rfc1034identifier)"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "$SRCROOT/SyncTests/SyncTests-Bridging-Header.h"; + SWIFT_VERSION = 3.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Client.app/Client"; + }; + name = Fennec_Enterprise; + }; + E6DCC2161DCBB6F100CEC4B7 /* Fennec_Enterprise */ = { + isa = XCBuildConfiguration; + buildSettings = { + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + INFOPLIST_FILE = ReadingListTests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "org.mozilla.ios.$(PRODUCT_NAME:rfc1034identifier)"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = ""; + SWIFT_VERSION = 3.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Client.app/Client"; + }; + name = Fennec_Enterprise; + }; + E6DCC2171DCBB6F100CEC4B7 /* Fennec_Enterprise */ = { + isa = XCBuildConfiguration; + buildSettings = { + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + GCC_NO_COMMON_BLOCKS = YES; + INFOPLIST_FILE = SharedTests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "org.mozilla.$(PRODUCT_NAME:rfc1034identifier)"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "$SRCROOT/Shared/Shared-Bridging-Header.h"; + SWIFT_VERSION = 3.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Client.app/Client"; + }; + name = Fennec_Enterprise; + }; + E6DCC2181DCBB6F100CEC4B7 /* Fennec_Enterprise */ = { + isa = XCBuildConfiguration; + buildSettings = { + IPHONEOS_DEPLOYMENT_TARGET = 10.3; + LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_NAME = L10nSnapshotTests; + SWIFT_VERSION = 3.0; + TEST_TARGET_NAME = Client; + USES_XCTRUNNER = YES; + }; + name = Fennec_Enterprise; + }; + E6DCC2191DCBB6F100CEC4B7 /* Fennec_Enterprise */ = { + isa = XCBuildConfiguration; + buildSettings = { + IPHONEOS_DEPLOYMENT_TARGET = 10.3; + LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_NAME = MarketingUITests; + }; + name = Fennec_Enterprise; + }; + E6DCC21A1DCBB6F100CEC4B7 /* Fennec_Enterprise */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + INFOPLIST_FILE = XCUITests/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 10.3; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + PRODUCT_BUNDLE_IDENTIFIER = com.mozilla.XCUITests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + SWIFT_OBJC_BRIDGING_HEADER = "$(SRCROOT)/XCUITests/XCUITests-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 3.0; + TEST_TARGET_NAME = Client; + }; + name = Fennec_Enterprise; + }; + E6DCC21B1DCBB6F100CEC4B7 /* Fennec_Enterprise */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + BUNDLE_LOADER = "$(TEST_HOST)"; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_SUSPICIOUS_MOVES = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + INFOPLIST_FILE = StoragePerfTests/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 10.3; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + PRODUCT_BUNDLE_IDENTIFIER = com.mozilla.StoragePerfTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 3.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Client.app/Client"; + }; + name = Fennec_Enterprise; + }; + E6F965151B2F1CF20034B023 /* Fennec */ = { + isa = XCBuildConfiguration; + buildSettings = { + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + GCC_NO_COMMON_BLOCKS = YES; + INFOPLIST_FILE = SharedTests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "org.mozilla.$(PRODUCT_NAME:rfc1034identifier)"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "$SRCROOT/Shared/Shared-Bridging-Header.h"; + SWIFT_VERSION = 3.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Client.app/Client"; + }; + name = Fennec; + }; + E6F965171B2F1CF20034B023 /* Firefox */ = { + isa = XCBuildConfiguration; + buildSettings = { + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + GCC_NO_COMMON_BLOCKS = YES; + INFOPLIST_FILE = SharedTests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + MTL_ENABLE_DEBUG_INFO = NO; + PRODUCT_BUNDLE_IDENTIFIER = "org.mozilla.$(PRODUCT_NAME:rfc1034identifier)"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "$SRCROOT/Shared/Shared-Bridging-Header.h"; + SWIFT_VERSION = 3.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Client.app/Client"; + }; + name = Firefox; + }; + E6FCC4291C40562400DF6113 /* FirefoxBeta */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = E6FCC43C1C40565200DF6113 /* FirefoxBeta.xcconfig */; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + ALWAYS_SEARCH_USER_PATHS = NO; + APPLICATION_EXTENSION_API_ONLY = NO; + CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_ACTIVITY_MODE = ""; + "DEBUG_ACTIVITY_MODE[sdk=iphonesimulator*]" = disable; + DEVELOPMENT_TEAM = 43AQ936H96; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Carthage/Build/iOS", + "$(BUILD_DIR)/Release$(EFFECTIVE_PLATFORM_NAME)", + ); + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + HEADER_SEARCH_PATHS = ( + "$(inherited)", + /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, + "$(SRCROOT)", + "$(SDKROOT)/usr/include/libxml2", + "$(BUILD_DIR)/Release$(EFFECTIVE_PLATFORM_NAME)/include/**", + ); + IPHONEOS_DEPLOYMENT_TARGET = 10.3; + LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks"; + LIBRARY_SEARCH_PATHS = "$(BUILD_DIR)/Release$(EFFECTIVE_PLATFORM_NAME)"; + MOZ_INTERNAL_URL_SCHEME = "firefox-beta"; + MTL_ENABLE_DEBUG_INFO = NO; + OTHER_LDFLAGS = ( + "-ObjC", + "-lxml2", + ); + OTHER_SWIFT_FLAGS = "-DMOZ_CHANNEL_BETA"; + PRODUCT_BUNDLE_IDENTIFIER = "$(MOZ_BUNDLE_ID)"; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = ""; + SWIFT_INCLUDE_PATHS = "$(PROJECT_DIR)/Storage/modules"; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = FirefoxBeta; + }; + E6FCC42A1C40562400DF6113 /* FirefoxBeta */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon_Beta; + CODE_SIGN_ENTITLEMENTS = "$(SRCROOT)/Client/Entitlements/FirefoxBetaApplication.entitlements"; + CODE_SIGN_IDENTITY = "iPhone Developer"; + DEVELOPMENT_TEAM = 43AQ936H96; + ENABLE_BITCODE = NO; + ENABLE_TESTABILITY = YES; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(BUILD_DIR)/Release$(EFFECTIVE_PLATFORM_NAME)", + "$(PROJECT_DIR)/Carthage/Build/iOS", + "$(PROJECT_DIR)/ThirdParty/BuddyBuild", + "$(PROJECT_DIR)/ThirdParty/Leanplum", + ); + HEADER_SEARCH_PATHS = ( + "$(inherited)", + /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, + "$(SRCROOT)", + "$(SDKROOT)/usr/include/libxml2", + "$(BUILD_DIR)/Debug$(EFFECTIVE_PLATFORM_NAME)/include/**", + "$(BUILD_DIR)/Release$(EFFECTIVE_PLATFORM_NAME)/include/**", + ); + INFOPLIST_FILE = Client/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + OTHER_LDFLAGS = ( + "-ObjC", + "-lxml2", + ); + OTHER_SWIFT_FLAGS = "-DMOZ_CHANNEL_BETA -DMOZ_TARGET_CLIENT"; + PRODUCT_MODULE_NAME = Client; + PRODUCT_NAME = Client; + PROVISIONING_PROFILE_SPECIFIER = "Firefox Beta Development"; + SWIFT_OBJC_BRIDGING_HEADER = "$(PROJECT_DIR)/Client-Bridging-Header.h"; + SWIFT_VERSION = 3.0; + }; + name = FirefoxBeta; + }; + E6FCC42B1C40562400DF6113 /* FirefoxBeta */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Carthage/Build/iOS", + "$(BUILD_DIR)/Release$(EFFECTIVE_PLATFORM_NAME)", + ); + INFOPLIST_FILE = ClientTests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "org.mozilla.ios.$(PRODUCT_NAME:rfc1034identifier)"; + PRODUCT_NAME = ClientTests; + SWIFT_VERSION = 3.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Client.app/Client"; + }; + name = FirefoxBeta; + }; + E6FCC42C1C40562400DF6113 /* FirefoxBeta */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = NO; + APPLICATION_EXTENSION_API_ONLY = YES; + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_ENTITLEMENTS = "$(SRCROOT)/Extensions/Entitlements/FirefoxBeta.entitlements"; + CODE_SIGN_IDENTITY = "iPhone Developer"; + DEVELOPMENT_TEAM = ""; + ENABLE_BITCODE = NO; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Carthage/Build/iOS", + "$(BUILD_DIR)/Release$(EFFECTIVE_PLATFORM_NAME)", + ); + INFOPLIST_FILE = Extensions/SendTo/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "@executable_path/../../Frameworks"; + OTHER_SWIFT_FLAGS = "-DMOZ_CHANNEL_BETA -DMOZ_TARGET_SENDTO"; + PRODUCT_BUNDLE_IDENTIFIER = "$(MOZ_BUNDLE_ID).$(PRODUCT_NAME:rfc1034identifier)"; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = "Firefox Beta SentTo Development"; + SKIP_INSTALL = YES; + SWIFT_VERSION = 3.0; + }; + name = FirefoxBeta; + }; + E6FCC42D1C40562400DF6113 /* FirefoxBeta */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = NO; + APPLICATION_EXTENSION_API_ONLY = YES; + CODE_SIGN_ENTITLEMENTS = "$(SRCROOT)/Extensions/Entitlements/FirefoxBeta.entitlements"; + CODE_SIGN_IDENTITY = "iPhone Developer"; + DEVELOPMENT_TEAM = ""; + ENABLE_BITCODE = NO; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Carthage/Build/iOS", + "$(BUILD_DIR)/Release$(EFFECTIVE_PLATFORM_NAME)", + ); + INFOPLIST_FILE = Extensions/ShareTo/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "@executable_path/../../Frameworks"; + OTHER_SWIFT_FLAGS = "-DMOZ_CHANNEL_BETA -DMOZ_TARGET_SHARETO"; + PRODUCT_BUNDLE_IDENTIFIER = "$(MOZ_BUNDLE_ID).$(PRODUCT_NAME:rfc1034identifier)"; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = "Firefox Beta ShareTo Development"; + SKIP_INSTALL = YES; + SWIFT_VERSION = 3.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = FirefoxBeta; + }; + E6FCC42E1C40562400DF6113 /* FirefoxBeta */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = NO; + APPLICATION_EXTENSION_API_ONLY = YES; + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_ENTITLEMENTS = "$(SRCROOT)/Extensions/Entitlements/FirefoxBeta.entitlements"; + CODE_SIGN_IDENTITY = "iPhone Developer"; + DEVELOPMENT_TEAM = ""; + ENABLE_BITCODE = NO; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Carthage/Build/iOS", + "$(BUILD_DIR)/Release$(EFFECTIVE_PLATFORM_NAME)", + ); + INFOPLIST_FILE = Extensions/ViewLater/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "@executable_path/../../Frameworks"; + OTHER_SWIFT_FLAGS = "-DMOZ_CHANNEL_BETA -DMOZ_TARGET_VIEWLATER"; + PRODUCT_BUNDLE_IDENTIFIER = "$(MOZ_BUNDLE_ID).$(PRODUCT_NAME:rfc1034identifier)"; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = "Firefox Beta ViewLater Development"; + SKIP_INSTALL = YES; + SWIFT_VERSION = 3.0; + }; + name = FirefoxBeta; + }; + E6FCC42F1C40562400DF6113 /* FirefoxBeta */ = { + isa = XCBuildConfiguration; + buildSettings = { + OTHER_LDFLAGS = "-ObjC"; + PRODUCT_BUNDLE_IDENTIFIER = "org.allizom.$(PRODUCT_NAME:rfc1034identifier)"; + PRODUCT_NAME = UITests; + SWIFT_VERSION = 3.0; + }; + name = FirefoxBeta; + }; + E6FCC4311C40562400DF6113 /* FirefoxBeta */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = NO; + APPLICATION_EXTENSION_API_ONLY = YES; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_TESTABILITY = YES; + INFOPLIST_FILE = "Shared/Supporting Files/Info.plist"; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "org.mozilla.$(PRODUCT_NAME:rfc1034identifier)"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SWIFT_OBJC_BRIDGING_HEADER = "$SRCROOT/Shared/Shared-Bridging-Header.h"; + SWIFT_VERSION = 3.0; + }; + name = FirefoxBeta; + }; + E6FCC4321C40562400DF6113 /* FirefoxBeta */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = NO; + APPLICATION_EXTENSION_API_ONLY = YES; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_TESTABILITY = YES; + INFOPLIST_FILE = Storage/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + MTL_ENABLE_DEBUG_INFO = NO; + OTHER_CFLAGS = "-DSQLITE_HAS_CODEC"; + PRODUCT_BUNDLE_IDENTIFIER = "org.mozilla.$(PRODUCT_NAME:rfc1034identifier)"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SWIFT_OBJC_BRIDGING_HEADER = "$SRCROOT/Storage/Storage-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + SWIFT_VERSION = 3.0; + }; + name = FirefoxBeta; + }; + E6FCC4331C40562400DF6113 /* FirefoxBeta */ = { + isa = XCBuildConfiguration; + buildSettings = { + ENABLE_STRICT_OBJC_MSGSEND = YES; + INFOPLIST_FILE = StorageTests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + MTL_ENABLE_DEBUG_INFO = NO; + PRODUCT_BUNDLE_IDENTIFIER = "org.mozilla.$(PRODUCT_NAME:rfc1034identifier)"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "$SRCROOT/Storage/Storage-Bridging-Header.h"; + SWIFT_VERSION = 3.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Client.app/Client"; + }; + name = FirefoxBeta; + }; + E6FCC4341C40562400DF6113 /* FirefoxBeta */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = NO; + APPLICATION_EXTENSION_API_ONLY = YES; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_BITCODE = NO; + ENABLE_TESTABILITY = YES; + FRAMEWORK_SEARCH_PATHS = ( + "$(PROJECT_DIR)/Carthage/Build/iOS", + "$(BUILD_DIR)/Release$(EFFECTIVE_PLATFORM_NAME)", + ); + HEADER_SEARCH_PATHS = ( + /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, + "$(SRCROOT)", + "$(SDKROOT)/usr/include/libxml2", + "$(BUILD_DIR)/Release$(EFFECTIVE_PLATFORM_NAME)/include/**", + ); + INFOPLIST_FILE = Account/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + MTL_ENABLE_DEBUG_INFO = NO; + PRODUCT_BUNDLE_IDENTIFIER = "org.mozilla.$(PRODUCT_NAME:rfc1034identifier)"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SWIFT_OBJC_BRIDGING_HEADER = "$SRCROOT/Account/Account-Bridging-Header.h"; + SWIFT_VERSION = 3.0; + }; + name = FirefoxBeta; + }; + E6FCC4351C40562400DF6113 /* FirefoxBeta */ = { + isa = XCBuildConfiguration; + buildSettings = { + ENABLE_STRICT_OBJC_MSGSEND = YES; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Carthage/Build/iOS", + "$(BUILD_DIR)/Release$(EFFECTIVE_PLATFORM_NAME)", + ); + INFOPLIST_FILE = AccountTests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + MTL_ENABLE_DEBUG_INFO = NO; + PRODUCT_BUNDLE_IDENTIFIER = "org.mozilla.$(PRODUCT_NAME:rfc1034identifier)"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "$SRCROOT/Account/Account-Bridging-Header.h"; + SWIFT_VERSION = 3.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Client.app/Client"; + }; + name = FirefoxBeta; + }; + E6FCC4361C40562400DF6113 /* FirefoxBeta */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = NO; + APPLICATION_EXTENSION_API_ONLY = YES; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_BITCODE = NO; + ENABLE_TESTABILITY = YES; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Carthage/Build/iOS", + "$(BUILD_DIR)/Release$(EFFECTIVE_PLATFORM_NAME)", + ); + INFOPLIST_FILE = Sync/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + MTL_ENABLE_DEBUG_INFO = NO; + PRODUCT_BUNDLE_IDENTIFIER = "org.mozilla.$(PRODUCT_NAME:rfc1034identifier)"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SWIFT_OBJC_BRIDGING_HEADER = "$SRCROOT/Sync/Sync-Bridging-Header.h"; + SWIFT_VERSION = 3.0; + }; + name = FirefoxBeta; + }; + E6FCC4371C40562400DF6113 /* FirefoxBeta */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_ENABLE_MODULES = YES; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Carthage/Build/iOS", + "$(BUILD_DIR)/Release$(EFFECTIVE_PLATFORM_NAME)", + ); + INFOPLIST_FILE = SyncTests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + MTL_ENABLE_DEBUG_INFO = NO; + PRODUCT_BUNDLE_IDENTIFIER = "org.mozilla.$(PRODUCT_NAME:rfc1034identifier)"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "$SRCROOT/SyncTests/SyncTests-Bridging-Header.h"; + SWIFT_VERSION = 3.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Client.app/Client"; + }; + name = FirefoxBeta; + }; + E6FCC4381C40562400DF6113 /* FirefoxBeta */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = NO; + APPLICATION_EXTENSION_API_ONLY = YES; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_TESTABILITY = YES; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Carthage/Build/iOS", + "$(BUILD_DIR)/Release$(EFFECTIVE_PLATFORM_NAME)", + ); + GCC_NO_COMMON_BLOCKS = YES; + INFOPLIST_FILE = ReadingList/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + MTL_ENABLE_DEBUG_INFO = NO; + PRODUCT_BUNDLE_IDENTIFIER = "org.mozilla.ios.$(PRODUCT_NAME:rfc1034identifier)"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SWIFT_OBJC_BRIDGING_HEADER = ""; + SWIFT_VERSION = 3.0; + }; + name = FirefoxBeta; + }; + E6FCC4391C40562400DF6113 /* FirefoxBeta */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + GCC_WARN_UNDECLARED_SELECTOR = YES; + INFOPLIST_FILE = ReadingListTests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + MTL_ENABLE_DEBUG_INFO = NO; + PRODUCT_BUNDLE_IDENTIFIER = "org.mozilla.ios.$(PRODUCT_NAME:rfc1034identifier)"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = ""; + SWIFT_VERSION = 3.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Client.app/Client"; + }; + name = FirefoxBeta; + }; + E6FCC43A1C40562400DF6113 /* FirefoxBeta */ = { + isa = XCBuildConfiguration; + buildSettings = { + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + GCC_NO_COMMON_BLOCKS = YES; + INFOPLIST_FILE = SharedTests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + MTL_ENABLE_DEBUG_INFO = NO; + PRODUCT_BUNDLE_IDENTIFIER = "org.mozilla.$(PRODUCT_NAME:rfc1034identifier)"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "$SRCROOT/Shared/Shared-Bridging-Header.h"; + SWIFT_VERSION = 3.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Client.app/Client"; + }; + name = FirefoxBeta; + }; + F84B21DB1A090F8100AAB793 /* Fennec */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = E60961861B62B8A700DD640F /* Fennec.xcconfig */; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + ALWAYS_SEARCH_USER_PATHS = NO; + APPLICATION_EXTENSION_API_ONLY = NO; + CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_ACTIVITY_MODE = ""; + "DEBUG_ACTIVITY_MODE[sdk=iphonesimulator*]" = disable; + DEVELOPMENT_TEAM = 43AQ936H96; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Carthage/Build/iOS", + "$(BUILD_DIR)/Release$(EFFECTIVE_PLATFORM_NAME)", + ); + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = "DEBUG=1"; + GCC_SYMBOLS_PRIVATE_EXTERN = NO; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + HEADER_SEARCH_PATHS = ( + "$(inherited)", + /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, + "$(SRCROOT)", + "$(SDKROOT)/usr/include/libxml2", + "$(BUILD_DIR)/Release$(EFFECTIVE_PLATFORM_NAME)/include/**", + ); + IPHONEOS_DEPLOYMENT_TARGET = 10.3; + LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks"; + LIBRARY_SEARCH_PATHS = "$(BUILD_DIR)/Release$(EFFECTIVE_PLATFORM_NAME)"; + MOZ_INTERNAL_URL_SCHEME = fennec; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + OTHER_LDFLAGS = ( + "-ObjC", + "-lxml2", + ); + OTHER_SWIFT_FLAGS = "-DMOZ_CHANNEL_FENNEC"; + PRODUCT_BUNDLE_IDENTIFIER = "$(MOZ_BUNDLE_ID)"; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = ""; + SWIFT_INCLUDE_PATHS = "$(PROJECT_DIR)/Storage/modules"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + TARGETED_DEVICE_FAMILY = "1,2"; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Fennec; + }; + F84B21DE1A090F8100AAB793 /* Fennec */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon_Developer; + CODE_SIGN_ENTITLEMENTS = "$(SRCROOT)/Client/Entitlements/FennecApplication.entitlements"; + CODE_SIGN_IDENTITY = "iPhone Developer"; + DEVELOPMENT_TEAM = 43AQ936H96; + ENABLE_BITCODE = NO; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(BUILD_DIR)/Release$(EFFECTIVE_PLATFORM_NAME)", + "$(PROJECT_DIR)/Carthage/Build/iOS", + "$(PROJECT_DIR)/ThirdParty/BuddyBuild", + "$(PROJECT_DIR)/ThirdParty/Leanplum", + ); + HEADER_SEARCH_PATHS = ( + "$(inherited)", + /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, + "$(SRCROOT)", + "$(SDKROOT)/usr/include/libxml2", + "$(BUILD_DIR)/Debug$(EFFECTIVE_PLATFORM_NAME)/include/**", + "$(BUILD_DIR)/Release$(EFFECTIVE_PLATFORM_NAME)/include/**", + ); + INFOPLIST_FILE = Client/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + OTHER_LDFLAGS = ( + "-ObjC", + "-lxml2", + "-framework", + BuddyBuildSDK, + "-framework", + AssetsLibrary, + "-framework", + CoreText, + "-framework", + CoreTelephony, + "-framework", + SystemConfiguration, + "-framework", + QuartzCore, + "-framework", + CoreVideo, + "-framework", + AVFoundation, + "-framework", + CoreMedia, + ); + OTHER_SWIFT_FLAGS = "-DMOZ_CHANNEL_FENNEC -DMOZ_TARGET_CLIENT"; + PRODUCT_MODULE_NAME = Client; + PRODUCT_NAME = Client; + PROVISIONING_PROFILE_SPECIFIER = "Fennec Development"; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = BUDDYBUILD; + SWIFT_OBJC_BRIDGING_HEADER = "$(PROJECT_DIR)/Client-Bridging-Header.h"; + SWIFT_VERSION = 3.0; + }; + name = Fennec; + }; + F84B21E11A090F8100AAB793 /* Fennec */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Carthage/Build/iOS", + "$(BUILD_DIR)/Release$(EFFECTIVE_PLATFORM_NAME)", + ); + INFOPLIST_FILE = ClientTests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "org.mozilla.ios.$(PRODUCT_NAME:rfc1034identifier)"; + PRODUCT_NAME = ClientTests; + SWIFT_VERSION = 3.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Client.app/Client"; + }; + name = Fennec; + }; + F84B22561A0920C600AAB793 /* Fennec */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = NO; + APPLICATION_EXTENSION_API_ONLY = YES; + CODE_SIGN_ENTITLEMENTS = "$(SRCROOT)/Extensions/Entitlements/Fennec.entitlements"; + CODE_SIGN_IDENTITY = "iPhone Developer"; + DEVELOPMENT_TEAM = 43AQ936H96; + ENABLE_BITCODE = NO; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Carthage/Build/iOS", + "$(BUILD_DIR)/Release$(EFFECTIVE_PLATFORM_NAME)", + ); + INFOPLIST_FILE = Extensions/ShareTo/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "@executable_path/../../Frameworks"; + OTHER_SWIFT_FLAGS = "-DMOZ_CHANNEL_FENNEC -DMOZ_TARGET_SHARETO"; + PRODUCT_BUNDLE_IDENTIFIER = "$(MOZ_BUNDLE_ID).$(PRODUCT_NAME:rfc1034identifier)"; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = "Fennec ShareTo Development"; + SKIP_INSTALL = YES; + SWIFT_VERSION = 3.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Fennec; + }; + F84B22681A09210A00AAB793 /* Fennec */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = NO; + APPLICATION_EXTENSION_API_ONLY = YES; + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_ENTITLEMENTS = "$(SRCROOT)/Extensions/Entitlements/Fennec.entitlements"; + CODE_SIGN_IDENTITY = "iPhone Developer"; + DEVELOPMENT_TEAM = 43AQ936H96; + ENABLE_BITCODE = NO; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Carthage/Build/iOS", + "$(BUILD_DIR)/Release$(EFFECTIVE_PLATFORM_NAME)", + ); + INFOPLIST_FILE = Extensions/SendTo/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "@executable_path/../../Frameworks"; + OTHER_SWIFT_FLAGS = "-DMOZ_CHANNEL_FENNEC -DMOZ_TARGET_SENDTO"; + PRODUCT_BUNDLE_IDENTIFIER = "$(MOZ_BUNDLE_ID).$(PRODUCT_NAME:rfc1034identifier)"; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = "Fennec SendTo Development"; + SKIP_INSTALL = YES; + SWIFT_VERSION = 3.0; + }; + name = Fennec; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 282731971ABC9BE800AA1954 /* Build configuration list for PBXNativeTarget "Sync" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 282731771ABC9BE800AA1954 /* Fennec */, + E6DCC20E1DCBB6F100CEC4B7 /* Fennec_Enterprise */, + E448FCA81AEE7A6000869B6C /* Firefox */, + E6FCC4361C40562400DF6113 /* FirefoxBeta */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Fennec; + }; + 282731981ABC9BE800AA1954 /* Build configuration list for PBXNativeTarget "SyncTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 2827317A1ABC9BE800AA1954 /* Fennec */, + E6DCC2151DCBB6F100CEC4B7 /* Fennec_Enterprise */, + E448FCA91AEE7A6000869B6C /* Firefox */, + E6FCC4371C40562400DF6113 /* FirefoxBeta */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Fennec; + }; + 288A2D9F1AB8B3260023ABC3 /* Build configuration list for PBXNativeTarget "Shared" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 288A2DA01AB8B3260023ABC3 /* Fennec */, + E6DCC20B1DCBB6F100CEC4B7 /* Fennec_Enterprise */, + E448FCA31AEE7A6000869B6C /* Firefox */, + E6FCC4311C40562400DF6113 /* FirefoxBeta */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Fennec; + }; + 2FA436141ABB83B4008031D1 /* Build configuration list for PBXNativeTarget "Account" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 2FA436151ABB83B4008031D1 /* Fennec */, + E6DCC20D1DCBB6F100CEC4B7 /* Fennec_Enterprise */, + E448FCA61AEE7A6000869B6C /* Firefox */, + E6FCC4341C40562400DF6113 /* FirefoxBeta */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Fennec; + }; + 2FA436181ABB83B4008031D1 /* Build configuration list for PBXNativeTarget "AccountTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 2FA436191ABB83B4008031D1 /* Fennec */, + E6DCC2141DCBB6F100CEC4B7 /* Fennec_Enterprise */, + E448FCA71AEE7A6000869B6C /* Firefox */, + E6FCC4351C40562400DF6113 /* FirefoxBeta */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Fennec; + }; + 2FCAE2331ABB51F900877008 /* Build configuration list for PBXNativeTarget "Storage" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 2FCAE2341ABB51F900877008 /* Fennec */, + E6DCC20C1DCBB6F100CEC4B7 /* Fennec_Enterprise */, + E448FCA41AEE7A6000869B6C /* Firefox */, + E6FCC4321C40562400DF6113 /* FirefoxBeta */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Fennec; + }; + 2FCAE2371ABB51F900877008 /* Build configuration list for PBXNativeTarget "StorageTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 2FCAE2381ABB51F900877008 /* Fennec */, + E6DCC2131DCBB6F100CEC4B7 /* Fennec_Enterprise */, + E448FCA51AEE7A6000869B6C /* Firefox */, + E6FCC4331C40562400DF6113 /* FirefoxBeta */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Fennec; + }; + 39409A521C90E68300DAE683 /* Build configuration list for PBXNativeTarget "Today" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 39409A401C90E68300DAE683 /* Fennec */, + E6DCC2091DCBB6F100CEC4B7 /* Fennec_Enterprise */, + 39409A411C90E68300DAE683 /* Firefox */, + 39409A421C90E68300DAE683 /* FirefoxBeta */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Fennec; + }; + 397848F51ED86605004C0C0B /* Build configuration list for PBXNativeTarget "NotificationService" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 397848E31ED86605004C0C0B /* Fennec */, + 397848E51ED86605004C0C0B /* Firefox */, + 397848E61ED86605004C0C0B /* FirefoxBeta */, + 3958DAB01ED98DCB0054AA27 /* Fennec_Enterprise */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Fennec; + }; + 3B43E3E91D95C48E00BBA9DB /* Build configuration list for PBXNativeTarget "StoragePerfTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 3B43E3D71D95C48E00BBA9DB /* Fennec */, + E6DCC21B1DCBB6F100CEC4B7 /* Fennec_Enterprise */, + 3B43E3D81D95C48E00BBA9DB /* Firefox */, + 3B43E3D91D95C48E00BBA9DB /* FirefoxBeta */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Fennec; + }; + 3BFE4B201D342FB900DDF53F /* Build configuration list for PBXNativeTarget "XCUITests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 3BFE4B0E1D342FB900DDF53F /* Fennec */, + E6DCC21A1DCBB6F100CEC4B7 /* Fennec_Enterprise */, + 3BFE4B0F1D342FB900DDF53F /* Firefox */, + 3BFE4B101D342FB900DDF53F /* FirefoxBeta */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Fennec; + }; + D39FA1671A83E0EC00EE869C /* Build configuration list for PBXNativeTarget "UITests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + D39FA1681A83E0EC00EE869C /* Fennec */, + E6DCC2111DCBB6F100CEC4B7 /* Fennec_Enterprise */, + D39FA1691A83E0EC00EE869C /* Release */, + E448FCA21AEE7A6000869B6C /* Firefox */, + E6FCC42F1C40562400DF6113 /* FirefoxBeta */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Fennec; + }; + E4BA8A581B4B0A1700BC2E95 /* Build configuration list for PBXNativeTarget "ViewLater" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + E4BA8A361B4B0A1700BC2E95 /* Fennec */, + E6DCC20A1DCBB6F100CEC4B7 /* Fennec_Enterprise */, + E4BA8A381B4B0A1700BC2E95 /* Firefox */, + E6FCC42E1C40562400DF6113 /* FirefoxBeta */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Fennec; + }; + E4D567531ADECE2900F1EFE7 /* Build configuration list for PBXNativeTarget "ReadingList" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + E4D567311ADECE2900F1EFE7 /* Fennec */, + E6DCC20F1DCBB6F100CEC4B7 /* Fennec_Enterprise */, + E448FCAA1AEE7A6000869B6C /* Firefox */, + E6FCC4381C40562400DF6113 /* FirefoxBeta */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Fennec; + }; + E4D567541ADECE2900F1EFE7 /* Build configuration list for PBXNativeTarget "ReadingListTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + E4D567341ADECE2900F1EFE7 /* Fennec */, + E6DCC2161DCBB6F100CEC4B7 /* Fennec_Enterprise */, + E448FCAB1AEE7A6000869B6C /* Firefox */, + E6FCC4391C40562400DF6113 /* FirefoxBeta */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Fennec; + }; + E60138631C89EAE700DF9756 /* Build configuration list for PBXNativeTarget "L10nSnapshotTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + E601384C1C89EAE600DF9756 /* Fennec */, + E6DCC2181DCBB6F100CEC4B7 /* Fennec_Enterprise */, + E601384D1C89EAE600DF9756 /* Firefox */, + E601384E1C89EAE600DF9756 /* FirefoxBeta */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Fennec; + }; + E60138641C89EAE700DF9756 /* Build configuration list for PBXNativeTarget "MarketingUITests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + E60138511C89EAE600DF9756 /* Fennec */, + E6DCC2191DCBB6F100CEC4B7 /* Fennec_Enterprise */, + E60138521C89EAE600DF9756 /* Firefox */, + E60138531C89EAE600DF9756 /* FirefoxBeta */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Fennec; + }; + E69DB08C1E97DEAA008A67E6 /* Build configuration list for PBXNativeTarget "SyncTelemetry" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + E69DB08D1E97DEAA008A67E6 /* Fennec */, + E69DB08E1E97DEAA008A67E6 /* Fennec_Enterprise */, + E69DB08F1E97DEAA008A67E6 /* Firefox */, + E69DB0901E97DEAA008A67E6 /* FirefoxBeta */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Fennec; + }; + E69DB0921E97DEAA008A67E6 /* Build configuration list for PBXNativeTarget "SyncTelemetryTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + E69DB0931E97DEAA008A67E6 /* Fennec */, + E69DB0941E97DEAA008A67E6 /* Fennec_Enterprise */, + E69DB0951E97DEAA008A67E6 /* Firefox */, + E69DB0961E97DEAA008A67E6 /* FirefoxBeta */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Fennec; + }; + E6F965381B2F1CF20034B023 /* Build configuration list for PBXNativeTarget "SharedTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + E6F965151B2F1CF20034B023 /* Fennec */, + E6DCC2171DCBB6F100CEC4B7 /* Fennec_Enterprise */, + E6F965171B2F1CF20034B023 /* Firefox */, + E6FCC43A1C40562400DF6113 /* FirefoxBeta */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Fennec; + }; + F84B21B91A090F8100AAB793 /* Build configuration list for PBXProject "Client" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + F84B21DB1A090F8100AAB793 /* Fennec */, + E6DCC2051DCBB6F100CEC4B7 /* Fennec_Enterprise */, + E448FC9D1AEE7A6000869B6C /* Firefox */, + E6FCC4291C40562400DF6113 /* FirefoxBeta */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Fennec; + }; + F84B21DD1A090F8100AAB793 /* Build configuration list for PBXNativeTarget "Client" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + F84B21DE1A090F8100AAB793 /* Fennec */, + E6DCC2061DCBB6F100CEC4B7 /* Fennec_Enterprise */, + E448FC9E1AEE7A6000869B6C /* Firefox */, + E6FCC42A1C40562400DF6113 /* FirefoxBeta */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Fennec; + }; + F84B21E01A090F8100AAB793 /* Build configuration list for PBXNativeTarget "ClientTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + F84B21E11A090F8100AAB793 /* Fennec */, + E6DCC2101DCBB6F100CEC4B7 /* Fennec_Enterprise */, + E448FC9F1AEE7A6000869B6C /* Firefox */, + E6FCC42B1C40562400DF6113 /* FirefoxBeta */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Fennec; + }; + F84B22551A0920C600AAB793 /* Build configuration list for PBXNativeTarget "ShareTo" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + F84B22561A0920C600AAB793 /* Fennec */, + E6DCC2081DCBB6F100CEC4B7 /* Fennec_Enterprise */, + E448FCA01AEE7A6000869B6C /* Firefox */, + E6FCC42D1C40562400DF6113 /* FirefoxBeta */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Fennec; + }; + F84B22671A09210A00AAB793 /* Build configuration list for PBXNativeTarget "SendTo" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + F84B22681A09210A00AAB793 /* Fennec */, + E6DCC2071DCBB6F100CEC4B7 /* Fennec_Enterprise */, + E448FCA11AEE7A6000869B6C /* Firefox */, + E6FCC42C1C40562400DF6113 /* FirefoxBeta */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Fennec; + }; +/* End XCConfigurationList section */ + }; + rootObject = F84B21B61A090F8100AAB793 /* Project object */; +} diff --git a/mobile/ios/Client.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/mobile/ios/Client.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000000..9c19f56bf0 --- /dev/null +++ b/mobile/ios/Client.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/mobile/ios/Client.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/mobile/ios/Client.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000000..0c67376eba --- /dev/null +++ b/mobile/ios/Client.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,5 @@ + + + + + diff --git a/mobile/ios/Client.xcodeproj/xcshareddata/xcbaselines/2FCAE2231ABB51F800877008.xcbaseline/FE8885C6-BB1A-48D2-9B90-E9F952C98F6B.plist b/mobile/ios/Client.xcodeproj/xcshareddata/xcbaselines/2FCAE2231ABB51F800877008.xcbaseline/FE8885C6-BB1A-48D2-9B90-E9F952C98F6B.plist new file mode 100644 index 0000000000..4ee1dc46e5 --- /dev/null +++ b/mobile/ios/Client.xcodeproj/xcshareddata/xcbaselines/2FCAE2231ABB51F800877008.xcbaseline/FE8885C6-BB1A-48D2-9B90-E9F952C98F6B.plist @@ -0,0 +1,22 @@ + + + + + classNames + + TestSQLiteHistoryFrecencyPerf + + testFrecencyPerf() + + com.apple.XCTPerformanceMetric_WallClockTime + + baselineAverage + 0.16 + baselineIntegrationDisplayName + 30 Jul 2015, Jul 30 07:02:07 + + + + + + diff --git a/mobile/ios/Client.xcodeproj/xcshareddata/xcbaselines/2FCAE2231ABB51F800877008.xcbaseline/Info.plist b/mobile/ios/Client.xcodeproj/xcshareddata/xcbaselines/2FCAE2231ABB51F800877008.xcbaseline/Info.plist new file mode 100644 index 0000000000..e2f0962393 --- /dev/null +++ b/mobile/ios/Client.xcodeproj/xcshareddata/xcbaselines/2FCAE2231ABB51F800877008.xcbaseline/Info.plist @@ -0,0 +1,40 @@ + + + + + runDestinationsByUUID + + FE8885C6-BB1A-48D2-9B90-E9F952C98F6B + + localComputer + + busSpeedInMHz + 100 + cpuCount + 1 + cpuKind + Intel Core i7 + cpuSpeedInMHz + 3100 + logicalCPUCoresPerPackage + 4 + modelCode + MacBookPro12,1 + physicalCPUCoresPerPackage + 2 + platformIdentifier + com.apple.platform.macosx + + targetArchitecture + x86_64 + targetDevice + + modelCode + iPhone7,2 + platformIdentifier + com.apple.platform.iphonesimulator + + + + + diff --git a/mobile/ios/Client.xcodeproj/xcshareddata/xcbaselines/E6F9650B1B2F1CF20034B023.xcbaseline/F9839E4F-67D2-4920-8CE8-94CEE3A6C3D7.plist b/mobile/ios/Client.xcodeproj/xcshareddata/xcbaselines/E6F9650B1B2F1CF20034B023.xcbaseline/F9839E4F-67D2-4920-8CE8-94CEE3A6C3D7.plist new file mode 100644 index 0000000000..310b1e7a0a --- /dev/null +++ b/mobile/ios/Client.xcodeproj/xcshareddata/xcbaselines/E6F9650B1B2F1CF20034B023.xcbaseline/F9839E4F-67D2-4920-8CE8-94CEE3A6C3D7.plist @@ -0,0 +1,22 @@ + + + + + classNames + + EffectiveTLDUtilsTests + + testTLDEntriesLoadFromDiskPerformance() + + com.apple.XCTPerformanceMetric_WallClockTime + + baselineAverage + 0.36 + baselineIntegrationDisplayName + Local Baseline + + + + + + diff --git a/mobile/ios/Client.xcodeproj/xcshareddata/xcbaselines/E6F9650B1B2F1CF20034B023.xcbaseline/Info.plist b/mobile/ios/Client.xcodeproj/xcshareddata/xcbaselines/E6F9650B1B2F1CF20034B023.xcbaseline/Info.plist new file mode 100644 index 0000000000..fecefec129 --- /dev/null +++ b/mobile/ios/Client.xcodeproj/xcshareddata/xcbaselines/E6F9650B1B2F1CF20034B023.xcbaseline/Info.plist @@ -0,0 +1,40 @@ + + + + + runDestinationsByUUID + + F9839E4F-67D2-4920-8CE8-94CEE3A6C3D7 + + localComputer + + busSpeedInMHz + 100 + cpuCount + 1 + cpuKind + Intel Core i7 + cpuSpeedInMHz + 2800 + logicalCPUCoresPerPackage + 8 + modelCode + MacBookPro11,3 + physicalCPUCoresPerPackage + 4 + platformIdentifier + com.apple.platform.macosx + + targetArchitecture + x86_64 + targetDevice + + modelCode + iPhone7,2 + platformIdentifier + com.apple.platform.iphonesimulator + + + + + diff --git a/mobile/ios/Client.xcodeproj/xcshareddata/xcschemes/Account.xcscheme b/mobile/ios/Client.xcodeproj/xcshareddata/xcschemes/Account.xcscheme new file mode 100644 index 0000000000..bc5dd22bd1 --- /dev/null +++ b/mobile/ios/Client.xcodeproj/xcshareddata/xcschemes/Account.xcscheme @@ -0,0 +1,104 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/mobile/ios/Client.xcodeproj/xcshareddata/xcschemes/Fennec.xcscheme b/mobile/ios/Client.xcodeproj/xcshareddata/xcschemes/Fennec.xcscheme new file mode 100644 index 0000000000..51261d4078 --- /dev/null +++ b/mobile/ios/Client.xcodeproj/xcshareddata/xcschemes/Fennec.xcscheme @@ -0,0 +1,199 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/mobile/ios/Client.xcodeproj/xcshareddata/xcschemes/Fennec_Enterprise.xcscheme b/mobile/ios/Client.xcodeproj/xcshareddata/xcschemes/Fennec_Enterprise.xcscheme new file mode 100644 index 0000000000..6453158497 --- /dev/null +++ b/mobile/ios/Client.xcodeproj/xcshareddata/xcschemes/Fennec_Enterprise.xcscheme @@ -0,0 +1,213 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/mobile/ios/Client.xcodeproj/xcshareddata/xcschemes/Fennec_Enterprise_UITests.xcscheme b/mobile/ios/Client.xcodeproj/xcshareddata/xcschemes/Fennec_Enterprise_UITests.xcscheme new file mode 100644 index 0000000000..c89b48a6b7 --- /dev/null +++ b/mobile/ios/Client.xcodeproj/xcshareddata/xcschemes/Fennec_Enterprise_UITests.xcscheme @@ -0,0 +1,319 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/mobile/ios/Client.xcodeproj/xcshareddata/xcschemes/Fennec_Enterprise_XCUITests.xcscheme b/mobile/ios/Client.xcodeproj/xcshareddata/xcschemes/Fennec_Enterprise_XCUITests.xcscheme new file mode 100644 index 0000000000..940a62ba45 --- /dev/null +++ b/mobile/ios/Client.xcodeproj/xcshareddata/xcschemes/Fennec_Enterprise_XCUITests.xcscheme @@ -0,0 +1,294 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/mobile/ios/Client.xcodeproj/xcshareddata/xcschemes/Fennec_Enterprise_XCUITests_iPad.xcscheme b/mobile/ios/Client.xcodeproj/xcshareddata/xcschemes/Fennec_Enterprise_XCUITests_iPad.xcscheme new file mode 100644 index 0000000000..d511ab999a --- /dev/null +++ b/mobile/ios/Client.xcodeproj/xcshareddata/xcschemes/Fennec_Enterprise_XCUITests_iPad.xcscheme @@ -0,0 +1,294 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/mobile/ios/Client.xcodeproj/xcshareddata/xcschemes/Firefox.xcscheme b/mobile/ios/Client.xcodeproj/xcshareddata/xcschemes/Firefox.xcscheme new file mode 100644 index 0000000000..4432205890 --- /dev/null +++ b/mobile/ios/Client.xcodeproj/xcshareddata/xcschemes/Firefox.xcscheme @@ -0,0 +1,201 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/mobile/ios/Client.xcodeproj/xcshareddata/xcschemes/FirefoxBeta.xcscheme b/mobile/ios/Client.xcodeproj/xcshareddata/xcschemes/FirefoxBeta.xcscheme new file mode 100644 index 0000000000..fe3954f939 --- /dev/null +++ b/mobile/ios/Client.xcodeproj/xcshareddata/xcschemes/FirefoxBeta.xcscheme @@ -0,0 +1,201 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/mobile/ios/Client.xcodeproj/xcshareddata/xcschemes/L10nSnapshotTests.xcscheme b/mobile/ios/Client.xcodeproj/xcshareddata/xcschemes/L10nSnapshotTests.xcscheme new file mode 100644 index 0000000000..d773100859 --- /dev/null +++ b/mobile/ios/Client.xcodeproj/xcshareddata/xcschemes/L10nSnapshotTests.xcscheme @@ -0,0 +1,93 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/mobile/ios/Client.xcodeproj/xcshareddata/xcschemes/MarketingUITests.xcscheme b/mobile/ios/Client.xcodeproj/xcshareddata/xcschemes/MarketingUITests.xcscheme new file mode 100644 index 0000000000..7f195f25a2 --- /dev/null +++ b/mobile/ios/Client.xcodeproj/xcshareddata/xcschemes/MarketingUITests.xcscheme @@ -0,0 +1,93 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/mobile/ios/Client.xcodeproj/xcshareddata/xcschemes/ReadingList.xcscheme b/mobile/ios/Client.xcodeproj/xcshareddata/xcschemes/ReadingList.xcscheme new file mode 100644 index 0000000000..5895c7f40d --- /dev/null +++ b/mobile/ios/Client.xcodeproj/xcshareddata/xcschemes/ReadingList.xcscheme @@ -0,0 +1,113 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/mobile/ios/Client.xcodeproj/xcshareddata/xcschemes/SendTo.xcscheme b/mobile/ios/Client.xcodeproj/xcshareddata/xcschemes/SendTo.xcscheme new file mode 100644 index 0000000000..e12a817e38 --- /dev/null +++ b/mobile/ios/Client.xcodeproj/xcshareddata/xcschemes/SendTo.xcscheme @@ -0,0 +1,99 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/mobile/ios/Client.xcodeproj/xcshareddata/xcschemes/ShareTo.xcscheme b/mobile/ios/Client.xcodeproj/xcshareddata/xcschemes/ShareTo.xcscheme new file mode 100644 index 0000000000..3dbd7d6e72 --- /dev/null +++ b/mobile/ios/Client.xcodeproj/xcshareddata/xcschemes/ShareTo.xcscheme @@ -0,0 +1,99 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/mobile/ios/Client.xcodeproj/xcshareddata/xcschemes/Shared.xcscheme b/mobile/ios/Client.xcodeproj/xcshareddata/xcschemes/Shared.xcscheme new file mode 100644 index 0000000000..7b3ef1cb36 --- /dev/null +++ b/mobile/ios/Client.xcodeproj/xcshareddata/xcschemes/Shared.xcscheme @@ -0,0 +1,99 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/mobile/ios/Client.xcodeproj/xcshareddata/xcschemes/Storage.xcscheme b/mobile/ios/Client.xcodeproj/xcshareddata/xcschemes/Storage.xcscheme new file mode 100644 index 0000000000..e3ed976da6 --- /dev/null +++ b/mobile/ios/Client.xcodeproj/xcshareddata/xcschemes/Storage.xcscheme @@ -0,0 +1,109 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/mobile/ios/Client.xcodeproj/xcshareddata/xcschemes/Sync.xcscheme b/mobile/ios/Client.xcodeproj/xcshareddata/xcschemes/Sync.xcscheme new file mode 100644 index 0000000000..68b214cb86 --- /dev/null +++ b/mobile/ios/Client.xcodeproj/xcshareddata/xcschemes/Sync.xcscheme @@ -0,0 +1,121 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/mobile/ios/Client.xcodeproj/xcshareddata/xcschemes/Telemetry.xcscheme b/mobile/ios/Client.xcodeproj/xcshareddata/xcschemes/Telemetry.xcscheme new file mode 100644 index 0000000000..5adf21e183 --- /dev/null +++ b/mobile/ios/Client.xcodeproj/xcshareddata/xcschemes/Telemetry.xcscheme @@ -0,0 +1,101 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/mobile/ios/Client.xcodeproj/xcshareddata/xcschemes/Today.xcscheme b/mobile/ios/Client.xcodeproj/xcshareddata/xcschemes/Today.xcscheme new file mode 100644 index 0000000000..4ac66e7ed4 --- /dev/null +++ b/mobile/ios/Client.xcodeproj/xcshareddata/xcschemes/Today.xcscheme @@ -0,0 +1,125 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/mobile/ios/Client.xcodeproj/xcshareddata/xcschemes/ViewLater.xcscheme b/mobile/ios/Client.xcodeproj/xcshareddata/xcschemes/ViewLater.xcscheme new file mode 100644 index 0000000000..75b9d5efdc --- /dev/null +++ b/mobile/ios/Client.xcodeproj/xcshareddata/xcschemes/ViewLater.xcscheme @@ -0,0 +1,99 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/mobile/ios/Client/Application/AdjustIntegration.swift b/mobile/ios/Client/Application/AdjustIntegration.swift new file mode 100644 index 0000000000..8c7e6ca290 --- /dev/null +++ b/mobile/ios/Client/Application/AdjustIntegration.swift @@ -0,0 +1,195 @@ +/* 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 AdjustSdk + +private let AdjustIntegrationErrorDomain = "org.mozilla.ios.Firefox.AdjustIntegrationErrorDomain" + +private let AdjustAttributionFileName = "AdjustAttribution.json" + +private let AdjustAppTokenKey = "AdjustAppToken" +private let AdjustEnvironmentKey = "AdjustEnvironment" + +private let AdjustSandboxEnvironment = "sandbox" +private let AdjustProductionEnvironment = "production" + +private enum AdjustEnvironment: String { + case Sandbox = "sandbox" + case Production = "production" +} + +private struct AdjustSettings { + var appToken: String + var environment: AdjustEnvironment +} + +/// Simple (singleton) object to contain all code related to Adjust. The idea is to capture all logic +/// here so that we have one single place where we can see what Adjust is doing. Ideally you only call +/// functions in this object from other parts of the application. + +class AdjustIntegration: NSObject { + let profile: Profile + + init(profile: Profile) { + self.profile = profile + } + + /// Return an ADJConfig object if Adjust has been enabled. It is determined from the values in + /// the Info.plist file if Adjust should be enabled, and if so, what its application token and + /// environment are. If those keys are either missing or empty in the Info.plist then it is + /// assumed that Adjust is not enabled for this build. + + fileprivate func getConfig() -> ADJConfig? { + guard let settings = getSettings() else { + return nil + } + + let config = ADJConfig(appToken: settings.appToken, environment: settings.environment.rawValue) + if settings.environment == .Sandbox { + config?.logLevel = ADJLogLevelDebug + } + config?.delegate = self + return config + } + + /// Returns the Adjust settings from our Info.plist. If the settings are missing or invalid, such as an unknown + /// environment, then it will return nil. + + fileprivate func getSettings() -> AdjustSettings? { + let bundle = Bundle.main + guard let adjustAppToken = bundle.object(forInfoDictionaryKey: AdjustAppTokenKey) as? String, + let adjustEnvironment = bundle.object(forInfoDictionaryKey: AdjustEnvironmentKey) as? String else { + return nil + } + guard !adjustAppToken.isEmpty && !adjustEnvironment.isEmpty else { + return nil + } + guard let environment = AdjustEnvironment(rawValue: adjustEnvironment) else { + Logger.browserLogger.error("Adjust - Invalid environment provided: \(adjustEnvironment)") + return nil + } + return AdjustSettings(appToken: adjustAppToken, environment: environment) + } + + /// Returns true if the attribution file is present. + + fileprivate func hasAttribution() throws -> Bool { + return FileManager.default.fileExists(atPath: try getAttributionPath()) + } + + /// Save an `ADJAttribution` instance to a JSON file. Throws an error if the file could not be written. The file + /// written is a JSON file with a single dictionary in it. We add one extra item to it that contains the current + /// timestamp in seconds since the UNIX epoch. + + fileprivate func saveAttribution(_ attribution: ADJAttribution) throws { + if let attributionDictionary = attribution.dictionary() { + let dictionary = NSMutableDictionary(dictionary: attributionDictionary) + dictionary["_timestamp"] = NSNumber(value: Int64(Date().timeIntervalSince1970) as Int64) + let data = try JSONSerialization.data(withJSONObject: dictionary, options: [JSONSerialization.WritingOptions.prettyPrinted]) + try data.write(to: URL(fileURLWithPath: try getAttributionPath()), options: []) + } + } + + /// Return the path to the `AdjustAttribution.json` file. Throws an `NSError` if we could not build the path. + + fileprivate func getAttributionPath() throws -> String { + guard let url = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else { + throw NSError(domain: AdjustIntegrationErrorDomain, code: -1, + userInfo: [NSLocalizedDescriptionKey: "Could not build \(AdjustAttributionFileName) path"]) + } + + return url.appendingPathComponent(AdjustAttributionFileName).path + } + + /// Return true if Adjust should be enabled. If the user has disabled the Send Anonymous Usage Data then we immediately + /// return false. Otherwise we only do one ping, which means we only enable it if we have not seen the attributiond + /// data yet. + + fileprivate func shouldEnable() throws -> Bool { + if profile.prefs.boolForKey(AppConstants.PrefSendUsageData) ?? true { + return true + } + return try hasAttribution() == false + } + + /// Return true if retention (session) tracking should be enabled. This follows the Send Anonymous Usage Data + /// setting. + + fileprivate func shouldTrackRetention() -> Bool { + return profile.prefs.boolForKey(AppConstants.PrefSendUsageData) ?? true + } +} + +extension AdjustIntegration: AdjustDelegate { + /// This is called as part of `UIApplication.didFinishLaunchingWithOptions()`. We always initialize the + /// Adjust SDK. We always let it send the initial attribution ping. Session tracking is only enabled if + /// the Send Anonymous Usage Data setting is turned on. + + func triggerApplicationDidFinishLaunchingWithOptions(_ launchOptions: [AnyHashable: Any]?) { + do { + if let config = getConfig() { + // Always initialize Adjust - otherwise we cannot enable/disable it later. Their SDK must be + // initialized through appDidFinishLaunching otherwise it will be in a bad state. + Adjust.appDidLaunch(config) + + // Disable it right now if we have the attribution and if the user has disabled session tracking. If + // we do not have attribution yet then we wait until it comes in and at that point make the decision + // to disable Adjust again. + if try hasAttribution() { + if !shouldTrackRetention() { + Logger.browserLogger.info("Adjust - Disabling because sending of usage data is not allowed") + Adjust.setEnabled(false) + } + } + } else { + Logger.browserLogger.info("Adjust - Skipping because no or invalid config found") + } + } catch let error { + Logger.browserLogger.error("Adjust - Disabling because we failed to configure: \(error)") + Adjust.setEnabled(false) + } + } + + /// This is called when Adjust has figured out the attribution. It will call us with a summary + /// of all the things it knows. Like the campaign ID. We simply save this to a local file so + /// that we know we have done a single attribution ping to Adjust. + /// + /// Here we also disable Adjust based on the Send Anonymous Usage Data setting. + + func adjustAttributionChanged(_ attribution: ADJAttribution!) { + do { + Logger.browserLogger.info("Adjust - Saving attribution info to disk") + try saveAttribution(attribution) + } catch let error { + Logger.browserLogger.error("Adjust - Failed to save attribution: \(error)") + } + // Keep Adjust enabled only if the user has allowed this + if shouldTrackRetention() { + Logger.browserLogger.info("Adjust - Enabling because user allows anonymous usage data collection") + Adjust.setEnabled(true) + } else { + Logger.browserLogger.info("Adjust - Disabling because user does not allow anonymous usage data collection") + Adjust.setEnabled(false) + } + } + + /// This is called from the Settings screen. The settings screen will remember the choice in the + /// profile and then use this method to disable or enable Adjust. + + static func setEnabled(_ enabled: Bool) { + Adjust.setEnabled(enabled) + } + + /// Store the deeplink url from Adjust SDK. Per Adjust documentation, any interstitial view launched could interfere + /// with launching the deeplink. We let the interstial view decide what to do with deeplink. + /// Ref: https://github.com/adjust/ios_sdk#deferred-deep-linking-scenario + + func adjustDeeplinkResponse(_ deeplink: URL!) -> Bool { + profile.prefs.setString("\(deeplink)", forKey: "AdjustDeeplinkKey") + return true + } + +} diff --git a/mobile/ios/Client/Application/AppDelegate.swift b/mobile/ios/Client/Application/AppDelegate.swift new file mode 100644 index 0000000000..ed26683162 --- /dev/null +++ b/mobile/ios/Client/Application/AppDelegate.swift @@ -0,0 +1,884 @@ +/* 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 Shared +import Storage +import AVFoundation +import XCGLogger +import MessageUI +import SDWebImage +import SwiftKeychainWrapper +import LocalAuthentication +import SyncTelemetry +import SwiftRouter +import Sync +import CoreSpotlight + +private let log = Logger.browserLogger + +let LatestAppVersionProfileKey = "latestAppVersion" +let AllowThirdPartyKeyboardsKey = "settings.allowThirdPartyKeyboards" +private let InitialPingSentKey = "initialPingSent" + +class AppDelegate: UIResponder, UIApplicationDelegate, UIViewControllerRestoration { + public static func viewController(withRestorationIdentifierPath identifierComponents: [Any], coder: NSCoder) -> UIViewController? { + return nil + } + + var window: UIWindow? + var browserViewController: BrowserViewController! + var rootViewController: UIViewController! + weak var profile: Profile? + var tabManager: TabManager! + var adjustIntegration: AdjustIntegration? + var applicationCleanlyBackgrounded = true + + weak var application: UIApplication? + var launchOptions: [AnyHashable: Any]? + + let appVersion = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as! String + + var openInFirefoxParams: LaunchParams? + + var receivedURLs: [URL]? + var unifiedTelemetry: UnifiedTelemetry? + + @discardableResult func application(_ application: UIApplication, willFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { + // + // Determine if the application cleanly exited last time it was used. We default to true in + // case we have never done this before. Then check if the "ApplicationCleanlyBackgrounded" user + // default exists and whether was properly set to true on app exit. + // + // Then we always set the user default to false. It will be set to true when we the application + // is backgrounded. + // + + self.applicationCleanlyBackgrounded = true + + let defaults = UserDefaults() + if defaults.object(forKey: "ApplicationCleanlyBackgrounded") != nil { + self.applicationCleanlyBackgrounded = defaults.bool(forKey: "ApplicationCleanlyBackgrounded") + } + defaults.set(false, forKey: "ApplicationCleanlyBackgrounded") + defaults.synchronize() + + // Hold references to willFinishLaunching parameters for delayed app launch + self.application = application + self.launchOptions = launchOptions + + self.window = UIWindow(frame: UIScreen.main.bounds) + self.window!.backgroundColor = UIColor.white + + // Short circuit the app if we want to email logs from the debug menu + if DebugSettingsBundleOptions.launchIntoEmailComposer { + self.window?.rootViewController = UIViewController() + presentEmailComposerWithLogs() + return true + } else { + return startApplication(application, withLaunchOptions: launchOptions) + } + } + + @discardableResult fileprivate func startApplication(_ application: UIApplication, withLaunchOptions launchOptions: [AnyHashable: Any]?) -> Bool { + log.info("startApplication begin") + + // Need to get "settings.sendUsageData" this way so that Sentry can be initialized + // before getting the Profile. + let sendUsageData = NSUserDefaultsPrefs(prefix: "profile").boolForKey(AppConstants.PrefSendUsageData) ?? true + Sentry.shared.setup(sendUsageData: sendUsageData) + + // Set the Firefox UA for browsing. + setUserAgent() + + // Start the keyboard helper to monitor and cache keyboard state. + KeyboardHelper.defaultHelper.startObserving() + + DynamicFontHelper.defaultHelper.startObserving() + + MenuHelper.defaultHelper.setItems() + + let logDate = Date() + // Create a new sync log file on cold app launch. Note that this doesn't roll old logs. + Logger.syncLogger.newLogWithDate(logDate) + + Logger.browserLogger.newLogWithDate(logDate) + + let profile = getProfile(application) + + unifiedTelemetry = UnifiedTelemetry(profile: profile) + + if !DebugSettingsBundleOptions.disableLocalWebServer { + // Set up a web server that serves us static content. Do this early so that it is ready when the UI is presented. + setUpWebServer(profile) + } + + do { + // for aural progress bar: play even with silent switch on, and do not stop audio from other apps (like music) + try AVAudioSession.sharedInstance().setCategory(AVAudioSessionCategoryPlayback, with: AVAudioSessionCategoryOptions.mixWithOthers) + } catch _ { + print("Error: Failed to assign AVAudioSession category to allow playing with silent switch on for aural progress bar") + } + + let imageStore = DiskImageStore(files: profile.files, namespace: "TabManagerScreenshots", quality: UIConstants.ScreenshotQuality) + + // Temporary fix for Bug 1390871 - NSInvalidArgumentException: -[WKContentView menuHelperFindInPage]: unrecognized selector + if #available(iOS 11, *) { + if let clazz = NSClassFromString("WKCont" + "ent" + "View"), let swizzledMethod = class_getInstanceMethod(TabWebViewMenuHelper.self, #selector(TabWebViewMenuHelper.swizzledMenuHelperFindInPage)) { + class_addMethod(clazz, MenuHelper.SelectorFindInPage, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod)) + } + } + + self.tabManager = TabManager(prefs: profile.prefs, imageStore: imageStore) + self.tabManager.stateDelegate = self + + // Add restoration class, the factory that will return the ViewController we + // will restore with. + + browserViewController = BrowserViewController(profile: self.profile!, tabManager: self.tabManager) + browserViewController.edgesForExtendedLayout = [] + + browserViewController.restorationIdentifier = NSStringFromClass(BrowserViewController.self) + browserViewController.restorationClass = AppDelegate.self + + let navigationController = UINavigationController(rootViewController: browserViewController) + navigationController.delegate = self + navigationController.isNavigationBarHidden = true + navigationController.edgesForExtendedLayout = UIRectEdge(rawValue: 0) + rootViewController = navigationController + + self.window!.rootViewController = rootViewController + + NotificationCenter.default.addObserver(forName: NSNotification.Name.FSReadingListAddReadingListItem, object: nil, queue: nil) { (notification) -> Void in + if let userInfo = notification.userInfo, let url = userInfo["URL"] as? URL { + let title = (userInfo["Title"] as? String) ?? "" + profile.readingList?.createRecordWithURL(url.absoluteString, title: title, addedBy: UIDevice.current.name) + } + } + + NotificationCenter.default.addObserver(forName: NotificationFirefoxAccountDeviceRegistrationUpdated, object: nil, queue: nil) { _ in + profile.flushAccount() + } + + // check to see if we started 'cos someone tapped on a notification. + if let localNotification = launchOptions?[UIApplicationLaunchOptionsKey.localNotification] as? UILocalNotification { + viewURLInNewTab(localNotification) + } + + adjustIntegration = AdjustIntegration(profile: profile) + + if LeanPlumClient.shouldEnable(profile: profile) { + LeanPlumClient.shared.setup(profile: profile) + LeanPlumClient.shared.set(enabled: true) + } + + self.updateAuthenticationInfo() + SystemUtils.onFirstRun() + + let fxaLoginHelper = FxALoginHelper.sharedInstance + fxaLoginHelper.application(application, didLoadProfile: profile) + + setUpDeepLinks(application: application) + + log.info("startApplication end") + return true + } + + func setUpDeepLinks(application: UIApplication) { + let router = Router.shared + let rootNav = rootViewController as! UINavigationController + + router.map("homepanel/:page", handler: { (params: [String: String]?) -> (Bool) in + guard let page = params?["page"] else { + return false + } + + assert(Thread.isMainThread, "Opening homepanels requires being invoked on the main thread") + + switch page { + case "bookmarks": + self.browserViewController.openURLInNewTab(HomePanelType.bookmarks.localhostURL, isPrivileged: true) + case "history": + self.browserViewController.openURLInNewTab(HomePanelType.history.localhostURL, isPrivileged: true) + case "new-private-tab": + self.browserViewController.openBlankNewTab(focusLocationField: false, isPrivate: true) + default: + break + } + + return true + }) + + // Route to general settings page like this: "...settings/general" + router.map("settings/:page", handler: { (params: [String: String]?) -> (Bool) in + guard let page = params?["page"] else { + return false + } + + assert(Thread.isMainThread, "Opening settings requires being invoked on the main thread") + + let settingsTableViewController = AppSettingsTableViewController() + settingsTableViewController.profile = self.profile + settingsTableViewController.tabManager = self.tabManager + settingsTableViewController.settingsDelegate = self.browserViewController + + let controller = SettingsNavigationController(rootViewController: settingsTableViewController) + controller.popoverDelegate = self.browserViewController + controller.modalPresentationStyle = UIModalPresentationStyle.formSheet + + rootNav.present(controller, animated: true, completion: nil) + + switch page { + case "newtab": + let viewController = NewTabChoiceViewController(prefs: self.getProfile(application).prefs) + controller.pushViewController(viewController, animated: true) + case "homepage": + let viewController = HomePageSettingsViewController() + viewController.profile = self.getProfile(application) + viewController.tabManager = self.tabManager + controller.pushViewController(viewController, animated: true) + case "mailto": + let viewController = OpenWithSettingsViewController(prefs: self.getProfile(application).prefs) + controller.pushViewController(viewController, animated: true) + case "search": + let viewController = SearchSettingsTableViewController() + viewController.model = self.getProfile(application).searchEngines + viewController.profile = self.getProfile(application) + controller.pushViewController(viewController, animated: true) + case "clear-private-data": + let viewController = ClearPrivateDataTableViewController() + viewController.profile = self.getProfile(application) + viewController.tabManager = self.tabManager + controller.pushViewController(viewController, animated: true) + case "fxa": + self.browserViewController.presentSignInViewController() + default: + break + } + + return true + }) + } + + func applicationWillTerminate(_ application: UIApplication) { + // We have only five seconds here, so let's hope this doesn't take too long. + self.profile?.shutdown() + + // Allow deinitializers to close our database connections. + self.profile = nil + self.tabManager = nil + self.browserViewController = nil + self.rootViewController = nil + } + + /** + * We maintain a weak reference to the profile so that we can pause timed + * syncs when we're backgrounded. + * + * The long-lasting ref to the profile lives in BrowserViewController, + * which we set in application:willFinishLaunchingWithOptions:. + * + * If that ever disappears, we won't be able to grab the profile to stop + * syncing... but in that case the profile's deinit will take care of things. + */ + func getProfile(_ application: UIApplication) -> Profile { + if let profile = self.profile { + return profile + } + let p = BrowserProfile(localName: "profile", syncDelegate: application.syncDelegate) + self.profile = p + return p + } + + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { + // Override point for customization after application launch. + var shouldPerformAdditionalDelegateHandling = true + + adjustIntegration?.triggerApplicationDidFinishLaunchingWithOptions(launchOptions) + + #if BUDDYBUILD + print("Setting up BuddyBuild SDK") + BuddyBuildSDK.setup() + #endif + + window!.makeKeyAndVisible() + + // Now roll logs. + DispatchQueue.global(qos: DispatchQoS.background.qosClass).async { + Logger.syncLogger.deleteOldLogsDownToSizeLimit() + Logger.browserLogger.deleteOldLogsDownToSizeLimit() + } + + // If a shortcut was launched, display its information and take the appropriate action + if let shortcutItem = launchOptions?[UIApplicationLaunchOptionsKey.shortcutItem] as? UIApplicationShortcutItem { + + QuickActions.sharedInstance.launchedShortcutItem = shortcutItem + // This will block "performActionForShortcutItem:completionHandler" from being called. + shouldPerformAdditionalDelegateHandling = false + } + + return shouldPerformAdditionalDelegateHandling + } + + func application(_ application: UIApplication, open url: URL, sourceApplication: String?, annotation: Any) -> Bool { + guard let components = URLComponents(url: url, resolvingAgainstBaseURL: false) else { + return false + } + + guard let urlTypes = Bundle.main.object(forInfoDictionaryKey: "CFBundleURLTypes") as? [AnyObject], + let urlSchemes = urlTypes.first?["CFBundleURLSchemes"] as? [String] else { + // Something very strange has happened; org.mozilla.Client should be the zeroeth URL type. + log.error("Custom URL schemes not available for validating") + return false + } + + guard let scheme = components.scheme, urlSchemes.contains(scheme) else { + log.warning("Cannot handle \(components.scheme ?? "nil") URL scheme") + return false + } + + guard let host = url.host else { + log.warning("Cannot handle nil URL host") + return false + } + + let query = url.getQuery() + + switch host { + case "open-url": + let url = query["url"]?.unescape() ?? "" + let isPrivate = NSString(string: query["private"] ?? "false").boolValue + + let params = LaunchParams(url: URL(string: url), isPrivate: isPrivate) + + if application.applicationState == .active { + // If we are active then we can ask the BVC to open the new tab right away. + // Otherwise, we remember the URL and we open it in applicationDidBecomeActive. + launchFromURL(params) + } else { + openInFirefoxParams = params + } + return true + case "deep-link": + guard let url = query["url"], Bundle.main.bundleIdentifier == sourceApplication else { + break + } + Router.shared.routeURL(url) + return true + case "fxa-signin": + if AppConstants.MOZ_FXA_DEEP_LINK_FORM_FILL { + // FxA form filling requires a `signin` query param and host = fxa-signin + // Ex. firefox://fxa-signin?signin=&someQuery=... + guard query["signin"] != nil else { + break + } + let fxaParams: FxALaunchParams + fxaParams = FxALaunchParams(query: query) + launchFxAFromURL(fxaParams) + return true + } + break + default: () + } + return false + } + + func launchFxAFromURL(_ params: FxALaunchParams) { + self.browserViewController.presentSignInViewController(params) + } + + func launchFromURL(_ params: LaunchParams) { + let isPrivate = params.isPrivate ?? false + if let newURL = params.url { + self.browserViewController.switchToTabForURLOrOpen(newURL, isPrivate: isPrivate, isPrivileged: false) + } else { + self.browserViewController.openBlankNewTab(focusLocationField: true, isPrivate: isPrivate) + } + + LeanPlumClient.shared.track(event: .openedNewTab, withParameters: ["Source": "External App or Extension" as AnyObject]) + } + + // We sync in the foreground only, to avoid the possibility of runaway resource usage. + // Eventually we'll sync in response to notifications. + func applicationDidBecomeActive(_ application: UIApplication) { + guard !DebugSettingsBundleOptions.launchIntoEmailComposer else { + return + } + + // + // We are back in the foreground, so set CleanlyBackgrounded to false so that we can detect that + // the application was cleanly backgrounded later. + // + + let defaults = UserDefaults() + defaults.set(false, forKey: "ApplicationCleanlyBackgrounded") + defaults.synchronize() + + if let profile = self.profile { + profile.reopen() + + if profile.prefs.boolForKey(PendingAccountDisconnectedKey) ?? false { + FxALoginHelper.sharedInstance.applicationDidDisconnect(application) + } + + profile.syncManager.applicationDidBecomeActive() + } + + // We could load these here, but then we have to futz with the tab counter + // and making NSURLRequests. + self.browserViewController.loadQueuedTabs(receivedURLs: self.receivedURLs) + self.receivedURLs = nil + application.applicationIconBadgeNumber = 0 + + // handle quick actions is available + let quickActions = QuickActions.sharedInstance + if let shortcut = quickActions.launchedShortcutItem { + // dispatch asynchronously so that BVC is all set up for handling new tabs + // when we try and open them + quickActions.handleShortCutItem(shortcut, withBrowserViewController: browserViewController) + quickActions.launchedShortcutItem = nil + } + + // Check if we have a URL from an external app or extension waiting to launch, + // then launch it on the main thread. + if let params = openInFirefoxParams { + openInFirefoxParams = nil + DispatchQueue.main.async { + self.launchFromURL(params) + } + } + + UnifiedTelemetry.recordEvent(category: .action, method: .foreground, object: .app) + } + + func applicationDidEnterBackground(_ application: UIApplication) { + // + // At this point we are happy to mark the app as CleanlyBackgrounded. If a crash happens in background + // sync then that crash will still be reported. But we won't bother the user with the Restore Tabs + // dialog. We don't have to because at this point we already saved the tab state properly. + // + + let defaults = UserDefaults() + defaults.set(true, forKey: "ApplicationCleanlyBackgrounded") + defaults.synchronize() + + syncOnDidEnterBackground(application: application) + + UnifiedTelemetry.recordEvent(category: .action, method: .background, object: .app) + } + + fileprivate func syncOnDidEnterBackground(application: UIApplication) { + guard let profile = self.profile else { + return + } + + profile.syncManager.applicationDidEnterBackground() + + var taskId: UIBackgroundTaskIdentifier = 0 + taskId = application.beginBackgroundTask (expirationHandler: { _ in + print("Running out of background time, but we have a profile shutdown pending.") + self.shutdownProfileWhenNotActive(application) + application.endBackgroundTask(taskId) + }) + + if profile.hasSyncableAccount() { + profile.syncManager.syncEverything(why: .backgrounded).uponQueue(DispatchQueue.main) { _ in + self.shutdownProfileWhenNotActive(application) + application.endBackgroundTask(taskId) + } + } else { + profile.shutdown() + application.endBackgroundTask(taskId) + } + } + + fileprivate func shutdownProfileWhenNotActive(_ application: UIApplication) { + // Only shutdown the profile if we are not in the foreground + guard application.applicationState != UIApplicationState.active else { + return + } + + profile?.shutdown() + } + + func applicationWillEnterForeground(_ application: UIApplication) { + // The reason we need to call this method here instead of `applicationDidBecomeActive` + // is that this method is only invoked whenever the application is entering the foreground where as + // `applicationDidBecomeActive` will get called whenever the Touch ID authentication overlay disappears. + self.updateAuthenticationInfo() + } + + fileprivate func updateAuthenticationInfo() { + if let authInfo = KeychainWrapper.sharedAppContainerKeychain.authenticationInfo() { + if !LAContext().canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: nil) { + authInfo.useTouchID = false + KeychainWrapper.sharedAppContainerKeychain.setAuthenticationInfo(authInfo) + } + } + } + + fileprivate func setUpWebServer(_ profile: Profile) { + let server = WebServer.sharedInstance + ReaderModeHandlers.register(server, profile: profile) + ErrorPageHelper.register(server, certStore: profile.certStore) + AboutHomeHandler.register(server) + AboutLicenseHandler.register(server) + SessionRestoreHandler.register(server) + + // Bug 1223009 was an issue whereby CGDWebserver crashed when moving to a background task + // catching and handling the error seemed to fix things, but we're not sure why. + // Either way, not implicitly unwrapping a try is not a great way of doing things + // so this is better anyway. + do { + try server.start() + } catch let err as NSError { + print("Error: Unable to start WebServer \(err)") + } + } + + fileprivate func setUserAgent() { + let firefoxUA = UserAgent.defaultUserAgent() + + // Set the UA for WKWebView (via defaults), the favicon fetcher, and the image loader. + // This only needs to be done once per runtime. Note that we use defaults here that are + // readable from extensions, so they can just use the cached identifier. + let defaults = UserDefaults(suiteName: AppInfo.sharedContainerIdentifier)! + defaults.register(defaults: ["UserAgent": firefoxUA]) + + SDWebImageDownloader.shared().setValue(firefoxUA, forHTTPHeaderField: "User-Agent") + + // Record the user agent for use by search suggestion clients. + SearchViewController.userAgent = firefoxUA + + // Some sites will only serve HTML that points to .ico files. + // The FaviconFetcher is explicitly for getting high-res icons, so use the desktop user agent. + FaviconFetcher.userAgent = UserAgent.desktopUserAgent() + } + + func application(_ application: UIApplication, handleActionWithIdentifier identifier: String?, for notification: UILocalNotification, completionHandler: @escaping () -> Void) { + if let actionId = identifier { + if let action = SentTabAction(rawValue: actionId) { + viewURLInNewTab(notification) + switch action { + case .bookmark: + addBookmark(notification) + break + case .readingList: + addToReadingList(notification) + break + default: + break + } + } else { + print("ERROR: Unknown notification action received") + } + } else { + print("ERROR: Unknown notification received") + } + } + + func application(_ application: UIApplication, didReceive notification: UILocalNotification) { + viewURLInNewTab(notification) + } + + fileprivate func presentEmailComposerWithLogs() { + if let buildNumber = Bundle.main.object(forInfoDictionaryKey: String(kCFBundleVersionKey)) as? NSString { + let mailComposeViewController = MFMailComposeViewController() + mailComposeViewController.mailComposeDelegate = self + mailComposeViewController.setSubject("Debug Info for iOS client version v\(appVersion) (\(buildNumber))") + + if DebugSettingsBundleOptions.attachLogsToDebugEmail { + do { + let logNamesAndData = try Logger.diskLogFilenamesAndData() + logNamesAndData.forEach { nameAndData in + if let data = nameAndData.1 { + mailComposeViewController.addAttachmentData(data, mimeType: "text/plain", fileName: nameAndData.0) + } + } + } catch _ { + print("Failed to retrieve logs from device") + } + } + + if DebugSettingsBundleOptions.attachTabStateToDebugEmail { + if let tabStateDebugData = TabManager.tabRestorationDebugInfo().data(using: String.Encoding.utf8) { + mailComposeViewController.addAttachmentData(tabStateDebugData, mimeType: "text/plain", fileName: "tabState.txt") + } + + if let tabStateData = TabManager.tabArchiveData() { + mailComposeViewController.addAttachmentData(tabStateData as Data, mimeType: "application/octet-stream", fileName: "tabsState.archive") + } + } + + self.window?.rootViewController?.present(mailComposeViewController, animated: true, completion: nil) + } + } + + func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([Any]?) -> Void) -> Bool { + + // If the `NSUserActivity` has a `webpageURL`, it is either a deep link or an old history item + // reached via a "Spotlight" search before we began indexing visited pages via CoreSpotlight. + if let url = userActivity.webpageURL { + let query = url.getQuery() + + // Check for fxa sign-in code and launch the login screen directly + if query["signin"] != nil { + browserViewController.launchFxAFromDeeplinkURL(url) + return true + } + + // Per Adjust documenation, https://docs.adjust.com/en/universal-links/#running-campaigns-through-universal-links, + // it is recommended that links contain the `deep_link` query parameter. This link will also + // be url encoded. + if let deepLink = query["deep_link"]?.removingPercentEncoding, let url = URL(string: deepLink) { + browserViewController.switchToTabForURLOrOpen(url, isPrivileged: true) + return true + } + + browserViewController.switchToTabForURLOrOpen(url, isPrivileged: true) + return true + } + + // Otherwise, check if the `NSUserActivity` is a CoreSpotlight item and switch to its tab or + // open a new one. + if userActivity.activityType == CSSearchableItemActionType { + if let userInfo = userActivity.userInfo, + let urlString = userInfo[CSSearchableItemActivityIdentifier] as? String, + let url = URL(string: urlString) { + browserViewController.switchToTabForURLOrOpen(url, isPrivileged: true) + return true + } + } + + return false + } + + fileprivate func viewURLInNewTab(_ notification: UILocalNotification) { + if let alertURL = notification.userInfo?[TabSendURLKey] as? String { + if let urlToOpen = URL(string: alertURL) { + browserViewController.openURLInNewTab(urlToOpen, isPrivileged: true) + } + } + } + + fileprivate func addBookmark(_ notification: UILocalNotification) { + if let alertURL = notification.userInfo?[TabSendURLKey] as? String, + let title = notification.userInfo?[TabSendTitleKey] as? String { + let tabState = TabState(isPrivate: false, desktopSite: false, isBookmarked: false, url: URL(string: alertURL), title: title, favicon: nil) + browserViewController.addBookmark(tabState) + + let userData = [QuickActions.TabURLKey: alertURL, + QuickActions.TabTitleKey: title] + QuickActions.sharedInstance.addDynamicApplicationShortcutItemOfType(.openLastBookmark, withUserData: userData, toApplication: UIApplication.shared) + } + } + + fileprivate func addToReadingList(_ notification: UILocalNotification) { + if let alertURL = notification.userInfo?[TabSendURLKey] as? String, + let title = notification.userInfo?[TabSendTitleKey] as? String { + if let urlToOpen = URL(string: alertURL) { + NotificationCenter.default.post(name: NSNotification.Name.FSReadingListAddReadingListItem, object: self, userInfo: ["URL": urlToOpen, "Title": title]) + } + } + } + + func application(_ application: UIApplication, performActionFor shortcutItem: UIApplicationShortcutItem, completionHandler: @escaping (Bool) -> Void) { + let handledShortCutItem = QuickActions.sharedInstance.handleShortCutItem(shortcutItem, withBrowserViewController: browserViewController) + + completionHandler(handledShortCutItem) + } +} + +// MARK: - Root View Controller Animations +extension AppDelegate: UINavigationControllerDelegate { + func navigationController(_ navigationController: UINavigationController, animationControllerFor operation: UINavigationControllerOperation, from fromVC: UIViewController, to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? { + if operation == UINavigationControllerOperation.push { + return BrowserToTrayAnimator() + } else if operation == UINavigationControllerOperation.pop { + return TrayToBrowserAnimator() + } else { + return nil + } + } +} + +extension AppDelegate: TabManagerStateDelegate { + func tabManagerWillStoreTabs(_ tabs: [Tab]) { + // It is possible that not all tabs have loaded yet, so we filter out tabs with a nil URL. + let storedTabs: [RemoteTab] = tabs.flatMap( Tab.toTab ) + + // Don't insert into the DB immediately. We tend to contend with more important + // work like querying for top sites. + let queue = DispatchQueue.global(qos: DispatchQoS.background.qosClass) + queue.asyncAfter(deadline: DispatchTime.now() + Double(Int64(ProfileRemoteTabsSyncDelay * Double(NSEC_PER_MSEC))) / Double(NSEC_PER_SEC)) { + self.profile?.storeTabs(storedTabs) + } + } +} + +extension AppDelegate: MFMailComposeViewControllerDelegate { + func mailComposeController(_ controller: MFMailComposeViewController, didFinishWith result: MFMailComposeResult, error: Error?) { + // Dismiss the view controller and start the app up + controller.dismiss(animated: true, completion: nil) + startApplication(application!, withLaunchOptions: self.launchOptions) + } +} + +extension AppDelegate { + func application(_ application: UIApplication, didRegister notificationSettings: UIUserNotificationSettings) { + FxALoginHelper.sharedInstance.application(application, didRegisterUserNotificationSettings: notificationSettings) + } +} + +extension AppDelegate { + func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) { + FxALoginHelper.sharedInstance.apnsRegisterDidSucceed(deviceToken) + } + + func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) { + print("failed to register. \(error)") + FxALoginHelper.sharedInstance.apnsRegisterDidFail() + } + + func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable: Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) { + if Logger.logPII && log.isEnabledFor(level: .info) { + NSLog("APNS NOTIFICATION \(userInfo)") + } + + // At this point, we know that NotificationService has been run. + // We get to this point if the notification was received while the app was in the foreground + // OR the app was backgrounded and now the user has tapped on the notification. + // Either way, if this method is being run, then the app is foregrounded. + + // Either way, we should zero the badge number. + application.applicationIconBadgeNumber = 0 + + guard let profile = self.profile else { + return completionHandler(.noData) + } + + // NotificationService will have decrypted the push message, and done some syncing + // activity. If the `client` collection was synced, and there are `displayURI` commands (i.e. sent tabs) + // NotificationService will have collected them for us in the userInfo. + if let serializedTabs = userInfo["sentTabs"] as? [[String: String]] { + // Let's go ahead and open those. + let receivedURLs = serializedTabs.flatMap { item -> URL? in + guard let tabURL = item["url"] else { + return nil + } + return URL(string: tabURL) + } + + if receivedURLs.count > 0 { + // Remember which URLs we received so we can filter them out later when + // loading the queued tabs. + self.receivedURLs = receivedURLs + + // If we're in the foreground, load the queued tabs now. + if application.applicationState == UIApplicationState.active { + DispatchQueue.main.async { + self.browserViewController.loadQueuedTabs(receivedURLs: self.receivedURLs) + self.receivedURLs = nil + } + } + + return completionHandler(.newData) + } + } + + // By now, we've dealt with any sent tab notifications. + // + // The only thing left to do now is to perform actions that can only be performed + // while the app is foregrounded. + // + // Use the push message handler to re-parse the message, + // this time with a BrowserProfile and processing the return + // differently than in NotificationService. + let handler = FxAPushMessageHandler(with: profile) + handler.handle(userInfo: userInfo).upon { res in + if let message = res.successValue { + switch message { + case .accountVerified: + _ = handler.postVerification() + case .thisDeviceDisconnected: + FxALoginHelper.sharedInstance.applicationDidDisconnect(application) + default: + break + } + } + + completionHandler(res.isSuccess ? .newData : .failed) + } + } + + func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable: Any]) { + let completionHandler: (UIBackgroundFetchResult) -> Void = { _ in } + self.application(application, didReceiveRemoteNotification: userInfo, fetchCompletionHandler: completionHandler) + } +} + +struct FxALaunchParams { + var query: [String: String] +} + +struct LaunchParams { + let url: URL? + let isPrivate: Bool? +} + +extension UIApplication { + var syncDelegate: SyncDelegate { + return AppSyncDelegate(app: self) + } + + static var isInPrivateMode: Bool { + let appDelegate = UIApplication.shared.delegate as? AppDelegate + return appDelegate?.browserViewController.tabManager.selectedTab?.isPrivate ?? false + } +} + +class AppSyncDelegate: SyncDelegate { + let app: UIApplication + + init(app: UIApplication) { + self.app = app + } + + open func displaySentTab(for url: URL, title: String, from deviceName: String?) { + DispatchQueue.main.sync { + if let appDelegate = app.delegate as? AppDelegate, app.applicationState == .active { + appDelegate.browserViewController.switchToTabForURLOrOpen(url, isPrivileged: false) + return + } + + // check to see what the current notification settings are and only try and send a notification if + // the user has agreed to them + if let currentSettings = app.currentUserNotificationSettings { + if currentSettings.types.rawValue & UIUserNotificationType.alert.rawValue != 0 { + if Logger.logPII { + log.info("Displaying notification for URL \(url.absoluteString)") + } + + let notification = UILocalNotification() + notification.fireDate = Date() + notification.timeZone = NSTimeZone.default + let title: String + if let deviceName = deviceName { + title = String(format: Strings.SentTab_TabArrivingNotification_WithDevice_title, deviceName) + } else { + title = Strings.SentTab_TabArrivingNotification_NoDevice_title + } + notification.alertTitle = title + notification.alertBody = url.absoluteDisplayExternalString + notification.userInfo = [TabSendURLKey: url.absoluteString, TabSendTitleKey: title] + notification.alertAction = nil + + // Restore this when we fix Bug 1364420. + // notification.category = TabSendCategory + + app.presentLocalNotificationNow(notification) + } + } + } + } +} diff --git a/mobile/ios/Client/Application/Crasher.h b/mobile/ios/Client/Application/Crasher.h new file mode 100644 index 0000000000..a4f782c497 --- /dev/null +++ b/mobile/ios/Client/Application/Crasher.h @@ -0,0 +1,13 @@ +// +// Crasher.h +// Client +// +// Created by Steph Leroux on 2015-10-08. +// Copyright © 2015 Mozilla. All rights reserved. +// + +#import + +@interface Crasher : NSObject + +@end diff --git a/mobile/ios/Client/Application/Crasher.m b/mobile/ios/Client/Application/Crasher.m new file mode 100644 index 0000000000..97c3c687a1 --- /dev/null +++ b/mobile/ios/Client/Application/Crasher.m @@ -0,0 +1,13 @@ +// +// Crasher.m +// Client +// +// Created by Steph Leroux on 2015-10-08. +// Copyright © 2015 Mozilla. All rights reserved. +// + +#import "Crasher.h" + +@implementation Crasher + +@end diff --git a/mobile/ios/Client/Application/DebugSettingsBundleOptions.swift b/mobile/ios/Client/Application/DebugSettingsBundleOptions.swift new file mode 100644 index 0000000000..c4f85fe00a --- /dev/null +++ b/mobile/ios/Client/Application/DebugSettingsBundleOptions.swift @@ -0,0 +1,35 @@ +/* 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 MessageUI + +struct DebugSettingsBundleOptions { + + /// Don't restore tabs on app launch + static var skipSessionRestore: Bool { + return UserDefaults.standard.bool(forKey: "SettingsBundleSkipSessionRestore") + } + + /// Disable the local web server we use for restoration, error pages, etc + static var disableLocalWebServer: Bool { + return UserDefaults.standard.bool(forKey: "SettingsBundleDisableLocalWebServer") + } + + /// When enabled, the app launch will be replaced with the mail compose view appearing with the device + /// logs pre-attached. When the mail is sent, the app continues launching normally. + static var launchIntoEmailComposer: Bool { + return ((attachTabStateToDebugEmail || attachLogsToDebugEmail) && MFMailComposeViewController.canSendMail()) + } + + /// When enabled, the email composer will have the tab state attached. + static var attachTabStateToDebugEmail: Bool { + return UserDefaults.standard.bool(forKey: "SettingsBundleEmailTabState") + } + + /// When enabled, the email composer will have the application logs attached. + static var attachLogsToDebugEmail: Bool { + return UserDefaults.standard.bool(forKey: "SettingsBundleEmailLogsOnLaunch") + } +} diff --git a/mobile/ios/Client/Application/LaunchScreen.xib b/mobile/ios/Client/Application/LaunchScreen.xib new file mode 100644 index 0000000000..702a0a9655 --- /dev/null +++ b/mobile/ios/Client/Application/LaunchScreen.xib @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/mobile/ios/Client/Application/LeanplumIntegration.swift b/mobile/ios/Client/Application/LeanplumIntegration.swift new file mode 100644 index 0000000000..b105773e3c --- /dev/null +++ b/mobile/ios/Client/Application/LeanplumIntegration.swift @@ -0,0 +1,320 @@ +/* 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 AdSupport +import Shared +import Leanplum + +private let LPAppIdKey = "LeanplumAppId" +private let LPProductionKeyKey = "LeanplumProductionKey" +private let LPDevelopmentKeyKey = "LeanplumDevelopmentKey" +private let AppRequestedUserNotificationsPrefKey = "applicationDidRequestUserNotificationPermissionPrefKey" + +// FxA Custom Leanplum message template for A/B testing push notifications. +private struct LPMessage { + static let FxAPrePush = "FxA Prepush v1" + static let ArgAcceptAction = "Accept action" + static let ArgCancelAction = "Cancel action" + static let ArgTitleText = "Title.Text" + static let ArgTitleColor = "Title.Color" + static let ArgMessageText = "Message.Text" + static let ArgMessageColor = "Message.Color" + static let ArgAcceptButtonText = "Accept button.Text" + static let ArgCancelButtonText = "Cancel button.Text" + static let ArgCancelButtonTextColor = "Cancel button.Text color" + // These defaults are overridden though Leanplum webUI + static let DefaultAskToAskTitle = NSLocalizedString("Firefox Sync Requires Push", comment: "Default push to ask title") + static let DefaultAskToAskMessage = NSLocalizedString("Firefox will stay in sync faster with Push Notifications enabled.", comment: "Default push to ask message") + static let DefaultOkButtonText = NSLocalizedString("Enable Push", comment: "Default push alert ok button text") + static let DefaultLaterButtonText = NSLocalizedString("Don't Enable", comment: "Default push alert cancel button text") +} + +private let log = Logger.browserLogger + +enum LPEvent: String { + case firstRun = "E_First_Run" + case secondRun = "E_Second_Run" + case openedApp = "E_Opened_App" + case dismissedOnboarding = "E_Dismissed_Onboarding" + case openedLogins = "Opened Login Manager" + case openedBookmark = "E_Opened_Bookmark" + case openedNewTab = "E_Opened_New_Tab" + case openedPocketStory = "E_Opened_Pocket_Story" + case interactWithURLBar = "E_Interact_With_Search_URL_Area" + case savedBookmark = "E_Saved_Bookmark" + case openedTelephoneLink = "Opened Telephone Link" + case openedMailtoLink = "E_Opened_Mailto_Link" + case saveImage = "E_Download_Media_Saved_Image" + case savedLoginAndPassword = "E_Saved_Login_And_Password" + case clearPrivateData = "E_Cleared_Private_Data" + case downloadedFocus = "E_User_Downloaded_Focus" + case downloadedPocket = "E_User_Downloaded_Pocket" + case userSharedWebpage = "E_User_Tapped_Share_Button" + case signsInFxa = "E_User_Signed_In_To_FxA" + case useReaderView = "E_User_Used_Reader_View" + case trackingProtectionSettings = "E_Tracking_Protection_Settings_Changed" +} + +struct LPAttributeKey { + static let focusInstalled = "Focus Installed" + static let klarInstalled = "Klar Installed" + static let signedInSync = "Signed In Sync" + static let mailtoIsDefault = "Mailto Is Default" + static let pocketInstalled = "Pocket Installed" + static let telemetryOptIn = "Telemetry Opt In" +} + +struct MozillaAppSchemes { + static let focus = "firefox-focus" + static let focusDE = "firefox-klar" + static let pocket = "pocket" +} + +private let supportedLocales = ["en_US", "de_DE", "en_GB", "en_CA", "en_AU", "zh_TW", "en_HK", "en_SG", + "fr_FR", "it_IT", "id_ID", "id_ID", "pt_BR", "pl_PL", "ru_RU", "es_ES", "es_MX"] + +private struct LPSettings { + var appId: String + var developmentKey: String + var productionKey: String +} + +class LeanPlumClient { + static let shared = LeanPlumClient() + + // Setup + private weak var profile: Profile? + private var prefs: Prefs? { return profile?.prefs } + private var enabled: Bool = true + + // This defines an external Leanplum varible to enable/disable FxA prepush dialogs. + // The primary result is having a feature flag controlled by Leanplum, and falling back + // to prompting with native push permissions. + private var useFxAPrePush: LPVar = LPVar.define("useFxAPrePush", with: false) + + private func isPrivateMode() -> Bool { + // Need to be run on main thread since isInPrivateMode requires to be on the main thread. + assert(Thread.isMainThread) + return UIApplication.isInPrivateMode + } + + func isLPEnabled() -> Bool { + return enabled && Leanplum.hasStarted() + } + + static func shouldEnable(profile: Profile) -> Bool { + return AppConstants.MOZ_ENABLE_LEANPLUM && (profile.prefs.boolForKey(AppConstants.PrefSendUsageData) ?? true) + } + + func setup(profile: Profile) { + self.profile = profile + } + + fileprivate func start() { + guard let settings = getSettings(), supportedLocales.contains(Locale.current.identifier), !Leanplum.hasStarted() else { + enabled = false + log.error("LeanplumIntegration - Could not be started") + return + } + + if UIDevice.current.name.contains("MozMMADev") { + log.info("LeanplumIntegration - Setting up for Development") + Leanplum.setDeviceId(UIDevice.current.identifierForVendor?.uuidString) + Leanplum.setAppId(settings.appId, withDevelopmentKey: settings.developmentKey) + } else { + log.info("LeanplumIntegration - Setting up for Production") + Leanplum.setAppId(settings.appId, withProductionKey: settings.productionKey) + } + + Leanplum.syncResourcesAsync(true) + + let attributes: [AnyHashable: Any] = [ + LPAttributeKey.mailtoIsDefault: mailtoIsDefault(), + LPAttributeKey.focusInstalled: focusInstalled(), + LPAttributeKey.klarInstalled: klarInstalled(), + LPAttributeKey.pocketInstalled: pocketInstalled(), + LPAttributeKey.signedInSync: profile?.hasAccount() ?? false + ] + + self.setupCustomTemplates() + + Leanplum.start(withUserId: nil, userAttributes: attributes, responseHandler: { _ in + self.track(event: .openedApp) + + // We need to check if the app is a clean install to use for + // preventing the What's New URL from appearing. + if self.prefs?.intForKey(IntroViewControllerSeenProfileKey) == nil { + self.prefs?.setString(AppInfo.appVersion, forKey: LatestAppVersionProfileKey) + self.track(event: .firstRun) + } else if self.prefs?.boolForKey("SecondRun") == nil { + self.prefs?.setBool(true, forKey: "SecondRun") + self.track(event: .secondRun) + } + + self.checkIfAppWasInstalled(key: PrefsKeys.HasFocusInstalled, isAppInstalled: self.focusInstalled(), lpEvent: .downloadedFocus) + self.checkIfAppWasInstalled(key: PrefsKeys.HasPocketInstalled, isAppInstalled: self.pocketInstalled(), lpEvent: .downloadedPocket) + }) + } + + // Events + func track(event: LPEvent, withParameters parameters: [String: AnyObject]? = nil) { + guard isLPEnabled() else { + return + } + ensureMainThread { + guard !self.isPrivateMode() else { + return + } + if let params = parameters { + Leanplum.track(event.rawValue, withParameters: params) + } else { + Leanplum.track(event.rawValue) + } + } + } + + func set(attributes: [AnyHashable: Any]) { + guard isLPEnabled() else { + return + } + ensureMainThread { + if !self.isPrivateMode() { + Leanplum.setUserAttributes(attributes) + } + } + } + + func set(enabled: Bool) { + // Setting up Test Mode stops sending things to server. + if enabled { start() } + self.enabled = enabled + Leanplum.setTestModeEnabled(!enabled) + } + + func isFxAPrePushEnabled() -> Bool { + return AppConstants.MOZ_FXA_LEANPLUM_AB_PUSH_TEST && useFxAPrePush.boolValue() + } + + /* + This is used to determine if an app was installed after firefox was installed + */ + private func checkIfAppWasInstalled(key: String, isAppInstalled: Bool, lpEvent: LPEvent) { + // if no key is present. create one and set it. + // if the app is already installed then the flag will set true and the second block will never run + if self.prefs?.boolForKey(key) == nil { + self.prefs?.setBool(isAppInstalled, forKey: key) + } + // on a subsquent launch if the app is installed and the key is false then switch the flag to true + if !(self.prefs?.boolForKey(key) ?? false), isAppInstalled { + self.prefs?.setBool(isAppInstalled, forKey: key) + self.track(event: lpEvent) + } + } + + private func canOpenApp(scheme: String) -> Bool { + return URL(string: "\(scheme)://").flatMap { UIApplication.shared.canOpenURL($0) } ?? false + } + + private func focusInstalled() -> Bool { + return canOpenApp(scheme: MozillaAppSchemes.focus) + } + + private func klarInstalled() -> Bool { + return canOpenApp(scheme: MozillaAppSchemes.focusDE) + } + + private func pocketInstalled() -> Bool { + return canOpenApp(scheme: MozillaAppSchemes.pocket) + } + + private func mailtoIsDefault() -> Bool { + return (prefs?.stringForKey(PrefsKeys.KeyMailToOption) ?? "mailto:") == "mailto:" + } + + private func getSettings() -> LPSettings? { + let bundle = Bundle.main + guard let appId = bundle.object(forInfoDictionaryKey: LPAppIdKey) as? String, + let productionKey = bundle.object(forInfoDictionaryKey: LPProductionKeyKey) as? String, + let developmentKey = bundle.object(forInfoDictionaryKey: LPDevelopmentKeyKey) as? String else { + return nil + } + return LPSettings(appId: appId, developmentKey: developmentKey, productionKey: productionKey) + } + + // This must be called before `Leanplum.start` in order to correctly setup + // custom message templates. + private func setupCustomTemplates() { + // These properties are exposed through the Leanplum web interface. + // Ref: https://github.com/Leanplum/Leanplum-iOS-Samples/blob/master/iOS_customMessageTemplates/iOS_customMessageTemplates/LPMessageTemplates.m + let args: [LPActionArg] = [ + LPActionArg(named: LPMessage.ArgTitleText, with: LPMessage.DefaultAskToAskTitle), + LPActionArg(named: LPMessage.ArgTitleColor, with: UIColor.black), + LPActionArg(named: LPMessage.ArgMessageText, with: LPMessage.DefaultAskToAskMessage), + LPActionArg(named: LPMessage.ArgMessageColor, with: UIColor.black), + LPActionArg(named: LPMessage.ArgAcceptButtonText, with: LPMessage.DefaultOkButtonText), + LPActionArg(named: LPMessage.ArgCancelAction, withAction: nil), + LPActionArg(named: LPMessage.ArgCancelButtonText, with: LPMessage.DefaultLaterButtonText), + LPActionArg(named: LPMessage.ArgCancelButtonTextColor, with: UIColor.gray) + ] + + let responder: LeanplumActionBlock = { (context) -> Bool in + // Before proceeding, double check that Leanplum FxA prepush config value has been enabled. + if !self.isFxAPrePushEnabled() { + return false + } + + guard let context = context else { + return false + } + + // Don't display permission screen if they have already allowed/disabled push permissions + if self.prefs?.boolForKey(AppRequestedUserNotificationsPrefKey) ?? false { + FxALoginHelper.sharedInstance.readyForSyncing() + return false + } + + // Present Alert View onto the current top view controller + let rootViewController = UIApplication.topViewController() + let alert = UIAlertController(title: context.stringNamed(LPMessage.ArgTitleText), message: context.stringNamed(LPMessage.ArgMessageText), preferredStyle: .alert) + + alert.addAction(UIAlertAction(title: context.stringNamed(LPMessage.ArgCancelButtonText), style: .cancel, handler: { (action) -> Void in + // Log cancel event and call ready for syncing + context.runTrackedActionNamed(LPMessage.ArgCancelAction) + FxALoginHelper.sharedInstance.readyForSyncing() + })) + + alert.addAction(UIAlertAction(title: context.stringNamed(LPMessage.ArgAcceptButtonText), style: .default, handler: { (action) -> Void in + // Log accept event and present push permission modal + context.runTrackedActionNamed(LPMessage.ArgAcceptAction) + FxALoginHelper.sharedInstance.requestUserNotifications(UIApplication.shared) + self.prefs?.setBool(true, forKey: AppRequestedUserNotificationsPrefKey) + })) + + rootViewController?.present(alert, animated: true, completion: nil) + return true + } + + // Register or update the custom Leanplum message + Leanplum.defineAction(LPMessage.FxAPrePush, of: kLeanplumActionKindMessage, withArguments: args, withOptions: [:], withResponder: responder) + } +} + +extension UIApplication { + // Extension to get the current top most view controller + class func topViewController(base: UIViewController? = UIApplication.shared.keyWindow?.rootViewController) -> UIViewController? { + if let nav = base as? UINavigationController { + return topViewController(base: nav.visibleViewController) + } + if let tab = base as? UITabBarController { + if let selected = tab.selectedViewController { + return topViewController(base: selected) + } + } + if let presented = base?.presentedViewController { + return topViewController(base: presented) + } + return base + } +} diff --git a/mobile/ios/Client/Application/QuickActions.swift b/mobile/ios/Client/Application/QuickActions.swift new file mode 100644 index 0000000000..6bb06149a6 --- /dev/null +++ b/mobile/ios/Client/Application/QuickActions.swift @@ -0,0 +1,142 @@ +/* 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 Storage + +import Shared +import XCGLogger + +enum ShortcutType: String { + case newTab = "NewTab" + case newPrivateTab = "NewPrivateTab" + case openLastBookmark = "OpenLastBookmark" + case qrCode = "QRCode" + + init?(fullType: String) { + guard let last = fullType.components(separatedBy: ".").last else { return nil } + + self.init(rawValue: last) + } + + var type: String { + return Bundle.main.bundleIdentifier! + ".\(self.rawValue)" + } +} + +protocol QuickActionHandlerDelegate { + func handleShortCutItemType(_ type: ShortcutType, userData: [String: NSSecureCoding]?) +} + +class QuickActions: NSObject { + + fileprivate let log = Logger.browserLogger + + static let QuickActionsVersion = "1.0" + static let QuickActionsVersionKey = "dynamicQuickActionsVersion" + + static let TabURLKey = "url" + static let TabTitleKey = "title" + + fileprivate let lastBookmarkTitle = NSLocalizedString("Open Last Bookmark", tableName: "3DTouchActions", comment: "String describing the action of opening the last added bookmark from the home screen Quick Actions via 3D Touch") + fileprivate let _lastTabTitle = NSLocalizedString("Open Last Tab", tableName: "3DTouchActions", comment: "String describing the action of opening the last tab sent to Firefox from the home screen Quick Actions via 3D Touch") + + static var sharedInstance = QuickActions() + + var launchedShortcutItem: UIApplicationShortcutItem? + + // MARK: Administering Quick Actions + func addDynamicApplicationShortcutItemOfType(_ type: ShortcutType, fromShareItem shareItem: ShareItem, toApplication application: UIApplication) { + var userData = [QuickActions.TabURLKey: shareItem.url] + if let title = shareItem.title { + userData[QuickActions.TabTitleKey] = title + } + QuickActions.sharedInstance.addDynamicApplicationShortcutItemOfType(type, withUserData: userData, toApplication: application) + } + + @discardableResult func addDynamicApplicationShortcutItemOfType(_ type: ShortcutType, withUserData userData: [AnyHashable: Any] = [AnyHashable: Any](), toApplication application: UIApplication) -> Bool { + // add the quick actions version so that it is always in the user info + var userData: [AnyHashable: Any] = userData + userData[QuickActions.QuickActionsVersionKey] = QuickActions.QuickActionsVersion + var dynamicShortcutItems = application.shortcutItems ?? [UIApplicationShortcutItem]() + switch type { + case .openLastBookmark: + let openLastBookmarkShortcut = UIMutableApplicationShortcutItem(type: ShortcutType.openLastBookmark.type, + localizedTitle: lastBookmarkTitle, + localizedSubtitle: userData[QuickActions.TabTitleKey] as? String, + icon: UIApplicationShortcutIcon(templateImageName: "quick_action_last_bookmark"), + userInfo: userData + ) + if let index = (dynamicShortcutItems.index { $0.type == ShortcutType.openLastBookmark.type }) { + dynamicShortcutItems[index] = openLastBookmarkShortcut + } else { + dynamicShortcutItems.append(openLastBookmarkShortcut) + } + default: + log.warning("Cannot add static shortcut item of type \(type)") + return false + } + application.shortcutItems = dynamicShortcutItems + return true + } + + func removeDynamicApplicationShortcutItemOfType(_ type: ShortcutType, fromApplication application: UIApplication) { + guard var dynamicShortcutItems = application.shortcutItems, + let index = (dynamicShortcutItems.index { $0.type == type.type }) else { return } + + dynamicShortcutItems.remove(at: index) + application.shortcutItems = dynamicShortcutItems + } + + // MARK: Handling Quick Actions + @discardableResult func handleShortCutItem(_ shortcutItem: UIApplicationShortcutItem, withBrowserViewController bvc: BrowserViewController ) -> Bool { + + // Verify that the provided `shortcutItem`'s `type` is one handled by the application. + guard let shortCutType = ShortcutType(fullType: shortcutItem.type) else { return false } + + DispatchQueue.main.async { + self.handleShortCutItemOfType(shortCutType, userData: shortcutItem.userInfo, browserViewController: bvc) + } + + return true + } + + fileprivate func handleShortCutItemOfType(_ type: ShortcutType, userData: [String: NSSecureCoding]?, browserViewController: BrowserViewController) { + switch type { + case .newTab: + handleOpenNewTab(withBrowserViewController: browserViewController, isPrivate: false) + case .newPrivateTab: + handleOpenNewTab(withBrowserViewController: browserViewController, isPrivate: true) + // even though we're removing OpenLastTab, it's possible that someone will use an existing last tab quick action to open the app + // the first time after upgrading, so we should still handle it + case .openLastBookmark: + if let urlToOpen = (userData?[QuickActions.TabURLKey] as? String)?.asURL { + handleOpenURL(withBrowserViewController: browserViewController, urlToOpen: urlToOpen) + } + case .qrCode: + handleQRCode(with: browserViewController) + } + } + + fileprivate func handleOpenNewTab(withBrowserViewController bvc: BrowserViewController, isPrivate: Bool) { + bvc.openBlankNewTab(focusLocationField: true, isPrivate: isPrivate) + } + + fileprivate func handleOpenURL(withBrowserViewController bvc: BrowserViewController, urlToOpen: URL) { + // open bookmark in a non-private browsing tab + bvc.switchToPrivacyMode(isPrivate: false) + + // find out if bookmarked URL is currently open + // if so, open to that tab, + // otherwise, create a new tab with the bookmarked URL + bvc.switchToTabForURLOrOpen(urlToOpen, isPrivileged: true) + } + + fileprivate func handleQRCode(with vc: QRCodeViewControllerDelegate & UIViewController) { + let qrCodeViewController = QRCodeViewController() + qrCodeViewController.qrCodeDelegate = vc + let controller = UINavigationController(rootViewController: qrCodeViewController) + vc.present(controller, animated: true, completion: nil) + } +} diff --git a/mobile/ios/Client/Application/Settings.bundle/Root.plist b/mobile/ios/Client/Application/Settings.bundle/Root.plist new file mode 100644 index 0000000000..43c451cab2 --- /dev/null +++ b/mobile/ios/Client/Application/Settings.bundle/Root.plist @@ -0,0 +1,57 @@ + + + + + StringsTable + Root + PreferenceSpecifiers + + + Type + PSGroupSpecifier + Title + Debug Settings + + + Type + PSToggleSwitchSpecifier + Title + Email Saved Tab State + Key + SettingsBundleEmailTabState + DefaultValue + + + + Type + PSToggleSwitchSpecifier + Title + Email Logs On Launch + Key + SettingsBundleEmailLogsOnLaunch + DefaultValue + + + + Type + PSToggleSwitchSpecifier + Title + Disable Local Web Server + Key + SettingsBundleDisableLocalWebServer + DefaultValue + + + + Type + PSToggleSwitchSpecifier + Title + Skip Session Restore + Key + SettingsBundleSkipSessionRestore + DefaultValue + + + + + diff --git a/mobile/ios/Client/Application/Settings.bundle/en.lproj/Root.strings b/mobile/ios/Client/Application/Settings.bundle/en.lproj/Root.strings new file mode 100644 index 0000000000..8cd87b9d6b Binary files /dev/null and b/mobile/ios/Client/Application/Settings.bundle/en.lproj/Root.strings differ diff --git a/mobile/ios/Client/Application/TestAppDelegate.swift b/mobile/ios/Client/Application/TestAppDelegate.swift new file mode 100644 index 0000000000..d984514477 --- /dev/null +++ b/mobile/ios/Client/Application/TestAppDelegate.swift @@ -0,0 +1,96 @@ +/* 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 SDWebImage +import XCGLogger + +private let log = Logger.browserLogger + +class TestAppDelegate: AppDelegate { + override func getProfile(_ application: UIApplication) -> Profile { + if let profile = self.profile { + return profile + } + + var profile: BrowserProfile + let launchArguments = ProcessInfo.processInfo.arguments + if launchArguments.contains(LaunchArguments.ClearProfile) { + // Use a clean profile for each test session. + log.debug("Deleting all files in 'Documents' directory to clear the profile") + profile = BrowserProfile(localName: "testProfile", syncDelegate: application.syncDelegate, clear: true) + } else { + profile = BrowserProfile(localName: "testProfile", syncDelegate: application.syncDelegate) + } + + // Don't show the What's New page. + if launchArguments.contains(LaunchArguments.SkipWhatsNew) { + profile.prefs.setString(AppInfo.appVersion, forKey: LatestAppVersionProfileKey) + } + + // Skip the intro when requested by for example tests or automation + if launchArguments.contains(LaunchArguments.SkipIntro) { + profile.prefs.setInt(1, forKey: IntroViewControllerSeenProfileKey) + } + + self.profile = profile + return profile + } + + override func application(_ application: UIApplication, willFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { + // If the app is running from a XCUITest reset all settings in the app + if ProcessInfo.processInfo.arguments.contains(LaunchArguments.ClearProfile) { + resetApplication() + } + + return super.application(application, willFinishLaunchingWithOptions: launchOptions) + } + + /** + Use this to reset the application between tests. + **/ + func resetApplication() { + log.debug("Wiping everything for a clean start.") + + // Clear image cache + SDImageCache.shared().clearDisk() + SDImageCache.shared().clearMemory() + + // Clear the cookie/url cache + URLCache.shared.removeAllCachedResponses() + let storage = HTTPCookieStorage.shared + if let cookies = storage.cookies { + for cookie in cookies { + storage.deleteCookie(cookie) + } + } + + // Clear the documents directory + var rootPath: String = "" + let sharedContainerIdentifier = AppInfo.sharedContainerIdentifier + if let url = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: sharedContainerIdentifier) { + rootPath = url.path + } else { + rootPath = (NSSearchPathForDirectoriesInDomains(FileManager.SearchPathDirectory.documentDirectory, FileManager.SearchPathDomainMask.userDomainMask, true)[0]) + } + let manager = FileManager.default + let documents = URL(fileURLWithPath: rootPath) + let docContents = try! manager.contentsOfDirectory(atPath: rootPath) + for content in docContents { + do { + try manager.removeItem(at: documents.appendingPathComponent(content)) + } catch { + log.debug("Couldn't delete some document contents.") + } + } + } + + override func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { + // Speed up the animations to 100 times as fast. + defer { application.keyWindow?.layer.speed = 100.0 } + return super.application(application, didFinishLaunchingWithOptions: launchOptions) + } + +} diff --git a/mobile/ios/Client/Application/WebServer.swift b/mobile/ios/Client/Application/WebServer.swift new file mode 100644 index 0000000000..ba9e160b0a --- /dev/null +++ b/mobile/ios/Client/Application/WebServer.swift @@ -0,0 +1,88 @@ +/* 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 GCDWebServers +import Shared + +class WebServer { + private let log = Logger.browserLogger + + static let WebServerSharedInstance = WebServer() + + class var sharedInstance: WebServer { + return WebServerSharedInstance + } + + let server: GCDWebServer = GCDWebServer() + + var base: String { + return "http://localhost:\(server.port)" + } + + /// The private credentials for accessing resources on this Web server. + let credentials: URLCredential + + /// A random, transient token used for authenticating requests. + /// Other apps are able to make requests to our local Web server, + /// so this prevents them from accessing any resources. + fileprivate let sessionToken = UUID().uuidString + + init() { + credentials = URLCredential(user: sessionToken, password: "", persistence: .forSession) + } + + @discardableResult func start() throws -> Bool { + if !server.isRunning { + try server.start(options: [ + GCDWebServerOption_Port: 6571, + GCDWebServerOption_BindToLocalhost: true, + GCDWebServerOption_AutomaticallySuspendInBackground: true, + GCDWebServerOption_AuthenticationMethod: GCDWebServerAuthenticationMethod_Basic, + GCDWebServerOption_AuthenticationAccounts: [sessionToken: ""] + ]) + } + return server.isRunning + } + + /// Convenience method to register a dynamic handler. Will be mounted at $base/$module/$resource + func registerHandlerForMethod(_ method: String, module: String, resource: String, handler: @escaping (_ request: GCDWebServerRequest?) -> GCDWebServerResponse!) { + // Prevent serving content if the requested host isn't a whitelisted local host. + let wrappedHandler = {(request: GCDWebServerRequest?) -> GCDWebServerResponse? in + guard let request = request, request.url.isLocal else { + return GCDWebServerResponse(statusCode: 403) + } + + return handler(request) + } + server.addHandler(forMethod: method, path: "/\(module)/\(resource)", request: GCDWebServerRequest.self, processBlock: wrappedHandler) + } + + /// Convenience method to register a resource in the main bundle. Will be mounted at $base/$module/$resource + func registerMainBundleResource(_ resource: String, module: String) { + if let path = Bundle.main.path(forResource: resource, ofType: nil) { + server.addGETHandler(forPath: "/\(module)/\(resource)", filePath: path, isAttachment: false, cacheAge: UInt.max, allowRangeRequests: true) + } + } + + /// Convenience method to register all resources in the main bundle of a specific type. Will be mounted at $base/$module/$resource + func registerMainBundleResourcesOfType(_ type: String, module: String) { + for path: String in Bundle.paths(forResourcesOfType: type, inDirectory: Bundle.main.bundlePath) { + if let resource = NSURL(string: path)?.lastPathComponent { + server.addGETHandler(forPath: "/\(module)/\(resource)", filePath: path as String, isAttachment: false, cacheAge: UInt.max, allowRangeRequests: true) + } else { + log.warning("Unable to locate resource at path: '\(path)'") + } + } + } + + /// Return a full url, as a string, for a resource in a module. No check is done to find out if the resource actually exist. + func URLForResource(_ resource: String, module: String) -> String { + return "\(base)/\(module)/\(resource)" + } + + func baseReaderModeURL() -> String { + return WebServer.sharedInstance.URLForResource("page", module: "reader-mode") + } +} diff --git a/mobile/ios/Client/Application/main.swift b/mobile/ios/Client/Application/main.swift new file mode 100644 index 0000000000..86bc5ef193 --- /dev/null +++ b/mobile/ios/Client/Application/main.swift @@ -0,0 +1,16 @@ +/* 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 Shared + +private var appDelegate: AppDelegate.Type + +if AppConstants.IsRunningTest { + appDelegate = TestAppDelegate.self +} else { + appDelegate = AppDelegate.self +} + +private let pointer = UnsafeMutableRawPointer(CommandLine.unsafeArgv).bindMemory(to: UnsafeMutablePointer.self, capacity: Int(CommandLine.argc)) +UIApplicationMain(CommandLine.argc, pointer, NSStringFromClass(UIApplication.self), NSStringFromClass(appDelegate)) diff --git a/mobile/ios/Client/Assets/About/Licenses.html b/mobile/ios/Client/Assets/About/Licenses.html new file mode 100644 index 0000000000..3dc42de43c --- /dev/null +++ b/mobile/ios/Client/Assets/About/Licenses.html @@ -0,0 +1,674 @@ + + + + + + + +

Licenses

+ +

Firefox for iOS

+ +
+ + + + +

Mozilla Public License
Version 2.0

+

1. Definitions

+
+
1.1. “Contributor”
+

means each individual or legal entity that creates, contributes to the creation of, or owns Covered Software.

+
+
1.2. “Contributor Version”
+

means the combination of the Contributions of others (if any) used by a Contributor and that particular Contributor’s Contribution.

+
+
1.3. “Contribution”
+

means Covered Software of a particular Contributor.

+
+
1.4. “Covered Software”
+

means Source Code Form to which the initial Contributor has attached the notice in Exhibit A, the Executable Form of such Source Code Form, and Modifications of such Source Code Form, in each case including portions thereof.

+
+
1.5. “Incompatible With Secondary Licenses”
+

means

+
    +
  1. that the initial Contributor has attached the notice described in Exhibit B to the Covered Software; or

  2. +
  3. that the Covered Software was made available under the terms of version 1.1 or earlier of the License, but not also under the terms of a Secondary License.

  4. +
+
+
1.6. “Executable Form”
+

means any form of the work other than Source Code Form.

+
+
1.7. “Larger Work”
+

means a work that combines Covered Software with other material, in a separate file or files, that is not Covered Software.

+
+
1.8. “License”
+

means this document.

+
+
1.9. “Licensable”
+

means having the right to grant, to the maximum extent possible, whether at the time of the initial grant or subsequently, any and all of the rights conveyed by this License.

+
+
1.10. “Modifications”
+

means any of the following:

+
    +
  1. any file in Source Code Form that results from an addition to, deletion from, or modification of the contents of Covered Software; or

  2. +
  3. any new file in Source Code Form that contains any Covered Software.

  4. +
+
+
1.11. “Patent Claims” of a Contributor
+

means any patent claim(s), including without limitation, method, process, and apparatus claims, in any patent Licensable by such Contributor that would be infringed, but for the grant of the License, by the making, using, selling, offering for sale, having made, import, or transfer of either its Contributions or its Contributor Version.

+
+
1.12. “Secondary License”
+

means either the GNU General Public License, Version 2.0, the GNU Lesser General Public License, Version 2.1, the GNU Affero General Public License, Version 3.0, or any later versions of those licenses.

+
+
1.13. “Source Code Form”
+

means the form of the work preferred for making modifications.

+
+
1.14. “You” (or “Your”)
+

means an individual or a legal entity exercising rights under this License. For legal entities, “You” includes any entity that controls, is controlled by, or is under common control with You. For purposes of this definition, “control” means (a) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (b) ownership of more than fifty percent (50%) of the outstanding shares or beneficial ownership of such entity.

+
+
+

2. License Grants and Conditions

+

2.1. Grants

+

Each Contributor hereby grants You a world-wide, royalty-free, non-exclusive license:

+
    +
  1. under intellectual property rights (other than patent or trademark) Licensable by such Contributor to use, reproduce, make available, modify, display, perform, distribute, and otherwise exploit its Contributions, either on an unmodified basis, with Modifications, or as part of a Larger Work; and

  2. +
  3. under Patent Claims of such Contributor to make, use, sell, offer for sale, have made, import, and otherwise transfer either its Contributions or its Contributor Version.

  4. +
+

2.2. Effective Date

+

The licenses granted in Section 2.1 with respect to any Contribution become effective for each Contribution on the date the Contributor first distributes such Contribution.

+

2.3. Limitations on Grant Scope

+

The licenses granted in this Section 2 are the only rights granted under this License. No additional rights or licenses will be implied from the distribution or licensing of Covered Software under this License. Notwithstanding Section 2.1(b) above, no patent license is granted by a Contributor:

+
    +
  1. for any code that a Contributor has removed from Covered Software; or

  2. +
  3. for infringements caused by: (i) Your and any other third party’s modifications of Covered Software, or (ii) the combination of its Contributions with other software (except as part of its Contributor Version); or

  4. +
  5. under Patent Claims infringed by Covered Software in the absence of its Contributions.

  6. +
+

This License does not grant any rights in the trademarks, service marks, or logos of any Contributor (except as may be necessary to comply with the notice requirements in Section 3.4).

+

2.4. Subsequent Licenses

+

No Contributor makes additional grants as a result of Your choice to distribute the Covered Software under a subsequent version of this License (see Section 10.2) or under the terms of a Secondary License (if permitted under the terms of Section 3.3).

+

2.5. Representation

+

Each Contributor represents that the Contributor believes its Contributions are its original creation(s) or it has sufficient rights to grant the rights to its Contributions conveyed by this License.

+

2.6. Fair Use

+

This License is not intended to limit any rights You have under applicable copyright doctrines of fair use, fair dealing, or other equivalents.

+

2.7. Conditions

+

Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in Section 2.1.

+

3. Responsibilities

+

3.1. Distribution of Source Form

+

All distribution of Covered Software in Source Code Form, including any Modifications that You create or to which You contribute, must be under the terms of this License. You must inform recipients that the Source Code Form of the Covered Software is governed by the terms of this License, and how they can obtain a copy of this License. You may not attempt to alter or restrict the recipients’ rights in the Source Code Form.

+

3.2. Distribution of Executable Form

+

If You distribute Covered Software in Executable Form then:

+
    +
  1. such Covered Software must also be made available in Source Code Form, as described in Section 3.1, and You must inform recipients of the Executable Form how they can obtain a copy of such Source Code Form by reasonable means in a timely manner, at a charge no more than the cost of distribution to the recipient; and

  2. +
  3. You may distribute such Executable Form under the terms of this License, or sublicense it under different terms, provided that the license for the Executable Form does not attempt to limit or alter the recipients’ rights in the Source Code Form under this License.

  4. +
+

3.3. Distribution of a Larger Work

+

You may create and distribute a Larger Work under terms of Your choice, provided that You also comply with the requirements of this License for the Covered Software. If the Larger Work is a combination of Covered Software with a work governed by one or more Secondary Licenses, and the Covered Software is not Incompatible With Secondary Licenses, this License permits You to additionally distribute such Covered Software under the terms of such Secondary License(s), so that the recipient of the Larger Work may, at their option, further distribute the Covered Software under the terms of either this License or such Secondary License(s).

+

3.4. Notices

+

You may not remove or alter the substance of any license notices (including copyright notices, patent notices, disclaimers of warranty, or limitations of liability) contained within the Source Code Form of the Covered Software, except that You may alter any license notices to the extent required to remedy known factual inaccuracies.

+

3.5. Application of Additional Terms

+

You may choose to offer, and to charge a fee for, warranty, support, indemnity or liability obligations to one or more recipients of Covered Software. However, You may do so only on Your own behalf, and not on behalf of any Contributor. You must make it absolutely clear that any such warranty, support, indemnity, or liability obligation is offered by You alone, and You hereby agree to indemnify every Contributor for any liability incurred by such Contributor as a result of warranty, support, indemnity or liability terms You offer. You may include additional disclaimers of warranty and limitations of liability specific to any jurisdiction.

+

4. Inability to Comply Due to Statute or Regulation

+

If it is impossible for You to comply with any of the terms of this License with respect to some or all of the Covered Software due to statute, judicial order, or regulation then You must: (a) comply with the terms of this License to the maximum extent possible; and (b) describe the limitations and the code they affect. Such description must be placed in a text file included with all distributions of the Covered Software under this License. Except to the extent prohibited by statute or regulation, such description must be sufficiently detailed for a recipient of ordinary skill to be able to understand it.

+

5. Termination

+

5.1. The rights granted under this License will terminate automatically if You fail to comply with any of its terms. However, if You become compliant, then the rights granted under this License from a particular Contributor are reinstated (a) provisionally, unless and until such Contributor explicitly and finally terminates Your grants, and (b) on an ongoing basis, if such Contributor fails to notify You of the non-compliance by some reasonable means prior to 60 days after You have come back into compliance. Moreover, Your grants from a particular Contributor are reinstated on an ongoing basis if such Contributor notifies You of the non-compliance by some reasonable means, this is the first time You have received notice of non-compliance with this License from such Contributor, and You become compliant prior to 30 days after Your receipt of the notice.

+

5.2. If You initiate litigation against any entity by asserting a patent infringement claim (excluding declaratory judgment actions, counter-claims, and cross-claims) alleging that a Contributor Version directly or indirectly infringes any patent, then the rights granted to You by any and all Contributors for the Covered Software under Section 2.1 of this License shall terminate.

+

5.3. In the event of termination under Sections 5.1 or 5.2 above, all end user license agreements (excluding distributors and resellers) which have been validly granted by You or Your distributors under this License prior to termination shall survive termination.

+

6. Disclaimer of Warranty

+

Covered Software is provided under this License on an “as is” basis, without warranty of any kind, either expressed, implied, or statutory, including, without limitation, warranties that the Covered Software is free of defects, merchantable, fit for a particular purpose or non-infringing. The entire risk as to the quality and performance of the Covered Software is with You. Should any Covered Software prove defective in any respect, You (not any Contributor) assume the cost of any necessary servicing, repair, or correction. This disclaimer of warranty constitutes an essential part of this License. No use of any Covered Software is authorized under this License except under this disclaimer.

+

7. Limitation of Liability

+

Under no circumstances and under no legal theory, whether tort (including negligence), contract, or otherwise, shall any Contributor, or anyone who distributes Covered Software as permitted above, be liable to You for any direct, indirect, special, incidental, or consequential damages of any character including, without limitation, damages for lost profits, loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses, even if such party shall have been informed of the possibility of such damages. This limitation of liability shall not apply to liability for death or personal injury resulting from such party’s negligence to the extent applicable law prohibits such limitation. Some jurisdictions do not allow the exclusion or limitation of incidental or consequential damages, so this exclusion and limitation may not apply to You.

+

8. Litigation

+

Any litigation relating to this License may be brought only in the courts of a jurisdiction where the defendant maintains its principal place of business and such litigation shall be governed by laws of that jurisdiction, without reference to its conflict-of-law provisions. Nothing in this Section shall prevent a party’s ability to bring cross-claims or counter-claims.

+

9. Miscellaneous

+

This License represents the complete agreement concerning the subject matter hereof. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. Any law or regulation which provides that the language of a contract shall be construed against the drafter shall not be used to construe this License against a Contributor.

+

10. Versions of the License

+

10.1. New Versions

+

Mozilla Foundation is the license steward. Except as provided in Section 10.3, no one other than the license steward has the right to modify or publish new versions of this License. Each version will be given a distinguishing version number.

+

10.2. Effect of New Versions

+

You may distribute the Covered Software under the terms of the version of the License under which You originally received the Covered Software, or under the terms of any subsequent version published by the license steward.

+

10.3. Modified Versions

+

If you create software not governed by this License, and you want to create a new license for such software, you may create and use a modified version of this License if you rename the license and remove any references to the name of the license steward (except to note that such modified license differs from this License).

+

10.4. Distributing Source Code Form that is Incompatible With Secondary Licenses

+

If You choose to distribute Source Code Form that is Incompatible With Secondary Licenses under the terms of this version of the License, the notice described in Exhibit B of this License must be attached.

+

Exhibit A - Source Code Form License Notice

+
+

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/.

+
+

If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice.

+

You may add additional accurate notices of copyright ownership.

+

Exhibit B - “Incompatible With Secondary Licenses” Notice

+
+

This Source Code Form is “Incompatible With Secondary Licenses”, as defined by the Mozilla Public License, v. 2.0.

+
+ + + + + + + +
+ +

-

+ + + +

Alamofire

+ +
+

Copyright (c) 2014 Alamofire (http://alamofire.org/)

+ +

Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions:

+ +

The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software.

+ +

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE.

+
+

-

+ + + +

Base32

+ +
+

The MIT License (MIT)

+ +

Copyright (c) 2015 Norio Nomura

+ +

Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions:

+ +

The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software.

+ +

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE.

+
+

-

+ +

Box

+ +
+

The MIT License (MIT)

+ +

Copyright (c) 2014 Rob Rix

+ +

Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions:

+ +

The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software.

+ +

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE

+
+

-

+ +

Deferred

+ +
+

Copyright (c) 2014 John Gallagher <jgallagher@bignerdranch.com> + +

Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions:

+ +

The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software.

+ +

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE.

+
+ +

FilledPageControl

+

Copyright (c) 2016 Kyle Zaragoza

+ +

Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions:

+ +

The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software.

+ +

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE.

+ + +

GCDWebServer

+ +
+

Copyright (c) 2012-2014, Pierre-Olivier Latour

+

All rights reserved.

+ +

Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met:

+ +
    +
  • Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer.
  • +
  • Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution.
  • +
  • The name of Pierre-Olivier Latour may not be used to endorse +or promote products derived from this software without specific +prior written permission.
  • +
+ +

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL PIERRE-OLIVIER LATOUR BE LIABLE FOR ANY +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

+
+

-

+ + +

KIF

+ +
+

Copyright 2011 Square, Inc.

+

A full list of contributors is available at https://github.com/square/KIF/contributors

+ +

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.

+
+ +

Result

+ +
+

Copyright (c) 2014 John Gallagher <jgallagher@bignerdranch.com>

+ +

Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions:

+ +

The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software.

+ +

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE.

+
+ +

SDWebImage

+ +
+

Copyright (c) 2009 Olivier Poitrey <rs@dailymotion.com>

+ +

Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions:

+ +

The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software.

+ +

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE.

+
+

-

+ + +

SQLite.swift

+ +
+

(The MIT License)

+ +

Copyright (c) 2014-2015 Stephen Celis (<stephen@stephencelis.com>)

+ +

Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions:

+ +

The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software.

+ +

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE.

+
+

-

+ + +

Fuzi

+ +
+

Copyright (c) 2015 Ce Zheng

+ +

Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions:

+ +

The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software.

+ +

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE.

+
+

-

+ + +

Snap

+ +
+

Copyright (c) 2011-Present SnapKit Team - https://github.com/SnapKit

+ +

Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions:

+ +

The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software.

+ +

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE.

+
+

-

+ + +

XCGLogger

+ +
+

The MIT License (MIT)

+ +

Copyright (c) 2014 Dave Wood, Cerebral Gardens http://www.cerebralgardens.com/

+ +

Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions:

+ +

The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software.

+ +

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE.

+
+

-

+ + +

Readability

+ +
+

Copyright (c) 2010 Arc90 Inc

+ +

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.

+
+

-

+ + +

sqlchipher

+ +
+

Copyright (c) 2008, ZETETIC LLC

+

All rights reserved.

+ +

Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met:

+ +
    +
  • Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer.
  • +
  • Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution.
  • +
  • Neither the name of the ZETETIC LLC nor the +names of its contributors may be used to endorse or promote products +derived from this software without specific prior written permission.
  • +
+ +

THIS SOFTWARE IS PROVIDED BY ZETETIC LLC ''AS IS'' AND ANY +EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL ZETETIC LLC BE LIABLE FOR ANY +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

+
+

-

+ + +

SwiftKeychainWrapper

+ +
+

The MIT License (MIT)

+ +

Copyright (c) 2014 Jason

+ +

Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions:

+ +

The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software.

+ +

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE.

+
+

-

+ + +

Swift-JSON

+ +
+

The MIT License (MIT)

+ +

Copyright (c) 2014 Dan Kogai

+ +

Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions:

+ +

The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software.

+ +

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE.

+
+ +
+

UIImageColors

+ + +

Created by Jathu Satkunarajah (@jathu) on 2015-06-11 - Toronto

+

Original Cocoa version by Panic Inc. - Portland

+ +

MIT License

+ +

Copyright (c) 2015 Jathu Satkunarajah

+ +

Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions:

+ +

The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software.

+ +

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE.

+
+ +

UIImageViewAligned

+ +
+

The MIT License (MIT)

+ +

Copyright (c) 2013 Andrei Stanescu

+ +

Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions:

+ +

The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software.

+ +

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

+
+ + diff --git a/mobile/ios/Client/Assets/CertError.css b/mobile/ios/Client/Assets/CertError.css new file mode 100644 index 0000000000..436541d92b --- /dev/null +++ b/mobile/ios/Client/Assets/CertError.css @@ -0,0 +1,76 @@ +/* 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/. */ + +html, +body { + margin: 0; + padding: 0; + height: 100%; +} + +body { + background-color: white; + padding: 0 65px; + color: #333; + font-size: 13px; + font-family: "Helvetica Neue Medium" sans-serif; +} + +h1 { + color: #333; + font-size: 16px; + font-family: "Helvetica Neue Medium" sans-serif; + font-weight: 300; + margin-bottom: 25px; +} + +/* Add a set of stripes at the top of pages */ +#decoration { + background-image: repeating-linear-gradient(-65deg, #ed2, #ed2 10px, + #fe3 10px, #fe3 20px, + #ed2 20px); + position: fixed; + top: 0; + left: 0; + height: 32px; + width: 100%; +} + +a { + color: rgb(76, 158, 255); + text-decoration: none; +} + +button { + width: 100%; + border: none; + padding: 1rem; + font-family: "Helvetica Neue Medium" sans-serif; + background-color: rgb(76, 158, 255); + color: white; + font-size: 16px; + font-weight: 300; + border-radius: 5px; +} + +#errorPageContainer { + max-width: 300px; + margin: 0 auto; + -webkit-transform: translateY(70px); + padding-bottom: 10px; +} + +#certErrorCode { + color: #999; +} + +#advancedButton { + background-color: white; + color: #777; + border: 1px solid #aaa; +} + +#advancedContent { + margin-bottom: 20px; +} diff --git a/mobile/ios/Client/Assets/CertError.html b/mobile/ios/Client/Assets/CertError.html new file mode 100644 index 0000000000..3e576475b9 --- /dev/null +++ b/mobile/ios/Client/Assets/CertError.html @@ -0,0 +1,49 @@ + + + + + %error_title% + + + + + + + +
+

%error_title%

+

%actions%

+

%long_description%

+ + +
+

%cert_error%

+

%warning_advanced1%

+

%warning_advanced2%

+
%warning_actions%
+
+
+ +
+ + + + diff --git a/mobile/ios/Client/Assets/ContextMenu.js b/mobile/ios/Client/Assets/ContextMenu.js new file mode 100644 index 0000000000..91e701c0c0 --- /dev/null +++ b/mobile/ios/Client/Assets/ContextMenu.js @@ -0,0 +1,28 @@ +/* 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/. */ + +window.addEventListener('touchstart', function(evt) { + var target = evt.target; + + var targetLink = target.closest('a'); + var targetImage = target.closest('img'); + + if (!targetLink && !targetImage) { + return; + } + + var data = {}; + + if (targetLink) { + data.link = targetLink.href; + } + + if (targetImage) { + data.image = targetImage.src; + } + + if (data.link || data.image) { + webkit.messageHandlers.contextMenuMessageHandler.postMessage(data); + } +}, true); diff --git a/mobile/ios/Client/Assets/CustomSearchHelper.js b/mobile/ios/Client/Assets/CustomSearchHelper.js new file mode 100644 index 0000000000..5dc8e7dc83 --- /dev/null +++ b/mobile/ios/Client/Assets/CustomSearchHelper.js @@ -0,0 +1,42 @@ +/* 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/. */ +"use strict"; + +if (!window.__firefox__) { + Object.defineProperty(window, '__firefox__', { + enumerable: false, + configurable: false, + writable: false, + value: {} + }); +} + +Object.defineProperty(window.__firefox__, 'searchQueryForField', { + enumerable: false, + configurable: false, + writable: false, + value: function() { + var input = document.activeElement; + if (input.tagName.toLowerCase() !== 'input') return null; + var form = input.form; + if (!form || form.method.toLowerCase() != 'get') return null; + + var inputs = form.getElementsByTagName('input'); + inputs = Array.prototype.slice.call(inputs, 0); + var params = inputs.map(function(element) { + if (element.name == input.name) return [element.name, '{searchTerms}'].join('='); + return [element.name, element.value].map(encodeURIComponent).join('='); + }); + + var selectFields = form.getElementsByTagName('select'); + selectFields = Array.prototype.slice.call(selectFields, 0); + var selectParams = selectFields.map(function(e){ + return [e.name, e.options[e.selectedIndex].value].map(encodeURIComponent).join('='); + }); + params = params.concat(selectParams); + if (!form.action) return null; //an invalid form. + var url = [form.action, params.join('&')].join('?'); + return url; + } +}); diff --git a/mobile/ios/Client/Assets/Favicons.js b/mobile/ios/Client/Assets/Favicons.js new file mode 100644 index 0000000000..bc867b752b --- /dev/null +++ b/mobile/ios/Client/Assets/Favicons.js @@ -0,0 +1,60 @@ +/* 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/. */ + + +if (!window.__firefox__) { + Object.defineProperty(window, '__firefox__', { + enumerable: false, + configurable: false, + writable: false, + value: {} + }); +} + +Object.defineProperty(window.__firefox__, 'favicons', { + enumerable: false, + configurable: false, + writable: false, + value: (function() { + // These integers should be kept in sync with the IconType raw-values + var ICON = 0; + var APPLE = 1; + var APPLE_PRECOMPOSED = 2; + var GUESS = 3; + + var selectors = { + "link[rel~='icon']": ICON, + "link[rel='apple-touch-icon']": APPLE, + "link[rel='apple-touch-icon-precomposed']": APPLE_PRECOMPOSED + }; + + function getAll() { + var favicons = {}; + + for (var selector in selectors) { + var icons = document.querySelectorAll(selector); + for (var i = 0; i < icons.length; i++) { + var href = icons[i].href; + favicons[href] = selectors[selector]; + } + } + + // If we didn't find anything in the page, look to see if a favicon.ico file exists for the domain + if (Object.keys(favicons).length === 0) { + var href = document.location.origin + "/favicon.ico"; + favicons[href] = GUESS; + } + return favicons; + } + + function getFavicons() { + var favicons = getAll(); + webkit.messageHandlers.faviconsMessageHandler.postMessage(favicons); + } + + return { + getFavicons: getFavicons + }; + })() +}); diff --git a/mobile/ios/Client/Assets/FindInPage.js b/mobile/ios/Client/Assets/FindInPage.js new file mode 100644 index 0000000000..51680c8ca4 --- /dev/null +++ b/mobile/ios/Client/Assets/FindInPage.js @@ -0,0 +1,292 @@ +/* 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/. */ + +(function() { +"use strict"; + +var DEBUG_ENABLED = false; +var MATCH_HIGHLIGHT_ACTIVE = "#f19750"; +var MATCH_HIGHLIGHT_INACTIVE = "#ffde49"; +var SCROLL_INTERVAL_INCREMENT = 5; +var SCROLL_INTERVAL_DURATION = 400; +var SCROLL_OFFSET = 60; + +var activeHighlightSpan = null; +var lastSearch; +var scrollInterval; +var activeIndex = 0; +var highlightSpans = []; + +function debug(str) { + if (DEBUG_ENABLED) { + console.log("FindInPage: " + str); + } +} + +function isElementVisible(elem) { + return getComputedStyle(elem).visibility !== "hidden"; +} + +function isRectInViewport(rect) { + var left = rect.left + document.body.scrollLeft; + var right = rect.right + document.body.scrollLeft; + var top = rect.top + document.body.scrollTop; + var bottom = rect.bottom + document.body.scrollTop; + + return rect.width > 0 && + rect.height > 0 && + right >= 0 && + bottom >= 0 && + left <= document.body.scrollWidth && + top <= document.body.scrollHeight; +} + +function findMatches(text) { + // For case-insensitive matching. + var lowerText = text.toLocaleLowerCase(); + var upperText = text.toLocaleUpperCase(); + + var matches = []; + var range = document.createRange(); + var walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT, null, false); + var textLength = text.length; + var node; + while (node = walker.nextNode()) { + var textContent = node.textContent; + findString: for (var i = 0; i < textContent.length - textLength + 1; ++i) { + for (var j = 0; j < textLength; ++j) { + var nextChar = textContent[i + j]; + if (lowerText[j] !== nextChar && upperText[j] !== nextChar) { + continue findString; + } + } + + // This node is a TextNode, not an Element. Its parent is the nearest Element. + var element = node.parentNode; + + // Find the rect of just the text for this match. + range.setStart(node, i); + range.setEnd(node, i + textLength); + var textRect = range.getBoundingClientRect(); + + // We have a match, but we need to make sure it's visible. The condition + // below checks the following cases: + // * If this element or any of its parents has style visibility hidden. + // The visibility style is inherited, so we need to check only this + // element and not all of its ancestors. + // * If the highlight will be outside of the page's bounds. We determine + // this by comparing the bounds of the text rect. + // * If the element style display is set to none. display:none collapses + // the element's space, so this will again be detected by looking at + // the text's rect: if the element is collapsed, the width and height + // will be zero. + if (isElementVisible(element) && isRectInViewport(textRect)) { + matches.push({ node: node, index: i }); + + // Resume searching after this match to prevent overlapping results. + i += textLength- 1; + } + } + } + + return matches; +} + +function flattenNode(node) { + var parent = node.parentNode; + if (!parent) { + return; + } + + while (node.firstChild) { + parent.insertBefore(node.firstChild, node); + } + + node.remove(); + parent.normalize(); +} + +function clearHighlights() { + if (highlightSpans.length > 0) { + for (var span of highlightSpans) { + flattenNode(span); + } + highlightSpans = []; + } + + activeHighlightSpan = null; +} + +function highlightAllMatches(text) { + debug("Searching: " + text); + + clearHighlights(); + + if (!text.trim()) { + webkit.messageHandlers.findInPageHandler.postMessage({ totalResults: 0 }); + return; + } + + var range = document.createRange(); + var matches = findMatches(text); + var highlightTemplate = document.createElement("span"); + highlightTemplate.style.backgroundColor = MATCH_HIGHLIGHT_INACTIVE; + + // If there are multiple matches in the same node, inserting a highlight span before other matches + // in that node will invalidate other matches since the node itself changes. By iterating through + // results in reverse, we highlight matches last in the node first so earlier matches are unaffected. + for (var i = matches.length - 1; i >= 0; --i) { + var match = matches[i]; + var highlight = highlightTemplate.cloneNode(); + + range.setStart(match.node, match.index); + range.setEnd(match.node, match.index + text.length); + range.surroundContents(highlight); + highlightSpans.unshift(highlight); + } + + debug(matches.length + " highlighted rects created!"); + webkit.messageHandlers.findInPageHandler.postMessage({ totalResults: matches.length }); +} + +function getIDForRect(rect) { + return rect.top + "," + rect.bottom + "," + rect.left + "," + rect.right; +} + +function updateActiveHighlight() { + // Reset the color of the previous highlight. + if (activeHighlightSpan) { + activeHighlightSpan.style.backgroundColor = MATCH_HIGHLIGHT_INACTIVE; + } + + if (!highlightSpans.length) { + return; + } + + activeHighlightSpan = highlightSpans[activeIndex]; + activeHighlightSpan.style.backgroundColor = MATCH_HIGHLIGHT_ACTIVE; + + // Find the position of the element centered on the screen, then scroll to it. + var rect = activeHighlightSpan.getBoundingClientRect(); + var top = SCROLL_OFFSET + rect.top + scrollY - window.innerHeight / 2; + var left = rect.left + scrollX - window.innerWidth / 2; + left = clamp(left, 0, document.body.scrollWidth); + top = clamp(top, 0, document.body.scrollHeight); + scrollToSelection(left, top, SCROLL_INTERVAL_DURATION); + debug("Scrolled to: " + left + ", " + top); +} + +function scrollToSelection(left, top, duration) { + var time = 0; + var startX = scrollX; + var startY = scrollY; + clearInterval(scrollInterval); + scrollInterval = setInterval(function() { + var xStep = easeOutCubic(time, startX, left - startX, duration); + var yStep = easeOutCubic(time, startY, top - startY, duration); + window.scrollTo(xStep, yStep); + time += SCROLL_INTERVAL_INCREMENT; + if (time >= duration) { + clearInterval(scrollInterval); + } + }, SCROLL_INTERVAL_INCREMENT); +} + +function easeOutCubic(currentTime, startValue, changeInValue, duration) { + return changeInValue * (Math.pow(currentTime / duration - 1, 3) + 1) + startValue; +} + +function clamp(number, min, max) { + return Math.max(min, Math.min(number, max)); +} + +function updateSearch(text) { + if (lastSearch == text) { + // The text is the same, so we're either finding either the next or previous result. + var totalResults = highlightSpans.length; + activeIndex = (activeIndex + totalResults) % totalResults; + } else { + // Store the current active rect to decide which new match should be active. + var activeHighlightRect = null; + if (activeHighlightSpan) { + activeHighlightRect = activeHighlightSpan.getBoundingClientRect(); + } + + // The search text changed, so scan the page for new results. + highlightAllMatches(text); + + // If we found a match at or after the last match, use that position + // instead of starting again from the top. + activeIndex = 0; + if (activeHighlightRect) { + for (var i = 0; i < highlightSpans.length; i++) { + var highlight = highlightSpans[i]; + var highlightRect = highlight.getBoundingClientRect(); + if ((highlightRect.top == activeHighlightRect.top && highlightRect.left >= activeHighlightRect.left) || + (highlightRect.top > activeHighlightRect.top)) { + activeIndex = i; + break; + } + } + } + + lastSearch = text; + } + + // Update the UI with the current match index. + var currentResult = highlightSpans.length ? activeIndex + 1 : 0; + webkit.messageHandlers.findInPageHandler.postMessage({ currentResult: currentResult }); + + updateActiveHighlight(); +} + +if (!window.__firefox__) { + Object.defineProperty(window, '__firefox__', { + enumerable: false, + configurable: false, + writable: false, + value: {} + }); +} + +Object.defineProperty(window.__firefox__, 'find', { + enumerable: false, + configurable: false, + writable: false, + value: function(text) { + updateSearch(text); + } +}); + +Object.defineProperty(window.__firefox__, 'findNext', { + enumerable: false, + configurable: false, + writable: false, + value: function(text) { + activeIndex++; + updateSearch(text); + } +}); + +Object.defineProperty(window.__firefox__, 'findPrevious', { + enumerable: false, + configurable: false, + writable: false, + value: function(text) { + activeIndex--; + updateSearch(text); + } +}); + +Object.defineProperty(window.__firefox__, 'findDone', { + enumerable: false, + configurable: false, + writable: false, + value: function() { + clearHighlights(); + lastSearch = null; + } +}); + +})(); diff --git a/mobile/ios/Client/Assets/Fonts/CharisSILB.ttf b/mobile/ios/Client/Assets/Fonts/CharisSILB.ttf new file mode 100644 index 0000000000..379487af39 Binary files /dev/null and b/mobile/ios/Client/Assets/Fonts/CharisSILB.ttf differ diff --git a/mobile/ios/Client/Assets/Fonts/CharisSILBI.ttf b/mobile/ios/Client/Assets/Fonts/CharisSILBI.ttf new file mode 100644 index 0000000000..917f3692d8 Binary files /dev/null and b/mobile/ios/Client/Assets/Fonts/CharisSILBI.ttf differ diff --git a/mobile/ios/Client/Assets/Fonts/CharisSILI.ttf b/mobile/ios/Client/Assets/Fonts/CharisSILI.ttf new file mode 100644 index 0000000000..852ae437be Binary files /dev/null and b/mobile/ios/Client/Assets/Fonts/CharisSILI.ttf differ diff --git a/mobile/ios/Client/Assets/Fonts/CharisSILR.ttf b/mobile/ios/Client/Assets/Fonts/CharisSILR.ttf new file mode 100644 index 0000000000..5b55ec98dd Binary files /dev/null and b/mobile/ios/Client/Assets/Fonts/CharisSILR.ttf differ diff --git a/mobile/ios/Client/Assets/Fonts/FiraSans-Bold.ttf b/mobile/ios/Client/Assets/Fonts/FiraSans-Bold.ttf new file mode 100644 index 0000000000..04830e7e1b Binary files /dev/null and b/mobile/ios/Client/Assets/Fonts/FiraSans-Bold.ttf differ diff --git a/mobile/ios/Client/Assets/Fonts/FiraSans-BoldItalic.ttf b/mobile/ios/Client/Assets/Fonts/FiraSans-BoldItalic.ttf new file mode 100644 index 0000000000..671c3f190b Binary files /dev/null and b/mobile/ios/Client/Assets/Fonts/FiraSans-BoldItalic.ttf differ diff --git a/mobile/ios/Client/Assets/Fonts/FiraSans-Book.ttf b/mobile/ios/Client/Assets/Fonts/FiraSans-Book.ttf new file mode 100644 index 0000000000..4da8f660ec Binary files /dev/null and b/mobile/ios/Client/Assets/Fonts/FiraSans-Book.ttf differ diff --git a/mobile/ios/Client/Assets/Fonts/FiraSans-Italic.ttf b/mobile/ios/Client/Assets/Fonts/FiraSans-Italic.ttf new file mode 100644 index 0000000000..3b7d61bfbe Binary files /dev/null and b/mobile/ios/Client/Assets/Fonts/FiraSans-Italic.ttf differ diff --git a/mobile/ios/Client/Assets/Fonts/FiraSans-Light.ttf b/mobile/ios/Client/Assets/Fonts/FiraSans-Light.ttf new file mode 100644 index 0000000000..314116d14c Binary files /dev/null and b/mobile/ios/Client/Assets/Fonts/FiraSans-Light.ttf differ diff --git a/mobile/ios/Client/Assets/Fonts/FiraSans-Medium.ttf b/mobile/ios/Client/Assets/Fonts/FiraSans-Medium.ttf new file mode 100644 index 0000000000..e6a6637b0a Binary files /dev/null and b/mobile/ios/Client/Assets/Fonts/FiraSans-Medium.ttf differ diff --git a/mobile/ios/Client/Assets/Fonts/FiraSans-Regular.ttf b/mobile/ios/Client/Assets/Fonts/FiraSans-Regular.ttf new file mode 100644 index 0000000000..a4e6563806 Binary files /dev/null and b/mobile/ios/Client/Assets/Fonts/FiraSans-Regular.ttf differ diff --git a/mobile/ios/Client/Assets/Fonts/FiraSans-SemiBold.ttf b/mobile/ios/Client/Assets/Fonts/FiraSans-SemiBold.ttf new file mode 100644 index 0000000000..be24d2b6db Binary files /dev/null and b/mobile/ios/Client/Assets/Fonts/FiraSans-SemiBold.ttf differ diff --git a/mobile/ios/Client/Assets/Fonts/FiraSans-UltraLight.ttf b/mobile/ios/Client/Assets/Fonts/FiraSans-UltraLight.ttf new file mode 100644 index 0000000000..b1da1656bb Binary files /dev/null and b/mobile/ios/Client/Assets/Fonts/FiraSans-UltraLight.ttf differ diff --git a/mobile/ios/Client/Assets/FxASignIn.js b/mobile/ios/Client/Assets/FxASignIn.js new file mode 100644 index 0000000000..064b6110f2 --- /dev/null +++ b/mobile/ios/Client/Assets/FxASignIn.js @@ -0,0 +1,15 @@ +/* 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/. */ + +/** + * Transport postMessage events from the fxa-content-server to the embedding + * webview. + */ + +"use strict"; + +function handleAccountsCommand(evt) { + webkit.messageHandlers.accountsCommandHandler.postMessage({ type: evt.type, detail: evt.detail }); +}; +window.addEventListener("FirefoxAccountsCommand", handleAccountsCommand); diff --git a/mobile/ios/Client/Assets/HistoryStateHelper.js b/mobile/ios/Client/Assets/HistoryStateHelper.js new file mode 100644 index 0000000000..0cdb8ff2ad --- /dev/null +++ b/mobile/ios/Client/Assets/HistoryStateHelper.js @@ -0,0 +1,36 @@ +/* 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/. */ + +(function() { + var nativeHistoryPushState = window.history.pushState; + var nativeHistoryReplaceState = window.history.replaceState; + + // We need to catch calls to `history.pushState()` in order to + // notify the BrowserViewController so that history can be + // recorded in single-page web applications that do all rendering + // on the client side. + window.history.pushState = function(state, title, url) { + nativeHistoryPushState.apply(this, arguments); + webkit.messageHandlers.historyStateHelper.postMessage({ + pushState: true, + state: state, + title: title, + url: url + }); + }; + + // We need to catch calls to `history.replaceState()` in order to + // notify the BrowserViewController so that history can be + // recorded in single-page web applications that do all rendering + // on the client side. + window.history.replaceState = function(state, title, url) { + nativeHistoryReplaceState.apply(this, arguments); + webkit.messageHandlers.historyStateHelper.postMessage({ + replaceState: true, + state: state, + title: title, + url: url + }); + }; +})(); diff --git a/mobile/ios/Client/Assets/Images.xcassets/AddSearch.imageset/AddSearch.png b/mobile/ios/Client/Assets/Images.xcassets/AddSearch.imageset/AddSearch.png new file mode 100644 index 0000000000..2d1eadbbf9 Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/AddSearch.imageset/AddSearch.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/AddSearch.imageset/AddSearch@2x.png b/mobile/ios/Client/Assets/Images.xcassets/AddSearch.imageset/AddSearch@2x.png new file mode 100644 index 0000000000..ba099d0056 Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/AddSearch.imageset/AddSearch@2x.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/AddSearch.imageset/AddSearch@3x.png b/mobile/ios/Client/Assets/Images.xcassets/AddSearch.imageset/AddSearch@3x.png new file mode 100644 index 0000000000..b4b3454816 Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/AddSearch.imageset/AddSearch@3x.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/AddSearch.imageset/Contents.json b/mobile/ios/Client/Assets/Images.xcassets/AddSearch.imageset/Contents.json new file mode 100644 index 0000000000..140382e425 --- /dev/null +++ b/mobile/ios/Client/Assets/Images.xcassets/AddSearch.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "AddSearch.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "AddSearch@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "AddSearch@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/mobile/ios/Client/Assets/Images.xcassets/AppIcon.appiconset/Contents.json b/mobile/ios/Client/Assets/Images.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000000..ac0824585c --- /dev/null +++ b/mobile/ios/Client/Assets/Images.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,150 @@ +{ + "images" : [ + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "icon-40.png", + "scale" : "2x" + }, + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "icon-60.png", + "scale" : "3x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "icon-29.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "icon-29@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "icon-29@3x.png", + "scale" : "3x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "icon-40@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "icon-40@3x.png", + "scale" : "3x" + }, + { + "idiom" : "iphone", + "size" : "57x57", + "scale" : "1x" + }, + { + "idiom" : "iphone", + "size" : "57x57", + "scale" : "2x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "icon-60@2x.png", + "scale" : "2x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "icon-60@3x.png", + "scale" : "3x" + }, + { + "idiom" : "ipad", + "size" : "20x20", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "20x20", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "icon-29.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "icon-29@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "icon-40.png", + "scale" : "1x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "icon-40@2x.png", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "50x50", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "50x50", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "72x72", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "72x72", + "scale" : "2x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "icon-76.png", + "scale" : "1x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "icon-76@2x.png", + "scale" : "2x" + }, + { + "size" : "83.5x83.5", + "idiom" : "ipad", + "filename" : "icon-83.5@2x.png", + "scale" : "2x" + }, + { + "size" : "1024x1024", + "idiom" : "ios-marketing", + "filename" : "icon-512@2x.png", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/mobile/ios/Client/Assets/Images.xcassets/AppIcon.appiconset/iTunesArtwork@2x-1.png b/mobile/ios/Client/Assets/Images.xcassets/AppIcon.appiconset/iTunesArtwork@2x-1.png new file mode 100644 index 0000000000..18b49af615 Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/AppIcon.appiconset/iTunesArtwork@2x-1.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/AppIcon.appiconset/icon-29.png b/mobile/ios/Client/Assets/Images.xcassets/AppIcon.appiconset/icon-29.png new file mode 100644 index 0000000000..e686b65993 Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/AppIcon.appiconset/icon-29.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/AppIcon.appiconset/icon-29@2x.png b/mobile/ios/Client/Assets/Images.xcassets/AppIcon.appiconset/icon-29@2x.png new file mode 100644 index 0000000000..5ed8287c39 Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/AppIcon.appiconset/icon-29@2x.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/AppIcon.appiconset/icon-29@3x.png b/mobile/ios/Client/Assets/Images.xcassets/AppIcon.appiconset/icon-29@3x.png new file mode 100644 index 0000000000..218370a360 Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/AppIcon.appiconset/icon-29@3x.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/AppIcon.appiconset/icon-40.png b/mobile/ios/Client/Assets/Images.xcassets/AppIcon.appiconset/icon-40.png new file mode 100644 index 0000000000..cc8e1fa4ce Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/AppIcon.appiconset/icon-40.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/AppIcon.appiconset/icon-40@2x.png b/mobile/ios/Client/Assets/Images.xcassets/AppIcon.appiconset/icon-40@2x.png new file mode 100644 index 0000000000..b41485156b Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/AppIcon.appiconset/icon-40@2x.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/AppIcon.appiconset/icon-40@3x.png b/mobile/ios/Client/Assets/Images.xcassets/AppIcon.appiconset/icon-40@3x.png new file mode 100644 index 0000000000..66c0d8c3cb Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/AppIcon.appiconset/icon-40@3x.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/AppIcon.appiconset/icon-512@2x.png b/mobile/ios/Client/Assets/Images.xcassets/AppIcon.appiconset/icon-512@2x.png new file mode 100644 index 0000000000..e4707a3197 Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/AppIcon.appiconset/icon-512@2x.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/AppIcon.appiconset/icon-60.png b/mobile/ios/Client/Assets/Images.xcassets/AppIcon.appiconset/icon-60.png new file mode 100644 index 0000000000..c9e15a99c2 Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/AppIcon.appiconset/icon-60.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/AppIcon.appiconset/icon-60@2x.png b/mobile/ios/Client/Assets/Images.xcassets/AppIcon.appiconset/icon-60@2x.png new file mode 100644 index 0000000000..2d343f6583 Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/AppIcon.appiconset/icon-60@2x.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/AppIcon.appiconset/icon-60@3x.png b/mobile/ios/Client/Assets/Images.xcassets/AppIcon.appiconset/icon-60@3x.png new file mode 100644 index 0000000000..4efcf6ef42 Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/AppIcon.appiconset/icon-60@3x.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/AppIcon.appiconset/icon-76.png b/mobile/ios/Client/Assets/Images.xcassets/AppIcon.appiconset/icon-76.png new file mode 100644 index 0000000000..c7ff33a5e9 Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/AppIcon.appiconset/icon-76.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/AppIcon.appiconset/icon-76@2x.png b/mobile/ios/Client/Assets/Images.xcassets/AppIcon.appiconset/icon-76@2x.png new file mode 100644 index 0000000000..d63965910e Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/AppIcon.appiconset/icon-76@2x.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/AppIcon.appiconset/icon-83.5@2x.png b/mobile/ios/Client/Assets/Images.xcassets/AppIcon.appiconset/icon-83.5@2x.png new file mode 100644 index 0000000000..677771fef8 Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/AppIcon.appiconset/icon-83.5@2x.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/AppIcon_Beta.appiconset/Contents.json b/mobile/ios/Client/Assets/Images.xcassets/AppIcon_Beta.appiconset/Contents.json new file mode 100644 index 0000000000..f5925b7973 --- /dev/null +++ b/mobile/ios/Client/Assets/Images.xcassets/AppIcon_Beta.appiconset/Contents.json @@ -0,0 +1,151 @@ +{ + "images" : [ + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "icon-40.png", + "scale" : "2x" + }, + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "icon-60.png", + "scale" : "3x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "icon-29.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "icon-29@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "icon-29@3x.png", + "scale" : "3x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "icon-40@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "icon-40@3x.png", + "scale" : "3x" + }, + { + "idiom" : "iphone", + "size" : "57x57", + "scale" : "1x" + }, + { + "idiom" : "iphone", + "size" : "57x57", + "scale" : "2x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "icon-60@2x.png", + "scale" : "2x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "icon-60@3x.png", + "scale" : "3x" + }, + { + "idiom" : "ipad", + "size" : "20x20", + "scale" : "1x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "icon-40.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "icon-29.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "icon-29@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "icon-40.png", + "scale" : "1x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "icon-40@2x.png", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "50x50", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "50x50", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "72x72", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "72x72", + "scale" : "2x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "icon-76.png", + "scale" : "1x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "icon-76@2x.png", + "scale" : "2x" + }, + { + "size" : "83.5x83.5", + "idiom" : "ipad", + "filename" : "icon-83.5@2x.png", + "scale" : "2x" + }, + { + "size" : "1024x1024", + "idiom" : "ios-marketing", + "filename" : "icon-512@2x.png", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/mobile/ios/Client/Assets/Images.xcassets/AppIcon_Beta.appiconset/iTunesArtwork@2x-1.png b/mobile/ios/Client/Assets/Images.xcassets/AppIcon_Beta.appiconset/iTunesArtwork@2x-1.png new file mode 100644 index 0000000000..18b49af615 Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/AppIcon_Beta.appiconset/iTunesArtwork@2x-1.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/AppIcon_Beta.appiconset/icon-29.png b/mobile/ios/Client/Assets/Images.xcassets/AppIcon_Beta.appiconset/icon-29.png new file mode 100644 index 0000000000..ce2ec8ce67 Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/AppIcon_Beta.appiconset/icon-29.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/AppIcon_Beta.appiconset/icon-29@2x.png b/mobile/ios/Client/Assets/Images.xcassets/AppIcon_Beta.appiconset/icon-29@2x.png new file mode 100644 index 0000000000..64985b4fbe Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/AppIcon_Beta.appiconset/icon-29@2x.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/AppIcon_Beta.appiconset/icon-29@3x.png b/mobile/ios/Client/Assets/Images.xcassets/AppIcon_Beta.appiconset/icon-29@3x.png new file mode 100644 index 0000000000..8c3c173d94 Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/AppIcon_Beta.appiconset/icon-29@3x.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/AppIcon_Beta.appiconset/icon-40.png b/mobile/ios/Client/Assets/Images.xcassets/AppIcon_Beta.appiconset/icon-40.png new file mode 100644 index 0000000000..4cb6e2e52d Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/AppIcon_Beta.appiconset/icon-40.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/AppIcon_Beta.appiconset/icon-40@2x.png b/mobile/ios/Client/Assets/Images.xcassets/AppIcon_Beta.appiconset/icon-40@2x.png new file mode 100644 index 0000000000..f3dae1ea0d Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/AppIcon_Beta.appiconset/icon-40@2x.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/AppIcon_Beta.appiconset/icon-40@3x.png b/mobile/ios/Client/Assets/Images.xcassets/AppIcon_Beta.appiconset/icon-40@3x.png new file mode 100644 index 0000000000..ddb92c1a75 Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/AppIcon_Beta.appiconset/icon-40@3x.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/AppIcon_Beta.appiconset/icon-512@2x.png b/mobile/ios/Client/Assets/Images.xcassets/AppIcon_Beta.appiconset/icon-512@2x.png new file mode 100644 index 0000000000..b0bcee4f46 Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/AppIcon_Beta.appiconset/icon-512@2x.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/AppIcon_Beta.appiconset/icon-60.png b/mobile/ios/Client/Assets/Images.xcassets/AppIcon_Beta.appiconset/icon-60.png new file mode 100644 index 0000000000..72b81451df Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/AppIcon_Beta.appiconset/icon-60.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/AppIcon_Beta.appiconset/icon-60@2x.png b/mobile/ios/Client/Assets/Images.xcassets/AppIcon_Beta.appiconset/icon-60@2x.png new file mode 100644 index 0000000000..db49d15a64 Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/AppIcon_Beta.appiconset/icon-60@2x.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/AppIcon_Beta.appiconset/icon-60@3x.png b/mobile/ios/Client/Assets/Images.xcassets/AppIcon_Beta.appiconset/icon-60@3x.png new file mode 100644 index 0000000000..deb57c142e Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/AppIcon_Beta.appiconset/icon-60@3x.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/AppIcon_Beta.appiconset/icon-76.png b/mobile/ios/Client/Assets/Images.xcassets/AppIcon_Beta.appiconset/icon-76.png new file mode 100644 index 0000000000..1d07e4e792 Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/AppIcon_Beta.appiconset/icon-76.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/AppIcon_Beta.appiconset/icon-76@2x.png b/mobile/ios/Client/Assets/Images.xcassets/AppIcon_Beta.appiconset/icon-76@2x.png new file mode 100644 index 0000000000..c6caeed618 Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/AppIcon_Beta.appiconset/icon-76@2x.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/AppIcon_Beta.appiconset/icon-83.5@2x.png b/mobile/ios/Client/Assets/Images.xcassets/AppIcon_Beta.appiconset/icon-83.5@2x.png new file mode 100644 index 0000000000..507f9e0e3f Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/AppIcon_Beta.appiconset/icon-83.5@2x.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/AppIcon_Developer.appiconset/Contents.json b/mobile/ios/Client/Assets/Images.xcassets/AppIcon_Developer.appiconset/Contents.json new file mode 100644 index 0000000000..7f766f0ae1 --- /dev/null +++ b/mobile/ios/Client/Assets/Images.xcassets/AppIcon_Developer.appiconset/Contents.json @@ -0,0 +1,149 @@ +{ + "images" : [ + { + "idiom" : "iphone", + "size" : "20x20", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "20x20", + "scale" : "3x" + }, + { + "idiom" : "iphone", + "size" : "29x29", + "scale" : "1x" + }, + { + "idiom" : "iphone", + "size" : "29x29", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "29x29", + "scale" : "3x" + }, + { + "idiom" : "iphone", + "size" : "40x40", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-60@2x.png", + "scale" : "3x" + }, + { + "size" : "57x57", + "idiom" : "iphone", + "filename" : "Icon.png", + "scale" : "1x" + }, + { + "size" : "57x57", + "idiom" : "iphone", + "filename" : "Icon@2x.png", + "scale" : "2x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-60@2x.png", + "scale" : "2x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-60@3x.png", + "scale" : "3x" + }, + { + "idiom" : "ipad", + "size" : "20x20", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "20x20", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-Small.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-Small@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-Small-40.png", + "scale" : "1x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-Small-40@2x.png", + "scale" : "2x" + }, + { + "size" : "50x50", + "idiom" : "ipad", + "filename" : "Icon-Small-50.png", + "scale" : "1x" + }, + { + "size" : "50x50", + "idiom" : "ipad", + "filename" : "Icon-Small-50@2x.png", + "scale" : "2x" + }, + { + "size" : "72x72", + "idiom" : "ipad", + "filename" : "Icon-72.png", + "scale" : "1x" + }, + { + "size" : "72x72", + "idiom" : "ipad", + "filename" : "Icon-72@2x.png", + "scale" : "2x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-76.png", + "scale" : "1x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-76@2x.png", + "scale" : "2x" + }, + { + "size" : "83.5x83.5", + "idiom" : "ipad", + "filename" : "Icon-iPadPro@2x.png", + "scale" : "2x" + }, + { + "idiom" : "ios-marketing", + "size" : "1024x1024", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/mobile/ios/Client/Assets/Images.xcassets/AppIcon_Developer.appiconset/Icon-60@2x.png b/mobile/ios/Client/Assets/Images.xcassets/AppIcon_Developer.appiconset/Icon-60@2x.png new file mode 100644 index 0000000000..69afebf0da Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/AppIcon_Developer.appiconset/Icon-60@2x.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/AppIcon_Developer.appiconset/Icon-60@3x.png b/mobile/ios/Client/Assets/Images.xcassets/AppIcon_Developer.appiconset/Icon-60@3x.png new file mode 100644 index 0000000000..180cf0d82c Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/AppIcon_Developer.appiconset/Icon-60@3x.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/AppIcon_Developer.appiconset/Icon-72.png b/mobile/ios/Client/Assets/Images.xcassets/AppIcon_Developer.appiconset/Icon-72.png new file mode 100644 index 0000000000..536b4a6542 Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/AppIcon_Developer.appiconset/Icon-72.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/AppIcon_Developer.appiconset/Icon-72@2x.png b/mobile/ios/Client/Assets/Images.xcassets/AppIcon_Developer.appiconset/Icon-72@2x.png new file mode 100644 index 0000000000..9e9edef349 Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/AppIcon_Developer.appiconset/Icon-72@2x.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/AppIcon_Developer.appiconset/Icon-76.png b/mobile/ios/Client/Assets/Images.xcassets/AppIcon_Developer.appiconset/Icon-76.png new file mode 100644 index 0000000000..0eef6df03a Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/AppIcon_Developer.appiconset/Icon-76.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/AppIcon_Developer.appiconset/Icon-76@2x.png b/mobile/ios/Client/Assets/Images.xcassets/AppIcon_Developer.appiconset/Icon-76@2x.png new file mode 100644 index 0000000000..825494321e Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/AppIcon_Developer.appiconset/Icon-76@2x.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/AppIcon_Developer.appiconset/Icon-Small-40.png b/mobile/ios/Client/Assets/Images.xcassets/AppIcon_Developer.appiconset/Icon-Small-40.png new file mode 100644 index 0000000000..ed4c714c6e Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/AppIcon_Developer.appiconset/Icon-Small-40.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/AppIcon_Developer.appiconset/Icon-Small-40@2x.png b/mobile/ios/Client/Assets/Images.xcassets/AppIcon_Developer.appiconset/Icon-Small-40@2x.png new file mode 100644 index 0000000000..38fc1017b1 Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/AppIcon_Developer.appiconset/Icon-Small-40@2x.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/AppIcon_Developer.appiconset/Icon-Small-50.png b/mobile/ios/Client/Assets/Images.xcassets/AppIcon_Developer.appiconset/Icon-Small-50.png new file mode 100644 index 0000000000..02bb55fee5 Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/AppIcon_Developer.appiconset/Icon-Small-50.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/AppIcon_Developer.appiconset/Icon-Small-50@2x.png b/mobile/ios/Client/Assets/Images.xcassets/AppIcon_Developer.appiconset/Icon-Small-50@2x.png new file mode 100644 index 0000000000..a6272eada9 Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/AppIcon_Developer.appiconset/Icon-Small-50@2x.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/AppIcon_Developer.appiconset/Icon-Small.png b/mobile/ios/Client/Assets/Images.xcassets/AppIcon_Developer.appiconset/Icon-Small.png new file mode 100644 index 0000000000..8f8c3cf8a8 Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/AppIcon_Developer.appiconset/Icon-Small.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/AppIcon_Developer.appiconset/Icon-Small@2x.png b/mobile/ios/Client/Assets/Images.xcassets/AppIcon_Developer.appiconset/Icon-Small@2x.png new file mode 100644 index 0000000000..7d3f3136c5 Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/AppIcon_Developer.appiconset/Icon-Small@2x.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/AppIcon_Developer.appiconset/Icon-iPadPro@2x.png b/mobile/ios/Client/Assets/Images.xcassets/AppIcon_Developer.appiconset/Icon-iPadPro@2x.png new file mode 100644 index 0000000000..68c747d41a Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/AppIcon_Developer.appiconset/Icon-iPadPro@2x.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/AppIcon_Developer.appiconset/Icon.png b/mobile/ios/Client/Assets/Images.xcassets/AppIcon_Developer.appiconset/Icon.png new file mode 100644 index 0000000000..d9d8ee0f56 Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/AppIcon_Developer.appiconset/Icon.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/AppIcon_Developer.appiconset/Icon@2x.png b/mobile/ios/Client/Assets/Images.xcassets/AppIcon_Developer.appiconset/Icon@2x.png new file mode 100644 index 0000000000..a744cc6d93 Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/AppIcon_Developer.appiconset/Icon@2x.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/Contents.json b/mobile/ios/Client/Assets/Images.xcassets/Contents.json new file mode 100644 index 0000000000..da4a164c91 --- /dev/null +++ b/mobile/ios/Client/Assets/Images.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/mobile/ios/Client/Assets/Images.xcassets/Find In Page/Contents.json b/mobile/ios/Client/Assets/Images.xcassets/Find In Page/Contents.json new file mode 100644 index 0000000000..da4a164c91 --- /dev/null +++ b/mobile/ios/Client/Assets/Images.xcassets/Find In Page/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/mobile/ios/Client/Assets/Images.xcassets/Find In Page/find_close.imageset/Contents.json b/mobile/ios/Client/Assets/Images.xcassets/Find In Page/find_close.imageset/Contents.json new file mode 100644 index 0000000000..ce0bef5ead --- /dev/null +++ b/mobile/ios/Client/Assets/Images.xcassets/Find In Page/find_close.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "close.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "close@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "close@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/mobile/ios/Client/Assets/Images.xcassets/Find In Page/find_close.imageset/close.png b/mobile/ios/Client/Assets/Images.xcassets/Find In Page/find_close.imageset/close.png new file mode 100644 index 0000000000..bc8f358e63 Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/Find In Page/find_close.imageset/close.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/Find In Page/find_close.imageset/close@2x.png b/mobile/ios/Client/Assets/Images.xcassets/Find In Page/find_close.imageset/close@2x.png new file mode 100644 index 0000000000..b2e3df259e Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/Find In Page/find_close.imageset/close@2x.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/Find In Page/find_close.imageset/close@3x.png b/mobile/ios/Client/Assets/Images.xcassets/Find In Page/find_close.imageset/close@3x.png new file mode 100644 index 0000000000..271f5d22da Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/Find In Page/find_close.imageset/close@3x.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/Find In Page/find_next.imageset/Contents.json b/mobile/ios/Client/Assets/Images.xcassets/Find In Page/find_next.imageset/Contents.json new file mode 100644 index 0000000000..84d814f3a8 --- /dev/null +++ b/mobile/ios/Client/Assets/Images.xcassets/Find In Page/find_next.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "down-caret.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "down-caret@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "down-caret@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/mobile/ios/Client/Assets/Images.xcassets/Find In Page/find_next.imageset/down-caret.png b/mobile/ios/Client/Assets/Images.xcassets/Find In Page/find_next.imageset/down-caret.png new file mode 100644 index 0000000000..ca70b1d339 Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/Find In Page/find_next.imageset/down-caret.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/Find In Page/find_next.imageset/down-caret@2x.png b/mobile/ios/Client/Assets/Images.xcassets/Find In Page/find_next.imageset/down-caret@2x.png new file mode 100644 index 0000000000..90dffe4746 Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/Find In Page/find_next.imageset/down-caret@2x.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/Find In Page/find_next.imageset/down-caret@3x.png b/mobile/ios/Client/Assets/Images.xcassets/Find In Page/find_next.imageset/down-caret@3x.png new file mode 100644 index 0000000000..40281ff09c Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/Find In Page/find_next.imageset/down-caret@3x.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/Find In Page/find_previous.imageset/Contents.json b/mobile/ios/Client/Assets/Images.xcassets/Find In Page/find_previous.imageset/Contents.json new file mode 100644 index 0000000000..50177e45fc --- /dev/null +++ b/mobile/ios/Client/Assets/Images.xcassets/Find In Page/find_previous.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "up-caret.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "up-caret@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "up-caret@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/mobile/ios/Client/Assets/Images.xcassets/Find In Page/find_previous.imageset/up-caret.png b/mobile/ios/Client/Assets/Images.xcassets/Find In Page/find_previous.imageset/up-caret.png new file mode 100644 index 0000000000..ad0177faad Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/Find In Page/find_previous.imageset/up-caret.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/Find In Page/find_previous.imageset/up-caret@2x.png b/mobile/ios/Client/Assets/Images.xcassets/Find In Page/find_previous.imageset/up-caret@2x.png new file mode 100644 index 0000000000..07b96e97e0 Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/Find In Page/find_previous.imageset/up-caret@2x.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/Find In Page/find_previous.imageset/up-caret@3x.png b/mobile/ios/Client/Assets/Images.xcassets/Find In Page/find_previous.imageset/up-caret@3x.png new file mode 100644 index 0000000000..885cdcfabf Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/Find In Page/find_previous.imageset/up-caret@3x.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/New Tab - Activity Stream/Contents.json b/mobile/ios/Client/Assets/Images.xcassets/New Tab - Activity Stream/Contents.json new file mode 100644 index 0000000000..da4a164c91 --- /dev/null +++ b/mobile/ios/Client/Assets/Images.xcassets/New Tab - Activity Stream/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/mobile/ios/Client/Assets/Images.xcassets/New Tab - Activity Stream/context_bookmark.imageset/Contents.json b/mobile/ios/Client/Assets/Images.xcassets/New Tab - Activity Stream/context_bookmark.imageset/Contents.json new file mode 100644 index 0000000000..934a827005 --- /dev/null +++ b/mobile/ios/Client/Assets/Images.xcassets/New Tab - Activity Stream/context_bookmark.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "context-bookmark.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "context-bookmark@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "context-bookmark@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/mobile/ios/Client/Assets/Images.xcassets/New Tab - Activity Stream/context_bookmark.imageset/context-bookmark.png b/mobile/ios/Client/Assets/Images.xcassets/New Tab - Activity Stream/context_bookmark.imageset/context-bookmark.png new file mode 100644 index 0000000000..e841e66870 Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/New Tab - Activity Stream/context_bookmark.imageset/context-bookmark.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/New Tab - Activity Stream/context_bookmark.imageset/context-bookmark@2x.png b/mobile/ios/Client/Assets/Images.xcassets/New Tab - Activity Stream/context_bookmark.imageset/context-bookmark@2x.png new file mode 100644 index 0000000000..c141cbf11a Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/New Tab - Activity Stream/context_bookmark.imageset/context-bookmark@2x.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/New Tab - Activity Stream/context_bookmark.imageset/context-bookmark@3x.png b/mobile/ios/Client/Assets/Images.xcassets/New Tab - Activity Stream/context_bookmark.imageset/context-bookmark@3x.png new file mode 100644 index 0000000000..3ee251dbc5 Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/New Tab - Activity Stream/context_bookmark.imageset/context-bookmark@3x.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/New Tab - Activity Stream/context_open.imageset/Contents.json b/mobile/ios/Client/Assets/Images.xcassets/New Tab - Activity Stream/context_open.imageset/Contents.json new file mode 100644 index 0000000000..fe685af20f --- /dev/null +++ b/mobile/ios/Client/Assets/Images.xcassets/New Tab - Activity Stream/context_open.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "context-open.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "context-open@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "context-open@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/mobile/ios/Client/Assets/Images.xcassets/New Tab - Activity Stream/context_open.imageset/context-open.png b/mobile/ios/Client/Assets/Images.xcassets/New Tab - Activity Stream/context_open.imageset/context-open.png new file mode 100644 index 0000000000..97836f00a8 Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/New Tab - Activity Stream/context_open.imageset/context-open.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/New Tab - Activity Stream/context_open.imageset/context-open@2x.png b/mobile/ios/Client/Assets/Images.xcassets/New Tab - Activity Stream/context_open.imageset/context-open@2x.png new file mode 100644 index 0000000000..237e493f79 Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/New Tab - Activity Stream/context_open.imageset/context-open@2x.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/New Tab - Activity Stream/context_open.imageset/context-open@3x.png b/mobile/ios/Client/Assets/Images.xcassets/New Tab - Activity Stream/context_open.imageset/context-open@3x.png new file mode 100644 index 0000000000..ac353e0edf Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/New Tab - Activity Stream/context_open.imageset/context-open@3x.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/New Tab - Activity Stream/context_sync.imageset/Contents.json b/mobile/ios/Client/Assets/Images.xcassets/New Tab - Activity Stream/context_sync.imageset/Contents.json new file mode 100644 index 0000000000..d969b7c7cb --- /dev/null +++ b/mobile/ios/Client/Assets/Images.xcassets/New Tab - Activity Stream/context_sync.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "context-sync.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "context-sync@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "context-sync@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/mobile/ios/Client/Assets/Images.xcassets/New Tab - Activity Stream/context_sync.imageset/context-sync.png b/mobile/ios/Client/Assets/Images.xcassets/New Tab - Activity Stream/context_sync.imageset/context-sync.png new file mode 100644 index 0000000000..c6d8de4d1d Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/New Tab - Activity Stream/context_sync.imageset/context-sync.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/New Tab - Activity Stream/context_sync.imageset/context-sync@2x.png b/mobile/ios/Client/Assets/Images.xcassets/New Tab - Activity Stream/context_sync.imageset/context-sync@2x.png new file mode 100644 index 0000000000..3ab46d84a8 Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/New Tab - Activity Stream/context_sync.imageset/context-sync@2x.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/New Tab - Activity Stream/context_sync.imageset/context-sync@3x.png b/mobile/ios/Client/Assets/Images.xcassets/New Tab - Activity Stream/context_sync.imageset/context-sync@3x.png new file mode 100644 index 0000000000..4be467bb33 Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/New Tab - Activity Stream/context_sync.imageset/context-sync@3x.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/New Tab - Activity Stream/context_viewed.imageset/Contents.json b/mobile/ios/Client/Assets/Images.xcassets/New Tab - Activity Stream/context_viewed.imageset/Contents.json new file mode 100644 index 0000000000..8d80d53aed --- /dev/null +++ b/mobile/ios/Client/Assets/Images.xcassets/New Tab - Activity Stream/context_viewed.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "context-viewed.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "context-viewed@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "context-viewed@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/mobile/ios/Client/Assets/Images.xcassets/New Tab - Activity Stream/context_viewed.imageset/context-viewed.png b/mobile/ios/Client/Assets/Images.xcassets/New Tab - Activity Stream/context_viewed.imageset/context-viewed.png new file mode 100644 index 0000000000..451d832b79 Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/New Tab - Activity Stream/context_viewed.imageset/context-viewed.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/New Tab - Activity Stream/context_viewed.imageset/context-viewed@2x.png b/mobile/ios/Client/Assets/Images.xcassets/New Tab - Activity Stream/context_viewed.imageset/context-viewed@2x.png new file mode 100644 index 0000000000..2251d042e0 Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/New Tab - Activity Stream/context_viewed.imageset/context-viewed@2x.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/New Tab - Activity Stream/context_viewed.imageset/context-viewed@3x.png b/mobile/ios/Client/Assets/Images.xcassets/New Tab - Activity Stream/context_viewed.imageset/context-viewed@3x.png new file mode 100644 index 0000000000..83f269615f Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/New Tab - Activity Stream/context_viewed.imageset/context-viewed@3x.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/New Tab - Activity Stream/defaultTopSiteIcon.imageset/Contents.json b/mobile/ios/Client/Assets/Images.xcassets/New Tab - Activity Stream/defaultTopSiteIcon.imageset/Contents.json new file mode 100644 index 0000000000..87ea3dc957 --- /dev/null +++ b/mobile/ios/Client/Assets/Images.xcassets/New Tab - Activity Stream/defaultTopSiteIcon.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "defaultTopSiteIcon.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "defaultTopSiteIcon@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "defaultTopSiteIcon@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/mobile/ios/Client/Assets/Images.xcassets/New Tab - Activity Stream/defaultTopSiteIcon.imageset/defaultTopSiteIcon.png b/mobile/ios/Client/Assets/Images.xcassets/New Tab - Activity Stream/defaultTopSiteIcon.imageset/defaultTopSiteIcon.png new file mode 100644 index 0000000000..b9eaee02c2 Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/New Tab - Activity Stream/defaultTopSiteIcon.imageset/defaultTopSiteIcon.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/New Tab - Activity Stream/defaultTopSiteIcon.imageset/defaultTopSiteIcon@2x.png b/mobile/ios/Client/Assets/Images.xcassets/New Tab - Activity Stream/defaultTopSiteIcon.imageset/defaultTopSiteIcon@2x.png new file mode 100644 index 0000000000..377e56b11c Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/New Tab - Activity Stream/defaultTopSiteIcon.imageset/defaultTopSiteIcon@2x.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/New Tab - Activity Stream/defaultTopSiteIcon.imageset/defaultTopSiteIcon@3x.png b/mobile/ios/Client/Assets/Images.xcassets/New Tab - Activity Stream/defaultTopSiteIcon.imageset/defaultTopSiteIcon@3x.png new file mode 100644 index 0000000000..813b775563 Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/New Tab - Activity Stream/defaultTopSiteIcon.imageset/defaultTopSiteIcon@3x.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/New Tab - Activity Stream/pin_small.imageset/Contents.json b/mobile/ios/Client/Assets/Images.xcassets/New Tab - Activity Stream/pin_small.imageset/Contents.json new file mode 100644 index 0000000000..58b6fb9a2b --- /dev/null +++ b/mobile/ios/Client/Assets/Images.xcassets/New Tab - Activity Stream/pin_small.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "pin-small.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "pin-small@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "pin-small@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/mobile/ios/Client/Assets/Images.xcassets/New Tab - Activity Stream/pin_small.imageset/pin-small.png b/mobile/ios/Client/Assets/Images.xcassets/New Tab - Activity Stream/pin_small.imageset/pin-small.png new file mode 100644 index 0000000000..e396ad4df7 Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/New Tab - Activity Stream/pin_small.imageset/pin-small.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/New Tab - Activity Stream/pin_small.imageset/pin-small@2x.png b/mobile/ios/Client/Assets/Images.xcassets/New Tab - Activity Stream/pin_small.imageset/pin-small@2x.png new file mode 100644 index 0000000000..84ada8d0b6 Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/New Tab - Activity Stream/pin_small.imageset/pin-small@2x.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/New Tab - Activity Stream/pin_small.imageset/pin-small@3x.png b/mobile/ios/Client/Assets/Images.xcassets/New Tab - Activity Stream/pin_small.imageset/pin-small@3x.png new file mode 100644 index 0000000000..f1c3a4061d Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/New Tab - Activity Stream/pin_small.imageset/pin-small@3x.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/New Tab - Context menu/Contents.json b/mobile/ios/Client/Assets/Images.xcassets/New Tab - Context menu/Contents.json new file mode 100644 index 0000000000..da4a164c91 --- /dev/null +++ b/mobile/ios/Client/Assets/Images.xcassets/New Tab - Context menu/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/mobile/ios/Client/Assets/Images.xcassets/New Tab - Context menu/action_bookmark.imageset/Contents.json b/mobile/ios/Client/Assets/Images.xcassets/New Tab - Context menu/action_bookmark.imageset/Contents.json new file mode 100644 index 0000000000..634f28fb7f --- /dev/null +++ b/mobile/ios/Client/Assets/Images.xcassets/New Tab - Context menu/action_bookmark.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "action-bookmark.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "action-bookmark@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "action-bookmark@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/mobile/ios/Client/Assets/Images.xcassets/New Tab - Context menu/action_bookmark.imageset/action-bookmark.png b/mobile/ios/Client/Assets/Images.xcassets/New Tab - Context menu/action_bookmark.imageset/action-bookmark.png new file mode 100644 index 0000000000..8855921529 Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/New Tab - Context menu/action_bookmark.imageset/action-bookmark.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/New Tab - Context menu/action_bookmark.imageset/action-bookmark@2x.png b/mobile/ios/Client/Assets/Images.xcassets/New Tab - Context menu/action_bookmark.imageset/action-bookmark@2x.png new file mode 100644 index 0000000000..4a19d302dd Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/New Tab - Context menu/action_bookmark.imageset/action-bookmark@2x.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/New Tab - Context menu/action_bookmark.imageset/action-bookmark@3x.png b/mobile/ios/Client/Assets/Images.xcassets/New Tab - Context menu/action_bookmark.imageset/action-bookmark@3x.png new file mode 100644 index 0000000000..2d6b48f8fe Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/New Tab - Context menu/action_bookmark.imageset/action-bookmark@3x.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/New Tab - Context menu/action_bookmark_remove.imageset/Contents.json b/mobile/ios/Client/Assets/Images.xcassets/New Tab - Context menu/action_bookmark_remove.imageset/Contents.json new file mode 100644 index 0000000000..f61da3da9f --- /dev/null +++ b/mobile/ios/Client/Assets/Images.xcassets/New Tab - Context menu/action_bookmark_remove.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "action-bookmark-remove.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "action-bookmark-remove@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "action-bookmark-remove@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/mobile/ios/Client/Assets/Images.xcassets/New Tab - Context menu/action_bookmark_remove.imageset/action-bookmark-remove.png b/mobile/ios/Client/Assets/Images.xcassets/New Tab - Context menu/action_bookmark_remove.imageset/action-bookmark-remove.png new file mode 100644 index 0000000000..8d66b36d99 Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/New Tab - Context menu/action_bookmark_remove.imageset/action-bookmark-remove.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/New Tab - Context menu/action_bookmark_remove.imageset/action-bookmark-remove@2x.png b/mobile/ios/Client/Assets/Images.xcassets/New Tab - Context menu/action_bookmark_remove.imageset/action-bookmark-remove@2x.png new file mode 100644 index 0000000000..bdb615f03e Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/New Tab - Context menu/action_bookmark_remove.imageset/action-bookmark-remove@2x.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/New Tab - Context menu/action_bookmark_remove.imageset/action-bookmark-remove@3x.png b/mobile/ios/Client/Assets/Images.xcassets/New Tab - Context menu/action_bookmark_remove.imageset/action-bookmark-remove@3x.png new file mode 100644 index 0000000000..388cddb0b7 Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/New Tab - Context menu/action_bookmark_remove.imageset/action-bookmark-remove@3x.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/New Tab - Context menu/action_delete.imageset/Contents.json b/mobile/ios/Client/Assets/Images.xcassets/New Tab - Context menu/action_delete.imageset/Contents.json new file mode 100644 index 0000000000..af874c87b7 --- /dev/null +++ b/mobile/ios/Client/Assets/Images.xcassets/New Tab - Context menu/action_delete.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "action-delete.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "action-delete@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "action-delete@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/mobile/ios/Client/Assets/Images.xcassets/New Tab - Context menu/action_delete.imageset/action-delete.png b/mobile/ios/Client/Assets/Images.xcassets/New Tab - Context menu/action_delete.imageset/action-delete.png new file mode 100644 index 0000000000..c5558b03fa Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/New Tab - Context menu/action_delete.imageset/action-delete.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/New Tab - Context menu/action_delete.imageset/action-delete@2x.png b/mobile/ios/Client/Assets/Images.xcassets/New Tab - Context menu/action_delete.imageset/action-delete@2x.png new file mode 100644 index 0000000000..4887e4d800 Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/New Tab - Context menu/action_delete.imageset/action-delete@2x.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/New Tab - Context menu/action_delete.imageset/action-delete@3x.png b/mobile/ios/Client/Assets/Images.xcassets/New Tab - Context menu/action_delete.imageset/action-delete@3x.png new file mode 100644 index 0000000000..7c9b013017 Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/New Tab - Context menu/action_delete.imageset/action-delete@3x.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/New Tab - Context menu/action_pin.imageset/Contents.json b/mobile/ios/Client/Assets/Images.xcassets/New Tab - Context menu/action_pin.imageset/Contents.json new file mode 100644 index 0000000000..4853fc215e --- /dev/null +++ b/mobile/ios/Client/Assets/Images.xcassets/New Tab - Context menu/action_pin.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "pin-28.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "pin-28@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "pin-28@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/mobile/ios/Client/Assets/Images.xcassets/New Tab - Context menu/action_pin.imageset/pin-28.png b/mobile/ios/Client/Assets/Images.xcassets/New Tab - Context menu/action_pin.imageset/pin-28.png new file mode 100644 index 0000000000..aa499fd220 Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/New Tab - Context menu/action_pin.imageset/pin-28.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/New Tab - Context menu/action_pin.imageset/pin-28@2x.png b/mobile/ios/Client/Assets/Images.xcassets/New Tab - Context menu/action_pin.imageset/pin-28@2x.png new file mode 100644 index 0000000000..c6a15d4061 Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/New Tab - Context menu/action_pin.imageset/pin-28@2x.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/New Tab - Context menu/action_pin.imageset/pin-28@3x.png b/mobile/ios/Client/Assets/Images.xcassets/New Tab - Context menu/action_pin.imageset/pin-28@3x.png new file mode 100644 index 0000000000..7f86c43493 Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/New Tab - Context menu/action_pin.imageset/pin-28@3x.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/New Tab - Context menu/action_remove.imageset/Contents.json b/mobile/ios/Client/Assets/Images.xcassets/New Tab - Context menu/action_remove.imageset/Contents.json new file mode 100644 index 0000000000..83911224ce --- /dev/null +++ b/mobile/ios/Client/Assets/Images.xcassets/New Tab - Context menu/action_remove.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "action-remove.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "action-remove@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "action-remove@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/mobile/ios/Client/Assets/Images.xcassets/New Tab - Context menu/action_remove.imageset/action-remove.png b/mobile/ios/Client/Assets/Images.xcassets/New Tab - Context menu/action_remove.imageset/action-remove.png new file mode 100644 index 0000000000..bb734fd300 Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/New Tab - Context menu/action_remove.imageset/action-remove.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/New Tab - Context menu/action_remove.imageset/action-remove@2x.png b/mobile/ios/Client/Assets/Images.xcassets/New Tab - Context menu/action_remove.imageset/action-remove@2x.png new file mode 100644 index 0000000000..f284416a28 Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/New Tab - Context menu/action_remove.imageset/action-remove@2x.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/New Tab - Context menu/action_remove.imageset/action-remove@3x.png b/mobile/ios/Client/Assets/Images.xcassets/New Tab - Context menu/action_remove.imageset/action-remove@3x.png new file mode 100644 index 0000000000..ce55a980f6 Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/New Tab - Context menu/action_remove.imageset/action-remove@3x.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/New Tab - Context menu/action_share.imageset/Contents.json b/mobile/ios/Client/Assets/Images.xcassets/New Tab - Context menu/action_share.imageset/Contents.json new file mode 100644 index 0000000000..d3f5a2b43a --- /dev/null +++ b/mobile/ios/Client/Assets/Images.xcassets/New Tab - Context menu/action_share.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "action-share.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "action-share@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "action-share@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/mobile/ios/Client/Assets/Images.xcassets/New Tab - Context menu/action_share.imageset/action-share.png b/mobile/ios/Client/Assets/Images.xcassets/New Tab - Context menu/action_share.imageset/action-share.png new file mode 100644 index 0000000000..f6906f3268 Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/New Tab - Context menu/action_share.imageset/action-share.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/New Tab - Context menu/action_share.imageset/action-share@2x.png b/mobile/ios/Client/Assets/Images.xcassets/New Tab - Context menu/action_share.imageset/action-share@2x.png new file mode 100644 index 0000000000..36c42af0cd Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/New Tab - Context menu/action_share.imageset/action-share@2x.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/New Tab - Context menu/action_share.imageset/action-share@3x.png b/mobile/ios/Client/Assets/Images.xcassets/New Tab - Context menu/action_share.imageset/action-share@3x.png new file mode 100644 index 0000000000..48dd93260c Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/New Tab - Context menu/action_share.imageset/action-share@3x.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/New Tab - Context menu/action_unpin.imageset/Contents.json b/mobile/ios/Client/Assets/Images.xcassets/New Tab - Context menu/action_unpin.imageset/Contents.json new file mode 100644 index 0000000000..ecbfadc9b4 --- /dev/null +++ b/mobile/ios/Client/Assets/Images.xcassets/New Tab - Context menu/action_unpin.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "pinremove.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "pinremove@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "pinremove@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/mobile/ios/Client/Assets/Images.xcassets/New Tab - Context menu/action_unpin.imageset/pinremove.png b/mobile/ios/Client/Assets/Images.xcassets/New Tab - Context menu/action_unpin.imageset/pinremove.png new file mode 100644 index 0000000000..ed7405b12a Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/New Tab - Context menu/action_unpin.imageset/pinremove.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/New Tab - Context menu/action_unpin.imageset/pinremove@2x.png b/mobile/ios/Client/Assets/Images.xcassets/New Tab - Context menu/action_unpin.imageset/pinremove@2x.png new file mode 100644 index 0000000000..7be480e450 Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/New Tab - Context menu/action_unpin.imageset/pinremove@2x.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/New Tab - Context menu/action_unpin.imageset/pinremove@3x.png b/mobile/ios/Client/Assets/Images.xcassets/New Tab - Context menu/action_unpin.imageset/pinremove@3x.png new file mode 100644 index 0000000000..0d318d2df7 Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/New Tab - Context menu/action_unpin.imageset/pinremove@3x.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/New Tab - Reader/Contents.json b/mobile/ios/Client/Assets/Images.xcassets/New Tab - Reader/Contents.json new file mode 100644 index 0000000000..da4a164c91 --- /dev/null +++ b/mobile/ios/Client/Assets/Images.xcassets/New Tab - Reader/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/mobile/ios/Client/Assets/Images.xcassets/New Tab - Reader/MarkAsRead.imageset/Contents.json b/mobile/ios/Client/Assets/Images.xcassets/New Tab - Reader/MarkAsRead.imageset/Contents.json new file mode 100644 index 0000000000..4c1381f4b6 --- /dev/null +++ b/mobile/ios/Client/Assets/Images.xcassets/New Tab - Reader/MarkAsRead.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "UnRead.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "UnRead@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "UnRead@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/mobile/ios/Client/Assets/Images.xcassets/New Tab - Reader/MarkAsRead.imageset/UnRead.png b/mobile/ios/Client/Assets/Images.xcassets/New Tab - Reader/MarkAsRead.imageset/UnRead.png new file mode 100644 index 0000000000..1fd050cdd6 Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/New Tab - Reader/MarkAsRead.imageset/UnRead.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/New Tab - Reader/MarkAsRead.imageset/UnRead@2x.png b/mobile/ios/Client/Assets/Images.xcassets/New Tab - Reader/MarkAsRead.imageset/UnRead@2x.png new file mode 100644 index 0000000000..102338529b Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/New Tab - Reader/MarkAsRead.imageset/UnRead@2x.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/New Tab - Reader/MarkAsRead.imageset/UnRead@3x.png b/mobile/ios/Client/Assets/Images.xcassets/New Tab - Reader/MarkAsRead.imageset/UnRead@3x.png new file mode 100644 index 0000000000..36e4f55100 Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/New Tab - Reader/MarkAsRead.imageset/UnRead@3x.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/New Tab - Reader/MarkAsUnread.imageset/Contents.json b/mobile/ios/Client/Assets/Images.xcassets/New Tab - Reader/MarkAsUnread.imageset/Contents.json new file mode 100644 index 0000000000..d5a344b77e --- /dev/null +++ b/mobile/ios/Client/Assets/Images.xcassets/New Tab - Reader/MarkAsUnread.imageset/Contents.json @@ -0,0 +1,26 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "Read.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "Read@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "Read@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + }, + "properties" : { + "template-rendering-intent" : "template" + } +} \ No newline at end of file diff --git a/mobile/ios/Client/Assets/Images.xcassets/New Tab - Reader/MarkAsUnread.imageset/Read.png b/mobile/ios/Client/Assets/Images.xcassets/New Tab - Reader/MarkAsUnread.imageset/Read.png new file mode 100644 index 0000000000..2c3e6c0675 Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/New Tab - Reader/MarkAsUnread.imageset/Read.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/New Tab - Reader/MarkAsUnread.imageset/Read@2x.png b/mobile/ios/Client/Assets/Images.xcassets/New Tab - Reader/MarkAsUnread.imageset/Read@2x.png new file mode 100644 index 0000000000..80150c9a02 Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/New Tab - Reader/MarkAsUnread.imageset/Read@2x.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/New Tab - Reader/MarkAsUnread.imageset/Read@3x.png b/mobile/ios/Client/Assets/Images.xcassets/New Tab - Reader/MarkAsUnread.imageset/Read@3x.png new file mode 100644 index 0000000000..b019faaff7 Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/New Tab - Reader/MarkAsUnread.imageset/Read@3x.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/New Tab - Reader/ReaderModeActive.imageset/Contents.json b/mobile/ios/Client/Assets/Images.xcassets/New Tab - Reader/ReaderModeActive.imageset/Contents.json new file mode 100644 index 0000000000..2590c5c946 --- /dev/null +++ b/mobile/ios/Client/Assets/Images.xcassets/New Tab - Reader/ReaderModeActive.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "reader-active.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "reader-active@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "reader-active@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/mobile/ios/Client/Assets/Images.xcassets/New Tab - Reader/ReaderModeActive.imageset/reader-active.png b/mobile/ios/Client/Assets/Images.xcassets/New Tab - Reader/ReaderModeActive.imageset/reader-active.png new file mode 100644 index 0000000000..d52506cf9e Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/New Tab - Reader/ReaderModeActive.imageset/reader-active.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/New Tab - Reader/ReaderModeActive.imageset/reader-active@2x.png b/mobile/ios/Client/Assets/Images.xcassets/New Tab - Reader/ReaderModeActive.imageset/reader-active@2x.png new file mode 100644 index 0000000000..c5f14e2c80 Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/New Tab - Reader/ReaderModeActive.imageset/reader-active@2x.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/New Tab - Reader/ReaderModeActive.imageset/reader-active@3x.png b/mobile/ios/Client/Assets/Images.xcassets/New Tab - Reader/ReaderModeActive.imageset/reader-active@3x.png new file mode 100644 index 0000000000..c06efb3720 Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/New Tab - Reader/ReaderModeActive.imageset/reader-active@3x.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/New Tab - Reader/ReaderModeAvailable.imageset/Contents.json b/mobile/ios/Client/Assets/Images.xcassets/New Tab - Reader/ReaderModeAvailable.imageset/Contents.json new file mode 100644 index 0000000000..c38bff5d49 --- /dev/null +++ b/mobile/ios/Client/Assets/Images.xcassets/New Tab - Reader/ReaderModeAvailable.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "readerOff.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "readerOff@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "readerOff@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/mobile/ios/Client/Assets/Images.xcassets/New Tab - Reader/ReaderModeAvailable.imageset/readerOff.png b/mobile/ios/Client/Assets/Images.xcassets/New Tab - Reader/ReaderModeAvailable.imageset/readerOff.png new file mode 100644 index 0000000000..64466e2197 Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/New Tab - Reader/ReaderModeAvailable.imageset/readerOff.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/New Tab - Reader/ReaderModeAvailable.imageset/readerOff@2x.png b/mobile/ios/Client/Assets/Images.xcassets/New Tab - Reader/ReaderModeAvailable.imageset/readerOff@2x.png new file mode 100644 index 0000000000..52cb45c219 Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/New Tab - Reader/ReaderModeAvailable.imageset/readerOff@2x.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/New Tab - Reader/ReaderModeAvailable.imageset/readerOff@3x.png b/mobile/ios/Client/Assets/Images.xcassets/New Tab - Reader/ReaderModeAvailable.imageset/readerOff@3x.png new file mode 100644 index 0000000000..d6b183e99c Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/New Tab - Reader/ReaderModeAvailable.imageset/readerOff@3x.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/New Tab - Reader/SettingsSansSerif.imageset/AaFira.png b/mobile/ios/Client/Assets/Images.xcassets/New Tab - Reader/SettingsSansSerif.imageset/AaFira.png new file mode 100644 index 0000000000..dcb18d3343 Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/New Tab - Reader/SettingsSansSerif.imageset/AaFira.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/New Tab - Reader/SettingsSansSerif.imageset/AaFira@2x.png b/mobile/ios/Client/Assets/Images.xcassets/New Tab - Reader/SettingsSansSerif.imageset/AaFira@2x.png new file mode 100644 index 0000000000..161377faab Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/New Tab - Reader/SettingsSansSerif.imageset/AaFira@2x.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/New Tab - Reader/SettingsSansSerif.imageset/AaFira@3x.png b/mobile/ios/Client/Assets/Images.xcassets/New Tab - Reader/SettingsSansSerif.imageset/AaFira@3x.png new file mode 100644 index 0000000000..289749ccb9 Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/New Tab - Reader/SettingsSansSerif.imageset/AaFira@3x.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/New Tab - Reader/SettingsSansSerif.imageset/Contents.json b/mobile/ios/Client/Assets/Images.xcassets/New Tab - Reader/SettingsSansSerif.imageset/Contents.json new file mode 100644 index 0000000000..8c210bb280 --- /dev/null +++ b/mobile/ios/Client/Assets/Images.xcassets/New Tab - Reader/SettingsSansSerif.imageset/Contents.json @@ -0,0 +1,26 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "AaFira.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "AaFira@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "AaFira@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + }, + "properties" : { + "template-rendering-intent" : "template" + } +} \ No newline at end of file diff --git a/mobile/ios/Client/Assets/Images.xcassets/New Tab - Reader/SettingsSerif.imageset/AaCharis.png b/mobile/ios/Client/Assets/Images.xcassets/New Tab - Reader/SettingsSerif.imageset/AaCharis.png new file mode 100644 index 0000000000..76b6706fde Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/New Tab - Reader/SettingsSerif.imageset/AaCharis.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/New Tab - Reader/SettingsSerif.imageset/AaCharis@2x.png b/mobile/ios/Client/Assets/Images.xcassets/New Tab - Reader/SettingsSerif.imageset/AaCharis@2x.png new file mode 100644 index 0000000000..a28c649f5d Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/New Tab - Reader/SettingsSerif.imageset/AaCharis@2x.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/New Tab - Reader/SettingsSerif.imageset/AaCharis@3x.png b/mobile/ios/Client/Assets/Images.xcassets/New Tab - Reader/SettingsSerif.imageset/AaCharis@3x.png new file mode 100644 index 0000000000..e9b70c0700 Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/New Tab - Reader/SettingsSerif.imageset/AaCharis@3x.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/New Tab - Reader/SettingsSerif.imageset/Contents.json b/mobile/ios/Client/Assets/Images.xcassets/New Tab - Reader/SettingsSerif.imageset/Contents.json new file mode 100644 index 0000000000..b9bf38976e --- /dev/null +++ b/mobile/ios/Client/Assets/Images.xcassets/New Tab - Reader/SettingsSerif.imageset/Contents.json @@ -0,0 +1,26 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "AaCharis.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "AaCharis@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "AaCharis@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + }, + "properties" : { + "template-rendering-intent" : "template" + } +} \ No newline at end of file diff --git a/mobile/ios/Client/Assets/Images.xcassets/New Tab - Reader/addToReadingList.imageset/Contents.json b/mobile/ios/Client/Assets/Images.xcassets/New Tab - Reader/addToReadingList.imageset/Contents.json new file mode 100644 index 0000000000..974ebfa990 --- /dev/null +++ b/mobile/ios/Client/Assets/Images.xcassets/New Tab - Reader/addToReadingList.imageset/Contents.json @@ -0,0 +1,26 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "reading-list-add.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "reading-list-add@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "reading-list-add@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + }, + "properties" : { + "template-rendering-intent" : "template" + } +} \ No newline at end of file diff --git a/mobile/ios/Client/Assets/Images.xcassets/New Tab - Reader/addToReadingList.imageset/reading-list-add.png b/mobile/ios/Client/Assets/Images.xcassets/New Tab - Reader/addToReadingList.imageset/reading-list-add.png new file mode 100644 index 0000000000..c1d899a31f Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/New Tab - Reader/addToReadingList.imageset/reading-list-add.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/New Tab - Reader/addToReadingList.imageset/reading-list-add@2x.png b/mobile/ios/Client/Assets/Images.xcassets/New Tab - Reader/addToReadingList.imageset/reading-list-add@2x.png new file mode 100644 index 0000000000..66ca5a99ec Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/New Tab - Reader/addToReadingList.imageset/reading-list-add@2x.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/New Tab - Reader/addToReadingList.imageset/reading-list-add@3x.png b/mobile/ios/Client/Assets/Images.xcassets/New Tab - Reader/addToReadingList.imageset/reading-list-add@3x.png new file mode 100644 index 0000000000..998f4a164c Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/New Tab - Reader/addToReadingList.imageset/reading-list-add@3x.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/New Tab - Reader/brightnessMax.imageset/Contents.json b/mobile/ios/Client/Assets/Images.xcassets/New Tab - Reader/brightnessMax.imageset/Contents.json new file mode 100644 index 0000000000..f4763c0619 --- /dev/null +++ b/mobile/ios/Client/Assets/Images.xcassets/New Tab - Reader/brightnessMax.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x", + "filename" : "brightnessMax.png" + }, + { + "idiom" : "universal", + "scale" : "2x", + "filename" : "brightnessMax@2x.png" + }, + { + "idiom" : "universal", + "scale" : "3x", + "filename" : "brightnessMax@3x.png" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/mobile/ios/Client/Assets/Images.xcassets/New Tab - Reader/brightnessMax.imageset/brightnessMax.png b/mobile/ios/Client/Assets/Images.xcassets/New Tab - Reader/brightnessMax.imageset/brightnessMax.png new file mode 100644 index 0000000000..5cf8fc9846 Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/New Tab - Reader/brightnessMax.imageset/brightnessMax.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/New Tab - Reader/brightnessMax.imageset/brightnessMax@2x.png b/mobile/ios/Client/Assets/Images.xcassets/New Tab - Reader/brightnessMax.imageset/brightnessMax@2x.png new file mode 100644 index 0000000000..4244ba147a Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/New Tab - Reader/brightnessMax.imageset/brightnessMax@2x.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/New Tab - Reader/brightnessMax.imageset/brightnessMax@3x.png b/mobile/ios/Client/Assets/Images.xcassets/New Tab - Reader/brightnessMax.imageset/brightnessMax@3x.png new file mode 100644 index 0000000000..65aef04923 Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/New Tab - Reader/brightnessMax.imageset/brightnessMax@3x.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/New Tab - Reader/brightnessMin.imageset/Contents.json b/mobile/ios/Client/Assets/Images.xcassets/New Tab - Reader/brightnessMin.imageset/Contents.json new file mode 100644 index 0000000000..7d3d89c412 --- /dev/null +++ b/mobile/ios/Client/Assets/Images.xcassets/New Tab - Reader/brightnessMin.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x", + "filename" : "brightnessMin.png" + }, + { + "idiom" : "universal", + "scale" : "2x", + "filename" : "brightnessMin@2x.png" + }, + { + "idiom" : "universal", + "scale" : "3x", + "filename" : "brightnessMin@3x.png" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/mobile/ios/Client/Assets/Images.xcassets/New Tab - Reader/brightnessMin.imageset/brightnessMin.png b/mobile/ios/Client/Assets/Images.xcassets/New Tab - Reader/brightnessMin.imageset/brightnessMin.png new file mode 100644 index 0000000000..66c5b4ff73 Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/New Tab - Reader/brightnessMin.imageset/brightnessMin.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/New Tab - Reader/brightnessMin.imageset/brightnessMin@2x.png b/mobile/ios/Client/Assets/Images.xcassets/New Tab - Reader/brightnessMin.imageset/brightnessMin@2x.png new file mode 100644 index 0000000000..f305528294 Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/New Tab - Reader/brightnessMin.imageset/brightnessMin@2x.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/New Tab - Reader/brightnessMin.imageset/brightnessMin@3x.png b/mobile/ios/Client/Assets/Images.xcassets/New Tab - Reader/brightnessMin.imageset/brightnessMin@3x.png new file mode 100644 index 0000000000..9e0aadf5f5 Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/New Tab - Reader/brightnessMin.imageset/brightnessMin@3x.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/New Tab - Reader/removeFromReadingList.imageset/Contents.json b/mobile/ios/Client/Assets/Images.xcassets/New Tab - Reader/removeFromReadingList.imageset/Contents.json new file mode 100644 index 0000000000..369bcceaf3 --- /dev/null +++ b/mobile/ios/Client/Assets/Images.xcassets/New Tab - Reader/removeFromReadingList.imageset/Contents.json @@ -0,0 +1,26 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "removeFromList.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "removeFromList@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "removeFromList@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + }, + "properties" : { + "template-rendering-intent" : "template" + } +} \ No newline at end of file diff --git a/mobile/ios/Client/Assets/Images.xcassets/New Tab - Reader/removeFromReadingList.imageset/removeFromList.png b/mobile/ios/Client/Assets/Images.xcassets/New Tab - Reader/removeFromReadingList.imageset/removeFromList.png new file mode 100644 index 0000000000..c752f67cd7 Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/New Tab - Reader/removeFromReadingList.imageset/removeFromList.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/New Tab - Reader/removeFromReadingList.imageset/removeFromList@2x.png b/mobile/ios/Client/Assets/Images.xcassets/New Tab - Reader/removeFromReadingList.imageset/removeFromList@2x.png new file mode 100644 index 0000000000..9391410930 Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/New Tab - Reader/removeFromReadingList.imageset/removeFromList@2x.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/New Tab - Reader/removeFromReadingList.imageset/removeFromList@3x.png b/mobile/ios/Client/Assets/Images.xcassets/New Tab - Reader/removeFromReadingList.imageset/removeFromList@3x.png new file mode 100644 index 0000000000..c233e8f1fa Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/New Tab - Reader/removeFromReadingList.imageset/removeFromList@3x.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/TabToolbar/Contents.json b/mobile/ios/Client/Assets/Images.xcassets/TabToolbar/Contents.json new file mode 100644 index 0000000000..da4a164c91 --- /dev/null +++ b/mobile/ios/Client/Assets/Images.xcassets/TabToolbar/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/mobile/ios/Client/Assets/Images.xcassets/TabToolbar/nav-add.imageset/Contents.json b/mobile/ios/Client/Assets/Images.xcassets/TabToolbar/nav-add.imageset/Contents.json new file mode 100644 index 0000000000..7ff94876b9 --- /dev/null +++ b/mobile/ios/Client/Assets/Images.xcassets/TabToolbar/nav-add.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "nav-add.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "nav-add@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "nav-add@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/mobile/ios/Client/Assets/Images.xcassets/TabToolbar/nav-add.imageset/nav-add.png b/mobile/ios/Client/Assets/Images.xcassets/TabToolbar/nav-add.imageset/nav-add.png new file mode 100644 index 0000000000..f06bd5d50b Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/TabToolbar/nav-add.imageset/nav-add.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/TabToolbar/nav-add.imageset/nav-add@2x.png b/mobile/ios/Client/Assets/Images.xcassets/TabToolbar/nav-add.imageset/nav-add@2x.png new file mode 100644 index 0000000000..cc583a213a Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/TabToolbar/nav-add.imageset/nav-add@2x.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/TabToolbar/nav-add.imageset/nav-add@3x.png b/mobile/ios/Client/Assets/Images.xcassets/TabToolbar/nav-add.imageset/nav-add@3x.png new file mode 100644 index 0000000000..450497424e Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/TabToolbar/nav-add.imageset/nav-add@3x.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/TabToolbar/nav-back.imageset/Contents.json b/mobile/ios/Client/Assets/Images.xcassets/TabToolbar/nav-back.imageset/Contents.json new file mode 100644 index 0000000000..3bad3d0c6e --- /dev/null +++ b/mobile/ios/Client/Assets/Images.xcassets/TabToolbar/nav-back.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "back.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "back@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "back@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/mobile/ios/Client/Assets/Images.xcassets/TabToolbar/nav-back.imageset/back.png b/mobile/ios/Client/Assets/Images.xcassets/TabToolbar/nav-back.imageset/back.png new file mode 100644 index 0000000000..7c59b71627 Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/TabToolbar/nav-back.imageset/back.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/TabToolbar/nav-back.imageset/back@2x.png b/mobile/ios/Client/Assets/Images.xcassets/TabToolbar/nav-back.imageset/back@2x.png new file mode 100644 index 0000000000..490cd4472f Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/TabToolbar/nav-back.imageset/back@2x.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/TabToolbar/nav-back.imageset/back@3x.png b/mobile/ios/Client/Assets/Images.xcassets/TabToolbar/nav-back.imageset/back@3x.png new file mode 100644 index 0000000000..b6bc96c8ad Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/TabToolbar/nav-back.imageset/back@3x.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/TabToolbar/nav-forward.imageset/Contents.json b/mobile/ios/Client/Assets/Images.xcassets/TabToolbar/nav-forward.imageset/Contents.json new file mode 100644 index 0000000000..b92c79b15c --- /dev/null +++ b/mobile/ios/Client/Assets/Images.xcassets/TabToolbar/nav-forward.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "forward.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "forward@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "forward@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/mobile/ios/Client/Assets/Images.xcassets/TabToolbar/nav-forward.imageset/forward.png b/mobile/ios/Client/Assets/Images.xcassets/TabToolbar/nav-forward.imageset/forward.png new file mode 100644 index 0000000000..740d299799 Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/TabToolbar/nav-forward.imageset/forward.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/TabToolbar/nav-forward.imageset/forward@2x.png b/mobile/ios/Client/Assets/Images.xcassets/TabToolbar/nav-forward.imageset/forward@2x.png new file mode 100644 index 0000000000..7ff732ad45 Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/TabToolbar/nav-forward.imageset/forward@2x.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/TabToolbar/nav-forward.imageset/forward@3x.png b/mobile/ios/Client/Assets/Images.xcassets/TabToolbar/nav-forward.imageset/forward@3x.png new file mode 100644 index 0000000000..d84ee20140 Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/TabToolbar/nav-forward.imageset/forward@3x.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/TabToolbar/nav-menu.imageset/Contents.json b/mobile/ios/Client/Assets/Images.xcassets/TabToolbar/nav-menu.imageset/Contents.json new file mode 100644 index 0000000000..acf46f4dde --- /dev/null +++ b/mobile/ios/Client/Assets/Images.xcassets/TabToolbar/nav-menu.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "menu.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "menu@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "menu@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/mobile/ios/Client/Assets/Images.xcassets/TabToolbar/nav-menu.imageset/menu.png b/mobile/ios/Client/Assets/Images.xcassets/TabToolbar/nav-menu.imageset/menu.png new file mode 100644 index 0000000000..116d40a453 Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/TabToolbar/nav-menu.imageset/menu.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/TabToolbar/nav-menu.imageset/menu@2x.png b/mobile/ios/Client/Assets/Images.xcassets/TabToolbar/nav-menu.imageset/menu@2x.png new file mode 100644 index 0000000000..a25a4468bf Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/TabToolbar/nav-menu.imageset/menu@2x.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/TabToolbar/nav-menu.imageset/menu@3x.png b/mobile/ios/Client/Assets/Images.xcassets/TabToolbar/nav-menu.imageset/menu@3x.png new file mode 100644 index 0000000000..20e730495e Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/TabToolbar/nav-menu.imageset/menu@3x.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/TabToolbar/nav-refresh.imageset/Contents.json b/mobile/ios/Client/Assets/Images.xcassets/TabToolbar/nav-refresh.imageset/Contents.json new file mode 100644 index 0000000000..0a91290ed2 --- /dev/null +++ b/mobile/ios/Client/Assets/Images.xcassets/TabToolbar/nav-refresh.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "refresh.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "refresh@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "refresh@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/mobile/ios/Client/Assets/Images.xcassets/TabToolbar/nav-refresh.imageset/refresh.png b/mobile/ios/Client/Assets/Images.xcassets/TabToolbar/nav-refresh.imageset/refresh.png new file mode 100644 index 0000000000..eb35ce7c36 Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/TabToolbar/nav-refresh.imageset/refresh.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/TabToolbar/nav-refresh.imageset/refresh@2x.png b/mobile/ios/Client/Assets/Images.xcassets/TabToolbar/nav-refresh.imageset/refresh@2x.png new file mode 100644 index 0000000000..eff3d95d24 Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/TabToolbar/nav-refresh.imageset/refresh@2x.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/TabToolbar/nav-refresh.imageset/refresh@3x.png b/mobile/ios/Client/Assets/Images.xcassets/TabToolbar/nav-refresh.imageset/refresh@3x.png new file mode 100644 index 0000000000..f7b06550db Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/TabToolbar/nav-refresh.imageset/refresh@3x.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/TabToolbar/nav-share.imageset/Contents.json b/mobile/ios/Client/Assets/Images.xcassets/TabToolbar/nav-share.imageset/Contents.json new file mode 100644 index 0000000000..72c2f3ca8c --- /dev/null +++ b/mobile/ios/Client/Assets/Images.xcassets/TabToolbar/nav-share.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "share.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "share@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "share@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/mobile/ios/Client/Assets/Images.xcassets/TabToolbar/nav-share.imageset/share.png b/mobile/ios/Client/Assets/Images.xcassets/TabToolbar/nav-share.imageset/share.png new file mode 100644 index 0000000000..f6906f3268 Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/TabToolbar/nav-share.imageset/share.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/TabToolbar/nav-share.imageset/share@2x.png b/mobile/ios/Client/Assets/Images.xcassets/TabToolbar/nav-share.imageset/share@2x.png new file mode 100644 index 0000000000..36c42af0cd Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/TabToolbar/nav-share.imageset/share@2x.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/TabToolbar/nav-share.imageset/share@3x.png b/mobile/ios/Client/Assets/Images.xcassets/TabToolbar/nav-share.imageset/share@3x.png new file mode 100644 index 0000000000..48dd93260c Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/TabToolbar/nav-share.imageset/share@3x.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/TabToolbar/nav-stop.imageset/Contents.json b/mobile/ios/Client/Assets/Images.xcassets/TabToolbar/nav-stop.imageset/Contents.json new file mode 100644 index 0000000000..ca835ece3d --- /dev/null +++ b/mobile/ios/Client/Assets/Images.xcassets/TabToolbar/nav-stop.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "stop.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "stop@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "stop@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/mobile/ios/Client/Assets/Images.xcassets/TabToolbar/nav-stop.imageset/stop.png b/mobile/ios/Client/Assets/Images.xcassets/TabToolbar/nav-stop.imageset/stop.png new file mode 100644 index 0000000000..de43491c35 Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/TabToolbar/nav-stop.imageset/stop.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/TabToolbar/nav-stop.imageset/stop@2x.png b/mobile/ios/Client/Assets/Images.xcassets/TabToolbar/nav-stop.imageset/stop@2x.png new file mode 100644 index 0000000000..21c8128bd0 Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/TabToolbar/nav-stop.imageset/stop@2x.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/TabToolbar/nav-stop.imageset/stop@3x.png b/mobile/ios/Client/Assets/Images.xcassets/TabToolbar/nav-stop.imageset/stop@3x.png new file mode 100644 index 0000000000..3dfeb19a23 Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/TabToolbar/nav-stop.imageset/stop@3x.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/bookmark.imageset/Contents.json b/mobile/ios/Client/Assets/Images.xcassets/bookmark.imageset/Contents.json new file mode 100644 index 0000000000..190a7b48f1 --- /dev/null +++ b/mobile/ios/Client/Assets/Images.xcassets/bookmark.imageset/Contents.json @@ -0,0 +1,26 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "bookmark.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "bookmark@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "bookmark@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + }, + "properties" : { + "template-rendering-intent" : "template" + } +} \ No newline at end of file diff --git a/mobile/ios/Client/Assets/Images.xcassets/bookmark.imageset/bookmark.png b/mobile/ios/Client/Assets/Images.xcassets/bookmark.imageset/bookmark.png new file mode 100644 index 0000000000..968043a85c Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/bookmark.imageset/bookmark.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/bookmark.imageset/bookmark@2x.png b/mobile/ios/Client/Assets/Images.xcassets/bookmark.imageset/bookmark@2x.png new file mode 100644 index 0000000000..455992cae9 Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/bookmark.imageset/bookmark@2x.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/bookmark.imageset/bookmark@3x.png b/mobile/ios/Client/Assets/Images.xcassets/bookmark.imageset/bookmark@3x.png new file mode 100644 index 0000000000..b8192846fd Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/bookmark.imageset/bookmark@3x.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/bookmarkFolder.imageset/Contents.json b/mobile/ios/Client/Assets/Images.xcassets/bookmarkFolder.imageset/Contents.json new file mode 100644 index 0000000000..aad745b808 --- /dev/null +++ b/mobile/ios/Client/Assets/Images.xcassets/bookmarkFolder.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "icon-folder.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "icon-folder@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "icon-folder@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/mobile/ios/Client/Assets/Images.xcassets/bookmarkFolder.imageset/icon-folder.png b/mobile/ios/Client/Assets/Images.xcassets/bookmarkFolder.imageset/icon-folder.png new file mode 100644 index 0000000000..1fe7ac72e2 Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/bookmarkFolder.imageset/icon-folder.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/bookmarkFolder.imageset/icon-folder@2x.png b/mobile/ios/Client/Assets/Images.xcassets/bookmarkFolder.imageset/icon-folder@2x.png new file mode 100644 index 0000000000..f75db8002a Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/bookmarkFolder.imageset/icon-folder@2x.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/bookmarkFolder.imageset/icon-folder@3x.png b/mobile/ios/Client/Assets/Images.xcassets/bookmarkFolder.imageset/icon-folder@3x.png new file mode 100644 index 0000000000..77939b1f9b Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/bookmarkFolder.imageset/icon-folder@3x.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/bookmarkHighlighted.imageset/Contents.json b/mobile/ios/Client/Assets/Images.xcassets/bookmarkHighlighted.imageset/Contents.json new file mode 100644 index 0000000000..cced077bda --- /dev/null +++ b/mobile/ios/Client/Assets/Images.xcassets/bookmarkHighlighted.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "bookmarkHighlighted.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "bookmarkHighlighted@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "bookmarkHighlighted@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/mobile/ios/Client/Assets/Images.xcassets/bookmarkHighlighted.imageset/bookmarkHighlighted.png b/mobile/ios/Client/Assets/Images.xcassets/bookmarkHighlighted.imageset/bookmarkHighlighted.png new file mode 100644 index 0000000000..dea20c6d27 Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/bookmarkHighlighted.imageset/bookmarkHighlighted.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/bookmarkHighlighted.imageset/bookmarkHighlighted@2x.png b/mobile/ios/Client/Assets/Images.xcassets/bookmarkHighlighted.imageset/bookmarkHighlighted@2x.png new file mode 100644 index 0000000000..fd6dd4e1e5 Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/bookmarkHighlighted.imageset/bookmarkHighlighted@2x.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/bookmarkHighlighted.imageset/bookmarkHighlighted@3x.png b/mobile/ios/Client/Assets/Images.xcassets/bookmarkHighlighted.imageset/bookmarkHighlighted@3x.png new file mode 100644 index 0000000000..e2ac2acb52 Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/bookmarkHighlighted.imageset/bookmarkHighlighted@3x.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/bookmarked.imageset/Contents.json b/mobile/ios/Client/Assets/Images.xcassets/bookmarked.imageset/Contents.json new file mode 100644 index 0000000000..24892bd7c6 --- /dev/null +++ b/mobile/ios/Client/Assets/Images.xcassets/bookmarked.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "bookmarkPressed.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "bookmarkPressed@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "bookmarkPressed@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/mobile/ios/Client/Assets/Images.xcassets/bookmarked.imageset/bookmarkPressed.png b/mobile/ios/Client/Assets/Images.xcassets/bookmarked.imageset/bookmarkPressed.png new file mode 100644 index 0000000000..efbc0cccb2 Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/bookmarked.imageset/bookmarkPressed.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/bookmarked.imageset/bookmarkPressed@2x.png b/mobile/ios/Client/Assets/Images.xcassets/bookmarked.imageset/bookmarkPressed@2x.png new file mode 100644 index 0000000000..ff7202a0e0 Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/bookmarked.imageset/bookmarkPressed@2x.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/bookmarked.imageset/bookmarkPressed@3x.png b/mobile/ios/Client/Assets/Images.xcassets/bookmarked.imageset/bookmarkPressed@3x.png new file mode 100644 index 0000000000..e80eda99f7 Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/bookmarked.imageset/bookmarkPressed@3x.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/bookmarked_passive.imageset/Contents.json b/mobile/ios/Client/Assets/Images.xcassets/bookmarked_passive.imageset/Contents.json new file mode 100644 index 0000000000..968704a015 --- /dev/null +++ b/mobile/ios/Client/Assets/Images.xcassets/bookmarked_passive.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x", + "filename" : "bookmarked_passive.png" + }, + { + "idiom" : "universal", + "scale" : "2x", + "filename" : "bookmarked_passive@2x.png" + }, + { + "idiom" : "universal", + "scale" : "3x", + "filename" : "bookmarked_passive@3x.png" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/mobile/ios/Client/Assets/Images.xcassets/bookmarked_passive.imageset/bookmarked_passive.png b/mobile/ios/Client/Assets/Images.xcassets/bookmarked_passive.imageset/bookmarked_passive.png new file mode 100644 index 0000000000..a937a5233a Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/bookmarked_passive.imageset/bookmarked_passive.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/bookmarked_passive.imageset/bookmarked_passive@2x.png b/mobile/ios/Client/Assets/Images.xcassets/bookmarked_passive.imageset/bookmarked_passive@2x.png new file mode 100644 index 0000000000..358737078c Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/bookmarked_passive.imageset/bookmarked_passive@2x.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/bookmarked_passive.imageset/bookmarked_passive@3x.png b/mobile/ios/Client/Assets/Images.xcassets/bookmarked_passive.imageset/bookmarked_passive@3x.png new file mode 100644 index 0000000000..7a27b727ef Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/bookmarked_passive.imageset/bookmarked_passive@3x.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/context_pocket.imageset/Contents.json b/mobile/ios/Client/Assets/Images.xcassets/context_pocket.imageset/Contents.json new file mode 100644 index 0000000000..d421e25e59 --- /dev/null +++ b/mobile/ios/Client/Assets/Images.xcassets/context_pocket.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "context_pocket.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "context_pocket@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "context_pocket@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/mobile/ios/Client/Assets/Images.xcassets/context_pocket.imageset/context_pocket.png b/mobile/ios/Client/Assets/Images.xcassets/context_pocket.imageset/context_pocket.png new file mode 100644 index 0000000000..d2ee7afbd6 Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/context_pocket.imageset/context_pocket.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/context_pocket.imageset/context_pocket@2x.png b/mobile/ios/Client/Assets/Images.xcassets/context_pocket.imageset/context_pocket@2x.png new file mode 100644 index 0000000000..f5c8eec3ef Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/context_pocket.imageset/context_pocket@2x.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/context_pocket.imageset/context_pocket@3x.png b/mobile/ios/Client/Assets/Images.xcassets/context_pocket.imageset/context_pocket@3x.png new file mode 100644 index 0000000000..cefa86453a Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/context_pocket.imageset/context_pocket@3x.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/defaultFavicon.imageset/Contents.json b/mobile/ios/Client/Assets/Images.xcassets/defaultFavicon.imageset/Contents.json new file mode 100644 index 0000000000..46fab86fe2 --- /dev/null +++ b/mobile/ios/Client/Assets/Images.xcassets/defaultFavicon.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "newtab-icon.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "newtab-icon@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "newtab-icon@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/mobile/ios/Client/Assets/Images.xcassets/defaultFavicon.imageset/newtab-icon.png b/mobile/ios/Client/Assets/Images.xcassets/defaultFavicon.imageset/newtab-icon.png new file mode 100644 index 0000000000..e3ba4f3ff4 Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/defaultFavicon.imageset/newtab-icon.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/defaultFavicon.imageset/newtab-icon@2x.png b/mobile/ios/Client/Assets/Images.xcassets/defaultFavicon.imageset/newtab-icon@2x.png new file mode 100644 index 0000000000..32bffde9da Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/defaultFavicon.imageset/newtab-icon@2x.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/defaultFavicon.imageset/newtab-icon@3x.png b/mobile/ios/Client/Assets/Images.xcassets/defaultFavicon.imageset/newtab-icon@3x.png new file mode 100644 index 0000000000..2f7a93285f Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/defaultFavicon.imageset/newtab-icon@3x.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/deviceTypeDesktop.imageset/Contents.json b/mobile/ios/Client/Assets/Images.xcassets/deviceTypeDesktop.imageset/Contents.json new file mode 100644 index 0000000000..182ea4dc07 --- /dev/null +++ b/mobile/ios/Client/Assets/Images.xcassets/deviceTypeDesktop.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "deviceTypeDesktop.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "deviceTypeDesktop@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "deviceTypeDesktop@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/mobile/ios/Client/Assets/Images.xcassets/deviceTypeDesktop.imageset/deviceTypeDesktop.png b/mobile/ios/Client/Assets/Images.xcassets/deviceTypeDesktop.imageset/deviceTypeDesktop.png new file mode 100644 index 0000000000..0f09177a54 Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/deviceTypeDesktop.imageset/deviceTypeDesktop.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/deviceTypeDesktop.imageset/deviceTypeDesktop@2x.png b/mobile/ios/Client/Assets/Images.xcassets/deviceTypeDesktop.imageset/deviceTypeDesktop@2x.png new file mode 100644 index 0000000000..2fa6c821ef Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/deviceTypeDesktop.imageset/deviceTypeDesktop@2x.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/deviceTypeDesktop.imageset/deviceTypeDesktop@3x.png b/mobile/ios/Client/Assets/Images.xcassets/deviceTypeDesktop.imageset/deviceTypeDesktop@3x.png new file mode 100644 index 0000000000..feaa8547b4 Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/deviceTypeDesktop.imageset/deviceTypeDesktop@3x.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/deviceTypeMobile.imageset/Contents.json b/mobile/ios/Client/Assets/Images.xcassets/deviceTypeMobile.imageset/Contents.json new file mode 100644 index 0000000000..68bc633d8b --- /dev/null +++ b/mobile/ios/Client/Assets/Images.xcassets/deviceTypeMobile.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "deviceTypeMobile.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "deviceTypeMobile@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "deviceTypeMobile@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/mobile/ios/Client/Assets/Images.xcassets/deviceTypeMobile.imageset/deviceTypeMobile.png b/mobile/ios/Client/Assets/Images.xcassets/deviceTypeMobile.imageset/deviceTypeMobile.png new file mode 100644 index 0000000000..0228490a3d Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/deviceTypeMobile.imageset/deviceTypeMobile.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/deviceTypeMobile.imageset/deviceTypeMobile@2x.png b/mobile/ios/Client/Assets/Images.xcassets/deviceTypeMobile.imageset/deviceTypeMobile@2x.png new file mode 100644 index 0000000000..37d63b2109 Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/deviceTypeMobile.imageset/deviceTypeMobile@2x.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/deviceTypeMobile.imageset/deviceTypeMobile@3x.png b/mobile/ios/Client/Assets/Images.xcassets/deviceTypeMobile.imageset/deviceTypeMobile@3x.png new file mode 100644 index 0000000000..e910b3a4f1 Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/deviceTypeMobile.imageset/deviceTypeMobile@3x.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/faviconFox.imageset/Contents.json b/mobile/ios/Client/Assets/Images.xcassets/faviconFox.imageset/Contents.json new file mode 100644 index 0000000000..2ae9938182 --- /dev/null +++ b/mobile/ios/Client/Assets/Images.xcassets/faviconFox.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "back-panel-home.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "back-panel-home@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "back-panel-home@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/mobile/ios/Client/Assets/Images.xcassets/faviconFox.imageset/back-panel-home.png b/mobile/ios/Client/Assets/Images.xcassets/faviconFox.imageset/back-panel-home.png new file mode 100644 index 0000000000..a253d0ae9d Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/faviconFox.imageset/back-panel-home.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/faviconFox.imageset/back-panel-home@2x.png b/mobile/ios/Client/Assets/Images.xcassets/faviconFox.imageset/back-panel-home@2x.png new file mode 100644 index 0000000000..ce4766af7f Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/faviconFox.imageset/back-panel-home@2x.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/faviconFox.imageset/back-panel-home@3x.png b/mobile/ios/Client/Assets/Images.xcassets/faviconFox.imageset/back-panel-home@3x.png new file mode 100644 index 0000000000..22459edbca Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/faviconFox.imageset/back-panel-home@3x.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/fxLogo.imageset/Contents.json b/mobile/ios/Client/Assets/Images.xcassets/fxLogo.imageset/Contents.json new file mode 100644 index 0000000000..70eee9725c --- /dev/null +++ b/mobile/ios/Client/Assets/Images.xcassets/fxLogo.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x", + "filename" : "fxLogo.png" + }, + { + "idiom" : "universal", + "scale" : "2x", + "filename" : "fxLogo@2x.png" + }, + { + "idiom" : "universal", + "scale" : "3x", + "filename" : "fxLogo@3x.png" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/mobile/ios/Client/Assets/Images.xcassets/fxLogo.imageset/fxLogo.png b/mobile/ios/Client/Assets/Images.xcassets/fxLogo.imageset/fxLogo.png new file mode 100644 index 0000000000..e288c69fb2 Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/fxLogo.imageset/fxLogo.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/fxLogo.imageset/fxLogo@2x.png b/mobile/ios/Client/Assets/Images.xcassets/fxLogo.imageset/fxLogo@2x.png new file mode 100644 index 0000000000..bbf4cac125 Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/fxLogo.imageset/fxLogo@2x.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/fxLogo.imageset/fxLogo@3x.png b/mobile/ios/Client/Assets/Images.xcassets/fxLogo.imageset/fxLogo@3x.png new file mode 100644 index 0000000000..66ea6798a6 Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/fxLogo.imageset/fxLogo@3x.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/fxa_get_started.imageset/Contents.json b/mobile/ios/Client/Assets/Images.xcassets/fxa_get_started.imageset/Contents.json new file mode 100644 index 0000000000..6d765487cb --- /dev/null +++ b/mobile/ios/Client/Assets/Images.xcassets/fxa_get_started.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x", + "filename" : "fxa_get_started.png" + }, + { + "idiom" : "universal", + "scale" : "2x", + "filename" : "fxa_get_started@2x.png" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/mobile/ios/Client/Assets/Images.xcassets/fxa_get_started.imageset/fxa_get_started.png b/mobile/ios/Client/Assets/Images.xcassets/fxa_get_started.imageset/fxa_get_started.png new file mode 100644 index 0000000000..2af9c957a5 Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/fxa_get_started.imageset/fxa_get_started.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/fxa_get_started.imageset/fxa_get_started@2x.png b/mobile/ios/Client/Assets/Images.xcassets/fxa_get_started.imageset/fxa_get_started@2x.png new file mode 100644 index 0000000000..bbe82d9d72 Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/fxa_get_started.imageset/fxa_get_started@2x.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/goBack.imageset/Contents.json b/mobile/ios/Client/Assets/Images.xcassets/goBack.imageset/Contents.json new file mode 100644 index 0000000000..5c966d08ce --- /dev/null +++ b/mobile/ios/Client/Assets/Images.xcassets/goBack.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "goback.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "goback@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "goback@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/mobile/ios/Client/Assets/Images.xcassets/goBack.imageset/goback.png b/mobile/ios/Client/Assets/Images.xcassets/goBack.imageset/goback.png new file mode 100644 index 0000000000..ae4f92b6a0 Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/goBack.imageset/goback.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/goBack.imageset/goback@2x.png b/mobile/ios/Client/Assets/Images.xcassets/goBack.imageset/goback@2x.png new file mode 100644 index 0000000000..be1cd00498 Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/goBack.imageset/goback@2x.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/goBack.imageset/goback@3x.png b/mobile/ios/Client/Assets/Images.xcassets/goBack.imageset/goback@3x.png new file mode 100644 index 0000000000..4d9ec58409 Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/goBack.imageset/goback@3x.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/key.imageset/Contents.json b/mobile/ios/Client/Assets/Images.xcassets/key.imageset/Contents.json new file mode 100644 index 0000000000..562ad20c79 --- /dev/null +++ b/mobile/ios/Client/Assets/Images.xcassets/key.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "key.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "key@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "key@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/mobile/ios/Client/Assets/Images.xcassets/key.imageset/key.png b/mobile/ios/Client/Assets/Images.xcassets/key.imageset/key.png new file mode 100644 index 0000000000..34ad21b09b Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/key.imageset/key.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/key.imageset/key@2x.png b/mobile/ios/Client/Assets/Images.xcassets/key.imageset/key@2x.png new file mode 100644 index 0000000000..d1d1c3eb81 Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/key.imageset/key@2x.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/key.imageset/key@3x.png b/mobile/ios/Client/Assets/Images.xcassets/key.imageset/key@3x.png new file mode 100644 index 0000000000..45cc19f1e8 Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/key.imageset/key@3x.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/largePrivateMask.imageset/Contents.json b/mobile/ios/Client/Assets/Images.xcassets/largePrivateMask.imageset/Contents.json new file mode 100644 index 0000000000..94f6266bb2 --- /dev/null +++ b/mobile/ios/Client/Assets/Images.xcassets/largePrivateMask.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "largePrivateMask.pdf" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/mobile/ios/Client/Assets/Images.xcassets/largePrivateMask.imageset/largePrivateMask.pdf b/mobile/ios/Client/Assets/Images.xcassets/largePrivateMask.imageset/largePrivateMask.pdf new file mode 100644 index 0000000000..abfb68f1ed --- /dev/null +++ b/mobile/ios/Client/Assets/Images.xcassets/largePrivateMask.imageset/largePrivateMask.pdf @@ -0,0 +1,237 @@ +%PDF-1.5 % +10 0 obj <>/OCGs[14 0 R]>>/Pages 3 0 R/Type/Catalog>> endobj 13 0 obj <>stream + + + + + 2015-09-15T14:01:13Z + 2017-11-06T14:39:46-08:00 + 2017-11-06T14:39:46-08:00 + + + + 256 + 128 + JPEG + /9j/4AAQSkZJRgABAgEASABIAAD/7QAsUGhvdG9zaG9wIDMuMAA4QklNA+0AAAAAABAASAAAAAEA AQBIAAAAAQAB/+4ADkFkb2JlAGTAAAAAAf/bAIQABgQEBAUEBgUFBgkGBQYJCwgGBggLDAoKCwoK DBAMDAwMDAwQDA4PEA8ODBMTFBQTExwbGxscHx8fHx8fHx8fHwEHBwcNDA0YEBAYGhURFRofHx8f Hx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8f/8AAEQgAgAEAAwER AAIRAQMRAf/EAaIAAAAHAQEBAQEAAAAAAAAAAAQFAwIGAQAHCAkKCwEAAgIDAQEBAQEAAAAAAAAA AQACAwQFBgcICQoLEAACAQMDAgQCBgcDBAIGAnMBAgMRBAAFIRIxQVEGE2EicYEUMpGhBxWxQiPB UtHhMxZi8CRygvElQzRTkqKyY3PCNUQnk6OzNhdUZHTD0uIIJoMJChgZhJRFRqS0VtNVKBry4/PE 1OT0ZXWFlaW1xdXl9WZ2hpamtsbW5vY3R1dnd4eXp7fH1+f3OEhYaHiImKi4yNjo+Ck5SVlpeYmZ qbnJ2en5KjpKWmp6ipqqusra6voRAAICAQIDBQUEBQYECAMDbQEAAhEDBCESMUEFURNhIgZxgZEy obHwFMHR4SNCFVJicvEzJDRDghaSUyWiY7LCB3PSNeJEgxdUkwgJChgZJjZFGidkdFU38qOzwygp 0+PzhJSktMTU5PRldYWVpbXF1eX1RlZmdoaWprbG1ub2R1dnd4eXp7fH1+f3OEhYaHiImKi4yNjo +DlJWWl5iZmpucnZ6fkqOkpaanqKmqq6ytrq+v/aAAwDAQACEQMRAD8A9U4qlPmTzX5f8t2X1zWb xLWNqiNDVpJCOyItWb6Bt3xV455j/wCckbx2eLy9piRR1ot1eku5HiIkIVT82OKsD1P83PzF1An1 dbmhU1ottxt6A+BiCN+OKsfuvMOv3ZJu9Tu7gt1Ms8j13r+0x74qgXd3Yu7FmPViak/ScVW4qvjl liblE7IxFCVJBp9GKpnZ+bfNNmQbTWL2Cm4EdxKo+4NTtirItN/On8x7AgDVTdRj/ddzHHLX5sVD /wDDYqzTRv8AnJW9UqmtaPHKP2prNzGR/wA85Odf+DGKvRvL35veQtcKxw6itpcv0tr0eg1fDkSY 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 GgCoigBQBsAAOgxVdirsVdir/9k= + + + + Mac OS X 10.10.5 Quartz PDFContext + 1 + False + False + + 1.388889 + 0.791667 + Inches + + + + Cyan + Magenta + Yellow + Black + + + + + + Default Swatch Group + 0 + + + + Black + PROCESS + 100.000000 + RGB + 0 + 0 + 0 + + + + + + + application/pdf + proof:pdf + uuid:b9883ec5-5836-314e-b8d2-97d21cff564b + uuid:88a3fba3-8faa-d24d-9c47-69a3287e942f + + + + + + + + + + + + + + + + + + + + + + + + + endstream endobj 3 0 obj <> endobj 2 0 obj <>/Resources<>/ExtGState<>/Properties<>>>/Thumb 20 0 R/TrimBox[0.0 0.0 100.0 57.0]/Type/Page>> endobj 16 0 obj <>stream +HTSKn1 hQ(i[*,rAU +$?f$''z/W<\ }~ +NR +e MsF/t;P[z#GWYZt(,c>MtB)%q@,Dًwr-R,m7v: %"ЙqC_9:nT)M{K +;V!N FF5lGiD7z x FvjHw\ ]|ӹXi؟}p{9Qavl(e AQd>stream +8;RZ]$e#Rn]+2bfg=fs@(^:b,?t.'XK=KO:?O+[Q";h+MFW''~> endstream endobj 21 0 obj [/Indexed/DeviceRGB 255 22 0 R] endobj 22 0 obj <>stream +8;X]O>EqN@%''O_@%e@?J;%+8(9e>X=MR6S?i^YgA3=].HDXF.R$lIL@"pJ+EP(%0 +b]6ajmNZn*!='OQZeQ^Y*,=]?C.B+\Ulg9dhD*"iC[;*=3`oP1[!S^)?1)IZ4dup` +E1r!/,*0[*9.aFIR2&b-C#soRZ7Dl%MLY\.?d>Mn +6%Q2oYfNRF$$+ON<+]RUJmC0InDZ4OTs0S!saG>GGKUlQ*Q?45:CI&4J'_2j$XKrcYp0n+Xl_nU*O( +l[$6Nn+Z_Nq0]s7hs]`XX1nZ8&94a\~> endstream endobj 14 0 obj <> endobj 23 0 obj [/View/Design] endobj 24 0 obj <>>> endobj 19 0 obj <> endobj 18 0 obj [/ICCBased 25 0 R] endobj 25 0 obj <>stream +HyTSwoɞc [5laQIBHADED2mtFOE.c}08׎8GNg9w߽'0 ֠Jb  + 2y.-;!KZ ^i"L0- @8(r;q7Ly&Qq4j|9 +V)gB0iW8#8wթ8_٥ʨQQj@&A)/g>'Kt;\ ӥ$պFZUn(4T%)뫔0C&Zi8bxEB;Pӓ̹A om?W= +x-[0}y)7ta>jT7@tܛ`q2ʀ&6ZLĄ?_yxg)˔zçLU*uSkSeO4?׸c. R ߁-25 S>ӣVd`rn~Y&+`;A4 A9=-tl`;~p Gp| [`L`< "A YA+Cb(R,*T2B- +ꇆnQt}MA0alSx k&^>0|>_',G!"F$H:R!zFQd?r 9\A&G rQ hE]a4zBgE#H *B=0HIpp0MxJ$D1D, VĭKĻYdE"EI2EBGt4MzNr!YK ?%_&#(0J:EAiQ(()ӔWT6U@P+!~mD eԴ!hӦh/']B/ҏӿ?a0nhF!X8܌kc&5S6lIa2cKMA!E#ƒdV(kel }}Cq9 +N')].uJr + wG xR^[oƜchg`>b$*~ :Eb~,m,-ݖ,Y¬*6X[ݱF=3뭷Y~dó ti zf6~`{v.Ng#{}}jc1X6fm;'_9 r:8q:˜O:ϸ8uJqnv=MmR 4 +n3ܣkGݯz=[==<=GTB(/S,]6*-W:#7*e^YDY}UjAyT`#D="b{ų+ʯ:!kJ4Gmt}uC%K7YVfFY .=b?SƕƩȺy چ k5%4m7lqlioZlG+Zz͹mzy]?uuw|"űNwW&e֥ﺱ*|j5kyݭǯg^ykEklD_p߶7Dmo꿻1ml{Mś nLl<9O[$h՛BdҞ@iءG&vVǥ8nRĩ7u\ЭD-u`ֲK³8%yhYѹJº;.! +zpg_XQKFAǿ=ȼ:ɹ8ʷ6˶5̵5͵6ζ7ϸ9к<Ѿ?DINU\dlvۀ܊ݖޢ)߯6DScs 2F[p(@Xr4Pm8Ww)Km endstream endobj 17 0 obj <> endobj 26 0 obj <> endobj 27 0 obj <>stream +%!PS-Adobe-3.0 %%Creator: Adobe Illustrator(R) 17.0 %%AI8_CreatorVersion: 22.0.0 %%For: (Bryan Bell) () %%Title: (largePrivateMask.pdf) %%CreationDate: 11/6/17 2:39 PM %%Canvassize: 16383 %%BoundingBox: 0 3 100 54 %%HiResBoundingBox: 0.12500000000091 3.93747167911351 99.8749999999982 53.8125319459614 %%DocumentProcessColors: Cyan Magenta Yellow Black %AI5_FileFormat 13.0 %AI12_BuildNumber: 244 %AI3_ColorUsage: Color %AI7_ImageSettings: 0 %%RGBProcessColor: 0 0 0 (Black) %%+ 0 0 0 ([Registration]) %AI3_Cropmarks: 0 0 100 57 %AI3_TemplateBox: 49.5 28.5 49.5 28.5 %AI3_TileBox: -328 -259.5 406 316.5 %AI3_DocumentPreview: None %AI5_ArtSize: 14400 14400 %AI5_RulerUnits: 0 %AI9_ColorModel: 1 %AI5_ArtFlags: 0 0 0 1 0 0 1 0 0 %AI5_TargetResolution: 800 %AI5_NumLayers: 1 %AI17_Begin_Content_if_version_gt:17 1 %AI9_OpenToView: -29.25 96.75 8 1908 1031 26 0 0 5 44 0 0 0 1 1 0 1 1 0 1 %AI17_Alternate_Content %AI9_OpenToView: -29.25 96.75 8 1908 1031 26 0 0 5 44 0 0 0 1 1 0 1 1 0 1 %AI17_End_Versioned_Content %AI5_OpenViewLayers: 7 %%PageOrigin:-670 -692 %AI7_GridSettings: 1 1 1 1 0 0 0.639215707778931 0.639215707778931 0.639215707778931 0.819607853889465 0.819607853889465 0.819607853889465 %AI9_Flatten: 1 %AI12_CMSettings: 00.MS %%EndComments endstream endobj 28 0 obj <>stream +%%BoundingBox: 0 3 100 54 %%HiResBoundingBox: 0.12500000000091 3.93747167911351 99.8749999999982 53.8125319459614 %AI7_Thumbnail: 128 64 8 %%BeginData: 14612 Hex Bytes %0000330000660000990000CC0033000033330033660033990033CC0033FF %0066000066330066660066990066CC0066FF009900009933009966009999 %0099CC0099FF00CC0000CC3300CC6600CC9900CCCC00CCFF00FF3300FF66 %00FF9900FFCC3300003300333300663300993300CC3300FF333300333333 %3333663333993333CC3333FF3366003366333366663366993366CC3366FF %3399003399333399663399993399CC3399FF33CC0033CC3333CC6633CC99 %33CCCC33CCFF33FF0033FF3333FF6633FF9933FFCC33FFFF660000660033 %6600666600996600CC6600FF6633006633336633666633996633CC6633FF %6666006666336666666666996666CC6666FF669900669933669966669999 %6699CC6699FF66CC0066CC3366CC6666CC9966CCCC66CCFF66FF0066FF33 %66FF6666FF9966FFCC66FFFF9900009900339900669900999900CC9900FF %9933009933339933669933999933CC9933FF996600996633996666996699 %9966CC9966FF9999009999339999669999999999CC9999FF99CC0099CC33 %99CC6699CC9999CCCC99CCFF99FF0099FF3399FF6699FF9999FFCC99FFFF %CC0000CC0033CC0066CC0099CC00CCCC00FFCC3300CC3333CC3366CC3399 %CC33CCCC33FFCC6600CC6633CC6666CC6699CC66CCCC66FFCC9900CC9933 %CC9966CC9999CC99CCCC99FFCCCC00CCCC33CCCC66CCCC99CCCCCCCCCCFF %CCFF00CCFF33CCFF66CCFF99CCFFCCCCFFFFFF0033FF0066FF0099FF00CC %FF3300FF3333FF3366FF3399FF33CCFF33FFFF6600FF6633FF6666FF6699 %FF66CCFF66FFFF9900FF9933FF9966FF9999FF99CCFF99FFFFCC00FFCC33 %FFCC66FFCC99FFCCCCFFCCFFFFFF33FFFF66FFFF99FFFFCC110000001100 %000011111111220000002200000022222222440000004400000044444444 %550000005500000055555555770000007700000077777777880000008800 %000088888888AA000000AA000000AAAAAAAABB000000BB000000BBBBBBBB %DD000000DD000000DDDDDDDDEE000000EE000000EEEEEEEE0000000000FF %00FF0000FFFFFF0000FF00FFFFFF00FFFFFF %524C45FD0EFFA87DA87D7DFD04522752525227FD0452FD047DA8A8FD35FF %A8A87D7D527D52522752525227FD04527D527D7DA8A8FD18FFA87D525227 %2727522752275227522752275227522752275227272752527D7DA8FD2CFF %A87D52522752275227522752275227522752275227522752272727522752 %7DA8FD12FF7D525227522752525227525252275252522752525227525252 %275252522752275227527DA8A8FD25FFA87D525227522752275227525252 %275252522752525227525252275252522752275227527DFD0EFF7D522752 %275227522752275227522752275227522752275227522752275227522752 %27522752275252A8A8FD1FFF7D7D27272752275227522752275227522752 %27522752275227522752275227522752275227522727277DFD0BFF525227 %522752525227525252275252522752525227525252275252522752525227 %52525227525252275227527DFD1BFFA87D52522752525227525252275252 %522752525227525252275252522752525227525252275252522752525227 %7DFD09FF5227275227522752275227522752275227522752275227522752 %2752275227522752275227522752275227522727277D7DFD15FFA8A85252 %275227522752275227522752275227522752275227522752275227522752 %27522752275227522752275227522752FD07FFFD04522752525227525252 %275252522752525227525252275252522752525227525252275252522752 %5252275252522752527DA8FD11FF7D522752275227525252275252522752 %525227525252275252522752525227525252275252522752525227525252 %27525252277DFD05FF7D2727522752275227522752275227522752275227 %522752275227522752275227522752275227522752275227522752275227 %52275252A8A8FD09FFA8FF7D522727275227522752275227522752275227 %522752275227522752275227522752275227522752275227522752275227 %522752275227A8FD04FF5227525252275252522752525227525252275252 %522752525227525252275252522752525227525252275252522752525227 %52525227522752527D7DFD05A87D7D525227525252275252522752525227 %525252275252522752525227525252275252522752525227525252275252 %522752525227525252275252FFFFFFA82752275227522752275227522752 %275227522752275227522752275227522752275227522752275227522752 %275227522752275227522752275227522727275227272752275227522752 %275227522752275227522752275227522752275227522752275227522752 %27522752275227522752275227522752275227527DFFFFFD045227525252 %275252522752525227525252275252522752525227525252275252522752 %525227525252275252522752525227525252275252522752525227525252 %275252522752525227525252275252522752525227525252275252522752 %5252275252522752525227525252275252522752525227525252277DFFA8 %522752275227522752275227522752275227522752275227522752275227 %522752275227522752275227522752275227522752275227522752275227 %522752275227522752275227522752275227522752275227522752275227 %522752275227522752275227522752275227522752275227522752275227 %522752275227FFA827525252275252522752525227525252275252522752 %525227525252275252522752525227525252275252522752525227525252 %275252522752525227525252275252522752525227525252275252522752 %525227525252275252522752525227525252275252522752525227525252 %2752525227525252275252522752A87D2727522752275227522752275227 %522752275227522752275227522752275227522752275227522752275227 %522752275227522752275227522752275227522752275227522752275227 %522752275227522752275227522752275227522752275227522752275227 %52275227522752275227522752275227522752275227A87D275227525252 %275252522752525227525252275252522752525227525252275252522752 %525227525252275252522752525227525252275252522752525227525252 %275252522752525227525252275252522752525227525252275252522752 %525227525252275252522752525227525252275252522752525227525252 %7D5252275227522752275227522752275227522752275227522752275227 %522752275227522752275227522752275227522752275227522752275227 %522752275227522752275227522752275227522752275227522752275227 %522752275227522752275227522752275227522752275227522752275227 %522752275227522752522752275252522752525227525252275252522752 %525227525252275252522752525227525252275252522752525227525252 %275252522752525227525252275252522752525227525252275252522752 %525227525252275252522752525227525252275252522752525227525252 %27525252275252522752525227FD04522752275227522752275227522752 %275227522752275227522752275227522752275227522752275227522752 %275227522752275227522752275227522752275227522752275227522752 %275227522752275227522752275227522752275227522752275227522752 %275227522752275227522752275227522752275227522752522752525227 %525252275252522752525227525252275252522752525227525252275252 %522752525227525252275252522752525227525252275252522752525227 %525252275252522752525227525252275252522752525227525252275252 %522752525227525252275252522752525227525252275252522752525227 %525227522752275227522752275227522752275227522752275227522752 %27522752FD0B275227522752275227522752275227522752275227522752 %2752275227522752275227522752275227522752FD092752275227522752 %275227522752275227522752275227522752275227522752275252275252 %522752525227525252275252522752525227525252275252522752527D52 %A87DA8A8A87D7D5252275227525252275252522752525227525252275252 %522752525227525252275252522752525227522752527D7DA87DA87DA87D %7D5252275227522752525227525252275252522752525227525252275252 %522752522752275227522752275227522752275227522752275227522752 %2752527D7DFD0BFFA8A87D52275227522752275227522752275227522752 %275227522752275227522752275227522727277DA8FD0BFFA8A87D522727 %27522752275227522752275227522752275227522752275227FD04522752 %5252275252522752525227525252275252522752277DA8FD11FFA87D2752 %525227525252275252522752525227525252275252522752525227525252 %27527DFD12FFA87D52275252522752525227525252275252522752525227 %5252522727522752275227522752275227522752275227522752272752A8 %FD15FFA85227275227522752275227522752275227522752275227522752 %275227522752A8FD15FFA852275227522752275227522752275227522752 %2752275227FD045227525252275252522752525227525252275227527DFD %19FF52522752525227525252275252522752525227525252275252522752 %277DFD19FFA8525227525252275252522752525227525252275252522727 %522752275227522752275227522752275227522752A8FD1BFF5227275227 %522752275227522752275227522752275227522752277DFD1CFF52272752 %275227522752275227522752275227522752522752525227525252275252 %522752525227522752FD1EFF525227525252275252522752525227525252 %2752525227522752FD1EFF52522752525227525252275252522752525227 %FD0452275227522752275227522752275227522752A8FD1EFFA827522752 %2752275227522752275227522752275227522752A8FD1FFF522727522752 %27522752275227522752275227527D275252522752525227525252275252 %522752A8FD20FF7D27525252275252522752525227525252275252522752 %7DFD20FFA8275252522752525227525252275252522752527D2727522752 %27522752275227522752272752FD21FFA852275227522752275227522752 %275227522752275227FD22FF7D2752275227522752275227522752275227 %7DA82752275252522752525227525252275227A8FD22FF52522752525227 %52525227525252275252522752277DFD22FFA85227525252275252522752 %5252275252527D7D272752275227522752275227522752275252FD22FF7D %27522752275227522752275227522752275227277DFD21FFA85227522752 %27522752275227522752275227A8A8275227525252275252522752525227 %FD0452FD21FF7D52275252522752525227525252275252522752277DFD20 %FFA87D275227525252275252522752525227525252A8A852275227522752 %27522752275227522752272727A8FD1EFF7D522752275227522752275227 %522752275227522752277DFD1EFF7D522752275227522752275227522752 %2752275227FFFFFD045227525252275252522752525227525252277DA8FD %1AFFA8525227525252275252522752525227525252275252522752277DA8 %FD1AFFA852522752275252522752525227525252275252522752FFFF5227 %52275227522752275227522752275227522752275252A8A8FD15FF7D5227 %52275227522752275227522752275227522752275227522752272752A8A8 %FD15FF7D7D2727275227522752275227522752275227522752272752FFFF %7D525252275252522752525227525252275252522752525227527DA8FD0E %FFA8A87D7D52522752525227525252275252522752525227525252275252 %52275252522752275252A8A8FD0FFFA87D52522752275252522752525227 %525252275252522752525227A8FFFFA82752275227522752275227522752 %2752275227522752275227522752527D7DFD05A87DA87D7D525227272752 %275227522752275227522752275227522752275227522752275227522752 %2752275227522752527D7DA87DA8A8A87DA87D5227522752275227522752 %2752275227522752275227522752275227277DFFFFA85227525252275252 %522752525227525252275252522752525227522752275227522752275227 %522752275252522752525227525252275252522752525227525252275252 %522752525227525252275252522752275227522752275227522752275227 %5252522752525227525252275252522752525227525252275252FD04FF52 %272752275227522752275227522752275227522752275227522752275227 %522752275227522752275227522752275227522752275227522752275227 %522752275227522752275227522752275227522752275227522752275227 %522752275227522752275227522752275227522752275227522752275227 %522752FD04FF7D2752525227525252275252522752525227525252275252 %522752525227525252275252522752525227525252275252522752525227 %525252275252522752525227525252275252522752525227525252275252 %522752525227525252275252522752525227525252275252522752525227 %5252522752525227527DFD04FFA852275227522752275227522752275227 %522752275227522752275227522752275227522752275227522752275227 %522752275227522752275227522752275227522752275227522752275227 %522752275227522752275227522752275227522752275227522752275227 %52275227522752275227522752275227A8FD05FF52522752525227525252 %275252522752525227525252275252522752525227525252275252522752 %525227525252275252522752525227525252275252522752525227525252 %275252522752525227525252275252522752525227525252275252522752 %52522752525227525252275252522752525227522752FD06FF7D27522752 %275227522752275227522752275227522752275227522752275227522752 %275227522752275227522752275227522752275227522752275227522752 %275227522752275227522752275227522752275227522752275227522752 %27522752275227522752275227522752275227522752275227277DFD06FF %A85227525252275252522752525227525252275252522752525227525252 %275252522752525227525252275252522752525227525252275252522752 %525227525252275252522752525227525252275252522752525227525252 %275252522752525227525252275252522752525227525252275252522752 %27FD08FF5227275227522752275227522752275227522752275227522752 %275227522752275227522752275227522752275227522752275227522752 %275227522752275227522752275227522752275227522752275227522752 %275227522752275227522752275227522752275227522752275227522752 %2752277DFD08FFA827522752525227525252275252522752525227525252 %275252522752525227525252275252522752525227525252275252522752 %525227522752275227522752525227525252275252522752525227525252 %275252522752525227525252275252522752525227525252275252522752 %5252275252527DFD08FFA852275227522752275227522752275227522752 %275227522752275227522752275227522752275227522752275227522752 %275227522727275227522752272727522752275227522752275227522752 %275227522752275227522752275227522752275227522752275227522752 %275227522752275252FD0AFF7D5227525252275252522752525227525252 %275252522752525227525252275252522752525227525252275252522752 %52522752275252A8A8FFA8FFA8A87D7D5252275252522752525227525252 %275252522752525227525252275252522752525227525252275252522752 %525227525252275227A8FD0BFF5252275227522752275227522752275227 %522752275227522752275227522752275227522752275227522752275227 %52272752A8A8FD0BFF7D5227522752275227522752275227522752275227 %522752275227522752275227522752275227522752275227522752275227 %52FD0CFFA827522752525227525252275252522752525227525252275252 %522752525227525252275252522752525227525252277DA8FD10FFA85252 %275227525252275252522752525227525252275252522752525227525252 %27525252275252522752525227525252A8FD0DFF7D275227522752275227 %522752275227522752275227522752275227522752275227522752275227 %522752272752A8FD14FF7D52275227522752275227522752275227522752 %275227522752275227522752275227522752275227522752272752FD0FFF %522752525227525252275252522752525227525252275252522752525227 %525252275252522752525227527DFD18FF7D275252522752525227525252 %275252522752525227525252275252522752525227525252275252522752 %52FD10FF7D27275227522752275227522752275227522752275227522752 %27522752275227522752275227522752A8FD1AFFA8275227522752275227 %522752275227522752275227522752275227522752275227522752275227 %5227A8FD11FF7D5227525252275252522752525227525252275252522752 %525227525252275252522752525227A8FD1EFF5252275227525252275252 %522752525227525252275252522752525227525252275252522752277DFD %13FF52272752275227522752275227522752275227522752275227522752 %275227522752272752A8FD20FF7D52275227522752275227522752275227 %52275227522752275227522752275227522752277DFD15FFFD0452275252 %5227525252275252522752525227525252275252522752525227527DFD23 %FFA852275252522752525227525252275252522752525227525252275252 %5227525252277DFD17FF5227275227522752275227522752275227522752 %2752275227522752275227527DFD26FF7D27522752275227522752275227 %5227522752275227522752275227522752277DFD19FF7D52275227525252 %2752525227525252275252522752525227525252277DFD29FFA852522752 %275252522752525227525252275252522752525227525252277DFD1BFF7D %5227522752275227522752275227522752275227522752272727A8FD2CFF %525227522752275227522752275227522752275227522752272727A8FD1D %FFA87D275252522752525227525252275252522752525227527DFD2FFFA8 %7D2752525227525252275252522752525227525252275252FD21FFA85252 %27522752275227522752275227522752277DA8FD32FFA852522752275227 %52275227522752275227522752A8FD24FFA87D5252275227522752525227 %5227527DFD37FFA87D52522752525227525252275227527DA8FD29FFA87D %525227522752275252A8A8FD3BFF7D7D525227522752527D7DA8A8FD14FF %FF %%EndData endstream endobj 29 0 obj <>stream +%AI12_CompressedDatax}z}ށwf%!@ tBP3?γ_ӇiswvŒlY%YXNMv:9CNӣz}:IV뉰RȀn؞n|_í>=]z> q0ToK@;PG1_\N8#bO0#Q5mO"D4#A$ ZPS~`7TxT`$`J 8`ws??)P<Xȧ-{Eo"^i_v:%1'|T}m.vEp`pTOD_:;z1]M }s[s`5mu >,0 Ę@).7Rx)82z9'r,F=i#ZOʁ"zy,Tئ,r {Nh)GfT}c1ObgZ6XA%[yÊRou{==/ݟ[EzĮ7SKM!42+\'zԎ4/ĥ#s#K/cp9T_S򿗑z/59P'ols/[@#³tcr !>(xGN2^Rh)x'PGOn]LL6;kt<,'3f6$iR*mtq=>[8le@z]0Jd:LsLM G& h~J "p(0t@4~g?4Г`A +hAQ.fB8@dfHH݉^b^v :odXB!WBMBѰQdBT4 P  '8x +h0O @ `vWM|Ir_M&Bo;p7}V zV) +郪Jem67׊;sTza|O-%*vt5vF}Wo?cHP8k-:|l (Vr Aq./6Eÿ%!>}t8ې{r4R@6QB7 +|nHNXrLMJAaS@ yhusWP.v%+zKPp$4vt 훸#\_v4+$Oڬبrt:N/|^bS5|4k0o{p60cH +RZFv?]Q`hSP(GF. -mYp$yӣbmGavG2#cN7 $9~vt6pԥ[ ]dy(''U"=BXz:kJky=AYߩRT_p㐇q:qJIusi#_G8> (Nr~8s`$,7Ȃᢆa859[p(@PEJ!3]3@a2p̮83vf #O褛ѩ֜@jL,Z0)Y-;p7hBJoToK_Jz͈ +:n=^7ԃ=hx`Z%rG\G &)9nE^=;Z +zʶG~#<GP0A8Q5$*_C,VoӕD ל#db^^v pL;㪽)s{]4H@nx(jmkP~N`s<#Hoe| + KgP'؄fa@cXiji;_G4y a>Fe*f z8Vj4vC^FnϧE`iN*Ԝ^#ɭ={ ^^h1q8:`NKY ;Q %T m)bԏPb I?BGBL>Wk\:.F *åZ|!dТߌ=&DD=̷;n-0nAb H /w'T*>=OPPCD&m|ŊGnD61U؝syv#1\ B[FfN!rRa܏WvM;v S`ϙ ~LgAkN Y</(gۣgޓvJj&jhIF# pfl3%ThIf'ZqNC. +\vᢓѐTZo[l-㽓։@bTN7DB4_"꯽GK9B`R,z#䜟j9Б %PC7P`qqc-ػ~ro! fS4Z +D1 `+4i;[`9Tv^|2Җ"je:'l-OãLGXѩ>?|* x) Itew;J:,0ʊhM 6k ~u6aDibDh,QгPpýgfB"7jCO뮥s>vmάEsB$C̈Δ[%ŗ5}6QD&Cb&j _XX$Yc|Z aNK$Sq%KY,a`9 i$V4?Ruo^ܧVӎ,6J-FZxO?s`n]ب]o`ބ&2 ʂM;bGhJÓOW b--3 ?Q̆Oy Ӑ Ik, C%`(KkCX5T'~Zv`4agϰlo"%hmi2ZO }9ݍPH{cw#wcj,6Y=kocmVc?7!iGg6n뙅`IMdue&Waa/? ژ_Se2Yj*SnS ãLv4?L´>LccGl龖ΓL14?Z4>k h77žg8v_կy 5~6X0`&oe†%V#>,eʦ3[ +]m-MVY2`Om㱜r踭k k-iMR1>OҶ~,sZ7z\%&1gBP9rxVR6,6yޗm4/5=۽g.^5jb>g:b_&w :_c0:BX9~f 1Z?eq >vEvgh9ʮJt 8zXgvƴ˗/ܻW_?ڌVa\So٩W}g YFkOrX֌x{7d+j ( +څ}Ĉm)%v%uaoh۳?3N?QSSGNיcx|[=!uitCйrN,d2`[9u9!#P>m 4Z֡hv|B}Yn'|9‡ [*NXG^2@=,_?Qc5/hGr{s,{oXހT}l|qzd +x6k=ě~9><[!ymC2|-ƥa;{~t?2 :\m9O +/d1;'^_f?_-Onwfx%O}4̻O~Mzo2[ KN6} +R2μ3KXa6=NczO:sgRr"83Q5uOlz>W=lHeߕFkI,sÞk}bұ孝C>]DU+?,f,ݑB +q봓E<)fk]ōc=//Rn)>ƙO@>O|n4E{xU?/_`D\˶+?|iܰEb'v%Lg*WIDZWgxxMruz*x5[YkX+=}j NoV:m^ym=7SKQG} fЬl8㶱xɷٌG]Y$*%ӱZ!g\7TtN[;_U=)[^ydž{lQ~zobM^ fNj1%>c3c^~>Xt~56!W{}-Îj% =P:}g8JS'kT{ڨ=iFcTG^my8Ki8 ?1NN64tgxVtO,}Ҽڰ [߶Eͻi/+\>[+ܾK?Og?/=dΦ«~~ȽEֽ1&l|nsVs;;Ws=bxq_k ;'3FNih 1gJ&=69p y{㝰RV>'@?`'MqpY<Dž`m{/ 9gG]+0T3tȍ"i;=^g|Z//OEawTICj̲]: +c% n$G<WwÝ}SeѼ։h`.rXPG1i{OYDC$mlў tBUn<#'gY8J7P7S+H2~x'Z@EЂDڟHC/&~0"B{5m7҃+CZLmTg>kuՊ$R`FdKR +)P6Ѵ%!=XASik" {O4RQ!?O2Dj?S[&i`!XY- kVi0b۔Ay慏B]CZ4mOiX`\L" -cO6e):A2Oaiy,Rc57Qs5&ϲH- B +hx.Mn^y 4/ka৑6Q\c}OR:FZ\駀RMEHm!«jCّH?4Z.^2H?x԰ =GC +3m?.e6ޅ&@Ak۰'ɱvm,N7!]/) ki6I"t[[e~>NJZu&%~6Yޗ!_@`$ R٢NCa ~2Dl4U!B +TAxC">kV45glwAJ M#ͽq "QhMjسP*\+zDHӷNm@?Buw(Oċ[|nRS\#/sCHϟn>c:$s2jm#z[zKbn-k z-[<0q1ogr9Y`%L}&@{,3ᕀ#i#юF\Z*<|̦V蝭v+QS4vk"C2wb' + ;)`7}FK머";ẽ/)TgjRc{K45fGH}?0 \ xH'Cz-H c-B +q}XH G)dr}X҂)$Z"HY j$C9uyח%Hp\?#Úo >s 5֋3;R!"зjڻ'Ri,{I#Ibßf3'Mp 4MRMFw1͚'qH>d-S>ș?NKާ:?(vHZ k~Uy$^D$)':3f9\j|~S6>NΠ͠aC>F j?|3mK,G,%`U"f$\P{tPFtי/,\2EB>ɉE;z !{.R@@B3y,RvNKWdbi'yR|SEZlˬJʛwh==*@o! +Z/z%=u< s: e ,YtfQڥ6i+^fCH{J`U74h1E9˺f;f.1CKAt C>Ogܑ?|`I|zly5eeXb|<YAHc5-LG'/ږ_țU䨼-Ȟ%/.SbJsb9|.GKT:K:fjTS4_hA%b̠"@ ϑi`+II1ݓb[(Fo4h/0\E|bn֎}'7(9&oJHY[p~ULjH+KvRYE6eE/\@,_P_ +Jˬx#/6Hyn7ibpTJ21ҙ9 ]> 6+@ v~9}L0! -ey:B]V:tYxTEE ^ESd] eGvJybH[L7ʕnRde_<^LǼ6 +?SBp)Pz9}dd |SR1n .ڻKF٬7 +|zA6ʅӘ+ *IӛWO]àN;F/gV4&5IgdϜV׏O'aFm|i.)g>Zv}s@')sqJ6YMG$Plύ6f$Tvy(Y$$Lvs!1Qd m^{ίEgb!Xs%bW5伔{EI +)#߯h ee= Y6MTQr)N\xbFՕ?Ӛ]$"<0ȸ6K<0QM^Z5*s-7Jל\(`j?<> ݂ o"CkUsf.W~7)^N1:/z4(Ib[)Tn{eK?*;_GSѢvcOZ+JZ (Ӟ9#r38( meS־$1DYirq} NNsRDd\|B=R󧤖+43e9t{G[zdƐ\͈憐fV >Vн#fJuAN٣$=WK=2Jl##+5yhSetqZetzd+Y-OGo$#v.R<_9+r a9h#CYih*g:ih[=2:3$LH'9Y[yv>42W2O¼a_D. ߸FM '˥i SN]ܒW%XWwa}Geafת:A!uW^J$NRlt،{Éc.ʇSNppߺFޖ3n*[|8Qo⌸{É|Wkxp[x{]^kqup秾b[o%G!/=&!&R'"lLdKSLonNBȩ1ROx)8\e"V> Q!:eb` +oh2.^ޞMQ2<{eؾ(4-B!GmoZ"(j#8/3땂s2DPliw 8s  Vt% +L*\.{T qqF*aДD-#0n6]I7evڹKFjN;ev_@ #DPt 4"rr^8]^DNjkjY*ȫ^?N{x ʒT|nnk6YXMvC}ȷgq]fEUtR~*tRtR=eI)܄dIIޫpS6h$̽餺f)hJw̦RNtR^al:)PeIRȯ2NbwΦTCe#NEr$jޘM'K'}g-tµMҩW^M'@>>l:E^?^tR*ڿ:N@ T-4V!qtS+#,q *MbPo˩.vv!A20W'%s* +:eNTF&T+41XhSEy4/g. 4[y1+ ;U|^,2LR?㗹:2w4'˩ϗ#JBw7կTKNp&rXeΥZ5eDEBwg̝"'k +R{^Ĺs4SWV +[jP_oR[1%TQk<$u +MuT>w -NPKk"i5s-]UZ5|i$ R`<.k_qkxej jݐtDe7W2k](_^)|"Jy/Cq[*ipGPᎫ*]Z.z .%5{Ga9{G!k?2kBpnf!(`o7N:pK>%T.vA4LP<28143RlZ3n ^<ؽ68b{ٕ`.ZT$&C51i쒘#^+r\ݞ[fHڤ-utZQˎܻ#_\)E=c66JKc<ī)D&(/ywbUmIWk+W>f5Sz.h>ݲrR  p$Oyrws~c0?Mݬrue j9_LjIE]z٬Yx[P\0CCWRl伔3X6` 0N>f-U^eǮJz 8UDN雷dG)}_3E{* X]TXDKǧgٸqum\%'.(WQӕgVwv0FcԪȂ#vs0(VO"M%dv(+JC"zwUkɗUԓ-5gjTbu$sE5glg9Y6Yl/sEYZs52o9toظHb)q|r[O|:m֢|lZQ>{CwVWQ>qſSOZIԯ(K[3`Q]?&.wC0NwC v7Wh[u4ֲQwP u~Utr"qwc]?e]AtC]?dU?qk)3;Swߒ<* + Z)aC]?~+U)Gso8[/c覺~~WfS/@uY]?E=*fuR.w{uxP$]ZWS*g䚺~ +]&VOB,JcYbJuI5uakIx.֬4mü)t$Mu8jKӚ๺$;Ա +)CQ^POC YU?Z2]⬴3$3'߯Yt]۴quTRO߅y]JAnԹ8 WXO2~˱K)9c%Y[]zm]?~nZ+=꧓=,I5S'偿rU?]슺~UTҞXՏw/Mu4-o';7O^Kj]?eʱ{SV yg|7;#ȕuOlDٵWSe^]Oba]YOy7ɧ&.%.~Ax1˺UԎ6t?cW>cH9^2|ַy-664<$GO% +sߊ7f9:5\4Ct061<9v7kTu,L S/x}Wވ[xcuh.x{l;O,ޯ^%v×$_VR"OO,ByäcǒhjHWFuD)x-j=2%Y_fjAɗp|Qkwx#s5a/^9fـgu$ ةOCCۍ>7ԧz0C }ҙ̓h]Z EQ#ݛf@>ejm ^YoC.m[>:ctNjp?@yOԧ糓qG|3'M}S\ggC3 ^8~Ȓ7-aْ~8oU@^)Tvh mJZf1S6's aEk$/.Bmle]: Bsbmxz+#hmLNF}$\Goa*R[׆mpJ fPZli +/ +CJ4гKIj\)bf}"Q|ӑE z]&}@뽦\{<OsʵKͧp/U }+EƮºLx{/j˝©s͘C ?pvT-¿z97Z=a"g3"C+R9dXnLl\$)o*[,H3Xv \?J߯8Y0 ㎿Bqjf rpacb̃Ϛ0dejO2gc1~'\iOl%QғEh Ϙ?O ̧33.sk.+e|˝ WgV0`4y%ɧەcv $!fTcW3?}./9ӾkPDto(-*O`X)cfoӺe%խ#{mьCjߏdfɶ^~*SC&`u>`}kw#) + s;Jn}U.;2`[vfDvݒB%2QDG)wtf{7=,Km an}v{m:#3GP޾{w-q7͐KZIQ^^4fʧt]s|2-DGW,pf}# 6J}uXp(Í 7=4fecY$ku]-#] n/R.[MF2~$Ֆv +׹"/x.+cVbJ?2h*?f%1s X!VaWy*?Xz3kX$O0pqՖYbvsLj z;>Sϗ&F |8gI 1R_&H>#s6\>ꇹ*ݜU]*U+}}n_JcWͯpw.\iTVAo[QNݭ =O@r,Ŕ? D{8^zfft<<5 xX6UvN{0T[˻7z*xV̡@ "?21РŠagga^ ?yg^l`/=H 6ϻhJoZlO0axAOۆ= 0*!KGEaJyI_L1Ck;@1cc%TQz߉xW+0W #3 Dy|1DD2'oƒOU&&" R:ŠϗcDpvE2FN>aRD"V*N[@,@͋jq2:(wQmoƂR.f5vWɧ-ۉ0s d٨H)`,J px2;a$dmq#SV'YT:NJ-i㤁p b7L,xb1QFWH!,{ IљZay=8 R^hpK jw@`h"uE}X8BqGmW +.\0-s>RQJ(ͿEȼ Oy(ˈ $ڎj |pl]T@*? X^&"O?SisF.:1+t٬;"s%xiDK ॔%2n^& +8K^~Q|b~S%K/<:xɚ<̅P]_@oQ${3(iMrU xkU/J߄ _x"M*yV+?u`DtvbǏ㧀gی2e(7^ +|'f4+t W>}HY0K$T&&!;jyQ'F| Լї/-~$ГTMxM27OzW8%f9{TwLe ]]\ݘfN |̧I$D_ :y_ȿ%HyW'.fW) fs#>O)Y%VuT Z.ON_n|LG}LM52R$w2Mfk-!:+Jggªh?38_|Yu*s-̃9ON)y')ڼ?pǏOD pOV8oX%\E[*U /Z652gN5NNLP^3C{|xNzx>j\rJ?ՙ6B5M8WaXŰuef\<:Gh"Gz|"lp t +QZ4GY+X$Jgf~8n`/u^%=Ns̾p_| >3g_ҿP;zul#61L$zNt8QtJйOu{4wBOua16c&(i0v?BfS  +ZB$}oO84gHƳr58ęgiT<5L"i4q/mflvtcEh&U1;f_OPz%DL}C(q#/zm P;pP'ss+Y7!#fNHα DA[W]5*xV)ޥ+@X{i=EʁwY:ٕfE{hIxD}w9ǐ9$< .3$z| +i!8#`3$Hy$FBDx9"xиR9} iNGfv-eERɠ&B 8Ǯ$:For^ Dm[iiF9๼Nkzob*Rtf >l]kUa zt&ak) B#G@[tጔ-­rJ=IvL}Sm?O~X;Xb#9f>X'RKsO!"vMJܛjg׆qFd:~҄QTJn~7J_ͯR]7Jn%tqJ1m'K 46r[=%u¿᨞E`| §Ά Ht7Eqɿ1[)JY}LO`~@h ^ts)_Oa;Qs,\P/Q?'Ѡ C|( D#PG \{Ȋ(80EKˣ(0ȡI(hGC1y:y }zjޫ;_?r;v񬝜|\ҤtFZ~]kS) 1=jS3$Y߾,f\4kf͢TRk]gA8 ޹nU-Zg&͂'Yyp p"h%> Wrx>N+b85Zʞ ͓%"UEqnRPRS\ѯ't*P'ě75#Wdo*;ECSh3xFY@-LWT[c^ ӧ -RJܡ;Y~Ġ}Wk Ņhʘ3lG CxA9ѠGQlk}FoЂzDYf UhL$p +VCL[4 +$bC΁`Ho&DD!Cv `Jу&`1`Rm03׆]YF׽!*`GPš;>PѹʘdXAS:j 41ؔc/&B<Έ 3g`E;;Y t0 TgqQ6 [fl 3K`; +H5)vuCۈu{I`1nG DOEvT|⦃t.I|=hq|IuPbI0!z]1GD~Ibcwݎ^tP~D!7˺~s;prx\N)wǧsy|]~'p endstream endobj 15 0 obj [14 0 R] endobj 30 0 obj <> endobj xref 0 31 0000000000 65535 f +0000000000 00000 f +0000012937 00000 n +0000012865 00000 n +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000000016 00000 n +0000000000 00000 f +0000000000 00000 f +0000000149 00000 n +0000014522 00000 n +0000058196 00000 n +0000013314 00000 n +0000017505 00000 n +0000014822 00000 n +0000014709 00000 n +0000013771 00000 n +0000013961 00000 n +0000014009 00000 n +0000014593 00000 n +0000014624 00000 n +0000014857 00000 n +0000017579 00000 n +0000017759 00000 n +0000019061 00000 n +0000033903 00000 n +0000058221 00000 n +trailer <<94763EDBEEB34E7F913256011A8A4B43>]>> startxref 58352 %%EOF \ No newline at end of file diff --git a/mobile/ios/Client/Assets/Images.xcassets/lock_verified.imageset/Contents.json b/mobile/ios/Client/Assets/Images.xcassets/lock_verified.imageset/Contents.json new file mode 100644 index 0000000000..2757a67080 --- /dev/null +++ b/mobile/ios/Client/Assets/Images.xcassets/lock_verified.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "lock-small.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "lock-small@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "lock-small@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/mobile/ios/Client/Assets/Images.xcassets/lock_verified.imageset/lock-small.png b/mobile/ios/Client/Assets/Images.xcassets/lock_verified.imageset/lock-small.png new file mode 100644 index 0000000000..38c781c84c Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/lock_verified.imageset/lock-small.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/lock_verified.imageset/lock-small@2x.png b/mobile/ios/Client/Assets/Images.xcassets/lock_verified.imageset/lock-small@2x.png new file mode 100644 index 0000000000..a604594c5f Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/lock_verified.imageset/lock-small@2x.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/lock_verified.imageset/lock-small@3x.png b/mobile/ios/Client/Assets/Images.xcassets/lock_verified.imageset/lock-small@3x.png new file mode 100644 index 0000000000..763c383528 Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/lock_verified.imageset/lock-small@3x.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/loginSelected.imageset/Contents.json b/mobile/ios/Client/Assets/Images.xcassets/loginSelected.imageset/Contents.json new file mode 100644 index 0000000000..b1df1ed205 --- /dev/null +++ b/mobile/ios/Client/Assets/Images.xcassets/loginSelected.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "loginSelected.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "loginSelected@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "loginSelected@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/mobile/ios/Client/Assets/Images.xcassets/loginSelected.imageset/loginSelected.png b/mobile/ios/Client/Assets/Images.xcassets/loginSelected.imageset/loginSelected.png new file mode 100644 index 0000000000..b8ac7da808 Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/loginSelected.imageset/loginSelected.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/loginSelected.imageset/loginSelected@2x.png b/mobile/ios/Client/Assets/Images.xcassets/loginSelected.imageset/loginSelected@2x.png new file mode 100644 index 0000000000..f990c0ff34 Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/loginSelected.imageset/loginSelected@2x.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/loginSelected.imageset/loginSelected@3x.png b/mobile/ios/Client/Assets/Images.xcassets/loginSelected.imageset/loginSelected@3x.png new file mode 100644 index 0000000000..a37f7e95b2 Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/loginSelected.imageset/loginSelected@3x.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/loginUnselected.imageset/Contents.json b/mobile/ios/Client/Assets/Images.xcassets/loginUnselected.imageset/Contents.json new file mode 100644 index 0000000000..196abc64c8 --- /dev/null +++ b/mobile/ios/Client/Assets/Images.xcassets/loginUnselected.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "loginUnselected.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "loginUnselected@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "loginUnselected@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/mobile/ios/Client/Assets/Images.xcassets/loginUnselected.imageset/loginUnselected.png b/mobile/ios/Client/Assets/Images.xcassets/loginUnselected.imageset/loginUnselected.png new file mode 100644 index 0000000000..07ae5fc48a Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/loginUnselected.imageset/loginUnselected.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/loginUnselected.imageset/loginUnselected@2x.png b/mobile/ios/Client/Assets/Images.xcassets/loginUnselected.imageset/loginUnselected@2x.png new file mode 100644 index 0000000000..20c677eafb Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/loginUnselected.imageset/loginUnselected@2x.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/loginUnselected.imageset/loginUnselected@3x.png b/mobile/ios/Client/Assets/Images.xcassets/loginUnselected.imageset/loginUnselected@3x.png new file mode 100644 index 0000000000..f1db95d6de Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/loginUnselected.imageset/loginUnselected@3x.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/quickSearch.imageset/Contents.json b/mobile/ios/Client/Assets/Images.xcassets/quickSearch.imageset/Contents.json new file mode 100644 index 0000000000..57af6143c9 --- /dev/null +++ b/mobile/ios/Client/Assets/Images.xcassets/quickSearch.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x", + "filename" : "quick-search.png" + }, + { + "idiom" : "universal", + "scale" : "2x", + "filename" : "quick-search@2x.png" + }, + { + "idiom" : "universal", + "scale" : "3x", + "filename" : "quick-search@3x.png" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/mobile/ios/Client/Assets/Images.xcassets/quickSearch.imageset/quick-search.png b/mobile/ios/Client/Assets/Images.xcassets/quickSearch.imageset/quick-search.png new file mode 100644 index 0000000000..3f93adb554 Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/quickSearch.imageset/quick-search.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/quickSearch.imageset/quick-search@2x.png b/mobile/ios/Client/Assets/Images.xcassets/quickSearch.imageset/quick-search@2x.png new file mode 100644 index 0000000000..f65607d044 Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/quickSearch.imageset/quick-search@2x.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/quickSearch.imageset/quick-search@3x.png b/mobile/ios/Client/Assets/Images.xcassets/quickSearch.imageset/quick-search@3x.png new file mode 100644 index 0000000000..0bcf025b15 Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/quickSearch.imageset/quick-search@3x.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/quick_action_go_to_copied.imageset/Contents.json b/mobile/ios/Client/Assets/Images.xcassets/quick_action_go_to_copied.imageset/Contents.json new file mode 100644 index 0000000000..fe5f062330 --- /dev/null +++ b/mobile/ios/Client/Assets/Images.xcassets/quick_action_go_to_copied.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "go-to-copied.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "go-to-copied2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "go-to-copied@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/mobile/ios/Client/Assets/Images.xcassets/quick_action_go_to_copied.imageset/go-to-copied.png b/mobile/ios/Client/Assets/Images.xcassets/quick_action_go_to_copied.imageset/go-to-copied.png new file mode 100644 index 0000000000..c574fb22fc Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/quick_action_go_to_copied.imageset/go-to-copied.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/quick_action_go_to_copied.imageset/go-to-copied2x.png b/mobile/ios/Client/Assets/Images.xcassets/quick_action_go_to_copied.imageset/go-to-copied2x.png new file mode 100644 index 0000000000..aec91e18d3 Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/quick_action_go_to_copied.imageset/go-to-copied2x.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/quick_action_go_to_copied.imageset/go-to-copied@3x.png b/mobile/ios/Client/Assets/Images.xcassets/quick_action_go_to_copied.imageset/go-to-copied@3x.png new file mode 100644 index 0000000000..384e965951 Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/quick_action_go_to_copied.imageset/go-to-copied@3x.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/quick_action_last_bookmark.imageset/Contents.json b/mobile/ios/Client/Assets/Images.xcassets/quick_action_last_bookmark.imageset/Contents.json new file mode 100644 index 0000000000..260654cbbb --- /dev/null +++ b/mobile/ios/Client/Assets/Images.xcassets/quick_action_last_bookmark.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "last-bookmark.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "last-bookmark@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "last-bookmark@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/mobile/ios/Client/Assets/Images.xcassets/quick_action_last_bookmark.imageset/last-bookmark.png b/mobile/ios/Client/Assets/Images.xcassets/quick_action_last_bookmark.imageset/last-bookmark.png new file mode 100644 index 0000000000..ecffed2bfd Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/quick_action_last_bookmark.imageset/last-bookmark.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/quick_action_last_bookmark.imageset/last-bookmark@2x.png b/mobile/ios/Client/Assets/Images.xcassets/quick_action_last_bookmark.imageset/last-bookmark@2x.png new file mode 100644 index 0000000000..fb4fc6a511 Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/quick_action_last_bookmark.imageset/last-bookmark@2x.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/quick_action_last_bookmark.imageset/last-bookmark@3x.png b/mobile/ios/Client/Assets/Images.xcassets/quick_action_last_bookmark.imageset/last-bookmark@3x.png new file mode 100644 index 0000000000..a1ea2d78e5 Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/quick_action_last_bookmark.imageset/last-bookmark@3x.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/quick_action_last_tab.imageset/Contents.json b/mobile/ios/Client/Assets/Images.xcassets/quick_action_last_tab.imageset/Contents.json new file mode 100644 index 0000000000..7b153e8204 --- /dev/null +++ b/mobile/ios/Client/Assets/Images.xcassets/quick_action_last_tab.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "open-tab.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "open-tab2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "open-tab@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/mobile/ios/Client/Assets/Images.xcassets/quick_action_last_tab.imageset/open-tab.png b/mobile/ios/Client/Assets/Images.xcassets/quick_action_last_tab.imageset/open-tab.png new file mode 100644 index 0000000000..67c6253d2d Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/quick_action_last_tab.imageset/open-tab.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/quick_action_last_tab.imageset/open-tab2x.png b/mobile/ios/Client/Assets/Images.xcassets/quick_action_last_tab.imageset/open-tab2x.png new file mode 100644 index 0000000000..aab81ec4c4 Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/quick_action_last_tab.imageset/open-tab2x.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/quick_action_last_tab.imageset/open-tab@3x.png b/mobile/ios/Client/Assets/Images.xcassets/quick_action_last_tab.imageset/open-tab@3x.png new file mode 100644 index 0000000000..b59b3621ae Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/quick_action_last_tab.imageset/open-tab@3x.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/quick_action_new_private_tab.imageset/Contents.json b/mobile/ios/Client/Assets/Images.xcassets/quick_action_new_private_tab.imageset/Contents.json new file mode 100644 index 0000000000..da3489bf6a --- /dev/null +++ b/mobile/ios/Client/Assets/Images.xcassets/quick_action_new_private_tab.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "new-private-tab.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "new-private-tab@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "new-private-tab@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/mobile/ios/Client/Assets/Images.xcassets/quick_action_new_private_tab.imageset/new-private-tab.png b/mobile/ios/Client/Assets/Images.xcassets/quick_action_new_private_tab.imageset/new-private-tab.png new file mode 100644 index 0000000000..642ead018f Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/quick_action_new_private_tab.imageset/new-private-tab.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/quick_action_new_private_tab.imageset/new-private-tab@2x.png b/mobile/ios/Client/Assets/Images.xcassets/quick_action_new_private_tab.imageset/new-private-tab@2x.png new file mode 100644 index 0000000000..138e7c232b Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/quick_action_new_private_tab.imageset/new-private-tab@2x.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/quick_action_new_private_tab.imageset/new-private-tab@3x.png b/mobile/ios/Client/Assets/Images.xcassets/quick_action_new_private_tab.imageset/new-private-tab@3x.png new file mode 100644 index 0000000000..64f917fb17 Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/quick_action_new_private_tab.imageset/new-private-tab@3x.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/quick_action_new_tab.imageset/Contents.json b/mobile/ios/Client/Assets/Images.xcassets/quick_action_new_tab.imageset/Contents.json new file mode 100644 index 0000000000..a5a5041271 --- /dev/null +++ b/mobile/ios/Client/Assets/Images.xcassets/quick_action_new_tab.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "new-tab.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "new-tab@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "new-tab@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/mobile/ios/Client/Assets/Images.xcassets/quick_action_new_tab.imageset/new-tab.png b/mobile/ios/Client/Assets/Images.xcassets/quick_action_new_tab.imageset/new-tab.png new file mode 100644 index 0000000000..7f5ef797b0 Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/quick_action_new_tab.imageset/new-tab.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/quick_action_new_tab.imageset/new-tab@2x.png b/mobile/ios/Client/Assets/Images.xcassets/quick_action_new_tab.imageset/new-tab@2x.png new file mode 100644 index 0000000000..c4996d9b2e Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/quick_action_new_tab.imageset/new-tab@2x.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/quick_action_new_tab.imageset/new-tab@3x.png b/mobile/ios/Client/Assets/Images.xcassets/quick_action_new_tab.imageset/new-tab@3x.png new file mode 100644 index 0000000000..6d40497957 Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/quick_action_new_tab.imageset/new-tab@3x.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/quick_action_reading_list.imageset/Contents.json b/mobile/ios/Client/Assets/Images.xcassets/quick_action_reading_list.imageset/Contents.json new file mode 100644 index 0000000000..3fa08d35cf --- /dev/null +++ b/mobile/ios/Client/Assets/Images.xcassets/quick_action_reading_list.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "reading-list.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "reading-list2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "reading-list@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/mobile/ios/Client/Assets/Images.xcassets/quick_action_reading_list.imageset/reading-list.png b/mobile/ios/Client/Assets/Images.xcassets/quick_action_reading_list.imageset/reading-list.png new file mode 100644 index 0000000000..c9a538a83b Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/quick_action_reading_list.imageset/reading-list.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/quick_action_reading_list.imageset/reading-list2x.png b/mobile/ios/Client/Assets/Images.xcassets/quick_action_reading_list.imageset/reading-list2x.png new file mode 100644 index 0000000000..c59bcaede3 Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/quick_action_reading_list.imageset/reading-list2x.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/quick_action_reading_list.imageset/reading-list@3x.png b/mobile/ios/Client/Assets/Images.xcassets/quick_action_reading_list.imageset/reading-list@3x.png new file mode 100644 index 0000000000..fa02e16368 Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/quick_action_reading_list.imageset/reading-list@3x.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/reader.imageset/Contents.json b/mobile/ios/Client/Assets/Images.xcassets/reader.imageset/Contents.json new file mode 100644 index 0000000000..0b33e17a84 --- /dev/null +++ b/mobile/ios/Client/Assets/Images.xcassets/reader.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "readermode.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "readermode@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "readermode@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/mobile/ios/Client/Assets/Images.xcassets/reader.imageset/readermode.png b/mobile/ios/Client/Assets/Images.xcassets/reader.imageset/readermode.png new file mode 100644 index 0000000000..53a0cde354 Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/reader.imageset/readermode.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/reader.imageset/readermode@2x.png b/mobile/ios/Client/Assets/Images.xcassets/reader.imageset/readermode@2x.png new file mode 100644 index 0000000000..12013f58a7 Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/reader.imageset/readermode@2x.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/reader.imageset/readermode@3x.png b/mobile/ios/Client/Assets/Images.xcassets/reader.imageset/readermode@3x.png new file mode 100644 index 0000000000..5b6790ebd8 Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/reader.imageset/readermode@3x.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/recently_closed.imageset/Contents.json b/mobile/ios/Client/Assets/Images.xcassets/recently_closed.imageset/Contents.json new file mode 100644 index 0000000000..4e7ad4d5ad --- /dev/null +++ b/mobile/ios/Client/Assets/Images.xcassets/recently_closed.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "closedRecently.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "closedRecently@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "closedRecently@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/mobile/ios/Client/Assets/Images.xcassets/recently_closed.imageset/closedRecently.png b/mobile/ios/Client/Assets/Images.xcassets/recently_closed.imageset/closedRecently.png new file mode 100644 index 0000000000..ef0a8b07d3 Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/recently_closed.imageset/closedRecently.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/recently_closed.imageset/closedRecently@2x.png b/mobile/ios/Client/Assets/Images.xcassets/recently_closed.imageset/closedRecently@2x.png new file mode 100644 index 0000000000..3f187384c3 Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/recently_closed.imageset/closedRecently@2x.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/recently_closed.imageset/closedRecently@3x.png b/mobile/ios/Client/Assets/Images.xcassets/recently_closed.imageset/closedRecently@3x.png new file mode 100644 index 0000000000..5a425f0742 Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/recently_closed.imageset/closedRecently@3x.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/search.imageset/Contents.json b/mobile/ios/Client/Assets/Images.xcassets/search.imageset/Contents.json new file mode 100644 index 0000000000..b5c88ada0d --- /dev/null +++ b/mobile/ios/Client/Assets/Images.xcassets/search.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x", + "filename" : "search-suggestion-magnifying-glass.png" + }, + { + "idiom" : "universal", + "scale" : "2x", + "filename" : "search-suggestion-magnifying-glass@2x.png" + }, + { + "idiom" : "universal", + "scale" : "3x", + "filename" : "search-suggestion-magnifying-glass@3x.png" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/mobile/ios/Client/Assets/Images.xcassets/search.imageset/search-suggestion-magnifying-glass.png b/mobile/ios/Client/Assets/Images.xcassets/search.imageset/search-suggestion-magnifying-glass.png new file mode 100644 index 0000000000..d8b4b1a402 Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/search.imageset/search-suggestion-magnifying-glass.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/search.imageset/search-suggestion-magnifying-glass@2x.png b/mobile/ios/Client/Assets/Images.xcassets/search.imageset/search-suggestion-magnifying-glass@2x.png new file mode 100644 index 0000000000..d9b07b3099 Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/search.imageset/search-suggestion-magnifying-glass@2x.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/search.imageset/search-suggestion-magnifying-glass@3x.png b/mobile/ios/Client/Assets/Images.xcassets/search.imageset/search-suggestion-magnifying-glass@3x.png new file mode 100644 index 0000000000..6e68778951 Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/search.imageset/search-suggestion-magnifying-glass@3x.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/settings.imageset/Contents.json b/mobile/ios/Client/Assets/Images.xcassets/settings.imageset/Contents.json new file mode 100644 index 0000000000..80578099fa --- /dev/null +++ b/mobile/ios/Client/Assets/Images.xcassets/settings.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "Settings-cog.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "Settings-cog@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "Settings-cog@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/mobile/ios/Client/Assets/Images.xcassets/settings.imageset/Settings-cog.png b/mobile/ios/Client/Assets/Images.xcassets/settings.imageset/Settings-cog.png new file mode 100644 index 0000000000..ce922b5e77 Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/settings.imageset/Settings-cog.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/settings.imageset/Settings-cog@2x.png b/mobile/ios/Client/Assets/Images.xcassets/settings.imageset/Settings-cog@2x.png new file mode 100644 index 0000000000..33f3a1faeb Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/settings.imageset/Settings-cog@2x.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/settings.imageset/Settings-cog@3x.png b/mobile/ios/Client/Assets/Images.xcassets/settings.imageset/Settings-cog@3x.png new file mode 100644 index 0000000000..dcd226d7d0 Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/settings.imageset/Settings-cog@3x.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/shareRequestDesktopSite.imageset/Contents.json b/mobile/ios/Client/Assets/Images.xcassets/shareRequestDesktopSite.imageset/Contents.json new file mode 100644 index 0000000000..13301f1ef2 --- /dev/null +++ b/mobile/ios/Client/Assets/Images.xcassets/shareRequestDesktopSite.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "RDS.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "RDS@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "RDS@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/mobile/ios/Client/Assets/Images.xcassets/shareRequestDesktopSite.imageset/RDS.png b/mobile/ios/Client/Assets/Images.xcassets/shareRequestDesktopSite.imageset/RDS.png new file mode 100644 index 0000000000..d4c7ce8935 Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/shareRequestDesktopSite.imageset/RDS.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/shareRequestDesktopSite.imageset/RDS@2x.png b/mobile/ios/Client/Assets/Images.xcassets/shareRequestDesktopSite.imageset/RDS@2x.png new file mode 100644 index 0000000000..038a8678b8 Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/shareRequestDesktopSite.imageset/RDS@2x.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/shareRequestDesktopSite.imageset/RDS@3x.png b/mobile/ios/Client/Assets/Images.xcassets/shareRequestDesktopSite.imageset/RDS@3x.png new file mode 100644 index 0000000000..e25b19da0b Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/shareRequestDesktopSite.imageset/RDS@3x.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/shareRequestMobileSite.imageset/Contents.json b/mobile/ios/Client/Assets/Images.xcassets/shareRequestMobileSite.imageset/Contents.json new file mode 100644 index 0000000000..0adc6482ae --- /dev/null +++ b/mobile/ios/Client/Assets/Images.xcassets/shareRequestMobileSite.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "RMS.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "RMS@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "RMS@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/mobile/ios/Client/Assets/Images.xcassets/shareRequestMobileSite.imageset/RMS.png b/mobile/ios/Client/Assets/Images.xcassets/shareRequestMobileSite.imageset/RMS.png new file mode 100644 index 0000000000..6b80f26814 Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/shareRequestMobileSite.imageset/RMS.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/shareRequestMobileSite.imageset/RMS@2x.png b/mobile/ios/Client/Assets/Images.xcassets/shareRequestMobileSite.imageset/RMS@2x.png new file mode 100644 index 0000000000..2462e5f291 Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/shareRequestMobileSite.imageset/RMS@2x.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/shareRequestMobileSite.imageset/RMS@3x.png b/mobile/ios/Client/Assets/Images.xcassets/shareRequestMobileSite.imageset/RMS@3x.png new file mode 100644 index 0000000000..188adce0a6 Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/shareRequestMobileSite.imageset/RMS@3x.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/smallPrivateMask.imageset/Contents.json b/mobile/ios/Client/Assets/Images.xcassets/smallPrivateMask.imageset/Contents.json new file mode 100644 index 0000000000..da3489bf6a --- /dev/null +++ b/mobile/ios/Client/Assets/Images.xcassets/smallPrivateMask.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "new-private-tab.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "new-private-tab@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "new-private-tab@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/mobile/ios/Client/Assets/Images.xcassets/smallPrivateMask.imageset/new-private-tab.png b/mobile/ios/Client/Assets/Images.xcassets/smallPrivateMask.imageset/new-private-tab.png new file mode 100644 index 0000000000..642ead018f Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/smallPrivateMask.imageset/new-private-tab.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/smallPrivateMask.imageset/new-private-tab@2x.png b/mobile/ios/Client/Assets/Images.xcassets/smallPrivateMask.imageset/new-private-tab@2x.png new file mode 100644 index 0000000000..138e7c232b Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/smallPrivateMask.imageset/new-private-tab@2x.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/smallPrivateMask.imageset/new-private-tab@3x.png b/mobile/ios/Client/Assets/Images.xcassets/smallPrivateMask.imageset/new-private-tab@3x.png new file mode 100644 index 0000000000..64f917fb17 Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/smallPrivateMask.imageset/new-private-tab@3x.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/splash.imageset/Contents.json b/mobile/ios/Client/Assets/Images.xcassets/splash.imageset/Contents.json new file mode 100644 index 0000000000..5b0a67688c --- /dev/null +++ b/mobile/ios/Client/Assets/Images.xcassets/splash.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "splash.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "splash@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "splash@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/mobile/ios/Client/Assets/Images.xcassets/splash.imageset/splash.png b/mobile/ios/Client/Assets/Images.xcassets/splash.imageset/splash.png new file mode 100644 index 0000000000..9db6904ce1 Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/splash.imageset/splash.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/splash.imageset/splash@2x.png b/mobile/ios/Client/Assets/Images.xcassets/splash.imageset/splash@2x.png new file mode 100644 index 0000000000..c12fa29e41 Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/splash.imageset/splash@2x.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/splash.imageset/splash@3x.png b/mobile/ios/Client/Assets/Images.xcassets/splash.imageset/splash@3x.png new file mode 100644 index 0000000000..5459357835 Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/splash.imageset/splash@3x.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/synced_devices.imageset/Contents.json b/mobile/ios/Client/Assets/Images.xcassets/synced_devices.imageset/Contents.json new file mode 100644 index 0000000000..977698d614 --- /dev/null +++ b/mobile/ios/Client/Assets/Images.xcassets/synced_devices.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "Synced Panel.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "Synced Panel@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "Synced Panel@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/mobile/ios/Client/Assets/Images.xcassets/synced_devices.imageset/Synced Panel.png b/mobile/ios/Client/Assets/Images.xcassets/synced_devices.imageset/Synced Panel.png new file mode 100644 index 0000000000..c889e158cb Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/synced_devices.imageset/Synced Panel.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/synced_devices.imageset/Synced Panel@2x.png b/mobile/ios/Client/Assets/Images.xcassets/synced_devices.imageset/Synced Panel@2x.png new file mode 100644 index 0000000000..7b47639594 Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/synced_devices.imageset/Synced Panel@2x.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/synced_devices.imageset/Synced Panel@3x.png b/mobile/ios/Client/Assets/Images.xcassets/synced_devices.imageset/Synced Panel@3x.png new file mode 100644 index 0000000000..25e4001a66 Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/synced_devices.imageset/Synced Panel@3x.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/topTabs-closeTabs.imageset/Contents.json b/mobile/ios/Client/Assets/Images.xcassets/topTabs-closeTabs.imageset/Contents.json new file mode 100644 index 0000000000..5519b4b26a --- /dev/null +++ b/mobile/ios/Client/Assets/Images.xcassets/topTabs-closeTabs.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "closeTab.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "closeTab@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "closeTab@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/mobile/ios/Client/Assets/Images.xcassets/topTabs-closeTabs.imageset/closeTab.png b/mobile/ios/Client/Assets/Images.xcassets/topTabs-closeTabs.imageset/closeTab.png new file mode 100644 index 0000000000..c41ba45c9a Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/topTabs-closeTabs.imageset/closeTab.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/topTabs-closeTabs.imageset/closeTab@2x.png b/mobile/ios/Client/Assets/Images.xcassets/topTabs-closeTabs.imageset/closeTab@2x.png new file mode 100644 index 0000000000..be2c23aca7 Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/topTabs-closeTabs.imageset/closeTab@2x.png differ diff --git a/mobile/ios/Client/Assets/Images.xcassets/topTabs-closeTabs.imageset/closeTab@3x.png b/mobile/ios/Client/Assets/Images.xcassets/topTabs-closeTabs.imageset/closeTab@3x.png new file mode 100644 index 0000000000..75eddea608 Binary files /dev/null and b/mobile/ios/Client/Assets/Images.xcassets/topTabs-closeTabs.imageset/closeTab@3x.png differ diff --git a/mobile/ios/Client/Assets/LoginsHelper.js b/mobile/ios/Client/Assets/LoginsHelper.js new file mode 100644 index 0000000000..ece8ae8781 --- /dev/null +++ b/mobile/ios/Client/Assets/LoginsHelper.js @@ -0,0 +1,713 @@ +/* vim: set ts=2 sts=2 sw=2 et tw=80: */ +/* 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/. */ + +(function() { +"use strict"; + +var gEnabled = true; +var gStoreWhenAutocompleteOff = true; +var gAutofillForms = true; +var gDebug = false; + +var KEYCODE_ARROW_DOWN = 40; + +function log(pieces) { + if (!gDebug) + return; + alert(pieces); +} + +var LoginManagerContent = { + _getRandomId: function() { + return Math.round(Math.random() * (Number.MAX_VALUE - Number.MIN_VALUE) + Number.MIN_VALUE).toString() + }, + + _messages: [ "RemoteLogins:loginsFound" ], + + // Map from form login requests to information about that request. + _requests: { }, + + _takeRequest: function(msg) { + var data = msg; + var request = this._requests[data.requestId]; + this._requests[data.requestId] = undefined; + return request; + }, + + _sendRequest: function(requestData, messageData) { + var requestId = this._getRandomId(); + messageData.requestId = requestId; + webkit.messageHandlers.loginsManagerMessageHandler.postMessage(messageData); + + var self = this; + return new Promise(function(resolve, reject) { + requestData.promise = { resolve: resolve, reject: reject }; + self._requests[requestId] = requestData; + }); + }, + + receiveMessage: function (msg) { + var request = this._takeRequest(msg); + switch (msg.name) { + case "RemoteLogins:loginsFound": { + request.promise.resolve({ form: request.form, + loginsFound: msg.logins }); + break; + } + + case "RemoteLogins:loginsAutoCompleted": { + request.promise.resolve(msg.logins); + break; + } + } + }, + + _asyncFindLogins : function (form, options) { + // XXX - Unlike desktop, I want to avoid doing a lookup if there is no username/password in this form + var fields = this._getFormFields(form, false); + if (!fields[0] || !fields[1]) { + return Promise.reject("No logins found"); + } + + fields[0].addEventListener("blur", onBlur) + + var formOrigin = LoginUtils._getPasswordOrigin(); + var actionOrigin = LoginUtils._getActionOrigin(form); + if (actionOrigin == null) { + return Promise.reject("Action origin is null") + } + + // XXX - Allowing the page to set origin information in this message is a security problem. Right now its just ignored... + // TODO: We need to designate what type of message we're sending here... + var requestData = { form: form }; + var messageData = { type: "request", formOrigin: formOrigin, actionOrigin: actionOrigin }; + return this._sendRequest(requestData, messageData); + }, + + loginsFound : function (form, loginsFound) { + var autofillForm = gAutofillForms; // && !PrivateBrowsingUtils.isContentWindowPrivate(doc.defaultView); + this._fillForm(form, autofillForm, false, false, false, loginsFound); + }, + + /* + * onUsernameInput + * + * Listens for DOMAutoComplete and blur events on an input field. + */ + onUsernameInput : function(event) { + if (!gEnabled) + return; + + var acInputField = event.target; + + // This is probably a bit over-conservatative. + if (!(acInputField.ownerDocument instanceof HTMLDocument)) + return; + + if (!this._isUsernameFieldType(acInputField)) + return; + + var acForm = acInputField.form; + if (!acForm) + return; + + // If the username is blank, bail out now -- we don't want + // fillForm() to try filling in a login without a username + // to filter on (bug 471906). + if (!acInputField.value) + return; + + log("onUsernameInput from", event.type); + + // Make sure the username field fillForm will use is the + // same field as the autocomplete was activated on. + var [usernameField, passwordField, ignored] = + this._getFormFields(acForm, false); + if (usernameField == acInputField && passwordField) { + var self = this; + this._asyncFindLogins(acForm, { showMasterPassword: false }) + .then(function(res) { + self._fillForm(res.form, true, true, true, true, res.loginsFound); + }).then(null, log); + } else { + // Ignore the event, it's for some input we don't care about. + } + }, + + /* + * _getPasswordFields + * + * Returns an array of password field elements for the specified form. + * If no pw fields are found, or if more than 3 are found, then null + * is returned. + * + * skipEmptyFields can be set to ignore password fields with no value. + */ + _getPasswordFields : function (form, skipEmptyFields) { + // Locate the password fields in the form. + var pwFields = []; + for (var i = 0; i < form.elements.length; i++) { + var element = form.elements[i]; + if (!(element instanceof HTMLInputElement) || + element.type != "password") + continue; + + if (skipEmptyFields && !element.value) + continue; + + pwFields[pwFields.length] = { index : i, + element : element }; + } + + // If too few or too many fields, bail out. + if (pwFields.length == 0) { + log("(form ignored -- no password fields.)"); + return null; + } else if (pwFields.length > 3) { + log("(form ignored -- too many password fields. [ got ", + pwFields.length, "])"); + return null; + } + return pwFields; + }, + + _isUsernameFieldType: function(element) { + if (!(element instanceof HTMLInputElement)) + return false; + + var fieldType = (element.hasAttribute("type") ? + element.getAttribute("type").toLowerCase() : + element.type); + if (fieldType == "text" || + fieldType == "email" || + fieldType == "url" || + fieldType == "tel" || + fieldType == "number") { + return true; + } + return false; + }, + + /* + * _getFormFields + * + * Returns the username and password fields found in the form. + * Can handle complex forms by trying to figure out what the + * relevant fields are. + * + * Returns: [usernameField, newPasswordField, oldPasswordField] + * + * usernameField may be null. + * newPasswordField will always be non-null. + * oldPasswordField may be null. If null, newPasswordField is just + * "theLoginField". If not null, the form is apparently a + * change-password field, with oldPasswordField containing the password + * that is being changed. + */ + _getFormFields : function (form, isSubmission) { + var usernameField = null; + + // Locate the password field(s) in the form. Up to 3 supported. + // If there's no password field, there's nothing for us to do. + var pwFields = this._getPasswordFields(form, isSubmission); + if (!pwFields) + return [null, null, null]; + + // Locate the username field in the form by searching backwards + // from the first passwordfield, assume the first text field is the + // username. We might not find a username field if the user is + // already logged in to the site. + for (var i = pwFields[0].index - 1; i >= 0; i--) { + var element = form.elements[i]; + if (this._isUsernameFieldType(element)) { + usernameField = element; + break; + } + } + + if (!usernameField) + log("(form -- no username field found)"); + + + // If we're not submitting a form (it's a page load), there are no + // password field values for us to use for identifying fields. So, + // just assume the first password field is the one to be filled in. + if (!isSubmission || pwFields.length == 1) + return [usernameField, pwFields[0].element, null]; + + + // Try to figure out WTF is in the form based on the password values. + var oldPasswordField, newPasswordField; + var pw1 = pwFields[0].element.value; + var pw2 = pwFields[1].element.value; + var pw3 = (pwFields[2] ? pwFields[2].element.value : null); + + if (pwFields.length == 3) { + // Look for two identical passwords, that's the new password + + if (pw1 == pw2 && pw2 == pw3) { + // All 3 passwords the same? Weird! Treat as if 1 pw field. + newPasswordField = pwFields[0].element; + oldPasswordField = null; + } else if (pw1 == pw2) { + newPasswordField = pwFields[0].element; + oldPasswordField = pwFields[2].element; + } else if (pw2 == pw3) { + oldPasswordField = pwFields[0].element; + newPasswordField = pwFields[2].element; + } else if (pw1 == pw3) { + // A bit odd, but could make sense with the right page layout. + newPasswordField = pwFields[0].element; + oldPasswordField = pwFields[1].element; + } else { + // We can't tell which of the 3 passwords should be saved. + log("(form ignored -- all 3 pw fields differ)"); + return [null, null, null]; + } + } else { // pwFields.length == 2 + if (pw1 == pw2) { + // Treat as if 1 pw field + newPasswordField = pwFields[0].element; + oldPasswordField = null; + } else { + // Just assume that the 2nd password is the new password + oldPasswordField = pwFields[0].element; + newPasswordField = pwFields[1].element; + } + } + + return [usernameField, newPasswordField, oldPasswordField]; + }, + + /* + * _isAutoCompleteDisabled + * + * Returns true if the page requests autocomplete be disabled for the + * specified form input. + */ + _isAutocompleteDisabled : function (element) { + if (element && element.hasAttribute("autocomplete") && + element.getAttribute("autocomplete").toLowerCase() == "off") + return true; + + return false; + }, + + /* + * _onFormSubmit + * + * Called by the our observer when notified of a form submission. + * [Note that this happens before any DOM onsubmit handlers are invoked.] + * Looks for a password change in the submitted form, so we can update + * our stored password. + */ + _onFormSubmit : function (form) { + var doc = form.ownerDocument; + var win = doc.defaultView; + + // XXX - We'll handle private mode in Swift + // if (PrivateBrowsingUtils.isContentWindowPrivate(win)) { + // We won't do anything in private browsing mode anyway, + // so there's no need to perform further checks. + // log("(form submission ignored in private browsing mode)"); + // return; + // } + + // If password saving is disabled (globally or for host), bail out now. + if (!gEnabled) + return; + + var hostname = LoginUtils._getPasswordOrigin(doc.documentURI); + if (!hostname) { + log("(form submission ignored -- invalid hostname)"); + return; + } + + var formSubmitURL = LoginUtils._getActionOrigin(form); + + // Get the appropriate fields from the form. + // [usernameField, newPasswordField, oldPasswordField] + var fields = this._getFormFields(form, true); + var usernameField = fields[0]; + var newPasswordField = fields[1]; + var oldPasswordField = fields[2]; + + // Need at least 1 valid password field to do anything. + if (newPasswordField == null) + return; + + // Check for autocomplete=off attribute. We don't use it to prevent + // autofilling (for existing logins), but won't save logins when it's + // present and the storeWhenAutocompleteOff pref is false. + // XXX spin out a bug that we don't update timeLastUsed in this case? + if ((this._isAutocompleteDisabled(form) || + this._isAutocompleteDisabled(usernameField) || + this._isAutocompleteDisabled(newPasswordField) || + this._isAutocompleteDisabled(oldPasswordField)) && !gStoreWhenAutocompleteOff) { + log("(form submission ignored -- autocomplete=off found)"); + return; + } + + // Don't try to send DOM nodes over IPC. + var mockUsername = usernameField ? { name: usernameField.name, + value: usernameField.value } : + null; + var mockPassword = { name: newPasswordField.name, + value: newPasswordField.value }; + var mockOldPassword = oldPasswordField ? + { name: oldPasswordField.name, + value: oldPasswordField.value } : + null; + + // Make sure to pass the opener's top in case it was in a frame. + var opener = win.opener ? win.opener.top : null; + + webkit.messageHandlers.loginsManagerMessageHandler.postMessage({ + type: "submit", + hostname: hostname, + username: mockUsername.value, + usernameField: mockUsername.name, + password: mockPassword.value, + passwordField: mockPassword.name, + formSubmitURL: formSubmitURL + }); + }, + + /* + * _fillform + * + * Fill the form with login information if we can find it. This will find + * an array of logins if not given any, otherwise it will use the logins + * passed in. The logins are returned so they can be reused for + * optimization. Success of action is also returned in format + * [success, foundLogins]. + * + * - autofillForm denotes if we should fill the form in automatically + * - ignoreAutocomplete denotes if we should ignore autocomplete=off + * attributes + * - userTriggered is an indication of whether this filling was triggered by + * the user + * - foundLogins is an array of nsILoginInfo for optimization + */ + _fillForm : function (form, autofillForm, ignoreAutocomplete, + clobberPassword, userTriggered, foundLogins) { + // Heuristically determine what the user/pass fields are + // We do this before checking to see if logins are stored, + // so that the user isn't prompted for a master password + // without need. + var fields = this._getFormFields(form, false); + var usernameField = fields[0]; + var passwordField = fields[1]; + + // Need a valid password field to do anything. + if (passwordField == null) + return [false, foundLogins]; + + // If the password field is disabled or read-only, there's nothing to do. + if (passwordField.disabled || passwordField.readOnly) { + log("not filling form, password field disabled or read-only"); + return [false, foundLogins]; + } + + // Discard logins which have username/password values that don't + // fit into the fields (as specified by the maxlength attribute). + // The user couldn't enter these values anyway, and it helps + // with sites that have an extra PIN to be entered (bug 391514) + var maxUsernameLen = Number.MAX_VALUE; + var maxPasswordLen = Number.MAX_VALUE; + + // If attribute wasn't set, default is -1. + if (usernameField && usernameField.maxLength >= 0) + maxUsernameLen = usernameField.maxLength; + if (passwordField.maxLength >= 0) + maxPasswordLen = passwordField.maxLength; + + var createLogin = function(login) { + return { + hostname: login.hostname, + formSubmitURL: login.formSubmitURL, + httpReal : login.httpRealm, + username: login.username, + password: login.password, + usernameField: login.usernameField, + passwordField: login.passwordField + } + } + foundLogins = map(foundLogins, createLogin); + var logins = foundLogins.filter(function (l) { + var fit = (l.username.length <= maxUsernameLen && + l.password.length <= maxPasswordLen); + if (!fit) + log("Ignored", l.username, "login: won't fit"); + + return fit; + }, this); + + + // Nothing to do if we have no matching logins available. + if (logins.length == 0) + return [false, foundLogins]; + + // The reason we didn't end up filling the form, if any. We include + // this in the formInfo object we send with the passwordmgr-found-logins + // notification. See the _notifyFoundLogins docs for possible values. + var didntFillReason = null; + + // Attach autocomplete stuff to the username field, if we have + // one. This is normally used to select from multiple accounts, + // but even with one account we should refill if the user edits. + // if (usernameField) + // formFillService.markAsLoginManagerField(usernameField); + + // Don't clobber an existing password. + if (passwordField.value && !clobberPassword) { + didntFillReason = "existingPassword"; + return [false, foundLogins]; + } + + // If the form has an autocomplete=off attribute in play, don't + // fill in the login automatically. We check this after attaching + // the autocomplete stuff to the username field, so the user can + // still manually select a login to be filled in. + var isFormDisabled = false; + if (!ignoreAutocomplete && + (this._isAutocompleteDisabled(form) || + this._isAutocompleteDisabled(usernameField) || + this._isAutocompleteDisabled(passwordField))) { + + isFormDisabled = true; + log("form not filled, has autocomplete=off"); + } + + // Variable such that we reduce code duplication and can be sure we + // should be firing notifications if and only if we can fill the form. + var selectedLogin = null; + + if (usernameField && (usernameField.value || usernameField.disabled || usernameField.readOnly)) { + // If username was specified in the field, it's disabled or it's readOnly, only fill in the + // password if we find a matching login. + var username = usernameField.value.toLowerCase(); + + var matchingLogins = logins.filter(function(l) { return l.username.toLowerCase() == username }); + if (matchingLogins.length) { + // If there are multiple, and one matches case, use it + for (var i = 0; i < matchingLogins.length; i++) { + var l = matchingLogins[i]; + if (l.username == usernameField.value) { + selectedLogin = l; + } + } + // Otherwise just use the first + if (!selectedLogin) { + selectedLogin = matchingLogins[0]; + } + } else { + didntFillReason = "existingUsername"; + log("Password not filled. None of the stored logins match the username already present."); + } + } else if (logins.length == 1) { + selectedLogin = logins[0]; + } else { + // We have multiple logins. Handle a special case here, for sites + // which have a normal user+pass login *and* a password-only login + // (eg, a PIN). Prefer the login that matches the type of the form + // (user+pass or pass-only) when there's exactly one that matches. + var matchingLogins; + if (usernameField) + matchingLogins = logins.filter(function(l) { return l.username }); + else + matchingLogins = logins.filter(function(l) { return !l.username }); + + // We really don't want to type on phones, so we always autofill with something... + //if (matchingLogins.length == 1) { + selectedLogin = matchingLogins[0]; + //} else { + //didntFillReason = "multipleLogins"; + //log("Multiple logins for form, so not filling any."); + //} + } + + var didFillForm = false; + if (selectedLogin && autofillForm && !isFormDisabled) { + // Fill the form + if (usernameField) { + // Don't modify the username field if it's disabled or readOnly so we preserve its case. + var disabledOrReadOnly = usernameField.disabled || usernameField.readOnly; + + var userNameDiffers = selectedLogin.username != usernameField.value; + // Don't replace the username if it differs only in case, and the user triggered + // this autocomplete. We assume that if it was user-triggered the entered text + // is desired. + var userEnteredDifferentCase = userTriggered && userNameDiffers && usernameField.value.toLowerCase() == selectedLogin.username.toLowerCase(); + + if (!disabledOrReadOnly && !userEnteredDifferentCase && userNameDiffers) { + usernameField.value = selectedLogin.username; + dispatchKeyboardEvent(usernameField, 'keydown', KEYCODE_ARROW_DOWN); + dispatchKeyboardEvent(usernameField, 'keyup', KEYCODE_ARROW_DOWN); + } + } + if (passwordField.value != selectedLogin.password) { + passwordField.value = selectedLogin.password; + dispatchKeyboardEvent(passwordField, 'keydown', KEYCODE_ARROW_DOWN); + dispatchKeyboardEvent(passwordField, 'keyup', KEYCODE_ARROW_DOWN); + } + didFillForm = true; + } else if (selectedLogin && !autofillForm) { + // For when autofillForm is false, but we still have the information + // to fill a form, we notify observers. + didntFillReason = "noAutofillForms"; + // Services.obs.notifyObservers(form, "passwordmgr-found-form", didntFillReason); + log("autofillForms=false but form can be filled; notified observers"); + } else if (selectedLogin && isFormDisabled) { + // For when autocomplete is off, but we still have the information + // to fill a form, we notify observers. + didntFillReason = "autocompleteOff"; + // Services.obs.notifyObservers(form, "passwordmgr-found-form", didntFillReason); + log("autocomplete=off but form can be filled; notified observers"); + } + + // this._notifyFoundLogins(didntFillReason, usernameField, passwordField, foundLogins, selectedLogin); + return [didFillForm, foundLogins]; + }, +} + +var LoginUtils = { + /* + * _getPasswordOrigin + * + * Get the parts of the URL we want for identification. + */ + _getPasswordOrigin : function (uriString, allowJS) { + // All of this logic is moved to swift (so that we don't need a uri parser here) + return uriString; + }, + + _getActionOrigin : function(form) { + var uriString = form.action; + + // A blank or missing action submits to where it came from. + if (uriString == "") + uriString = form.baseURI; // ala bug 297761 + + return this._getPasswordOrigin(uriString, true); + }, +} + +function onBlur(event) { + LoginManagerContent.onUsernameInput(event) +} + +var documentBody = document.body +var observer = new MutationObserver(function(mutations) { + for(var idx = 0; idx < mutations.length; ++idx){ + findForms(mutations[idx].addedNodes); + } +}); + +function findForms(nodes) { + for (var i = 0; i < nodes.length; i++) { + var node = nodes[i]; + if (node.nodeName === "FORM") { + findLogins(node); + } else if(node.hasChildNodes()) { + findForms(node.childNodes); + } + + } + return false; +} + +observer.observe(documentBody, { attributes: false, childList: true, characterData: false, subtree: true }); + +function findLogins(form) { + try { + LoginManagerContent._asyncFindLogins(form, { }) + .then(function(res) { + LoginManagerContent.loginsFound(res.form, res.loginsFound); + }).then(null, log); + } catch(ex) { + // Eat errors to avoid leaking them to the page + log(ex); + } + } + +window.addEventListener("load", function(event) { + for (var i = 0; i < document.forms.length; i++) { + findLogins(document.forms[i]); + } +}); + +window.addEventListener("submit", function(event) { + try { + LoginManagerContent._onFormSubmit(event.target); + } catch(ex) { + // Eat errors to avoid leaking them to the page + log(ex); + } +}); + +if (!window.__firefox__) { + Object.defineProperty(window, '__firefox__', { + enumerable: false, + configurable: false, + writable: false, + value: {} + }); +} + +function LoginInjector() { + this.inject = function(msg) { + try { + LoginManagerContent.receiveMessage(msg); + } catch(ex) { + // Eat errors to avoid leaking them to the page + // alert(ex); + } + }; +} + +Object.defineProperty(window.__firefox__, 'logins', { + enumerable: false, + configurable: false, + writable: false, + value: Object.freeze(new LoginInjector()) +}); + +function map(array, callback) { + var T, A, k; + + if (array == null) { + throw new TypeError(' array is null or not defined'); + } + + var O = Object(array); + var len = O.length >>> 0; + if (typeof callback !== 'function') { + throw new TypeError(callback + ' is not a function'); + } + if (arguments.length > 1) { + T = array; + } + A = new Array(len); + k = 0; + while (k < len) { + var kValue, mappedValue; + if (k in O) { + kValue = O[k]; + mappedValue = callback.call(T, kValue, k, O); + A[k] = mappedValue; + } + k++; + } + return A; +}; + +function dispatchKeyboardEvent(element, eventName, keyCode) { + var event = document.createEvent('KeyboardEvent'); + event.initKeyboardEvent(eventName, true, true, window, 0, 0, 0, 0, 0, keyCode); + element.dispatchEvent(event); +} + +})(); diff --git a/mobile/ios/Client/Assets/NetError.css b/mobile/ios/Client/Assets/NetError.css new file mode 100644 index 0000000000..7035c4a7c9 --- /dev/null +++ b/mobile/ios/Client/Assets/NetError.css @@ -0,0 +1,110 @@ +/* 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/. */ + +html, +body { + margin: 0; + padding: 0; + height: 100%; +} + +body { + background-color: white; + padding: 0 65px; + color: #333; + font-size: 16px; + -webkit-text-size-adjust: none; + font: -apple-system-body; +} + +h1 { + color: #333; + font-size: 16px; + -webkit-text-size-adjust: none; + font: -apple-system-body; + font-weight: 300; +} + +/* Add a set of stripes at the top of pages */ +#decoration { + background-image: repeating-linear-gradient(-65deg, #eee, #eee 10px, + white 10px, white 20px, + #eee 20px); + position: fixed; + top: 0; + left: 0; + height: 32px; + width: 100%; +} + +a { + color: rgb(76, 158, 255); + text-decoration: none; +} + +#errorShortDesc { + /* Margins between the li and buttons below it won't be collapsed. Remove the bottom margin here. */ + margin: 20px 0 0; +} + +button { + /* Force buttons to display: block here to try and enfoce collapsing margins */ + display: block; + width: 100%; + border: none; + padding: 1rem; + font: -apple-system-body; + background-color: rgb(76, 158, 255); + color: white; + font-size: 16px; + font-weight: 300; + border-radius: 5px; + background-image: none; + margin: 10px 0 0; + position: -webkit-sticky; + bottom: 5px; +} + +#errorPageContainer { + /* If the page is greater than 550px center the content. + * This number should be kept in sync with the media query for tablets below */ + max-width: 550px; + margin: 0 auto; + -webkit-transform: translateY(127px); + padding-bottom: 10px; + + min-height: calc(100% - 127px - 10px); + display: flex; + flex-direction: column; +} + +/* On large screen devices (hopefully a 7+ inch tablet, we already center content (see #errorPageContainer above). + Apply tablet specific styles here */ +@media (min-width: 550px) { + #errorTitle, + #errorShortDescText { + width: 50%; + } + + button { + position: absolute; + top: -0.5em; + right: 0px; + bottom: auto; + padding-left: 5%; + padding-right: 5%; + min-width: 40%; + width: auto; + } +} + +#errorLongContent { + font: -apple-system-body; + color: #ccc; + font-size: 12px; +} + +#errorLongDesc { + margin-bottom: 78px; +} diff --git a/mobile/ios/Client/Assets/NetError.html b/mobile/ios/Client/Assets/NetError.html new file mode 100644 index 0000000000..cd88482cdd --- /dev/null +++ b/mobile/ios/Client/Assets/NetError.html @@ -0,0 +1,50 @@ + + + + + %error_title% + + + + + + + + + +
+ + +
+

%error_title%

+
+ + +
+ + +
+

%short_description%

+
+ + +
%actions%
+
+ +
+ +
+ + + diff --git a/mobile/ios/Client/Assets/NightModeHelper.js b/mobile/ios/Client/Assets/NightModeHelper.js new file mode 100644 index 0000000000..ac83b1f6ce --- /dev/null +++ b/mobile/ios/Client/Assets/NightModeHelper.js @@ -0,0 +1,132 @@ +/* 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/. */ + +(function() { +"use strict"; + +if (!window.__firefox__) { + Object.defineProperty(window, '__firefox__', { + enumerable: false, + configurable: false, + writable: false, + value: {} + }); +} + +Object.defineProperty(window.__firefox__, 'NightMode', { + enumerable: false, + configurable: false, + writable: false, + value: { enabled: false } +}); + +const NIGHT_MODE_INVERT_FILTER_CSS = 'brightness(80%) invert(100%) hue-rotate(180deg)'; + +const NIGHT_MODE_STYLESHEET = `html { + -webkit-filter: hue-rotate(180deg) invert(100%) !important; +} +img,video { + -webkit-filter: ${NIGHT_MODE_INVERT_FILTER_CSS} !important; +}`; + +var styleElement; + +function getStyleElement() { + if (styleElement) { + return styleElement; + } + + styleElement = document.createElement('style'); + styleElement.type = 'text/css'; + styleElement.appendChild(document.createTextNode(NIGHT_MODE_STYLESHEET)); + + return styleElement; +} + +function applyInvertFilterToChildBackgroundImageElements(parentNode) { + parentNode.querySelectorAll('[style*="background"]').forEach(function(el) { + if ((el.style.backgroundImage || '').startsWith('url')) { + applyInvertFilterToElement(el); + } + }); +} + +function applyInvertFilterToElement(el) { + invertedBackgroundImageElements.push(el); + el.__firefox__NightMode_originalFilter = el.style.webkitFilter; + el.style.webkitFilter = NIGHT_MODE_INVERT_FILTER_CSS; +} + +function removeInvertFilterFromElement(el) { + el.style.webkitFilter = el.__firefox__NightMode_originalFilter; + delete el.__firefox__NightMode_originalFilter; +} + +var invertedBackgroundImageElements = null; + +// Create a `MutationObserver` that checks for new elements +// added that have a `background-image` in their `style` +// property/attribute. +var observer = new MutationObserver(function(mutations) { + mutations.forEach(function(mutation) { + mutation.addedNodes.forEach(function(node) { + if (node.nodeType === Node.ELEMENT_NODE) { + applyInvertFilterToChildBackgroundImageElements(node); + } + }); + }); +}); + +Object.defineProperty(window.__firefox__.NightMode, 'setEnabled', { + enumerable: false, + configurable: false, + writable: false, + value: function(enabled) { + if (enabled === window.__firefox__.NightMode.enabled) { + return; + } + + window.__firefox__.NightMode.enabled = enabled; + + var styleElement = getStyleElement(); + + if (enabled) { + invertedBackgroundImageElements = []; + + // Apply the NightMode CSS to the document. + document.documentElement.appendChild(styleElement); + + // Add the "invert" CSS class name to all elements with a + // `background-image` in their `style` property/attribute. + applyInvertFilterToChildBackgroundImageElements(document); + + // Observe for future elements in the document containing + // `background-image` in their `style` property/attribute + // so that we can also apply the "invert" CSS class name + // to them as they are added. + observer.observe(document.documentElement, { + childList: true, + subtree: true + }); + return; + } + + // Stop observing for future elements in the document. + observer.disconnect(); + + // Remove the "invert" CSS class name from all elements + // it was previously applied to. + invertedBackgroundImageElements.forEach(removeInvertFilterFromElement); + + // Remove the NightMode CSS from the document. + var styleElementParentNode = styleElement.parentNode; + if (styleElementParentNode) { + styleElementParentNode.removeChild(styleElement); + } + + invertedBackgroundImageElements = null; + } +}); + +})(); diff --git a/mobile/ios/Client/Assets/NoImageModeHelper.js b/mobile/ios/Client/Assets/NoImageModeHelper.js new file mode 100644 index 0000000000..de241bace0 --- /dev/null +++ b/mobile/ios/Client/Assets/NoImageModeHelper.js @@ -0,0 +1,94 @@ +/* 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/. */ + +(function() { +"use strict"; + +if (!window.__firefox__) { + Object.defineProperty(window, '__firefox__', { + enumerable: false, + configurable: false, + writable: false, + value: {} + }); +} + +Object.defineProperty(window.__firefox__, 'NoImageMode', { + enumerable: false, + configurable: false, + writable: false, + value: { enabled: false } +}); + +var className = "__firefox__NoImageMode"; + +function initializeStyleSheet () { + var noImageCSS = "*{background-image:none !important;}img,iframe{visibility:hidden !important;}"; + var newCss = document.getElementById(className); + if (!newCss) { + var cssStyle = document.createElement("style"); + cssStyle.type = "text/css"; + cssStyle.id = className; + cssStyle.appendChild(document.createTextNode(noImageCSS)); + document.documentElement.appendChild(cssStyle); + } else { + newCss.innerHTML = noImageCSS; + } +} + +Object.defineProperty(window.__firefox__.NoImageMode, 'setEnabled', { + enumerable: false, + configurable: false, + writable: false, + value: function(enabled) { + if (enabled === window.__firefox__.NoImageMode.enabled) { + return; + } + window.__firefox__.NoImageMode.enabled = enabled; + if (enabled) { + initializeStyleSheet(); + return; + } + + // Disable no image mode // + + // It would be useful to also revert the changes we've made, rather than just to prevent any more images from being loaded + var style = document.getElementById(className); + if (style) { + style.remove(); + } + + [].slice.apply(document.getElementsByTagName('img')).forEach(function(el) { + var src = el.src; + el.src = ''; + el.src = src; + }); + + [].slice.apply(document.querySelectorAll('[style*="background"]')).forEach(function(el) { + var backgroundImage = el.style.backgroundImage; + el.style.backgroundImage = 'none'; + el.style.backgroundImage = backgroundImage; + }); + + [].slice.apply(document.styleSheets).forEach(function(styleSheet) { + [].slice.apply(styleSheet.rules || []).forEach(function(rules) { + var style = rules.style; + if (!style) { + return; + } + + var backgroundImage = style.backgroundImage; + style.backgroundImage = 'none'; + style.backgroundImage = backgroundImage; + }); + }); + } +}); + +window.addEventListener("DOMContentLoaded", function (event) { + window.__firefox__.NoImageMode.setEnabled(window.__firefox__.NoImageMode.enabled); +}); + +})(); + diff --git a/mobile/ios/Client/Assets/PrintHelper.js b/mobile/ios/Client/Assets/PrintHelper.js new file mode 100644 index 0000000000..02c849781e --- /dev/null +++ b/mobile/ios/Client/Assets/PrintHelper.js @@ -0,0 +1,10 @@ +/* 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/. */ + +(function() { + "use strict"; + window.print = function() { + webkit.messageHandlers.printHandler.postMessage({}) + }; +}) (); \ No newline at end of file diff --git a/mobile/ios/Client/Assets/Search/README.md b/mobile/ios/Client/Assets/Search/README.md new file mode 100644 index 0000000000..e37ecced43 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/README.md @@ -0,0 +1,57 @@ +# Search Plugins + +Locale-based search engines are imported from the Android l10n repos. To import the latest set of plugins, execute the `./scrape_plugins.py` script. + +*Do not make changes to any files in `SearchPlugins` -- these changes will be overwritten on the next import!* If you need to make changes to search plugins, you have the following options (in preferred order): + +1. Update the plugin directly in the Android l10n repos. This is preferred if your changes apply to both platforms. Since the iOS engines are imported from Android, any changes to the Android l10n repos will be picked up here when the import script is run. +2. Define an overlay. Overlays allow local, iOS-specific modifications to be applied after the files are imported. + +## Import process +The Android search engines are scraped from the Mercurial web frontend to the l10n repos. + +1. The list of all plugins is scraped from the Aurora l10n repo. +2. We only import search engines under supported locales on iOS. The list of supported locales is determined by running the `./get_supported_locales.swift` script from the scraping script. +3. We then scrape the `searchplugins` directory to get the list of plugins for each locale. +4. Each file found in step 3 is downloaded into `SearchPlugins/`. +5. Any search overlays are applied to the downloaded file. Note that search overlays are not applied to overridden files. +6. We also scrape the default search engine for each locale from the `region.properties` prefs file. The default is found by parsing the `browser.search.defaultenginename` preference, and the default engine name is written to `SearchPlugins//default.txt`. + +## Overlays + +### Background +For the most part, the engines are the same on both platforms. There are, however, certain iOS-specific changes that we want to make to some of the engines. Previously, we would modify the engine XML files directly, but this made it difficult to re-import the engines from Android without losing our local changes. + +To address this, we've added support for search engine overlays. These are XML files located in the `SearchOverlays` directory that contain document transformations to be applied after each import. + +#### Usage +To add an overlay, append the overlay definition to the corresponding XML file (or create it if it doesn't exist) in the search engine in the `SearchOverlays` directory. The overlay file name is based on the plugin file name used in `SearchPlugins`; for example, to create a Yahoo overlay, we would add an overlay to `SearchOverlays/yahoo.xml`. + +Overlay files have the following structure: +``` + + + + + + + + +``` + +The root node of an overlay document must be a `SearchOverlay` element. It can have any number of `` or `` nodes, which will perform those respective actions on the engine matching this file. + +### API + +##### append +* Node name: `append` +* Required attribute: `parent` - The value for `parent` is an XPath expression identifying elements in the search plugin XML. Note that all nodes must be prefixed by the `search` namespace. +* Children: `append` must have exactly one child element; the element may have any number of children. This element will be appended to any elements matching the `parent` XPath expression. + +##### replace +* Node name: `replace` +* Required attribute: `target` - The value for `target` is an XPath expression identifying elements in the search plugin XML. Note that all nodes must be prefixed by the `search` namespace. +* Children: `replace` must have exactly one child element; the element may have any number of children. This element will replace any elements matching the `target` XPath expression. + +### Tests +Execute `./run_tests.py` to run tests. This uses test files in the `Tests` directory to check overlay behavior. diff --git a/mobile/ios/Client/Assets/Search/SearchOverlays/baidu.xml b/mobile/ios/Client/Assets/Search/SearchOverlays/baidu.xml new file mode 100644 index 0000000000..98934a1620 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchOverlays/baidu.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/mobile/ios/Client/Assets/Search/SearchOverlays/bing.xml b/mobile/ios/Client/Assets/Search/SearchOverlays/bing.xml new file mode 100644 index 0000000000..f71110ea14 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchOverlays/bing.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/mobile/ios/Client/Assets/Search/SearchOverlays/duckduckgo.xml b/mobile/ios/Client/Assets/Search/SearchOverlays/duckduckgo.xml new file mode 100644 index 0000000000..ada0acfd35 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchOverlays/duckduckgo.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/mobile/ios/Client/Assets/Search/SearchOverlays/qwant.xml b/mobile/ios/Client/Assets/Search/SearchOverlays/qwant.xml new file mode 100644 index 0000000000..233ad0b72d --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchOverlays/qwant.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/mobile/ios/Client/Assets/Search/SearchOverlays/yahoo.xml b/mobile/ios/Client/Assets/Search/SearchOverlays/yahoo.xml new file mode 100644 index 0000000000..ced2af9f84 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchOverlays/yahoo.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/ar/default.txt b/mobile/ios/Client/Assets/Search/SearchPlugins/ar/default.txt new file mode 100644 index 0000000000..ced95a5cb9 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/ar/default.txt @@ -0,0 +1 @@ +Google \ No newline at end of file diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/ar/list.txt b/mobile/ios/Client/Assets/Search/SearchPlugins/ar/list.txt new file mode 100644 index 0000000000..3c6483de88 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/ar/list.txt @@ -0,0 +1,3 @@ +google +wikipedia-ar +twitter diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/ar/wikipedia-ar.xml b/mobile/ios/Client/Assets/Search/SearchPlugins/ar/wikipedia-ar.xml new file mode 100644 index 0000000000..b7d1f01566 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/ar/wikipedia-ar.xml @@ -0,0 +1,23 @@ + + + +ويكيبيديا (ar) +UTF-8 +data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAQAAABIkb+zAAAEXElEQVR4Ae3cA4xs2QJG4b9aj41n27Zt27Zt27Zt27Zt+2KM5lXP924qOzs56VPjKSRnrfhv7NUsp08v98+PsxwT4nJ+nPunl8KZ8/WYQL+eMydJrx5/EhN603lAHpVJ5bzZNp035uyZXM7Uy3LmM7ms9CITzVRGQRfQBXQBXUAX0AV0AV1AF9AFdAFdQBfQBXQBXcBMjgU7M5+1vqtZyXqmc/IsZDEL+53LP3KRPCCv62+rWd7vnpx6v/M5Vd935OF5R+6RtRyeQ3NYNjKTxZw2p8tiduR8uWfemY3+dkjWM5fFLPU/8q9yrRwbHLP39WkHamPD5z1WnNMb/dpW/uAdbipu413+qcl2H3N3cXFv9DNNtvmg68uxMY6dPfewTZNvOauouqEdULlTY532IgqHua+ZxnoHm4ANj63biRNQPZedVPY5nTR1BXsVbNuyTlsDqy4tW/wK4NYiJ01AXMlm+wGrL1cDnWbLejC4m7T4DvAOOSkD4v0qztGyLzpUweOk4UXAp6XV72DNmU7qgPPao+DJ0uLTFPzXXGN5I9acU1o8vb14jZzUAfFeBdvMtuwLDlfwIKme2248T1p9MvY59zACLqcy4Kf5eQr+ZbYRfrAFaXHaf/FRGUZAfE/Bzwf8OKwpuJ/0vYRNPEpavT248rACbqfimtLiqxT8w4yIz+Of5qTV7+LHMqyAaf9R8Clp8ex2K7iXuAa4q7R6aXCH4QXE4xVsOq+0+DYFfzXth/ilnrT6Tvzb9DADlqwqDPjTd377FHwI3EBaPb0NPFaGGRBvoLBiUVr8EFS+KgN8KpYtDjvgQo6i8Hhp8ZJUjnIZaXXGNrxKhh0QX6TwHzPS4uco/FtPWr0T9jnPKAJuOOiCc/UqVG4jrf4AH5dRBPT8UWHg3/BvUfhl635ZcNXRBMSDqFxFWvwklVu07O/BT2RUAad0GIWPtexXgMpPt+xntAt3Gl1AvJjCPufasn4d/6NyY2n4DPzXzCgDzmEvhVdIw+tj02X8nsIPGvus7Xi8jDIgPkLhSPON5ad4t7gbletJ9S71n+BIA65C5dGal1g3nENM+weF70j1R3iNjDogfkrhn6br5dU/4UXS94EtF7+vUC8IjjzgrlRuJ33v27j2dTLbKXxd+r4Pn5RRB9RfxsL3y4H/i0dI9TFUrirOZDeuPh4B8TQqVyjH/ZtZqZ7KIRS+JJ6Fn8k4BNRL9IUPmncwbr/1b37lanbiLuMTEG+nsNfb8cOWq0DLFA7E/8yMU8AlNLnaoP/alSfKOAXEN6h8Qlo8k3UKq5bGLeAWFPa6oLT6Ogqvk3ELmPJ3wOtlgOe0B2w6//gFxCPBsjPIQN8JPi3jGDDvSDxVDPaCNnFNOdGczrNzIrEnRyR5XPZlMIdmKofnpTnx6B783QV0ARNOF9AFdAFdQBfQBXQBXUAX0AV0AV3A6OgCVjLJrEzlj5lk/jiVt2eSefvEvzTJxL84zMS/PM//Af1pBRpQAkXBAAAAAElFTkSuQmCC + + + + + + + + + + + + + +https://ar.wikipedia.org/wiki/خاص:بحث + diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/as/amazon-in.xml b/mobile/ios/Client/Assets/Search/SearchPlugins/as/amazon-in.xml new file mode 100644 index 0000000000..05c5311e31 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/as/amazon-in.xml @@ -0,0 +1,15 @@ + + + +Amazon.in +utf-8 +data:image/png;base64,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 + + + + + +https://www.amazon.in/ + diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/as/default.txt b/mobile/ios/Client/Assets/Search/SearchPlugins/as/default.txt new file mode 100644 index 0000000000..ced95a5cb9 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/as/default.txt @@ -0,0 +1 @@ +Google \ No newline at end of file diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/as/eBay-in.xml b/mobile/ios/Client/Assets/Search/SearchPlugins/as/eBay-in.xml new file mode 100644 index 0000000000..8d27d340f9 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/as/eBay-in.xml @@ -0,0 +1,17 @@ + + + +eBay India +UTF-8 +data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAAEEfUpiAAAEWklEQVRYw8WXfUxVdRjHP79z7guXBBTEEBXEraktCydjWi7RZW82lr340kZNXaX+41p/tF42Xakw3ZqNyeZMnZVYtjnLNLXMzJFLA1FrgiIioveCIvcyLvdyzz3n6Q+7FxIM5YJ8/jovv/2e7/Oc5+X8FICIbCKCiCyTLmgArcUbmb2ugirP3u4rlIjkA3P+3aFIiYjQBa21eCMARv2XFB5Mo9sKRORZ+R80IMv0NAJQ7Q4CYDYdJmwGehZ+O0pEXEA7d0B1i0UnbUqpd7urvg2NXtAAQhWnmF5Ug9tnQDiIZYUBCBo+NPf0WQTLfqc1aDEyyU77kWyCB9Kpa/6NOHsS3Mm9tQcejLoZk0gzEgehb2zp9VP0Ro8S522qv2sJGkDLR6uwmpvBsvA8n8+3f/oA+Oakly3HbhI6swKA9u9dFB5MA6DKs5drvtO3Noh/4Tm8a9fRunUbzkmTGJGgs3ZfIx/sbiT/YQPTdxbjUgnKkcL7z3goPJjG0PhM0pMeo19iYBIrIvJaH4x3iIgeyYNUYAWQepc2jyqlSiPWX+mr/wAqhiwEWKPFGL6sWDfoXm1Ltjf0bYNQ+SnEMPB4b7WLX6vbOmu2uQyrrZqg4Ys+izRGDaBxzlwcUybjXbkaEYuSIzfIGz8E9eZZQudWoidPQ+mJ/F25nNrrRwBY/3MWADYAfXgyrcUb0UeOQNpDLJ85HIAnH4rHrP8CQ9nQEibyiH0In1Ys5KXsLSyadqhL1ztfgy1rHP5du8HpJOu9Kjb8dJ3aGyHE9KMSJhA6XwhiMT/na3ZXLiEt8dFoGt8TJy5vFrfvTOR2R6yJVNr/eXCPhJSINN1DFd7O5EggvX0oxg97aipeGXjCIrJaRByRduACGoBk7j85GlAwSMYBVmiAzuChawwydyVgzmd1TC+qGRABtsiF5fXSsmoNofIKbBPGI01NWIEAQws/AZJpCVjMWHeRKy0GLodGlbuDd55KYf2r6VjXD9Nxcj44UtDiM7C8FcgDmWxTJgnOUbwxdX+XarPYXJZHYtwoFuTsRInIMqDEnfc09swxOPPyOgdBXS3tPxxg8YubONfmoK5oAkp1qs/++AJpiXZ+XJ5IuGEXVls1yriBWCFMz3600QvZ7j+Hy5bI61P3IWLxedlMklzpzJuyE6A0GgHb2AyU3UHCooKogY7ySvSx49B8Q7jW4Oe8J8j4kXEA1N80uNDYweKJlQT2L8D5+CHs45be8jLgJnBtD0op3nriKFuPz2bHyZcxwn4SnGkR40T6wDKgBMC8epXgsTLCl6+AaeLIzcE1K499f/nRFIxJtvPVcS++YJjMFAdvz0hhWLyO+C8Sqi1Ggo3orrHoGQVI4DKixWFLnYlpGWz4ZSKjh+Uyf0rpf6ZBzD93vWFZYfnij3zZc3ppT6932Aa6zJTSKcj9bsDGWb/0gUuDaL8mMgnnyv2nMHpc7TKSU4FcIBvI6GdvbwLlwAmlVPQU9g9eOPBg6mUX4QAAAABJRU5ErkJggg== + + + + + + + +http://search.ebay.in/ + diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/as/list.txt b/mobile/ios/Client/Assets/Search/SearchPlugins/as/list.txt new file mode 100644 index 0000000000..42dc76c624 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/as/list.txt @@ -0,0 +1,6 @@ +amazon-in +bing +eBay-in +google +wikipedia-as +yahoo-in diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/as/wikipedia-as.xml b/mobile/ios/Client/Assets/Search/SearchPlugins/as/wikipedia-as.xml new file mode 100644 index 0000000000..11fa0e3307 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/as/wikipedia-as.xml @@ -0,0 +1,23 @@ + + + +অসমীয়া ৱিকিপিডিয়া (as) +UTF-8 +data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAQAAABIkb+zAAAEXElEQVR4Ae3cA4xs2QJG4b9aj41n27Zt27Zt27Zt27Zt+2KM5lXP924qOzs56VPjKSRnrfhv7NUsp08v98+PsxwT4nJ+nPunl8KZ8/WYQL+eMydJrx5/EhN603lAHpVJ5bzZNp035uyZXM7Uy3LmM7ms9CITzVRGQRfQBXQBXUAX0AV0AV1AF9AFdAFdQBfQBXQBXcBMjgU7M5+1vqtZyXqmc/IsZDEL+53LP3KRPCCv62+rWd7vnpx6v/M5Vd935OF5R+6RtRyeQ3NYNjKTxZw2p8tiduR8uWfemY3+dkjWM5fFLPU/8q9yrRwbHLP39WkHamPD5z1WnNMb/dpW/uAdbipu413+qcl2H3N3cXFv9DNNtvmg68uxMY6dPfewTZNvOauouqEdULlTY532IgqHua+ZxnoHm4ANj63biRNQPZedVPY5nTR1BXsVbNuyTlsDqy4tW/wK4NYiJ01AXMlm+wGrL1cDnWbLejC4m7T4DvAOOSkD4v0qztGyLzpUweOk4UXAp6XV72DNmU7qgPPao+DJ0uLTFPzXXGN5I9acU1o8vb14jZzUAfFeBdvMtuwLDlfwIKme2248T1p9MvY59zACLqcy4Kf5eQr+ZbYRfrAFaXHaf/FRGUZAfE/Bzwf8OKwpuJ/0vYRNPEpavT248rACbqfimtLiqxT8w4yIz+Of5qTV7+LHMqyAaf9R8Clp8ex2K7iXuAa4q7R6aXCH4QXE4xVsOq+0+DYFfzXth/ilnrT6Tvzb9DADlqwqDPjTd377FHwI3EBaPb0NPFaGGRBvoLBiUVr8EFS+KgN8KpYtDjvgQo6i8Hhp8ZJUjnIZaXXGNrxKhh0QX6TwHzPS4uco/FtPWr0T9jnPKAJuOOiCc/UqVG4jrf4AH5dRBPT8UWHg3/BvUfhl635ZcNXRBMSDqFxFWvwklVu07O/BT2RUAad0GIWPtexXgMpPt+xntAt3Gl1AvJjCPufasn4d/6NyY2n4DPzXzCgDzmEvhVdIw+tj02X8nsIPGvus7Xi8jDIgPkLhSPON5ad4t7gbletJ9S71n+BIA65C5dGal1g3nENM+weF70j1R3iNjDogfkrhn6br5dU/4UXS94EtF7+vUC8IjjzgrlRuJ33v27j2dTLbKXxd+r4Pn5RRB9RfxsL3y4H/i0dI9TFUrirOZDeuPh4B8TQqVyjH/ZtZqZ7KIRS+JJ6Fn8k4BNRL9IUPmncwbr/1b37lanbiLuMTEG+nsNfb8cOWq0DLFA7E/8yMU8AlNLnaoP/alSfKOAXEN6h8Qlo8k3UKq5bGLeAWFPa6oLT6Ogqvk3ELmPJ3wOtlgOe0B2w6//gFxCPBsjPIQN8JPi3jGDDvSDxVDPaCNnFNOdGczrNzIrEnRyR5XPZlMIdmKofnpTnx6B783QV0ARNOF9AFdAFdQBfQBXQBXUAX0AV0AV3A6OgCVjLJrEzlj5lk/jiVt2eSefvEvzTJxL84zMS/PM//Af1pBRpQAkXBAAAAAElFTkSuQmCC + + + + + + + + + + + + + +https://as.wikipedia.org/wiki/বিশেষ:সন্ধান + diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/as/yahoo-in.xml b/mobile/ios/Client/Assets/Search/SearchPlugins/as/yahoo-in.xml new file mode 100644 index 0000000000..67e95519df --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/as/yahoo-in.xml @@ -0,0 +1,22 @@ + + + +Yahoo +UTF-8 +data:image/png;base64,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 + + + + + + + + + + + + +https://in.search.yahoo.com/ + diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/az/azerdict.xml b/mobile/ios/Client/Assets/Search/SearchPlugins/az/azerdict.xml new file mode 100644 index 0000000000..c839cb042d --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/az/azerdict.xml @@ -0,0 +1,17 @@ + + + +Azerdict +UTF-8 +data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAYAAADimHc4AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAA2hpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMC1jMDYxIDY0LjE0MDk0OSwgMjAxMC8xMi8wNy0xMDo1NzowMSAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtcE1NOk9yaWdpbmFsRG9jdW1lbnRJRD0ieG1wLmRpZDowMTgwMTE3NDA3MjA2ODExOEE2REI2ODhGMkQ2RTdCRSIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDowQzJCNDY4NTI2REMxMUU1ODVCMkNGRkY2RTc3OUJCRiIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDowQzJCNDY4NDI2REMxMUU1ODVCMkNGRkY2RTc3OUJCRiIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ1M1LjEgTWFjaW50b3NoIj4gPHhtcE1NOkRlcml2ZWRGcm9tIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6MDI4MDExNzQwNzIwNjgxMThBNkRCNjg4RjJENkU3QkUiIHN0UmVmOmRvY3VtZW50SUQ9InhtcC5kaWQ6MDE4MDExNzQwNzIwNjgxMThBNkRCNjg4RjJENkU3QkUiLz4gPC9yZGY6RGVzY3Jp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 + + + + + + + +http://azerdict.com/ + diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/az/default.txt b/mobile/ios/Client/Assets/Search/SearchPlugins/az/default.txt new file mode 100644 index 0000000000..ced95a5cb9 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/az/default.txt @@ -0,0 +1 @@ +Google \ No newline at end of file diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/az/list.txt b/mobile/ios/Client/Assets/Search/SearchPlugins/az/list.txt new file mode 100644 index 0000000000..6ebff05141 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/az/list.txt @@ -0,0 +1,6 @@ +amazondotcom +azerdict +bing +duckduckgo +google +wikipedia-az diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/az/wikipedia-az.xml b/mobile/ios/Client/Assets/Search/SearchPlugins/az/wikipedia-az.xml new file mode 100644 index 0000000000..48dc338cd7 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/az/wikipedia-az.xml @@ -0,0 +1,23 @@ + + + +Vikipediya (az) +UTF-8 +data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAQAAABIkb+zAAAEXElEQVR4Ae3cA4xs2QJG4b9aj41n27Zt27Zt27Zt27Zt+2KM5lXP924qOzs56VPjKSRnrfhv7NUsp08v98+PsxwT4nJ+nPunl8KZ8/WYQL+eMydJrx5/EhN603lAHpVJ5bzZNp035uyZXM7Uy3LmM7ms9CITzVRGQRfQBXQBXUAX0AV0AV1AF9AFdAFdQBfQBXQBXcBMjgU7M5+1vqtZyXqmc/IsZDEL+53LP3KRPCCv62+rWd7vnpx6v/M5Vd935OF5R+6RtRyeQ3NYNjKTxZw2p8tiduR8uWfemY3+dkjWM5fFLPU/8q9yrRwbHLP39WkHamPD5z1WnNMb/dpW/uAdbipu413+qcl2H3N3cXFv9DNNtvmg68uxMY6dPfewTZNvOauouqEdULlTY532IgqHua+ZxnoHm4ANj63biRNQPZedVPY5nTR1BXsVbNuyTlsDqy4tW/wK4NYiJ01AXMlm+wGrL1cDnWbLejC4m7T4DvAOOSkD4v0qztGyLzpUweOk4UXAp6XV72DNmU7qgPPao+DJ0uLTFPzXXGN5I9acU1o8vb14jZzUAfFeBdvMtuwLDlfwIKme2248T1p9MvY59zACLqcy4Kf5eQr+ZbYRfrAFaXHaf/FRGUZAfE/Bzwf8OKwpuJ/0vYRNPEpavT248rACbqfimtLiqxT8w4yIz+Of5qTV7+LHMqyAaf9R8Clp8ex2K7iXuAa4q7R6aXCH4QXE4xVsOq+0+DYFfzXth/ilnrT6Tvzb9DADlqwqDPjTd377FHwI3EBaPb0NPFaGGRBvoLBiUVr8EFS+KgN8KpYtDjvgQo6i8Hhp8ZJUjnIZaXXGNrxKhh0QX6TwHzPS4uco/FtPWr0T9jnPKAJuOOiCc/UqVG4jrf4AH5dRBPT8UWHg3/BvUfhl635ZcNXRBMSDqFxFWvwklVu07O/BT2RUAad0GIWPtexXgMpPt+xntAt3Gl1AvJjCPufasn4d/6NyY2n4DPzXzCgDzmEvhVdIw+tj02X8nsIPGvus7Xi8jDIgPkLhSPON5ad4t7gbletJ9S71n+BIA65C5dGal1g3nENM+weF70j1R3iNjDogfkrhn6br5dU/4UXS94EtF7+vUC8IjjzgrlRuJ33v27j2dTLbKXxd+r4Pn5RRB9RfxsL3y4H/i0dI9TFUrirOZDeuPh4B8TQqVyjH/ZtZqZ7KIRS+JJ6Fn8k4BNRL9IUPmncwbr/1b37lanbiLuMTEG+nsNfb8cOWq0DLFA7E/8yMU8AlNLnaoP/alSfKOAXEN6h8Qlo8k3UKq5bGLeAWFPa6oLT6Ogqvk3ELmPJ3wOtlgOe0B2w6//gFxCPBsjPIQN8JPi3jGDDvSDxVDPaCNnFNOdGczrNzIrEnRyR5XPZlMIdmKofnpTnx6B783QV0ARNOF9AFdAFdQBfQBXQBXUAX0AV0AV3A6OgCVjLJrEzlj5lk/jiVt2eSefvEvzTJxL84zMS/PM//Af1pBRpQAkXBAAAAAElFTkSuQmCC + + + + + + + + + + + + + +https://az.wikipedia.org/wiki/Xüsusi:Axtar + diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/be/be.wikipedia.org.xml b/mobile/ios/Client/Assets/Search/SearchPlugins/be/be.wikipedia.org.xml new file mode 100644 index 0000000000..b8d586ea78 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/be/be.wikipedia.org.xml @@ -0,0 +1,23 @@ + + + +Вікіпедыя (be) +UTF-8 +data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAQAAABIkb+zAAAEXElEQVR4Ae3cA4xs2QJG4b9aj41n27Zt27Zt27Zt27Zt+2KM5lXP924qOzs56VPjKSRnrfhv7NUsp08v98+PsxwT4nJ+nPunl8KZ8/WYQL+eMydJrx5/EhN603lAHpVJ5bzZNp035uyZXM7Uy3LmM7ms9CITzVRGQRfQBXQBXUAX0AV0AV1AF9AFdAFdQBfQBXQBXcBMjgU7M5+1vqtZyXqmc/IsZDEL+53LP3KRPCCv62+rWd7vnpx6v/M5Vd935OF5R+6RtRyeQ3NYNjKTxZw2p8tiduR8uWfemY3+dkjWM5fFLPU/8q9yrRwbHLP39WkHamPD5z1WnNMb/dpW/uAdbipu413+qcl2H3N3cXFv9DNNtvmg68uxMY6dPfewTZNvOauouqEdULlTY532IgqHua+ZxnoHm4ANj63biRNQPZedVPY5nTR1BXsVbNuyTlsDqy4tW/wK4NYiJ01AXMlm+wGrL1cDnWbLejC4m7T4DvAOOSkD4v0qztGyLzpUweOk4UXAp6XV72DNmU7qgPPao+DJ0uLTFPzXXGN5I9acU1o8vb14jZzUAfFeBdvMtuwLDlfwIKme2248T1p9MvY59zACLqcy4Kf5eQr+ZbYRfrAFaXHaf/FRGUZAfE/Bzwf8OKwpuJ/0vYRNPEpavT248rACbqfimtLiqxT8w4yIz+Of5qTV7+LHMqyAaf9R8Clp8ex2K7iXuAa4q7R6aXCH4QXE4xVsOq+0+DYFfzXth/ilnrT6Tvzb9DADlqwqDPjTd377FHwI3EBaPb0NPFaGGRBvoLBiUVr8EFS+KgN8KpYtDjvgQo6i8Hhp8ZJUjnIZaXXGNrxKhh0QX6TwHzPS4uco/FtPWr0T9jnPKAJuOOiCc/UqVG4jrf4AH5dRBPT8UWHg3/BvUfhl635ZcNXRBMSDqFxFWvwklVu07O/BT2RUAad0GIWPtexXgMpPt+xntAt3Gl1AvJjCPufasn4d/6NyY2n4DPzXzCgDzmEvhVdIw+tj02X8nsIPGvus7Xi8jDIgPkLhSPON5ad4t7gbletJ9S71n+BIA65C5dGal1g3nENM+weF70j1R3iNjDogfkrhn6br5dU/4UXS94EtF7+vUC8IjjzgrlRuJ33v27j2dTLbKXxd+r4Pn5RRB9RfxsL3y4H/i0dI9TFUrirOZDeuPh4B8TQqVyjH/ZtZqZ7KIRS+JJ6Fn8k4BNRL9IUPmncwbr/1b37lanbiLuMTEG+nsNfb8cOWq0DLFA7E/8yMU8AlNLnaoP/alSfKOAXEN6h8Qlo8k3UKq5bGLeAWFPa6oLT6Ogqvk3ELmPJ3wOtlgOe0B2w6//gFxCPBsjPIQN8JPi3jGDDvSDxVDPaCNnFNOdGczrNzIrEnRyR5XPZlMIdmKofnpTnx6B783QV0ARNOF9AFdAFdQBfQBXQBXUAX0AV0AV3A6OgCVjLJrEzlj5lk/jiVt2eSefvEvzTJxL84zMS/PM//Af1pBRpQAkXBAAAAAElFTkSuQmCC + + + + + + + + + + + + + +https://be.wikipedia.org/wiki/Адмысловае:Search + diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/be/default.txt b/mobile/ios/Client/Assets/Search/SearchPlugins/be/default.txt new file mode 100644 index 0000000000..daaaf0e061 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/be/default.txt @@ -0,0 +1 @@ +Яндекс \ No newline at end of file diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/be/list.txt b/mobile/ios/Client/Assets/Search/SearchPlugins/be/list.txt new file mode 100644 index 0000000000..ba637d0f1d --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/be/list.txt @@ -0,0 +1,7 @@ +be.wikipedia.org +bing +google:hidden +google-nocodes +tut.by +yahoo +yandex.by diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/be/tut.by.xml b/mobile/ios/Client/Assets/Search/SearchPlugins/be/tut.by.xml new file mode 100644 index 0000000000..82addfc5ff --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/be/tut.by.xml @@ -0,0 +1,18 @@ + + + +TUT.BY +Пошук TUT.BY +UTF-8 +data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAYAAADimHc4AAAACXBIWXMAAAsTAAALEwEAmpwYAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAB4xJREFUeNrsnE9MHFUcxx9Is6EFBLVpQkmLJiZdPRQPtl6MJBRjoomQ9uKFopeaFEM92ITGxjYam9RES0IPvdiFQ/WgKSb1YtukvUk9tFyK0UShKVhC//BHQJSC7/uYJcvuzM5v3ryZN7PzfsmELdAd5vd5v7/v97bst2d3rTIj2qTcqMAAMACMGAAGgBEDwAAwYgAYAEYMAAPASHhSEYU/IvXCLlb5yh6WSqfZpobtrHLvy7a/tzw+wf67O86Wbo+wxaEbbOHnG2xldi7WAMp0NeOg9Np3D7LNXPEV2+ul32dp5Fc2fX6A/f3TlVjCCB0AVvrT3V2Oq1xWVubmOISr7GFvn7ASA8BmxW89fky54u3kYe9Z9uh8fywsIhQAW4/3cHfTEeqDIV7c+6iHLfI4kdgsCAF1x48XQ1e+yC54XGm40C/cXSIBwOVA+an0Lq0P+FT3Ybbti1PJAgDlN3wzwMqrqyPxkDX72yILoSKKys/m+wUZlI8ADgiQSR4XShZAeU21tPJnvx9k85evuBZX2aKtjtcQXusHQFiZnWVTn0bHGpRmQV59PnL36a8HpFNG2Zrir/e7RM1QUgCgCAQ8qsxfvipWooqiqer1FuHjqZYH8HfebI9EwaYkCCPd9KL8qc9OsYlDXcoUgNX856stoi1BemgOCrVJyWRB1AwDK2/y6DHhdlQLXBhWNWIJRba0tggXFnsAeAiqD4bLmf3uYqAPhCxncegXstuMPYA6YpULt2OnfLivmgPtQhnw5Spk4tBhkcq6Lh6+cHRbga80FMqDKbsJVqSd27EL3HBTiA9+ejhwR4Cw45K7tdXsb9faL/JlAVuIK9au+HHKmhAg68/1iXzfjyzdXtsnoNQGqF9iCQCrh1Jg5Wc7eODa9zqKZinYrPErD3r7hEW5p7H74gcASqQUXdggKaxm0645u4p4AFdEKbgq9+6JH4DNhOCFvFw211fVyJvmVbaKZ4kcAGygu7sf+yC4TIBCLaooscAtI0JPSVcckAdACJJO2QWswi1XpwRQqiwQshy4xZjFgBrS6iuWqzutcgRulQXbcoQ36SvkARQ3WTezR4C8+06HKMKqWvdZ35tlj/jKV52XL42MuMcBHoh11APSANwyIErwBQQUaEH0hnLlcYSnI8xoYlwBuLkYndVlIgC4uRjd0xD5/l1FnIidC0KzLhKrjGCNuuKENABKxhCFDQ/q34GJ63i5oHH3LKeqtUW78mGFbu4QDbuVUrQA7BXoDsaUjm0s9wMQhCm7TnUK2sq+ABxwB7AwFNMNGRyKcBP0/XVZAe5NGd6a1zgj5AuAU7dzww2qq7VsfgM65b5oCuqcD/IFAM02StsY4+lhZ0T1586S9hQoiyjSdQC1baxin5cqGLqijMoghgU9JhM4ADwAJRhjNWJwN2gICLrUAyEPbLZLY1kJ3yOOfAcNASt/2+nPae6Tu07dq18ZAOTRGLalQsC8TrGpCJliC2C9HIWKyjkBZb0gWAFlBGR9tX7cI8bZ/QTnbKYj3sfjiHqt5vokK0rPB4hzYZe8m7XXw9bZQ94YXfEzPYGtT92WoPyYKoIg1Q87wRCTDHZHlPBxBlz5Ks+e6YYQyDlhvxDCFp0QAtkPQHaBcwBxEZ2nKAPbkAEEnMXyEpiTCCHQHTHMZeLUiqopt1KEAAAzQd4AjS5AwAGNsKwBDbY7b7VLucGQIcw88UHdM2/wF41B3+mfm8Ns5sK3PM35l6VeTLOyVEr5PdASmTzaw+6f/pI9nrq/PhfqdWcOmdamhgZycelDhpAFZfiLUKsSFFDIlGQOWztlMehqOu1syWZlIWRH/QBwhL/4SpffFceceEGF0REMyFKAwMV4/diyiEL4EACa+IubUQqGTu0JFGd+Nk8iCOGlstXVVfb7c+lp/o8nWQIkQhBmnv9jpDabhg6yhIhskRhAdjSYWwckBkCEIAidCxcESZIbioA7Eu4nvxLOsISJH0ugzBsVkYxdK+IMS6DIQ/AF4EwBAG4SoygMDARiqiz/8Wn9lq4LLABygiVUvELw0dfaoOMNACwyJw0Ed5Ec6D2Zu/rtLCDrn8YMhOIiMVM0ZhdnCwBwQkhHO1mCxQ0CflbsDLSDdFq63SDrdUC+8LoAvuqTJINAoxCzRtlT9GgAYnpDoh8F12MbXx0BWBCu8S+vMSN+5DpXfrPTD922JPFxs8NGh9IybOnQUYpagGUFKJlv8Wun0afnoNtk5/e9WEA2KLclOTOSVH6bm/JJFpBnCYgJu41+Xd1OM0X5JAvIswQEk+tGx84B14vyPVlAnjWgoOg2+t4gvVzxR7z+JykAFgTEhQxL2B6CjcxYRZbUppb0ZJx1w0Z+/ZBg5ePZG2WV78sC8qyh2bKGpKSqY9aqv+b3jZQAyAHRydbarTtLWPEnuOIzqt5QKYASBqFc8YECyHNNgHEwporHDmFGhavRAiCviGuzrrdjEFgRVAe95PORBmADoznn0l1ZD1sVvrjCULpWAA5AmiwYTVZquztAZY+yteYiFH4rbIVHDgABDLPg5EojKzzTMGpduZL13doVHTsASRHzwa2a5X8BBgBnvaAfQW8DmwAAAABJRU5ErkJggg== + + + + + + + +http://search.tut.by/ + diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/be/yandex.by.xml b/mobile/ios/Client/Assets/Search/SearchPlugins/be/yandex.by.xml new file mode 100644 index 0000000000..48483b25c4 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/be/yandex.by.xml @@ -0,0 +1,22 @@ + + + +Яндекс +Пошук з дапамогаю Яндекс +UTF-8 +data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAYAAADimHc4AAAD8GlDQ1BJQ0MgUHJvZmlsZQAAOI2NVd1v21QUP4lvXKQWP6Cxjg4Vi69VU1u5GxqtxgZJk6XpQhq5zdgqpMl1bhpT1za2021Vn/YCbwz4A4CyBx6QeEIaDMT2su0BtElTQRXVJKQ9dNpAaJP2gqpwrq9Tu13GuJGvfznndz7v0TVAx1ea45hJGWDe8l01n5GPn5iWO1YhCc9BJ/RAp6Z7TrpcLgIuxoVH1sNfIcHeNwfa6/9zdVappwMknkJsVz19HvFpgJSpO64PIN5G+fAp30Hc8TziHS4miFhheJbjLMMzHB8POFPqKGKWi6TXtSriJcT9MzH5bAzzHIK1I08t6hq6zHpRdu2aYdJYuk9Q/881bzZa8Xrx6fLmJo/iu4/VXnfH1BB/rmu5ScQvI77m+BkmfxXxvcZcJY14L0DymZp7pML5yTcW61PvIN6JuGr4halQvmjNlCa4bXJ5zj6qhpxrujeKPYMXEd+q00KR5yNAlWZzrF+Ie+uNsdC/MO4tTOZafhbroyXuR3Df08bLiHsQf+ja6gTPWVimZl7l/oUrjl8OcxDWLbNU5D6JRL2gxkDu16fGuC054OMhclsyXTOOFEL+kmMGs4i5kfNuQ62EnBuam8tzP+Q+tSqhz9SuqpZlvR1EfBiOJTSgYMMM7jpYsAEyqJCHDL4dcFFTAwNMlFDUUpQYiadhDmXteeWAw3HEmA2s15k1RmnP4RHuhBybdBOF7MfnICmSQ2SYjIBM3iRvkcMki9IRcnDTthyLz2Ld2fTzPjTQK+Mdg8y5nkZfFO+se9LQr3/09xZr+5GcaSufeAfAww60mAPx+q8u/bAr8rFCLrx7s+vqEkw8qb+p26n11Aruq6m1iJH6PbWGv1VIY25mkNE8PkaQhxfLIF7DZXx80HD/A3l2jLclYs061xNpWCfoB6WHJTjbH0mV35Q/lRXlC+W8cndbl9t2SfhU+Fb4UfhO+F74GWThknBZ+Em4InwjXIyd1ePnY/Psg3pb1TJNu15TMKWMtFt6ScpKL0ivSMXIn9QtDUlj0h7U7N48t3i8eC0GnMC91dX2sTivgloDTgUVeEGHLTizbf5Da9JLhkhh29QOs1luMcScmBXTIIt7xRFxSBxnuJWfuAd1I7jntkyd/pgKaIwVr3MgmDo2q8x6IdB5QH162mcX7ajtnHGN2bov71OU1+U0fqqoXLD0wX5ZM005UHmySz3qLtDqILDvIL+iH6jB9y2x83ok898GOPQX3lk3Itl0A+BrD6D7tUjWh3fis58BXDigN9yF8M5PJH4B8Gr79/F/XRm8m241mw/wvur4BGDj42bzn+Vmc+NL9L8GcMn8F1kAcXgSteGGAAAG7ElEQVR4Ae2de2wURRzHvzN71xaplMcp9CGP+PinRjGiKFETfMYKakCCiIKgQEFbUAmi8RXxgSJqDaCIkBhE5A8hEnzFgAaMBgOxKBWDIBbaAnI8W6X0bnecKdqSdqbhZO92djLzR9n9fWdndj7ffdzN/XYh+LdUVVVl9S4qmERARjOwYkJI7n+a/ffMCTDGGjjbKs522e6auoXFxcVNolUi/sTj8cKcqLMGhPQX67akmQBjlY0Jd0gsFqsl4sjvU1Sw0cJPM/S2zXMTqmvqBpL6o4fL+OXmrba6XU8/AX5ZKqfimp/+rmwPMgKCvTgD6u0NV4Yn/TFxY6YWfvpBq3oQ7KlKtPHMELAGZIazshdrgBJNZgRrQGY4K3uxBijRZEawBmSGs7IXa4ASTWYEa0BmOCt7sQYo0WRGsAZkhrOyF2uAEk1mhEhmuvGpl4a/QL9YC7r+e5Ctv4Ds2g3UN4A0Nf+41GEnrF9vNG3Z0GGdIMRwGFC9B5G5C0BXrAQ53vj/OAmzNCx6G5BIwJk7H86ceSB82cSirwHxg4jeWwr63Q8mcm8Zk54G/HkA0ZK7QbfvaNlRUxf0M4Bf46PD71fCZ9EovJF3wh02FKz/xUD3bgBt/TBH330f0enPhMYv7QyIcHh0y1YpQO/yS5FcVAF2QT+pHsagVgaQr7+Fs3SFlKN3w3VILF8E5ORI9bAGW8/doEfAGCJPzpLuhceP+MTSd4yDLwarjQF0zZegVb9KDUjOexXI7SzVwh7UxgBn8QdSlu7Ng8EGXSnVTAjqYcC+/RDXf1nxJo6VhY2JaWEA/eobEH4PaFtYtzx411/bNmzUuh4GqI7+a64GIlp9UPPdfC0MIJU/SwfGrjA/Wz54A/g3X7Jjl9yAiy6Uxk0KBm4Aqd178ikRCVVWlC+JmhUK3ADU7VUSZbEeSs0UIXADSMPfapadOqk1Q5TADcDx42qUTvC7p945f5TgR9gR5BMn/Bmlxq0EbgDr0kWJhxw+otRMEQI3AHlqA1C7zxTOynEEbgDr10e9c9u2KzVThMANQPeuYEUFUp5k04/SuEnB4A3gNL3LLpEypet4IlUyKdVMCephwNBbpDzJocMQM6UmFz0MKLkJLCtLytmpWCiNmxLUwgB0ORve6LukTEViFl31qVQzIaiHAZxkcka58iyITH0C+L3aBN7txqCNASjMh/volHY7KALkyFFkDR0Fsn2nVA9zUB8DOEX38XJ4AwdIeZI9tYgOvh0i8w0GJeqShmNH2v8YK0WQoWDdPmTdOuJk7r+iS5bfE+5wnpp41QCw/F5AN/5dgjS/ewrOilWIzH6z3ZZikE3H9LuM6WeAQMd/pMm6bSSIj9d9XQ3Q6hLUctjy+0HTutVwS25sCZm6oKcBgjafokh+tBiJipfBzomZyl+f1EQVYW/cPWj6aQPcCWNUVUIdD0fSTeezgP0HpKBZTjbYqXNJ/IE9unmLtK6OwVAYQDZuhrP6cyk/t3Q83Odntmr842p28aDWdc2X9L0HnAIu8tSLp6y1LjKeMe1OndQaCOGS9gZQfuRTfgbIils6DujBH1EKcdHbAPGY6rOzpXgZn8BzyyZKtTAFtTaALlkGuvMPKU/3oQf4N+A8qRamoL4GHKvnUwoVUpasax7cKdwAA4q2BjhvvA1y8JAUsVs2AR1mU0i30jOopwF8Qs6Z/56UGOPfkN3J46VaGINaGhCZNQekUZ4V504rNeqBPe0MIFu3gX74sfRgFtnS7oSxUi2sQe0MiDz9kvR5MQHYfWQyIKYlDCpaGUB4HhBdu16Kl/U8F+6D90m1MAf1McDzoJpyEICTj/HfizuZ9ZoCMS5tDKDLV4Ly67+ssIJeENPSJhY9DGhsROSF15R8k9MfBrKzlXqYBS0McBYsgXhYT1bYeYXwxoyUSUbEgjeAf9t1Xl+ghJmcUQYo0haVG4VICNwAMd9D+LyPrLC+vXnK4giZZEwsWAP4TCdVvCVFEBbpivZVBWk81iLPvQKiyP/3zu8Lb9SwNPauR9OBnQFkUyWcTz5TUnBnTgMcR6mbIgRnwG87W9IJ28L0+AuavBF3tA2f3npuLkSmRLuiaW6RnqmJ7eiZGwjsDDAXaWojswakxsv32tYA35Gm1qA1IDVevte2BviONLUGrQGp8fK9tjXAd6SpNWgNSI2X77WtAb4jTa1Ba0BqvHyvbQ3wHWlqDVLxH8untomt7RcBwZ4SkCq/GrTtpEZAsKcMbFlqm9nafhEQ7OnumrqFYKzSr0ZtO6dJgDMX7JtfsBCPxwtzos4aENL/NDe31c6EAIffmHCHxGKx2uZPQWKhuqZuIL8p8Jf2YKO9MZ8JXfm2zUxPsi0XrAVzUfMf8/fTP37wylMAAAAASUVORK5CYII= + + + + + + + + + + + +https://www.yandex.by/ + diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/bn-IN/default.txt b/mobile/ios/Client/Assets/Search/SearchPlugins/bn-IN/default.txt new file mode 100644 index 0000000000..ced95a5cb9 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/bn-IN/default.txt @@ -0,0 +1 @@ +Google \ No newline at end of file diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/bn-IN/eBay-in.xml b/mobile/ios/Client/Assets/Search/SearchPlugins/bn-IN/eBay-in.xml new file mode 100644 index 0000000000..8d27d340f9 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/bn-IN/eBay-in.xml @@ -0,0 +1,17 @@ + + + +eBay India +UTF-8 +data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAAEEfUpiAAAEWklEQVRYw8WXfUxVdRjHP79z7guXBBTEEBXEraktCydjWi7RZW82lr340kZNXaX+41p/tF42Xakw3ZqNyeZMnZVYtjnLNLXMzJFLA1FrgiIioveCIvcyLvdyzz3n6Q+7FxIM5YJ8/jovv/2e7/Oc5+X8FICIbCKCiCyTLmgArcUbmb2ugirP3u4rlIjkA3P+3aFIiYjQBa21eCMARv2XFB5Mo9sKRORZ+R80IMv0NAJQ7Q4CYDYdJmwGehZ+O0pEXEA7d0B1i0UnbUqpd7urvg2NXtAAQhWnmF5Ug9tnQDiIZYUBCBo+NPf0WQTLfqc1aDEyyU77kWyCB9Kpa/6NOHsS3Mm9tQcejLoZk0gzEgehb2zp9VP0Ro8S522qv2sJGkDLR6uwmpvBsvA8n8+3f/oA+Oakly3HbhI6swKA9u9dFB5MA6DKs5drvtO3Noh/4Tm8a9fRunUbzkmTGJGgs3ZfIx/sbiT/YQPTdxbjUgnKkcL7z3goPJjG0PhM0pMeo19iYBIrIvJaH4x3iIgeyYNUYAWQepc2jyqlSiPWX+mr/wAqhiwEWKPFGL6sWDfoXm1Ltjf0bYNQ+SnEMPB4b7WLX6vbOmu2uQyrrZqg4Ys+izRGDaBxzlwcUybjXbkaEYuSIzfIGz8E9eZZQudWoidPQ+mJ/F25nNrrRwBY/3MWADYAfXgyrcUb0UeOQNpDLJ85HIAnH4rHrP8CQ9nQEibyiH0In1Ys5KXsLSyadqhL1ztfgy1rHP5du8HpJOu9Kjb8dJ3aGyHE9KMSJhA6XwhiMT/na3ZXLiEt8dFoGt8TJy5vFrfvTOR2R6yJVNr/eXCPhJSINN1DFd7O5EggvX0oxg97aipeGXjCIrJaRByRduACGoBk7j85GlAwSMYBVmiAzuChawwydyVgzmd1TC+qGRABtsiF5fXSsmoNofIKbBPGI01NWIEAQws/AZJpCVjMWHeRKy0GLodGlbuDd55KYf2r6VjXD9Nxcj44UtDiM7C8FcgDmWxTJgnOUbwxdX+XarPYXJZHYtwoFuTsRInIMqDEnfc09swxOPPyOgdBXS3tPxxg8YubONfmoK5oAkp1qs/++AJpiXZ+XJ5IuGEXVls1yriBWCFMz3600QvZ7j+Hy5bI61P3IWLxedlMklzpzJuyE6A0GgHb2AyU3UHCooKogY7ySvSx49B8Q7jW4Oe8J8j4kXEA1N80uNDYweKJlQT2L8D5+CHs45be8jLgJnBtD0op3nriKFuPz2bHyZcxwn4SnGkR40T6wDKgBMC8epXgsTLCl6+AaeLIzcE1K499f/nRFIxJtvPVcS++YJjMFAdvz0hhWLyO+C8Sqi1Ggo3orrHoGQVI4DKixWFLnYlpGWz4ZSKjh+Uyf0rpf6ZBzD93vWFZYfnij3zZc3ppT6932Aa6zJTSKcj9bsDGWb/0gUuDaL8mMgnnyv2nMHpc7TKSU4FcIBvI6GdvbwLlwAmlVPQU9g9eOPBg6mUX4QAAAABJRU5ErkJggg== + + + + + + + +http://search.ebay.in/ + diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/bn-IN/list.txt b/mobile/ios/Client/Assets/Search/SearchPlugins/bn-IN/list.txt new file mode 100644 index 0000000000..fe09799927 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/bn-IN/list.txt @@ -0,0 +1,7 @@ +amazondotcom +bing +eBay-in +google +rediff +wikipedia-bn +yahoo-in diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/bn-IN/rediff.xml b/mobile/ios/Client/Assets/Search/SearchPlugins/bn-IN/rediff.xml new file mode 100644 index 0000000000..7905815b3e --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/bn-IN/rediff.xml @@ -0,0 +1,14 @@ + + + +Rediff +windows-1252 +data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAYAAADimHc4AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAA2hpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMC1jMDYxIDY0LjE0MDk0OSwgMjAxMC8xMi8wNy0xMDo1NzowMSAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtcE1NOk9yaWdpbmFsRG9jdW1lbnRJRD0ieG1wLmRpZDowMTgwMTE3NDA3MjA2ODExOEE2REI2ODhGMkQ2RTdCRSIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDpCNjU0MTA3MEUyQzcxMUU1QTJGRTgwNUQ3ODM3MDA4NyIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDpCNjU0MTA2RkUyQzcxMUU1QTJGRTgwNUQ3ODM3MDA4NyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ1M1LjEgTWFjaW50b3NoIj4gPHhtcE1NOkRlcml2ZWRGcm9tIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6MDE4MDExNzQwNzIwNjgxMTkwMEE4NjY0REI0QThFNzkiIHN0UmVmOmRvY3VtZW50SUQ9InhtcC5kaWQ6MDE4MDExNzQwNzIwNjgxMThBNkRCNjg4RjJENkU3QkUiLz4gPC9yZGY6RGVzY3Jp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 + + + + +http://search.rediff.com/ + diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/bn-IN/wikipedia-bn.xml b/mobile/ios/Client/Assets/Search/SearchPlugins/bn-IN/wikipedia-bn.xml new file mode 100644 index 0000000000..49f8e8afdf --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/bn-IN/wikipedia-bn.xml @@ -0,0 +1,23 @@ + + + +উইকিপিডিয়া (bn) +UTF-8 +data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAQAAABIkb+zAAAEXElEQVR4Ae3cA4xs2QJG4b9aj41n27Zt27Zt27Zt27Zt+2KM5lXP924qOzs56VPjKSRnrfhv7NUsp08v98+PsxwT4nJ+nPunl8KZ8/WYQL+eMydJrx5/EhN603lAHpVJ5bzZNp035uyZXM7Uy3LmM7ms9CITzVRGQRfQBXQBXUAX0AV0AV1AF9AFdAFdQBfQBXQBXcBMjgU7M5+1vqtZyXqmc/IsZDEL+53LP3KRPCCv62+rWd7vnpx6v/M5Vd935OF5R+6RtRyeQ3NYNjKTxZw2p8tiduR8uWfemY3+dkjWM5fFLPU/8q9yrRwbHLP39WkHamPD5z1WnNMb/dpW/uAdbipu413+qcl2H3N3cXFv9DNNtvmg68uxMY6dPfewTZNvOauouqEdULlTY532IgqHua+ZxnoHm4ANj63biRNQPZedVPY5nTR1BXsVbNuyTlsDqy4tW/wK4NYiJ01AXMlm+wGrL1cDnWbLejC4m7T4DvAOOSkD4v0qztGyLzpUweOk4UXAp6XV72DNmU7qgPPao+DJ0uLTFPzXXGN5I9acU1o8vb14jZzUAfFeBdvMtuwLDlfwIKme2248T1p9MvY59zACLqcy4Kf5eQr+ZbYRfrAFaXHaf/FRGUZAfE/Bzwf8OKwpuJ/0vYRNPEpavT248rACbqfimtLiqxT8w4yIz+Of5qTV7+LHMqyAaf9R8Clp8ex2K7iXuAa4q7R6aXCH4QXE4xVsOq+0+DYFfzXth/ilnrT6Tvzb9DADlqwqDPjTd377FHwI3EBaPb0NPFaGGRBvoLBiUVr8EFS+KgN8KpYtDjvgQo6i8Hhp8ZJUjnIZaXXGNrxKhh0QX6TwHzPS4uco/FtPWr0T9jnPKAJuOOiCc/UqVG4jrf4AH5dRBPT8UWHg3/BvUfhl635ZcNXRBMSDqFxFWvwklVu07O/BT2RUAad0GIWPtexXgMpPt+xntAt3Gl1AvJjCPufasn4d/6NyY2n4DPzXzCgDzmEvhVdIw+tj02X8nsIPGvus7Xi8jDIgPkLhSPON5ad4t7gbletJ9S71n+BIA65C5dGal1g3nENM+weF70j1R3iNjDogfkrhn6br5dU/4UXS94EtF7+vUC8IjjzgrlRuJ33v27j2dTLbKXxd+r4Pn5RRB9RfxsL3y4H/i0dI9TFUrirOZDeuPh4B8TQqVyjH/ZtZqZ7KIRS+JJ6Fn8k4BNRL9IUPmncwbr/1b37lanbiLuMTEG+nsNfb8cOWq0DLFA7E/8yMU8AlNLnaoP/alSfKOAXEN6h8Qlo8k3UKq5bGLeAWFPa6oLT6Ogqvk3ELmPJ3wOtlgOe0B2w6//gFxCPBsjPIQN8JPi3jGDDvSDxVDPaCNnFNOdGczrNzIrEnRyR5XPZlMIdmKofnpTnx6B783QV0ARNOF9AFdAFdQBfQBXQBXUAX0AV0AV3A6OgCVjLJrEzlj5lk/jiVt2eSefvEvzTJxL84zMS/PM//Af1pBRpQAkXBAAAAAElFTkSuQmCC + + + + + + + + + + + + + +https://bn.wikipedia.org/wiki/বিশেষ:Search + diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/bn-IN/yahoo-in.xml b/mobile/ios/Client/Assets/Search/SearchPlugins/bn-IN/yahoo-in.xml new file mode 100644 index 0000000000..67e95519df --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/bn-IN/yahoo-in.xml @@ -0,0 +1,22 @@ + + + +Yahoo +UTF-8 +data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAYAAADimHc4AAAAAXNSR0IArs4c6QAACJ5JREFUeAHtXWlsVFUUPm9mOt2gLWKXIFtLKRZIGoIS/CEJa7ASEExdECPYVhYjiAlq0CAgIJBIABUFWkqKEaGiEKRAoAIKBEGBmNJCW2ihQGnL0oW2dJmO9wyly3DPw/bN2+SehPS9c+bee873vbvf95CgSZzglOIgPZ7dJjgB+rO/nR/YxF+PIFApAWSxnJLSIDZZAonBDMB0AHFwKMwJNd8zEkbivRB1EWDgZ0jgOyUNht+w4JMvwFcXcPfc8UFvwlySXob0RADnBvcfiXv1EWDNT6JFAie2+0L0QSCBNUGuDlef4h/zUhF7C8NAjHb0exA6IwFCdERAEKAj+Fi0IEAQoDMCOhcvaoAgQGcEdC5e1ABBgM4I6Fy8qAGCAJ0R0Ll4UQN0JsCmdvmDYoMh8tlAuH6hCipK61xbQE9084GQcF8IjfCDAxsK4cKxO2q7wc1/8LgQwH/Fl6qhtKAaHA1OsNkt4O1vheCevvBPxk3I/l1d31Qn4OKpcpidGgOdu9q5IPSOCYAPBx+FRgfXrJrS5m2B+K/6Q0hvP24ZJYyQX1Zc5No8qVS9CcKnPnXeedJnJGBUYk/SrpZh7Lu9SPCxzOTZWVBX3ahW8c35qk4AlnQo5SpkHr7VXKj7xWufR4F/F9UrY3OxASF2iFsQ2XzvfnFyVzH8vbvEXa3KvSYEoOfrp2dCfS2/nQl40g6vLIxSJUBepm980Q/8A714JqitdsCm2ee4NjWUmhFQlFMFO5bRberYWT2he/9OasTYJs/IIYEwYlr3NrrWN2mLc+HmlXutVapea0YARrFz+UW4dv4uNyCrzQLTVkdzbZ5SSizahK8HgCS5TuM8lG1hViXsXpX/kF5NhaYENNQ5Yf2MTDKemNHB8Mz4ENKu1DDi7R5sSBxEZrNhZiY46l3npcjfeNqgKQHofNaR23BkyzUyjqmrogGHiJ4W7OSx7afkcOpV1cf8vLI9HymvFDdd6rxsqCqvd9Pevw3r4w8T5oVzbUqUk5f1A+zseXL3Tr3sUJmXxlM6XQgoL66DrZ/kkDFMmh8Jwb19SXt7Ddjxjn6Hnmv8MP8CVJSwWboOogsBGOf+by/DpdPl3JC9fa2sQ8bzwcoFO97EdQPBwo6g8ST3ZBlbDrnCM2mi040AJ5tkbpyVCU4nv9MbMiEUcB1JqYyZ0RP6DA7kZuNwOAE7XvRFL9GNAAw4989yOJhUSMYev7Y/ePl03MWAYDtMXkp3vPvXXYb80xVk+VoYOh6dh7zDvqC6gu6QX/ooosMlTVnBZrxB/Blvxc06+HEB3Q91uNB2JtSdAFys+2lJHun2xI/7QEhE+zvkqOeCYPhUesa79dMLUF3WQJarlUF3AjDQ9DUFUJRXxY3Z7mOF+LUDuDZKabGyGe839Iw3/2wFHNxIN31UvmroDUEAzpA3f5BNxjf4xRAYMjGUtLsbxszsBRGD+B0v/nbTnHO6dryt/TUEAegQLv+e2Vfa2rc219gh+3Rij/YjJCjMDq8voVdWj20v0mXGS7ltGALQwZT3s6Chnj8m7NrdF15d1JeKo1n/1pfR9FJzjQO2sFm4kcRQBOC+cfraAhKf2Dnh0CuGfp1h4Iiu8Pzkp8j0u1Ze0nSpmXSklcFQBKBfaYvzoKy4tpWLLZdWqwTTvxvY9G5nix6vrF6Sa6m5rbbl7mZhDexcSe9HtPxS2yvDEVBT0QC4NkNJ1NAu3OHluLnh0D2a3tDBfWkt9ngpvym94QhAR39je8h5p8oon13Lyn6BLXvIXXv4yO7xZv1xG45vKyLz09NgSAKALQ/hqQRKgkK94c2VTzebceHOx7+FkGYDu2hsdELKHDqv1r/V49qYBDAkck+UwYmfb5CYjErsAdHDurgW7IZOCiN/l5FcCPln9F3vIZ1jBvai9h7+cqRcKo1suASxJmsYeHnzx//Xc+6yztcCoeH8w1W46fNe3yP3T+Rp5HN7izFsDcBASi7VwK+rC8iYukV1IsHHRGmLcg0NPvpoaALQwR1L6WEp2inB2rH368uU2TB6wxNwr9IhOyylkMS1Ja1POFC+yOkNTwA6f2jzVXL7khfc2f2lcHoPva7ES6OXzhQE4Jbhpv84lHQ0NELKXOMOO92JNgUB6PT5o3fgeNqjJ1P71l2Ba9n8vQX34I1wbxoCEKxtn+W4JlYUcJW36mD7Qv23GSn/eHpTEYBP9il2dJwSPPxbdUf/bUbKP57eVARgAJmH6PcMSvKreTEaWmc6Aupr+Rs2iLKFLVebTUxHAHXCDYG32gQBqj+AeNSQEkEAhYwH9RJxxhOLsIga4EGkiaxsbOuREnzLxmxiOo99OvM3XhB4PJBlNjEdAXLtvKgBGjx+ckNN0QfoTIBc7dDAtQ4V8b9qguRqR4fQ0SCR6QiQA5l620YDHDtchOkIwBNwlIhOmELGg3rq/A8WYbPT5HjQBY9mZboagB9TosTGjqiYTUznsUXGY7nmyajEyIRjTJeJt1pdzuIhLbOJ6TyWW4wTfYAGj5/cMFT0ARoQILsaKjNE1cC1DhVhuibIzr4jQYnoAyhkPKi3+9EE4Dc/zSam89hL5mNOcs2TUYkxHQFyK55mnAfQ20tGfWSa/MJPzdRWNbhe0MBagaclzDgKMh0By8f/BfeqHFDJvnbS/J0ftgQUFOYN3jL9g1GfJ9MRUJJf8zCW7CWrsiL+u8UP/9hYGtP1AcaCT7k3ggDlGCrKQRCgCD7liQUByjFUlIMgQBF8yhMLApRjqCgHQYAi+JQnFgQox1BRDoIARfApTywIUI6hohyQgEpFOYjEShCoxPdNzPNauZJQDZgWsccakGRA3x4Xl5IkJzilONh7gP0d+bhEbYQ4JZAy0uCF0awJkpwS+E5BhREcexx8QKybMGfYN8n9mpAez24T2PI6/vcV9BdSHyQSf9uDQGVTf5uUBrHJ+OBj4n8BJ5EPp7ErQgIAAAAASUVORK5CYII= + + + + + + + + + + + + +https://in.search.yahoo.com/ + diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/br/default.txt b/mobile/ios/Client/Assets/Search/SearchPlugins/br/default.txt new file mode 100644 index 0000000000..ced95a5cb9 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/br/default.txt @@ -0,0 +1 @@ +Google \ No newline at end of file diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/br/eBay-france.xml b/mobile/ios/Client/Assets/Search/SearchPlugins/br/eBay-france.xml new file mode 100644 index 0000000000..5d1a2db0b8 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/br/eBay-france.xml @@ -0,0 +1,13 @@ + + + +eBay France +ISO-8859-1 +data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAAEEfUpiAAAEWklEQVRYw8WXfUxVdRjHP79z7guXBBTEEBXEraktCydjWi7RZW82lr340kZNXaX+41p/tF42Xakw3ZqNyeZMnZVYtjnLNLXMzJFLA1FrgiIioveCIvcyLvdyzz3n6Q+7FxIM5YJ8/jovv/2e7/Oc5+X8FICIbCKCiCyTLmgArcUbmb2ugirP3u4rlIjkA3P+3aFIiYjQBa21eCMARv2XFB5Mo9sKRORZ+R80IMv0NAJQ7Q4CYDYdJmwGehZ+O0pEXEA7d0B1i0UnbUqpd7urvg2NXtAAQhWnmF5Ug9tnQDiIZYUBCBo+NPf0WQTLfqc1aDEyyU77kWyCB9Kpa/6NOHsS3Mm9tQcejLoZk0gzEgehb2zp9VP0Ro8S522qv2sJGkDLR6uwmpvBsvA8n8+3f/oA+Oakly3HbhI6swKA9u9dFB5MA6DKs5drvtO3Noh/4Tm8a9fRunUbzkmTGJGgs3ZfIx/sbiT/YQPTdxbjUgnKkcL7z3goPJjG0PhM0pMeo19iYBIrIvJaH4x3iIgeyYNUYAWQepc2jyqlSiPWX+mr/wAqhiwEWKPFGL6sWDfoXm1Ltjf0bYNQ+SnEMPB4b7WLX6vbOmu2uQyrrZqg4Ys+izRGDaBxzlwcUybjXbkaEYuSIzfIGz8E9eZZQudWoidPQ+mJ/F25nNrrRwBY/3MWADYAfXgyrcUb0UeOQNpDLJ85HIAnH4rHrP8CQ9nQEibyiH0In1Ys5KXsLSyadqhL1ztfgy1rHP5du8HpJOu9Kjb8dJ3aGyHE9KMSJhA6XwhiMT/na3ZXLiEt8dFoGt8TJy5vFrfvTOR2R6yJVNr/eXCPhJSINN1DFd7O5EggvX0oxg97aipeGXjCIrJaRByRduACGoBk7j85GlAwSMYBVmiAzuChawwydyVgzmd1TC+qGRABtsiF5fXSsmoNofIKbBPGI01NWIEAQws/AZJpCVjMWHeRKy0GLodGlbuDd55KYf2r6VjXD9Nxcj44UtDiM7C8FcgDmWxTJgnOUbwxdX+XarPYXJZHYtwoFuTsRInIMqDEnfc09swxOPPyOgdBXS3tPxxg8YubONfmoK5oAkp1qs/++AJpiXZ+XJ5IuGEXVls1yriBWCFMz3600QvZ7j+Hy5bI61P3IWLxedlMklzpzJuyE6A0GgHb2AyU3UHCooKogY7ySvSx49B8Q7jW4Oe8J8j4kXEA1N80uNDYweKJlQT2L8D5+CHs45be8jLgJnBtD0op3nriKFuPz2bHyZcxwn4SnGkR40T6wDKgBMC8epXgsTLCl6+AaeLIzcE1K499f/nRFIxJtvPVcS++YJjMFAdvz0hhWLyO+C8Sqi1Ggo3orrHoGQVI4DKixWFLnYlpGWz4ZSKjh+Uyf0rpf6ZBzD93vWFZYfnij3zZc3ppT6932Aa6zJTSKcj9bsDGWb/0gUuDaL8mMgnnyv2nMHpc7TKSU4FcIBvI6GdvbwLlwAmlVPQU9g9eOPBg6mUX4QAAAABJRU5ErkJggg== + + + +http://search.ebay.fr/ + diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/br/list.txt b/mobile/ios/Client/Assets/Search/SearchPlugins/br/list.txt new file mode 100644 index 0000000000..822a78a50d --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/br/list.txt @@ -0,0 +1,6 @@ +bing +eBay-france +google +twitter +wikipedia-br +yahoo-france diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/br/wikipedia-br.xml b/mobile/ios/Client/Assets/Search/SearchPlugins/br/wikipedia-br.xml new file mode 100644 index 0000000000..14d8184f11 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/br/wikipedia-br.xml @@ -0,0 +1,23 @@ + + + +Wikipedia (br) +UTF-8 +data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAQAAABIkb+zAAAEXElEQVR4Ae3cA4xs2QJG4b9aj41n27Zt27Zt27Zt27Zt+2KM5lXP924qOzs56VPjKSRnrfhv7NUsp08v98+PsxwT4nJ+nPunl8KZ8/WYQL+eMydJrx5/EhN603lAHpVJ5bzZNp035uyZXM7Uy3LmM7ms9CITzVRGQRfQBXQBXUAX0AV0AV1AF9AFdAFdQBfQBXQBXcBMjgU7M5+1vqtZyXqmc/IsZDEL+53LP3KRPCCv62+rWd7vnpx6v/M5Vd935OF5R+6RtRyeQ3NYNjKTxZw2p8tiduR8uWfemY3+dkjWM5fFLPU/8q9yrRwbHLP39WkHamPD5z1WnNMb/dpW/uAdbipu413+qcl2H3N3cXFv9DNNtvmg68uxMY6dPfewTZNvOauouqEdULlTY532IgqHua+ZxnoHm4ANj63biRNQPZedVPY5nTR1BXsVbNuyTlsDqy4tW/wK4NYiJ01AXMlm+wGrL1cDnWbLejC4m7T4DvAOOSkD4v0qztGyLzpUweOk4UXAp6XV72DNmU7qgPPao+DJ0uLTFPzXXGN5I9acU1o8vb14jZzUAfFeBdvMtuwLDlfwIKme2248T1p9MvY59zACLqcy4Kf5eQr+ZbYRfrAFaXHaf/FRGUZAfE/Bzwf8OKwpuJ/0vYRNPEpavT248rACbqfimtLiqxT8w4yIz+Of5qTV7+LHMqyAaf9R8Clp8ex2K7iXuAa4q7R6aXCH4QXE4xVsOq+0+DYFfzXth/ilnrT6Tvzb9DADlqwqDPjTd377FHwI3EBaPb0NPFaGGRBvoLBiUVr8EFS+KgN8KpYtDjvgQo6i8Hhp8ZJUjnIZaXXGNrxKhh0QX6TwHzPS4uco/FtPWr0T9jnPKAJuOOiCc/UqVG4jrf4AH5dRBPT8UWHg3/BvUfhl635ZcNXRBMSDqFxFWvwklVu07O/BT2RUAad0GIWPtexXgMpPt+xntAt3Gl1AvJjCPufasn4d/6NyY2n4DPzXzCgDzmEvhVdIw+tj02X8nsIPGvus7Xi8jDIgPkLhSPON5ad4t7gbletJ9S71n+BIA65C5dGal1g3nENM+weF70j1R3iNjDogfkrhn6br5dU/4UXS94EtF7+vUC8IjjzgrlRuJ33v27j2dTLbKXxd+r4Pn5RRB9RfxsL3y4H/i0dI9TFUrirOZDeuPh4B8TQqVyjH/ZtZqZ7KIRS+JJ6Fn8k4BNRL9IUPmncwbr/1b37lanbiLuMTEG+nsNfb8cOWq0DLFA7E/8yMU8AlNLnaoP/alSfKOAXEN6h8Qlo8k3UKq5bGLeAWFPa6oLT6Ogqvk3ELmPJ3wOtlgOe0B2w6//gFxCPBsjPIQN8JPi3jGDDvSDxVDPaCNnFNOdGczrNzIrEnRyR5XPZlMIdmKofnpTnx6B783QV0ARNOF9AFdAFdQBfQBXQBXUAX0AV0AV3A6OgCVjLJrEzlj5lk/jiVt2eSefvEvzTJxL84zMS/PM//Af1pBRpQAkXBAAAAAElFTkSuQmCC + + + + + + + + + + + + + +https://br.wikipedia.org/wiki/Dibar:Klask + diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/br/yahoo-france.xml b/mobile/ios/Client/Assets/Search/SearchPlugins/br/yahoo-france.xml new file mode 100644 index 0000000000..5bdc936c24 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/br/yahoo-france.xml @@ -0,0 +1,22 @@ + + + +Yahoo +UTF-8 +data:image/png;base64,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 + + + + + + + + + + + + +https://fr.search.yahoo.com/ + diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/ca/default.txt b/mobile/ios/Client/Assets/Search/SearchPlugins/ca/default.txt new file mode 100644 index 0000000000..ced95a5cb9 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/ca/default.txt @@ -0,0 +1 @@ +Google \ No newline at end of file diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/ca/diec2.xml b/mobile/ios/Client/Assets/Search/SearchPlugins/ca/diec2.xml new file mode 100644 index 0000000000..fd74d61d22 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/ca/diec2.xml @@ -0,0 +1,13 @@ + + +DIEC2 +ISO-8859-1 +data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAYAAADimHc4AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAA2hpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMC1jMDYxIDY0LjE0MDk0OSwgMjAxMC8xMi8wNy0xMDo1NzowMSAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtcE1NOk9yaWdpbmFsRG9jdW1lbnRJRD0ieG1wLmRpZDowMTgwMTE3NDA3MjA2ODExOEE2REI2ODhGMkQ2RTdCRSIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDpCOTA4RjFFQTJENDYxMUU1QTQ4NkU3NUVFNEYzMjg2MSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDpCOTA4RjFFOTJENDYxMUU1QTQ4NkU3NUVFNEYzMjg2MSIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ1M1LjEgTWFjaW50b3NoIj4gPHhtcE1NOkRlcml2ZWRGcm9tIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6RkI3RjExNzQwNzIwNjgxMTgwQkE5RTFBODgxMUZCMzUiIHN0UmVmOmRvY3VtZW50SUQ9InhtcC5kaWQ6MDE4MDExNzQwNzIwNjgxMThBNkRCNjg4RjJENkU3QkUiLz4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+IDw/eHBhY2tldCBlbmQ9InIiPz6n8NEIAAADFklEQVR42uydvUsbYRjAn8R4Jn7ix1bI4ObglqKlf0EpFJyKk9Chqwgubv4hWigO0qng6K4EanUQuukQujiooKIkfqTP02Tq3XvNe1zult8PnpjLm3u5e373fsTlKbSrVZFGQ5RAY0Xjg8br7jGkR0vjh8aextfusYgJaIu80jjRaBOZxEk351JQAYGOgCN9P89DmimnGrWivnwm+blgOV8xAR/JRW4s2RTU1CkovOBWKiLlMilKg2ZT5P4+quWi5NztrK+LrK2RvDTY2RFZXY1qCUrOk0ZHRSYnSV4ajI05m4rOludnEpcWMbkskp18QUDOlBINp5sbv3MCXedHRsKf229C6+vlpfe+BgZExsej225vRZ6eeu+rUBCZmOj8/RfbtdjuxXeuL5X6LOD8XGRhwXO3uySyvR29PVtc1M3YRe99zc2JHBxEty0vixwe9t6XbTSOj0VmZsJtm5siW1t+97m/r79ta30WYE/Y9bXfOa4RYyPg8tKvv6srd5v149PXw0PnGlyjyfc+Hx8zWAOihmsv04bzEfB8BuK+79vX4GCya04xNyzC7IIQAAhAACAAAYAABAACEAAIQAAgAAGAAAQAAhAACEAAIAABgAAEAAIQAAhAACAAAYAABAACEAAIQAAgAAGAAAQAAhAACEAAIAABgAAEAAIQAAhAACAAAYAABAACEAAIQAAgAAGAAAQggBQgAAFeuIrexBFXlc+n7NT/vu/bV1zRnSSVBBPkxr+KkhXK8a0x6ar7ZUVvpqf9Ejc15W6z6/K5Nitj5Sq8YzXBfO8zrihQagJmZ0XOzvzOCRzl6YeGROp1/0JuLnZ3kxVyi8LqiG1s+N1nTNHO9ARYAtKqshqXgCQkSICT4eFOsAizCEMuApJUkgPvXLrXgLs7/1qKEI3VpXQtg+1qtSmNRnibUqmIlMskLw2saqyVxw3z20bAT403oSarNGoB/eSXrQHfyENufLcpKNAp6EgP5slHppxq1GwEtDTedT+A7JJvOW/ZCBAdAX//YaDxSeO9xlty1BfqGnsaX7oPvvwRYACUNV/GZTKtGgAAAABJRU5ErkJggg== + + + + +http://mdlc.iec.cat + diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/ca/list.txt b/mobile/ios/Client/Assets/Search/SearchPlugins/ca/list.txt new file mode 100644 index 0000000000..2e0a625f6e --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/ca/list.txt @@ -0,0 +1,4 @@ +google +twitter +wikipedia-ca +diec2 diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/ca/wikipedia-ca.xml b/mobile/ios/Client/Assets/Search/SearchPlugins/ca/wikipedia-ca.xml new file mode 100644 index 0000000000..78ac873e38 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/ca/wikipedia-ca.xml @@ -0,0 +1,23 @@ + + + +Viquipèdia (ca) +UTF-8 +data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAQAAABIkb+zAAAEXElEQVR4Ae3cA4xs2QJG4b9aj41n27Zt27Zt27Zt27Zt+2KM5lXP924qOzs56VPjKSRnrfhv7NUsp08v98+PsxwT4nJ+nPunl8KZ8/WYQL+eMydJrx5/EhN603lAHpVJ5bzZNp035uyZXM7Uy3LmM7ms9CITzVRGQRfQBXQBXUAX0AV0AV1AF9AFdAFdQBfQBXQBXcBMjgU7M5+1vqtZyXqmc/IsZDEL+53LP3KRPCCv62+rWd7vnpx6v/M5Vd935OF5R+6RtRyeQ3NYNjKTxZw2p8tiduR8uWfemY3+dkjWM5fFLPU/8q9yrRwbHLP39WkHamPD5z1WnNMb/dpW/uAdbipu413+qcl2H3N3cXFv9DNNtvmg68uxMY6dPfewTZNvOauouqEdULlTY532IgqHua+ZxnoHm4ANj63biRNQPZedVPY5nTR1BXsVbNuyTlsDqy4tW/wK4NYiJ01AXMlm+wGrL1cDnWbLejC4m7T4DvAOOSkD4v0qztGyLzpUweOk4UXAp6XV72DNmU7qgPPao+DJ0uLTFPzXXGN5I9acU1o8vb14jZzUAfFeBdvMtuwLDlfwIKme2248T1p9MvY59zACLqcy4Kf5eQr+ZbYRfrAFaXHaf/FRGUZAfE/Bzwf8OKwpuJ/0vYRNPEpavT248rACbqfimtLiqxT8w4yIz+Of5qTV7+LHMqyAaf9R8Clp8ex2K7iXuAa4q7R6aXCH4QXE4xVsOq+0+DYFfzXth/ilnrT6Tvzb9DADlqwqDPjTd377FHwI3EBaPb0NPFaGGRBvoLBiUVr8EFS+KgN8KpYtDjvgQo6i8Hhp8ZJUjnIZaXXGNrxKhh0QX6TwHzPS4uco/FtPWr0T9jnPKAJuOOiCc/UqVG4jrf4AH5dRBPT8UWHg3/BvUfhl635ZcNXRBMSDqFxFWvwklVu07O/BT2RUAad0GIWPtexXgMpPt+xntAt3Gl1AvJjCPufasn4d/6NyY2n4DPzXzCgDzmEvhVdIw+tj02X8nsIPGvus7Xi8jDIgPkLhSPON5ad4t7gbletJ9S71n+BIA65C5dGal1g3nENM+weF70j1R3iNjDogfkrhn6br5dU/4UXS94EtF7+vUC8IjjzgrlRuJ33v27j2dTLbKXxd+r4Pn5RRB9RfxsL3y4H/i0dI9TFUrirOZDeuPh4B8TQqVyjH/ZtZqZ7KIRS+JJ6Fn8k4BNRL9IUPmncwbr/1b37lanbiLuMTEG+nsNfb8cOWq0DLFA7E/8yMU8AlNLnaoP/alSfKOAXEN6h8Qlo8k3UKq5bGLeAWFPa6oLT6Ogqvk3ELmPJ3wOtlgOe0B2w6//gFxCPBsjPIQN8JPi3jGDDvSDxVDPaCNnFNOdGczrNzIrEnRyR5XPZlMIdmKofnpTnx6B783QV0ARNOF9AFdAFdQBfQBXQBXUAX0AV0AV3A6OgCVjLJrEzlj5lk/jiVt2eSefvEvzTJxL84zMS/PM//Af1pBRpQAkXBAAAAAElFTkSuQmCC + + + + + + + + + + + + + +https://ca.wikipedia.org/wiki/Especial:Cerca + diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/cs/default.txt b/mobile/ios/Client/Assets/Search/SearchPlugins/cs/default.txt new file mode 100644 index 0000000000..ced95a5cb9 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/cs/default.txt @@ -0,0 +1 @@ +Google \ No newline at end of file diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/cs/heureka-cz.xml b/mobile/ios/Client/Assets/Search/SearchPlugins/cs/heureka-cz.xml new file mode 100644 index 0000000000..b01fcfc8a4 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/cs/heureka-cz.xml @@ -0,0 +1,17 @@ + + + +Heuréka +UTF-8 +data:image/png;base64,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 + + + + + + + +http://www.heureka.cz/ + diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/cs/list.txt b/mobile/ios/Client/Assets/Search/SearchPlugins/cs/list.txt new file mode 100644 index 0000000000..776e62a0eb --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/cs/list.txt @@ -0,0 +1,7 @@ +duckduckgo +google +heureka-cz +mapy-cz +seznam-cz +twitter +wikipedia-cz diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/cs/mapy-cz.xml b/mobile/ios/Client/Assets/Search/SearchPlugins/cs/mapy-cz.xml new file mode 100644 index 0000000000..5916844661 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/cs/mapy-cz.xml @@ -0,0 +1,14 @@ + + + +Mapy.cz +UTF-8 +data:image/png;base64,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 + + + + +http://www.mapy.cz/ + diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/cs/seznam-cz.xml b/mobile/ios/Client/Assets/Search/SearchPlugins/cs/seznam-cz.xml new file mode 100644 index 0000000000..d14eb2c469 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/cs/seznam-cz.xml @@ -0,0 +1,17 @@ + + + +Seznam +UTF-8 +data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAYAAADimHc4AAAACXBIWXMAAAsTAAALEwEAmpwYAAA58mlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4KPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNS42LWMwMTQgNzkuMTU2Nzk3LCAyMDE0LzA4LzIwLTA5OjUzOjAyICAgICAgICAiPgogICA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPgogICAgICA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIgogICAgICAgICAgICB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iCiAgICAgICAgICAgIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIKICAgICAgICAgICAgeG1sbnM6cGhvdG9zaG9wPSJodHRwOi8vbnMuYWRvYmUuY29tL3Bob3Rvc2hvcC8xLjAvIgogICAgICAgICAgICB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIKICAgICAgICAgICAgeG1sbnM6c3RFdnQ9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZUV2ZW50IyIKICAgICAgICAgICAgeG1sbnM6dGlmZj0iaHR0cDovL25zLmFkb2JlLmNvbS90aWZmLzEuMC8iCiAgICAgICAgICAgIHhtbG5zOmV4aWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20vZXhpZi8xLjAvIj4KICAgICAgICAgPHhtcDpDcmVhdG9yVG9vbD5BZG9iZSBQaG90b3Nob3AgQ0MgMjAxNCAoTWFjaW50b3NoKTwveG1wOkNyZWF0b3JUb29sPgogICAgICAgICA8eG1wOkNyZWF0ZURhdGU+MjAxNS0wOC0xMFQxMjo1NDo0MSswMjowMDwveG1wOkNyZWF0ZURhdGU+CiAgICAgICAgIDx4bXA6TW9kaWZ5RGF0ZT4yMDE1LTA4LTEwVDE0OjQyOjE0KzAyOjAwPC94bXA6TW9kaWZ5RGF0ZT4KICAgICAgICAgPHhtcDpNZXRhZGF0YURhdGU+MjAxNS0wOC0xMFQxNDo0MjoxNCswMjowMDwveG1wOk1ldGFkYXRhRGF0ZT4KICAgICAgICAgPGRjOmZvcm1hdD5pbWFnZS9wbmc8L2RjOmZvcm1hdD4KICAgICAgICAgPHBob3Rvc2hvcDpDb2xvck1vZGU+MzwvcGhvdG9zaG9wOkNvbG9yTW9kZT4KICAgICAgICAgPHhtcE1NOkluc3RhbmNlSUQ+eG1wLmlpZDo5NzIxNWU1OC05MWNjLTRkZmEtOGM4Zi1lMGMwZjNjMzg5ZGE8L3htcE1NOkluc3RhbmNlSUQ+CiAgICAgICAgIDx4bXBNTTpEb2N1bWVudElEPmFkb2JlOmRvY2lkOnBob3Rvc2hvcDphMDBkMDRhZS03ZmU5LTExNzgtOTk0Yi1kYzk1ZTg5ZDQ3NTU8L3htcE1NOkRvY3VtZW50SUQ+CiAgICAgICAgIDx4bXBNTTpPcmlnaW5hbERvY3VtZW50SUQ+eG1wLmRpZDplMTVmZjczMi0yMjQyLTQzMWMtYTQ0Yy02ODYwMDVkOTc5OTg8L3htcE1NOk9yaWdpbmFsRG9jdW1lbnRJRD4KICAgICAgICAgPHhtcE1NOkhpc3Rvcnk+CiAgICAgICAgICAgIDxyZGY6U2VxPgogICAgICAgICAgICAgICA8cmRmOmxpIHJkZjpwYXJzZVR5cGU9IlJlc291cmNlIj4KICAgICAgICAgICAgICAgICAgPHN0RXZ0OmFjdGlvbj5jcmVhdGVkPC9zdEV2dDphY3Rpb24+CiAgICAgICAgICAgICAgICAgIDxzdEV2dDppbnN0YW5jZUlEPnhtcC5paWQ6ZTE1ZmY3MzItMjI0Mi00MzFjLWE0NGMtNjg2MDA1ZDk3OTk4PC9zdEV2dDppbnN0YW5jZUlEPgogICAgICAgICAgICAgICAgICA8c3RFdnQ6d2hlbj4yMDE1LTA4LTEwVDEyOjU0OjQxKzAyOjAwPC9zdEV2dDp3aGVuPgogICAgICAgICAgICAgICAgICA8c3RFdnQ6c29mdHdhcmVBZ2VudD5BZG9iZSBQaG90b3Nob3AgQ0MgMjAxNCAoTWFjaW50b3NoKTwvc3RFdnQ6c29mdHdhcmVBZ2VudD4KICAgICAgICAgICAgICAgPC9yZGY6bGk+CiAgICAgICAgICAgICAgIDxyZGY6bGkgcmRmOnBhcnNlVHlwZT0iUmVzb3VyY2UiPgogICAgICAgICAgICAgICAgICA8c3RFdnQ6YWN0aW9uPnNhdmVkPC9zdEV2dDphY3Rpb24+CiAgICAgICAgICAgICAgICAgIDxzdEV2dDppbnN0YW5jZUlEPnhtcC5paWQ6OTcyMTVlNTgtOTFjYy00ZGZhLThjOGYtZTBjMGYzYzM4OWRhPC9zdEV2dDppbnN0YW5jZUlEPgogICAgICAgICAgICAgICAgICA8c3RFdnQ6d2hlbj4yMDE1LTA4LTEwVDE0OjQyOjE0KzAyOjAwPC9zdEV2dDp3aGVuPgogICAgICAgICAgICAgICAgICA8c3RFdnQ6c29mdHdhcmVBZ2VudD5BZG9iZSBQaG90b3Nob3AgQ0MgMjAxNCAoTWFjaW50b3NoKTwvc3RFdnQ6c29mdHdhcmVBZ2VudD4KICAgICAgICAgICAgICAgICAgPHN0RXZ0OmNoYW5nZWQ+Lzwvc3RFdnQ6Y2hhbmdlZD4KICAgICAgICAgICAgICAgPC9yZGY6bGk+CiAgICAgICAgICAgIDwvcmRmOlNlcT4KICAgICAgICAgPC94bXBNTTpIaXN0b3J5PgogICAgICAgICA8dGlmZjpPcmllbnRhdGlvbj4xPC90aWZmOk9yaWVudGF0aW9uPgogICAgICAgICA8dGlmZjpYUmVzb2x1dGlvbj43MjAwMDAvMTAwMDA8L3RpZmY6WFJlc29sdXRpb24+CiAgICAgICAgIDx0aWZmOllSZXNvbHV0aW9uPjcyMDAwMC8xMDAwMDwvdGlmZjpZUmVzb2x1dGlvbj4KICAgICAgICAgPHRpZmY6UmVzb2x1dGlvblVuaXQ+MjwvdGlmZjpSZXNvbHV0aW9uVW5pdD4KICAgICAgICAgPGV4aWY6Q29sb3JTcGFjZT42NTUzNTwvZXhpZjpDb2xvclNwYWNlPgogICAgICAgICA8ZXhpZjpQaXhlbFhEaW1lbnNpb24+OTY8L2V4aWY6UGl4ZWxYRGltZW5zaW9uPgogICAgICAgICA8ZXhpZjpQaXhlbFlEaW1lbnNpb24+OTY8L2V4aWY6UGl4ZWxZRGltZW5zaW9uPgogICAgICA8L3JkZjpEZXNjcmlwdGlvbj4KICAgPC9yZGY6UkRGPgo8L3g6eG1wbWV0YT4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAKPD94cGFja2V0IGVuZD0idyI/PqvEAjkAAAAgY0hSTQAAbXUAAHOgAAD83QAAg2QAAHDoAADsaAAAMD4AABCQ5OyZ6gAAB5ZJREFUeNrsnW1MW9Ufxz/3QktbWsrTHliU8TSHE1h9GCMsODaCYZqgU3QLIzjnXBY1McaYGLMlf5NlL3xjsvhGw7bMABqBjGyJGhQ3lWTAlmzE8CDMQec2mAPa0ZZCC60v6NR/4nja7QP0fJP7hlzOvff7Ofec3znnd26lS/wtCXgd2A9sAAwIKSkb0AVUAccBL4DkA7AaqAaKhE8BUTNQAQzJvpovzA+sinyeS7KvyRHmBwfC67Kv3RcKjvbLvg5XKDjaIItoJ6gyyMKD4EoAEAAEACEBQAAQEgAEACEBQAAQEgAEACEBQFlps7NJ+eILTKOjRG/eHFL3Jl3yrU0uO9Mfe4zYF14gdudOdE88AZIEgHtwkO6nnsJ965YAoNhDqFRoc3KIzs1Fv2UL+q1bUT/00H3Pt7e00FtYiHd6WgBYcJup16PLyUFrMqEzmdA9/jja7GykqKgFlXPr8GEG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 + + + + + + + +http://search.seznam.cz/ + diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/cs/wikipedia-cz.xml b/mobile/ios/Client/Assets/Search/SearchPlugins/cs/wikipedia-cz.xml new file mode 100644 index 0000000000..36b15cde86 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/cs/wikipedia-cz.xml @@ -0,0 +1,23 @@ + + + +Wikipedie (cs) +UTF-8 +data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAQAAABIkb+zAAAEXElEQVR4Ae3cA4xs2QJG4b9aj41n27Zt27Zt27Zt27Zt+2KM5lXP924qOzs56VPjKSRnrfhv7NUsp08v98+PsxwT4nJ+nPunl8KZ8/WYQL+eMydJrx5/EhN603lAHpVJ5bzZNp035uyZXM7Uy3LmM7ms9CITzVRGQRfQBXQBXUAX0AV0AV1AF9AFdAFdQBfQBXQBXcBMjgU7M5+1vqtZyXqmc/IsZDEL+53LP3KRPCCv62+rWd7vnpx6v/M5Vd935OF5R+6RtRyeQ3NYNjKTxZw2p8tiduR8uWfemY3+dkjWM5fFLPU/8q9yrRwbHLP39WkHamPD5z1WnNMb/dpW/uAdbipu413+qcl2H3N3cXFv9DNNtvmg68uxMY6dPfewTZNvOauouqEdULlTY532IgqHua+ZxnoHm4ANj63biRNQPZedVPY5nTR1BXsVbNuyTlsDqy4tW/wK4NYiJ01AXMlm+wGrL1cDnWbLejC4m7T4DvAOOSkD4v0qztGyLzpUweOk4UXAp6XV72DNmU7qgPPao+DJ0uLTFPzXXGN5I9acU1o8vb14jZzUAfFeBdvMtuwLDlfwIKme2248T1p9MvY59zACLqcy4Kf5eQr+ZbYRfrAFaXHaf/FRGUZAfE/Bzwf8OKwpuJ/0vYRNPEpavT248rACbqfimtLiqxT8w4yIz+Of5qTV7+LHMqyAaf9R8Clp8ex2K7iXuAa4q7R6aXCH4QXE4xVsOq+0+DYFfzXth/ilnrT6Tvzb9DADlqwqDPjTd377FHwI3EBaPb0NPFaGGRBvoLBiUVr8EFS+KgN8KpYtDjvgQo6i8Hhp8ZJUjnIZaXXGNrxKhh0QX6TwHzPS4uco/FtPWr0T9jnPKAJuOOiCc/UqVG4jrf4AH5dRBPT8UWHg3/BvUfhl635ZcNXRBMSDqFxFWvwklVu07O/BT2RUAad0GIWPtexXgMpPt+xntAt3Gl1AvJjCPufasn4d/6NyY2n4DPzXzCgDzmEvhVdIw+tj02X8nsIPGvus7Xi8jDIgPkLhSPON5ad4t7gbletJ9S71n+BIA65C5dGal1g3nENM+weF70j1R3iNjDogfkrhn6br5dU/4UXS94EtF7+vUC8IjjzgrlRuJ33v27j2dTLbKXxd+r4Pn5RRB9RfxsL3y4H/i0dI9TFUrirOZDeuPh4B8TQqVyjH/ZtZqZ7KIRS+JJ6Fn8k4BNRL9IUPmncwbr/1b37lanbiLuMTEG+nsNfb8cOWq0DLFA7E/8yMU8AlNLnaoP/alSfKOAXEN6h8Qlo8k3UKq5bGLeAWFPa6oLT6Ogqvk3ELmPJ3wOtlgOe0B2w6//gFxCPBsjPIQN8JPi3jGDDvSDxVDPaCNnFNOdGczrNzIrEnRyR5XPZlMIdmKofnpTnx6B783QV0ARNOF9AFdAFdQBfQBXQBXUAX0AV0AV3A6OgCVjLJrEzlj5lk/jiVt2eSefvEvzTJxL84zMS/PM//Af1pBRpQAkXBAAAAAElFTkSuQmCC + + + + + + + + + + + + + +https://cs.wikipedia.org/wiki/Speciální:Hledání + diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/cy/amazon-en-GB.xml b/mobile/ios/Client/Assets/Search/SearchPlugins/cy/amazon-en-GB.xml new file mode 100644 index 0000000000..a63eb0caa3 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/cy/amazon-en-GB.xml @@ -0,0 +1,12 @@ + +Amazon.co.uk +ISO-8859-1 +data:image/png;base64,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 + + + + + + +https://www.amazon.co.uk/ + diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/cy/default.txt b/mobile/ios/Client/Assets/Search/SearchPlugins/cy/default.txt new file mode 100644 index 0000000000..ced95a5cb9 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/cy/default.txt @@ -0,0 +1 @@ +Google \ No newline at end of file diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/cy/list.txt b/mobile/ios/Client/Assets/Search/SearchPlugins/cy/list.txt new file mode 100644 index 0000000000..84e480939f --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/cy/list.txt @@ -0,0 +1,6 @@ +amazon-en-GB +bing +duckduckgo +google +wikipedia-cy +yahoo-en-GB diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/cy/wikipedia-cy.xml b/mobile/ios/Client/Assets/Search/SearchPlugins/cy/wikipedia-cy.xml new file mode 100644 index 0000000000..6a4da7037f --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/cy/wikipedia-cy.xml @@ -0,0 +1,23 @@ + + + +Wicipedia (cy) +UTF-8 +data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAQAAABIkb+zAAAEXElEQVR4Ae3cA4xs2QJG4b9aj41n27Zt27Zt27Zt27Zt+2KM5lXP924qOzs56VPjKSRnrfhv7NUsp08v98+PsxwT4nJ+nPunl8KZ8/WYQL+eMydJrx5/EhN603lAHpVJ5bzZNp035uyZXM7Uy3LmM7ms9CITzVRGQRfQBXQBXUAX0AV0AV1AF9AFdAFdQBfQBXQBXcBMjgU7M5+1vqtZyXqmc/IsZDEL+53LP3KRPCCv62+rWd7vnpx6v/M5Vd935OF5R+6RtRyeQ3NYNjKTxZw2p8tiduR8uWfemY3+dkjWM5fFLPU/8q9yrRwbHLP39WkHamPD5z1WnNMb/dpW/uAdbipu413+qcl2H3N3cXFv9DNNtvmg68uxMY6dPfewTZNvOauouqEdULlTY532IgqHua+ZxnoHm4ANj63biRNQPZedVPY5nTR1BXsVbNuyTlsDqy4tW/wK4NYiJ01AXMlm+wGrL1cDnWbLejC4m7T4DvAOOSkD4v0qztGyLzpUweOk4UXAp6XV72DNmU7qgPPao+DJ0uLTFPzXXGN5I9acU1o8vb14jZzUAfFeBdvMtuwLDlfwIKme2248T1p9MvY59zACLqcy4Kf5eQr+ZbYRfrAFaXHaf/FRGUZAfE/Bzwf8OKwpuJ/0vYRNPEpavT248rACbqfimtLiqxT8w4yIz+Of5qTV7+LHMqyAaf9R8Clp8ex2K7iXuAa4q7R6aXCH4QXE4xVsOq+0+DYFfzXth/ilnrT6Tvzb9DADlqwqDPjTd377FHwI3EBaPb0NPFaGGRBvoLBiUVr8EFS+KgN8KpYtDjvgQo6i8Hhp8ZJUjnIZaXXGNrxKhh0QX6TwHzPS4uco/FtPWr0T9jnPKAJuOOiCc/UqVG4jrf4AH5dRBPT8UWHg3/BvUfhl635ZcNXRBMSDqFxFWvwklVu07O/BT2RUAad0GIWPtexXgMpPt+xntAt3Gl1AvJjCPufasn4d/6NyY2n4DPzXzCgDzmEvhVdIw+tj02X8nsIPGvus7Xi8jDIgPkLhSPON5ad4t7gbletJ9S71n+BIA65C5dGal1g3nENM+weF70j1R3iNjDogfkrhn6br5dU/4UXS94EtF7+vUC8IjjzgrlRuJ33v27j2dTLbKXxd+r4Pn5RRB9RfxsL3y4H/i0dI9TFUrirOZDeuPh4B8TQqVyjH/ZtZqZ7KIRS+JJ6Fn8k4BNRL9IUPmncwbr/1b37lanbiLuMTEG+nsNfb8cOWq0DLFA7E/8yMU8AlNLnaoP/alSfKOAXEN6h8Qlo8k3UKq5bGLeAWFPa6oLT6Ogqvk3ELmPJ3wOtlgOe0B2w6//gFxCPBsjPIQN8JPi3jGDDvSDxVDPaCNnFNOdGczrNzIrEnRyR5XPZlMIdmKofnpTnx6B783QV0ARNOF9AFdAFdQBfQBXQBXUAX0AV0AV3A6OgCVjLJrEzlj5lk/jiVt2eSefvEvzTJxL84zMS/PM//Af1pBRpQAkXBAAAAAElFTkSuQmCC + + + + + + + + + + + + + +https://cy.wikipedia.org/wiki/Arbennig:Search + diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/cy/yahoo-en-GB.xml b/mobile/ios/Client/Assets/Search/SearchPlugins/cy/yahoo-en-GB.xml new file mode 100644 index 0000000000..91713ad634 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/cy/yahoo-en-GB.xml @@ -0,0 +1,22 @@ + + + +Yahoo.co.uk +UTF-8 +data:image/png;base64,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 + + + + + + + + + + + + +https://uk.search.yahoo.com/ + diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/da/amazon-co-uk.xml b/mobile/ios/Client/Assets/Search/SearchPlugins/da/amazon-co-uk.xml new file mode 100644 index 0000000000..3a3fbabb19 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/da/amazon-co-uk.xml @@ -0,0 +1,16 @@ + + + +Amazon.co.uk +ISO-8859-1 +data:image/png;base64,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 + + + + + + +https://www.amazon.co.uk/ + diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/da/default.txt b/mobile/ios/Client/Assets/Search/SearchPlugins/da/default.txt new file mode 100644 index 0000000000..ced95a5cb9 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/da/default.txt @@ -0,0 +1 @@ +Google \ No newline at end of file diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/da/list.txt b/mobile/ios/Client/Assets/Search/SearchPlugins/da/list.txt new file mode 100644 index 0000000000..3108316eb0 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/da/list.txt @@ -0,0 +1,4 @@ +amazon-co-uk +google +twitter +wikipedia-da diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/da/wikipedia-da.xml b/mobile/ios/Client/Assets/Search/SearchPlugins/da/wikipedia-da.xml new file mode 100644 index 0000000000..a6c740de48 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/da/wikipedia-da.xml @@ -0,0 +1,23 @@ + + + +Wikipedia (da) +UTF-8 +data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAQAAABIkb+zAAAEXElEQVR4Ae3cA4xs2QJG4b9aj41n27Zt27Zt27Zt27Zt+2KM5lXP924qOzs56VPjKSRnrfhv7NUsp08v98+PsxwT4nJ+nPunl8KZ8/WYQL+eMydJrx5/EhN603lAHpVJ5bzZNp035uyZXM7Uy3LmM7ms9CITzVRGQRfQBXQBXUAX0AV0AV1AF9AFdAFdQBfQBXQBXcBMjgU7M5+1vqtZyXqmc/IsZDEL+53LP3KRPCCv62+rWd7vnpx6v/M5Vd935OF5R+6RtRyeQ3NYNjKTxZw2p8tiduR8uWfemY3+dkjWM5fFLPU/8q9yrRwbHLP39WkHamPD5z1WnNMb/dpW/uAdbipu413+qcl2H3N3cXFv9DNNtvmg68uxMY6dPfewTZNvOauouqEdULlTY532IgqHua+ZxnoHm4ANj63biRNQPZedVPY5nTR1BXsVbNuyTlsDqy4tW/wK4NYiJ01AXMlm+wGrL1cDnWbLejC4m7T4DvAOOSkD4v0qztGyLzpUweOk4UXAp6XV72DNmU7qgPPao+DJ0uLTFPzXXGN5I9acU1o8vb14jZzUAfFeBdvMtuwLDlfwIKme2248T1p9MvY59zACLqcy4Kf5eQr+ZbYRfrAFaXHaf/FRGUZAfE/Bzwf8OKwpuJ/0vYRNPEpavT248rACbqfimtLiqxT8w4yIz+Of5qTV7+LHMqyAaf9R8Clp8ex2K7iXuAa4q7R6aXCH4QXE4xVsOq+0+DYFfzXth/ilnrT6Tvzb9DADlqwqDPjTd377FHwI3EBaPb0NPFaGGRBvoLBiUVr8EFS+KgN8KpYtDjvgQo6i8Hhp8ZJUjnIZaXXGNrxKhh0QX6TwHzPS4uco/FtPWr0T9jnPKAJuOOiCc/UqVG4jrf4AH5dRBPT8UWHg3/BvUfhl635ZcNXRBMSDqFxFWvwklVu07O/BT2RUAad0GIWPtexXgMpPt+xntAt3Gl1AvJjCPufasn4d/6NyY2n4DPzXzCgDzmEvhVdIw+tj02X8nsIPGvus7Xi8jDIgPkLhSPON5ad4t7gbletJ9S71n+BIA65C5dGal1g3nENM+weF70j1R3iNjDogfkrhn6br5dU/4UXS94EtF7+vUC8IjjzgrlRuJ33v27j2dTLbKXxd+r4Pn5RRB9RfxsL3y4H/i0dI9TFUrirOZDeuPh4B8TQqVyjH/ZtZqZ7KIRS+JJ6Fn8k4BNRL9IUPmncwbr/1b37lanbiLuMTEG+nsNfb8cOWq0DLFA7E/8yMU8AlNLnaoP/alSfKOAXEN6h8Qlo8k3UKq5bGLeAWFPa6oLT6Ogqvk3ELmPJ3wOtlgOe0B2w6//gFxCPBsjPIQN8JPi3jGDDvSDxVDPaCNnFNOdGczrNzIrEnRyR5XPZlMIdmKofnpTnx6B783QV0ARNOF9AFdAFdQBfQBXQBXUAX0AV0AV3A6OgCVjLJrEzlj5lk/jiVt2eSefvEvzTJxL84zMS/PM//Af1pBRpQAkXBAAAAAElFTkSuQmCC + + + + + + + + + + + + + +https://da.wikipedia.org/wiki/Speciel:Søgning + diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/de/amazon-de.xml b/mobile/ios/Client/Assets/Search/SearchPlugins/de/amazon-de.xml new file mode 100644 index 0000000000..1253945732 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/de/amazon-de.xml @@ -0,0 +1,12 @@ + +Amazon.de +ISO-8859-1 +data:image/png;base64,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 + + + + + + +https://www.amazon.de/ + diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/de/default.txt b/mobile/ios/Client/Assets/Search/SearchPlugins/de/default.txt new file mode 100644 index 0000000000..ced95a5cb9 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/de/default.txt @@ -0,0 +1 @@ +Google \ No newline at end of file diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/de/list.txt b/mobile/ios/Client/Assets/Search/SearchPlugins/de/list.txt new file mode 100644 index 0000000000..01536d292d --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/de/list.txt @@ -0,0 +1,8 @@ +amazon-de +bing +duckduckgo +google +qwant +twitter +wikipedia-de +yahoo-de diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/de/wikipedia-de.xml b/mobile/ios/Client/Assets/Search/SearchPlugins/de/wikipedia-de.xml new file mode 100644 index 0000000000..d576b753ac --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/de/wikipedia-de.xml @@ -0,0 +1,23 @@ + + + +Wikipedia (de) +UTF-8 +data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAQAAABIkb+zAAAEXElEQVR4Ae3cA4xs2QJG4b9aj41n27Zt27Zt27Zt27Zt+2KM5lXP924qOzs56VPjKSRnrfhv7NUsp08v98+PsxwT4nJ+nPunl8KZ8/WYQL+eMydJrx5/EhN603lAHpVJ5bzZNp035uyZXM7Uy3LmM7ms9CITzVRGQRfQBXQBXUAX0AV0AV1AF9AFdAFdQBfQBXQBXcBMjgU7M5+1vqtZyXqmc/IsZDEL+53LP3KRPCCv62+rWd7vnpx6v/M5Vd935OF5R+6RtRyeQ3NYNjKTxZw2p8tiduR8uWfemY3+dkjWM5fFLPU/8q9yrRwbHLP39WkHamPD5z1WnNMb/dpW/uAdbipu413+qcl2H3N3cXFv9DNNtvmg68uxMY6dPfewTZNvOauouqEdULlTY532IgqHua+ZxnoHm4ANj63biRNQPZedVPY5nTR1BXsVbNuyTlsDqy4tW/wK4NYiJ01AXMlm+wGrL1cDnWbLejC4m7T4DvAOOSkD4v0qztGyLzpUweOk4UXAp6XV72DNmU7qgPPao+DJ0uLTFPzXXGN5I9acU1o8vb14jZzUAfFeBdvMtuwLDlfwIKme2248T1p9MvY59zACLqcy4Kf5eQr+ZbYRfrAFaXHaf/FRGUZAfE/Bzwf8OKwpuJ/0vYRNPEpavT248rACbqfimtLiqxT8w4yIz+Of5qTV7+LHMqyAaf9R8Clp8ex2K7iXuAa4q7R6aXCH4QXE4xVsOq+0+DYFfzXth/ilnrT6Tvzb9DADlqwqDPjTd377FHwI3EBaPb0NPFaGGRBvoLBiUVr8EFS+KgN8KpYtDjvgQo6i8Hhp8ZJUjnIZaXXGNrxKhh0QX6TwHzPS4uco/FtPWr0T9jnPKAJuOOiCc/UqVG4jrf4AH5dRBPT8UWHg3/BvUfhl635ZcNXRBMSDqFxFWvwklVu07O/BT2RUAad0GIWPtexXgMpPt+xntAt3Gl1AvJjCPufasn4d/6NyY2n4DPzXzCgDzmEvhVdIw+tj02X8nsIPGvus7Xi8jDIgPkLhSPON5ad4t7gbletJ9S71n+BIA65C5dGal1g3nENM+weF70j1R3iNjDogfkrhn6br5dU/4UXS94EtF7+vUC8IjjzgrlRuJ33v27j2dTLbKXxd+r4Pn5RRB9RfxsL3y4H/i0dI9TFUrirOZDeuPh4B8TQqVyjH/ZtZqZ7KIRS+JJ6Fn8k4BNRL9IUPmncwbr/1b37lanbiLuMTEG+nsNfb8cOWq0DLFA7E/8yMU8AlNLnaoP/alSfKOAXEN6h8Qlo8k3UKq5bGLeAWFPa6oLT6Ogqvk3ELmPJ3wOtlgOe0B2w6//gFxCPBsjPIQN8JPi3jGDDvSDxVDPaCNnFNOdGczrNzIrEnRyR5XPZlMIdmKofnpTnx6B783QV0ARNOF9AFdAFdQBfQBXQBXUAX0AV0AV3A6OgCVjLJrEzlj5lk/jiVt2eSefvEvzTJxL84zMS/PM//Af1pBRpQAkXBAAAAAElFTkSuQmCC + + + + + + + + + + + + + +https://de.wikipedia.org/wiki/Spezial:Suche + diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/de/yahoo-de.xml b/mobile/ios/Client/Assets/Search/SearchPlugins/de/yahoo-de.xml new file mode 100644 index 0000000000..04b36eea83 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/de/yahoo-de.xml @@ -0,0 +1,22 @@ + + + +Yahoo +UTF-8 +data:image/png;base64,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 + + + + + + + + + + + + +https://de.search.yahoo.com/ + diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/dsb/amazon-de.xml b/mobile/ios/Client/Assets/Search/SearchPlugins/dsb/amazon-de.xml new file mode 100644 index 0000000000..d19cc3133a --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/dsb/amazon-de.xml @@ -0,0 +1,16 @@ + + + +Amazon.de +ISO-8859-1 +data:image/png;base64,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 + + + + + + +https://www.amazon.de/ + diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/dsb/default.txt b/mobile/ios/Client/Assets/Search/SearchPlugins/dsb/default.txt new file mode 100644 index 0000000000..ced95a5cb9 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/dsb/default.txt @@ -0,0 +1 @@ +Google \ No newline at end of file diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/dsb/list.txt b/mobile/ios/Client/Assets/Search/SearchPlugins/dsb/list.txt new file mode 100644 index 0000000000..0ed2c032aa --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/dsb/list.txt @@ -0,0 +1,6 @@ +amazon-de +bing +google +twitter +wikipedia-dsb +yahoo-de diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/dsb/wikipedia-dsb.xml b/mobile/ios/Client/Assets/Search/SearchPlugins/dsb/wikipedia-dsb.xml new file mode 100644 index 0000000000..a4a8a16f20 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/dsb/wikipedia-dsb.xml @@ -0,0 +1,23 @@ + + + +Wikipedija (dsb) +UTF-8 +data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAQAAABIkb+zAAAEXElEQVR4Ae3cA4xs2QJG4b9aj41n27Zt27Zt27Zt27Zt+2KM5lXP924qOzs56VPjKSRnrfhv7NUsp08v98+PsxwT4nJ+nPunl8KZ8/WYQL+eMydJrx5/EhN603lAHpVJ5bzZNp035uyZXM7Uy3LmM7ms9CITzVRGQRfQBXQBXUAX0AV0AV1AF9AFdAFdQBfQBXQBXcBMjgU7M5+1vqtZyXqmc/IsZDEL+53LP3KRPCCv62+rWd7vnpx6v/M5Vd935OF5R+6RtRyeQ3NYNjKTxZw2p8tiduR8uWfemY3+dkjWM5fFLPU/8q9yrRwbHLP39WkHamPD5z1WnNMb/dpW/uAdbipu413+qcl2H3N3cXFv9DNNtvmg68uxMY6dPfewTZNvOauouqEdULlTY532IgqHua+ZxnoHm4ANj63biRNQPZedVPY5nTR1BXsVbNuyTlsDqy4tW/wK4NYiJ01AXMlm+wGrL1cDnWbLejC4m7T4DvAOOSkD4v0qztGyLzpUweOk4UXAp6XV72DNmU7qgPPao+DJ0uLTFPzXXGN5I9acU1o8vb14jZzUAfFeBdvMtuwLDlfwIKme2248T1p9MvY59zACLqcy4Kf5eQr+ZbYRfrAFaXHaf/FRGUZAfE/Bzwf8OKwpuJ/0vYRNPEpavT248rACbqfimtLiqxT8w4yIz+Of5qTV7+LHMqyAaf9R8Clp8ex2K7iXuAa4q7R6aXCH4QXE4xVsOq+0+DYFfzXth/ilnrT6Tvzb9DADlqwqDPjTd377FHwI3EBaPb0NPFaGGRBvoLBiUVr8EFS+KgN8KpYtDjvgQo6i8Hhp8ZJUjnIZaXXGNrxKhh0QX6TwHzPS4uco/FtPWr0T9jnPKAJuOOiCc/UqVG4jrf4AH5dRBPT8UWHg3/BvUfhl635ZcNXRBMSDqFxFWvwklVu07O/BT2RUAad0GIWPtexXgMpPt+xntAt3Gl1AvJjCPufasn4d/6NyY2n4DPzXzCgDzmEvhVdIw+tj02X8nsIPGvus7Xi8jDIgPkLhSPON5ad4t7gbletJ9S71n+BIA65C5dGal1g3nENM+weF70j1R3iNjDogfkrhn6br5dU/4UXS94EtF7+vUC8IjjzgrlRuJ33v27j2dTLbKXxd+r4Pn5RRB9RfxsL3y4H/i0dI9TFUrirOZDeuPh4B8TQqVyjH/ZtZqZ7KIRS+JJ6Fn8k4BNRL9IUPmncwbr/1b37lanbiLuMTEG+nsNfb8cOWq0DLFA7E/8yMU8AlNLnaoP/alSfKOAXEN6h8Qlo8k3UKq5bGLeAWFPa6oLT6Ogqvk3ELmPJ3wOtlgOe0B2w6//gFxCPBsjPIQN8JPi3jGDDvSDxVDPaCNnFNOdGczrNzIrEnRyR5XPZlMIdmKofnpTnx6B783QV0ARNOF9AFdAFdQBfQBXQBXUAX0AV0AV3A6OgCVjLJrEzlj5lk/jiVt2eSefvEvzTJxL84zMS/PM//Af1pBRpQAkXBAAAAAElFTkSuQmCC + + + + + + + + + + + + + +https://dsb.wikipedia.org/wiki/Specialne:Pytaś + diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/dsb/yahoo-de.xml b/mobile/ios/Client/Assets/Search/SearchPlugins/dsb/yahoo-de.xml new file mode 100644 index 0000000000..04b36eea83 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/dsb/yahoo-de.xml @@ -0,0 +1,22 @@ + + + +Yahoo +UTF-8 +data:image/png;base64,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 + + + + + + + + + + + + +https://de.search.yahoo.com/ + diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/el/default.txt b/mobile/ios/Client/Assets/Search/SearchPlugins/el/default.txt new file mode 100644 index 0000000000..ced95a5cb9 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/el/default.txt @@ -0,0 +1 @@ +Google \ No newline at end of file diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/el/list.txt b/mobile/ios/Client/Assets/Search/SearchPlugins/el/list.txt new file mode 100644 index 0000000000..3da7e54726 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/el/list.txt @@ -0,0 +1,4 @@ +google +twitter +wikipedia-el +skroutz diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/el/skroutz.xml b/mobile/ios/Client/Assets/Search/SearchPlugins/el/skroutz.xml new file mode 100644 index 0000000000..7fa892102a --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/el/skroutz.xml @@ -0,0 +1,15 @@ + + + +Skroutz +Ενσωματωμένη Αναζήτηση στο www.skroutz.gr +UTF-8 +data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAYAAADimHc4AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAA2hpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMC1jMDYxIDY0LjE0MDk0OSwgMjAxMC8xMi8wNy0xMDo1NzowMSAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtcE1NOk9yaWdpbmFsRG9jdW1lbnRJRD0ieG1wLmRpZDowMTgwMTE3NDA3MjA2ODExOEE2REI2ODhGMkQ2RTdCRSIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDozOTM2Q0Q2NjNDREQxMUU1Qjk0RUFGQzRBMEY2MzVFQyIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDozOTM2Q0Q2NTNDREQxMUU1Qjk0RUFGQzRBMEY2MzVFQyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ1M1LjEgTWFjaW50b3NoIj4gPHhtcE1NOkRlcml2ZWRGcm9tIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6MDM4MDExNzQwNzIwNjgxMUI0NTZBNEVEM0I1NUYwMzUiIHN0UmVmOmRvY3VtZW50SUQ9InhtcC5kaWQ6MDE4MDExNzQwNzIwNjgxMThBNkRCNjg4RjJENkU3QkUiLz4gPC9yZGY6RGVzY3Jp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 +http://www.skroutz.gr/ + + + + + diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/el/wikipedia-el.xml b/mobile/ios/Client/Assets/Search/SearchPlugins/el/wikipedia-el.xml new file mode 100644 index 0000000000..64fcb3caec --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/el/wikipedia-el.xml @@ -0,0 +1,23 @@ + + + +Wikipedia (el) +UTF-8 +data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAQAAABIkb+zAAAEXElEQVR4Ae3cA4xs2QJG4b9aj41n27Zt27Zt27Zt27Zt+2KM5lXP924qOzs56VPjKSRnrfhv7NUsp08v98+PsxwT4nJ+nPunl8KZ8/WYQL+eMydJrx5/EhN603lAHpVJ5bzZNp035uyZXM7Uy3LmM7ms9CITzVRGQRfQBXQBXUAX0AV0AV1AF9AFdAFdQBfQBXQBXcBMjgU7M5+1vqtZyXqmc/IsZDEL+53LP3KRPCCv62+rWd7vnpx6v/M5Vd935OF5R+6RtRyeQ3NYNjKTxZw2p8tiduR8uWfemY3+dkjWM5fFLPU/8q9yrRwbHLP39WkHamPD5z1WnNMb/dpW/uAdbipu413+qcl2H3N3cXFv9DNNtvmg68uxMY6dPfewTZNvOauouqEdULlTY532IgqHua+ZxnoHm4ANj63biRNQPZedVPY5nTR1BXsVbNuyTlsDqy4tW/wK4NYiJ01AXMlm+wGrL1cDnWbLejC4m7T4DvAOOSkD4v0qztGyLzpUweOk4UXAp6XV72DNmU7qgPPao+DJ0uLTFPzXXGN5I9acU1o8vb14jZzUAfFeBdvMtuwLDlfwIKme2248T1p9MvY59zACLqcy4Kf5eQr+ZbYRfrAFaXHaf/FRGUZAfE/Bzwf8OKwpuJ/0vYRNPEpavT248rACbqfimtLiqxT8w4yIz+Of5qTV7+LHMqyAaf9R8Clp8ex2K7iXuAa4q7R6aXCH4QXE4xVsOq+0+DYFfzXth/ilnrT6Tvzb9DADlqwqDPjTd377FHwI3EBaPb0NPFaGGRBvoLBiUVr8EFS+KgN8KpYtDjvgQo6i8Hhp8ZJUjnIZaXXGNrxKhh0QX6TwHzPS4uco/FtPWr0T9jnPKAJuOOiCc/UqVG4jrf4AH5dRBPT8UWHg3/BvUfhl635ZcNXRBMSDqFxFWvwklVu07O/BT2RUAad0GIWPtexXgMpPt+xntAt3Gl1AvJjCPufasn4d/6NyY2n4DPzXzCgDzmEvhVdIw+tj02X8nsIPGvus7Xi8jDIgPkLhSPON5ad4t7gbletJ9S71n+BIA65C5dGal1g3nENM+weF70j1R3iNjDogfkrhn6br5dU/4UXS94EtF7+vUC8IjjzgrlRuJ33v27j2dTLbKXxd+r4Pn5RRB9RfxsL3y4H/i0dI9TFUrirOZDeuPh4B8TQqVyjH/ZtZqZ7KIRS+JJ6Fn8k4BNRL9IUPmncwbr/1b37lanbiLuMTEG+nsNfb8cOWq0DLFA7E/8yMU8AlNLnaoP/alSfKOAXEN6h8Qlo8k3UKq5bGLeAWFPa6oLT6Ogqvk3ELmPJ3wOtlgOe0B2w6//gFxCPBsjPIQN8JPi3jGDDvSDxVDPaCNnFNOdGczrNzIrEnRyR5XPZlMIdmKofnpTnx6B783QV0ARNOF9AFdAFdQBfQBXQBXUAX0AV0AV3A6OgCVjLJrEzlj5lk/jiVt2eSefvEvzTJxL84zMS/PM//Af1pBRpQAkXBAAAAAElFTkSuQmCC + + + + + + + + + + + + + +https://el.wikipedia.org/wiki/Ειδικό:Αναζήτηση + diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/en-GB/amazon-en-GB.xml b/mobile/ios/Client/Assets/Search/SearchPlugins/en-GB/amazon-en-GB.xml new file mode 100644 index 0000000000..3a3fbabb19 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/en-GB/amazon-en-GB.xml @@ -0,0 +1,16 @@ + + + +Amazon.co.uk +ISO-8859-1 +data:image/png;base64,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 + + + + + + +https://www.amazon.co.uk/ + diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/en-GB/default.txt b/mobile/ios/Client/Assets/Search/SearchPlugins/en-GB/default.txt new file mode 100644 index 0000000000..ced95a5cb9 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/en-GB/default.txt @@ -0,0 +1 @@ +Google \ No newline at end of file diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/en-GB/list.txt b/mobile/ios/Client/Assets/Search/SearchPlugins/en-GB/list.txt new file mode 100644 index 0000000000..28c00428fb --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/en-GB/list.txt @@ -0,0 +1,8 @@ +amazon-en-GB +bing +duckduckgo +google +qwant +twitter +wikipedia +yahoo-en-GB diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/en-GB/yahoo-en-GB.xml b/mobile/ios/Client/Assets/Search/SearchPlugins/en-GB/yahoo-en-GB.xml new file mode 100644 index 0000000000..91713ad634 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/en-GB/yahoo-en-GB.xml @@ -0,0 +1,22 @@ + + + +Yahoo.co.uk +UTF-8 +data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAYAAADimHc4AAAAAXNSR0IArs4c6QAACJ5JREFUeAHtXWlsVFUUPm9mOt2gLWKXIFtLKRZIGoIS/CEJa7ASEExdECPYVhYjiAlq0CAgIJBIABUFWkqKEaGiEKRAoAIKBEGBmNJCW2ihQGnL0oW2dJmO9wyly3DPw/bN2+SehPS9c+bee873vbvf95CgSZzglOIgPZ7dJjgB+rO/nR/YxF+PIFApAWSxnJLSIDZZAonBDMB0AHFwKMwJNd8zEkbivRB1EWDgZ0jgOyUNht+w4JMvwFcXcPfc8UFvwlySXob0RADnBvcfiXv1EWDNT6JFAie2+0L0QSCBNUGuDlef4h/zUhF7C8NAjHb0exA6IwFCdERAEKAj+Fi0IEAQoDMCOhcvaoAgQGcEdC5e1ABBgM4I6Fy8qAGCAJ0R0Ll4UQN0JsCmdvmDYoMh8tlAuH6hCipK61xbQE9084GQcF8IjfCDAxsK4cKxO2q7wc1/8LgQwH/Fl6qhtKAaHA1OsNkt4O1vheCevvBPxk3I/l1d31Qn4OKpcpidGgOdu9q5IPSOCYAPBx+FRgfXrJrS5m2B+K/6Q0hvP24ZJYyQX1Zc5No8qVS9CcKnPnXeedJnJGBUYk/SrpZh7Lu9SPCxzOTZWVBX3ahW8c35qk4AlnQo5SpkHr7VXKj7xWufR4F/F9UrY3OxASF2iFsQ2XzvfnFyVzH8vbvEXa3KvSYEoOfrp2dCfS2/nQl40g6vLIxSJUBepm980Q/8A714JqitdsCm2ee4NjWUmhFQlFMFO5bRberYWT2he/9OasTYJs/IIYEwYlr3NrrWN2mLc+HmlXutVapea0YARrFz+UW4dv4uNyCrzQLTVkdzbZ5SSizahK8HgCS5TuM8lG1hViXsXpX/kF5NhaYENNQ5Yf2MTDKemNHB8Mz4ENKu1DDi7R5sSBxEZrNhZiY46l3npcjfeNqgKQHofNaR23BkyzUyjqmrogGHiJ4W7OSx7afkcOpV1cf8vLI9HymvFDdd6rxsqCqvd9Pevw3r4w8T5oVzbUqUk5f1A+zseXL3Tr3sUJmXxlM6XQgoL66DrZ/kkDFMmh8Jwb19SXt7Ddjxjn6Hnmv8MP8CVJSwWboOogsBGOf+by/DpdPl3JC9fa2sQ8bzwcoFO97EdQPBwo6g8ST3ZBlbDrnCM2mi040AJ5tkbpyVCU4nv9MbMiEUcB1JqYyZ0RP6DA7kZuNwOAE7XvRFL9GNAAw4989yOJhUSMYev7Y/ePl03MWAYDtMXkp3vPvXXYb80xVk+VoYOh6dh7zDvqC6gu6QX/ooosMlTVnBZrxB/Blvxc06+HEB3Q91uNB2JtSdAFys+2lJHun2xI/7QEhE+zvkqOeCYPhUesa79dMLUF3WQJarlUF3AjDQ9DUFUJRXxY3Z7mOF+LUDuDZKabGyGe839Iw3/2wFHNxIN31UvmroDUEAzpA3f5BNxjf4xRAYMjGUtLsbxszsBRGD+B0v/nbTnHO6dryt/TUEAegQLv+e2Vfa2rc219gh+3Rij/YjJCjMDq8voVdWj20v0mXGS7ltGALQwZT3s6Chnj8m7NrdF15d1JeKo1n/1pfR9FJzjQO2sFm4kcRQBOC+cfraAhKf2Dnh0CuGfp1h4Iiu8Pzkp8j0u1Ze0nSpmXSklcFQBKBfaYvzoKy4tpWLLZdWqwTTvxvY9G5nix6vrF6Sa6m5rbbl7mZhDexcSe9HtPxS2yvDEVBT0QC4NkNJ1NAu3OHluLnh0D2a3tDBfWkt9ngpvym94QhAR39je8h5p8oon13Lyn6BLXvIXXv4yO7xZv1xG45vKyLz09NgSAKALQ/hqQRKgkK94c2VTzebceHOx7+FkGYDu2hsdELKHDqv1r/V49qYBDAkck+UwYmfb5CYjErsAdHDurgW7IZOCiN/l5FcCPln9F3vIZ1jBvai9h7+cqRcKo1suASxJmsYeHnzx//Xc+6yztcCoeH8w1W46fNe3yP3T+Rp5HN7izFsDcBASi7VwK+rC8iYukV1IsHHRGmLcg0NPvpoaALQwR1L6WEp2inB2rH368uU2TB6wxNwr9IhOyylkMS1Ja1POFC+yOkNTwA6f2jzVXL7khfc2f2lcHoPva7ES6OXzhQE4Jbhpv84lHQ0NELKXOMOO92JNgUB6PT5o3fgeNqjJ1P71l2Ba9n8vQX34I1wbxoCEKxtn+W4JlYUcJW36mD7Qv23GSn/eHpTEYBP9il2dJwSPPxbdUf/bUbKP57eVARgAJmH6PcMSvKreTEaWmc6Aupr+Rs2iLKFLVebTUxHAHXCDYG32gQBqj+AeNSQEkEAhYwH9RJxxhOLsIga4EGkiaxsbOuREnzLxmxiOo99OvM3XhB4PJBlNjEdAXLtvKgBGjx+ckNN0QfoTIBc7dDAtQ4V8b9qguRqR4fQ0SCR6QiQA5l620YDHDtchOkIwBNwlIhOmELGg3rq/A8WYbPT5HjQBY9mZboagB9TosTGjqiYTUznsUXGY7nmyajEyIRjTJeJt1pdzuIhLbOJ6TyWW4wTfYAGj5/cMFT0ARoQILsaKjNE1cC1DhVhuibIzr4jQYnoAyhkPKi3+9EE4Dc/zSam89hL5mNOcs2TUYkxHQFyK55mnAfQ20tGfWSa/MJPzdRWNbhe0MBagaclzDgKMh0By8f/BfeqHFDJvnbS/J0ftgQUFOYN3jL9g1GfJ9MRUJJf8zCW7CWrsiL+u8UP/9hYGtP1AcaCT7k3ggDlGCrKQRCgCD7liQUByjFUlIMgQBF8yhMLApRjqCgHQYAi+JQnFgQox1BRDoIARfApTywIUI6hohyQgEpFOYjEShCoxPdNzPNauZJQDZgWsccakGRA3x4Xl5IkJzilONh7gP0d+bhEbYQ4JZAy0uCF0awJkpwS+E5BhREcexx8QKybMGfYN8n9mpAez24T2PI6/vcV9BdSHyQSf9uDQGVTf5uUBrHJ+OBj4n8BJ5EPp7ErQgIAAAAASUVORK5CYII= + + + + + + + + + + + + +https://uk.search.yahoo.com/ + diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/en-ZA/default.txt b/mobile/ios/Client/Assets/Search/SearchPlugins/en-ZA/default.txt new file mode 100644 index 0000000000..ced95a5cb9 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/en-ZA/default.txt @@ -0,0 +1 @@ +Google \ No newline at end of file diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/en-ZA/list.txt b/mobile/ios/Client/Assets/Search/SearchPlugins/en-ZA/list.txt new file mode 100644 index 0000000000..517b81ae99 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/en-ZA/list.txt @@ -0,0 +1,3 @@ +google +twitter +wikipedia diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/en/amazondotcom.xml b/mobile/ios/Client/Assets/Search/SearchPlugins/en/amazondotcom.xml new file mode 100644 index 0000000000..84b1c7e76b --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/en/amazondotcom.xml @@ -0,0 +1,16 @@ + + + +Amazon.com +ISO-8859-1 +data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAYAAADimHc4AAAAAXNSR0IArs4c6QAACixJREFUeAHtXQ1MldcZfuRXlIr4A1MoYAf+xKBswlKmdTJ1Q7RZdc5kM7XWsXaGWk2WTDPc0qlzOmZqnHGZoxrntuiydB1UXZSRqS1h03WTVdDa+osov/I3qAicve93ufTC/eHc673f+ejOmxzu933n/c77nuc5/+fcC/CJjKDLXArlFFopCB38igFjytgyxoz1APkM3ZVQ0KCbgwFjzZgbwmxo8M0B3rGAM+YjgunPdyhspKDFXASeInPVTMABCvHm2tbW+hCI5eaHO4cnNCRKEGhjArhd0qIIgSBFdrXZPgQ0AYqLgiZAE6AYAcXmdQ3QBChGQLF5XQM0AYoRUGxe1wBNgGIEFJvXNUAToBgBxeZ1DdAEKEZAsfkQxfZ9Nh8SEoKwsDD09PTg4cOHPqej+sVhQcC4ceOwdOlSZGRkICUlxQhJSUkIDuYNPaCpqQm3bt3CzZs3UVlZiVOnTqG8vNwgRzXAMvYdN4otdT1//nxBYIpHjx4Jb6W2tlbk5+eLqKgoS+WJCBnsj9ODwQqm38fHx4uSkhJvMXep39zcLNauXWt6HlwA7c4HaxGwZMkS0dDQ4BLMx3m4b98+dwCofm4dAhYuXOhTcyNLzObNm1WD7cq+NQiYNm2a4OYikNLZ2SmSk5NdgaDymTUIOHnyZCCx70/70KFDKsF2sm2JYyk02sHZs2dlRmyoqanB4cOHcfnyZbS2tmLq1KlYs2YN0tLSpN5vbGxEbGyspYaoTqx40YP75d2jR4/2l1BPFzt37hQjR450shkUFCR27drl6dUBcenp6U5pmJ1nB3tqmyAGr76+fgBArm4KCgqGBI0mX65edXq2YsWKIdNyACigusoX41JTUzFhwgTKr3uhzhnbtm1zr9AXc+TIkSF1WIHmGVJ6ZigpJ+DatWtG+9/d3e02v8eOHUNbW5vbeHtERUWF/dLjJy9tWEWUrwV1dHRgwYIFxrpOXFwcEhMTMWXKFCPwek9oaCi2b98uhVd7e7uUHi/kWUUs4wmvat6+fdsI58+f9wkfT7XIMUHqdxxvlV5bxxM/wEC9rVQqsnpSiT2mkmVqgEw+uDnicf/MmTMxY8YMozOdPHkyOEyaNAkTJ06UScZSOpYnIDMzEzk5OcjOzjYmW1Zqv/3BpCUJCA8Px+rVq7Fp0ybwMPXTLJYjYN68eSgsLAQtzn2ace/Pm6U64S1btuDcuXP/N+AzC5apAVu3bpUe77PjPJLh/V9enKOlDNAmjjFnWL9+PUd7FCuNgtjRgK51yKS/aNEiwkRO6urqxMaNG0VMTIyT39OnT5dKZMeOHU7vyvgZCB3lNSAiIgIHDx6kvA0tpaWlWL58ubEM7UrbfkrCVZxVnyknYOXKlcayw1AAXbhwAcuWLQPtarlVHY4EKO+E161b5xZQxwg6YuIRfNYdjnMEpQSMHj0avBs2lNy5cwdnzpwZSm1YzoSVEsCTLJmFMdllZj41JyPR0dEyaqboKCUgISFBKpOe2n3HBPj4oozwsrdVRCkBtL8rhYNMieWZ8+LFi6XSmzNnDkaM4PMI6kUpAXTmUwoB2kQ3JlmelGnDvv+wric9juMtSVmyhkrLH/HKJiVZWVlSEydW2rBhg1s/aQlDOh274qVLlwQtb7tNk4A1K840Q04Zos140dvba8fE42dXV5egFVJBTUd/OrQPIPbu3evxPU+Re/bs6U/LRMAH21RHAGeaRjieMHKKq6qqErRJL06fPi3oixlO8d4+sMAZIbUEcNOiUmjkNLhEmn2vloDIyEhx7949v3Fw8eJFo4bIJLh7926zwXZlTy0B3AzRdqN0X+AJ2OLiYkGza8GkXr161a0qnZ4QtGztCgwVz9QTwCTk5uYKBsZX2b9/v6DFuH4A6UyRqK6udkqODngJCzQ7/X5S3q1BAPvB+wJXrlxxAs3Tg7KyMsHfJXOVDzo5MeDcKa0pidmzZ7vUdXyfjg2J1DiI59IgXsmCePXLEHn0mZMKkTDOv3jxdJAd8lqSY4DrDUBvr9evenyBj56sWrXKCPSNGfCCnaPQsBXUvIAPbxUVFeHEiROO0U7XPOs9cOCAsXOWl5dnfDop9T1IiQW+/xXga3TSfWKkOy3gj+8B3/iV+3hvYnwm4P3XbL9Anf8W8Na/vTHpnS4f3OXzPnzssKWlxTgjSjXCu0QktHetAL5HKxkhEmsDnTSBH/WKRKISKj4TkDge+BNtv37uSeDvN4D8PwN/rZKwaFGVl2lVvPVj4E4Tfe+4A2im0Eb3wUTIdPqZ7Re/CLz0jM35FtoTGrvJPxnxmQA2HxEGvLEG+GaGzZmKu8AvSoHf/QPo7PKPg1ZJZewo4MHrNm/OXQO+9HP/eCZR4dwbYpC/VQhsfpP6AmoVZtEq76+fB+7uBn72dSDJ87F/9wkrjmG/f5ADHH8JGN/XF0QTAXZ59yP71eN/PlYNcDSfReeo3ngBmEJNk12YlGI6sv8b+rcFpyuBdqrSVpXYMcCqdFttznzK5iX7n7ETeO828PzTlI8Xbc+X7gdO/sc/OfEbAezOKGqSfvIcQMM2BHHKDtLVA5z9AHibCHmbnL9e7xCp6DKaBljL0wj0LwBcgIIdfL7RCLz8W+AMFRyWQmpqvz0XuEnPU34IdFN+/CF+JcDu0NNUgn65Gkjz8E2gqvs2Mt75EPgnlbC7D+xvB+6TxvCYmwzMozD3swCN9Z0KSncv8HoJ8Fox0NHXj4XSb4LcK6DmiAh79bitn/OXlwEhgJ3jDSeu0j9+FphG4+uhpLaNiLhlq+4f1gFcAm80ADXNQA+B4o3EUHOSNJ5KaozNNo9iuFA86WErmFobFF0CflQEVFQPtEYTMmPEV9NCaW79hJiBWr7dBYwAuzs8jHshkzrqbGAqAeKtcDvc9F+gjgh60GHLvL1kcjMXFgJEhtM/QKDdzbER9I9ZougZlVhZ6aH0j18AfvoX4H0axbkSHm7TLNgY+ZRfd6Xh+7OAE2B3jWsEt7PfnQ9wieJqrVLutwJ/uAjsKwU+qnfvCfcT9wuAvN9TP/COez1fY0wjwNFBHnFwx/fsLOAZao/NIqORatKb/wKOUYn/Gw0IZJZRRoYCcxKBd6mvCoQoIcAxI1HUbHx1JpBNIZ0yOmOS3HKAYxrurhvagTJqMspo3M4Alt/w3+jFnU1vnysnYLDDXOJmxQOfT7B1oHFjgTjqPGOfAMYQWWOorQ+ndp/7ho8f0XIB/VxcIwFdT4E7bW5OOHCH/kHt4NStd285AmQg4o7d25GRTLoqdIYlASqACpRNKktaVCKgCVCJPtnWBGgCFCOg2LyuAZoAxQgoNq9rgCZAMQKKzesaoAlQjIBi87oGaAIUI6DYvK4BmgDFCCg2zzWAtru1KEKgjQmoVGRcmyXsmYBCjYQyBAzseVeshALtsupgIgaMOWNvCJ0d0yQQBmYVQgafMR8gzEYuhXIKdGzJNGfMyrRqO4wpY8sY95f8/wEKvBLprcz3zwAAAABJRU5ErkJggg== + + + + + + +https://www.amazon.com/ + diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/en/bing.xml b/mobile/ios/Client/Assets/Search/SearchPlugins/en/bing.xml new file mode 100644 index 0000000000..48e15c62b1 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/en/bing.xml @@ -0,0 +1,20 @@ + + + +Bing +data:image/png;base64,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 + + + + + + + + + + + +http://www.bing.com + diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/en/default.txt b/mobile/ios/Client/Assets/Search/SearchPlugins/en/default.txt new file mode 100644 index 0000000000..b3259247ac --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/en/default.txt @@ -0,0 +1 @@ +Google diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/en/duckduckgo.xml b/mobile/ios/Client/Assets/Search/SearchPlugins/en/duckduckgo.xml new file mode 100644 index 0000000000..464cb0fdf3 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/en/duckduckgo.xml @@ -0,0 +1,23 @@ + + + +DuckDuckGo +UTF-8 +data:image/png;base64,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 + + + + + + + + + + + + + +https://duckduckgo.com + diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/en/google-2018.xml b/mobile/ios/Client/Assets/Search/SearchPlugins/en/google-2018.xml new file mode 100644 index 0000000000..82f70f41f4 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/en/google-2018.xml @@ -0,0 +1,17 @@ + + + +Google +UTF-8 +data:image/png;base64,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 + + + + + + + +https://www.google.com + diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/en/google-nocodes.xml b/mobile/ios/Client/Assets/Search/SearchPlugins/en/google-nocodes.xml new file mode 100644 index 0000000000..7ed636ae91 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/en/google-nocodes.xml @@ -0,0 +1,16 @@ + + + +Google +UTF-8 +data:image/png;base64,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 + + + + + + +https://www.google.com + diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/en/google.xml b/mobile/ios/Client/Assets/Search/SearchPlugins/en/google.xml new file mode 100644 index 0000000000..2a264c27cb --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/en/google.xml @@ -0,0 +1,17 @@ + + + +Google +UTF-8 +data:image/png;base64,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 + + + + + + + +https://www.google.com + diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/en/list.txt b/mobile/ios/Client/Assets/Search/SearchPlugins/en/list.txt new file mode 100644 index 0000000000..55b4d7effc --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/en/list.txt @@ -0,0 +1,9 @@ +amazondotcom +google:hidden +google-nocodes:hidden +google-2018 +twitter +wikipedia +yahoo +bing +duckduckgo diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/en/qwant.xml b/mobile/ios/Client/Assets/Search/SearchPlugins/en/qwant.xml new file mode 100644 index 0000000000..a80b2dbcac --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/en/qwant.xml @@ -0,0 +1,18 @@ + + + +Qwant +UTF-8 +data:image/png;base64,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 + + + + + + + + +https://www.qwant.com/ + diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/en/twitter.xml b/mobile/ios/Client/Assets/Search/SearchPlugins/en/twitter.xml new file mode 100644 index 0000000000..1334c4726f --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/en/twitter.xml @@ -0,0 +1,13 @@ + + + + Twitter + data:image/png;base64,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 + + + + UTF-8 + https://mobile.twitter.com/search + diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/en/wikipedia.xml b/mobile/ios/Client/Assets/Search/SearchPlugins/en/wikipedia.xml new file mode 100644 index 0000000000..2043036e6b --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/en/wikipedia.xml @@ -0,0 +1,23 @@ + + + +Wikipedia +UTF-8 +data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAQAAABIkb+zAAAEXElEQVR4Ae3cA4xs2QJG4b9aj41n27Zt27Zt27Zt27Zt+2KM5lXP924qOzs56VPjKSRnrfhv7NUsp08v98+PsxwT4nJ+nPunl8KZ8/WYQL+eMydJrx5/EhN603lAHpVJ5bzZNp035uyZXM7Uy3LmM7ms9CITzVRGQRfQBXQBXUAX0AV0AV1AF9AFdAFdQBfQBXQBXcBMjgU7M5+1vqtZyXqmc/IsZDEL+53LP3KRPCCv62+rWd7vnpx6v/M5Vd935OF5R+6RtRyeQ3NYNjKTxZw2p8tiduR8uWfemY3+dkjWM5fFLPU/8q9yrRwbHLP39WkHamPD5z1WnNMb/dpW/uAdbipu413+qcl2H3N3cXFv9DNNtvmg68uxMY6dPfewTZNvOauouqEdULlTY532IgqHua+ZxnoHm4ANj63biRNQPZedVPY5nTR1BXsVbNuyTlsDqy4tW/wK4NYiJ01AXMlm+wGrL1cDnWbLejC4m7T4DvAOOSkD4v0qztGyLzpUweOk4UXAp6XV72DNmU7qgPPao+DJ0uLTFPzXXGN5I9acU1o8vb14jZzUAfFeBdvMtuwLDlfwIKme2248T1p9MvY59zACLqcy4Kf5eQr+ZbYRfrAFaXHaf/FRGUZAfE/Bzwf8OKwpuJ/0vYRNPEpavT248rACbqfimtLiqxT8w4yIz+Of5qTV7+LHMqyAaf9R8Clp8ex2K7iXuAa4q7R6aXCH4QXE4xVsOq+0+DYFfzXth/ilnrT6Tvzb9DADlqwqDPjTd377FHwI3EBaPb0NPFaGGRBvoLBiUVr8EFS+KgN8KpYtDjvgQo6i8Hhp8ZJUjnIZaXXGNrxKhh0QX6TwHzPS4uco/FtPWr0T9jnPKAJuOOiCc/UqVG4jrf4AH5dRBPT8UWHg3/BvUfhl635ZcNXRBMSDqFxFWvwklVu07O/BT2RUAad0GIWPtexXgMpPt+xntAt3Gl1AvJjCPufasn4d/6NyY2n4DPzXzCgDzmEvhVdIw+tj02X8nsIPGvus7Xi8jDIgPkLhSPON5ad4t7gbletJ9S71n+BIA65C5dGal1g3nENM+weF70j1R3iNjDogfkrhn6br5dU/4UXS94EtF7+vUC8IjjzgrlRuJ33v27j2dTLbKXxd+r4Pn5RRB9RfxsL3y4H/i0dI9TFUrirOZDeuPh4B8TQqVyjH/ZtZqZ7KIRS+JJ6Fn8k4BNRL9IUPmncwbr/1b37lanbiLuMTEG+nsNfb8cOWq0DLFA7E/8yMU8AlNLnaoP/alSfKOAXEN6h8Qlo8k3UKq5bGLeAWFPa6oLT6Ogqvk3ELmPJ3wOtlgOe0B2w6//gFxCPBsjPIQN8JPi3jGDDvSDxVDPaCNnFNOdGczrNzIrEnRyR5XPZlMIdmKofnpTnx6B783QV0ARNOF9AFdAFdQBfQBXQBXUAX0AV0AV3A6OgCVjLJrEzlj5lk/jiVt2eSefvEvzTJxL84zMS/PM//Af1pBRpQAkXBAAAAAElFTkSuQmCC + + + + + + + + + + + + + +https://en.wikipedia.org/wiki/Special:Search + diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/en/yahoo.xml b/mobile/ios/Client/Assets/Search/SearchPlugins/en/yahoo.xml new file mode 100644 index 0000000000..fbc69d2a4c --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/en/yahoo.xml @@ -0,0 +1,29 @@ + + + +Yahoo +UTF-8 +data:image/png;base64,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 + + + + + + + + + + + + + + + + + + + + + diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/eo/default.txt b/mobile/ios/Client/Assets/Search/SearchPlugins/eo/default.txt new file mode 100644 index 0000000000..ced95a5cb9 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/eo/default.txt @@ -0,0 +1 @@ +Google \ No newline at end of file diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/eo/list.txt b/mobile/ios/Client/Assets/Search/SearchPlugins/eo/list.txt new file mode 100644 index 0000000000..0b9dd166db --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/eo/list.txt @@ -0,0 +1,7 @@ +bing +duckduckgo +google +reta-vortaro +twitter +wikipedia-eo +yahoo diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/eo/reta-vortaro.xml b/mobile/ios/Client/Assets/Search/SearchPlugins/eo/reta-vortaro.xml new file mode 100644 index 0000000000..3b0b28dce3 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/eo/reta-vortaro.xml @@ -0,0 +1,15 @@ + + + +Revoserĉo +UTF-8 +data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAYAAADimHc4AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAA2hpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMC1jMDYxIDY0LjE0MDk0OSwgMjAxMC8xMi8wNy0xMDo1NzowMSAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtcE1NOk9yaWdpbmFsRG9jdW1lbnRJRD0ieG1wLmRpZDowMTgwMTE3NDA3MjA2ODExOEE2REI2ODhGMkQ2RTdCRSIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDpBOTM4NEVGNDU4NzAxMUU2OEYzNEY5MEFEOEYxNTI2NCIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDpBOTM4NEVGMzU4NzAxMUU2OEYzNEY5MEFEOEYxNTI2NCIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ1M1LjEgTWFjaW50b3NoIj4gPHhtcE1NOkRlcml2ZWRGcm9tIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6MDI4MDExNzQwNzIwNjgxMTg4QzZFMzlDMjAxOTVDNkMiIHN0UmVmOmRvY3VtZW50SUQ9InhtcC5kaWQ6MDE4MDExNzQwNzIwNjgxMThBNkRCNjg4RjJENkU3QkUiLz4gPC9yZGY6RGVzY3Jp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 + + + + + +http://www.reta-vortaro.de/revo/ + diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/eo/wikipedia-eo.xml b/mobile/ios/Client/Assets/Search/SearchPlugins/eo/wikipedia-eo.xml new file mode 100644 index 0000000000..35d80add4c --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/eo/wikipedia-eo.xml @@ -0,0 +1,23 @@ + + + +Vikipedio (eo) +UTF-8 +data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAQAAABIkb+zAAAEXElEQVR4Ae3cA4xs2QJG4b9aj41n27Zt27Zt27Zt27Zt+2KM5lXP924qOzs56VPjKSRnrfhv7NUsp08v98+PsxwT4nJ+nPunl8KZ8/WYQL+eMydJrx5/EhN603lAHpVJ5bzZNp035uyZXM7Uy3LmM7ms9CITzVRGQRfQBXQBXUAX0AV0AV1AF9AFdAFdQBfQBXQBXcBMjgU7M5+1vqtZyXqmc/IsZDEL+53LP3KRPCCv62+rWd7vnpx6v/M5Vd935OF5R+6RtRyeQ3NYNjKTxZw2p8tiduR8uWfemY3+dkjWM5fFLPU/8q9yrRwbHLP39WkHamPD5z1WnNMb/dpW/uAdbipu413+qcl2H3N3cXFv9DNNtvmg68uxMY6dPfewTZNvOauouqEdULlTY532IgqHua+ZxnoHm4ANj63biRNQPZedVPY5nTR1BXsVbNuyTlsDqy4tW/wK4NYiJ01AXMlm+wGrL1cDnWbLejC4m7T4DvAOOSkD4v0qztGyLzpUweOk4UXAp6XV72DNmU7qgPPao+DJ0uLTFPzXXGN5I9acU1o8vb14jZzUAfFeBdvMtuwLDlfwIKme2248T1p9MvY59zACLqcy4Kf5eQr+ZbYRfrAFaXHaf/FRGUZAfE/Bzwf8OKwpuJ/0vYRNPEpavT248rACbqfimtLiqxT8w4yIz+Of5qTV7+LHMqyAaf9R8Clp8ex2K7iXuAa4q7R6aXCH4QXE4xVsOq+0+DYFfzXth/ilnrT6Tvzb9DADlqwqDPjTd377FHwI3EBaPb0NPFaGGRBvoLBiUVr8EFS+KgN8KpYtDjvgQo6i8Hhp8ZJUjnIZaXXGNrxKhh0QX6TwHzPS4uco/FtPWr0T9jnPKAJuOOiCc/UqVG4jrf4AH5dRBPT8UWHg3/BvUfhl635ZcNXRBMSDqFxFWvwklVu07O/BT2RUAad0GIWPtexXgMpPt+xntAt3Gl1AvJjCPufasn4d/6NyY2n4DPzXzCgDzmEvhVdIw+tj02X8nsIPGvus7Xi8jDIgPkLhSPON5ad4t7gbletJ9S71n+BIA65C5dGal1g3nENM+weF70j1R3iNjDogfkrhn6br5dU/4UXS94EtF7+vUC8IjjzgrlRuJ33v27j2dTLbKXxd+r4Pn5RRB9RfxsL3y4H/i0dI9TFUrirOZDeuPh4B8TQqVyjH/ZtZqZ7KIRS+JJ6Fn8k4BNRL9IUPmncwbr/1b37lanbiLuMTEG+nsNfb8cOWq0DLFA7E/8yMU8AlNLnaoP/alSfKOAXEN6h8Qlo8k3UKq5bGLeAWFPa6oLT6Ogqvk3ELmPJ3wOtlgOe0B2w6//gFxCPBsjPIQN8JPi3jGDDvSDxVDPaCNnFNOdGczrNzIrEnRyR5XPZlMIdmKofnpTnx6B783QV0ARNOF9AFdAFdQBfQBXQBXUAX0AV0AV3A6OgCVjLJrEzlj5lk/jiVt2eSefvEvzTJxL84zMS/PM//Af1pBRpQAkXBAAAAAElFTkSuQmCC + + + + + + + + + + + + + +https://eo.wikipedia.org/wiki/Specialaĵo:Serĉi + diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/es-AR/default.txt b/mobile/ios/Client/Assets/Search/SearchPlugins/es-AR/default.txt new file mode 100644 index 0000000000..ced95a5cb9 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/es-AR/default.txt @@ -0,0 +1 @@ +Google \ No newline at end of file diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/es-AR/list.txt b/mobile/ios/Client/Assets/Search/SearchPlugins/es-AR/list.txt new file mode 100644 index 0000000000..91eb653833 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/es-AR/list.txt @@ -0,0 +1,4 @@ +google +mercadolibre-ar +twitter +wikipedia-es diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/es-AR/mercadolibre-ar.xml b/mobile/ios/Client/Assets/Search/SearchPlugins/es-AR/mercadolibre-ar.xml new file mode 100644 index 0000000000..52155ee68c --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/es-AR/mercadolibre-ar.xml @@ -0,0 +1,14 @@ + + + +MercadoLibre Argentina +ISO-8859-1 +data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAYAAADimHc4AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAA2hpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMC1jMDYxIDY0LjE0MDk0OSwgMjAxMC8xMi8wNy0xMDo1NzowMSAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtcE1NOk9yaWdpbmFsRG9jdW1lbnRJRD0ieG1wLmRpZDowMzgwMTE3NDA3MjA2ODExOEE2REI2ODhGMkQ2RTdCRSIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDozRjI2NzFGMDJGQUQxMUU1QTU0MzhDNzBBMTQ4RTJDRiIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDozRjI2NzFFRjJGQUQxMUU1QTU0MzhDNzBBMTQ4RTJDRiIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ1M1LjEgTWFjaW50b3NoIj4gPHhtcE1NOkRlcml2ZWRGcm9tIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6MDE4MDExNzQwNzIwNjgxMThBNkRBMkY5QTdDQzAwNzYiIHN0UmVmOmRvY3VtZW50SUQ9InhtcC5kaWQ6MDM4MDExNzQwNzIwNjgxMThBNkRCNjg4RjJENkU3QkUiLz4gPC9yZGY6RGVzY3Jp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 + + + + +http://www.mercadolibre.com.ar/ + diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/es-AR/wikipedia-es.xml b/mobile/ios/Client/Assets/Search/SearchPlugins/es-AR/wikipedia-es.xml new file mode 100644 index 0000000000..b22d159ede --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/es-AR/wikipedia-es.xml @@ -0,0 +1,23 @@ + + + +Wikipedia (es) +UTF-8 +data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAQAAABIkb+zAAAEXElEQVR4Ae3cA4xs2QJG4b9aj41n27Zt27Zt27Zt27Zt+2KM5lXP924qOzs56VPjKSRnrfhv7NUsp08v98+PsxwT4nJ+nPunl8KZ8/WYQL+eMydJrx5/EhN603lAHpVJ5bzZNp035uyZXM7Uy3LmM7ms9CITzVRGQRfQBXQBXUAX0AV0AV1AF9AFdAFdQBfQBXQBXcBMjgU7M5+1vqtZyXqmc/IsZDEL+53LP3KRPCCv62+rWd7vnpx6v/M5Vd935OF5R+6RtRyeQ3NYNjKTxZw2p8tiduR8uWfemY3+dkjWM5fFLPU/8q9yrRwbHLP39WkHamPD5z1WnNMb/dpW/uAdbipu413+qcl2H3N3cXFv9DNNtvmg68uxMY6dPfewTZNvOauouqEdULlTY532IgqHua+ZxnoHm4ANj63biRNQPZedVPY5nTR1BXsVbNuyTlsDqy4tW/wK4NYiJ01AXMlm+wGrL1cDnWbLejC4m7T4DvAOOSkD4v0qztGyLzpUweOk4UXAp6XV72DNmU7qgPPao+DJ0uLTFPzXXGN5I9acU1o8vb14jZzUAfFeBdvMtuwLDlfwIKme2248T1p9MvY59zACLqcy4Kf5eQr+ZbYRfrAFaXHaf/FRGUZAfE/Bzwf8OKwpuJ/0vYRNPEpavT248rACbqfimtLiqxT8w4yIz+Of5qTV7+LHMqyAaf9R8Clp8ex2K7iXuAa4q7R6aXCH4QXE4xVsOq+0+DYFfzXth/ilnrT6Tvzb9DADlqwqDPjTd377FHwI3EBaPb0NPFaGGRBvoLBiUVr8EFS+KgN8KpYtDjvgQo6i8Hhp8ZJUjnIZaXXGNrxKhh0QX6TwHzPS4uco/FtPWr0T9jnPKAJuOOiCc/UqVG4jrf4AH5dRBPT8UWHg3/BvUfhl635ZcNXRBMSDqFxFWvwklVu07O/BT2RUAad0GIWPtexXgMpPt+xntAt3Gl1AvJjCPufasn4d/6NyY2n4DPzXzCgDzmEvhVdIw+tj02X8nsIPGvus7Xi8jDIgPkLhSPON5ad4t7gbletJ9S71n+BIA65C5dGal1g3nENM+weF70j1R3iNjDogfkrhn6br5dU/4UXS94EtF7+vUC8IjjzgrlRuJ33v27j2dTLbKXxd+r4Pn5RRB9RfxsL3y4H/i0dI9TFUrirOZDeuPh4B8TQqVyjH/ZtZqZ7KIRS+JJ6Fn8k4BNRL9IUPmncwbr/1b37lanbiLuMTEG+nsNfb8cOWq0DLFA7E/8yMU8AlNLnaoP/alSfKOAXEN6h8Qlo8k3UKq5bGLeAWFPa6oLT6Ogqvk3ELmPJ3wOtlgOe0B2w6//gFxCPBsjPIQN8JPi3jGDDvSDxVDPaCNnFNOdGczrNzIrEnRyR5XPZlMIdmKofnpTnx6B783QV0ARNOF9AFdAFdQBfQBXQBXUAX0AV0AV3A6OgCVjLJrEzlj5lk/jiVt2eSefvEvzTJxL84zMS/PM//Af1pBRpQAkXBAAAAAElFTkSuQmCC + + + + + + + + + + + + + +https://es.wikipedia.org/wiki/Especial:Buscar + diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/es-CL/default.txt b/mobile/ios/Client/Assets/Search/SearchPlugins/es-CL/default.txt new file mode 100644 index 0000000000..ced95a5cb9 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/es-CL/default.txt @@ -0,0 +1 @@ +Google \ No newline at end of file diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/es-CL/drae.xml b/mobile/ios/Client/Assets/Search/SearchPlugins/es-CL/drae.xml new file mode 100644 index 0000000000..26f6e04ee1 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/es-CL/drae.xml @@ -0,0 +1,12 @@ + + + +Diccionario RAE +data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAYAAADimHc4AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAA2hpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMC1jMDYxIDY0LjE0MDk0OSwgMjAxMC8xMi8wNy0xMDo1NzowMSAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtcE1NOk9yaWdpbmFsRG9jdW1lbnRJRD0ieG1wLmRpZDowMTgwMTE3NDA3MjA2ODExOEE2REI2ODhGMkQ2RTdCRSIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDozMkI5RjZGMEUxRDUxMUU1QjNGQjhCMkJCNTFGN0VBNiIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDozMkI5RjZFRkUxRDUxMUU1QjNGQjhCMkJCNTFGN0VBNiIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ1M1LjEgTWFjaW50b3NoIj4gPHhtcE1NOkRlcml2ZWRGcm9tIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6MDQ4MDExNzQwNzIwNjgxMUEyRThFMzA3QjczODA3QjMiIHN0UmVmOmRvY3VtZW50SUQ9InhtcC5kaWQ6MDE4MDExNzQwNzIwNjgxMThBNkRCNjg4RjJENkU3QkUiLz4gPC9yZGY6RGVzY3Jp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 + + + +http://dle.rae.es + diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/es-CL/list.txt b/mobile/ios/Client/Assets/Search/SearchPlugins/es-CL/list.txt new file mode 100644 index 0000000000..7d1f454896 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/es-CL/list.txt @@ -0,0 +1,7 @@ +bing +drae +google +mercadolibre-cl +twitter +wikipedia-es +yahoo-cl diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/es-CL/mercadolibre-cl.xml b/mobile/ios/Client/Assets/Search/SearchPlugins/es-CL/mercadolibre-cl.xml new file mode 100644 index 0000000000..65f1303f38 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/es-CL/mercadolibre-cl.xml @@ -0,0 +1,14 @@ + + + +MercadoLibre Chile +ISO-8859-1 +data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAYAAADimHc4AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAA2hpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMC1jMDYxIDY0LjE0MDk0OSwgMjAxMC8xMi8wNy0xMDo1NzowMSAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtcE1NOk9yaWdpbmFsRG9jdW1lbnRJRD0ieG1wLmRpZDowMzgwMTE3NDA3MjA2ODExOEE2REI2ODhGMkQ2RTdCRSIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDozRjI2NzFGMDJGQUQxMUU1QTU0MzhDNzBBMTQ4RTJDRiIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDozRjI2NzFFRjJGQUQxMUU1QTU0MzhDNzBBMTQ4RTJDRiIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ1M1LjEgTWFjaW50b3NoIj4gPHhtcE1NOkRlcml2ZWRGcm9tIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6MDE4MDExNzQwNzIwNjgxMThBNkRBMkY5QTdDQzAwNzYiIHN0UmVmOmRvY3VtZW50SUQ9InhtcC5kaWQ6MDM4MDExNzQwNzIwNjgxMThBNkRCNjg4RjJENkU3QkUiLz4gPC9yZGY6RGVzY3Jp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 + + + + +http://www.mercadolibre.cl/ + diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/es-CL/wikipedia-es.xml b/mobile/ios/Client/Assets/Search/SearchPlugins/es-CL/wikipedia-es.xml new file mode 100644 index 0000000000..b22d159ede --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/es-CL/wikipedia-es.xml @@ -0,0 +1,23 @@ + + + +Wikipedia (es) +UTF-8 +data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAQAAABIkb+zAAAEXElEQVR4Ae3cA4xs2QJG4b9aj41n27Zt27Zt27Zt27Zt+2KM5lXP924qOzs56VPjKSRnrfhv7NUsp08v98+PsxwT4nJ+nPunl8KZ8/WYQL+eMydJrx5/EhN603lAHpVJ5bzZNp035uyZXM7Uy3LmM7ms9CITzVRGQRfQBXQBXUAX0AV0AV1AF9AFdAFdQBfQBXQBXcBMjgU7M5+1vqtZyXqmc/IsZDEL+53LP3KRPCCv62+rWd7vnpx6v/M5Vd935OF5R+6RtRyeQ3NYNjKTxZw2p8tiduR8uWfemY3+dkjWM5fFLPU/8q9yrRwbHLP39WkHamPD5z1WnNMb/dpW/uAdbipu413+qcl2H3N3cXFv9DNNtvmg68uxMY6dPfewTZNvOauouqEdULlTY532IgqHua+ZxnoHm4ANj63biRNQPZedVPY5nTR1BXsVbNuyTlsDqy4tW/wK4NYiJ01AXMlm+wGrL1cDnWbLejC4m7T4DvAOOSkD4v0qztGyLzpUweOk4UXAp6XV72DNmU7qgPPao+DJ0uLTFPzXXGN5I9acU1o8vb14jZzUAfFeBdvMtuwLDlfwIKme2248T1p9MvY59zACLqcy4Kf5eQr+ZbYRfrAFaXHaf/FRGUZAfE/Bzwf8OKwpuJ/0vYRNPEpavT248rACbqfimtLiqxT8w4yIz+Of5qTV7+LHMqyAaf9R8Clp8ex2K7iXuAa4q7R6aXCH4QXE4xVsOq+0+DYFfzXth/ilnrT6Tvzb9DADlqwqDPjTd377FHwI3EBaPb0NPFaGGRBvoLBiUVr8EFS+KgN8KpYtDjvgQo6i8Hhp8ZJUjnIZaXXGNrxKhh0QX6TwHzPS4uco/FtPWr0T9jnPKAJuOOiCc/UqVG4jrf4AH5dRBPT8UWHg3/BvUfhl635ZcNXRBMSDqFxFWvwklVu07O/BT2RUAad0GIWPtexXgMpPt+xntAt3Gl1AvJjCPufasn4d/6NyY2n4DPzXzCgDzmEvhVdIw+tj02X8nsIPGvus7Xi8jDIgPkLhSPON5ad4t7gbletJ9S71n+BIA65C5dGal1g3nENM+weF70j1R3iNjDogfkrhn6br5dU/4UXS94EtF7+vUC8IjjzgrlRuJ33v27j2dTLbKXxd+r4Pn5RRB9RfxsL3y4H/i0dI9TFUrirOZDeuPh4B8TQqVyjH/ZtZqZ7KIRS+JJ6Fn8k4BNRL9IUPmncwbr/1b37lanbiLuMTEG+nsNfb8cOWq0DLFA7E/8yMU8AlNLnaoP/alSfKOAXEN6h8Qlo8k3UKq5bGLeAWFPa6oLT6Ogqvk3ELmPJ3wOtlgOe0B2w6//gFxCPBsjPIQN8JPi3jGDDvSDxVDPaCNnFNOdGczrNzIrEnRyR5XPZlMIdmKofnpTnx6B783QV0ARNOF9AFdAFdQBfQBXQBXUAX0AV0AV3A6OgCVjLJrEzlj5lk/jiVt2eSefvEvzTJxL84zMS/PM//Af1pBRpQAkXBAAAAAElFTkSuQmCC + + + + + + + + + + + + + +https://es.wikipedia.org/wiki/Especial:Buscar + diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/es-CL/yahoo-cl.xml b/mobile/ios/Client/Assets/Search/SearchPlugins/es-CL/yahoo-cl.xml new file mode 100644 index 0000000000..4464f93e3e --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/es-CL/yahoo-cl.xml @@ -0,0 +1,22 @@ + + + +Yahoo +UTF-8 +data:image/png;base64,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 + + + + + + + + + + + + +https://cl.search.yahoo.com/ + diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/es-ES/default.txt b/mobile/ios/Client/Assets/Search/SearchPlugins/es-ES/default.txt new file mode 100644 index 0000000000..ced95a5cb9 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/es-ES/default.txt @@ -0,0 +1 @@ +Google \ No newline at end of file diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/es-ES/list.txt b/mobile/ios/Client/Assets/Search/SearchPlugins/es-ES/list.txt new file mode 100644 index 0000000000..14fe1af92e --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/es-ES/list.txt @@ -0,0 +1,6 @@ +bing +duckduckgo +google +twitter +wikipedia-es +yahoo-es diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/es-ES/wikipedia-es.xml b/mobile/ios/Client/Assets/Search/SearchPlugins/es-ES/wikipedia-es.xml new file mode 100644 index 0000000000..b22d159ede --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/es-ES/wikipedia-es.xml @@ -0,0 +1,23 @@ + + + +Wikipedia (es) +UTF-8 +data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAQAAABIkb+zAAAEXElEQVR4Ae3cA4xs2QJG4b9aj41n27Zt27Zt27Zt27Zt+2KM5lXP924qOzs56VPjKSRnrfhv7NUsp08v98+PsxwT4nJ+nPunl8KZ8/WYQL+eMydJrx5/EhN603lAHpVJ5bzZNp035uyZXM7Uy3LmM7ms9CITzVRGQRfQBXQBXUAX0AV0AV1AF9AFdAFdQBfQBXQBXcBMjgU7M5+1vqtZyXqmc/IsZDEL+53LP3KRPCCv62+rWd7vnpx6v/M5Vd935OF5R+6RtRyeQ3NYNjKTxZw2p8tiduR8uWfemY3+dkjWM5fFLPU/8q9yrRwbHLP39WkHamPD5z1WnNMb/dpW/uAdbipu413+qcl2H3N3cXFv9DNNtvmg68uxMY6dPfewTZNvOauouqEdULlTY532IgqHua+ZxnoHm4ANj63biRNQPZedVPY5nTR1BXsVbNuyTlsDqy4tW/wK4NYiJ01AXMlm+wGrL1cDnWbLejC4m7T4DvAOOSkD4v0qztGyLzpUweOk4UXAp6XV72DNmU7qgPPao+DJ0uLTFPzXXGN5I9acU1o8vb14jZzUAfFeBdvMtuwLDlfwIKme2248T1p9MvY59zACLqcy4Kf5eQr+ZbYRfrAFaXHaf/FRGUZAfE/Bzwf8OKwpuJ/0vYRNPEpavT248rACbqfimtLiqxT8w4yIz+Of5qTV7+LHMqyAaf9R8Clp8ex2K7iXuAa4q7R6aXCH4QXE4xVsOq+0+DYFfzXth/ilnrT6Tvzb9DADlqwqDPjTd377FHwI3EBaPb0NPFaGGRBvoLBiUVr8EFS+KgN8KpYtDjvgQo6i8Hhp8ZJUjnIZaXXGNrxKhh0QX6TwHzPS4uco/FtPWr0T9jnPKAJuOOiCc/UqVG4jrf4AH5dRBPT8UWHg3/BvUfhl635ZcNXRBMSDqFxFWvwklVu07O/BT2RUAad0GIWPtexXgMpPt+xntAt3Gl1AvJjCPufasn4d/6NyY2n4DPzXzCgDzmEvhVdIw+tj02X8nsIPGvus7Xi8jDIgPkLhSPON5ad4t7gbletJ9S71n+BIA65C5dGal1g3nENM+weF70j1R3iNjDogfkrhn6br5dU/4UXS94EtF7+vUC8IjjzgrlRuJ33v27j2dTLbKXxd+r4Pn5RRB9RfxsL3y4H/i0dI9TFUrirOZDeuPh4B8TQqVyjH/ZtZqZ7KIRS+JJ6Fn8k4BNRL9IUPmncwbr/1b37lanbiLuMTEG+nsNfb8cOWq0DLFA7E/8yMU8AlNLnaoP/alSfKOAXEN6h8Qlo8k3UKq5bGLeAWFPa6oLT6Ogqvk3ELmPJ3wOtlgOe0B2w6//gFxCPBsjPIQN8JPi3jGDDvSDxVDPaCNnFNOdGczrNzIrEnRyR5XPZlMIdmKofnpTnx6B783QV0ARNOF9AFdAFdQBfQBXQBXUAX0AV0AV3A6OgCVjLJrEzlj5lk/jiVt2eSefvEvzTJxL84zMS/PM//Af1pBRpQAkXBAAAAAElFTkSuQmCC + + + + + + + + + + + + + +https://es.wikipedia.org/wiki/Especial:Buscar + diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/es-ES/yahoo-es.xml b/mobile/ios/Client/Assets/Search/SearchPlugins/es-ES/yahoo-es.xml new file mode 100644 index 0000000000..a9b50bc07e --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/es-ES/yahoo-es.xml @@ -0,0 +1,22 @@ + + + +Yahoo +UTF-8 +data:image/png;base64,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 + + + + + + + + + + + + +https://es.search.yahoo.com/ + diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/es-MX/default.txt b/mobile/ios/Client/Assets/Search/SearchPlugins/es-MX/default.txt new file mode 100644 index 0000000000..ced95a5cb9 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/es-MX/default.txt @@ -0,0 +1 @@ +Google \ No newline at end of file diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/es-MX/list.txt b/mobile/ios/Client/Assets/Search/SearchPlugins/es-MX/list.txt new file mode 100644 index 0000000000..757a33489c --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/es-MX/list.txt @@ -0,0 +1,8 @@ +amazondotcom +bing +duckduckgo +google +mercadolibre-mx +twitter +wikipedia-es +yahoo-mx diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/es-MX/mercadolibre-mx.xml b/mobile/ios/Client/Assets/Search/SearchPlugins/es-MX/mercadolibre-mx.xml new file mode 100644 index 0000000000..b536695fe6 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/es-MX/mercadolibre-mx.xml @@ -0,0 +1,14 @@ + + + +MercadoLibre Mexico +ISO-8859-1 +data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAYAAADimHc4AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAA2hpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMC1jMDYxIDY0LjE0MDk0OSwgMjAxMC8xMi8wNy0xMDo1NzowMSAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtcE1NOk9yaWdpbmFsRG9jdW1lbnRJRD0ieG1wLmRpZDowMzgwMTE3NDA3MjA2ODExOEE2REI2ODhGMkQ2RTdCRSIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDozRjI2NzFGMDJGQUQxMUU1QTU0MzhDNzBBMTQ4RTJDRiIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDozRjI2NzFFRjJGQUQxMUU1QTU0MzhDNzBBMTQ4RTJDRiIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ1M1LjEgTWFjaW50b3NoIj4gPHhtcE1NOkRlcml2ZWRGcm9tIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6MDE4MDExNzQwNzIwNjgxMThBNkRBMkY5QTdDQzAwNzYiIHN0UmVmOmRvY3VtZW50SUQ9InhtcC5kaWQ6MDM4MDExNzQwNzIwNjgxMThBNkRCNjg4RjJENkU3QkUiLz4gPC9yZGY6RGVzY3Jp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 + + + + +http://www.mercadolibre.com.mx/ + diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/es-MX/wikipedia-es.xml b/mobile/ios/Client/Assets/Search/SearchPlugins/es-MX/wikipedia-es.xml new file mode 100644 index 0000000000..b22d159ede --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/es-MX/wikipedia-es.xml @@ -0,0 +1,23 @@ + + + +Wikipedia (es) +UTF-8 +data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAQAAABIkb+zAAAEXElEQVR4Ae3cA4xs2QJG4b9aj41n27Zt27Zt27Zt27Zt+2KM5lXP924qOzs56VPjKSRnrfhv7NUsp08v98+PsxwT4nJ+nPunl8KZ8/WYQL+eMydJrx5/EhN603lAHpVJ5bzZNp035uyZXM7Uy3LmM7ms9CITzVRGQRfQBXQBXUAX0AV0AV1AF9AFdAFdQBfQBXQBXcBMjgU7M5+1vqtZyXqmc/IsZDEL+53LP3KRPCCv62+rWd7vnpx6v/M5Vd935OF5R+6RtRyeQ3NYNjKTxZw2p8tiduR8uWfemY3+dkjWM5fFLPU/8q9yrRwbHLP39WkHamPD5z1WnNMb/dpW/uAdbipu413+qcl2H3N3cXFv9DNNtvmg68uxMY6dPfewTZNvOauouqEdULlTY532IgqHua+ZxnoHm4ANj63biRNQPZedVPY5nTR1BXsVbNuyTlsDqy4tW/wK4NYiJ01AXMlm+wGrL1cDnWbLejC4m7T4DvAOOSkD4v0qztGyLzpUweOk4UXAp6XV72DNmU7qgPPao+DJ0uLTFPzXXGN5I9acU1o8vb14jZzUAfFeBdvMtuwLDlfwIKme2248T1p9MvY59zACLqcy4Kf5eQr+ZbYRfrAFaXHaf/FRGUZAfE/Bzwf8OKwpuJ/0vYRNPEpavT248rACbqfimtLiqxT8w4yIz+Of5qTV7+LHMqyAaf9R8Clp8ex2K7iXuAa4q7R6aXCH4QXE4xVsOq+0+DYFfzXth/ilnrT6Tvzb9DADlqwqDPjTd377FHwI3EBaPb0NPFaGGRBvoLBiUVr8EFS+KgN8KpYtDjvgQo6i8Hhp8ZJUjnIZaXXGNrxKhh0QX6TwHzPS4uco/FtPWr0T9jnPKAJuOOiCc/UqVG4jrf4AH5dRBPT8UWHg3/BvUfhl635ZcNXRBMSDqFxFWvwklVu07O/BT2RUAad0GIWPtexXgMpPt+xntAt3Gl1AvJjCPufasn4d/6NyY2n4DPzXzCgDzmEvhVdIw+tj02X8nsIPGvus7Xi8jDIgPkLhSPON5ad4t7gbletJ9S71n+BIA65C5dGal1g3nENM+weF70j1R3iNjDogfkrhn6br5dU/4UXS94EtF7+vUC8IjjzgrlRuJ33v27j2dTLbKXxd+r4Pn5RRB9RfxsL3y4H/i0dI9TFUrirOZDeuPh4B8TQqVyjH/ZtZqZ7KIRS+JJ6Fn8k4BNRL9IUPmncwbr/1b37lanbiLuMTEG+nsNfb8cOWq0DLFA7E/8yMU8AlNLnaoP/alSfKOAXEN6h8Qlo8k3UKq5bGLeAWFPa6oLT6Ogqvk3ELmPJ3wOtlgOe0B2w6//gFxCPBsjPIQN8JPi3jGDDvSDxVDPaCNnFNOdGczrNzIrEnRyR5XPZlMIdmKofnpTnx6B783QV0ARNOF9AFdAFdQBfQBXQBXUAX0AV0AV3A6OgCVjLJrEzlj5lk/jiVt2eSefvEvzTJxL84zMS/PM//Af1pBRpQAkXBAAAAAElFTkSuQmCC + + + + + + + + + + + + + +https://es.wikipedia.org/wiki/Especial:Buscar + diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/es-MX/yahoo-mx.xml b/mobile/ios/Client/Assets/Search/SearchPlugins/es-MX/yahoo-mx.xml new file mode 100644 index 0000000000..67fc1d3a5d --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/es-MX/yahoo-mx.xml @@ -0,0 +1,22 @@ + + + +Yahoo +UTF-8 +data:image/png;base64,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 + + + + + + + + + + + + +https://mx.search.yahoo.com/ + diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/et/amazon-co-uk.xml b/mobile/ios/Client/Assets/Search/SearchPlugins/et/amazon-co-uk.xml new file mode 100644 index 0000000000..3a3fbabb19 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/et/amazon-co-uk.xml @@ -0,0 +1,16 @@ + + + +Amazon.co.uk +ISO-8859-1 +data:image/png;base64,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 + + + + + + +https://www.amazon.co.uk/ + diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/et/default.txt b/mobile/ios/Client/Assets/Search/SearchPlugins/et/default.txt new file mode 100644 index 0000000000..ced95a5cb9 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/et/default.txt @@ -0,0 +1 @@ +Google \ No newline at end of file diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/et/list.txt b/mobile/ios/Client/Assets/Search/SearchPlugins/et/list.txt new file mode 100644 index 0000000000..0a860d5b5f --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/et/list.txt @@ -0,0 +1,4 @@ +amazon-co-uk +google +twitter +wikipedia-et diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/et/wikipedia-et.xml b/mobile/ios/Client/Assets/Search/SearchPlugins/et/wikipedia-et.xml new file mode 100644 index 0000000000..b5b71ebc18 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/et/wikipedia-et.xml @@ -0,0 +1,23 @@ + + + +Vikipeedia (et) +UTF-8 +data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAQAAABIkb+zAAAEXElEQVR4Ae3cA4xs2QJG4b9aj41n27Zt27Zt27Zt27Zt+2KM5lXP924qOzs56VPjKSRnrfhv7NUsp08v98+PsxwT4nJ+nPunl8KZ8/WYQL+eMydJrx5/EhN603lAHpVJ5bzZNp035uyZXM7Uy3LmM7ms9CITzVRGQRfQBXQBXUAX0AV0AV1AF9AFdAFdQBfQBXQBXcBMjgU7M5+1vqtZyXqmc/IsZDEL+53LP3KRPCCv62+rWd7vnpx6v/M5Vd935OF5R+6RtRyeQ3NYNjKTxZw2p8tiduR8uWfemY3+dkjWM5fFLPU/8q9yrRwbHLP39WkHamPD5z1WnNMb/dpW/uAdbipu413+qcl2H3N3cXFv9DNNtvmg68uxMY6dPfewTZNvOauouqEdULlTY532IgqHua+ZxnoHm4ANj63biRNQPZedVPY5nTR1BXsVbNuyTlsDqy4tW/wK4NYiJ01AXMlm+wGrL1cDnWbLejC4m7T4DvAOOSkD4v0qztGyLzpUweOk4UXAp6XV72DNmU7qgPPao+DJ0uLTFPzXXGN5I9acU1o8vb14jZzUAfFeBdvMtuwLDlfwIKme2248T1p9MvY59zACLqcy4Kf5eQr+ZbYRfrAFaXHaf/FRGUZAfE/Bzwf8OKwpuJ/0vYRNPEpavT248rACbqfimtLiqxT8w4yIz+Of5qTV7+LHMqyAaf9R8Clp8ex2K7iXuAa4q7R6aXCH4QXE4xVsOq+0+DYFfzXth/ilnrT6Tvzb9DADlqwqDPjTd377FHwI3EBaPb0NPFaGGRBvoLBiUVr8EFS+KgN8KpYtDjvgQo6i8Hhp8ZJUjnIZaXXGNrxKhh0QX6TwHzPS4uco/FtPWr0T9jnPKAJuOOiCc/UqVG4jrf4AH5dRBPT8UWHg3/BvUfhl635ZcNXRBMSDqFxFWvwklVu07O/BT2RUAad0GIWPtexXgMpPt+xntAt3Gl1AvJjCPufasn4d/6NyY2n4DPzXzCgDzmEvhVdIw+tj02X8nsIPGvus7Xi8jDIgPkLhSPON5ad4t7gbletJ9S71n+BIA65C5dGal1g3nENM+weF70j1R3iNjDogfkrhn6br5dU/4UXS94EtF7+vUC8IjjzgrlRuJ33v27j2dTLbKXxd+r4Pn5RRB9RfxsL3y4H/i0dI9TFUrirOZDeuPh4B8TQqVyjH/ZtZqZ7KIRS+JJ6Fn8k4BNRL9IUPmncwbr/1b37lanbiLuMTEG+nsNfb8cOWq0DLFA7E/8yMU8AlNLnaoP/alSfKOAXEN6h8Qlo8k3UKq5bGLeAWFPa6oLT6Ogqvk3ELmPJ3wOtlgOe0B2w6//gFxCPBsjPIQN8JPi3jGDDvSDxVDPaCNnFNOdGczrNzIrEnRyR5XPZlMIdmKofnpTnx6B783QV0ARNOF9AFdAFdQBfQBXQBXUAX0AV0AV3A6OgCVjLJrEzlj5lk/jiVt2eSefvEvzTJxL84zMS/PM//Af1pBRpQAkXBAAAAAElFTkSuQmCC + + + + + + + + + + + + + +https://et.wikipedia.org/wiki/Eri:Otsimine + diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/eu/default.txt b/mobile/ios/Client/Assets/Search/SearchPlugins/eu/default.txt new file mode 100644 index 0000000000..ced95a5cb9 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/eu/default.txt @@ -0,0 +1 @@ +Google \ No newline at end of file diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/eu/eBay-es.xml b/mobile/ios/Client/Assets/Search/SearchPlugins/eu/eBay-es.xml new file mode 100644 index 0000000000..63ffe34d09 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/eu/eBay-es.xml @@ -0,0 +1,17 @@ + + + +eBay +ISO-8859-1 +data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAAEEfUpiAAAEWklEQVRYw8WXfUxVdRjHP79z7guXBBTEEBXEraktCydjWi7RZW82lr340kZNXaX+41p/tF42Xakw3ZqNyeZMnZVYtjnLNLXMzJFLA1FrgiIioveCIvcyLvdyzz3n6Q+7FxIM5YJ8/jovv/2e7/Oc5+X8FICIbCKCiCyTLmgArcUbmb2ugirP3u4rlIjkA3P+3aFIiYjQBa21eCMARv2XFB5Mo9sKRORZ+R80IMv0NAJQ7Q4CYDYdJmwGehZ+O0pEXEA7d0B1i0UnbUqpd7urvg2NXtAAQhWnmF5Ug9tnQDiIZYUBCBo+NPf0WQTLfqc1aDEyyU77kWyCB9Kpa/6NOHsS3Mm9tQcejLoZk0gzEgehb2zp9VP0Ro8S522qv2sJGkDLR6uwmpvBsvA8n8+3f/oA+Oakly3HbhI6swKA9u9dFB5MA6DKs5drvtO3Noh/4Tm8a9fRunUbzkmTGJGgs3ZfIx/sbiT/YQPTdxbjUgnKkcL7z3goPJjG0PhM0pMeo19iYBIrIvJaH4x3iIgeyYNUYAWQepc2jyqlSiPWX+mr/wAqhiwEWKPFGL6sWDfoXm1Ltjf0bYNQ+SnEMPB4b7WLX6vbOmu2uQyrrZqg4Ys+izRGDaBxzlwcUybjXbkaEYuSIzfIGz8E9eZZQudWoidPQ+mJ/F25nNrrRwBY/3MWADYAfXgyrcUb0UeOQNpDLJ85HIAnH4rHrP8CQ9nQEibyiH0In1Ys5KXsLSyadqhL1ztfgy1rHP5du8HpJOu9Kjb8dJ3aGyHE9KMSJhA6XwhiMT/na3ZXLiEt8dFoGt8TJy5vFrfvTOR2R6yJVNr/eXCPhJSINN1DFd7O5EggvX0oxg97aipeGXjCIrJaRByRduACGoBk7j85GlAwSMYBVmiAzuChawwydyVgzmd1TC+qGRABtsiF5fXSsmoNofIKbBPGI01NWIEAQws/AZJpCVjMWHeRKy0GLodGlbuDd55KYf2r6VjXD9Nxcj44UtDiM7C8FcgDmWxTJgnOUbwxdX+XarPYXJZHYtwoFuTsRInIMqDEnfc09swxOPPyOgdBXS3tPxxg8YubONfmoK5oAkp1qs/++AJpiXZ+XJ5IuGEXVls1yriBWCFMz3600QvZ7j+Hy5bI61P3IWLxedlMklzpzJuyE6A0GgHb2AyU3UHCooKogY7ySvSx49B8Q7jW4Oe8J8j4kXEA1N80uNDYweKJlQT2L8D5+CHs45be8jLgJnBtD0op3nriKFuPz2bHyZcxwn4SnGkR40T6wDKgBMC8epXgsTLCl6+AaeLIzcE1K499f/nRFIxJtvPVcS++YJjMFAdvz0hhWLyO+C8Sqi1Ggo3orrHoGQVI4DKixWFLnYlpGWz4ZSKjh+Uyf0rpf6ZBzD93vWFZYfnij3zZc3ppT6932Aa6zJTSKcj9bsDGWb/0gUuDaL8mMgnnyv2nMHpc7TKSU4FcIBvI6GdvbwLlwAmlVPQU9g9eOPBg6mUX4QAAAABJRU5ErkJggg== + + + + + + + +http://search.ebay.es/ + diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/eu/elebila.xml b/mobile/ios/Client/Assets/Search/SearchPlugins/eu/elebila.xml new file mode 100644 index 0000000000..7ea497c643 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/eu/elebila.xml @@ -0,0 +1,15 @@ + + + + Elebila + UTF-8 + data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAYAAADimHc4AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAA2hpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMC1jMDYxIDY0LjE0MDk0OSwgMjAxMC8xMi8wNy0xMDo1NzowMSAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtcE1NOk9yaWdpbmFsRG9jdW1lbnRJRD0ieG1wLmRpZDowMzgwMTE3NDA3MjA2ODExOEE2REI2ODhGMkQ2RTdCRSIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDoxOEVGQjlGNDQzNDAxMUU1QjMxRUUxQjk0MEM4ODg5MSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDoxOEVGQjlGMzQzNDAxMUU1QjMxRUUxQjk0MEM4ODg5MSIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ1M1LjEgTWFjaW50b3NoIj4gPHhtcE1NOkRlcml2ZWRGcm9tIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6MDM4MDExNzQwNzIwNjgxMTkyQjBGRkQ2MzQ3QjE4OUUiIHN0UmVmOmRvY3VtZW50SUQ9InhtcC5kaWQ6MDM4MDExNzQwNzIwNjgxMThBNkRCNjg4RjJENkU3QkUiLz4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+IDw/eHBhY2tldCBlbmQ9InIiPz5ISaQBAAADRUlEQVR42uxcMWsUQRTezD1SpZO0kkLQKtU1koBCEAQTELRIEbEJBwH/g9iJ/gD/gZWCmkYMhgT0D1hYpUiKgI2BQKoDwffgFo4jYWdndue9mXwfvCQsu7nve9/svJl3tzc3Ho+rCQYcI45nHHcroA+ccnzgeM3xRw7MTQxY5PiMxCfDGccmx54YICP/gGMVeUmKC46h4x9PkXwVLHC8cZM5H9DBIzHgNvKghoEYMI886MEhBTAABgAwAAYAMAAGADAABgAw4FqBLJLaXXnVeM7Gz5dFGCDvB5zw75uWkv9w6X7jeV+PD4owwVkb+T7JF8h5PncKDABgAAzwhMzpMrdjFaRswuzc7lsXYECHJoQWZ0xBAAyAAQAMuBZFuO1O1HrrQEMPxZBsuzKZvtaKGdp6qO0LxiwHp6+tyWsaYUGPS0X2MvKaDTUrepwG2VniqU2wpMdpktUwwZqeTloRPg20nFoJKfVQzGipifoUnnpEXPX/6lHTZ1G2qIdiRkmbZNXnWm2saekJ3gmHjlS57t3fT+YM0NLjQkdLSdDUg16QMmCAMoKKcOy6fWfpsakkaOqhGNIlQUsPpiDUABiQ5UYoh6Wtjx7SJhm7EbK6n/DVQ7EvpLH7tLoRC9FDXb3YxpPl6t7arcbrDr8fVbsff5lvRaTSgyKMIgwDOoHchnI75jj9aOpxqUjnlPyUeqgP0lLALjueI/rWQ32RLgl96gmagkp5QM6CHoolff78RrU1euE/mmaEWtmMaelpfE7Y95MEbYS27ZlcxSGkf2RBT6c1ILaPnuLjKJb1NNaA0p5ctKbH5Ui6JBOcNmmt9wGs6KG2pJs+kudL0sIqyIIeCiE9vfzy/WYTi0tPC3qoK+K+51uuCxp6qCviJRVok0UYgAEwAIABMMASSmiL4w5QBuUuwPcusLpcphJGkc/u1VLLO7spqOQv88umBpRqQlZFuEQTzH13tA/atpAtP3uQpQGzRuS6AsreANQAAAbAAAAGwAAABsAAAAbAACDIgDHSoGvAb6RBDf/EgPfIgxq+SDNO/vjG8QD5SIpzjmFdhDc5DpGTZDjjWOc4clMH1ji2OfZlbkKOeoE8dv+W4w7HDznwX4ABAMgHwgM+bKkgAAAAAElFTkSuQmCC + + + + + + http://elebila.elhuyar.eus + diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/eu/list.txt b/mobile/ios/Client/Assets/Search/SearchPlugins/eu/list.txt new file mode 100644 index 0000000000..f93efb7403 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/eu/list.txt @@ -0,0 +1,6 @@ +bing +eBay-es +elebila +google +wikipedia-eu +yahoo-es diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/eu/wikipedia-eu.xml b/mobile/ios/Client/Assets/Search/SearchPlugins/eu/wikipedia-eu.xml new file mode 100644 index 0000000000..2482d1cf57 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/eu/wikipedia-eu.xml @@ -0,0 +1,23 @@ + + + +Wikipedia (eu) +UTF-8 +data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAQAAABIkb+zAAAEXElEQVR4Ae3cA4xs2QJG4b9aj41n27Zt27Zt27Zt27Zt+2KM5lXP924qOzs56VPjKSRnrfhv7NUsp08v98+PsxwT4nJ+nPunl8KZ8/WYQL+eMydJrx5/EhN603lAHpVJ5bzZNp035uyZXM7Uy3LmM7ms9CITzVRGQRfQBXQBXUAX0AV0AV1AF9AFdAFdQBfQBXQBXcBMjgU7M5+1vqtZyXqmc/IsZDEL+53LP3KRPCCv62+rWd7vnpx6v/M5Vd935OF5R+6RtRyeQ3NYNjKTxZw2p8tiduR8uWfemY3+dkjWM5fFLPU/8q9yrRwbHLP39WkHamPD5z1WnNMb/dpW/uAdbipu413+qcl2H3N3cXFv9DNNtvmg68uxMY6dPfewTZNvOauouqEdULlTY532IgqHua+ZxnoHm4ANj63biRNQPZedVPY5nTR1BXsVbNuyTlsDqy4tW/wK4NYiJ01AXMlm+wGrL1cDnWbLejC4m7T4DvAOOSkD4v0qztGyLzpUweOk4UXAp6XV72DNmU7qgPPao+DJ0uLTFPzXXGN5I9acU1o8vb14jZzUAfFeBdvMtuwLDlfwIKme2248T1p9MvY59zACLqcy4Kf5eQr+ZbYRfrAFaXHaf/FRGUZAfE/Bzwf8OKwpuJ/0vYRNPEpavT248rACbqfimtLiqxT8w4yIz+Of5qTV7+LHMqyAaf9R8Clp8ex2K7iXuAa4q7R6aXCH4QXE4xVsOq+0+DYFfzXth/ilnrT6Tvzb9DADlqwqDPjTd377FHwI3EBaPb0NPFaGGRBvoLBiUVr8EFS+KgN8KpYtDjvgQo6i8Hhp8ZJUjnIZaXXGNrxKhh0QX6TwHzPS4uco/FtPWr0T9jnPKAJuOOiCc/UqVG4jrf4AH5dRBPT8UWHg3/BvUfhl635ZcNXRBMSDqFxFWvwklVu07O/BT2RUAad0GIWPtexXgMpPt+xntAt3Gl1AvJjCPufasn4d/6NyY2n4DPzXzCgDzmEvhVdIw+tj02X8nsIPGvus7Xi8jDIgPkLhSPON5ad4t7gbletJ9S71n+BIA65C5dGal1g3nENM+weF70j1R3iNjDogfkrhn6br5dU/4UXS94EtF7+vUC8IjjzgrlRuJ33v27j2dTLbKXxd+r4Pn5RRB9RfxsL3y4H/i0dI9TFUrirOZDeuPh4B8TQqVyjH/ZtZqZ7KIRS+JJ6Fn8k4BNRL9IUPmncwbr/1b37lanbiLuMTEG+nsNfb8cOWq0DLFA7E/8yMU8AlNLnaoP/alSfKOAXEN6h8Qlo8k3UKq5bGLeAWFPa6oLT6Ogqvk3ELmPJ3wOtlgOe0B2w6//gFxCPBsjPIQN8JPi3jGDDvSDxVDPaCNnFNOdGczrNzIrEnRyR5XPZlMIdmKofnpTnx6B783QV0ARNOF9AFdAFdQBfQBXQBXUAX0AV0AV3A6OgCVjLJrEzlj5lk/jiVt2eSefvEvzTJxL84zMS/PM//Af1pBRpQAkXBAAAAAElFTkSuQmCC + + + + + + + + + + + + + +https://eu.wikipedia.org/wiki/Berezi:Bilatu + diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/eu/yahoo-es.xml b/mobile/ios/Client/Assets/Search/SearchPlugins/eu/yahoo-es.xml new file mode 100644 index 0000000000..a9b50bc07e --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/eu/yahoo-es.xml @@ -0,0 +1,22 @@ + + + +Yahoo +UTF-8 +data:image/png;base64,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 + + + + + + + + + + + + +https://es.search.yahoo.com/ + diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/fa/default.txt b/mobile/ios/Client/Assets/Search/SearchPlugins/fa/default.txt new file mode 100644 index 0000000000..ced95a5cb9 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/fa/default.txt @@ -0,0 +1 @@ +Google \ No newline at end of file diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/fa/list.txt b/mobile/ios/Client/Assets/Search/SearchPlugins/fa/list.txt new file mode 100644 index 0000000000..21721d9d13 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/fa/list.txt @@ -0,0 +1,3 @@ +google +wikipedia-fa +yahoo diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/fa/wikipedia-fa.xml b/mobile/ios/Client/Assets/Search/SearchPlugins/fa/wikipedia-fa.xml new file mode 100644 index 0000000000..dd66c88f72 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/fa/wikipedia-fa.xml @@ -0,0 +1,23 @@ + + + +ویکی‌پدیا (fa) +UTF-8 +data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAQAAABIkb+zAAAEXElEQVR4Ae3cA4xs2QJG4b9aj41n27Zt27Zt27Zt27Zt+2KM5lXP924qOzs56VPjKSRnrfhv7NUsp08v98+PsxwT4nJ+nPunl8KZ8/WYQL+eMydJrx5/EhN603lAHpVJ5bzZNp035uyZXM7Uy3LmM7ms9CITzVRGQRfQBXQBXUAX0AV0AV1AF9AFdAFdQBfQBXQBXcBMjgU7M5+1vqtZyXqmc/IsZDEL+53LP3KRPCCv62+rWd7vnpx6v/M5Vd935OF5R+6RtRyeQ3NYNjKTxZw2p8tiduR8uWfemY3+dkjWM5fFLPU/8q9yrRwbHLP39WkHamPD5z1WnNMb/dpW/uAdbipu413+qcl2H3N3cXFv9DNNtvmg68uxMY6dPfewTZNvOauouqEdULlTY532IgqHua+ZxnoHm4ANj63biRNQPZedVPY5nTR1BXsVbNuyTlsDqy4tW/wK4NYiJ01AXMlm+wGrL1cDnWbLejC4m7T4DvAOOSkD4v0qztGyLzpUweOk4UXAp6XV72DNmU7qgPPao+DJ0uLTFPzXXGN5I9acU1o8vb14jZzUAfFeBdvMtuwLDlfwIKme2248T1p9MvY59zACLqcy4Kf5eQr+ZbYRfrAFaXHaf/FRGUZAfE/Bzwf8OKwpuJ/0vYRNPEpavT248rACbqfimtLiqxT8w4yIz+Of5qTV7+LHMqyAaf9R8Clp8ex2K7iXuAa4q7R6aXCH4QXE4xVsOq+0+DYFfzXth/ilnrT6Tvzb9DADlqwqDPjTd377FHwI3EBaPb0NPFaGGRBvoLBiUVr8EFS+KgN8KpYtDjvgQo6i8Hhp8ZJUjnIZaXXGNrxKhh0QX6TwHzPS4uco/FtPWr0T9jnPKAJuOOiCc/UqVG4jrf4AH5dRBPT8UWHg3/BvUfhl635ZcNXRBMSDqFxFWvwklVu07O/BT2RUAad0GIWPtexXgMpPt+xntAt3Gl1AvJjCPufasn4d/6NyY2n4DPzXzCgDzmEvhVdIw+tj02X8nsIPGvus7Xi8jDIgPkLhSPON5ad4t7gbletJ9S71n+BIA65C5dGal1g3nENM+weF70j1R3iNjDogfkrhn6br5dU/4UXS94EtF7+vUC8IjjzgrlRuJ33v27j2dTLbKXxd+r4Pn5RRB9RfxsL3y4H/i0dI9TFUrirOZDeuPh4B8TQqVyjH/ZtZqZ7KIRS+JJ6Fn8k4BNRL9IUPmncwbr/1b37lanbiLuMTEG+nsNfb8cOWq0DLFA7E/8yMU8AlNLnaoP/alSfKOAXEN6h8Qlo8k3UKq5bGLeAWFPa6oLT6Ogqvk3ELmPJ3wOtlgOe0B2w6//gFxCPBsjPIQN8JPi3jGDDvSDxVDPaCNnFNOdGczrNzIrEnRyR5XPZlMIdmKofnpTnx6B783QV0ARNOF9AFdAFdQBfQBXQBXUAX0AV0AV3A6OgCVjLJrEzlj5lk/jiVt2eSefvEvzTJxL84zMS/PM//Af1pBRpQAkXBAAAAAElFTkSuQmCC + + + + + + + + + + + + + +https://fa.wikipedia.org/wiki/ویژه:جستجو + diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/ff/amazon-france.xml b/mobile/ios/Client/Assets/Search/SearchPlugins/ff/amazon-france.xml new file mode 100644 index 0000000000..b93c2752fb --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/ff/amazon-france.xml @@ -0,0 +1,17 @@ + + + +Amazon.fr +Recherche Amazon.fr +ISO-8859-1 +data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAYAAADimHc4AAAAAXNSR0IArs4c6QAACixJREFUeAHtXQ1MldcZfuRXlIr4A1MoYAf+xKBswlKmdTJ1Q7RZdc5kM7XWsXaGWk2WTDPc0qlzOmZqnHGZoxrntuiydB1UXZSRqS1h03WTVdDa+osov/I3qAicve93ufTC/eHc673f+ejOmxzu933n/c77nuc5/+fcC/CJjKDLXArlFFopCB38igFjytgyxoz1APkM3ZVQ0KCbgwFjzZgbwmxo8M0B3rGAM+YjgunPdyhspKDFXASeInPVTMABCvHm2tbW+hCI5eaHO4cnNCRKEGhjArhd0qIIgSBFdrXZPgQ0AYqLgiZAE6AYAcXmdQ3QBChGQLF5XQM0AYoRUGxe1wBNgGIEFJvXNUAToBgBxeZ1DdAEKEZAsfkQxfZ9Nh8SEoKwsDD09PTg4cOHPqej+sVhQcC4ceOwdOlSZGRkICUlxQhJSUkIDuYNPaCpqQm3bt3CzZs3UVlZiVOnTqG8vNwgRzXAMvYdN4otdT1//nxBYIpHjx4Jb6W2tlbk5+eLqKgoS+WJCBnsj9ODwQqm38fHx4uSkhJvMXep39zcLNauXWt6HlwA7c4HaxGwZMkS0dDQ4BLMx3m4b98+dwCofm4dAhYuXOhTcyNLzObNm1WD7cq+NQiYNm2a4OYikNLZ2SmSk5NdgaDymTUIOHnyZCCx70/70KFDKsF2sm2JYyk02sHZs2dlRmyoqanB4cOHcfnyZbS2tmLq1KlYs2YN0tLSpN5vbGxEbGyspYaoTqx40YP75d2jR4/2l1BPFzt37hQjR450shkUFCR27drl6dUBcenp6U5pmJ1nB3tqmyAGr76+fgBArm4KCgqGBI0mX65edXq2YsWKIdNyACigusoX41JTUzFhwgTKr3uhzhnbtm1zr9AXc+TIkSF1WIHmGVJ6ZigpJ+DatWtG+9/d3e02v8eOHUNbW5vbeHtERUWF/dLjJy9tWEWUrwV1dHRgwYIFxrpOXFwcEhMTMWXKFCPwek9oaCi2b98uhVd7e7uUHi/kWUUs4wmvat6+fdsI58+f9wkfT7XIMUHqdxxvlV5bxxM/wEC9rVQqsnpSiT2mkmVqgEw+uDnicf/MmTMxY8YMozOdPHkyOEyaNAkTJ06UScZSOpYnIDMzEzk5OcjOzjYmW1Zqv/3BpCUJCA8Px+rVq7Fp0ybwMPXTLJYjYN68eSgsLAQtzn2ace/Pm6U64S1btuDcuXP/N+AzC5apAVu3bpUe77PjPJLh/V9enKOlDNAmjjFnWL9+PUd7FCuNgtjRgK51yKS/aNEiwkRO6urqxMaNG0VMTIyT39OnT5dKZMeOHU7vyvgZCB3lNSAiIgIHDx6kvA0tpaWlWL58ubEM7UrbfkrCVZxVnyknYOXKlcayw1AAXbhwAcuWLQPtarlVHY4EKO+E161b5xZQxwg6YuIRfNYdjnMEpQSMHj0avBs2lNy5cwdnzpwZSm1YzoSVEsCTLJmFMdllZj41JyPR0dEyaqboKCUgISFBKpOe2n3HBPj4oozwsrdVRCkBtL8rhYNMieWZ8+LFi6XSmzNnDkaM4PMI6kUpAXTmUwoB2kQ3JlmelGnDvv+wric9juMtSVmyhkrLH/HKJiVZWVlSEydW2rBhg1s/aQlDOh274qVLlwQtb7tNk4A1K840Q04Zos140dvba8fE42dXV5egFVJBTUd/OrQPIPbu3evxPU+Re/bs6U/LRMAH21RHAGeaRjieMHKKq6qqErRJL06fPi3oixlO8d4+sMAZIbUEcNOiUmjkNLhEmn2vloDIyEhx7949v3Fw8eJFo4bIJLh7926zwXZlTy0B3AzRdqN0X+AJ2OLiYkGza8GkXr161a0qnZ4QtGztCgwVz9QTwCTk5uYKBsZX2b9/v6DFuH4A6UyRqK6udkqODngJCzQ7/X5S3q1BAPvB+wJXrlxxAs3Tg7KyMsHfJXOVDzo5MeDcKa0pidmzZ7vUdXyfjg2J1DiI59IgXsmCePXLEHn0mZMKkTDOv3jxdJAd8lqSY4DrDUBvr9evenyBj56sWrXKCPSNGfCCnaPQsBXUvIAPbxUVFeHEiROO0U7XPOs9cOCAsXOWl5dnfDop9T1IiQW+/xXga3TSfWKkOy3gj+8B3/iV+3hvYnwm4P3XbL9Anf8W8Na/vTHpnS4f3OXzPnzssKWlxTgjSjXCu0QktHetAL5HKxkhEmsDnTSBH/WKRKISKj4TkDge+BNtv37uSeDvN4D8PwN/rZKwaFGVl2lVvPVj4E4Tfe+4A2im0Eb3wUTIdPqZ7Re/CLz0jM35FtoTGrvJPxnxmQA2HxEGvLEG+GaGzZmKu8AvSoHf/QPo7PKPg1ZJZewo4MHrNm/OXQO+9HP/eCZR4dwbYpC/VQhsfpP6AmoVZtEq76+fB+7uBn72dSDJ87F/9wkrjmG/f5ADHH8JGN/XF0QTAXZ59yP71eN/PlYNcDSfReeo3ngBmEJNk12YlGI6sv8b+rcFpyuBdqrSVpXYMcCqdFttznzK5iX7n7ETeO828PzTlI8Xbc+X7gdO/sc/OfEbAezOKGqSfvIcQMM2BHHKDtLVA5z9AHibCHmbnL9e7xCp6DKaBljL0wj0LwBcgIIdfL7RCLz8W+AMFRyWQmpqvz0XuEnPU34IdFN+/CF+JcDu0NNUgn65Gkjz8E2gqvs2Mt75EPgnlbC7D+xvB+6TxvCYmwzMozD3swCN9Z0KSncv8HoJ8Fox0NHXj4XSb4LcK6DmiAh79bitn/OXlwEhgJ3jDSeu0j9+FphG4+uhpLaNiLhlq+4f1gFcAm80ADXNQA+B4o3EUHOSNJ5KaozNNo9iuFA86WErmFobFF0CflQEVFQPtEYTMmPEV9NCaW79hJiBWr7dBYwAuzs8jHshkzrqbGAqAeKtcDvc9F+gjgh60GHLvL1kcjMXFgJEhtM/QKDdzbER9I9ZougZlVhZ6aH0j18AfvoX4H0axbkSHm7TLNgY+ZRfd6Xh+7OAE2B3jWsEt7PfnQ9wieJqrVLutwJ/uAjsKwU+qnfvCfcT9wuAvN9TP/COez1fY0wjwNFBHnFwx/fsLOAZao/NIqORatKb/wKOUYn/Gw0IZJZRRoYCcxKBd6mvCoQoIcAxI1HUbHx1JpBNIZ0yOmOS3HKAYxrurhvagTJqMspo3M4Alt/w3+jFnU1vnysnYLDDXOJmxQOfT7B1oHFjgTjqPGOfAMYQWWOorQ+ndp/7ho8f0XIB/VxcIwFdT4E7bW5OOHCH/kHt4NStd285AmQg4o7d25GRTLoqdIYlASqACpRNKktaVCKgCVCJPtnWBGgCFCOg2LyuAZoAxQgoNq9rgCZAMQKKzesaoAlQjIBi87oGaAIUI6DYvK4BmgDFCCg2zzWAtru1KEKgjQmoVGRcmyXsmYBCjYQyBAzseVeshALtsupgIgaMOWNvCJ0d0yQQBmYVQgafMR8gzEYuhXIKdGzJNGfMyrRqO4wpY8sY95f8/wEKvBLprcz3zwAAAABJRU5ErkJggg== + + + + + + +https://www.amazon.fr/ + diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/ff/default.txt b/mobile/ios/Client/Assets/Search/SearchPlugins/ff/default.txt new file mode 100644 index 0000000000..ced95a5cb9 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/ff/default.txt @@ -0,0 +1 @@ +Google \ No newline at end of file diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/ff/eBay-france.xml b/mobile/ios/Client/Assets/Search/SearchPlugins/ff/eBay-france.xml new file mode 100644 index 0000000000..0db6faa349 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/ff/eBay-france.xml @@ -0,0 +1,14 @@ + + + +eBay France +Recherche intégrée eBay +ISO-8859-1 +data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAAEEfUpiAAAEWklEQVRYw8WXfUxVdRjHP79z7guXBBTEEBXEraktCydjWi7RZW82lr340kZNXaX+41p/tF42Xakw3ZqNyeZMnZVYtjnLNLXMzJFLA1FrgiIioveCIvcyLvdyzz3n6Q+7FxIM5YJ8/jovv/2e7/Oc5+X8FICIbCKCiCyTLmgArcUbmb2ugirP3u4rlIjkA3P+3aFIiYjQBa21eCMARv2XFB5Mo9sKRORZ+R80IMv0NAJQ7Q4CYDYdJmwGehZ+O0pEXEA7d0B1i0UnbUqpd7urvg2NXtAAQhWnmF5Ug9tnQDiIZYUBCBo+NPf0WQTLfqc1aDEyyU77kWyCB9Kpa/6NOHsS3Mm9tQcejLoZk0gzEgehb2zp9VP0Ro8S522qv2sJGkDLR6uwmpvBsvA8n8+3f/oA+Oakly3HbhI6swKA9u9dFB5MA6DKs5drvtO3Noh/4Tm8a9fRunUbzkmTGJGgs3ZfIx/sbiT/YQPTdxbjUgnKkcL7z3goPJjG0PhM0pMeo19iYBIrIvJaH4x3iIgeyYNUYAWQepc2jyqlSiPWX+mr/wAqhiwEWKPFGL6sWDfoXm1Ltjf0bYNQ+SnEMPB4b7WLX6vbOmu2uQyrrZqg4Ys+izRGDaBxzlwcUybjXbkaEYuSIzfIGz8E9eZZQudWoidPQ+mJ/F25nNrrRwBY/3MWADYAfXgyrcUb0UeOQNpDLJ85HIAnH4rHrP8CQ9nQEibyiH0In1Ys5KXsLSyadqhL1ztfgy1rHP5du8HpJOu9Kjb8dJ3aGyHE9KMSJhA6XwhiMT/na3ZXLiEt8dFoGt8TJy5vFrfvTOR2R6yJVNr/eXCPhJSINN1DFd7O5EggvX0oxg97aipeGXjCIrJaRByRduACGoBk7j85GlAwSMYBVmiAzuChawwydyVgzmd1TC+qGRABtsiF5fXSsmoNofIKbBPGI01NWIEAQws/AZJpCVjMWHeRKy0GLodGlbuDd55KYf2r6VjXD9Nxcj44UtDiM7C8FcgDmWxTJgnOUbwxdX+XarPYXJZHYtwoFuTsRInIMqDEnfc09swxOPPyOgdBXS3tPxxg8YubONfmoK5oAkp1qs/++AJpiXZ+XJ5IuGEXVls1yriBWCFMz3600QvZ7j+Hy5bI61P3IWLxedlMklzpzJuyE6A0GgHb2AyU3UHCooKogY7ySvSx49B8Q7jW4Oe8J8j4kXEA1N80uNDYweKJlQT2L8D5+CHs45be8jLgJnBtD0op3nriKFuPz2bHyZcxwn4SnGkR40T6wDKgBMC8epXgsTLCl6+AaeLIzcE1K499f/nRFIxJtvPVcS++YJjMFAdvz0hhWLyO+C8Sqi1Ggo3orrHoGQVI4DKixWFLnYlpGWz4ZSKjh+Uyf0rpf6ZBzD93vWFZYfnij3zZc3ppT6932Aa6zJTSKcj9bsDGWb/0gUuDaL8mMgnnyv2nMHpc7TKSU4FcIBvI6GdvbwLlwAmlVPQU9g9eOPBg6mUX4QAAAABJRU5ErkJggg== + + + +http://search.ebay.fr/ + diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/ff/list.txt b/mobile/ios/Client/Assets/Search/SearchPlugins/ff/list.txt new file mode 100644 index 0000000000..0a2dad5ef7 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/ff/list.txt @@ -0,0 +1,6 @@ +amazon-france +bing +eBay-france +google +wikipedia-fr +yahoo-france diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/ff/wikipedia-fr.xml b/mobile/ios/Client/Assets/Search/SearchPlugins/ff/wikipedia-fr.xml new file mode 100644 index 0000000000..c600f076e1 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/ff/wikipedia-fr.xml @@ -0,0 +1,23 @@ + + + +Wikipédia (fr) +UTF-8 +data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAQAAABIkb+zAAAEXElEQVR4Ae3cA4xs2QJG4b9aj41n27Zt27Zt27Zt27Zt+2KM5lXP924qOzs56VPjKSRnrfhv7NUsp08v98+PsxwT4nJ+nPunl8KZ8/WYQL+eMydJrx5/EhN603lAHpVJ5bzZNp035uyZXM7Uy3LmM7ms9CITzVRGQRfQBXQBXUAX0AV0AV1AF9AFdAFdQBfQBXQBXcBMjgU7M5+1vqtZyXqmc/IsZDEL+53LP3KRPCCv62+rWd7vnpx6v/M5Vd935OF5R+6RtRyeQ3NYNjKTxZw2p8tiduR8uWfemY3+dkjWM5fFLPU/8q9yrRwbHLP39WkHamPD5z1WnNMb/dpW/uAdbipu413+qcl2H3N3cXFv9DNNtvmg68uxMY6dPfewTZNvOauouqEdULlTY532IgqHua+ZxnoHm4ANj63biRNQPZedVPY5nTR1BXsVbNuyTlsDqy4tW/wK4NYiJ01AXMlm+wGrL1cDnWbLejC4m7T4DvAOOSkD4v0qztGyLzpUweOk4UXAp6XV72DNmU7qgPPao+DJ0uLTFPzXXGN5I9acU1o8vb14jZzUAfFeBdvMtuwLDlfwIKme2248T1p9MvY59zACLqcy4Kf5eQr+ZbYRfrAFaXHaf/FRGUZAfE/Bzwf8OKwpuJ/0vYRNPEpavT248rACbqfimtLiqxT8w4yIz+Of5qTV7+LHMqyAaf9R8Clp8ex2K7iXuAa4q7R6aXCH4QXE4xVsOq+0+DYFfzXth/ilnrT6Tvzb9DADlqwqDPjTd377FHwI3EBaPb0NPFaGGRBvoLBiUVr8EFS+KgN8KpYtDjvgQo6i8Hhp8ZJUjnIZaXXGNrxKhh0QX6TwHzPS4uco/FtPWr0T9jnPKAJuOOiCc/UqVG4jrf4AH5dRBPT8UWHg3/BvUfhl635ZcNXRBMSDqFxFWvwklVu07O/BT2RUAad0GIWPtexXgMpPt+xntAt3Gl1AvJjCPufasn4d/6NyY2n4DPzXzCgDzmEvhVdIw+tj02X8nsIPGvus7Xi8jDIgPkLhSPON5ad4t7gbletJ9S71n+BIA65C5dGal1g3nENM+weF70j1R3iNjDogfkrhn6br5dU/4UXS94EtF7+vUC8IjjzgrlRuJ33v27j2dTLbKXxd+r4Pn5RRB9RfxsL3y4H/i0dI9TFUrirOZDeuPh4B8TQqVyjH/ZtZqZ7KIRS+JJ6Fn8k4BNRL9IUPmncwbr/1b37lanbiLuMTEG+nsNfb8cOWq0DLFA7E/8yMU8AlNLnaoP/alSfKOAXEN6h8Qlo8k3UKq5bGLeAWFPa6oLT6Ogqvk3ELmPJ3wOtlgOe0B2w6//gFxCPBsjPIQN8JPi3jGDDvSDxVDPaCNnFNOdGczrNzIrEnRyR5XPZlMIdmKofnpTnx6B783QV0ARNOF9AFdAFdQBfQBXQBXUAX0AV0AV3A6OgCVjLJrEzlj5lk/jiVt2eSefvEvzTJxL84zMS/PM//Af1pBRpQAkXBAAAAAElFTkSuQmCC + + + + + + + + + + + + + +https://fr.wikipedia.org/wiki/Spécial:Recherche + diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/ff/yahoo-france.xml b/mobile/ios/Client/Assets/Search/SearchPlugins/ff/yahoo-france.xml new file mode 100644 index 0000000000..5bdc936c24 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/ff/yahoo-france.xml @@ -0,0 +1,22 @@ + + + +Yahoo +UTF-8 +data:image/png;base64,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 + + + + + + + + + + + + +https://fr.search.yahoo.com/ + diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/fi/default.txt b/mobile/ios/Client/Assets/Search/SearchPlugins/fi/default.txt new file mode 100644 index 0000000000..ced95a5cb9 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/fi/default.txt @@ -0,0 +1 @@ +Google \ No newline at end of file diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/fi/list.txt b/mobile/ios/Client/Assets/Search/SearchPlugins/fi/list.txt new file mode 100644 index 0000000000..586e5b8594 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/fi/list.txt @@ -0,0 +1,5 @@ +google +twitter +wikipedia-fi +yahoo-fi +amazondotcom diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/fi/wikipedia-fi.xml b/mobile/ios/Client/Assets/Search/SearchPlugins/fi/wikipedia-fi.xml new file mode 100644 index 0000000000..c82bee0195 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/fi/wikipedia-fi.xml @@ -0,0 +1,23 @@ + + + +Wikipedia (fi) +UTF-8 +data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAQAAABIkb+zAAAEXElEQVR4Ae3cA4xs2QJG4b9aj41n27Zt27Zt27Zt27Zt+2KM5lXP924qOzs56VPjKSRnrfhv7NUsp08v98+PsxwT4nJ+nPunl8KZ8/WYQL+eMydJrx5/EhN603lAHpVJ5bzZNp035uyZXM7Uy3LmM7ms9CITzVRGQRfQBXQBXUAX0AV0AV1AF9AFdAFdQBfQBXQBXcBMjgU7M5+1vqtZyXqmc/IsZDEL+53LP3KRPCCv62+rWd7vnpx6v/M5Vd935OF5R+6RtRyeQ3NYNjKTxZw2p8tiduR8uWfemY3+dkjWM5fFLPU/8q9yrRwbHLP39WkHamPD5z1WnNMb/dpW/uAdbipu413+qcl2H3N3cXFv9DNNtvmg68uxMY6dPfewTZNvOauouqEdULlTY532IgqHua+ZxnoHm4ANj63biRNQPZedVPY5nTR1BXsVbNuyTlsDqy4tW/wK4NYiJ01AXMlm+wGrL1cDnWbLejC4m7T4DvAOOSkD4v0qztGyLzpUweOk4UXAp6XV72DNmU7qgPPao+DJ0uLTFPzXXGN5I9acU1o8vb14jZzUAfFeBdvMtuwLDlfwIKme2248T1p9MvY59zACLqcy4Kf5eQr+ZbYRfrAFaXHaf/FRGUZAfE/Bzwf8OKwpuJ/0vYRNPEpavT248rACbqfimtLiqxT8w4yIz+Of5qTV7+LHMqyAaf9R8Clp8ex2K7iXuAa4q7R6aXCH4QXE4xVsOq+0+DYFfzXth/ilnrT6Tvzb9DADlqwqDPjTd377FHwI3EBaPb0NPFaGGRBvoLBiUVr8EFS+KgN8KpYtDjvgQo6i8Hhp8ZJUjnIZaXXGNrxKhh0QX6TwHzPS4uco/FtPWr0T9jnPKAJuOOiCc/UqVG4jrf4AH5dRBPT8UWHg3/BvUfhl635ZcNXRBMSDqFxFWvwklVu07O/BT2RUAad0GIWPtexXgMpPt+xntAt3Gl1AvJjCPufasn4d/6NyY2n4DPzXzCgDzmEvhVdIw+tj02X8nsIPGvus7Xi8jDIgPkLhSPON5ad4t7gbletJ9S71n+BIA65C5dGal1g3nENM+weF70j1R3iNjDogfkrhn6br5dU/4UXS94EtF7+vUC8IjjzgrlRuJ33v27j2dTLbKXxd+r4Pn5RRB9RfxsL3y4H/i0dI9TFUrirOZDeuPh4B8TQqVyjH/ZtZqZ7KIRS+JJ6Fn8k4BNRL9IUPmncwbr/1b37lanbiLuMTEG+nsNfb8cOWq0DLFA7E/8yMU8AlNLnaoP/alSfKOAXEN6h8Qlo8k3UKq5bGLeAWFPa6oLT6Ogqvk3ELmPJ3wOtlgOe0B2w6//gFxCPBsjPIQN8JPi3jGDDvSDxVDPaCNnFNOdGczrNzIrEnRyR5XPZlMIdmKofnpTnx6B783QV0ARNOF9AFdAFdQBfQBXQBXUAX0AV0AV3A6OgCVjLJrEzlj5lk/jiVt2eSefvEvzTJxL84zMS/PM//Af1pBRpQAkXBAAAAAElFTkSuQmCC + + + + + + + + + + + + + +https://fi.wikipedia.org/wiki/Toiminnot:Haku + diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/fi/yahoo-fi.xml b/mobile/ios/Client/Assets/Search/SearchPlugins/fi/yahoo-fi.xml new file mode 100644 index 0000000000..be75dc4319 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/fi/yahoo-fi.xml @@ -0,0 +1,22 @@ + + + +Yahoo +UTF-8 +data:image/png;base64,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 + + + + + + + + + + + + +https://fi.search.yahoo.com/ + diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/fr/default.txt b/mobile/ios/Client/Assets/Search/SearchPlugins/fr/default.txt new file mode 100644 index 0000000000..ced95a5cb9 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/fr/default.txt @@ -0,0 +1 @@ +Google \ No newline at end of file diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/fr/eBay-france.xml b/mobile/ios/Client/Assets/Search/SearchPlugins/fr/eBay-france.xml new file mode 100644 index 0000000000..0db6faa349 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/fr/eBay-france.xml @@ -0,0 +1,14 @@ + + + +eBay France +Recherche intégrée eBay +ISO-8859-1 +data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAAEEfUpiAAAEWklEQVRYw8WXfUxVdRjHP79z7guXBBTEEBXEraktCydjWi7RZW82lr340kZNXaX+41p/tF42Xakw3ZqNyeZMnZVYtjnLNLXMzJFLA1FrgiIioveCIvcyLvdyzz3n6Q+7FxIM5YJ8/jovv/2e7/Oc5+X8FICIbCKCiCyTLmgArcUbmb2ugirP3u4rlIjkA3P+3aFIiYjQBa21eCMARv2XFB5Mo9sKRORZ+R80IMv0NAJQ7Q4CYDYdJmwGehZ+O0pEXEA7d0B1i0UnbUqpd7urvg2NXtAAQhWnmF5Ug9tnQDiIZYUBCBo+NPf0WQTLfqc1aDEyyU77kWyCB9Kpa/6NOHsS3Mm9tQcejLoZk0gzEgehb2zp9VP0Ro8S522qv2sJGkDLR6uwmpvBsvA8n8+3f/oA+Oakly3HbhI6swKA9u9dFB5MA6DKs5drvtO3Noh/4Tm8a9fRunUbzkmTGJGgs3ZfIx/sbiT/YQPTdxbjUgnKkcL7z3goPJjG0PhM0pMeo19iYBIrIvJaH4x3iIgeyYNUYAWQepc2jyqlSiPWX+mr/wAqhiwEWKPFGL6sWDfoXm1Ltjf0bYNQ+SnEMPB4b7WLX6vbOmu2uQyrrZqg4Ys+izRGDaBxzlwcUybjXbkaEYuSIzfIGz8E9eZZQudWoidPQ+mJ/F25nNrrRwBY/3MWADYAfXgyrcUb0UeOQNpDLJ85HIAnH4rHrP8CQ9nQEibyiH0In1Ys5KXsLSyadqhL1ztfgy1rHP5du8HpJOu9Kjb8dJ3aGyHE9KMSJhA6XwhiMT/na3ZXLiEt8dFoGt8TJy5vFrfvTOR2R6yJVNr/eXCPhJSINN1DFd7O5EggvX0oxg97aipeGXjCIrJaRByRduACGoBk7j85GlAwSMYBVmiAzuChawwydyVgzmd1TC+qGRABtsiF5fXSsmoNofIKbBPGI01NWIEAQws/AZJpCVjMWHeRKy0GLodGlbuDd55KYf2r6VjXD9Nxcj44UtDiM7C8FcgDmWxTJgnOUbwxdX+XarPYXJZHYtwoFuTsRInIMqDEnfc09swxOPPyOgdBXS3tPxxg8YubONfmoK5oAkp1qs/++AJpiXZ+XJ5IuGEXVls1yriBWCFMz3600QvZ7j+Hy5bI61P3IWLxedlMklzpzJuyE6A0GgHb2AyU3UHCooKogY7ySvSx49B8Q7jW4Oe8J8j4kXEA1N80uNDYweKJlQT2L8D5+CHs45be8jLgJnBtD0op3nriKFuPz2bHyZcxwn4SnGkR40T6wDKgBMC8epXgsTLCl6+AaeLIzcE1K499f/nRFIxJtvPVcS++YJjMFAdvz0hhWLyO+C8Sqi1Ggo3orrHoGQVI4DKixWFLnYlpGWz4ZSKjh+Uyf0rpf6ZBzD93vWFZYfnij3zZc3ppT6932Aa6zJTSKcj9bsDGWb/0gUuDaL8mMgnnyv2nMHpc7TKSU4FcIBvI6GdvbwLlwAmlVPQU9g9eOPBg6mUX4QAAAABJRU5ErkJggg== + + + +http://search.ebay.fr/ + diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/fr/list.txt b/mobile/ios/Client/Assets/Search/SearchPlugins/fr/list.txt new file mode 100644 index 0000000000..94b850738a --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/fr/list.txt @@ -0,0 +1,8 @@ +bing +duckduckgo +eBay-france +google +qwant +twitter +wikipedia-fr +yahoo-france diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/fr/wikipedia-fr.xml b/mobile/ios/Client/Assets/Search/SearchPlugins/fr/wikipedia-fr.xml new file mode 100644 index 0000000000..c600f076e1 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/fr/wikipedia-fr.xml @@ -0,0 +1,23 @@ + + + +Wikipédia (fr) +UTF-8 +data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAQAAABIkb+zAAAEXElEQVR4Ae3cA4xs2QJG4b9aj41n27Zt27Zt27Zt27Zt+2KM5lXP924qOzs56VPjKSRnrfhv7NUsp08v98+PsxwT4nJ+nPunl8KZ8/WYQL+eMydJrx5/EhN603lAHpVJ5bzZNp035uyZXM7Uy3LmM7ms9CITzVRGQRfQBXQBXUAX0AV0AV1AF9AFdAFdQBfQBXQBXcBMjgU7M5+1vqtZyXqmc/IsZDEL+53LP3KRPCCv62+rWd7vnpx6v/M5Vd935OF5R+6RtRyeQ3NYNjKTxZw2p8tiduR8uWfemY3+dkjWM5fFLPU/8q9yrRwbHLP39WkHamPD5z1WnNMb/dpW/uAdbipu413+qcl2H3N3cXFv9DNNtvmg68uxMY6dPfewTZNvOauouqEdULlTY532IgqHua+ZxnoHm4ANj63biRNQPZedVPY5nTR1BXsVbNuyTlsDqy4tW/wK4NYiJ01AXMlm+wGrL1cDnWbLejC4m7T4DvAOOSkD4v0qztGyLzpUweOk4UXAp6XV72DNmU7qgPPao+DJ0uLTFPzXXGN5I9acU1o8vb14jZzUAfFeBdvMtuwLDlfwIKme2248T1p9MvY59zACLqcy4Kf5eQr+ZbYRfrAFaXHaf/FRGUZAfE/Bzwf8OKwpuJ/0vYRNPEpavT248rACbqfimtLiqxT8w4yIz+Of5qTV7+LHMqyAaf9R8Clp8ex2K7iXuAa4q7R6aXCH4QXE4xVsOq+0+DYFfzXth/ilnrT6Tvzb9DADlqwqDPjTd377FHwI3EBaPb0NPFaGGRBvoLBiUVr8EFS+KgN8KpYtDjvgQo6i8Hhp8ZJUjnIZaXXGNrxKhh0QX6TwHzPS4uco/FtPWr0T9jnPKAJuOOiCc/UqVG4jrf4AH5dRBPT8UWHg3/BvUfhl635ZcNXRBMSDqFxFWvwklVu07O/BT2RUAad0GIWPtexXgMpPt+xntAt3Gl1AvJjCPufasn4d/6NyY2n4DPzXzCgDzmEvhVdIw+tj02X8nsIPGvus7Xi8jDIgPkLhSPON5ad4t7gbletJ9S71n+BIA65C5dGal1g3nENM+weF70j1R3iNjDogfkrhn6br5dU/4UXS94EtF7+vUC8IjjzgrlRuJ33v27j2dTLbKXxd+r4Pn5RRB9RfxsL3y4H/i0dI9TFUrirOZDeuPh4B8TQqVyjH/ZtZqZ7KIRS+JJ6Fn8k4BNRL9IUPmncwbr/1b37lanbiLuMTEG+nsNfb8cOWq0DLFA7E/8yMU8AlNLnaoP/alSfKOAXEN6h8Qlo8k3UKq5bGLeAWFPa6oLT6Ogqvk3ELmPJ3wOtlgOe0B2w6//gFxCPBsjPIQN8JPi3jGDDvSDxVDPaCNnFNOdGczrNzIrEnRyR5XPZlMIdmKofnpTnx6B783QV0ARNOF9AFdAFdQBfQBXQBXUAX0AV0AV3A6OgCVjLJrEzlj5lk/jiVt2eSefvEvzTJxL84zMS/PM//Af1pBRpQAkXBAAAAAElFTkSuQmCC + + + + + + + + + + + + + +https://fr.wikipedia.org/wiki/Spécial:Recherche + diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/fr/yahoo-france.xml b/mobile/ios/Client/Assets/Search/SearchPlugins/fr/yahoo-france.xml new file mode 100644 index 0000000000..5bdc936c24 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/fr/yahoo-france.xml @@ -0,0 +1,22 @@ + + + +Yahoo +UTF-8 +data:image/png;base64,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 + + + + + + + + + + + + +https://fr.search.yahoo.com/ + diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/fy-NL/bolcom-fy-NL.xml b/mobile/ios/Client/Assets/Search/SearchPlugins/fy-NL/bolcom-fy-NL.xml new file mode 100644 index 0000000000..7e93a46a54 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/fy-NL/bolcom-fy-NL.xml @@ -0,0 +1,12 @@ + + + +bol.com +UTF-8 +data:image/png;base64,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 +http://www.bol.com/ + + + diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/fy-NL/default.txt b/mobile/ios/Client/Assets/Search/SearchPlugins/fy-NL/default.txt new file mode 100644 index 0000000000..ced95a5cb9 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/fy-NL/default.txt @@ -0,0 +1 @@ +Google \ No newline at end of file diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/fy-NL/list.txt b/mobile/ios/Client/Assets/Search/SearchPlugins/fy-NL/list.txt new file mode 100644 index 0000000000..b7fdb1e9be --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/fy-NL/list.txt @@ -0,0 +1,3 @@ +google +wikipedia-fy-NL +bolcom-fy-NL diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/fy-NL/wikipedia-fy-NL.xml b/mobile/ios/Client/Assets/Search/SearchPlugins/fy-NL/wikipedia-fy-NL.xml new file mode 100644 index 0000000000..4260d9993b --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/fy-NL/wikipedia-fy-NL.xml @@ -0,0 +1,23 @@ + + + +Wikipedia (fy) +UTF-8 +data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAQAAABIkb+zAAAEXElEQVR4Ae3cA4xs2QJG4b9aj41n27Zt27Zt27Zt27Zt+2KM5lXP924qOzs56VPjKSRnrfhv7NUsp08v98+PsxwT4nJ+nPunl8KZ8/WYQL+eMydJrx5/EhN603lAHpVJ5bzZNp035uyZXM7Uy3LmM7ms9CITzVRGQRfQBXQBXUAX0AV0AV1AF9AFdAFdQBfQBXQBXcBMjgU7M5+1vqtZyXqmc/IsZDEL+53LP3KRPCCv62+rWd7vnpx6v/M5Vd935OF5R+6RtRyeQ3NYNjKTxZw2p8tiduR8uWfemY3+dkjWM5fFLPU/8q9yrRwbHLP39WkHamPD5z1WnNMb/dpW/uAdbipu413+qcl2H3N3cXFv9DNNtvmg68uxMY6dPfewTZNvOauouqEdULlTY532IgqHua+ZxnoHm4ANj63biRNQPZedVPY5nTR1BXsVbNuyTlsDqy4tW/wK4NYiJ01AXMlm+wGrL1cDnWbLejC4m7T4DvAOOSkD4v0qztGyLzpUweOk4UXAp6XV72DNmU7qgPPao+DJ0uLTFPzXXGN5I9acU1o8vb14jZzUAfFeBdvMtuwLDlfwIKme2248T1p9MvY59zACLqcy4Kf5eQr+ZbYRfrAFaXHaf/FRGUZAfE/Bzwf8OKwpuJ/0vYRNPEpavT248rACbqfimtLiqxT8w4yIz+Of5qTV7+LHMqyAaf9R8Clp8ex2K7iXuAa4q7R6aXCH4QXE4xVsOq+0+DYFfzXth/ilnrT6Tvzb9DADlqwqDPjTd377FHwI3EBaPb0NPFaGGRBvoLBiUVr8EFS+KgN8KpYtDjvgQo6i8Hhp8ZJUjnIZaXXGNrxKhh0QX6TwHzPS4uco/FtPWr0T9jnPKAJuOOiCc/UqVG4jrf4AH5dRBPT8UWHg3/BvUfhl635ZcNXRBMSDqFxFWvwklVu07O/BT2RUAad0GIWPtexXgMpPt+xntAt3Gl1AvJjCPufasn4d/6NyY2n4DPzXzCgDzmEvhVdIw+tj02X8nsIPGvus7Xi8jDIgPkLhSPON5ad4t7gbletJ9S71n+BIA65C5dGal1g3nENM+weF70j1R3iNjDogfkrhn6br5dU/4UXS94EtF7+vUC8IjjzgrlRuJ33v27j2dTLbKXxd+r4Pn5RRB9RfxsL3y4H/i0dI9TFUrirOZDeuPh4B8TQqVyjH/ZtZqZ7KIRS+JJ6Fn8k4BNRL9IUPmncwbr/1b37lanbiLuMTEG+nsNfb8cOWq0DLFA7E/8yMU8AlNLnaoP/alSfKOAXEN6h8Qlo8k3UKq5bGLeAWFPa6oLT6Ogqvk3ELmPJ3wOtlgOe0B2w6//gFxCPBsjPIQN8JPi3jGDDvSDxVDPaCNnFNOdGczrNzIrEnRyR5XPZlMIdmKofnpTnx6B783QV0ARNOF9AFdAFdQBfQBXQBXUAX0AV0AV3A6OgCVjLJrEzlj5lk/jiVt2eSefvEvzTJxL84zMS/PM//Af1pBRpQAkXBAAAAAElFTkSuQmCC + + + + + + + + + + + + + +https://fy.wikipedia.org/wiki/Wiki:Sykje + diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/ga-IE/amazon-en-GB.xml b/mobile/ios/Client/Assets/Search/SearchPlugins/ga-IE/amazon-en-GB.xml new file mode 100644 index 0000000000..1ee5021e2c --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/ga-IE/amazon-en-GB.xml @@ -0,0 +1,17 @@ + + + +Amazon.co.uk +Amazon.co.uk: Cuardach Comhtháite +ISO-8859-1 +data:image/png;base64,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 + + + + + + +https://www.amazon.co.uk/ + diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/ga-IE/default.txt b/mobile/ios/Client/Assets/Search/SearchPlugins/ga-IE/default.txt new file mode 100644 index 0000000000..ced95a5cb9 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/ga-IE/default.txt @@ -0,0 +1 @@ +Google \ No newline at end of file diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/ga-IE/list.txt b/mobile/ios/Client/Assets/Search/SearchPlugins/ga-IE/list.txt new file mode 100644 index 0000000000..2be6aca20f --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/ga-IE/list.txt @@ -0,0 +1,5 @@ +amazon-en-GB +google +tearma +twitter +wikipedia-ga-IE diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/ga-IE/tearma.xml b/mobile/ios/Client/Assets/Search/SearchPlugins/ga-IE/tearma.xml new file mode 100644 index 0000000000..e9f9d21117 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/ga-IE/tearma.xml @@ -0,0 +1,16 @@ + + + +tearma.ie +UTF-8 +data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAYAAADimHc4AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAA2hpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMC1jMDYxIDY0LjE0MDk0OSwgMjAxMC8xMi8wNy0xMDo1NzowMSAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtcE1NOk9yaWdpbmFsRG9jdW1lbnRJRD0ieG1wLmRpZDowMzgwMTE3NDA3MjA2ODExOEE2REI2ODhGMkQ2RTdCRSIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDoyNkIzMUU3MTM3OEQxMUU1QTUxRjhDODRGRkUxMDlBRCIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDoyNkIzMUU3MDM3OEQxMUU1QTUxRjhDODRGRkUxMDlBRCIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ1M1LjEgTWFjaW50b3NoIj4gPHhtcE1NOkRlcml2ZWRGcm9tIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6Rjc3RjExNzQwNzIwNjgxMThBNkQ5OEE0NTNEODRERTgiIHN0UmVmOmRvY3VtZW50SUQ9InhtcC5kaWQ6MDM4MDExNzQwNzIwNjgxMThBNkRCNjg4RjJENkU3QkUiLz4gPC9yZGY6RGVzY3Jp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 + + + + + + +http://m.tearma.ie/ + diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/ga-IE/wikipedia-ga-IE.xml b/mobile/ios/Client/Assets/Search/SearchPlugins/ga-IE/wikipedia-ga-IE.xml new file mode 100644 index 0000000000..67d3bfb863 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/ga-IE/wikipedia-ga-IE.xml @@ -0,0 +1,23 @@ + + + +Vicipéid (ga) +UTF-8 +data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAQAAABIkb+zAAAEXElEQVR4Ae3cA4xs2QJG4b9aj41n27Zt27Zt27Zt27Zt+2KM5lXP924qOzs56VPjKSRnrfhv7NUsp08v98+PsxwT4nJ+nPunl8KZ8/WYQL+eMydJrx5/EhN603lAHpVJ5bzZNp035uyZXM7Uy3LmM7ms9CITzVRGQRfQBXQBXUAX0AV0AV1AF9AFdAFdQBfQBXQBXcBMjgU7M5+1vqtZyXqmc/IsZDEL+53LP3KRPCCv62+rWd7vnpx6v/M5Vd935OF5R+6RtRyeQ3NYNjKTxZw2p8tiduR8uWfemY3+dkjWM5fFLPU/8q9yrRwbHLP39WkHamPD5z1WnNMb/dpW/uAdbipu413+qcl2H3N3cXFv9DNNtvmg68uxMY6dPfewTZNvOauouqEdULlTY532IgqHua+ZxnoHm4ANj63biRNQPZedVPY5nTR1BXsVbNuyTlsDqy4tW/wK4NYiJ01AXMlm+wGrL1cDnWbLejC4m7T4DvAOOSkD4v0qztGyLzpUweOk4UXAp6XV72DNmU7qgPPao+DJ0uLTFPzXXGN5I9acU1o8vb14jZzUAfFeBdvMtuwLDlfwIKme2248T1p9MvY59zACLqcy4Kf5eQr+ZbYRfrAFaXHaf/FRGUZAfE/Bzwf8OKwpuJ/0vYRNPEpavT248rACbqfimtLiqxT8w4yIz+Of5qTV7+LHMqyAaf9R8Clp8ex2K7iXuAa4q7R6aXCH4QXE4xVsOq+0+DYFfzXth/ilnrT6Tvzb9DADlqwqDPjTd377FHwI3EBaPb0NPFaGGRBvoLBiUVr8EFS+KgN8KpYtDjvgQo6i8Hhp8ZJUjnIZaXXGNrxKhh0QX6TwHzPS4uco/FtPWr0T9jnPKAJuOOiCc/UqVG4jrf4AH5dRBPT8UWHg3/BvUfhl635ZcNXRBMSDqFxFWvwklVu07O/BT2RUAad0GIWPtexXgMpPt+xntAt3Gl1AvJjCPufasn4d/6NyY2n4DPzXzCgDzmEvhVdIw+tj02X8nsIPGvus7Xi8jDIgPkLhSPON5ad4t7gbletJ9S71n+BIA65C5dGal1g3nENM+weF70j1R3iNjDogfkrhn6br5dU/4UXS94EtF7+vUC8IjjzgrlRuJ33v27j2dTLbKXxd+r4Pn5RRB9RfxsL3y4H/i0dI9TFUrirOZDeuPh4B8TQqVyjH/ZtZqZ7KIRS+JJ6Fn8k4BNRL9IUPmncwbr/1b37lanbiLuMTEG+nsNfb8cOWq0DLFA7E/8yMU8AlNLnaoP/alSfKOAXEN6h8Qlo8k3UKq5bGLeAWFPa6oLT6Ogqvk3ELmPJ3wOtlgOe0B2w6//gFxCPBsjPIQN8JPi3jGDDvSDxVDPaCNnFNOdGczrNzIrEnRyR5XPZlMIdmKofnpTnx6B783QV0ARNOF9AFdAFdQBfQBXQBXUAX0AV0AV3A6OgCVjLJrEzlj5lk/jiVt2eSefvEvzTJxL84zMS/PM//Af1pBRpQAkXBAAAAAElFTkSuQmCC + + + + + + + + + + + + + +https://ga.wikipedia.org/wiki/Speisialta:Search + diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/gd/default.txt b/mobile/ios/Client/Assets/Search/SearchPlugins/gd/default.txt new file mode 100644 index 0000000000..ced95a5cb9 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/gd/default.txt @@ -0,0 +1 @@ +Google \ No newline at end of file diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/gd/faclair-beag.xml b/mobile/ios/Client/Assets/Search/SearchPlugins/gd/faclair-beag.xml new file mode 100644 index 0000000000..e57d903c4a --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/gd/faclair-beag.xml @@ -0,0 +1,13 @@ + + + +Am Faclair Beag +UTF-8 +data:image/png;base64,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 + + + +http://www.faclair.com/m + \ No newline at end of file diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/gd/list.txt b/mobile/ios/Client/Assets/Search/SearchPlugins/gd/list.txt new file mode 100644 index 0000000000..8402ab420e --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/gd/list.txt @@ -0,0 +1,6 @@ +bing +duckduckgo +faclair-beag +google +wikipedia-gd +yahoo-en-GB diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/gd/wikipedia-gd.xml b/mobile/ios/Client/Assets/Search/SearchPlugins/gd/wikipedia-gd.xml new file mode 100644 index 0000000000..4358141999 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/gd/wikipedia-gd.xml @@ -0,0 +1,23 @@ + + + +Wikipedia (gd) +UTF-8 +data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAQAAABIkb+zAAAEXElEQVR4Ae3cA4xs2QJG4b9aj41n27Zt27Zt27Zt27Zt+2KM5lXP924qOzs56VPjKSRnrfhv7NUsp08v98+PsxwT4nJ+nPunl8KZ8/WYQL+eMydJrx5/EhN603lAHpVJ5bzZNp035uyZXM7Uy3LmM7ms9CITzVRGQRfQBXQBXUAX0AV0AV1AF9AFdAFdQBfQBXQBXcBMjgU7M5+1vqtZyXqmc/IsZDEL+53LP3KRPCCv62+rWd7vnpx6v/M5Vd935OF5R+6RtRyeQ3NYNjKTxZw2p8tiduR8uWfemY3+dkjWM5fFLPU/8q9yrRwbHLP39WkHamPD5z1WnNMb/dpW/uAdbipu413+qcl2H3N3cXFv9DNNtvmg68uxMY6dPfewTZNvOauouqEdULlTY532IgqHua+ZxnoHm4ANj63biRNQPZedVPY5nTR1BXsVbNuyTlsDqy4tW/wK4NYiJ01AXMlm+wGrL1cDnWbLejC4m7T4DvAOOSkD4v0qztGyLzpUweOk4UXAp6XV72DNmU7qgPPao+DJ0uLTFPzXXGN5I9acU1o8vb14jZzUAfFeBdvMtuwLDlfwIKme2248T1p9MvY59zACLqcy4Kf5eQr+ZbYRfrAFaXHaf/FRGUZAfE/Bzwf8OKwpuJ/0vYRNPEpavT248rACbqfimtLiqxT8w4yIz+Of5qTV7+LHMqyAaf9R8Clp8ex2K7iXuAa4q7R6aXCH4QXE4xVsOq+0+DYFfzXth/ilnrT6Tvzb9DADlqwqDPjTd377FHwI3EBaPb0NPFaGGRBvoLBiUVr8EFS+KgN8KpYtDjvgQo6i8Hhp8ZJUjnIZaXXGNrxKhh0QX6TwHzPS4uco/FtPWr0T9jnPKAJuOOiCc/UqVG4jrf4AH5dRBPT8UWHg3/BvUfhl635ZcNXRBMSDqFxFWvwklVu07O/BT2RUAad0GIWPtexXgMpPt+xntAt3Gl1AvJjCPufasn4d/6NyY2n4DPzXzCgDzmEvhVdIw+tj02X8nsIPGvus7Xi8jDIgPkLhSPON5ad4t7gbletJ9S71n+BIA65C5dGal1g3nENM+weF70j1R3iNjDogfkrhn6br5dU/4UXS94EtF7+vUC8IjjzgrlRuJ33v27j2dTLbKXxd+r4Pn5RRB9RfxsL3y4H/i0dI9TFUrirOZDeuPh4B8TQqVyjH/ZtZqZ7KIRS+JJ6Fn8k4BNRL9IUPmncwbr/1b37lanbiLuMTEG+nsNfb8cOWq0DLFA7E/8yMU8AlNLnaoP/alSfKOAXEN6h8Qlo8k3UKq5bGLeAWFPa6oLT6Ogqvk3ELmPJ3wOtlgOe0B2w6//gFxCPBsjPIQN8JPi3jGDDvSDxVDPaCNnFNOdGczrNzIrEnRyR5XPZlMIdmKofnpTnx6B783QV0ARNOF9AFdAFdQBfQBXQBXUAX0AV0AV3A6OgCVjLJrEzlj5lk/jiVt2eSefvEvzTJxL84zMS/PM//Af1pBRpQAkXBAAAAAElFTkSuQmCC + + + + + + + + + + + + + +https://gd.wikipedia.org/wiki/Sònraichte:Search + diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/gd/yahoo-en-GB.xml b/mobile/ios/Client/Assets/Search/SearchPlugins/gd/yahoo-en-GB.xml new file mode 100644 index 0000000000..91713ad634 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/gd/yahoo-en-GB.xml @@ -0,0 +1,22 @@ + + + +Yahoo.co.uk +UTF-8 +data:image/png;base64,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 + + + + + + + + + + + + +https://uk.search.yahoo.com/ + diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/gl/default.txt b/mobile/ios/Client/Assets/Search/SearchPlugins/gl/default.txt new file mode 100644 index 0000000000..ced95a5cb9 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/gl/default.txt @@ -0,0 +1 @@ +Google \ No newline at end of file diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/gl/list.txt b/mobile/ios/Client/Assets/Search/SearchPlugins/gl/list.txt new file mode 100644 index 0000000000..5d0d1f3591 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/gl/list.txt @@ -0,0 +1,6 @@ +amazondotcom +bing +google +twitter +wikipedia-gl +yahoo-es diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/gl/wikipedia-gl.xml b/mobile/ios/Client/Assets/Search/SearchPlugins/gl/wikipedia-gl.xml new file mode 100644 index 0000000000..815f8113e9 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/gl/wikipedia-gl.xml @@ -0,0 +1,23 @@ + + + +Wikipedia (gl) +UTF-8 +data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAQAAABIkb+zAAAEXElEQVR4Ae3cA4xs2QJG4b9aj41n27Zt27Zt27Zt27Zt+2KM5lXP924qOzs56VPjKSRnrfhv7NUsp08v98+PsxwT4nJ+nPunl8KZ8/WYQL+eMydJrx5/EhN603lAHpVJ5bzZNp035uyZXM7Uy3LmM7ms9CITzVRGQRfQBXQBXUAX0AV0AV1AF9AFdAFdQBfQBXQBXcBMjgU7M5+1vqtZyXqmc/IsZDEL+53LP3KRPCCv62+rWd7vnpx6v/M5Vd935OF5R+6RtRyeQ3NYNjKTxZw2p8tiduR8uWfemY3+dkjWM5fFLPU/8q9yrRwbHLP39WkHamPD5z1WnNMb/dpW/uAdbipu413+qcl2H3N3cXFv9DNNtvmg68uxMY6dPfewTZNvOauouqEdULlTY532IgqHua+ZxnoHm4ANj63biRNQPZedVPY5nTR1BXsVbNuyTlsDqy4tW/wK4NYiJ01AXMlm+wGrL1cDnWbLejC4m7T4DvAOOSkD4v0qztGyLzpUweOk4UXAp6XV72DNmU7qgPPao+DJ0uLTFPzXXGN5I9acU1o8vb14jZzUAfFeBdvMtuwLDlfwIKme2248T1p9MvY59zACLqcy4Kf5eQr+ZbYRfrAFaXHaf/FRGUZAfE/Bzwf8OKwpuJ/0vYRNPEpavT248rACbqfimtLiqxT8w4yIz+Of5qTV7+LHMqyAaf9R8Clp8ex2K7iXuAa4q7R6aXCH4QXE4xVsOq+0+DYFfzXth/ilnrT6Tvzb9DADlqwqDPjTd377FHwI3EBaPb0NPFaGGRBvoLBiUVr8EFS+KgN8KpYtDjvgQo6i8Hhp8ZJUjnIZaXXGNrxKhh0QX6TwHzPS4uco/FtPWr0T9jnPKAJuOOiCc/UqVG4jrf4AH5dRBPT8UWHg3/BvUfhl635ZcNXRBMSDqFxFWvwklVu07O/BT2RUAad0GIWPtexXgMpPt+xntAt3Gl1AvJjCPufasn4d/6NyY2n4DPzXzCgDzmEvhVdIw+tj02X8nsIPGvus7Xi8jDIgPkLhSPON5ad4t7gbletJ9S71n+BIA65C5dGal1g3nENM+weF70j1R3iNjDogfkrhn6br5dU/4UXS94EtF7+vUC8IjjzgrlRuJ33v27j2dTLbKXxd+r4Pn5RRB9RfxsL3y4H/i0dI9TFUrirOZDeuPh4B8TQqVyjH/ZtZqZ7KIRS+JJ6Fn8k4BNRL9IUPmncwbr/1b37lanbiLuMTEG+nsNfb8cOWq0DLFA7E/8yMU8AlNLnaoP/alSfKOAXEN6h8Qlo8k3UKq5bGLeAWFPa6oLT6Ogqvk3ELmPJ3wOtlgOe0B2w6//gFxCPBsjPIQN8JPi3jGDDvSDxVDPaCNnFNOdGczrNzIrEnRyR5XPZlMIdmKofnpTnx6B783QV0ARNOF9AFdAFdQBfQBXQBXUAX0AV0AV3A6OgCVjLJrEzlj5lk/jiVt2eSefvEvzTJxL84zMS/PM//Af1pBRpQAkXBAAAAAElFTkSuQmCC + + + + + + + + + + + + + +https://gl.wikipedia.org/wiki/Especial:Procurar + diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/gl/yahoo-es.xml b/mobile/ios/Client/Assets/Search/SearchPlugins/gl/yahoo-es.xml new file mode 100644 index 0000000000..a9b50bc07e --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/gl/yahoo-es.xml @@ -0,0 +1,22 @@ + + + +Yahoo +UTF-8 +data:image/png;base64,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 + + + + + + + + + + + + +https://es.search.yahoo.com/ + diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/gu-IN/amazon-in.xml b/mobile/ios/Client/Assets/Search/SearchPlugins/gu-IN/amazon-in.xml new file mode 100644 index 0000000000..05c5311e31 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/gu-IN/amazon-in.xml @@ -0,0 +1,15 @@ + + + +Amazon.in +utf-8 +data:image/png;base64,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 + + + + + +https://www.amazon.in/ + diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/gu-IN/default.txt b/mobile/ios/Client/Assets/Search/SearchPlugins/gu-IN/default.txt new file mode 100644 index 0000000000..ced95a5cb9 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/gu-IN/default.txt @@ -0,0 +1 @@ +Google \ No newline at end of file diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/gu-IN/eBay-in.xml b/mobile/ios/Client/Assets/Search/SearchPlugins/gu-IN/eBay-in.xml new file mode 100644 index 0000000000..8d27d340f9 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/gu-IN/eBay-in.xml @@ -0,0 +1,17 @@ + + + +eBay India +UTF-8 +data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAAEEfUpiAAAEWklEQVRYw8WXfUxVdRjHP79z7guXBBTEEBXEraktCydjWi7RZW82lr340kZNXaX+41p/tF42Xakw3ZqNyeZMnZVYtjnLNLXMzJFLA1FrgiIioveCIvcyLvdyzz3n6Q+7FxIM5YJ8/jovv/2e7/Oc5+X8FICIbCKCiCyTLmgArcUbmb2ugirP3u4rlIjkA3P+3aFIiYjQBa21eCMARv2XFB5Mo9sKRORZ+R80IMv0NAJQ7Q4CYDYdJmwGehZ+O0pEXEA7d0B1i0UnbUqpd7urvg2NXtAAQhWnmF5Ug9tnQDiIZYUBCBo+NPf0WQTLfqc1aDEyyU77kWyCB9Kpa/6NOHsS3Mm9tQcejLoZk0gzEgehb2zp9VP0Ro8S522qv2sJGkDLR6uwmpvBsvA8n8+3f/oA+Oakly3HbhI6swKA9u9dFB5MA6DKs5drvtO3Noh/4Tm8a9fRunUbzkmTGJGgs3ZfIx/sbiT/YQPTdxbjUgnKkcL7z3goPJjG0PhM0pMeo19iYBIrIvJaH4x3iIgeyYNUYAWQepc2jyqlSiPWX+mr/wAqhiwEWKPFGL6sWDfoXm1Ltjf0bYNQ+SnEMPB4b7WLX6vbOmu2uQyrrZqg4Ys+izRGDaBxzlwcUybjXbkaEYuSIzfIGz8E9eZZQudWoidPQ+mJ/F25nNrrRwBY/3MWADYAfXgyrcUb0UeOQNpDLJ85HIAnH4rHrP8CQ9nQEibyiH0In1Ys5KXsLSyadqhL1ztfgy1rHP5du8HpJOu9Kjb8dJ3aGyHE9KMSJhA6XwhiMT/na3ZXLiEt8dFoGt8TJy5vFrfvTOR2R6yJVNr/eXCPhJSINN1DFd7O5EggvX0oxg97aipeGXjCIrJaRByRduACGoBk7j85GlAwSMYBVmiAzuChawwydyVgzmd1TC+qGRABtsiF5fXSsmoNofIKbBPGI01NWIEAQws/AZJpCVjMWHeRKy0GLodGlbuDd55KYf2r6VjXD9Nxcj44UtDiM7C8FcgDmWxTJgnOUbwxdX+XarPYXJZHYtwoFuTsRInIMqDEnfc09swxOPPyOgdBXS3tPxxg8YubONfmoK5oAkp1qs/++AJpiXZ+XJ5IuGEXVls1yriBWCFMz3600QvZ7j+Hy5bI61P3IWLxedlMklzpzJuyE6A0GgHb2AyU3UHCooKogY7ySvSx49B8Q7jW4Oe8J8j4kXEA1N80uNDYweKJlQT2L8D5+CHs45be8jLgJnBtD0op3nriKFuPz2bHyZcxwn4SnGkR40T6wDKgBMC8epXgsTLCl6+AaeLIzcE1K499f/nRFIxJtvPVcS++YJjMFAdvz0hhWLyO+C8Sqi1Ggo3orrHoGQVI4DKixWFLnYlpGWz4ZSKjh+Uyf0rpf6ZBzD93vWFZYfnij3zZc3ppT6932Aa6zJTSKcj9bsDGWb/0gUuDaL8mMgnnyv2nMHpc7TKSU4FcIBvI6GdvbwLlwAmlVPQU9g9eOPBg6mUX4QAAAABJRU5ErkJggg== + + + + + + + +http://search.ebay.in/ + diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/gu-IN/list.txt b/mobile/ios/Client/Assets/Search/SearchPlugins/gu-IN/list.txt new file mode 100644 index 0000000000..19a819dccf --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/gu-IN/list.txt @@ -0,0 +1,6 @@ +amazon-in +bing +eBay-in +google +wikipedia-gu +yahoo-in diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/gu-IN/wikipedia-gu.xml b/mobile/ios/Client/Assets/Search/SearchPlugins/gu-IN/wikipedia-gu.xml new file mode 100644 index 0000000000..f47992634f --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/gu-IN/wikipedia-gu.xml @@ -0,0 +1,23 @@ + + + +વિકિપીડિયા (gu) +UTF-8 +data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAQAAABIkb+zAAAEXElEQVR4Ae3cA4xs2QJG4b9aj41n27Zt27Zt27Zt27Zt+2KM5lXP924qOzs56VPjKSRnrfhv7NUsp08v98+PsxwT4nJ+nPunl8KZ8/WYQL+eMydJrx5/EhN603lAHpVJ5bzZNp035uyZXM7Uy3LmM7ms9CITzVRGQRfQBXQBXUAX0AV0AV1AF9AFdAFdQBfQBXQBXcBMjgU7M5+1vqtZyXqmc/IsZDEL+53LP3KRPCCv62+rWd7vnpx6v/M5Vd935OF5R+6RtRyeQ3NYNjKTxZw2p8tiduR8uWfemY3+dkjWM5fFLPU/8q9yrRwbHLP39WkHamPD5z1WnNMb/dpW/uAdbipu413+qcl2H3N3cXFv9DNNtvmg68uxMY6dPfewTZNvOauouqEdULlTY532IgqHua+ZxnoHm4ANj63biRNQPZedVPY5nTR1BXsVbNuyTlsDqy4tW/wK4NYiJ01AXMlm+wGrL1cDnWbLejC4m7T4DvAOOSkD4v0qztGyLzpUweOk4UXAp6XV72DNmU7qgPPao+DJ0uLTFPzXXGN5I9acU1o8vb14jZzUAfFeBdvMtuwLDlfwIKme2248T1p9MvY59zACLqcy4Kf5eQr+ZbYRfrAFaXHaf/FRGUZAfE/Bzwf8OKwpuJ/0vYRNPEpavT248rACbqfimtLiqxT8w4yIz+Of5qTV7+LHMqyAaf9R8Clp8ex2K7iXuAa4q7R6aXCH4QXE4xVsOq+0+DYFfzXth/ilnrT6Tvzb9DADlqwqDPjTd377FHwI3EBaPb0NPFaGGRBvoLBiUVr8EFS+KgN8KpYtDjvgQo6i8Hhp8ZJUjnIZaXXGNrxKhh0QX6TwHzPS4uco/FtPWr0T9jnPKAJuOOiCc/UqVG4jrf4AH5dRBPT8UWHg3/BvUfhl635ZcNXRBMSDqFxFWvwklVu07O/BT2RUAad0GIWPtexXgMpPt+xntAt3Gl1AvJjCPufasn4d/6NyY2n4DPzXzCgDzmEvhVdIw+tj02X8nsIPGvus7Xi8jDIgPkLhSPON5ad4t7gbletJ9S71n+BIA65C5dGal1g3nENM+weF70j1R3iNjDogfkrhn6br5dU/4UXS94EtF7+vUC8IjjzgrlRuJ33v27j2dTLbKXxd+r4Pn5RRB9RfxsL3y4H/i0dI9TFUrirOZDeuPh4B8TQqVyjH/ZtZqZ7KIRS+JJ6Fn8k4BNRL9IUPmncwbr/1b37lanbiLuMTEG+nsNfb8cOWq0DLFA7E/8yMU8AlNLnaoP/alSfKOAXEN6h8Qlo8k3UKq5bGLeAWFPa6oLT6Ogqvk3ELmPJ3wOtlgOe0B2w6//gFxCPBsjPIQN8JPi3jGDDvSDxVDPaCNnFNOdGczrNzIrEnRyR5XPZlMIdmKofnpTnx6B783QV0ARNOF9AFdAFdQBfQBXQBXUAX0AV0AV3A6OgCVjLJrEzlj5lk/jiVt2eSefvEvzTJxL84zMS/PM//Af1pBRpQAkXBAAAAAElFTkSuQmCC + + + + + + + + + + + + + +https://gu.wikipedia.org/wiki/વિશેષ:શોધ + diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/gu-IN/yahoo-in.xml b/mobile/ios/Client/Assets/Search/SearchPlugins/gu-IN/yahoo-in.xml new file mode 100644 index 0000000000..67e95519df --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/gu-IN/yahoo-in.xml @@ -0,0 +1,22 @@ + + + +Yahoo +UTF-8 +data:image/png;base64,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 + + + + + + + + + + + + +https://in.search.yahoo.com/ + diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/he/default.txt b/mobile/ios/Client/Assets/Search/SearchPlugins/he/default.txt new file mode 100644 index 0000000000..ced95a5cb9 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/he/default.txt @@ -0,0 +1 @@ +Google \ No newline at end of file diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/he/list.txt b/mobile/ios/Client/Assets/Search/SearchPlugins/he/list.txt new file mode 100644 index 0000000000..7d3db56d89 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/he/list.txt @@ -0,0 +1,3 @@ +google +twitter +wikipedia-he diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/he/wikipedia-he.xml b/mobile/ios/Client/Assets/Search/SearchPlugins/he/wikipedia-he.xml new file mode 100644 index 0000000000..886ed724ed --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/he/wikipedia-he.xml @@ -0,0 +1,23 @@ + + + +ויקיפדיה +UTF-8 +data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAQAAABIkb+zAAAEXElEQVR4Ae3cA4xs2QJG4b9aj41n27Zt27Zt27Zt27Zt+2KM5lXP924qOzs56VPjKSRnrfhv7NUsp08v98+PsxwT4nJ+nPunl8KZ8/WYQL+eMydJrx5/EhN603lAHpVJ5bzZNp035uyZXM7Uy3LmM7ms9CITzVRGQRfQBXQBXUAX0AV0AV1AF9AFdAFdQBfQBXQBXcBMjgU7M5+1vqtZyXqmc/IsZDEL+53LP3KRPCCv62+rWd7vnpx6v/M5Vd935OF5R+6RtRyeQ3NYNjKTxZw2p8tiduR8uWfemY3+dkjWM5fFLPU/8q9yrRwbHLP39WkHamPD5z1WnNMb/dpW/uAdbipu413+qcl2H3N3cXFv9DNNtvmg68uxMY6dPfewTZNvOauouqEdULlTY532IgqHua+ZxnoHm4ANj63biRNQPZedVPY5nTR1BXsVbNuyTlsDqy4tW/wK4NYiJ01AXMlm+wGrL1cDnWbLejC4m7T4DvAOOSkD4v0qztGyLzpUweOk4UXAp6XV72DNmU7qgPPao+DJ0uLTFPzXXGN5I9acU1o8vb14jZzUAfFeBdvMtuwLDlfwIKme2248T1p9MvY59zACLqcy4Kf5eQr+ZbYRfrAFaXHaf/FRGUZAfE/Bzwf8OKwpuJ/0vYRNPEpavT248rACbqfimtLiqxT8w4yIz+Of5qTV7+LHMqyAaf9R8Clp8ex2K7iXuAa4q7R6aXCH4QXE4xVsOq+0+DYFfzXth/ilnrT6Tvzb9DADlqwqDPjTd377FHwI3EBaPb0NPFaGGRBvoLBiUVr8EFS+KgN8KpYtDjvgQo6i8Hhp8ZJUjnIZaXXGNrxKhh0QX6TwHzPS4uco/FtPWr0T9jnPKAJuOOiCc/UqVG4jrf4AH5dRBPT8UWHg3/BvUfhl635ZcNXRBMSDqFxFWvwklVu07O/BT2RUAad0GIWPtexXgMpPt+xntAt3Gl1AvJjCPufasn4d/6NyY2n4DPzXzCgDzmEvhVdIw+tj02X8nsIPGvus7Xi8jDIgPkLhSPON5ad4t7gbletJ9S71n+BIA65C5dGal1g3nENM+weF70j1R3iNjDogfkrhn6br5dU/4UXS94EtF7+vUC8IjjzgrlRuJ33v27j2dTLbKXxd+r4Pn5RRB9RfxsL3y4H/i0dI9TFUrirOZDeuPh4B8TQqVyjH/ZtZqZ7KIRS+JJ6Fn8k4BNRL9IUPmncwbr/1b37lanbiLuMTEG+nsNfb8cOWq0DLFA7E/8yMU8AlNLnaoP/alSfKOAXEN6h8Qlo8k3UKq5bGLeAWFPa6oLT6Ogqvk3ELmPJ3wOtlgOe0B2w6//gFxCPBsjPIQN8JPi3jGDDvSDxVDPaCNnFNOdGczrNzIrEnRyR5XPZlMIdmKofnpTnx6B783QV0ARNOF9AFdAFdQBfQBXQBXUAX0AV0AV3A6OgCVjLJrEzlj5lk/jiVt2eSefvEvzTJxL84zMS/PM//Af1pBRpQAkXBAAAAAElFTkSuQmCC + + + + + + + + + + + + + +https://he.wikipedia.org/wiki/מיוחד:חיפוש + diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/hi-IN/amazon-in.xml b/mobile/ios/Client/Assets/Search/SearchPlugins/hi-IN/amazon-in.xml new file mode 100644 index 0000000000..05c5311e31 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/hi-IN/amazon-in.xml @@ -0,0 +1,15 @@ + + + +Amazon.in +utf-8 +data:image/png;base64,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 + + + + + +https://www.amazon.in/ + diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/hi-IN/default.txt b/mobile/ios/Client/Assets/Search/SearchPlugins/hi-IN/default.txt new file mode 100644 index 0000000000..ced95a5cb9 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/hi-IN/default.txt @@ -0,0 +1 @@ +Google \ No newline at end of file diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/hi-IN/list.txt b/mobile/ios/Client/Assets/Search/SearchPlugins/hi-IN/list.txt new file mode 100644 index 0000000000..b299ab348f --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/hi-IN/list.txt @@ -0,0 +1,6 @@ +amazon-in +bing +google +twitter +wikipedia-hi +yahoo-in diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/hi-IN/wikipedia-hi.xml b/mobile/ios/Client/Assets/Search/SearchPlugins/hi-IN/wikipedia-hi.xml new file mode 100644 index 0000000000..bb7d00de5e --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/hi-IN/wikipedia-hi.xml @@ -0,0 +1,23 @@ + + + +विकिपीडिया (hi) +UTF-8 +data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAQAAABIkb+zAAAEXElEQVR4Ae3cA4xs2QJG4b9aj41n27Zt27Zt27Zt27Zt+2KM5lXP924qOzs56VPjKSRnrfhv7NUsp08v98+PsxwT4nJ+nPunl8KZ8/WYQL+eMydJrx5/EhN603lAHpVJ5bzZNp035uyZXM7Uy3LmM7ms9CITzVRGQRfQBXQBXUAX0AV0AV1AF9AFdAFdQBfQBXQBXcBMjgU7M5+1vqtZyXqmc/IsZDEL+53LP3KRPCCv62+rWd7vnpx6v/M5Vd935OF5R+6RtRyeQ3NYNjKTxZw2p8tiduR8uWfemY3+dkjWM5fFLPU/8q9yrRwbHLP39WkHamPD5z1WnNMb/dpW/uAdbipu413+qcl2H3N3cXFv9DNNtvmg68uxMY6dPfewTZNvOauouqEdULlTY532IgqHua+ZxnoHm4ANj63biRNQPZedVPY5nTR1BXsVbNuyTlsDqy4tW/wK4NYiJ01AXMlm+wGrL1cDnWbLejC4m7T4DvAOOSkD4v0qztGyLzpUweOk4UXAp6XV72DNmU7qgPPao+DJ0uLTFPzXXGN5I9acU1o8vb14jZzUAfFeBdvMtuwLDlfwIKme2248T1p9MvY59zACLqcy4Kf5eQr+ZbYRfrAFaXHaf/FRGUZAfE/Bzwf8OKwpuJ/0vYRNPEpavT248rACbqfimtLiqxT8w4yIz+Of5qTV7+LHMqyAaf9R8Clp8ex2K7iXuAa4q7R6aXCH4QXE4xVsOq+0+DYFfzXth/ilnrT6Tvzb9DADlqwqDPjTd377FHwI3EBaPb0NPFaGGRBvoLBiUVr8EFS+KgN8KpYtDjvgQo6i8Hhp8ZJUjnIZaXXGNrxKhh0QX6TwHzPS4uco/FtPWr0T9jnPKAJuOOiCc/UqVG4jrf4AH5dRBPT8UWHg3/BvUfhl635ZcNXRBMSDqFxFWvwklVu07O/BT2RUAad0GIWPtexXgMpPt+xntAt3Gl1AvJjCPufasn4d/6NyY2n4DPzXzCgDzmEvhVdIw+tj02X8nsIPGvus7Xi8jDIgPkLhSPON5ad4t7gbletJ9S71n+BIA65C5dGal1g3nENM+weF70j1R3iNjDogfkrhn6br5dU/4UXS94EtF7+vUC8IjjzgrlRuJ33v27j2dTLbKXxd+r4Pn5RRB9RfxsL3y4H/i0dI9TFUrirOZDeuPh4B8TQqVyjH/ZtZqZ7KIRS+JJ6Fn8k4BNRL9IUPmncwbr/1b37lanbiLuMTEG+nsNfb8cOWq0DLFA7E/8yMU8AlNLnaoP/alSfKOAXEN6h8Qlo8k3UKq5bGLeAWFPa6oLT6Ogqvk3ELmPJ3wOtlgOe0B2w6//gFxCPBsjPIQN8JPi3jGDDvSDxVDPaCNnFNOdGczrNzIrEnRyR5XPZlMIdmKofnpTnx6B783QV0ARNOF9AFdAFdQBfQBXQBXUAX0AV0AV3A6OgCVjLJrEzlj5lk/jiVt2eSefvEvzTJxL84zMS/PM//Af1pBRpQAkXBAAAAAElFTkSuQmCC + + + + + + + + + + + + + +https://hi.wikipedia.org/wiki/विशेष:खोज + diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/hi-IN/yahoo-in.xml b/mobile/ios/Client/Assets/Search/SearchPlugins/hi-IN/yahoo-in.xml new file mode 100644 index 0000000000..67e95519df --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/hi-IN/yahoo-in.xml @@ -0,0 +1,22 @@ + + + +Yahoo +UTF-8 +data:image/png;base64,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 + + + + + + + + + + + + +https://in.search.yahoo.com/ + diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/hr/amazon-en-GB.xml b/mobile/ios/Client/Assets/Search/SearchPlugins/hr/amazon-en-GB.xml new file mode 100644 index 0000000000..3a3fbabb19 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/hr/amazon-en-GB.xml @@ -0,0 +1,16 @@ + + + +Amazon.co.uk +ISO-8859-1 +data:image/png;base64,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 + + + + + + +https://www.amazon.co.uk/ + diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/hr/default.txt b/mobile/ios/Client/Assets/Search/SearchPlugins/hr/default.txt new file mode 100644 index 0000000000..ced95a5cb9 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/hr/default.txt @@ -0,0 +1 @@ +Google \ No newline at end of file diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/hr/list.txt b/mobile/ios/Client/Assets/Search/SearchPlugins/hr/list.txt new file mode 100644 index 0000000000..e4faa0be43 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/hr/list.txt @@ -0,0 +1,7 @@ +amazon-en-GB +bing +duckduckgo +google +twitter +wikipedia-hr +yahoo diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/hr/wikipedia-hr.xml b/mobile/ios/Client/Assets/Search/SearchPlugins/hr/wikipedia-hr.xml new file mode 100644 index 0000000000..138f852710 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/hr/wikipedia-hr.xml @@ -0,0 +1,23 @@ + + + +Wikipedija (hr) +UTF-8 +data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAQAAABIkb+zAAAEXElEQVR4Ae3cA4xs2QJG4b9aj41n27Zt27Zt27Zt27Zt+2KM5lXP924qOzs56VPjKSRnrfhv7NUsp08v98+PsxwT4nJ+nPunl8KZ8/WYQL+eMydJrx5/EhN603lAHpVJ5bzZNp035uyZXM7Uy3LmM7ms9CITzVRGQRfQBXQBXUAX0AV0AV1AF9AFdAFdQBfQBXQBXcBMjgU7M5+1vqtZyXqmc/IsZDEL+53LP3KRPCCv62+rWd7vnpx6v/M5Vd935OF5R+6RtRyeQ3NYNjKTxZw2p8tiduR8uWfemY3+dkjWM5fFLPU/8q9yrRwbHLP39WkHamPD5z1WnNMb/dpW/uAdbipu413+qcl2H3N3cXFv9DNNtvmg68uxMY6dPfewTZNvOauouqEdULlTY532IgqHua+ZxnoHm4ANj63biRNQPZedVPY5nTR1BXsVbNuyTlsDqy4tW/wK4NYiJ01AXMlm+wGrL1cDnWbLejC4m7T4DvAOOSkD4v0qztGyLzpUweOk4UXAp6XV72DNmU7qgPPao+DJ0uLTFPzXXGN5I9acU1o8vb14jZzUAfFeBdvMtuwLDlfwIKme2248T1p9MvY59zACLqcy4Kf5eQr+ZbYRfrAFaXHaf/FRGUZAfE/Bzwf8OKwpuJ/0vYRNPEpavT248rACbqfimtLiqxT8w4yIz+Of5qTV7+LHMqyAaf9R8Clp8ex2K7iXuAa4q7R6aXCH4QXE4xVsOq+0+DYFfzXth/ilnrT6Tvzb9DADlqwqDPjTd377FHwI3EBaPb0NPFaGGRBvoLBiUVr8EFS+KgN8KpYtDjvgQo6i8Hhp8ZJUjnIZaXXGNrxKhh0QX6TwHzPS4uco/FtPWr0T9jnPKAJuOOiCc/UqVG4jrf4AH5dRBPT8UWHg3/BvUfhl635ZcNXRBMSDqFxFWvwklVu07O/BT2RUAad0GIWPtexXgMpPt+xntAt3Gl1AvJjCPufasn4d/6NyY2n4DPzXzCgDzmEvhVdIw+tj02X8nsIPGvus7Xi8jDIgPkLhSPON5ad4t7gbletJ9S71n+BIA65C5dGal1g3nENM+weF70j1R3iNjDogfkrhn6br5dU/4UXS94EtF7+vUC8IjjzgrlRuJ33v27j2dTLbKXxd+r4Pn5RRB9RfxsL3y4H/i0dI9TFUrirOZDeuPh4B8TQqVyjH/ZtZqZ7KIRS+JJ6Fn8k4BNRL9IUPmncwbr/1b37lanbiLuMTEG+nsNfb8cOWq0DLFA7E/8yMU8AlNLnaoP/alSfKOAXEN6h8Qlo8k3UKq5bGLeAWFPa6oLT6Ogqvk3ELmPJ3wOtlgOe0B2w6//gFxCPBsjPIQN8JPi3jGDDvSDxVDPaCNnFNOdGczrNzIrEnRyR5XPZlMIdmKofnpTnx6B783QV0ARNOF9AFdAFdQBfQBXQBXUAX0AV0AV3A6OgCVjLJrEzlj5lk/jiVt2eSefvEvzTJxL84zMS/PM//Af1pBRpQAkXBAAAAAElFTkSuQmCC + + + + + + + + + + + + + +https://hr.wikipedia.org/wiki/Posebno:Traži + diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/hsb/amazon-de.xml b/mobile/ios/Client/Assets/Search/SearchPlugins/hsb/amazon-de.xml new file mode 100644 index 0000000000..d19cc3133a --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/hsb/amazon-de.xml @@ -0,0 +1,16 @@ + + + +Amazon.de +ISO-8859-1 +data:image/png;base64,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 + + + + + + +https://www.amazon.de/ + diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/hsb/default.txt b/mobile/ios/Client/Assets/Search/SearchPlugins/hsb/default.txt new file mode 100644 index 0000000000..ced95a5cb9 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/hsb/default.txt @@ -0,0 +1 @@ +Google \ No newline at end of file diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/hsb/list.txt b/mobile/ios/Client/Assets/Search/SearchPlugins/hsb/list.txt new file mode 100644 index 0000000000..bc281cb69f --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/hsb/list.txt @@ -0,0 +1,6 @@ +amazon-de +bing +google +twitter +wikipedia-hsb +yahoo-de diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/hsb/wikipedia-hsb.xml b/mobile/ios/Client/Assets/Search/SearchPlugins/hsb/wikipedia-hsb.xml new file mode 100644 index 0000000000..fd20d12a23 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/hsb/wikipedia-hsb.xml @@ -0,0 +1,23 @@ + + + +Wikipedija (hsb) +UTF-8 +data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAQAAABIkb+zAAAEXElEQVR4Ae3cA4xs2QJG4b9aj41n27Zt27Zt27Zt27Zt+2KM5lXP924qOzs56VPjKSRnrfhv7NUsp08v98+PsxwT4nJ+nPunl8KZ8/WYQL+eMydJrx5/EhN603lAHpVJ5bzZNp035uyZXM7Uy3LmM7ms9CITzVRGQRfQBXQBXUAX0AV0AV1AF9AFdAFdQBfQBXQBXcBMjgU7M5+1vqtZyXqmc/IsZDEL+53LP3KRPCCv62+rWd7vnpx6v/M5Vd935OF5R+6RtRyeQ3NYNjKTxZw2p8tiduR8uWfemY3+dkjWM5fFLPU/8q9yrRwbHLP39WkHamPD5z1WnNMb/dpW/uAdbipu413+qcl2H3N3cXFv9DNNtvmg68uxMY6dPfewTZNvOauouqEdULlTY532IgqHua+ZxnoHm4ANj63biRNQPZedVPY5nTR1BXsVbNuyTlsDqy4tW/wK4NYiJ01AXMlm+wGrL1cDnWbLejC4m7T4DvAOOSkD4v0qztGyLzpUweOk4UXAp6XV72DNmU7qgPPao+DJ0uLTFPzXXGN5I9acU1o8vb14jZzUAfFeBdvMtuwLDlfwIKme2248T1p9MvY59zACLqcy4Kf5eQr+ZbYRfrAFaXHaf/FRGUZAfE/Bzwf8OKwpuJ/0vYRNPEpavT248rACbqfimtLiqxT8w4yIz+Of5qTV7+LHMqyAaf9R8Clp8ex2K7iXuAa4q7R6aXCH4QXE4xVsOq+0+DYFfzXth/ilnrT6Tvzb9DADlqwqDPjTd377FHwI3EBaPb0NPFaGGRBvoLBiUVr8EFS+KgN8KpYtDjvgQo6i8Hhp8ZJUjnIZaXXGNrxKhh0QX6TwHzPS4uco/FtPWr0T9jnPKAJuOOiCc/UqVG4jrf4AH5dRBPT8UWHg3/BvUfhl635ZcNXRBMSDqFxFWvwklVu07O/BT2RUAad0GIWPtexXgMpPt+xntAt3Gl1AvJjCPufasn4d/6NyY2n4DPzXzCgDzmEvhVdIw+tj02X8nsIPGvus7Xi8jDIgPkLhSPON5ad4t7gbletJ9S71n+BIA65C5dGal1g3nENM+weF70j1R3iNjDogfkrhn6br5dU/4UXS94EtF7+vUC8IjjzgrlRuJ33v27j2dTLbKXxd+r4Pn5RRB9RfxsL3y4H/i0dI9TFUrirOZDeuPh4B8TQqVyjH/ZtZqZ7KIRS+JJ6Fn8k4BNRL9IUPmncwbr/1b37lanbiLuMTEG+nsNfb8cOWq0DLFA7E/8yMU8AlNLnaoP/alSfKOAXEN6h8Qlo8k3UKq5bGLeAWFPa6oLT6Ogqvk3ELmPJ3wOtlgOe0B2w6//gFxCPBsjPIQN8JPi3jGDDvSDxVDPaCNnFNOdGczrNzIrEnRyR5XPZlMIdmKofnpTnx6B783QV0ARNOF9AFdAFdQBfQBXQBXUAX0AV0AV3A6OgCVjLJrEzlj5lk/jiVt2eSefvEvzTJxL84zMS/PM//Af1pBRpQAkXBAAAAAElFTkSuQmCC + + + + + + + + + + + + + +https://hsb.wikipedia.org/wiki/Specialnje:Pytać + diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/hsb/yahoo-de.xml b/mobile/ios/Client/Assets/Search/SearchPlugins/hsb/yahoo-de.xml new file mode 100644 index 0000000000..04b36eea83 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/hsb/yahoo-de.xml @@ -0,0 +1,22 @@ + + + +Yahoo +UTF-8 +data:image/png;base64,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 + + + + + + + + + + + + +https://de.search.yahoo.com/ + diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/hu/default.txt b/mobile/ios/Client/Assets/Search/SearchPlugins/hu/default.txt new file mode 100644 index 0000000000..ced95a5cb9 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/hu/default.txt @@ -0,0 +1 @@ +Google \ No newline at end of file diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/hu/list.txt b/mobile/ios/Client/Assets/Search/SearchPlugins/hu/list.txt new file mode 100644 index 0000000000..67c96f2e7d --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/hu/list.txt @@ -0,0 +1,5 @@ +google +sztaki-en-hu +vatera +twitter +wikipedia-hu diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/hu/sztaki-en-hu.xml b/mobile/ios/Client/Assets/Search/SearchPlugins/hu/sztaki-en-hu.xml new file mode 100644 index 0000000000..ac761c4c95 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/hu/sztaki-en-hu.xml @@ -0,0 +1,20 @@ + + + + SZTAKI angol-magyar + UTF-8 + data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAYAAADimHc4AAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAABWASURBVHja7F0JeJTltX7/WTJb9n2DkIQlBBAMyCK7tVrUehGqUFoQqKJV2lrbe3uvvb3PbR/FulIqYBWVgvWKxR1FihYkbIkkgBggZIVAlskkmSSz7/ecbxIFAaHMZCa0c3x+JyHL/895z3m/95zz/X+kqVOnDgNgWLFixbQBAwbcr1Qqp9LnWkQs6Obz+drtdvuWw4cPP//xxx/XzJw5M0VasmRJ4urVq79HX3+KjtiIm0JiNXQspuO4VFFR8W+5ubmb6BN1xC8htWOSJP1AlpSUtDzi/LBYIVHSPJlCoZgW8UXY7Fsy+l9UxA9hs2xZxAdhNUUEgPCaNwJAmC0CQASACAARiwAQASBiEQD+RQsBn8kMaPtv99nndAF2B3xWG3wWq/+VP3c44XW7IVMqoRs/BrJoHRwON2rqDYiKkiMmRg2tWgEVfdyvAXDb7P3P6T6JnKiA5PHCUlYGR20DfDYbvHStwvkEis/tos8dwvG6EcMgj4uFw+XE+9tPoLXdgtgkHVIStchI0SEnMw4DM2IRF6PqfwB4JalfXIjb7YW+3YrmFhOGD02DTqeB12mFZX85rAcryOkU8WYLvBT5PhcB4PEIQBSJ8Uj+0Xwo6XdwBuwvO4lGvQm65GjEaKMQG62CVhOF1CQthuUmYfyoTOQNiEc/edtQSPLwLgNOlwdNrRZUn+pE/al2mDptyEyPQ0pStKAZ894DsJQegkyjoqj3cHp8Vcf3ZoPL7QeRMqbuVAdO1LdDGa+BmrIougeEhDgNKqpasa24FkWF6bhhYg6GD06GIszvXyGPjQnbyc/ozag6aUQVOayhqQsnqvXoNJgx68YCPxVRlHuMXfAS70tKxTnO/2YK8wlgnU4PTBYnZZZFrAUxOhWSCJiWNjN2lzVg4pgs3DZzCAryksIHgMPlRaghMFlcOF7XjqPVBtSdNtJrK6pPtkPf3AWdTBKRHLz1xAePxwerzQub3Q1jlw06ApmBaOu0ovRII26Zlo87ZxWKbAk5AGaLA8lJoY368go9RX6boIQvTuiJgkyw2Ijj7S5o+tAJvZnh6raL8xnptbOlGy/VGNBGNHjfousQqwstCCQ1QnMiD0X18Tojyr5oQiVRTtkXjYJ6ukz2oEb8ZQFB/znNDhgJ+EGjB+CBZZNx15xx0LkJmIZGyLIzQghACMzr9eFoVTt276/HEYr6shMtaGjpgs3mFlEZEpOE50kq0YJtdSIrKx7Lf3kz7l40CRlxUTC9/QFqN74Ne1UNEh5YgoSl8/45AJBI72m1aowuzERjczfe2XYMtYfPwMUFkiZE6c6a00EFncmBtPRYLFgyGQ///NvIjlfAsWMPqh9fAxOpLVm0FvLoaLQ9too1LRLuX4i+1quKvna+SqWCTCaDRiPD3NtGYcyoDLy48TNseKMMeuJfxKr9g7m+SgTKPnRbEU0ydME9k3H/0im4tiAV1uJ9qFu9HsYPd3ARIhzv6TRRZR2FxB/MQeK3p0MRFQUX1RxXLQC9zj/b8nOS8cRvbsHd88bijy/uxYa3DsJutAKk1YO2HnHUUmEHkqAypRxzZ1+Lhx+6ERNHpAFV1Ti5+DEY39sGb7cFkk7rL+rsdiTccQvSHr4XuglFZ61d9DWv9+oDwFvXACdFkCp3AGl45XlfL6Rq909Pz8Hd88fi6bXF+Ojvx2EjIHwZSpE5V8zzbF1WSNEaTJ8yGL986Fu4dWo+0NiEpl89itZXNsHd1UWFnYarIFFhR0+diPSfL0PcLTec9yvlcjnVE04oFIqrBwBPYwusH++G9fOjwvmJd30Xsd++8PajSeNy8NYrC/HhJ5VYtWYX9pWdFC2FK21nmInnxxYNxEPE8XfdMRpRbW3Q//45tL70Ohx1p0TPSEaZ6TNboJ0wFqkPLELCnbfTv114PVLS9e87UI2crESkpsT2AQBB5l5uH9iLS+GorYf1yFHYjh5F+4Y3EHfbjUh76B5ETxwHKer8N3srVb/Tr8/D1k+OIyP1ykpDnUaJJ3/7XcyYkoeMGBkMz6xFy9qNsNdVU1apKerV8JhMUA8fgrTlS4TjuZd0IXO5PThGNcrqV/Zi05vlmHldKjZtvD/4AMhkwV3l1bExkE+bCEdlNYHhgjo3XzTQzCWfwbrsGJLn3YHU5fdCkZJ43s9yJXrX7aOv+NzcP/r+7NGw79qDqidXw1pBGehRQD3Y39rgKE9cMBspyxZCkZx40d9jJpn60qulWL9hP4ztFsQlxuBQbQfeeKcU8+6YEFwAuFEVPNkjE5wpH5KLrBX/hZT7Fvl5WXC6BB+pDS/PH/q0reqBLyUFAx7/H8h00XRaf4r7aCFlpaNMT7mMotGHqRPzcNPMoSQk/LWq1WqDobWJahc7KbrgbaVVyIPYDVSfxaPM/arBgwLDkxbJy9Hh4vu+ekfQFA4L6Lw8Nxg7Ouu8f2/QedHS0ozc3NxgrgHBWQRYJUgUZSzpfEGQbRJlE1OX1NM8+MYCt6cl7fN6vhbJHpSUlmLT5s2ora39Uslw9f2PKi3+fhdlsJbU04oVKzAwJyc4pNFYXeOLzwy892HfXgz36UbYjtfApTdw/yGwCyMq48GL9VAFLZyWc6P8qx6HaFNrikZCTnTDFPdVvwc4aTXh1aY6nKG1JS4zXTjvXMdL/0ChKwng2trbMESlwxOvvxoM/+sVYuYaoDmOHIPl032wV9bAcuAwPF2mIDTM/OzD84qLzgKoyOMhjenve8Xr2c49RT+yMoEcPGQQRo0ahWuvvRYZGRkiC1jXd3Z2oovqATsVYJwpl5sF/HNf7N6L7pZWxKanBqMbGrgKchz4nKjHS5FqhkTrgPwi0u4Ke8jfPIih62dtf7a1Sl6sVtuhHpqHMWPGYOLEiQKE1NRU4fDysjIYNr2DDL0RagpAOYN3FvBn13TnUBplpR1exLscOLW3BKPm3h7+QoxVjZsKL3eHEe5245fjwdD2l78CqINUz0qlFbIh+ZgwYQKmTZuGwsJCJCUlicg/SM5vf3INZlbUXVGLw+3zQuZ1Qb+jOCgABCyBHBWV8Fqswvn8Gk6zUvyuVdpgJ9oZN24cZs6cibFjxyIzM1P0cw4cOAD9089j1JU4vwdoOeVGNAmEltJy2No7wg+A60QdvFa7mN36+rhz+I3yn44XlHbo87JQVFSEGTNm4JprrkFCQgIcDgcOHjyIuieew5gjNQF3eDUEgP1MEwzHT4QZAIoqph+OfOZ/n9cXFufzErpBYUdVTipGjx6N6dOnC+5PTEwU7eSKigocenIVig5XBeV8SsoCGRVkTccqw7sGeM3k+G4TvDab6Cp+2QruI36/mL2tcKAkOwFFFPHsfM6A5ORkQTtVVVXY+cRKzCg9BnkwRp+ChqjopPfZEoQMCKgQ441SbuJBV2sb3IZ2sU8nqI6nN8k737gmuOB10te3yezYlhGDa0aOFAvu+PHjkZ6eLr5cV1eHj55ehSn7K6B0e4J2aXI6r5qyoKuxGS4KPGXUlU/2FFIgfW6qVFUDsiCRHudGl88TvDcJGfeOvHCePA2fjYC9QMtkr+TAm8lqDCeVw85nuckLLvP06dOn8dHz6zDx03JobI6gJiUvxCo6mg0Gsb4EBIA8JvqKf1g1IBMZ//2Q3/FBnhpxK8Jt7ETdggdgPXxUFGRnZ8FhmRt/TlAgd3gBJk+ejEmTJiE7O1sUWs3Nzfjbxr9g5Ae7EGMKvjKTetYBe1c3nBSEgWysUiCAdjQ33OSaPrzJvqeL+vVWUJ3Mg5difMgePgxTpkwRx6BBgxBFkWigqNz+zrvIfmMrkjq6++ayRBYQARAFcwYEtAZ4A1AuPLmyOG2kNDzo7LYjJVGHhHjNpdcOuxMeKuml3nkxOdpLXMo7n6OyM0U1LZYBlrVf4/5GqnJXaV2IGzZMUA47Pz8/X8yfjUYjdmz5EPHrNiGzub3P1ZfL6aDiLkAAus12xMVd2aht684qvP7OEbTquzB5Uj5+/uPpSLiEbDW+uw2ta9aLRZt7PGKfv9UKZWYq0u9bCNXCi+/HaWPnqx1QDx0sqtypU6di6NCh0Gg0MJvN2LOrGHjxNQxs0KNPBTGlABOu28dj0MAqfwUPH67UzB0WingdFi+4DjdNHwqtRnlRRWPaXQr9yhfR/bdd9CldPveOXCbItXFI+t53Mei/HoQ7Nxc799UhLycROVnn9pNI6GI1VbmeIXmYdN11X7YYoqOjYSMZXFJSAsPjqzDiwDE4+7rzQQg4YILDHh3w7wpoa+LUqflYuGgyopQXr+cspQdheOFVdL61FR4qXuQxup6mnQqp3/8h0h9cDPX4IpR83oSVP3kTO3Yex1uvLj0HACddI7cYOvIHYAJpfNb63FyLjY31N9fKy9G4+T1MH1IAaUhBn1OPlwKo02iAxtwBpTIqQAACsEGDki/qfPuJGrT+8WV0bN4Cd0enfxsISUluW8TfPAMD//NBKCdPQE2zGc/+6m389d3P0d7YiYT0WOLzc3v/G6K9qM3JxDiqbtn5XO1yi6G3ym04dQo/fO4ZMQ4NhXGB13XoMGK2fQRVgCIkaNtSeqdMztONaFv3Ggwvb4Jb3wqZVguuNXykFnQTi5D98H2Inj0LZzrsWPfUJ1i7fg/aWk2ATgUk6SCPkp+z7v7FpEd5bhrG9lS53NfnFgP38LnKPXToEBYuXAiFKnS3H7npvZhtViEioqLCmAFn7xjzdBjRvmGzoBt7VZ3o0UtajWhXaItGIW353UhcOAcdDhme+tNurFu/D9U1rf6tiaSexKrpdJ8jOV97czM+UfswcuQowfnXEfenpaUJsHnEWFxcLJzPi3AojaUnD2Y441QBAq8INOpZxRjf3oqWZ1+AjQomkLIRPE8aWZ2TjcxHlyDx7rvgjovHK5sP4rkXduPw4TP0fTIR8T2r2nm2ZcsWPPfySygoHC7UDktOnmhxljU0NGDXrl2YP3++WAdCbbzudHR0QEvZrVaHkYK4Am7+w0toW7tejCEltUp0RmVZ6Uj76VJk/vv9cMYm4INdNXhy5SbsLqkTY0TEqf2thYv0oQ6Vl2DjKyuFvmedf/311yMrK0tEXFNTE3bs2IFbb71VDFlCbRx0LHlbW1sxcODA8/a+hnYN4JksN8x0OrhooZVrdUhZOg8ZP14ERcFQbC+uxZp17+H9v1f6dynHqi/Z5ZR5DHj1z8+JaOcWw9erXHY+D1q47RAO44W/ra1NHHx9gcvQAHv4qpHDqGL1IHn+bGT/bCnk14xEeWUbnrr3NXzwt6OwWJ1+xyvluNS5lOiCzrcfSYlZorfD1JOXl/dllbtz507R5+fMCJcx/Zw5c0aMNzkDAgZApwtsEVGPvQYFm5+Hsmg0KhtNWPPI+1j/xgFYum1+ZZOo9XP8JZwv91mRJJVgaH7ql1Xu4MGDxQJrMpmwZ88e5OTkiClXuIzph3dS1NfXC/pLSUkJHAC1KkAWiotBty4Of3xqO9Zt3I3mRiMQQxEfy9zoInl06TGlzGdHoqcUhYPjhdJhxVNQUCCqXKvVin379omPeSEOp3HUc/SzAps1a1ZQfmdQ6oAPP9iJNc/8GpBcSNRwg0QiWrqMLYWcGvRtMRoJuTlJYpLFzh8xYgTi4uJEupeVlQne/c53vhNW53P0d3d3o7KyUgQFD/v7DQCzbhqPZzJVSKbqNCYmxi9P6bjUnSUsKVnZsJRjPuUIZ9rhnr5er8eRI0eE3LvzzjsRbuOmW2Njo6i8OVDi4+P7DwA6Krge/e0j2Lp1q4helmaXA0AvCHwTBEc8H6x0WOIxAMz/c+fODVmL4VLRz85nGbx48eKg/W6F1xmcrSSs1dlxTBcsGXu3CX7Tbai938OvXF3yJIsPBo45vzcbQnYr6zdwPy+8vK+I+1CszIIGgOQLziixN1q5NayjuoBpJRiR25c3yF1u9HNglZaWigV42bJlV34P24UAUATxYU1MJc3tBrRZuhETG3PBuySvJmPnWywWlJV/hk93F+Omm28O6r0BQe2G9lKJTe7BsjeehVnuEo+YkcnlVy0AXp8HzpYumMqqMVybjjlz5gT9HEG/S7IgdSDmXzsD//Hhi/BplVSpKQkEGe/Ev6qc72Hq66JislKP6FNmPLL2KRIb2v4PANs9k27DwYYavF5RLLqeXqIm6SqiIjEy5b1EDV2Q1Rnx65/9ClPGTeiTc/UJAFFyBX5/+72ob21Eid6/GdZHmSC2mUj9nncACzm/rg042ox75nwfD/xgaZ+drs/CMjk6Dn9a8AsUaFOBDgtFlLPnZourwPn17UBFI2ZPuxGP/eIRvpPx6gOAbUhqNjYs+TWGqpIBg9n/5phbff3U+SY7UGMAPj+NW8ZPw6rfPAatum+nbX1OzCOz8/Da/f+LMTFZAD8dpdsu7uXtNyDwdfD1cJYebwYONWDu9Jvx8uN/QHJC3w98QrIyFmbl4vWfPIqbs0cBZ4xAu9n//B5vmCmJKZGvo6mLov4M5Edb8NMf/gjrVqxEbHRonqQnGQwGnzZET8412Sz4/ZuvYM32zXDFq3mh8D+mhp8XFMoHefZGPVMOB0SVHqkyLX6z/BdY+r0FoQwBfUgB6LX3S3bid689j+OdTUBKjH9XBNcMvNhJfex43sltIUHQZvYrnZNtuHHydPyOpObo4SNCnYPhAYDN0NWBZ//6Z/zfrg/RJjn8QPDGXu1ZGSEFkWpctPhbHX7Hn6aob+hAXloWHlz4IxH1UUplOEgwfAD0WnnVUazf8le8u38njDKKzCSipQSNf5zJ07re50VI/2Cks9OZZvjZQ+Yexzd1Cr7Py8jG/NvuwMLZd2FgZlY4JUD4Aei1L2pP4K1PtuKtndtQ36mHjx+0Ha/1jzd5028vGHw/A29pEYD0PApRUIvXv6j3Op03A3AroZ3UjcEEhdWNMYUjMe/W2bht5k3hdnz/A6DXOk3d+LRsP7bv+RSfVXyO5u52dDqtfhC4mo5S+HdY9ILQ63zeVdfreOJ4iWqOlNgE4ejJYydg1rQbMGHM2HBRzdUDwNnWZe7G0eoTqD1Zj+O1VTjT3Ig2Ywe6CCSbeMaDu2esyQ/p1iI+Ng5JCYnIGzAIBflDMDgnF8MHD4Mqqt/+scD+DcB5xarXKxxvd9jFjLZ3WMMzB45slUotnohyFZlecTVdLTuaW8I67T/P35uO/A2ZCAARACIWASACQMQiAEQAiFgEgH89/0cACK85GICuiB/CZjUyp9O5JeKHsNl7ssbGxrX0QX3EFyG3LZIkbZG1trbyk+cW0fFFxCchsw/oWC6Xy42S1WotoA9avV5vts/n44f13ECv/Ghw7pR6I74Kmtp0UMQfp9etMpnsPfJ5F/k59f8FGAAhm1s5PDK1cgAAAABJRU5ErkJggg== + + + + + + + + + + + http://szotar.sztaki.hu/ + diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/hu/vatera.xml b/mobile/ios/Client/Assets/Search/SearchPlugins/hu/vatera.xml new file mode 100644 index 0000000000..b7f5a29808 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/hu/vatera.xml @@ -0,0 +1,15 @@ + + + + Vatera.hu + hu + ISO-8859-2 + ISO-8859-2 + data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAYAAADimHc4AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAA2hpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMC1jMDYxIDY0LjE0MDk0OSwgMjAxMC8xMi8wNy0xMDo1NzowMSAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtcE1NOk9yaWdpbmFsRG9jdW1lbnRJRD0ieG1wLmRpZDowMTgwMTE3NDA3MjA2ODExOEE2REI2ODhGMkQ2RTdCRSIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDpBNjZGQkZGMERGOTkxMUU1QkZFRUI4NzU3Q0YwODM1NiIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDpBNjZGQkZFRkRGOTkxMUU1QkZFRUI4NzU3Q0YwODM1NiIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ1M1LjEgTWFjaW50b3NoIj4gPHhtcE1NOkRlcml2ZWRGcm9tIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6MDM4MDExNzQwNzIwNjgxMTkyQjA4NjY3OTk1Qjk5MEMiIHN0UmVmOmRvY3VtZW50SUQ9InhtcC5kaWQ6MDE4MDExNzQwNzIwNjgxMThBNkRCNjg4RjJENkU3QkUiLz4gPC9yZGY6RGVzY3Jp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 + + + + http://m.vatera.hu/ + diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/hu/wikipedia-hu.xml b/mobile/ios/Client/Assets/Search/SearchPlugins/hu/wikipedia-hu.xml new file mode 100644 index 0000000000..368e603f37 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/hu/wikipedia-hu.xml @@ -0,0 +1,23 @@ + + + +Wikipédia (hu) +UTF-8 +data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAQAAABIkb+zAAAEXElEQVR4Ae3cA4xs2QJG4b9aj41n27Zt27Zt27Zt27Zt+2KM5lXP924qOzs56VPjKSRnrfhv7NUsp08v98+PsxwT4nJ+nPunl8KZ8/WYQL+eMydJrx5/EhN603lAHpVJ5bzZNp035uyZXM7Uy3LmM7ms9CITzVRGQRfQBXQBXUAX0AV0AV1AF9AFdAFdQBfQBXQBXcBMjgU7M5+1vqtZyXqmc/IsZDEL+53LP3KRPCCv62+rWd7vnpx6v/M5Vd935OF5R+6RtRyeQ3NYNjKTxZw2p8tiduR8uWfemY3+dkjWM5fFLPU/8q9yrRwbHLP39WkHamPD5z1WnNMb/dpW/uAdbipu413+qcl2H3N3cXFv9DNNtvmg68uxMY6dPfewTZNvOauouqEdULlTY532IgqHua+ZxnoHm4ANj63biRNQPZedVPY5nTR1BXsVbNuyTlsDqy4tW/wK4NYiJ01AXMlm+wGrL1cDnWbLejC4m7T4DvAOOSkD4v0qztGyLzpUweOk4UXAp6XV72DNmU7qgPPao+DJ0uLTFPzXXGN5I9acU1o8vb14jZzUAfFeBdvMtuwLDlfwIKme2248T1p9MvY59zACLqcy4Kf5eQr+ZbYRfrAFaXHaf/FRGUZAfE/Bzwf8OKwpuJ/0vYRNPEpavT248rACbqfimtLiqxT8w4yIz+Of5qTV7+LHMqyAaf9R8Clp8ex2K7iXuAa4q7R6aXCH4QXE4xVsOq+0+DYFfzXth/ilnrT6Tvzb9DADlqwqDPjTd377FHwI3EBaPb0NPFaGGRBvoLBiUVr8EFS+KgN8KpYtDjvgQo6i8Hhp8ZJUjnIZaXXGNrxKhh0QX6TwHzPS4uco/FtPWr0T9jnPKAJuOOiCc/UqVG4jrf4AH5dRBPT8UWHg3/BvUfhl635ZcNXRBMSDqFxFWvwklVu07O/BT2RUAad0GIWPtexXgMpPt+xntAt3Gl1AvJjCPufasn4d/6NyY2n4DPzXzCgDzmEvhVdIw+tj02X8nsIPGvus7Xi8jDIgPkLhSPON5ad4t7gbletJ9S71n+BIA65C5dGal1g3nENM+weF70j1R3iNjDogfkrhn6br5dU/4UXS94EtF7+vUC8IjjzgrlRuJ33v27j2dTLbKXxd+r4Pn5RRB9RfxsL3y4H/i0dI9TFUrirOZDeuPh4B8TQqVyjH/ZtZqZ7KIRS+JJ6Fn8k4BNRL9IUPmncwbr/1b37lanbiLuMTEG+nsNfb8cOWq0DLFA7E/8yMU8AlNLnaoP/alSfKOAXEN6h8Qlo8k3UKq5bGLeAWFPa6oLT6Ogqvk3ELmPJ3wOtlgOe0B2w6//gFxCPBsjPIQN8JPi3jGDDvSDxVDPaCNnFNOdGczrNzIrEnRyR5XPZlMIdmKofnpTnx6B783QV0ARNOF9AFdAFdQBfQBXQBXUAX0AV0AV3A6OgCVjLJrEzlj5lk/jiVt2eSefvEvzTJxL84zMS/PM//Af1pBRpQAkXBAAAAAElFTkSuQmCC + + + + + + + + + + + + + +https://hu.wikipedia.org/wiki/Speciális:Keresés + diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/hy-AM/default.txt b/mobile/ios/Client/Assets/Search/SearchPlugins/hy-AM/default.txt new file mode 100644 index 0000000000..ced95a5cb9 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/hy-AM/default.txt @@ -0,0 +1 @@ +Google \ No newline at end of file diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/hy-AM/list-am.xml b/mobile/ios/Client/Assets/Search/SearchPlugins/hy-AM/list-am.xml new file mode 100644 index 0000000000..e5b7cd94d5 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/hy-AM/list-am.xml @@ -0,0 +1,13 @@ + + + +List.am +UTF-8 +data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAYAAADimHc4AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAA2hpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMC1jMDYxIDY0LjE0MDk0OSwgMjAxMC8xMi8wNy0xMDo1NzowMSAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtcE1NOk9yaWdpbmFsRG9jdW1lbnRJRD0ieG1wLmRpZDowMTgwMTE3NDA3MjA2ODExOEE2REI2ODhGMkQ2RTdCRSIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDo1MjY4MTNGMEUyMDMxMUU1QjEwMEEwMkZCOUU1N0Y5MCIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDo1MjY4MTNFRkUyMDMxMUU1QjEwMEEwMkZCOUU1N0Y5MCIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ1M1LjEgTWFjaW50b3NoIj4gPHhtcE1NOkRlcml2ZWRGcm9tIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6MDE4MDExNzQwNzIwNjgxMThBNkRDRjQ4QkY4NDdGOTUiIHN0UmVmOmRvY3VtZW50SUQ9InhtcC5kaWQ6MDE4MDExNzQwNzIwNjgxMThBNkRCNjg4RjJENkU3QkUiLz4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+IDw/eHBhY2tldCBlbmQ9InIiPz6wgdxSAAABj0lEQVR42uzbPUoDURSA0STGcQGWFq4gYJHGJQiWwewgtetxB5GUbkAQbCIIWYFFSjubGX/vg5dWm7wZSc4HF4yIxHsyMwEz/bque7kqZhYziTnPj7W9mpjHmEXMTX7c62eAk5i7mDN7aqXnmMuYdQJIr/RlzMheWm0VMx7k047lt1/a+SwBTO2is6bpFFS74HZ3YR5YfqdVAzvoNgAAAAgAAAEAIAAABACAAOxPw23/ws/To6JP+OCldgQIAAABACAAAAQAgAAAEAAAAgBAAAAIAAABACAAAPR76TbVb2twBAAQAAACAEAA9qqt3x9wfHVY9Am/3r7/+TNfi3L3KAwm9f8GSDUfZf74arh7R4BTEAAAAgBAAAAIAAABACAAAAQAgAAAEAAAAgBAhSryOYNd/PRCqdwf4BQEQAAACAAAAQAgAAAEAIAA7A9AYw2d9ZYAnuyhs1YJYG4PnTVP/5Cp4otlzMg+2n31x4w314CL/A21t/y082bzLmidNGKuY+7tp1gPecfjvPPejwADAF1kP7+yQzo3AAAAAElFTkSuQmCC +http://www.list.am/category?q= + + + + diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/hy-AM/list.txt b/mobile/ios/Client/Assets/Search/SearchPlugins/hy-AM/list.txt new file mode 100644 index 0000000000..e73b45ffc3 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/hy-AM/list.txt @@ -0,0 +1,6 @@ +amazondotcom +bing +google +list-am +wikipedia-hy-AM +yahoo diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/hy-AM/wikipedia-hy-AM.xml b/mobile/ios/Client/Assets/Search/SearchPlugins/hy-AM/wikipedia-hy-AM.xml new file mode 100644 index 0000000000..098b620bd3 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/hy-AM/wikipedia-hy-AM.xml @@ -0,0 +1,23 @@ + + + +Վիքիպեդիա (hy) +UTF-8 +data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAQAAABIkb+zAAAEXElEQVR4Ae3cA4xs2QJG4b9aj41n27Zt27Zt27Zt27Zt+2KM5lXP924qOzs56VPjKSRnrfhv7NUsp08v98+PsxwT4nJ+nPunl8KZ8/WYQL+eMydJrx5/EhN603lAHpVJ5bzZNp035uyZXM7Uy3LmM7ms9CITzVRGQRfQBXQBXUAX0AV0AV1AF9AFdAFdQBfQBXQBXcBMjgU7M5+1vqtZyXqmc/IsZDEL+53LP3KRPCCv62+rWd7vnpx6v/M5Vd935OF5R+6RtRyeQ3NYNjKTxZw2p8tiduR8uWfemY3+dkjWM5fFLPU/8q9yrRwbHLP39WkHamPD5z1WnNMb/dpW/uAdbipu413+qcl2H3N3cXFv9DNNtvmg68uxMY6dPfewTZNvOauouqEdULlTY532IgqHua+ZxnoHm4ANj63biRNQPZedVPY5nTR1BXsVbNuyTlsDqy4tW/wK4NYiJ01AXMlm+wGrL1cDnWbLejC4m7T4DvAOOSkD4v0qztGyLzpUweOk4UXAp6XV72DNmU7qgPPao+DJ0uLTFPzXXGN5I9acU1o8vb14jZzUAfFeBdvMtuwLDlfwIKme2248T1p9MvY59zACLqcy4Kf5eQr+ZbYRfrAFaXHaf/FRGUZAfE/Bzwf8OKwpuJ/0vYRNPEpavT248rACbqfimtLiqxT8w4yIz+Of5qTV7+LHMqyAaf9R8Clp8ex2K7iXuAa4q7R6aXCH4QXE4xVsOq+0+DYFfzXth/ilnrT6Tvzb9DADlqwqDPjTd377FHwI3EBaPb0NPFaGGRBvoLBiUVr8EFS+KgN8KpYtDjvgQo6i8Hhp8ZJUjnIZaXXGNrxKhh0QX6TwHzPS4uco/FtPWr0T9jnPKAJuOOiCc/UqVG4jrf4AH5dRBPT8UWHg3/BvUfhl635ZcNXRBMSDqFxFWvwklVu07O/BT2RUAad0GIWPtexXgMpPt+xntAt3Gl1AvJjCPufasn4d/6NyY2n4DPzXzCgDzmEvhVdIw+tj02X8nsIPGvus7Xi8jDIgPkLhSPON5ad4t7gbletJ9S71n+BIA65C5dGal1g3nENM+weF70j1R3iNjDogfkrhn6br5dU/4UXS94EtF7+vUC8IjjzgrlRuJ33v27j2dTLbKXxd+r4Pn5RRB9RfxsL3y4H/i0dI9TFUrirOZDeuPh4B8TQqVyjH/ZtZqZ7KIRS+JJ6Fn8k4BNRL9IUPmncwbr/1b37lanbiLuMTEG+nsNfb8cOWq0DLFA7E/8yMU8AlNLnaoP/alSfKOAXEN6h8Qlo8k3UKq5bGLeAWFPa6oLT6Ogqvk3ELmPJ3wOtlgOe0B2w6//gFxCPBsjPIQN8JPi3jGDDvSDxVDPaCNnFNOdGczrNzIrEnRyR5XPZlMIdmKofnpTnx6B783QV0ARNOF9AFdAFdQBfQBXQBXUAX0AV0AV3A6OgCVjLJrEzlj5lk/jiVt2eSefvEvzTJxL84zMS/PM//Af1pBRpQAkXBAAAAAElFTkSuQmCC + + + + + + + + + + + + + +https://hy.wikipedia.org/wiki/Սպասարկող:Որոնել + diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/is/default.txt b/mobile/ios/Client/Assets/Search/SearchPlugins/is/default.txt new file mode 100644 index 0000000000..ced95a5cb9 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/is/default.txt @@ -0,0 +1 @@ +Google \ No newline at end of file diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/is/leit-is.xml b/mobile/ios/Client/Assets/Search/SearchPlugins/is/leit-is.xml new file mode 100644 index 0000000000..80b395d5d9 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/is/leit-is.xml @@ -0,0 +1,14 @@ + + + +leit.is +UTF-8 +data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAYAAADimHc4AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAA2hpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMC1jMDYxIDY0LjE0MDk0OSwgMjAxMC8xMi8wNy0xMDo1NzowMSAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtcE1NOk9yaWdpbmFsRG9jdW1lbnRJRD0ieG1wLmRpZDowMTgwMTE3NDA3MjA2ODExOEE2REI2ODhGMkQ2RTdCRSIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDoyRDEyM0RFNzNDRjExMUU1ODU5M0Q0QjRFMkEzNzU5NSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDoyRDEyM0RFNjNDRjExMUU1ODU5M0Q0QjRFMkEzNzU5NSIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ1M1LjEgTWFjaW50b3NoIj4gPHhtcE1NOkRlcml2ZWRGcm9tIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6MDY4MDExNzQwNzIwNjgxMTg3MUZBMEU2Mzg4RjI3OTgiIHN0UmVmOmRvY3VtZW50SUQ9InhtcC5kaWQ6MDE4MDExNzQwNzIwNjgxMThBNkRCNjg4RjJENkU3QkUiLz4gPC9yZGY6RGVzY3Jp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 + + + + +http://leit.is/ + diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/is/list.txt b/mobile/ios/Client/Assets/Search/SearchPlugins/is/list.txt new file mode 100644 index 0000000000..283d194f60 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/is/list.txt @@ -0,0 +1,6 @@ +amazondotcom +bing +google +leit-is +wikipedia-is +yahoo diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/is/wikipedia-is.xml b/mobile/ios/Client/Assets/Search/SearchPlugins/is/wikipedia-is.xml new file mode 100644 index 0000000000..7e31731196 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/is/wikipedia-is.xml @@ -0,0 +1,23 @@ + + + +Wikipedia (is) +UTF-8 +data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAQAAABIkb+zAAAEXElEQVR4Ae3cA4xs2QJG4b9aj41n27Zt27Zt27Zt27Zt+2KM5lXP924qOzs56VPjKSRnrfhv7NUsp08v98+PsxwT4nJ+nPunl8KZ8/WYQL+eMydJrx5/EhN603lAHpVJ5bzZNp035uyZXM7Uy3LmM7ms9CITzVRGQRfQBXQBXUAX0AV0AV1AF9AFdAFdQBfQBXQBXcBMjgU7M5+1vqtZyXqmc/IsZDEL+53LP3KRPCCv62+rWd7vnpx6v/M5Vd935OF5R+6RtRyeQ3NYNjKTxZw2p8tiduR8uWfemY3+dkjWM5fFLPU/8q9yrRwbHLP39WkHamPD5z1WnNMb/dpW/uAdbipu413+qcl2H3N3cXFv9DNNtvmg68uxMY6dPfewTZNvOauouqEdULlTY532IgqHua+ZxnoHm4ANj63biRNQPZedVPY5nTR1BXsVbNuyTlsDqy4tW/wK4NYiJ01AXMlm+wGrL1cDnWbLejC4m7T4DvAOOSkD4v0qztGyLzpUweOk4UXAp6XV72DNmU7qgPPao+DJ0uLTFPzXXGN5I9acU1o8vb14jZzUAfFeBdvMtuwLDlfwIKme2248T1p9MvY59zACLqcy4Kf5eQr+ZbYRfrAFaXHaf/FRGUZAfE/Bzwf8OKwpuJ/0vYRNPEpavT248rACbqfimtLiqxT8w4yIz+Of5qTV7+LHMqyAaf9R8Clp8ex2K7iXuAa4q7R6aXCH4QXE4xVsOq+0+DYFfzXth/ilnrT6Tvzb9DADlqwqDPjTd377FHwI3EBaPb0NPFaGGRBvoLBiUVr8EFS+KgN8KpYtDjvgQo6i8Hhp8ZJUjnIZaXXGNrxKhh0QX6TwHzPS4uco/FtPWr0T9jnPKAJuOOiCc/UqVG4jrf4AH5dRBPT8UWHg3/BvUfhl635ZcNXRBMSDqFxFWvwklVu07O/BT2RUAad0GIWPtexXgMpPt+xntAt3Gl1AvJjCPufasn4d/6NyY2n4DPzXzCgDzmEvhVdIw+tj02X8nsIPGvus7Xi8jDIgPkLhSPON5ad4t7gbletJ9S71n+BIA65C5dGal1g3nENM+weF70j1R3iNjDogfkrhn6br5dU/4UXS94EtF7+vUC8IjjzgrlRuJ33v27j2dTLbKXxd+r4Pn5RRB9RfxsL3y4H/i0dI9TFUrirOZDeuPh4B8TQqVyjH/ZtZqZ7KIRS+JJ6Fn8k4BNRL9IUPmncwbr/1b37lanbiLuMTEG+nsNfb8cOWq0DLFA7E/8yMU8AlNLnaoP/alSfKOAXEN6h8Qlo8k3UKq5bGLeAWFPa6oLT6Ogqvk3ELmPJ3wOtlgOe0B2w6//gFxCPBsjPIQN8JPi3jGDDvSDxVDPaCNnFNOdGczrNzIrEnRyR5XPZlMIdmKofnpTnx6B783QV0ARNOF9AFdAFdQBfQBXQBXUAX0AV0AV3A6OgCVjLJrEzlj5lk/jiVt2eSefvEvzTJxL84zMS/PM//Af1pBRpQAkXBAAAAAElFTkSuQmCC + + + + + + + + + + + + + +https://is.wikipedia.org/wiki/Kerfissíða:Leit + diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/it/default.txt b/mobile/ios/Client/Assets/Search/SearchPlugins/it/default.txt new file mode 100644 index 0000000000..ced95a5cb9 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/it/default.txt @@ -0,0 +1 @@ +Google \ No newline at end of file diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/it/list.txt b/mobile/ios/Client/Assets/Search/SearchPlugins/it/list.txt new file mode 100644 index 0000000000..c0ec68b395 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/it/list.txt @@ -0,0 +1,3 @@ +google +twitter +wikipedia-it diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/it/wikipedia-it.xml b/mobile/ios/Client/Assets/Search/SearchPlugins/it/wikipedia-it.xml new file mode 100644 index 0000000000..c30a83d631 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/it/wikipedia-it.xml @@ -0,0 +1,23 @@ + + + +Wikipedia (it) +UTF-8 +data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAQAAABIkb+zAAAEXElEQVR4Ae3cA4xs2QJG4b9aj41n27Zt27Zt27Zt27Zt+2KM5lXP924qOzs56VPjKSRnrfhv7NUsp08v98+PsxwT4nJ+nPunl8KZ8/WYQL+eMydJrx5/EhN603lAHpVJ5bzZNp035uyZXM7Uy3LmM7ms9CITzVRGQRfQBXQBXUAX0AV0AV1AF9AFdAFdQBfQBXQBXcBMjgU7M5+1vqtZyXqmc/IsZDEL+53LP3KRPCCv62+rWd7vnpx6v/M5Vd935OF5R+6RtRyeQ3NYNjKTxZw2p8tiduR8uWfemY3+dkjWM5fFLPU/8q9yrRwbHLP39WkHamPD5z1WnNMb/dpW/uAdbipu413+qcl2H3N3cXFv9DNNtvmg68uxMY6dPfewTZNvOauouqEdULlTY532IgqHua+ZxnoHm4ANj63biRNQPZedVPY5nTR1BXsVbNuyTlsDqy4tW/wK4NYiJ01AXMlm+wGrL1cDnWbLejC4m7T4DvAOOSkD4v0qztGyLzpUweOk4UXAp6XV72DNmU7qgPPao+DJ0uLTFPzXXGN5I9acU1o8vb14jZzUAfFeBdvMtuwLDlfwIKme2248T1p9MvY59zACLqcy4Kf5eQr+ZbYRfrAFaXHaf/FRGUZAfE/Bzwf8OKwpuJ/0vYRNPEpavT248rACbqfimtLiqxT8w4yIz+Of5qTV7+LHMqyAaf9R8Clp8ex2K7iXuAa4q7R6aXCH4QXE4xVsOq+0+DYFfzXth/ilnrT6Tvzb9DADlqwqDPjTd377FHwI3EBaPb0NPFaGGRBvoLBiUVr8EFS+KgN8KpYtDjvgQo6i8Hhp8ZJUjnIZaXXGNrxKhh0QX6TwHzPS4uco/FtPWr0T9jnPKAJuOOiCc/UqVG4jrf4AH5dRBPT8UWHg3/BvUfhl635ZcNXRBMSDqFxFWvwklVu07O/BT2RUAad0GIWPtexXgMpPt+xntAt3Gl1AvJjCPufasn4d/6NyY2n4DPzXzCgDzmEvhVdIw+tj02X8nsIPGvus7Xi8jDIgPkLhSPON5ad4t7gbletJ9S71n+BIA65C5dGal1g3nENM+weF70j1R3iNjDogfkrhn6br5dU/4UXS94EtF7+vUC8IjjzgrlRuJ33v27j2dTLbKXxd+r4Pn5RRB9RfxsL3y4H/i0dI9TFUrirOZDeuPh4B8TQqVyjH/ZtZqZ7KIRS+JJ6Fn8k4BNRL9IUPmncwbr/1b37lanbiLuMTEG+nsNfb8cOWq0DLFA7E/8yMU8AlNLnaoP/alSfKOAXEN6h8Qlo8k3UKq5bGLeAWFPa6oLT6Ogqvk3ELmPJ3wOtlgOe0B2w6//gFxCPBsjPIQN8JPi3jGDDvSDxVDPaCNnFNOdGczrNzIrEnRyR5XPZlMIdmKofnpTnx6B783QV0ARNOF9AFdAFdQBfQBXQBXUAX0AV0AV3A6OgCVjLJrEzlj5lk/jiVt2eSefvEvzTJxL84zMS/PM//Af1pBRpQAkXBAAAAAElFTkSuQmCC + + + + + + + + + + + + + +https://it.wikipedia.org/wiki/Speciale:Ricerca + diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/ja/amazon-jp.xml b/mobile/ios/Client/Assets/Search/SearchPlugins/ja/amazon-jp.xml new file mode 100644 index 0000000000..9dd84c2844 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/ja/amazon-jp.xml @@ -0,0 +1,30 @@ + + + +Amazon.co.jp +検索エンジン - Amazon.co.jp 検索 +UTF-8 +data:image/png;base64,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 + + + + + + + + +https://www.amazon.co.jp/ + diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/ja/default.txt b/mobile/ios/Client/Assets/Search/SearchPlugins/ja/default.txt new file mode 100644 index 0000000000..ced95a5cb9 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/ja/default.txt @@ -0,0 +1 @@ +Google \ No newline at end of file diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/ja/list.txt b/mobile/ios/Client/Assets/Search/SearchPlugins/ja/list.txt new file mode 100644 index 0000000000..d6504a9ca0 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/ja/list.txt @@ -0,0 +1,6 @@ +amazon-jp +bing +google +twitter-ja +wikipedia-ja +yahoo-jp diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/ja/twitter-ja.xml b/mobile/ios/Client/Assets/Search/SearchPlugins/ja/twitter-ja.xml new file mode 100644 index 0000000000..37c0fff880 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/ja/twitter-ja.xml @@ -0,0 +1,15 @@ + + + + Twitter + 検索エンジン - リアルタイム Twitter 検索 + data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAYAAADimHc4AAAAAXNSR0IArs4c6QAACW5JREFUeAHtXGmMFEUUft07x87sAS6ngILKYkTDoaLiFeSIKP4wGggmxoSsaOIRE3+q0R8aNeKtMUZB/WEMh5JIFGMEMYiCyC2Xigi4wCKw7DJ7zOwx7ft6aZ0duuesqt7Beklv73R1V736vjpevX7VBp0Ry7KMucub65JW8n4yaCxZVOWk6bMABAyKMaa7TcNcuPiufosMw7CQq4E/s79sGUrtnR8TWdPwW4tsBIzVFAneu2xWZYOJlq/Blw14ev7c0LnBA3tjzvKm+VYy+V76Lfq3fAR4OJpvMvh18ovSJbghgPnWtCdct1R9TT4CbOyY2tqRj7NnCWxpmp6JOkEJApoAJTB7F6IJ8MZGSYomQAnM3oVoAryxUZKiCVACs3chmgBvbJSkaAKUwOxdiCbAGxslKZoAJTB7F6IJ8MZGSYomQAnM3oVoAryxUZKiCVACs3chmgBvbJSkaAKUwOxdSMA76dxICXDcx/DqMjqv3KDqsEmxjiQ1tlt0NNZN/K/vcs4SMGFIgKaMDNOEoUGKBs/GOdFFtLWhk9b91UEbj3SefYPLlZqIQV1M2umEHdLjckf+l6QRgLHNjwZ2Ibf2eRMidPmgzFULc/J1I4L28XtjN324rY32nep2RXBw1KQ7Ly2ncUzqY1+fdr2n0IuZtSww1xru7g9eVUELfmyhLnGNJas2k4cH6eFJFRQqy3prrxtqa8rouVuqaOHWNlr1Z4edVhUyaMKQIF09LEjX8FHGLeqTnXHqFlwfKQRMvzhME4cG6CEG482Nrb0qK+vHtFEhJj1acPYmzxUPXBmlKwYFaVCFSZecV0a45sih5iR98Vvc+SnsLJwAKD39orCt4I0XBKmlI0IfbGsXprBbRpcNDFDdxMLBT83zetY5XeI8X7z9c6uU3izcDB3Crac/D0GOzLwkTI9OilLZf5ecJCHnINfgUe5pAeE16VGvg6eFF39ooQPNPfND/zCGJnHtVlxOZ+AEAely04Uh2wR8ZX0Lxd3nufRHcv592+gwDYzKYbehJUlvccuHxTSVh7jJI0L20PTUd7Gc9ct2o3ACBle4z4DjudW8MLWaXuc54eCZ1pRNuWzpgP2O2vJstxWc3s1m3DM3V/07qVs8Ab+0vpWOxMTZd2c314LV7Xkw01AzvNqk59nauJUnaREC6yV1uBORZ2oe0DfVolqyO06bj+a2ZkjNJ9P/wgk43pa5dQS5g9RNjNDTN1XSiCID82AmqhC0/I9/aafle8VbQcIJ+Ls1MwEOYFcMDtCC6dU0b3yEKoKFjeEDXeYbJ39RZ0zCr//USit+S4jKslc+wueAIy3dbHpaVMkLmWyCxQ0m0VtGhWn1nwlauS9B2XpQap415cLbT2r29uT73LoY/XpSsOWQUorwGsBXsvZgz2oypZyM/5ZzM5hVG6a3ZlbT49dW0CReecK8zCYoS6Y0J5JSwYfuwnsAMl3Frfl2BjRfwSLO8c9g8YMJDw6zfY1ddIRNwnQ50X72tfR7ivltGtl7cTH541kpBNSzmbZqfwdNvzhUsH7oFTfwqhQHBMPaH+wsO8ZENMbhUk5SFL5miRJX4MiSQgAw+WhHG41lj+SwIi0dB1/MKVhL0BDnivyzCgJyGGnzq6hj0cB6eHVDKzXFBbsP81OnqLvbOuXrLpwA+M2x2JoyMmQPFU+sidFfp+WO1UWhnOHhXE3qDFlkTRI+BMFyGM0r1NE1Pd7JGL89ahL4BilrjQTe0NAqz/x01BROwGF+15oqVew9xFGKItLn41V/4UPQ7uNd9ntTrwJL6TrMX9kinIAEdwDRDivZILjlD1e0iqFTOAGozOe/indauYEk89reE/JbP/SXQgCiC9YdEuu2lQm2W96bBLud3crANSkEIOOFHOZxok2+HY2yRAvcINvYBaJCpBGARcyz38foFEehlZpsYfBVRc1JIwCgH+WJ7Jm1MTrQ1Ns07euErNovx/fvVm+pBKBAWBNYDeNtErp2X5d6XrXvZFNalQhfiLkpDr/94l1x+uL3BCGA6qrzQzRmQO/AJ7fn/Li2cp9aC046AdW8Cn7k6gp2yiXtSDNETSB0pS+ujeH7+e5Afi+Tim0k0glAJDGCpqZwy+/rspSjHhS8AugFg/Q5AKWhYn1dYCh8f0ht6wcmSgjYe7LLfkPWV0lAANa7m9v442HqRQkBqBYWZhvq1Sxu8oVxBUc97/fJVFZGQJKb1xsclrgpx90o+YJY6P0Yepbt8W+IVEYAAMLmhlc5yOkrjv8BIX4LXhYt4FhP2eEtmeqplAAogsp+uL2dnuTF2cbDnb4RgQbwGjeGfALBMgFZaJox+9NGX9sigmvHDQ4SAm0HREzeWdOzHajQCuXyHMB/Z1MbrfXB6knXT3kPSFcAURMA4kteJUc5RhThijIFgbbvbekb4KOe0hdi2cDEihjxofdcHiHsXJQpMDff54143ype7Waqk+QqexeNuHvs472dwRcVvOVdGlErR9bBAPjlb3WOtkz6OGlKCUBrH9mvjK7j7aQzeJOGqmgJeDhf3tAidGeLA2CxZ2EE9GOnW4LtTLicsUsGTjh8GgDXh1aW8fbPAGFPQC5h68VWynkeky08sEt2tVMnDz99UYQRgAl0Pm8Vxcbm1P21flUaC6xFvPqWGdsvom7CzdAxbE7eNy5q+/tFKJhvHngBBOcfvgFRCiKcAKfSiGTGzpdJ5/NmC/eNk86tQs57OIwEIfE/1ncI/5yAEAU9MhE2BKXnv/1YF+FAtPSNF4Ts9wHY/i9SEPS7lcNH1hxM0GGBW0dF6pgtL2k9wK1gfACjtiZAGKbGDAhwAG+AsBEjF4EZCZDrOfYUQVM7jnVy9LWvi/hc1M56T47Vz5pPTjfEGESEfOCAwCytYFJgGYGcqpBp/4/Vaju/mrIPDm/BbhgVYYK2Uor/KCUgvW5ov9h6hKPBTiyt8JX0+hTyW7LnpRCV/l/PaAJ85lsToAnwGQGfi9c9QBPgMwI+F697gCbAZwR8Ll73AE2Azwj4XLzuAZoAnxHwuXjdAzQBPiPgc/G6B2gCfEbA5+KxW07ch5B9rkzJFc/Ym7wvZ3fJKX6uKMzYm6ZhLjxX6lNq9QD2hmVZxpzPmr4hsqaVWgVKW19j9dK7+8/gb5MaFkWC93KMwurSrlApac9YM+bAHpEhtqAnzF3eXJe0kvfzxDyW54YqJ02fBSAAYwdjPg87i+/qt8hu+JztP8DF2aE2lt4MAAAAAElFTkSuQmCC + + + + + UTF-8 + https://mobile.twitter.com/search + diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/ja/wikipedia-ja.xml b/mobile/ios/Client/Assets/Search/SearchPlugins/ja/wikipedia-ja.xml new file mode 100644 index 0000000000..65979ed1e7 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/ja/wikipedia-ja.xml @@ -0,0 +1,23 @@ + + + +Wikipedia (ja) +UTF-8 +data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAQAAABIkb+zAAAEXElEQVR4Ae3cA4xs2QJG4b9aj41n27Zt27Zt27Zt27Zt+2KM5lXP924qOzs56VPjKSRnrfhv7NUsp08v98+PsxwT4nJ+nPunl8KZ8/WYQL+eMydJrx5/EhN603lAHpVJ5bzZNp035uyZXM7Uy3LmM7ms9CITzVRGQRfQBXQBXUAX0AV0AV1AF9AFdAFdQBfQBXQBXcBMjgU7M5+1vqtZyXqmc/IsZDEL+53LP3KRPCCv62+rWd7vnpx6v/M5Vd935OF5R+6RtRyeQ3NYNjKTxZw2p8tiduR8uWfemY3+dkjWM5fFLPU/8q9yrRwbHLP39WkHamPD5z1WnNMb/dpW/uAdbipu413+qcl2H3N3cXFv9DNNtvmg68uxMY6dPfewTZNvOauouqEdULlTY532IgqHua+ZxnoHm4ANj63biRNQPZedVPY5nTR1BXsVbNuyTlsDqy4tW/wK4NYiJ01AXMlm+wGrL1cDnWbLejC4m7T4DvAOOSkD4v0qztGyLzpUweOk4UXAp6XV72DNmU7qgPPao+DJ0uLTFPzXXGN5I9acU1o8vb14jZzUAfFeBdvMtuwLDlfwIKme2248T1p9MvY59zACLqcy4Kf5eQr+ZbYRfrAFaXHaf/FRGUZAfE/Bzwf8OKwpuJ/0vYRNPEpavT248rACbqfimtLiqxT8w4yIz+Of5qTV7+LHMqyAaf9R8Clp8ex2K7iXuAa4q7R6aXCH4QXE4xVsOq+0+DYFfzXth/ilnrT6Tvzb9DADlqwqDPjTd377FHwI3EBaPb0NPFaGGRBvoLBiUVr8EFS+KgN8KpYtDjvgQo6i8Hhp8ZJUjnIZaXXGNrxKhh0QX6TwHzPS4uco/FtPWr0T9jnPKAJuOOiCc/UqVG4jrf4AH5dRBPT8UWHg3/BvUfhl635ZcNXRBMSDqFxFWvwklVu07O/BT2RUAad0GIWPtexXgMpPt+xntAt3Gl1AvJjCPufasn4d/6NyY2n4DPzXzCgDzmEvhVdIw+tj02X8nsIPGvus7Xi8jDIgPkLhSPON5ad4t7gbletJ9S71n+BIA65C5dGal1g3nENM+weF70j1R3iNjDogfkrhn6br5dU/4UXS94EtF7+vUC8IjjzgrlRuJ33v27j2dTLbKXxd+r4Pn5RRB9RfxsL3y4H/i0dI9TFUrirOZDeuPh4B8TQqVyjH/ZtZqZ7KIRS+JJ6Fn8k4BNRL9IUPmncwbr/1b37lanbiLuMTEG+nsNfb8cOWq0DLFA7E/8yMU8AlNLnaoP/alSfKOAXEN6h8Qlo8k3UKq5bGLeAWFPa6oLT6Ogqvk3ELmPJ3wOtlgOe0B2w6//gFxCPBsjPIQN8JPi3jGDDvSDxVDPaCNnFNOdGczrNzIrEnRyR5XPZlMIdmKofnpTnx6B783QV0ARNOF9AFdAFdQBfQBXQBXUAX0AV0AV3A6OgCVjLJrEzlj5lk/jiVt2eSefvEvzTJxL84zMS/PM//Af1pBRpQAkXBAAAAAElFTkSuQmCC + + + + + + + + + + + + + +https://ja.wikipedia.org/wiki/特別:検索 + diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/ja/yahoo-jp.xml b/mobile/ios/Client/Assets/Search/SearchPlugins/ja/yahoo-jp.xml new file mode 100644 index 0000000000..1cd56a5fed --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/ja/yahoo-jp.xml @@ -0,0 +1,17 @@ + + + +Yahoo! JAPAN +検索エンジン - Yahoo!検索 +UTF-8 +data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAEJGlDQ1BJQ0MgUHJvZmlsZQAAOBGFVd9v21QUPolvUqQWPyBYR4eKxa9VU1u5GxqtxgZJk6XtShal6dgqJOQ6N4mpGwfb6baqT3uBNwb8AUDZAw9IPCENBmJ72fbAtElThyqqSUh76MQPISbtBVXhu3ZiJ1PEXPX6yznfOec7517bRD1fabWaGVWIlquunc8klZOnFpSeTYrSs9RLA9Sr6U4tkcvNEi7BFffO6+EdigjL7ZHu/k72I796i9zRiSJPwG4VHX0Z+AxRzNRrtksUvwf7+Gm3BtzzHPDTNgQCqwKXfZwSeNHHJz1OIT8JjtAq6xWtCLwGPLzYZi+3YV8DGMiT4VVuG7oiZpGzrZJhcs/hL49xtzH/Dy6bdfTsXYNY+5yluWO4D4neK/ZUvok/17X0HPBLsF+vuUlhfwX4j/rSfAJ4H1H0qZJ9dN7nR19frRTeBt4Fe9FwpwtN+2p1MXscGLHR9SXrmMgjONd1ZxKzpBeA71b4tNhj6JGoyFNp4GHgwUp9qplfmnFW5oTdy7NamcwCI49kv6fN5IAHgD+0rbyoBc3SOjczohbyS1drbq6pQdqumllRC/0ymTtej8gpbbuVwpQfyw66dqEZyxZKxtHpJn+tZnpnEdrYBbueF9qQn93S7HQGGHnYP7w6L+YGHNtd1FJitqPAR+hERCNOFi1i1alKO6RQnjKUxL1GNjwlMsiEhcPLYTEiT9ISbN15OY/jx4SMshe9LaJRpTvHr3C/ybFYP1PZAfwfYrPsMBtnE6SwN9ib7AhLwTrBDgUKcm06FSrTfSj187xPdVQWOk5Q8vxAfSiIUc7Z7xr6zY/+hpqwSyv0I0/QMTRb7RMgBxNodTfSPqdraz/sDjzKBrv4zu2+a2t0/HHzjd2Lbcc2sG7GtsL42K+xLfxtUgI7YHqKlqHK8HbCCXgjHT1cAdMlDetv4FnQ2lLasaOl6vmB0CMmwT/IPszSueHQqv6i/qluqF+oF9TfO2qEGTumJH0qfSv9KH0nfS/9TIp0Wboi/SRdlb6RLgU5u++9nyXYe69fYRPdil1o1WufNSdTTsp75BfllPy8/LI8G7AUuV8ek6fkvfDsCfbNDP0dvRh0CrNqTbV7LfEEGDQPJQadBtfGVMWEq3QWWdufk6ZSNsjG2PQjp3ZcnOWWing6noonSInvi0/Ex+IzAreevPhe+CawpgP1/pMTMDo64G0sTCXIM+KdOnFWRfQKdJvQzV1+Bt8OokmrdtY2yhVX2a+qrykJfMq4Ml3VR4cVzTQVz+UoNne4vcKLoyS+gyKO6EHe+75Fdt0Mbe5bRIf/wjvrVmhbqBN97RD1vxrahvBOfOYzoosH9bq94uejSOQGkVM6sN/7HelL4t10t9F4gPdVzydEOx83Gv+uNxo7XyL/FtFl8z9ZAHF4bBsrEwAAAAlwSFlzAAALEwAACxMBAJqcGAAAASdJREFUWAntVNEOgjAQ4wxG//9rNZpMatKkGXfHhhBftpeNrdf2ysBKKdM/x+Wf4tAeBkYC9ppu3Z/BXB521OXd9QrMrNt0ZNjwHyDhkoaLu07P1f5SZ2+7NxlhYsTzGaRfA8pOkO5hrUU8i7A854wGvObAuTKAoojYM5Glp8l5BqDlGug1URtWYbyqiA+48BJ63XpEkTiEM3FwYYQGcLhlIhNHfTaYUmoABFsmKEJCds39bAZ20wAIIhMkz8TrlFBDPNZNBgCMBslaO6/xzQayFFrF2YTimw2w+JeZ3SvHrA9nrJEcf1bg1+7xfJoBvXy1KIQ5wj8hAErCgmzWe6K1iD4ycegd0Kgzo3qWJgBgL6l2qrW632VAwWesD30FewwOAyOBkcAH7E+nC5+hXw8AAAAASUVORK5CYII= + + + + + + +http://search.yahoo.co.jp/ + diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/ka/default.txt b/mobile/ios/Client/Assets/Search/SearchPlugins/ka/default.txt new file mode 100644 index 0000000000..ced95a5cb9 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/ka/default.txt @@ -0,0 +1 @@ +Google \ No newline at end of file diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/ka/list.txt b/mobile/ios/Client/Assets/Search/SearchPlugins/ka/list.txt new file mode 100644 index 0000000000..73cae8e2d9 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/ka/list.txt @@ -0,0 +1,6 @@ +amazondotcom +bing +duckduckgo +google +wikipedia-ka +yahoo diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/ka/wikipedia-ka.xml b/mobile/ios/Client/Assets/Search/SearchPlugins/ka/wikipedia-ka.xml new file mode 100644 index 0000000000..3c3062138c --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/ka/wikipedia-ka.xml @@ -0,0 +1,23 @@ + + + +ვიკიპედია (ka) +UTF-8 +data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAQAAABIkb+zAAAEXElEQVR4Ae3cA4xs2QJG4b9aj41n27Zt27Zt27Zt27Zt+2KM5lXP924qOzs56VPjKSRnrfhv7NUsp08v98+PsxwT4nJ+nPunl8KZ8/WYQL+eMydJrx5/EhN603lAHpVJ5bzZNp035uyZXM7Uy3LmM7ms9CITzVRGQRfQBXQBXUAX0AV0AV1AF9AFdAFdQBfQBXQBXcBMjgU7M5+1vqtZyXqmc/IsZDEL+53LP3KRPCCv62+rWd7vnpx6v/M5Vd935OF5R+6RtRyeQ3NYNjKTxZw2p8tiduR8uWfemY3+dkjWM5fFLPU/8q9yrRwbHLP39WkHamPD5z1WnNMb/dpW/uAdbipu413+qcl2H3N3cXFv9DNNtvmg68uxMY6dPfewTZNvOauouqEdULlTY532IgqHua+ZxnoHm4ANj63biRNQPZedVPY5nTR1BXsVbNuyTlsDqy4tW/wK4NYiJ01AXMlm+wGrL1cDnWbLejC4m7T4DvAOOSkD4v0qztGyLzpUweOk4UXAp6XV72DNmU7qgPPao+DJ0uLTFPzXXGN5I9acU1o8vb14jZzUAfFeBdvMtuwLDlfwIKme2248T1p9MvY59zACLqcy4Kf5eQr+ZbYRfrAFaXHaf/FRGUZAfE/Bzwf8OKwpuJ/0vYRNPEpavT248rACbqfimtLiqxT8w4yIz+Of5qTV7+LHMqyAaf9R8Clp8ex2K7iXuAa4q7R6aXCH4QXE4xVsOq+0+DYFfzXth/ilnrT6Tvzb9DADlqwqDPjTd377FHwI3EBaPb0NPFaGGRBvoLBiUVr8EFS+KgN8KpYtDjvgQo6i8Hhp8ZJUjnIZaXXGNrxKhh0QX6TwHzPS4uco/FtPWr0T9jnPKAJuOOiCc/UqVG4jrf4AH5dRBPT8UWHg3/BvUfhl635ZcNXRBMSDqFxFWvwklVu07O/BT2RUAad0GIWPtexXgMpPt+xntAt3Gl1AvJjCPufasn4d/6NyY2n4DPzXzCgDzmEvhVdIw+tj02X8nsIPGvus7Xi8jDIgPkLhSPON5ad4t7gbletJ9S71n+BIA65C5dGal1g3nENM+weF70j1R3iNjDogfkrhn6br5dU/4UXS94EtF7+vUC8IjjzgrlRuJ33v27j2dTLbKXxd+r4Pn5RRB9RfxsL3y4H/i0dI9TFUrirOZDeuPh4B8TQqVyjH/ZtZqZ7KIRS+JJ6Fn8k4BNRL9IUPmncwbr/1b37lanbiLuMTEG+nsNfb8cOWq0DLFA7E/8yMU8AlNLnaoP/alSfKOAXEN6h8Qlo8k3UKq5bGLeAWFPa6oLT6Ogqvk3ELmPJ3wOtlgOe0B2w6//gFxCPBsjPIQN8JPi3jGDDvSDxVDPaCNnFNOdGczrNzIrEnRyR5XPZlMIdmKofnpTnx6B783QV0ARNOF9AFdAFdQBfQBXQBXUAX0AV0AV3A6OgCVjLJrEzlj5lk/jiVt2eSefvEvzTJxL84zMS/PM//Af1pBRpQAkXBAAAAAElFTkSuQmCC + + + + + + + + + + + + + +https://ka.wikipedia.org/wiki/სპეციალური:ძიება + diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/kk/default.txt b/mobile/ios/Client/Assets/Search/SearchPlugins/kk/default.txt new file mode 100644 index 0000000000..daaaf0e061 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/kk/default.txt @@ -0,0 +1 @@ +Яндекс \ No newline at end of file diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/kk/list.txt b/mobile/ios/Client/Assets/Search/SearchPlugins/kk/list.txt new file mode 100644 index 0000000000..03bd02f058 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/kk/list.txt @@ -0,0 +1,7 @@ +bing +google:hidden +google-nocodes +twitter +wikipedia-kk +yahoo +yandex diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/kk/wikipedia-kk.xml b/mobile/ios/Client/Assets/Search/SearchPlugins/kk/wikipedia-kk.xml new file mode 100644 index 0000000000..2b88c036f2 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/kk/wikipedia-kk.xml @@ -0,0 +1,23 @@ + + + +Уикипедия (kk) +UTF-8 +data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAQAAABIkb+zAAAEXElEQVR4Ae3cA4xs2QJG4b9aj41n27Zt27Zt27Zt27Zt+2KM5lXP924qOzs56VPjKSRnrfhv7NUsp08v98+PsxwT4nJ+nPunl8KZ8/WYQL+eMydJrx5/EhN603lAHpVJ5bzZNp035uyZXM7Uy3LmM7ms9CITzVRGQRfQBXQBXUAX0AV0AV1AF9AFdAFdQBfQBXQBXcBMjgU7M5+1vqtZyXqmc/IsZDEL+53LP3KRPCCv62+rWd7vnpx6v/M5Vd935OF5R+6RtRyeQ3NYNjKTxZw2p8tiduR8uWfemY3+dkjWM5fFLPU/8q9yrRwbHLP39WkHamPD5z1WnNMb/dpW/uAdbipu413+qcl2H3N3cXFv9DNNtvmg68uxMY6dPfewTZNvOauouqEdULlTY532IgqHua+ZxnoHm4ANj63biRNQPZedVPY5nTR1BXsVbNuyTlsDqy4tW/wK4NYiJ01AXMlm+wGrL1cDnWbLejC4m7T4DvAOOSkD4v0qztGyLzpUweOk4UXAp6XV72DNmU7qgPPao+DJ0uLTFPzXXGN5I9acU1o8vb14jZzUAfFeBdvMtuwLDlfwIKme2248T1p9MvY59zACLqcy4Kf5eQr+ZbYRfrAFaXHaf/FRGUZAfE/Bzwf8OKwpuJ/0vYRNPEpavT248rACbqfimtLiqxT8w4yIz+Of5qTV7+LHMqyAaf9R8Clp8ex2K7iXuAa4q7R6aXCH4QXE4xVsOq+0+DYFfzXth/ilnrT6Tvzb9DADlqwqDPjTd377FHwI3EBaPb0NPFaGGRBvoLBiUVr8EFS+KgN8KpYtDjvgQo6i8Hhp8ZJUjnIZaXXGNrxKhh0QX6TwHzPS4uco/FtPWr0T9jnPKAJuOOiCc/UqVG4jrf4AH5dRBPT8UWHg3/BvUfhl635ZcNXRBMSDqFxFWvwklVu07O/BT2RUAad0GIWPtexXgMpPt+xntAt3Gl1AvJjCPufasn4d/6NyY2n4DPzXzCgDzmEvhVdIw+tj02X8nsIPGvus7Xi8jDIgPkLhSPON5ad4t7gbletJ9S71n+BIA65C5dGal1g3nENM+weF70j1R3iNjDogfkrhn6br5dU/4UXS94EtF7+vUC8IjjzgrlRuJ33v27j2dTLbKXxd+r4Pn5RRB9RfxsL3y4H/i0dI9TFUrirOZDeuPh4B8TQqVyjH/ZtZqZ7KIRS+JJ6Fn8k4BNRL9IUPmncwbr/1b37lanbiLuMTEG+nsNfb8cOWq0DLFA7E/8yMU8AlNLnaoP/alSfKOAXEN6h8Qlo8k3UKq5bGLeAWFPa6oLT6Ogqvk3ELmPJ3wOtlgOe0B2w6//gFxCPBsjPIQN8JPi3jGDDvSDxVDPaCNnFNOdGczrNzIrEnRyR5XPZlMIdmKofnpTnx6B783QV0ARNOF9AFdAFdQBfQBXQBXUAX0AV0AV3A6OgCVjLJrEzlj5lk/jiVt2eSefvEvzTJxL84zMS/PM//Af1pBRpQAkXBAAAAAElFTkSuQmCC + + + + + + + + + + + + + +https://kk.wikipedia.org/wiki/Арнайы:Іздеу + diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/kk/yandex.xml b/mobile/ios/Client/Assets/Search/SearchPlugins/kk/yandex.xml new file mode 100644 index 0000000000..0013363ef2 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/kk/yandex.xml @@ -0,0 +1,21 @@ + + + +Яндекс +UTF-8 +data:image/png;base64,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 + + + + + + + + + + + +https://www.yandex.kz/ + diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/kn/amazon-in.xml b/mobile/ios/Client/Assets/Search/SearchPlugins/kn/amazon-in.xml new file mode 100644 index 0000000000..05c5311e31 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/kn/amazon-in.xml @@ -0,0 +1,15 @@ + + + +Amazon.in +utf-8 +data:image/png;base64,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 + + + + + +https://www.amazon.in/ + diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/kn/default.txt b/mobile/ios/Client/Assets/Search/SearchPlugins/kn/default.txt new file mode 100644 index 0000000000..ced95a5cb9 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/kn/default.txt @@ -0,0 +1 @@ +Google \ No newline at end of file diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/kn/list.txt b/mobile/ios/Client/Assets/Search/SearchPlugins/kn/list.txt new file mode 100644 index 0000000000..a9ef4bc0de --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/kn/list.txt @@ -0,0 +1,7 @@ +amazon-in +bing +google +twitter +wikipedia-kn +wiktionary-kn +yahoo-in diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/kn/wikipedia-kn.xml b/mobile/ios/Client/Assets/Search/SearchPlugins/kn/wikipedia-kn.xml new file mode 100644 index 0000000000..fd0ed3ddc1 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/kn/wikipedia-kn.xml @@ -0,0 +1,23 @@ + + + +ವಿಕಿಪೀಡಿಯ (kn) +UTF-8 +data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAQAAABIkb+zAAAEXElEQVR4Ae3cA4xs2QJG4b9aj41n27Zt27Zt27Zt27Zt+2KM5lXP924qOzs56VPjKSRnrfhv7NUsp08v98+PsxwT4nJ+nPunl8KZ8/WYQL+eMydJrx5/EhN603lAHpVJ5bzZNp035uyZXM7Uy3LmM7ms9CITzVRGQRfQBXQBXUAX0AV0AV1AF9AFdAFdQBfQBXQBXcBMjgU7M5+1vqtZyXqmc/IsZDEL+53LP3KRPCCv62+rWd7vnpx6v/M5Vd935OF5R+6RtRyeQ3NYNjKTxZw2p8tiduR8uWfemY3+dkjWM5fFLPU/8q9yrRwbHLP39WkHamPD5z1WnNMb/dpW/uAdbipu413+qcl2H3N3cXFv9DNNtvmg68uxMY6dPfewTZNvOauouqEdULlTY532IgqHua+ZxnoHm4ANj63biRNQPZedVPY5nTR1BXsVbNuyTlsDqy4tW/wK4NYiJ01AXMlm+wGrL1cDnWbLejC4m7T4DvAOOSkD4v0qztGyLzpUweOk4UXAp6XV72DNmU7qgPPao+DJ0uLTFPzXXGN5I9acU1o8vb14jZzUAfFeBdvMtuwLDlfwIKme2248T1p9MvY59zACLqcy4Kf5eQr+ZbYRfrAFaXHaf/FRGUZAfE/Bzwf8OKwpuJ/0vYRNPEpavT248rACbqfimtLiqxT8w4yIz+Of5qTV7+LHMqyAaf9R8Clp8ex2K7iXuAa4q7R6aXCH4QXE4xVsOq+0+DYFfzXth/ilnrT6Tvzb9DADlqwqDPjTd377FHwI3EBaPb0NPFaGGRBvoLBiUVr8EFS+KgN8KpYtDjvgQo6i8Hhp8ZJUjnIZaXXGNrxKhh0QX6TwHzPS4uco/FtPWr0T9jnPKAJuOOiCc/UqVG4jrf4AH5dRBPT8UWHg3/BvUfhl635ZcNXRBMSDqFxFWvwklVu07O/BT2RUAad0GIWPtexXgMpPt+xntAt3Gl1AvJjCPufasn4d/6NyY2n4DPzXzCgDzmEvhVdIw+tj02X8nsIPGvus7Xi8jDIgPkLhSPON5ad4t7gbletJ9S71n+BIA65C5dGal1g3nENM+weF70j1R3iNjDogfkrhn6br5dU/4UXS94EtF7+vUC8IjjzgrlRuJ33v27j2dTLbKXxd+r4Pn5RRB9RfxsL3y4H/i0dI9TFUrirOZDeuPh4B8TQqVyjH/ZtZqZ7KIRS+JJ6Fn8k4BNRL9IUPmncwbr/1b37lanbiLuMTEG+nsNfb8cOWq0DLFA7E/8yMU8AlNLnaoP/alSfKOAXEN6h8Qlo8k3UKq5bGLeAWFPa6oLT6Ogqvk3ELmPJ3wOtlgOe0B2w6//gFxCPBsjPIQN8JPi3jGDDvSDxVDPaCNnFNOdGczrNzIrEnRyR5XPZlMIdmKofnpTnx6B783QV0ARNOF9AFdAFdQBfQBXQBXUAX0AV0AV3A6OgCVjLJrEzlj5lk/jiVt2eSefvEvzTJxL84zMS/PM//Af1pBRpQAkXBAAAAAElFTkSuQmCC + + + + + + + + + + + + + +https://kn.wikipedia.org/wiki/ವಿಶೇಷ:Search + diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/kn/wiktionary-kn.xml b/mobile/ios/Client/Assets/Search/SearchPlugins/kn/wiktionary-kn.xml new file mode 100644 index 0000000000..9b2353b526 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/kn/wiktionary-kn.xml @@ -0,0 +1,18 @@ + + + +ವಿಕ್ಷನರಿ (kn) +data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAYAAADimHc4AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAA2hpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMC1jMDYxIDY0LjE0MDk0OSwgMjAxMC8xMi8wNy0xMDo1NzowMSAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtcE1NOk9yaWdpbmFsRG9jdW1lbnRJRD0ieG1wLmRpZDpGRTdGMTE3NDA3MjA2ODExOTJCMEU1MkZGMDQ2RkQ0QyIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDozNURGRTgzRTMwMzMxMUU1OTlBMUM2MDY0MDI4RjgwMSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDozNURGRTgzRDMwMzMxMUU1OTlBMUM2MDY0MDI4RjgwMSIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ1M1LjEgTWFjaW50b3NoIj4gPHhtcE1NOkRlcml2ZWRGcm9tIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6MDA4MDExNzQwNzIwNjgxMTkyQjBFNTJGRjA0NkZENEMiIHN0UmVmOmRvY3VtZW50SUQ9InhtcC5kaWQ6RkU3RjExNzQwNzIwNjgxMTkyQjBFNTJGRjA0NkZENEMiLz4gPC9yZGY6RGVzY3Jp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 + + + + + + + + + +https://kn.wiktionary.org/wiki/ವಿಶೇಷ:Search + diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/kn/yahoo-in.xml b/mobile/ios/Client/Assets/Search/SearchPlugins/kn/yahoo-in.xml new file mode 100644 index 0000000000..67e95519df --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/kn/yahoo-in.xml @@ -0,0 +1,22 @@ + + + +Yahoo +UTF-8 +data:image/png;base64,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 + + + + + + + + + + + + +https://in.search.yahoo.com/ + diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/ko/danawa-kr.xml b/mobile/ios/Client/Assets/Search/SearchPlugins/ko/danawa-kr.xml new file mode 100644 index 0000000000..6be182a830 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/ko/danawa-kr.xml @@ -0,0 +1,15 @@ + + + +다나와 +다나와 쇼핑 검색 +EUC-KR +data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAYAAAGVn0euAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyJpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMy1jMDExIDY2LjE0NTY2MSwgMjAxMi8wMi8wNi0xNDo1NjoyNyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNiAoV2luZG93cykiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6QkM3MTNGRUVFRkU5MTFFNTlDMURCODFDN0ZGRUYzQ0UiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6QkM3MTNGRUZFRkU5MTFFNTlDMURCODFDN0ZGRUYzQ0UiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDpCQzcxM0ZFQ0VGRTkxMUU1OUMxREI4MUM3RkZFRjNDRSIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDpCQzcxM0ZFREVGRTkxMUU1OUMxREI4MUM3RkZFRjNDRSIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PhSI93wAACAdSURBVHjajFE7TsNA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 + + + + +http://m.danawa.com + diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/ko/daum-kr.xml b/mobile/ios/Client/Assets/Search/SearchPlugins/ko/daum-kr.xml new file mode 100644 index 0000000000..f257c6b0fc --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/ko/daum-kr.xml @@ -0,0 +1,20 @@ + + + +다음지도 +utf-8 +data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAYAAADimHc4AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAA2hpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMC1jMDYxIDY0LjE0MDk0OSwgMjAxMC8xMi8wNy0xMDo1NzowMSAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtcE1NOk9yaWdpbmFsRG9jdW1lbnRJRD0ieG1wLmRpZDowMTgwMTE3NDA3MjA2ODExOEE2REI2ODhGMkQ2RTdCRSIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDoyQzA1RkZCOUUyMDYxMUU1QjEwMEEwMkZCOUU1N0Y5MCIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDoyQzA1RkZCOEUyMDYxMUU1QjEwMEEwMkZCOUU1N0Y5MCIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ1M1LjEgTWFjaW50b3NoIj4gPHhtcE1NOkRlcml2ZWRGcm9tIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6MDg4MDExNzQwNzIwNjgxMThBNkRDRjQ4QkY4NDdGOTUiIHN0UmVmOmRvY3VtZW50SUQ9InhtcC5kaWQ6MDE4MDExNzQwNzIwNjgxMThBNkRCNjg4RjJENkU3QkUiLz4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+IDw/eHBhY2tldCBlbmQ9InIiPz6jtHwKAAACFElEQVR42uydsUrDUBhGbxrTdndw0EVQcalV6CK4OgiOBd+gs88gPoVvUHB0EhdBcKkgFB20aysW3FzaksT/4o0oRqx4mxvac+ADe2tD+50kf6Gk9QaDgTIUJQ1JXbJtboM9hpJryankxNxWnhGwKDmTbNJTJtxK9iVdLUDv6S1JhV4ypS2pFcxph/KzR3fe0AIO6MIZB/oUNGDguhvMWkBMD+4oUAECEAAIQAAgAAGAAAQAAhAACEAAIAABgAAEwGSZqx77uX6Co5GvgiBUW9V1VS53/r29o76d1+td1GfrCLBRPqcgQEDuZsBPd4SxUrHE+7Tmj6krjNLXf3t82uP0WjCLAnzdvPd1LRhzfgUW57pfCt+HXrSi4kJn+gXoPU7vqavz+Xp3tFuxU/7S8pqV7UT3C3/6/6feMzOAIQwIQAAgAAGAAAQAAhAACEAAAgABCAAEIAAQgABAAAIAAQgABCAAEIAAQAACAAEIAAQgABCAALDNt2vEkisZH1/CXD3R8zs729npP1jZjtfbmIyAhLTLVF2gv6qgXApn5wj4OBJSLlN1QTTlJ0lmAAIQAA7hFzQ4AhAACEAAIAABgAAEAAIQAAhAACAAAYAABAACpl/AkBqc8aoF3NCDM9paQJMenNHUH8oX5Y+WpEIf2e79kloyA/bMAmRXvu58mLwL6mobkkPJJf1MjCvTcc10rt4EGABiRGNcL0JtwgAAAABJRU5ErkJggg== + + + + + + + + + + +http://m.map.daum.net + diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/ko/default.txt b/mobile/ios/Client/Assets/Search/SearchPlugins/ko/default.txt new file mode 100644 index 0000000000..ced95a5cb9 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/ko/default.txt @@ -0,0 +1 @@ +Google \ No newline at end of file diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/ko/list.txt b/mobile/ios/Client/Assets/Search/SearchPlugins/ko/list.txt new file mode 100644 index 0000000000..a77260fab0 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/ko/list.txt @@ -0,0 +1,5 @@ +google +danawa-kr +twitter +daum-kr +naver-kr diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/ko/naver-kr.xml b/mobile/ios/Client/Assets/Search/SearchPlugins/ko/naver-kr.xml new file mode 100644 index 0000000000..db1bd2c0ca --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/ko/naver-kr.xml @@ -0,0 +1,21 @@ + + + +네이버 +data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAYAAADimHc4AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAA2hpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMC1jMDYxIDY0LjE0MDk0OSwgMjAxMC8xMi8wNy0xMDo1NzowMSAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtcE1NOk9yaWdpbmFsRG9jdW1lbnRJRD0ieG1wLmRpZDowMTgwMTE3NDA3MjA2ODExOEE2REI2ODhGMkQ2RTdCRSIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDo1MjY4MTNGNEUyMDMxMUU1QjEwMEEwMkZCOUU1N0Y5MCIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDo1MjY4MTNGM0UyMDMxMUU1QjEwMEEwMkZCOUU1N0Y5MCIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ1M1LjEgTWFjaW50b3NoIj4gPHhtcE1NOkRlcml2ZWRGcm9tIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6MDI4MDExNzQwNzIwNjgxMThBNkRDRjQ4QkY4NDdGOTUiIHN0UmVmOmRvY3VtZW50SUQ9InhtcC5kaWQ6MDE4MDExNzQwNzIwNjgxMThBNkRCNjg4RjJENkU3QkUiLz4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+IDw/eHBhY2tldCBlbmQ9InIiPz6ugdXHAAAFXElEQVR42uydbWhbVRjH/zdJ0ySte4u0o23a1fSFlA0G1uLY2BcVX3CIOjdwuCHKUD/o1FIdWlndxGmZE0Gc+2AFnbAXfGHOgcJQcZujiKK027q2Wbe+zNZWW9smuc2Lz7m9xS+Czb25Obvp829O2ny4J+c8v3P+5+Tk3FMFZ9ZCl5vSdkobKa3RX7MyJ5XSWUrHKB3UX0PRAZRS+pLSao5TVvQLpXspDTj0ln6Sg59VrdZj7nbotrOKY5J1iZhvFwA2cyykabMA0MBxkKYGB892pEobA1gSxQAYAANgMQAGwGIADIDFABgAiwEwABYDYAAsBsAAWFmQy9rsU/RImctCccgtg6KIJ7sBUGYDF/udKq+aCADl4/QA7iLKI57+tSL4sWH6lZj920jwlXwgv0jPwy4ARGHjE3ixcgduKQwhmlQNhF6BS3GidfATtI+cBHxV6UFIRslg3dhf+xrKCKCamkm7DB66/txkB97sO0ANoTADvfG/6nlmbSrzAKiy6hgu3noKNZ6AqaxG4+MoOrseyWRMb4nJ+V0Yn6TmVYDUmtOm3r8jEsbKH2+bfW8l8+3VYZ0FOdEXu2Y6J79rMT4I7SOgf1CrTqMV6601HBsy9f5XhI1qgbdmHLDFLGjbjbfjzsCjwNQlDSxPQyXoSHUL8n1BIDpoiRczgP/RIqcPH9eRFdGYAG1QVxhAtrVx6To8UPGkbkUOBiBDh4IvobCwjqyoPyfGA9sBEHPzo3X7yYqmgUTE9lZky3581+J6bK18Bpi2vxXZtvRtwRfgJxCI9Nnaihz2LbiCT0NkRck4WdGUba3I1v13/Q0r8VSwiWZFPfqqJQPIut5d8TRKl62j8SBsSyvKicn0F6G3Zgfj+ITtrCgnANxcUI2mqmYakMO2GwpyZlHljcDjCPrvoPGg15JlYwYwDx0XVuT0AjNjtrGinAIQ8gbwak0LWdFVGPoKkgGYV3PJFqwq3kBW1G2LWVFObks5HmoF8pYA6igc1/lSRU4CqHAX4+XaPUBsAAmLdjMsCACXolcxpA2o6Wt36VY0LN+EzunLDMCoeqKDuP/CTsPXf1b7uuldGQsaQJ13Bc5dO4qd/W2Gri9x+1HjZQCG5RTF85Rhb1czwhnY4sIA0lSSfhx5fm2NZ8P5RgYgRWI7oq8KHcMnsGvgIwYgTd5ytHTtQof4BowBZL0b0AerpeRJMbKi5xiANCsquAnh0VNovHKQAciBQD2Bpqb7uvegfeoiA5BiRa5F2krzfZ3PMwA5DBLUCyox9OdpPHH5bQYgB0KSxoMg3u9pxbd//8oApFiRs0C7BenBzmeRmO9dMwwg01ZUjrGJn7Gtdy8DkGZF9Cn5UPgdfDXezgDkWJFXuxlvE1lRxMDdmAwgE1bkKcPUZCce7tnNAOTNimrwed8BHBn7ngFIsSKaEcG1BI+cb8S4tluaAWS/F3hKoEZ68VDXKwxA2njgq8Y3/R+ibeTrhQwgpQWjIn+5qVwC+cX/tu551yhPO9zjsQuNGJn5y3RNykUZtDMqrNlpZ9EuVic9fGgbPmH4sA634sKQOjpbbUca/+JAwKKgpaa7cQ9BaCrZgjg1hpSBAM4d1gFXoWVt1ZrDOjJyXI2eh9aLlPRboNghrQ4Diajx1mvb42p0C4Lbn4HDkmAsgMI28pZRDU2uE4kyWLi7zuKN9Ir8e7d4byiLATAAFgNgACwGwABYDIABsBgAA2AxAAbAYgAMgMUArgMAKodBmiYFgJ84DtL0mwBwmOMgTYcFgPcECY5F9lu/iP3cGHA3Q8h68EXM1blZ0ACleko7KH3H8bFMP+gxrtdjjn8EGABZZoZg4pPHIQAAAABJRU5ErkJggg== + + + + + + + + + + + + +http://m.naver.com + diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/lo/default.txt b/mobile/ios/Client/Assets/Search/SearchPlugins/lo/default.txt new file mode 100644 index 0000000000..ced95a5cb9 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/lo/default.txt @@ -0,0 +1 @@ +Google \ No newline at end of file diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/lo/list.txt b/mobile/ios/Client/Assets/Search/SearchPlugins/lo/list.txt new file mode 100644 index 0000000000..211f4dc3dc --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/lo/list.txt @@ -0,0 +1,5 @@ +bing +google +twitter +wikipedia-lo +yahoo diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/lo/wikipedia-lo.xml b/mobile/ios/Client/Assets/Search/SearchPlugins/lo/wikipedia-lo.xml new file mode 100644 index 0000000000..ff637f53e4 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/lo/wikipedia-lo.xml @@ -0,0 +1,23 @@ + + + +ວິກິພີເດຍ (lo) +UTF-8 +data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAQAAABIkb+zAAAEXElEQVR4Ae3cA4xs2QJG4b9aj41n27Zt27Zt27Zt27Zt+2KM5lXP924qOzs56VPjKSRnrfhv7NUsp08v98+PsxwT4nJ+nPunl8KZ8/WYQL+eMydJrx5/EhN603lAHpVJ5bzZNp035uyZXM7Uy3LmM7ms9CITzVRGQRfQBXQBXUAX0AV0AV1AF9AFdAFdQBfQBXQBXcBMjgU7M5+1vqtZyXqmc/IsZDEL+53LP3KRPCCv62+rWd7vnpx6v/M5Vd935OF5R+6RtRyeQ3NYNjKTxZw2p8tiduR8uWfemY3+dkjWM5fFLPU/8q9yrRwbHLP39WkHamPD5z1WnNMb/dpW/uAdbipu413+qcl2H3N3cXFv9DNNtvmg68uxMY6dPfewTZNvOauouqEdULlTY532IgqHua+ZxnoHm4ANj63biRNQPZedVPY5nTR1BXsVbNuyTlsDqy4tW/wK4NYiJ01AXMlm+wGrL1cDnWbLejC4m7T4DvAOOSkD4v0qztGyLzpUweOk4UXAp6XV72DNmU7qgPPao+DJ0uLTFPzXXGN5I9acU1o8vb14jZzUAfFeBdvMtuwLDlfwIKme2248T1p9MvY59zACLqcy4Kf5eQr+ZbYRfrAFaXHaf/FRGUZAfE/Bzwf8OKwpuJ/0vYRNPEpavT248rACbqfimtLiqxT8w4yIz+Of5qTV7+LHMqyAaf9R8Clp8ex2K7iXuAa4q7R6aXCH4QXE4xVsOq+0+DYFfzXth/ilnrT6Tvzb9DADlqwqDPjTd377FHwI3EBaPb0NPFaGGRBvoLBiUVr8EFS+KgN8KpYtDjvgQo6i8Hhp8ZJUjnIZaXXGNrxKhh0QX6TwHzPS4uco/FtPWr0T9jnPKAJuOOiCc/UqVG4jrf4AH5dRBPT8UWHg3/BvUfhl635ZcNXRBMSDqFxFWvwklVu07O/BT2RUAad0GIWPtexXgMpPt+xntAt3Gl1AvJjCPufasn4d/6NyY2n4DPzXzCgDzmEvhVdIw+tj02X8nsIPGvus7Xi8jDIgPkLhSPON5ad4t7gbletJ9S71n+BIA65C5dGal1g3nENM+weF70j1R3iNjDogfkrhn6br5dU/4UXS94EtF7+vUC8IjjzgrlRuJ33v27j2dTLbKXxd+r4Pn5RRB9RfxsL3y4H/i0dI9TFUrirOZDeuPh4B8TQqVyjH/ZtZqZ7KIRS+JJ6Fn8k4BNRL9IUPmncwbr/1b37lanbiLuMTEG+nsNfb8cOWq0DLFA7E/8yMU8AlNLnaoP/alSfKOAXEN6h8Qlo8k3UKq5bGLeAWFPa6oLT6Ogqvk3ELmPJ3wOtlgOe0B2w6//gFxCPBsjPIQN8JPi3jGDDvSDxVDPaCNnFNOdGczrNzIrEnRyR5XPZlMIdmKofnpTnx6B783QV0ARNOF9AFdAFdQBfQBXQBXUAX0AV0AV3A6OgCVjLJrEzlj5lk/jiVt2eSefvEvzTJxL84zMS/PM//Af1pBRpQAkXBAAAAAElFTkSuQmCC + + + + + + + + + + + + + +https://lo.wikipedia.org/wiki/ພິເສດ:ຊອກຫາ + diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/lt/default.txt b/mobile/ios/Client/Assets/Search/SearchPlugins/lt/default.txt new file mode 100644 index 0000000000..ced95a5cb9 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/lt/default.txt @@ -0,0 +1 @@ +Google \ No newline at end of file diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/lt/list.txt b/mobile/ios/Client/Assets/Search/SearchPlugins/lt/list.txt new file mode 100644 index 0000000000..2ff63a596c --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/lt/list.txt @@ -0,0 +1,3 @@ +google +twitter +wikipedia-lt diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/lt/wikipedia-lt.xml b/mobile/ios/Client/Assets/Search/SearchPlugins/lt/wikipedia-lt.xml new file mode 100644 index 0000000000..930947eaae --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/lt/wikipedia-lt.xml @@ -0,0 +1,23 @@ + + + +Wikipedia (lt) +UTF-8 +data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAQAAABIkb+zAAAEXElEQVR4Ae3cA4xs2QJG4b9aj41n27Zt27Zt27Zt27Zt+2KM5lXP924qOzs56VPjKSRnrfhv7NUsp08v98+PsxwT4nJ+nPunl8KZ8/WYQL+eMydJrx5/EhN603lAHpVJ5bzZNp035uyZXM7Uy3LmM7ms9CITzVRGQRfQBXQBXUAX0AV0AV1AF9AFdAFdQBfQBXQBXcBMjgU7M5+1vqtZyXqmc/IsZDEL+53LP3KRPCCv62+rWd7vnpx6v/M5Vd935OF5R+6RtRyeQ3NYNjKTxZw2p8tiduR8uWfemY3+dkjWM5fFLPU/8q9yrRwbHLP39WkHamPD5z1WnNMb/dpW/uAdbipu413+qcl2H3N3cXFv9DNNtvmg68uxMY6dPfewTZNvOauouqEdULlTY532IgqHua+ZxnoHm4ANj63biRNQPZedVPY5nTR1BXsVbNuyTlsDqy4tW/wK4NYiJ01AXMlm+wGrL1cDnWbLejC4m7T4DvAOOSkD4v0qztGyLzpUweOk4UXAp6XV72DNmU7qgPPao+DJ0uLTFPzXXGN5I9acU1o8vb14jZzUAfFeBdvMtuwLDlfwIKme2248T1p9MvY59zACLqcy4Kf5eQr+ZbYRfrAFaXHaf/FRGUZAfE/Bzwf8OKwpuJ/0vYRNPEpavT248rACbqfimtLiqxT8w4yIz+Of5qTV7+LHMqyAaf9R8Clp8ex2K7iXuAa4q7R6aXCH4QXE4xVsOq+0+DYFfzXth/ilnrT6Tvzb9DADlqwqDPjTd377FHwI3EBaPb0NPFaGGRBvoLBiUVr8EFS+KgN8KpYtDjvgQo6i8Hhp8ZJUjnIZaXXGNrxKhh0QX6TwHzPS4uco/FtPWr0T9jnPKAJuOOiCc/UqVG4jrf4AH5dRBPT8UWHg3/BvUfhl635ZcNXRBMSDqFxFWvwklVu07O/BT2RUAad0GIWPtexXgMpPt+xntAt3Gl1AvJjCPufasn4d/6NyY2n4DPzXzCgDzmEvhVdIw+tj02X8nsIPGvus7Xi8jDIgPkLhSPON5ad4t7gbletJ9S71n+BIA65C5dGal1g3nENM+weF70j1R3iNjDogfkrhn6br5dU/4UXS94EtF7+vUC8IjjzgrlRuJ33v27j2dTLbKXxd+r4Pn5RRB9RfxsL3y4H/i0dI9TFUrirOZDeuPh4B8TQqVyjH/ZtZqZ7KIRS+JJ6Fn8k4BNRL9IUPmncwbr/1b37lanbiLuMTEG+nsNfb8cOWq0DLFA7E/8yMU8AlNLnaoP/alSfKOAXEN6h8Qlo8k3UKq5bGLeAWFPa6oLT6Ogqvk3ELmPJ3wOtlgOe0B2w6//gFxCPBsjPIQN8JPi3jGDDvSDxVDPaCNnFNOdGczrNzIrEnRyR5XPZlMIdmKofnpTnx6B783QV0ARNOF9AFdAFdQBfQBXQBXUAX0AV0AV3A6OgCVjLJrEzlj5lk/jiVt2eSefvEvzTJxL84zMS/PM//Af1pBRpQAkXBAAAAAElFTkSuQmCC + + + + + + + + + + + + + +https://lt.wikipedia.org/wiki/Specialus:Paieška + diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/lv/default.txt b/mobile/ios/Client/Assets/Search/SearchPlugins/lv/default.txt new file mode 100644 index 0000000000..ced95a5cb9 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/lv/default.txt @@ -0,0 +1 @@ +Google \ No newline at end of file diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/lv/list.txt b/mobile/ios/Client/Assets/Search/SearchPlugins/lv/list.txt new file mode 100644 index 0000000000..c26a6314a1 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/lv/list.txt @@ -0,0 +1,5 @@ +google +salidzinilv +sslv +twitter +wikipedia-lv diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/lv/salidzinilv.xml b/mobile/ios/Client/Assets/Search/SearchPlugins/lv/salidzinilv.xml new file mode 100644 index 0000000000..7558f54416 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/lv/salidzinilv.xml @@ -0,0 +1,18 @@ + + + +Salidzini.lv +UTF-8 +data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAYAAADimHc4AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAA2hpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMC1jMDYxIDY0LjE0MDk0OSwgMjAxMC8xMi8wNy0xMDo1NzowMSAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtcE1NOk9yaWdpbmFsRG9jdW1lbnRJRD0ieG1wLmRpZDowMTgwMTE3NDA3MjA2ODExOEE2REI2ODhGMkQ2RTdCRSIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDo2ODNBMDBFNkUyMjQxMUU1QjE0QUUxNkJFNEIwNTQwQiIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDo2ODNBMDBFNUUyMjQxMUU1QjE0QUUxNkJFNEIwNTQwQiIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ1M1LjEgTWFjaW50b3NoIj4gPHhtcE1NOkRlcml2ZWRGcm9tIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6MDg4MDExNzQwNzIwNjgxMThBNkRDRjQ4QkY4NDdGOTUiIHN0UmVmOmRvY3VtZW50SUQ9InhtcC5kaWQ6MDE4MDExNzQwNzIwNjgxMThBNkRCNjg4RjJENkU3QkUiLz4gPC9yZGY6RGVzY3Jp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 + + + + + + + + +http://salidzini.lv + diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/lv/sslv.xml b/mobile/ios/Client/Assets/Search/SearchPlugins/lv/sslv.xml new file mode 100644 index 0000000000..2108963235 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/lv/sslv.xml @@ -0,0 +1,14 @@ + + + +SS.lv +UTF-8 +data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAYAAADimHc4AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAA2hpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMC1jMDYxIDY0LjE0MDk0OSwgMjAxMC8xMi8wNy0xMDo1NzowMSAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtcE1NOk9yaWdpbmFsRG9jdW1lbnRJRD0ieG1wLmRpZDowMTgwMTE3NDA3MjA2ODExOEE2REI2ODhGMkQ2RTdCRSIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDo2ODNBMDBFQUUyMjQxMUU1QjE0QUUxNkJFNEIwNTQwQiIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDo2ODNBMDBFOUUyMjQxMUU1QjE0QUUxNkJFNEIwNTQwQiIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ1M1LjEgTWFjaW50b3NoIj4gPHhtcE1NOkRlcml2ZWRGcm9tIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6Rjk3RjExNzQwNzIwNjgxMUI2MEJEOTMwMkE4QTkwQjMiIHN0UmVmOmRvY3VtZW50SUQ9InhtcC5kaWQ6MDE4MDExNzQwNzIwNjgxMThBNkRCNjg4RjJENkU3QkUiLz4gPC9yZGY6RGVzY3Jp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 + + + + +https://www.ss.lv + diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/lv/wikipedia-lv.xml b/mobile/ios/Client/Assets/Search/SearchPlugins/lv/wikipedia-lv.xml new file mode 100644 index 0000000000..b0d82664cb --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/lv/wikipedia-lv.xml @@ -0,0 +1,23 @@ + + + +Vikipēdija (lv) +UTF-8 +data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAQAAABIkb+zAAAEXElEQVR4Ae3cA4xs2QJG4b9aj41n27Zt27Zt27Zt27Zt+2KM5lXP924qOzs56VPjKSRnrfhv7NUsp08v98+PsxwT4nJ+nPunl8KZ8/WYQL+eMydJrx5/EhN603lAHpVJ5bzZNp035uyZXM7Uy3LmM7ms9CITzVRGQRfQBXQBXUAX0AV0AV1AF9AFdAFdQBfQBXQBXcBMjgU7M5+1vqtZyXqmc/IsZDEL+53LP3KRPCCv62+rWd7vnpx6v/M5Vd935OF5R+6RtRyeQ3NYNjKTxZw2p8tiduR8uWfemY3+dkjWM5fFLPU/8q9yrRwbHLP39WkHamPD5z1WnNMb/dpW/uAdbipu413+qcl2H3N3cXFv9DNNtvmg68uxMY6dPfewTZNvOauouqEdULlTY532IgqHua+ZxnoHm4ANj63biRNQPZedVPY5nTR1BXsVbNuyTlsDqy4tW/wK4NYiJ01AXMlm+wGrL1cDnWbLejC4m7T4DvAOOSkD4v0qztGyLzpUweOk4UXAp6XV72DNmU7qgPPao+DJ0uLTFPzXXGN5I9acU1o8vb14jZzUAfFeBdvMtuwLDlfwIKme2248T1p9MvY59zACLqcy4Kf5eQr+ZbYRfrAFaXHaf/FRGUZAfE/Bzwf8OKwpuJ/0vYRNPEpavT248rACbqfimtLiqxT8w4yIz+Of5qTV7+LHMqyAaf9R8Clp8ex2K7iXuAa4q7R6aXCH4QXE4xVsOq+0+DYFfzXth/ilnrT6Tvzb9DADlqwqDPjTd377FHwI3EBaPb0NPFaGGRBvoLBiUVr8EFS+KgN8KpYtDjvgQo6i8Hhp8ZJUjnIZaXXGNrxKhh0QX6TwHzPS4uco/FtPWr0T9jnPKAJuOOiCc/UqVG4jrf4AH5dRBPT8UWHg3/BvUfhl635ZcNXRBMSDqFxFWvwklVu07O/BT2RUAad0GIWPtexXgMpPt+xntAt3Gl1AvJjCPufasn4d/6NyY2n4DPzXzCgDzmEvhVdIw+tj02X8nsIPGvus7Xi8jDIgPkLhSPON5ad4t7gbletJ9S71n+BIA65C5dGal1g3nENM+weF70j1R3iNjDogfkrhn6br5dU/4UXS94EtF7+vUC8IjjzgrlRuJ33v27j2dTLbKXxd+r4Pn5RRB9RfxsL3y4H/i0dI9TFUrirOZDeuPh4B8TQqVyjH/ZtZqZ7KIRS+JJ6Fn8k4BNRL9IUPmncwbr/1b37lanbiLuMTEG+nsNfb8cOWq0DLFA7E/8yMU8AlNLnaoP/alSfKOAXEN6h8Qlo8k3UKq5bGLeAWFPa6oLT6Ogqvk3ELmPJ3wOtlgOe0B2w6//gFxCPBsjPIQN8JPi3jGDDvSDxVDPaCNnFNOdGczrNzIrEnRyR5XPZlMIdmKofnpTnx6B783QV0ARNOF9AFdAFdQBfQBXQBXUAX0AV0AV3A6OgCVjLJrEzlj5lk/jiVt2eSefvEvzTJxL84zMS/PM//Af1pBRpQAkXBAAAAAElFTkSuQmCC + + + + + + + + + + + + + +https://lv.wikipedia.org/wiki/Special:Search + diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/ml/default.txt b/mobile/ios/Client/Assets/Search/SearchPlugins/ml/default.txt new file mode 100644 index 0000000000..ced95a5cb9 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/ml/default.txt @@ -0,0 +1 @@ +Google \ No newline at end of file diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/ml/eBay-in.xml b/mobile/ios/Client/Assets/Search/SearchPlugins/ml/eBay-in.xml new file mode 100644 index 0000000000..8d27d340f9 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/ml/eBay-in.xml @@ -0,0 +1,17 @@ + + + +eBay India +UTF-8 +data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAAEEfUpiAAAEWklEQVRYw8WXfUxVdRjHP79z7guXBBTEEBXEraktCydjWi7RZW82lr340kZNXaX+41p/tF42Xakw3ZqNyeZMnZVYtjnLNLXMzJFLA1FrgiIioveCIvcyLvdyzz3n6Q+7FxIM5YJ8/jovv/2e7/Oc5+X8FICIbCKCiCyTLmgArcUbmb2ugirP3u4rlIjkA3P+3aFIiYjQBa21eCMARv2XFB5Mo9sKRORZ+R80IMv0NAJQ7Q4CYDYdJmwGehZ+O0pEXEA7d0B1i0UnbUqpd7urvg2NXtAAQhWnmF5Ug9tnQDiIZYUBCBo+NPf0WQTLfqc1aDEyyU77kWyCB9Kpa/6NOHsS3Mm9tQcejLoZk0gzEgehb2zp9VP0Ro8S522qv2sJGkDLR6uwmpvBsvA8n8+3f/oA+Oakly3HbhI6swKA9u9dFB5MA6DKs5drvtO3Noh/4Tm8a9fRunUbzkmTGJGgs3ZfIx/sbiT/YQPTdxbjUgnKkcL7z3goPJjG0PhM0pMeo19iYBIrIvJaH4x3iIgeyYNUYAWQepc2jyqlSiPWX+mr/wAqhiwEWKPFGL6sWDfoXm1Ltjf0bYNQ+SnEMPB4b7WLX6vbOmu2uQyrrZqg4Ys+izRGDaBxzlwcUybjXbkaEYuSIzfIGz8E9eZZQudWoidPQ+mJ/F25nNrrRwBY/3MWADYAfXgyrcUb0UeOQNpDLJ85HIAnH4rHrP8CQ9nQEibyiH0In1Ys5KXsLSyadqhL1ztfgy1rHP5du8HpJOu9Kjb8dJ3aGyHE9KMSJhA6XwhiMT/na3ZXLiEt8dFoGt8TJy5vFrfvTOR2R6yJVNr/eXCPhJSINN1DFd7O5EggvX0oxg97aipeGXjCIrJaRByRduACGoBk7j85GlAwSMYBVmiAzuChawwydyVgzmd1TC+qGRABtsiF5fXSsmoNofIKbBPGI01NWIEAQws/AZJpCVjMWHeRKy0GLodGlbuDd55KYf2r6VjXD9Nxcj44UtDiM7C8FcgDmWxTJgnOUbwxdX+XarPYXJZHYtwoFuTsRInIMqDEnfc09swxOPPyOgdBXS3tPxxg8YubONfmoK5oAkp1qs/++AJpiXZ+XJ5IuGEXVls1yriBWCFMz3600QvZ7j+Hy5bI61P3IWLxedlMklzpzJuyE6A0GgHb2AyU3UHCooKogY7ySvSx49B8Q7jW4Oe8J8j4kXEA1N80uNDYweKJlQT2L8D5+CHs45be8jLgJnBtD0op3nriKFuPz2bHyZcxwn4SnGkR40T6wDKgBMC8epXgsTLCl6+AaeLIzcE1K499f/nRFIxJtvPVcS++YJjMFAdvz0hhWLyO+C8Sqi1Ggo3orrHoGQVI4DKixWFLnYlpGWz4ZSKjh+Uyf0rpf6ZBzD93vWFZYfnij3zZc3ppT6932Aa6zJTSKcj9bsDGWb/0gUuDaL8mMgnnyv2nMHpc7TKSU4FcIBvI6GdvbwLlwAmlVPQU9g9eOPBg6mUX4QAAAABJRU5ErkJggg== + + + + + + + +http://search.ebay.in/ + diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/ml/list.txt b/mobile/ios/Client/Assets/Search/SearchPlugins/ml/list.txt new file mode 100644 index 0000000000..9d7b7a0d25 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/ml/list.txt @@ -0,0 +1,7 @@ +bing +duckduckgo +eBay-in +google +twitter +wikipedia-ml +yahoo-in diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/ml/wikipedia-ml.xml b/mobile/ios/Client/Assets/Search/SearchPlugins/ml/wikipedia-ml.xml new file mode 100644 index 0000000000..746f5e0864 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/ml/wikipedia-ml.xml @@ -0,0 +1,23 @@ + + + +വിക്കിപീഡിയ (ml) +UTF-8 +data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAQAAABIkb+zAAAEXElEQVR4Ae3cA4xs2QJG4b9aj41n27Zt27Zt27Zt27Zt+2KM5lXP924qOzs56VPjKSRnrfhv7NUsp08v98+PsxwT4nJ+nPunl8KZ8/WYQL+eMydJrx5/EhN603lAHpVJ5bzZNp035uyZXM7Uy3LmM7ms9CITzVRGQRfQBXQBXUAX0AV0AV1AF9AFdAFdQBfQBXQBXcBMjgU7M5+1vqtZyXqmc/IsZDEL+53LP3KRPCCv62+rWd7vnpx6v/M5Vd935OF5R+6RtRyeQ3NYNjKTxZw2p8tiduR8uWfemY3+dkjWM5fFLPU/8q9yrRwbHLP39WkHamPD5z1WnNMb/dpW/uAdbipu413+qcl2H3N3cXFv9DNNtvmg68uxMY6dPfewTZNvOauouqEdULlTY532IgqHua+ZxnoHm4ANj63biRNQPZedVPY5nTR1BXsVbNuyTlsDqy4tW/wK4NYiJ01AXMlm+wGrL1cDnWbLejC4m7T4DvAOOSkD4v0qztGyLzpUweOk4UXAp6XV72DNmU7qgPPao+DJ0uLTFPzXXGN5I9acU1o8vb14jZzUAfFeBdvMtuwLDlfwIKme2248T1p9MvY59zACLqcy4Kf5eQr+ZbYRfrAFaXHaf/FRGUZAfE/Bzwf8OKwpuJ/0vYRNPEpavT248rACbqfimtLiqxT8w4yIz+Of5qTV7+LHMqyAaf9R8Clp8ex2K7iXuAa4q7R6aXCH4QXE4xVsOq+0+DYFfzXth/ilnrT6Tvzb9DADlqwqDPjTd377FHwI3EBaPb0NPFaGGRBvoLBiUVr8EFS+KgN8KpYtDjvgQo6i8Hhp8ZJUjnIZaXXGNrxKhh0QX6TwHzPS4uco/FtPWr0T9jnPKAJuOOiCc/UqVG4jrf4AH5dRBPT8UWHg3/BvUfhl635ZcNXRBMSDqFxFWvwklVu07O/BT2RUAad0GIWPtexXgMpPt+xntAt3Gl1AvJjCPufasn4d/6NyY2n4DPzXzCgDzmEvhVdIw+tj02X8nsIPGvus7Xi8jDIgPkLhSPON5ad4t7gbletJ9S71n+BIA65C5dGal1g3nENM+weF70j1R3iNjDogfkrhn6br5dU/4UXS94EtF7+vUC8IjjzgrlRuJ33v27j2dTLbKXxd+r4Pn5RRB9RfxsL3y4H/i0dI9TFUrirOZDeuPh4B8TQqVyjH/ZtZqZ7KIRS+JJ6Fn8k4BNRL9IUPmncwbr/1b37lanbiLuMTEG+nsNfb8cOWq0DLFA7E/8yMU8AlNLnaoP/alSfKOAXEN6h8Qlo8k3UKq5bGLeAWFPa6oLT6Ogqvk3ELmPJ3wOtlgOe0B2w6//gFxCPBsjPIQN8JPi3jGDDvSDxVDPaCNnFNOdGczrNzIrEnRyR5XPZlMIdmKofnpTnx6B783QV0ARNOF9AFdAFdQBfQBXQBXUAX0AV0AV3A6OgCVjLJrEzlj5lk/jiVt2eSefvEvzTJxL84zMS/PM//Af1pBRpQAkXBAAAAAElFTkSuQmCC + + + + + + + + + + + + + +https://ml.wikipedia.org/wiki/പ്രത്യേകം:അന്വേഷണം + diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/ml/yahoo-in.xml b/mobile/ios/Client/Assets/Search/SearchPlugins/ml/yahoo-in.xml new file mode 100644 index 0000000000..67e95519df --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/ml/yahoo-in.xml @@ -0,0 +1,22 @@ + + + +Yahoo +UTF-8 +data:image/png;base64,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 + + + + + + + + + + + + +https://in.search.yahoo.com/ + diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/mr/amazon-in.xml b/mobile/ios/Client/Assets/Search/SearchPlugins/mr/amazon-in.xml new file mode 100644 index 0000000000..05c5311e31 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/mr/amazon-in.xml @@ -0,0 +1,15 @@ + + + +Amazon.in +utf-8 +data:image/png;base64,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 + + + + + +https://www.amazon.in/ + diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/mr/default.txt b/mobile/ios/Client/Assets/Search/SearchPlugins/mr/default.txt new file mode 100644 index 0000000000..ced95a5cb9 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/mr/default.txt @@ -0,0 +1 @@ +Google \ No newline at end of file diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/mr/eBay-in.xml b/mobile/ios/Client/Assets/Search/SearchPlugins/mr/eBay-in.xml new file mode 100644 index 0000000000..8d27d340f9 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/mr/eBay-in.xml @@ -0,0 +1,17 @@ + + + +eBay India +UTF-8 +data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAAEEfUpiAAAEWklEQVRYw8WXfUxVdRjHP79z7guXBBTEEBXEraktCydjWi7RZW82lr340kZNXaX+41p/tF42Xakw3ZqNyeZMnZVYtjnLNLXMzJFLA1FrgiIioveCIvcyLvdyzz3n6Q+7FxIM5YJ8/jovv/2e7/Oc5+X8FICIbCKCiCyTLmgArcUbmb2ugirP3u4rlIjkA3P+3aFIiYjQBa21eCMARv2XFB5Mo9sKRORZ+R80IMv0NAJQ7Q4CYDYdJmwGehZ+O0pEXEA7d0B1i0UnbUqpd7urvg2NXtAAQhWnmF5Ug9tnQDiIZYUBCBo+NPf0WQTLfqc1aDEyyU77kWyCB9Kpa/6NOHsS3Mm9tQcejLoZk0gzEgehb2zp9VP0Ro8S522qv2sJGkDLR6uwmpvBsvA8n8+3f/oA+Oakly3HbhI6swKA9u9dFB5MA6DKs5drvtO3Noh/4Tm8a9fRunUbzkmTGJGgs3ZfIx/sbiT/YQPTdxbjUgnKkcL7z3goPJjG0PhM0pMeo19iYBIrIvJaH4x3iIgeyYNUYAWQepc2jyqlSiPWX+mr/wAqhiwEWKPFGL6sWDfoXm1Ltjf0bYNQ+SnEMPB4b7WLX6vbOmu2uQyrrZqg4Ys+izRGDaBxzlwcUybjXbkaEYuSIzfIGz8E9eZZQudWoidPQ+mJ/F25nNrrRwBY/3MWADYAfXgyrcUb0UeOQNpDLJ85HIAnH4rHrP8CQ9nQEibyiH0In1Ys5KXsLSyadqhL1ztfgy1rHP5du8HpJOu9Kjb8dJ3aGyHE9KMSJhA6XwhiMT/na3ZXLiEt8dFoGt8TJy5vFrfvTOR2R6yJVNr/eXCPhJSINN1DFd7O5EggvX0oxg97aipeGXjCIrJaRByRduACGoBk7j85GlAwSMYBVmiAzuChawwydyVgzmd1TC+qGRABtsiF5fXSsmoNofIKbBPGI01NWIEAQws/AZJpCVjMWHeRKy0GLodGlbuDd55KYf2r6VjXD9Nxcj44UtDiM7C8FcgDmWxTJgnOUbwxdX+XarPYXJZHYtwoFuTsRInIMqDEnfc09swxOPPyOgdBXS3tPxxg8YubONfmoK5oAkp1qs/++AJpiXZ+XJ5IuGEXVls1yriBWCFMz3600QvZ7j+Hy5bI61P3IWLxedlMklzpzJuyE6A0GgHb2AyU3UHCooKogY7ySvSx49B8Q7jW4Oe8J8j4kXEA1N80uNDYweKJlQT2L8D5+CHs45be8jLgJnBtD0op3nriKFuPz2bHyZcxwn4SnGkR40T6wDKgBMC8epXgsTLCl6+AaeLIzcE1K499f/nRFIxJtvPVcS++YJjMFAdvz0hhWLyO+C8Sqi1Ggo3orrHoGQVI4DKixWFLnYlpGWz4ZSKjh+Uyf0rpf6ZBzD93vWFZYfnij3zZc3ppT6932Aa6zJTSKcj9bsDGWb/0gUuDaL8mMgnnyv2nMHpc7TKSU4FcIBvI6GdvbwLlwAmlVPQU9g9eOPBg6mUX4QAAAABJRU5ErkJggg== + + + + + + + +http://search.ebay.in/ + diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/mr/list.txt b/mobile/ios/Client/Assets/Search/SearchPlugins/mr/list.txt new file mode 100644 index 0000000000..0e0551ee52 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/mr/list.txt @@ -0,0 +1,7 @@ +amazon-in +bing +eBay-in +google +rediff +wikipedia-mr +yahoo-in diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/mr/rediff.xml b/mobile/ios/Client/Assets/Search/SearchPlugins/mr/rediff.xml new file mode 100644 index 0000000000..7905815b3e --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/mr/rediff.xml @@ -0,0 +1,14 @@ + + + +Rediff +windows-1252 +data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAYAAADimHc4AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAA2hpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMC1jMDYxIDY0LjE0MDk0OSwgMjAxMC8xMi8wNy0xMDo1NzowMSAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtcE1NOk9yaWdpbmFsRG9jdW1lbnRJRD0ieG1wLmRpZDowMTgwMTE3NDA3MjA2ODExOEE2REI2ODhGMkQ2RTdCRSIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDpCNjU0MTA3MEUyQzcxMUU1QTJGRTgwNUQ3ODM3MDA4NyIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDpCNjU0MTA2RkUyQzcxMUU1QTJGRTgwNUQ3ODM3MDA4NyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ1M1LjEgTWFjaW50b3NoIj4gPHhtcE1NOkRlcml2ZWRGcm9tIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6MDE4MDExNzQwNzIwNjgxMTkwMEE4NjY0REI0QThFNzkiIHN0UmVmOmRvY3VtZW50SUQ9InhtcC5kaWQ6MDE4MDExNzQwNzIwNjgxMThBNkRCNjg4RjJENkU3QkUiLz4gPC9yZGY6RGVzY3Jp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 + + + + +http://search.rediff.com/ + diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/mr/wikipedia-mr.xml b/mobile/ios/Client/Assets/Search/SearchPlugins/mr/wikipedia-mr.xml new file mode 100644 index 0000000000..2ef18440a5 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/mr/wikipedia-mr.xml @@ -0,0 +1,23 @@ + + + +विकिपीडिया (mr) +UTF-8 +data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAQAAABIkb+zAAAEXElEQVR4Ae3cA4xs2QJG4b9aj41n27Zt27Zt27Zt27Zt+2KM5lXP924qOzs56VPjKSRnrfhv7NUsp08v98+PsxwT4nJ+nPunl8KZ8/WYQL+eMydJrx5/EhN603lAHpVJ5bzZNp035uyZXM7Uy3LmM7ms9CITzVRGQRfQBXQBXUAX0AV0AV1AF9AFdAFdQBfQBXQBXcBMjgU7M5+1vqtZyXqmc/IsZDEL+53LP3KRPCCv62+rWd7vnpx6v/M5Vd935OF5R+6RtRyeQ3NYNjKTxZw2p8tiduR8uWfemY3+dkjWM5fFLPU/8q9yrRwbHLP39WkHamPD5z1WnNMb/dpW/uAdbipu413+qcl2H3N3cXFv9DNNtvmg68uxMY6dPfewTZNvOauouqEdULlTY532IgqHua+ZxnoHm4ANj63biRNQPZedVPY5nTR1BXsVbNuyTlsDqy4tW/wK4NYiJ01AXMlm+wGrL1cDnWbLejC4m7T4DvAOOSkD4v0qztGyLzpUweOk4UXAp6XV72DNmU7qgPPao+DJ0uLTFPzXXGN5I9acU1o8vb14jZzUAfFeBdvMtuwLDlfwIKme2248T1p9MvY59zACLqcy4Kf5eQr+ZbYRfrAFaXHaf/FRGUZAfE/Bzwf8OKwpuJ/0vYRNPEpavT248rACbqfimtLiqxT8w4yIz+Of5qTV7+LHMqyAaf9R8Clp8ex2K7iXuAa4q7R6aXCH4QXE4xVsOq+0+DYFfzXth/ilnrT6Tvzb9DADlqwqDPjTd377FHwI3EBaPb0NPFaGGRBvoLBiUVr8EFS+KgN8KpYtDjvgQo6i8Hhp8ZJUjnIZaXXGNrxKhh0QX6TwHzPS4uco/FtPWr0T9jnPKAJuOOiCc/UqVG4jrf4AH5dRBPT8UWHg3/BvUfhl635ZcNXRBMSDqFxFWvwklVu07O/BT2RUAad0GIWPtexXgMpPt+xntAt3Gl1AvJjCPufasn4d/6NyY2n4DPzXzCgDzmEvhVdIw+tj02X8nsIPGvus7Xi8jDIgPkLhSPON5ad4t7gbletJ9S71n+BIA65C5dGal1g3nENM+weF70j1R3iNjDogfkrhn6br5dU/4UXS94EtF7+vUC8IjjzgrlRuJ33v27j2dTLbKXxd+r4Pn5RRB9RfxsL3y4H/i0dI9TFUrirOZDeuPh4B8TQqVyjH/ZtZqZ7KIRS+JJ6Fn8k4BNRL9IUPmncwbr/1b37lanbiLuMTEG+nsNfb8cOWq0DLFA7E/8yMU8AlNLnaoP/alSfKOAXEN6h8Qlo8k3UKq5bGLeAWFPa6oLT6Ogqvk3ELmPJ3wOtlgOe0B2w6//gFxCPBsjPIQN8JPi3jGDDvSDxVDPaCNnFNOdGczrNzIrEnRyR5XPZlMIdmKofnpTnx6B783QV0ARNOF9AFdAFdQBfQBXQBXUAX0AV0AV3A6OgCVjLJrEzlj5lk/jiVt2eSefvEvzTJxL84zMS/PM//Af1pBRpQAkXBAAAAAElFTkSuQmCC + + + + + + + + + + + + + +https://mr.wikipedia.org/wiki/विशेष:शोधा + diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/mr/yahoo-in.xml b/mobile/ios/Client/Assets/Search/SearchPlugins/mr/yahoo-in.xml new file mode 100644 index 0000000000..67e95519df --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/mr/yahoo-in.xml @@ -0,0 +1,22 @@ + + + +Yahoo +UTF-8 +data:image/png;base64,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 + + + + + + + + + + + + +https://in.search.yahoo.com/ + diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/ms/default.txt b/mobile/ios/Client/Assets/Search/SearchPlugins/ms/default.txt new file mode 100644 index 0000000000..ced95a5cb9 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/ms/default.txt @@ -0,0 +1 @@ +Google \ No newline at end of file diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/ms/list.txt b/mobile/ios/Client/Assets/Search/SearchPlugins/ms/list.txt new file mode 100644 index 0000000000..84d2fb5bf3 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/ms/list.txt @@ -0,0 +1,6 @@ +amazondotcom +bing +google +twitter +wikipedia-ms +yahoo diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/ms/wikipedia-ms.xml b/mobile/ios/Client/Assets/Search/SearchPlugins/ms/wikipedia-ms.xml new file mode 100644 index 0000000000..8b1f554755 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/ms/wikipedia-ms.xml @@ -0,0 +1,23 @@ + + + +Wikipedia (ms) +UTF-8 +data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAQAAABIkb+zAAAEXElEQVR4Ae3cA4xs2QJG4b9aj41n27Zt27Zt27Zt27Zt+2KM5lXP924qOzs56VPjKSRnrfhv7NUsp08v98+PsxwT4nJ+nPunl8KZ8/WYQL+eMydJrx5/EhN603lAHpVJ5bzZNp035uyZXM7Uy3LmM7ms9CITzVRGQRfQBXQBXUAX0AV0AV1AF9AFdAFdQBfQBXQBXcBMjgU7M5+1vqtZyXqmc/IsZDEL+53LP3KRPCCv62+rWd7vnpx6v/M5Vd935OF5R+6RtRyeQ3NYNjKTxZw2p8tiduR8uWfemY3+dkjWM5fFLPU/8q9yrRwbHLP39WkHamPD5z1WnNMb/dpW/uAdbipu413+qcl2H3N3cXFv9DNNtvmg68uxMY6dPfewTZNvOauouqEdULlTY532IgqHua+ZxnoHm4ANj63biRNQPZedVPY5nTR1BXsVbNuyTlsDqy4tW/wK4NYiJ01AXMlm+wGrL1cDnWbLejC4m7T4DvAOOSkD4v0qztGyLzpUweOk4UXAp6XV72DNmU7qgPPao+DJ0uLTFPzXXGN5I9acU1o8vb14jZzUAfFeBdvMtuwLDlfwIKme2248T1p9MvY59zACLqcy4Kf5eQr+ZbYRfrAFaXHaf/FRGUZAfE/Bzwf8OKwpuJ/0vYRNPEpavT248rACbqfimtLiqxT8w4yIz+Of5qTV7+LHMqyAaf9R8Clp8ex2K7iXuAa4q7R6aXCH4QXE4xVsOq+0+DYFfzXth/ilnrT6Tvzb9DADlqwqDPjTd377FHwI3EBaPb0NPFaGGRBvoLBiUVr8EFS+KgN8KpYtDjvgQo6i8Hhp8ZJUjnIZaXXGNrxKhh0QX6TwHzPS4uco/FtPWr0T9jnPKAJuOOiCc/UqVG4jrf4AH5dRBPT8UWHg3/BvUfhl635ZcNXRBMSDqFxFWvwklVu07O/BT2RUAad0GIWPtexXgMpPt+xntAt3Gl1AvJjCPufasn4d/6NyY2n4DPzXzCgDzmEvhVdIw+tj02X8nsIPGvus7Xi8jDIgPkLhSPON5ad4t7gbletJ9S71n+BIA65C5dGal1g3nENM+weF70j1R3iNjDogfkrhn6br5dU/4UXS94EtF7+vUC8IjjzgrlRuJ33v27j2dTLbKXxd+r4Pn5RRB9RfxsL3y4H/i0dI9TFUrirOZDeuPh4B8TQqVyjH/ZtZqZ7KIRS+JJ6Fn8k4BNRL9IUPmncwbr/1b37lanbiLuMTEG+nsNfb8cOWq0DLFA7E/8yMU8AlNLnaoP/alSfKOAXEN6h8Qlo8k3UKq5bGLeAWFPa6oLT6Ogqvk3ELmPJ3wOtlgOe0B2w6//gFxCPBsjPIQN8JPi3jGDDvSDxVDPaCNnFNOdGczrNzIrEnRyR5XPZlMIdmKofnpTnx6B783QV0ARNOF9AFdAFdQBfQBXQBXUAX0AV0AV3A6OgCVjLJrEzlj5lk/jiVt2eSefvEvzTJxL84zMS/PM//Af1pBRpQAkXBAAAAAElFTkSuQmCC + + + + + + + + + + + + + +https://ms.wikipedia.org/wiki/Khas:Gelintar + diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/my/default.txt b/mobile/ios/Client/Assets/Search/SearchPlugins/my/default.txt new file mode 100644 index 0000000000..ced95a5cb9 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/my/default.txt @@ -0,0 +1 @@ +Google \ No newline at end of file diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/my/list.txt b/mobile/ios/Client/Assets/Search/SearchPlugins/my/list.txt new file mode 100644 index 0000000000..13bc570250 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/my/list.txt @@ -0,0 +1,6 @@ +amazondotcom +bing +google +twitter +wikipedia-my +yahoo diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/my/wikipedia-my.xml b/mobile/ios/Client/Assets/Search/SearchPlugins/my/wikipedia-my.xml new file mode 100644 index 0000000000..8b03d391b6 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/my/wikipedia-my.xml @@ -0,0 +1,23 @@ + + + +Wikipedia (my) +UTF-8 +data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAQAAABIkb+zAAAEXElEQVR4Ae3cA4xs2QJG4b9aj41n27Zt27Zt27Zt27Zt+2KM5lXP924qOzs56VPjKSRnrfhv7NUsp08v98+PsxwT4nJ+nPunl8KZ8/WYQL+eMydJrx5/EhN603lAHpVJ5bzZNp035uyZXM7Uy3LmM7ms9CITzVRGQRfQBXQBXUAX0AV0AV1AF9AFdAFdQBfQBXQBXcBMjgU7M5+1vqtZyXqmc/IsZDEL+53LP3KRPCCv62+rWd7vnpx6v/M5Vd935OF5R+6RtRyeQ3NYNjKTxZw2p8tiduR8uWfemY3+dkjWM5fFLPU/8q9yrRwbHLP39WkHamPD5z1WnNMb/dpW/uAdbipu413+qcl2H3N3cXFv9DNNtvmg68uxMY6dPfewTZNvOauouqEdULlTY532IgqHua+ZxnoHm4ANj63biRNQPZedVPY5nTR1BXsVbNuyTlsDqy4tW/wK4NYiJ01AXMlm+wGrL1cDnWbLejC4m7T4DvAOOSkD4v0qztGyLzpUweOk4UXAp6XV72DNmU7qgPPao+DJ0uLTFPzXXGN5I9acU1o8vb14jZzUAfFeBdvMtuwLDlfwIKme2248T1p9MvY59zACLqcy4Kf5eQr+ZbYRfrAFaXHaf/FRGUZAfE/Bzwf8OKwpuJ/0vYRNPEpavT248rACbqfimtLiqxT8w4yIz+Of5qTV7+LHMqyAaf9R8Clp8ex2K7iXuAa4q7R6aXCH4QXE4xVsOq+0+DYFfzXth/ilnrT6Tvzb9DADlqwqDPjTd377FHwI3EBaPb0NPFaGGRBvoLBiUVr8EFS+KgN8KpYtDjvgQo6i8Hhp8ZJUjnIZaXXGNrxKhh0QX6TwHzPS4uco/FtPWr0T9jnPKAJuOOiCc/UqVG4jrf4AH5dRBPT8UWHg3/BvUfhl635ZcNXRBMSDqFxFWvwklVu07O/BT2RUAad0GIWPtexXgMpPt+xntAt3Gl1AvJjCPufasn4d/6NyY2n4DPzXzCgDzmEvhVdIw+tj02X8nsIPGvus7Xi8jDIgPkLhSPON5ad4t7gbletJ9S71n+BIA65C5dGal1g3nENM+weF70j1R3iNjDogfkrhn6br5dU/4UXS94EtF7+vUC8IjjzgrlRuJ33v27j2dTLbKXxd+r4Pn5RRB9RfxsL3y4H/i0dI9TFUrirOZDeuPh4B8TQqVyjH/ZtZqZ7KIRS+JJ6Fn8k4BNRL9IUPmncwbr/1b37lanbiLuMTEG+nsNfb8cOWq0DLFA7E/8yMU8AlNLnaoP/alSfKOAXEN6h8Qlo8k3UKq5bGLeAWFPa6oLT6Ogqvk3ELmPJ3wOtlgOe0B2w6//gFxCPBsjPIQN8JPi3jGDDvSDxVDPaCNnFNOdGczrNzIrEnRyR5XPZlMIdmKofnpTnx6B783QV0ARNOF9AFdAFdQBfQBXQBXUAX0AV0AV3A6OgCVjLJrEzlj5lk/jiVt2eSefvEvzTJxL84zMS/PM//Af1pBRpQAkXBAAAAAElFTkSuQmCC + + + + + + + + + + + + + +https://my.wikipedia.org/wiki/Special:Search + diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/nb-NO/default.txt b/mobile/ios/Client/Assets/Search/SearchPlugins/nb-NO/default.txt new file mode 100644 index 0000000000..ced95a5cb9 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/nb-NO/default.txt @@ -0,0 +1 @@ +Google \ No newline at end of file diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/nb-NO/gulesider-mobile-NO.xml b/mobile/ios/Client/Assets/Search/SearchPlugins/nb-NO/gulesider-mobile-NO.xml new file mode 100644 index 0000000000..bac0801df6 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/nb-NO/gulesider-mobile-NO.xml @@ -0,0 +1,16 @@ + + + +Gule sider mobil +UTF-8 +data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAYAAADimHc4AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAA2hpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMC1jMDYxIDY0LjE0MDk0OSwgMjAxMC8xMi8wNy0xMDo1NzowMSAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtcE1NOk9yaWdpbmFsRG9jdW1lbnRJRD0ieG1wLmRpZDowMzgwMTE3NDA3MjA2ODExOEE2REI2ODhGMkQ2RTdCRSIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDpGQ0JFNjNCODNBODcxMUU1OTA4RkJFMkNCMTYxQzY3MCIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDpGQ0JFNjNCNzNBODcxMUU1OTA4RkJFMkNCMTYxQzY3MCIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ1M1LjEgTWFjaW50b3NoIj4gPHhtcE1NOkRlcml2ZWRGcm9tIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6Rjg3RjExNzQwNzIwNjgxMThDMTQ5NzZBOENCMzZGNDciIHN0UmVmOmRvY3VtZW50SUQ9InhtcC5kaWQ6MDM4MDExNzQwNzIwNjgxMThBNkRCNjg4RjJENkU3QkUiLz4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+IDw/eHBhY2tldCBlbmQ9InIiPz46qzHsAAACjUlEQVR42uydzWsTQRiH391N4weRSjH1CyIIpUpvjRfBS/GoCBWRXvRUevIk6t8gKl4F/4CCUANtjwVBW/Ai4kGk1CIopBS3RFNEMUld3ykreN3dzs6WPk/4EViYzOR9dnfCTEK86E1dYgLNlOaG5ryADZqaGc19zbo54MUCqppZCp8bLc2EZsGPz/wGxc+Vgbjmw0bANc0FapI7Fc0DP77ngxsuGQHD1MEZgRFQpg7u8CkBAhAACEAAIAABgAAEAAIQAAhAACAAAYAABIBdSrY7uHL7ozS/dhO3O1ktS+PRaSn5Xqb+e38iuXrnkzTDTvIxDPbJ3OOh3S1g/tVmqnZvl3/JWtiT2rFsO6ZrYVfmF9upx8AtiDkAEIAAQAACAAEIAAQgABCAANg1Asp9Xup2/ZUgc//mNbKMwTbWV0NXGiOy8X0rcbuB/mDHBCw/H5FWO/kYjhwOrAswP1P9rM81az1EkfaS/kx6t/JTnsyEqdpOjVelfvags7EX4grIytPGxnbSEPieCqgV+v0xCSMAAYAABAACEAAIQAAgAAGAAAQUhiDDt6MD3yu8gMKvht69eVQO7Pek/SPZer7ZB7h1fbDwAgq/H7ATfFnvSPitl7id2ZA5dXzf3r4CsmKunKHx99LpRonbmi3J369HmYSzCkhTfEPadgjgUxAgAAGAAAQAAhAACEAAIGCvCoiosVsBnj62l6Qd5US1JGP1Q6nGPnauYl2A/f0AYA5AACAAAYAABAACEAAIQAAgAAGAAAQAAhCAAHAtoEMZ3Ar4QBmcsWUETFMHZ8wZAc80C9Qid8wf29z7NwlPaF5Sk9xoaS5rVv3/DlzUTGpemHsTNbLCquah5oxmyRz4K8AAibe+3Px7Wu8AAAAASUVORK5CYII= + + + + + + +http://m.gulesider.no/ + diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/nb-NO/list.txt b/mobile/ios/Client/Assets/Search/SearchPlugins/nb-NO/list.txt new file mode 100644 index 0000000000..66c0699ba4 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/nb-NO/list.txt @@ -0,0 +1,5 @@ +duckduckgo +google +gulesider-mobile-NO +twitter +wikipedia-NO diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/nb-NO/wikipedia-NO.xml b/mobile/ios/Client/Assets/Search/SearchPlugins/nb-NO/wikipedia-NO.xml new file mode 100644 index 0000000000..52180b96d5 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/nb-NO/wikipedia-NO.xml @@ -0,0 +1,23 @@ + + + +Wikipedia (no) +UTF-8 +data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAQAAABIkb+zAAAEXElEQVR4Ae3cA4xs2QJG4b9aj41n27Zt27Zt27Zt27Zt+2KM5lXP924qOzs56VPjKSRnrfhv7NUsp08v98+PsxwT4nJ+nPunl8KZ8/WYQL+eMydJrx5/EhN603lAHpVJ5bzZNp035uyZXM7Uy3LmM7ms9CITzVRGQRfQBXQBXUAX0AV0AV1AF9AFdAFdQBfQBXQBXcBMjgU7M5+1vqtZyXqmc/IsZDEL+53LP3KRPCCv62+rWd7vnpx6v/M5Vd935OF5R+6RtRyeQ3NYNjKTxZw2p8tiduR8uWfemY3+dkjWM5fFLPU/8q9yrRwbHLP39WkHamPD5z1WnNMb/dpW/uAdbipu413+qcl2H3N3cXFv9DNNtvmg68uxMY6dPfewTZNvOauouqEdULlTY532IgqHua+ZxnoHm4ANj63biRNQPZedVPY5nTR1BXsVbNuyTlsDqy4tW/wK4NYiJ01AXMlm+wGrL1cDnWbLejC4m7T4DvAOOSkD4v0qztGyLzpUweOk4UXAp6XV72DNmU7qgPPao+DJ0uLTFPzXXGN5I9acU1o8vb14jZzUAfFeBdvMtuwLDlfwIKme2248T1p9MvY59zACLqcy4Kf5eQr+ZbYRfrAFaXHaf/FRGUZAfE/Bzwf8OKwpuJ/0vYRNPEpavT248rACbqfimtLiqxT8w4yIz+Of5qTV7+LHMqyAaf9R8Clp8ex2K7iXuAa4q7R6aXCH4QXE4xVsOq+0+DYFfzXth/ilnrT6Tvzb9DADlqwqDPjTd377FHwI3EBaPb0NPFaGGRBvoLBiUVr8EFS+KgN8KpYtDjvgQo6i8Hhp8ZJUjnIZaXXGNrxKhh0QX6TwHzPS4uco/FtPWr0T9jnPKAJuOOiCc/UqVG4jrf4AH5dRBPT8UWHg3/BvUfhl635ZcNXRBMSDqFxFWvwklVu07O/BT2RUAad0GIWPtexXgMpPt+xntAt3Gl1AvJjCPufasn4d/6NyY2n4DPzXzCgDzmEvhVdIw+tj02X8nsIPGvus7Xi8jDIgPkLhSPON5ad4t7gbletJ9S71n+BIA65C5dGal1g3nENM+weF70j1R3iNjDogfkrhn6br5dU/4UXS94EtF7+vUC8IjjzgrlRuJ33v27j2dTLbKXxd+r4Pn5RRB9RfxsL3y4H/i0dI9TFUrirOZDeuPh4B8TQqVyjH/ZtZqZ7KIRS+JJ6Fn8k4BNRL9IUPmncwbr/1b37lanbiLuMTEG+nsNfb8cOWq0DLFA7E/8yMU8AlNLnaoP/alSfKOAXEN6h8Qlo8k3UKq5bGLeAWFPa6oLT6Ogqvk3ELmPJ3wOtlgOe0B2w6//gFxCPBsjPIQN8JPi3jGDDvSDxVDPaCNnFNOdGczrNzIrEnRyR5XPZlMIdmKofnpTnx6B783QV0ARNOF9AFdAFdQBfQBXQBXUAX0AV0AV3A6OgCVjLJrEzlj5lk/jiVt2eSefvEvzTJxL84zMS/PM//Af1pBRpQAkXBAAAAAElFTkSuQmCC + + + + + + + + + + + + + +https://no.wikipedia.org/wiki/Spesial:Søk + diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/nl/bolcom-nl.xml b/mobile/ios/Client/Assets/Search/SearchPlugins/nl/bolcom-nl.xml new file mode 100644 index 0000000000..7e93a46a54 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/nl/bolcom-nl.xml @@ -0,0 +1,12 @@ + + + +bol.com +UTF-8 +data:image/png;base64,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 +http://www.bol.com/ + + + diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/nl/default.txt b/mobile/ios/Client/Assets/Search/SearchPlugins/nl/default.txt new file mode 100644 index 0000000000..ced95a5cb9 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/nl/default.txt @@ -0,0 +1 @@ +Google \ No newline at end of file diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/nl/list.txt b/mobile/ios/Client/Assets/Search/SearchPlugins/nl/list.txt new file mode 100644 index 0000000000..1c4b14278f --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/nl/list.txt @@ -0,0 +1,3 @@ +google +wikipedia-nl +bolcom-nl \ No newline at end of file diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/nl/wikipedia-nl.xml b/mobile/ios/Client/Assets/Search/SearchPlugins/nl/wikipedia-nl.xml new file mode 100644 index 0000000000..6c53270a65 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/nl/wikipedia-nl.xml @@ -0,0 +1,23 @@ + + + +Wikipedia (nl) +UTF-8 +data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAQAAABIkb+zAAAEXElEQVR4Ae3cA4xs2QJG4b9aj41n27Zt27Zt27Zt27Zt+2KM5lXP924qOzs56VPjKSRnrfhv7NUsp08v98+PsxwT4nJ+nPunl8KZ8/WYQL+eMydJrx5/EhN603lAHpVJ5bzZNp035uyZXM7Uy3LmM7ms9CITzVRGQRfQBXQBXUAX0AV0AV1AF9AFdAFdQBfQBXQBXcBMjgU7M5+1vqtZyXqmc/IsZDEL+53LP3KRPCCv62+rWd7vnpx6v/M5Vd935OF5R+6RtRyeQ3NYNjKTxZw2p8tiduR8uWfemY3+dkjWM5fFLPU/8q9yrRwbHLP39WkHamPD5z1WnNMb/dpW/uAdbipu413+qcl2H3N3cXFv9DNNtvmg68uxMY6dPfewTZNvOauouqEdULlTY532IgqHua+ZxnoHm4ANj63biRNQPZedVPY5nTR1BXsVbNuyTlsDqy4tW/wK4NYiJ01AXMlm+wGrL1cDnWbLejC4m7T4DvAOOSkD4v0qztGyLzpUweOk4UXAp6XV72DNmU7qgPPao+DJ0uLTFPzXXGN5I9acU1o8vb14jZzUAfFeBdvMtuwLDlfwIKme2248T1p9MvY59zACLqcy4Kf5eQr+ZbYRfrAFaXHaf/FRGUZAfE/Bzwf8OKwpuJ/0vYRNPEpavT248rACbqfimtLiqxT8w4yIz+Of5qTV7+LHMqyAaf9R8Clp8ex2K7iXuAa4q7R6aXCH4QXE4xVsOq+0+DYFfzXth/ilnrT6Tvzb9DADlqwqDPjTd377FHwI3EBaPb0NPFaGGRBvoLBiUVr8EFS+KgN8KpYtDjvgQo6i8Hhp8ZJUjnIZaXXGNrxKhh0QX6TwHzPS4uco/FtPWr0T9jnPKAJuOOiCc/UqVG4jrf4AH5dRBPT8UWHg3/BvUfhl635ZcNXRBMSDqFxFWvwklVu07O/BT2RUAad0GIWPtexXgMpPt+xntAt3Gl1AvJjCPufasn4d/6NyY2n4DPzXzCgDzmEvhVdIw+tj02X8nsIPGvus7Xi8jDIgPkLhSPON5ad4t7gbletJ9S71n+BIA65C5dGal1g3nENM+weF70j1R3iNjDogfkrhn6br5dU/4UXS94EtF7+vUC8IjjzgrlRuJ33v27j2dTLbKXxd+r4Pn5RRB9RfxsL3y4H/i0dI9TFUrirOZDeuPh4B8TQqVyjH/ZtZqZ7KIRS+JJ6Fn8k4BNRL9IUPmncwbr/1b37lanbiLuMTEG+nsNfb8cOWq0DLFA7E/8yMU8AlNLnaoP/alSfKOAXEN6h8Qlo8k3UKq5bGLeAWFPa6oLT6Ogqvk3ELmPJ3wOtlgOe0B2w6//gFxCPBsjPIQN8JPi3jGDDvSDxVDPaCNnFNOdGczrNzIrEnRyR5XPZlMIdmKofnpTnx6B783QV0ARNOF9AFdAFdQBfQBXQBXUAX0AV0AV3A6OgCVjLJrEzlj5lk/jiVt2eSefvEvzTJxL84zMS/PM//Af1pBRpQAkXBAAAAAElFTkSuQmCC + + + + + + + + + + + + + +https://nl.wikipedia.org/wiki/Speciaal:Zoeken + diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/nn-NO/default.txt b/mobile/ios/Client/Assets/Search/SearchPlugins/nn-NO/default.txt new file mode 100644 index 0000000000..ced95a5cb9 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/nn-NO/default.txt @@ -0,0 +1 @@ +Google \ No newline at end of file diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/nn-NO/gulesider-mobile-NO.xml b/mobile/ios/Client/Assets/Search/SearchPlugins/nn-NO/gulesider-mobile-NO.xml new file mode 100644 index 0000000000..bac0801df6 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/nn-NO/gulesider-mobile-NO.xml @@ -0,0 +1,16 @@ + + + +Gule sider mobil +UTF-8 +data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAYAAADimHc4AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAA2hpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMC1jMDYxIDY0LjE0MDk0OSwgMjAxMC8xMi8wNy0xMDo1NzowMSAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtcE1NOk9yaWdpbmFsRG9jdW1lbnRJRD0ieG1wLmRpZDowMzgwMTE3NDA3MjA2ODExOEE2REI2ODhGMkQ2RTdCRSIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDpGQ0JFNjNCODNBODcxMUU1OTA4RkJFMkNCMTYxQzY3MCIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDpGQ0JFNjNCNzNBODcxMUU1OTA4RkJFMkNCMTYxQzY3MCIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ1M1LjEgTWFjaW50b3NoIj4gPHhtcE1NOkRlcml2ZWRGcm9tIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6Rjg3RjExNzQwNzIwNjgxMThDMTQ5NzZBOENCMzZGNDciIHN0UmVmOmRvY3VtZW50SUQ9InhtcC5kaWQ6MDM4MDExNzQwNzIwNjgxMThBNkRCNjg4RjJENkU3QkUiLz4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+IDw/eHBhY2tldCBlbmQ9InIiPz46qzHsAAACjUlEQVR42uydzWsTQRiH391N4weRSjH1CyIIpUpvjRfBS/GoCBWRXvRUevIk6t8gKl4F/4CCUANtjwVBW/Ai4kGk1CIopBS3RFNEMUld3ykreN3dzs6WPk/4EViYzOR9dnfCTEK86E1dYgLNlOaG5ryADZqaGc19zbo54MUCqppZCp8bLc2EZsGPz/wGxc+Vgbjmw0bANc0FapI7Fc0DP77ngxsuGQHD1MEZgRFQpg7u8CkBAhAACEAAIAABgAAEAAIQAAhAACAAAYAABIBdSrY7uHL7ozS/dhO3O1ktS+PRaSn5Xqb+e38iuXrnkzTDTvIxDPbJ3OOh3S1g/tVmqnZvl3/JWtiT2rFsO6ZrYVfmF9upx8AtiDkAEIAAQAACAAEIAAQgABCAANg1Asp9Xup2/ZUgc//mNbKMwTbWV0NXGiOy8X0rcbuB/mDHBCw/H5FWO/kYjhwOrAswP1P9rM81az1EkfaS/kx6t/JTnsyEqdpOjVelfvags7EX4grIytPGxnbSEPieCqgV+v0xCSMAAYAABAACEAAIQAAgAAGAAAQUhiDDt6MD3yu8gMKvht69eVQO7Pek/SPZer7ZB7h1fbDwAgq/H7ATfFnvSPitl7id2ZA5dXzf3r4CsmKunKHx99LpRonbmi3J369HmYSzCkhTfEPadgjgUxAgAAGAAAQAAhAACEAAIGCvCoiosVsBnj62l6Qd5US1JGP1Q6nGPnauYl2A/f0AYA5AACAAAYAABAACEAAIQAAgAAGAAAQAAhCAAHAtoEMZ3Ar4QBmcsWUETFMHZ8wZAc80C9Qid8wf29z7NwlPaF5Sk9xoaS5rVv3/DlzUTGpemHsTNbLCquah5oxmyRz4K8AAibe+3Px7Wu8AAAAASUVORK5CYII= + + + + + + +http://m.gulesider.no/ + diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/nn-NO/list.txt b/mobile/ios/Client/Assets/Search/SearchPlugins/nn-NO/list.txt new file mode 100644 index 0000000000..77033aeaa7 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/nn-NO/list.txt @@ -0,0 +1,5 @@ +duckduckgo +google +gulesider-mobile-NO +twitter +wikipedia-NN diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/nn-NO/wikipedia-NN.xml b/mobile/ios/Client/Assets/Search/SearchPlugins/nn-NO/wikipedia-NN.xml new file mode 100644 index 0000000000..fc7de5e432 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/nn-NO/wikipedia-NN.xml @@ -0,0 +1,23 @@ + + + +Wikipedia (nn) +UTF-8 +data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAQAAABIkb+zAAAEXElEQVR4Ae3cA4xs2QJG4b9aj41n27Zt27Zt27Zt27Zt+2KM5lXP924qOzs56VPjKSRnrfhv7NUsp08v98+PsxwT4nJ+nPunl8KZ8/WYQL+eMydJrx5/EhN603lAHpVJ5bzZNp035uyZXM7Uy3LmM7ms9CITzVRGQRfQBXQBXUAX0AV0AV1AF9AFdAFdQBfQBXQBXcBMjgU7M5+1vqtZyXqmc/IsZDEL+53LP3KRPCCv62+rWd7vnpx6v/M5Vd935OF5R+6RtRyeQ3NYNjKTxZw2p8tiduR8uWfemY3+dkjWM5fFLPU/8q9yrRwbHLP39WkHamPD5z1WnNMb/dpW/uAdbipu413+qcl2H3N3cXFv9DNNtvmg68uxMY6dPfewTZNvOauouqEdULlTY532IgqHua+ZxnoHm4ANj63biRNQPZedVPY5nTR1BXsVbNuyTlsDqy4tW/wK4NYiJ01AXMlm+wGrL1cDnWbLejC4m7T4DvAOOSkD4v0qztGyLzpUweOk4UXAp6XV72DNmU7qgPPao+DJ0uLTFPzXXGN5I9acU1o8vb14jZzUAfFeBdvMtuwLDlfwIKme2248T1p9MvY59zACLqcy4Kf5eQr+ZbYRfrAFaXHaf/FRGUZAfE/Bzwf8OKwpuJ/0vYRNPEpavT248rACbqfimtLiqxT8w4yIz+Of5qTV7+LHMqyAaf9R8Clp8ex2K7iXuAa4q7R6aXCH4QXE4xVsOq+0+DYFfzXth/ilnrT6Tvzb9DADlqwqDPjTd377FHwI3EBaPb0NPFaGGRBvoLBiUVr8EFS+KgN8KpYtDjvgQo6i8Hhp8ZJUjnIZaXXGNrxKhh0QX6TwHzPS4uco/FtPWr0T9jnPKAJuOOiCc/UqVG4jrf4AH5dRBPT8UWHg3/BvUfhl635ZcNXRBMSDqFxFWvwklVu07O/BT2RUAad0GIWPtexXgMpPt+xntAt3Gl1AvJjCPufasn4d/6NyY2n4DPzXzCgDzmEvhVdIw+tj02X8nsIPGvus7Xi8jDIgPkLhSPON5ad4t7gbletJ9S71n+BIA65C5dGal1g3nENM+weF70j1R3iNjDogfkrhn6br5dU/4UXS94EtF7+vUC8IjjzgrlRuJ33v27j2dTLbKXxd+r4Pn5RRB9RfxsL3y4H/i0dI9TFUrirOZDeuPh4B8TQqVyjH/ZtZqZ7KIRS+JJ6Fn8k4BNRL9IUPmncwbr/1b37lanbiLuMTEG+nsNfb8cOWq0DLFA7E/8yMU8AlNLnaoP/alSfKOAXEN6h8Qlo8k3UKq5bGLeAWFPa6oLT6Ogqvk3ELmPJ3wOtlgOe0B2w6//gFxCPBsjPIQN8JPi3jGDDvSDxVDPaCNnFNOdGczrNzIrEnRyR5XPZlMIdmKofnpTnx6B783QV0ARNOF9AFdAFdQBfQBXQBXUAX0AV0AV3A6OgCVjLJrEzlj5lk/jiVt2eSefvEvzTJxL84zMS/PM//Af1pBRpQAkXBAAAAAElFTkSuQmCC + + + + + + + + + + + + + +https://nn.wikipedia.org/wiki/Spesial:Søk + diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/or/amazon-in.xml b/mobile/ios/Client/Assets/Search/SearchPlugins/or/amazon-in.xml new file mode 100644 index 0000000000..05c5311e31 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/or/amazon-in.xml @@ -0,0 +1,15 @@ + + + +Amazon.in +utf-8 +data:image/png;base64,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 + + + + + +https://www.amazon.in/ + diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/or/default.txt b/mobile/ios/Client/Assets/Search/SearchPlugins/or/default.txt new file mode 100644 index 0000000000..ced95a5cb9 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/or/default.txt @@ -0,0 +1 @@ +Google \ No newline at end of file diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/or/eBay-in.xml b/mobile/ios/Client/Assets/Search/SearchPlugins/or/eBay-in.xml new file mode 100644 index 0000000000..8d27d340f9 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/or/eBay-in.xml @@ -0,0 +1,17 @@ + + + +eBay India +UTF-8 +data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAAEEfUpiAAAEWklEQVRYw8WXfUxVdRjHP79z7guXBBTEEBXEraktCydjWi7RZW82lr340kZNXaX+41p/tF42Xakw3ZqNyeZMnZVYtjnLNLXMzJFLA1FrgiIioveCIvcyLvdyzz3n6Q+7FxIM5YJ8/jovv/2e7/Oc5+X8FICIbCKCiCyTLmgArcUbmb2ugirP3u4rlIjkA3P+3aFIiYjQBa21eCMARv2XFB5Mo9sKRORZ+R80IMv0NAJQ7Q4CYDYdJmwGehZ+O0pEXEA7d0B1i0UnbUqpd7urvg2NXtAAQhWnmF5Ug9tnQDiIZYUBCBo+NPf0WQTLfqc1aDEyyU77kWyCB9Kpa/6NOHsS3Mm9tQcejLoZk0gzEgehb2zp9VP0Ro8S522qv2sJGkDLR6uwmpvBsvA8n8+3f/oA+Oakly3HbhI6swKA9u9dFB5MA6DKs5drvtO3Noh/4Tm8a9fRunUbzkmTGJGgs3ZfIx/sbiT/YQPTdxbjUgnKkcL7z3goPJjG0PhM0pMeo19iYBIrIvJaH4x3iIgeyYNUYAWQepc2jyqlSiPWX+mr/wAqhiwEWKPFGL6sWDfoXm1Ltjf0bYNQ+SnEMPB4b7WLX6vbOmu2uQyrrZqg4Ys+izRGDaBxzlwcUybjXbkaEYuSIzfIGz8E9eZZQudWoidPQ+mJ/F25nNrrRwBY/3MWADYAfXgyrcUb0UeOQNpDLJ85HIAnH4rHrP8CQ9nQEibyiH0In1Ys5KXsLSyadqhL1ztfgy1rHP5du8HpJOu9Kjb8dJ3aGyHE9KMSJhA6XwhiMT/na3ZXLiEt8dFoGt8TJy5vFrfvTOR2R6yJVNr/eXCPhJSINN1DFd7O5EggvX0oxg97aipeGXjCIrJaRByRduACGoBk7j85GlAwSMYBVmiAzuChawwydyVgzmd1TC+qGRABtsiF5fXSsmoNofIKbBPGI01NWIEAQws/AZJpCVjMWHeRKy0GLodGlbuDd55KYf2r6VjXD9Nxcj44UtDiM7C8FcgDmWxTJgnOUbwxdX+XarPYXJZHYtwoFuTsRInIMqDEnfc09swxOPPyOgdBXS3tPxxg8YubONfmoK5oAkp1qs/++AJpiXZ+XJ5IuGEXVls1yriBWCFMz3600QvZ7j+Hy5bI61P3IWLxedlMklzpzJuyE6A0GgHb2AyU3UHCooKogY7ySvSx49B8Q7jW4Oe8J8j4kXEA1N80uNDYweKJlQT2L8D5+CHs45be8jLgJnBtD0op3nriKFuPz2bHyZcxwn4SnGkR40T6wDKgBMC8epXgsTLCl6+AaeLIzcE1K499f/nRFIxJtvPVcS++YJjMFAdvz0hhWLyO+C8Sqi1Ggo3orrHoGQVI4DKixWFLnYlpGWz4ZSKjh+Uyf0rpf6ZBzD93vWFZYfnij3zZc3ppT6932Aa6zJTSKcj9bsDGWb/0gUuDaL8mMgnnyv2nMHpc7TKSU4FcIBvI6GdvbwLlwAmlVPQU9g9eOPBg6mUX4QAAAABJRU5ErkJggg== + + + + + + + +http://search.ebay.in/ + diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/or/list.txt b/mobile/ios/Client/Assets/Search/SearchPlugins/or/list.txt new file mode 100644 index 0000000000..065d0ca721 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/or/list.txt @@ -0,0 +1,7 @@ +amazon-in +bing +eBay-in +google +wikipedia-or +wiktionary-or +yahoo-in diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/or/wikipedia-or.xml b/mobile/ios/Client/Assets/Search/SearchPlugins/or/wikipedia-or.xml new file mode 100644 index 0000000000..e8d50ff8aa --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/or/wikipedia-or.xml @@ -0,0 +1,23 @@ + + + +ଉଇକିପିଡ଼ିଆ (or) +UTF-8 +data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAQAAABIkb+zAAAEXElEQVR4Ae3cA4xs2QJG4b9aj41n27Zt27Zt27Zt27Zt+2KM5lXP924qOzs56VPjKSRnrfhv7NUsp08v98+PsxwT4nJ+nPunl8KZ8/WYQL+eMydJrx5/EhN603lAHpVJ5bzZNp035uyZXM7Uy3LmM7ms9CITzVRGQRfQBXQBXUAX0AV0AV1AF9AFdAFdQBfQBXQBXcBMjgU7M5+1vqtZyXqmc/IsZDEL+53LP3KRPCCv62+rWd7vnpx6v/M5Vd935OF5R+6RtRyeQ3NYNjKTxZw2p8tiduR8uWfemY3+dkjWM5fFLPU/8q9yrRwbHLP39WkHamPD5z1WnNMb/dpW/uAdbipu413+qcl2H3N3cXFv9DNNtvmg68uxMY6dPfewTZNvOauouqEdULlTY532IgqHua+ZxnoHm4ANj63biRNQPZedVPY5nTR1BXsVbNuyTlsDqy4tW/wK4NYiJ01AXMlm+wGrL1cDnWbLejC4m7T4DvAOOSkD4v0qztGyLzpUweOk4UXAp6XV72DNmU7qgPPao+DJ0uLTFPzXXGN5I9acU1o8vb14jZzUAfFeBdvMtuwLDlfwIKme2248T1p9MvY59zACLqcy4Kf5eQr+ZbYRfrAFaXHaf/FRGUZAfE/Bzwf8OKwpuJ/0vYRNPEpavT248rACbqfimtLiqxT8w4yIz+Of5qTV7+LHMqyAaf9R8Clp8ex2K7iXuAa4q7R6aXCH4QXE4xVsOq+0+DYFfzXth/ilnrT6Tvzb9DADlqwqDPjTd377FHwI3EBaPb0NPFaGGRBvoLBiUVr8EFS+KgN8KpYtDjvgQo6i8Hhp8ZJUjnIZaXXGNrxKhh0QX6TwHzPS4uco/FtPWr0T9jnPKAJuOOiCc/UqVG4jrf4AH5dRBPT8UWHg3/BvUfhl635ZcNXRBMSDqFxFWvwklVu07O/BT2RUAad0GIWPtexXgMpPt+xntAt3Gl1AvJjCPufasn4d/6NyY2n4DPzXzCgDzmEvhVdIw+tj02X8nsIPGvus7Xi8jDIgPkLhSPON5ad4t7gbletJ9S71n+BIA65C5dGal1g3nENM+weF70j1R3iNjDogfkrhn6br5dU/4UXS94EtF7+vUC8IjjzgrlRuJ33v27j2dTLbKXxd+r4Pn5RRB9RfxsL3y4H/i0dI9TFUrirOZDeuPh4B8TQqVyjH/ZtZqZ7KIRS+JJ6Fn8k4BNRL9IUPmncwbr/1b37lanbiLuMTEG+nsNfb8cOWq0DLFA7E/8yMU8AlNLnaoP/alSfKOAXEN6h8Qlo8k3UKq5bGLeAWFPa6oLT6Ogqvk3ELmPJ3wOtlgOe0B2w6//gFxCPBsjPIQN8JPi3jGDDvSDxVDPaCNnFNOdGczrNzIrEnRyR5XPZlMIdmKofnpTnx6B783QV0ARNOF9AFdAFdQBfQBXQBXUAX0AV0AV3A6OgCVjLJrEzlj5lk/jiVt2eSefvEvzTJxL84zMS/PM//Af1pBRpQAkXBAAAAAElFTkSuQmCC + + + + + + + + + + + + + +https://or.wikipedia.org/wiki/ବିଶେଷ:ଖୋଜନ୍ତୁ + diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/or/wiktionary-or.xml b/mobile/ios/Client/Assets/Search/SearchPlugins/or/wiktionary-or.xml new file mode 100644 index 0000000000..1425b54b47 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/or/wiktionary-or.xml @@ -0,0 +1,17 @@ + + + +Wiktionary (or) +data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAYAAADimHc4AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAA2hpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMC1jMDYxIDY0LjE0MDk0OSwgMjAxMC8xMi8wNy0xMDo1NzowMSAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtcE1NOk9yaWdpbmFsRG9jdW1lbnRJRD0ieG1wLmRpZDpGRTdGMTE3NDA3MjA2ODExOTJCMEU1MkZGMDQ2RkQ0QyIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDozNURGRTgzRTMwMzMxMUU1OTlBMUM2MDY0MDI4RjgwMSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDozNURGRTgzRDMwMzMxMUU1OTlBMUM2MDY0MDI4RjgwMSIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ1M1LjEgTWFjaW50b3NoIj4gPHhtcE1NOkRlcml2ZWRGcm9tIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6MDA4MDExNzQwNzIwNjgxMTkyQjBFNTJGRjA0NkZENEMiIHN0UmVmOmRvY3VtZW50SUQ9InhtcC5kaWQ6RkU3RjExNzQwNzIwNjgxMTkyQjBFNTJGRjA0NkZENEMiLz4gPC9yZGY6RGVzY3Jp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 + + + + + + + + +https://or.wiktionary.org/wiki/ବିଶେଷ:ଖୋଜନ୍ତୁ + diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/or/yahoo-in.xml b/mobile/ios/Client/Assets/Search/SearchPlugins/or/yahoo-in.xml new file mode 100644 index 0000000000..67e95519df --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/or/yahoo-in.xml @@ -0,0 +1,22 @@ + + + +Yahoo +UTF-8 +data:image/png;base64,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 + + + + + + + + + + + + +https://in.search.yahoo.com/ + diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/pl/default.txt b/mobile/ios/Client/Assets/Search/SearchPlugins/pl/default.txt new file mode 100644 index 0000000000..ced95a5cb9 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/pl/default.txt @@ -0,0 +1 @@ +Google \ No newline at end of file diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/pl/list.txt b/mobile/ios/Client/Assets/Search/SearchPlugins/pl/list.txt new file mode 100644 index 0000000000..f6c2e98a62 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/pl/list.txt @@ -0,0 +1,5 @@ +bing +duckduckgo +google +twitter +wikipedia-pl diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/pl/wikipedia-pl.xml b/mobile/ios/Client/Assets/Search/SearchPlugins/pl/wikipedia-pl.xml new file mode 100644 index 0000000000..eb10ef3ea1 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/pl/wikipedia-pl.xml @@ -0,0 +1,23 @@ + + + +Wikipedia (pl) +UTF-8 +data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAQAAABIkb+zAAAEXElEQVR4Ae3cA4xs2QJG4b9aj41n27Zt27Zt27Zt27Zt+2KM5lXP924qOzs56VPjKSRnrfhv7NUsp08v98+PsxwT4nJ+nPunl8KZ8/WYQL+eMydJrx5/EhN603lAHpVJ5bzZNp035uyZXM7Uy3LmM7ms9CITzVRGQRfQBXQBXUAX0AV0AV1AF9AFdAFdQBfQBXQBXcBMjgU7M5+1vqtZyXqmc/IsZDEL+53LP3KRPCCv62+rWd7vnpx6v/M5Vd935OF5R+6RtRyeQ3NYNjKTxZw2p8tiduR8uWfemY3+dkjWM5fFLPU/8q9yrRwbHLP39WkHamPD5z1WnNMb/dpW/uAdbipu413+qcl2H3N3cXFv9DNNtvmg68uxMY6dPfewTZNvOauouqEdULlTY532IgqHua+ZxnoHm4ANj63biRNQPZedVPY5nTR1BXsVbNuyTlsDqy4tW/wK4NYiJ01AXMlm+wGrL1cDnWbLejC4m7T4DvAOOSkD4v0qztGyLzpUweOk4UXAp6XV72DNmU7qgPPao+DJ0uLTFPzXXGN5I9acU1o8vb14jZzUAfFeBdvMtuwLDlfwIKme2248T1p9MvY59zACLqcy4Kf5eQr+ZbYRfrAFaXHaf/FRGUZAfE/Bzwf8OKwpuJ/0vYRNPEpavT248rACbqfimtLiqxT8w4yIz+Of5qTV7+LHMqyAaf9R8Clp8ex2K7iXuAa4q7R6aXCH4QXE4xVsOq+0+DYFfzXth/ilnrT6Tvzb9DADlqwqDPjTd377FHwI3EBaPb0NPFaGGRBvoLBiUVr8EFS+KgN8KpYtDjvgQo6i8Hhp8ZJUjnIZaXXGNrxKhh0QX6TwHzPS4uco/FtPWr0T9jnPKAJuOOiCc/UqVG4jrf4AH5dRBPT8UWHg3/BvUfhl635ZcNXRBMSDqFxFWvwklVu07O/BT2RUAad0GIWPtexXgMpPt+xntAt3Gl1AvJjCPufasn4d/6NyY2n4DPzXzCgDzmEvhVdIw+tj02X8nsIPGvus7Xi8jDIgPkLhSPON5ad4t7gbletJ9S71n+BIA65C5dGal1g3nENM+weF70j1R3iNjDogfkrhn6br5dU/4UXS94EtF7+vUC8IjjzgrlRuJ33v27j2dTLbKXxd+r4Pn5RRB9RfxsL3y4H/i0dI9TFUrirOZDeuPh4B8TQqVyjH/ZtZqZ7KIRS+JJ6Fn8k4BNRL9IUPmncwbr/1b37lanbiLuMTEG+nsNfb8cOWq0DLFA7E/8yMU8AlNLnaoP/alSfKOAXEN6h8Qlo8k3UKq5bGLeAWFPa6oLT6Ogqvk3ELmPJ3wOtlgOe0B2w6//gFxCPBsjPIQN8JPi3jGDDvSDxVDPaCNnFNOdGczrNzIrEnRyR5XPZlMIdmKofnpTnx6B783QV0ARNOF9AFdAFdQBfQBXQBXUAX0AV0AV3A6OgCVjLJrEzlj5lk/jiVt2eSefvEvzTJxL84zMS/PM//Af1pBRpQAkXBAAAAAElFTkSuQmCC + + + + + + + + + + + + + +https://pl.wikipedia.org/wiki/Specjalna:Szukaj + diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/pt-BR/default.txt b/mobile/ios/Client/Assets/Search/SearchPlugins/pt-BR/default.txt new file mode 100644 index 0000000000..ced95a5cb9 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/pt-BR/default.txt @@ -0,0 +1 @@ +Google \ No newline at end of file diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/pt-BR/list.txt b/mobile/ios/Client/Assets/Search/SearchPlugins/pt-BR/list.txt new file mode 100644 index 0000000000..543d604025 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/pt-BR/list.txt @@ -0,0 +1,5 @@ +bing +google +twitter +wikipedia-br +yahoo-br diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/pt-BR/wikipedia-br.xml b/mobile/ios/Client/Assets/Search/SearchPlugins/pt-BR/wikipedia-br.xml new file mode 100644 index 0000000000..70ead749f0 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/pt-BR/wikipedia-br.xml @@ -0,0 +1,23 @@ + + + +Wikipedia (pt) +UTF-8 +data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAQAAABIkb+zAAAEXElEQVR4Ae3cA4xs2QJG4b9aj41n27Zt27Zt27Zt27Zt+2KM5lXP924qOzs56VPjKSRnrfhv7NUsp08v98+PsxwT4nJ+nPunl8KZ8/WYQL+eMydJrx5/EhN603lAHpVJ5bzZNp035uyZXM7Uy3LmM7ms9CITzVRGQRfQBXQBXUAX0AV0AV1AF9AFdAFdQBfQBXQBXcBMjgU7M5+1vqtZyXqmc/IsZDEL+53LP3KRPCCv62+rWd7vnpx6v/M5Vd935OF5R+6RtRyeQ3NYNjKTxZw2p8tiduR8uWfemY3+dkjWM5fFLPU/8q9yrRwbHLP39WkHamPD5z1WnNMb/dpW/uAdbipu413+qcl2H3N3cXFv9DNNtvmg68uxMY6dPfewTZNvOauouqEdULlTY532IgqHua+ZxnoHm4ANj63biRNQPZedVPY5nTR1BXsVbNuyTlsDqy4tW/wK4NYiJ01AXMlm+wGrL1cDnWbLejC4m7T4DvAOOSkD4v0qztGyLzpUweOk4UXAp6XV72DNmU7qgPPao+DJ0uLTFPzXXGN5I9acU1o8vb14jZzUAfFeBdvMtuwLDlfwIKme2248T1p9MvY59zACLqcy4Kf5eQr+ZbYRfrAFaXHaf/FRGUZAfE/Bzwf8OKwpuJ/0vYRNPEpavT248rACbqfimtLiqxT8w4yIz+Of5qTV7+LHMqyAaf9R8Clp8ex2K7iXuAa4q7R6aXCH4QXE4xVsOq+0+DYFfzXth/ilnrT6Tvzb9DADlqwqDPjTd377FHwI3EBaPb0NPFaGGRBvoLBiUVr8EFS+KgN8KpYtDjvgQo6i8Hhp8ZJUjnIZaXXGNrxKhh0QX6TwHzPS4uco/FtPWr0T9jnPKAJuOOiCc/UqVG4jrf4AH5dRBPT8UWHg3/BvUfhl635ZcNXRBMSDqFxFWvwklVu07O/BT2RUAad0GIWPtexXgMpPt+xntAt3Gl1AvJjCPufasn4d/6NyY2n4DPzXzCgDzmEvhVdIw+tj02X8nsIPGvus7Xi8jDIgPkLhSPON5ad4t7gbletJ9S71n+BIA65C5dGal1g3nENM+weF70j1R3iNjDogfkrhn6br5dU/4UXS94EtF7+vUC8IjjzgrlRuJ33v27j2dTLbKXxd+r4Pn5RRB9RfxsL3y4H/i0dI9TFUrirOZDeuPh4B8TQqVyjH/ZtZqZ7KIRS+JJ6Fn8k4BNRL9IUPmncwbr/1b37lanbiLuMTEG+nsNfb8cOWq0DLFA7E/8yMU8AlNLnaoP/alSfKOAXEN6h8Qlo8k3UKq5bGLeAWFPa6oLT6Ogqvk3ELmPJ3wOtlgOe0B2w6//gFxCPBsjPIQN8JPi3jGDDvSDxVDPaCNnFNOdGczrNzIrEnRyR5XPZlMIdmKofnpTnx6B783QV0ARNOF9AFdAFdQBfQBXQBXUAX0AV0AV3A6OgCVjLJrEzlj5lk/jiVt2eSefvEvzTJxL84zMS/PM//Af1pBRpQAkXBAAAAAElFTkSuQmCC + + + + + + + + + + + + + +https://pt.wikipedia.org/wiki/Especial:Busca + diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/pt-BR/yahoo-br.xml b/mobile/ios/Client/Assets/Search/SearchPlugins/pt-BR/yahoo-br.xml new file mode 100644 index 0000000000..fe94c7ac56 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/pt-BR/yahoo-br.xml @@ -0,0 +1,22 @@ + + + +Yahoo +UTF-8 +data:image/png;base64,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 + + + + + + + + + + + + +https://br.search.yahoo.com/ + diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/pt-PT/default.txt b/mobile/ios/Client/Assets/Search/SearchPlugins/pt-PT/default.txt new file mode 100644 index 0000000000..ced95a5cb9 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/pt-PT/default.txt @@ -0,0 +1 @@ +Google \ No newline at end of file diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/pt-PT/list.txt b/mobile/ios/Client/Assets/Search/SearchPlugins/pt-PT/list.txt new file mode 100644 index 0000000000..32dcd4bfea --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/pt-PT/list.txt @@ -0,0 +1,2 @@ +google +wikipedia-ptpt diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/pt-PT/wikipedia-ptpt.xml b/mobile/ios/Client/Assets/Search/SearchPlugins/pt-PT/wikipedia-ptpt.xml new file mode 100644 index 0000000000..8896a4257e --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/pt-PT/wikipedia-ptpt.xml @@ -0,0 +1,23 @@ + + + +Wikipedia (pt) +UTF-8 +data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAQAAABIkb+zAAAEXElEQVR4Ae3cA4xs2QJG4b9aj41n27Zt27Zt27Zt27Zt+2KM5lXP924qOzs56VPjKSRnrfhv7NUsp08v98+PsxwT4nJ+nPunl8KZ8/WYQL+eMydJrx5/EhN603lAHpVJ5bzZNp035uyZXM7Uy3LmM7ms9CITzVRGQRfQBXQBXUAX0AV0AV1AF9AFdAFdQBfQBXQBXcBMjgU7M5+1vqtZyXqmc/IsZDEL+53LP3KRPCCv62+rWd7vnpx6v/M5Vd935OF5R+6RtRyeQ3NYNjKTxZw2p8tiduR8uWfemY3+dkjWM5fFLPU/8q9yrRwbHLP39WkHamPD5z1WnNMb/dpW/uAdbipu413+qcl2H3N3cXFv9DNNtvmg68uxMY6dPfewTZNvOauouqEdULlTY532IgqHua+ZxnoHm4ANj63biRNQPZedVPY5nTR1BXsVbNuyTlsDqy4tW/wK4NYiJ01AXMlm+wGrL1cDnWbLejC4m7T4DvAOOSkD4v0qztGyLzpUweOk4UXAp6XV72DNmU7qgPPao+DJ0uLTFPzXXGN5I9acU1o8vb14jZzUAfFeBdvMtuwLDlfwIKme2248T1p9MvY59zACLqcy4Kf5eQr+ZbYRfrAFaXHaf/FRGUZAfE/Bzwf8OKwpuJ/0vYRNPEpavT248rACbqfimtLiqxT8w4yIz+Of5qTV7+LHMqyAaf9R8Clp8ex2K7iXuAa4q7R6aXCH4QXE4xVsOq+0+DYFfzXth/ilnrT6Tvzb9DADlqwqDPjTd377FHwI3EBaPb0NPFaGGRBvoLBiUVr8EFS+KgN8KpYtDjvgQo6i8Hhp8ZJUjnIZaXXGNrxKhh0QX6TwHzPS4uco/FtPWr0T9jnPKAJuOOiCc/UqVG4jrf4AH5dRBPT8UWHg3/BvUfhl635ZcNXRBMSDqFxFWvwklVu07O/BT2RUAad0GIWPtexXgMpPt+xntAt3Gl1AvJjCPufasn4d/6NyY2n4DPzXzCgDzmEvhVdIw+tj02X8nsIPGvus7Xi8jDIgPkLhSPON5ad4t7gbletJ9S71n+BIA65C5dGal1g3nENM+weF70j1R3iNjDogfkrhn6br5dU/4UXS94EtF7+vUC8IjjzgrlRuJ33v27j2dTLbKXxd+r4Pn5RRB9RfxsL3y4H/i0dI9TFUrirOZDeuPh4B8TQqVyjH/ZtZqZ7KIRS+JJ6Fn8k4BNRL9IUPmncwbr/1b37lanbiLuMTEG+nsNfb8cOWq0DLFA7E/8yMU8AlNLnaoP/alSfKOAXEN6h8Qlo8k3UKq5bGLeAWFPa6oLT6Ogqvk3ELmPJ3wOtlgOe0B2w6//gFxCPBsjPIQN8JPi3jGDDvSDxVDPaCNnFNOdGczrNzIrEnRyR5XPZlMIdmKofnpTnx6B783QV0ARNOF9AFdAFdQBfQBXQBXUAX0AV0AV3A6OgCVjLJrEzlj5lk/jiVt2eSefvEvzTJxL84zMS/PM//Af1pBRpQAkXBAAAAAElFTkSuQmCC + + + + + + + + + + + + + +https://pt.wikipedia.org/wiki/Especial:Pesquisar + diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/rm/default.txt b/mobile/ios/Client/Assets/Search/SearchPlugins/rm/default.txt new file mode 100644 index 0000000000..ced95a5cb9 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/rm/default.txt @@ -0,0 +1 @@ +Google \ No newline at end of file diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/rm/leo_ende_de.xml b/mobile/ios/Client/Assets/Search/SearchPlugins/rm/leo_ende_de.xml new file mode 100644 index 0000000000..29d6917208 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/rm/leo_ende_de.xml @@ -0,0 +1,15 @@ + + + +LEO Eng-Tud +UTF-8 +data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAYAAADimHc4AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAA2hpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMC1jMDYxIDY0LjE0MDk0OSwgMjAxMC8xMi8wNy0xMDo1NzowMSAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtcE1NOk9yaWdpbmFsRG9jdW1lbnRJRD0ieG1wLmRpZDowMTgwMTE3NDA3MjA2ODExOEE2REI2ODhGMkQ2RTdCRSIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDpFMzIyRTc3MEI2Q0IxMUU1QkU5RkY4OTVGNjNGQUYyMyIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDpFMzIyRTc2RkI2Q0IxMUU1QkU5RkY4OTVGNjNGQUYyMyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ1M1LjEgTWFjaW50b3NoIj4gPHhtcE1NOkRlcml2ZWRGcm9tIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6MDI4MDExNzQwNzIwNjgxMTg3MUY5RTZERjU2QUVFQ0EiIHN0UmVmOmRvY3VtZW50SUQ9InhtcC5kaWQ6MDE4MDExNzQwNzIwNjgxMThBNkRCNjg4RjJENkU3QkUiLz4gPC9yZGY6RGVzY3Jp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 + + + + + +http://dict.leo.org + diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/rm/list.txt b/mobile/ios/Client/Assets/Search/SearchPlugins/rm/list.txt new file mode 100644 index 0000000000..07dedc32ef --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/rm/list.txt @@ -0,0 +1,7 @@ +bing +duckduckgo +google +leo_ende_de +pledarigrond +wikipedia-rm +yahoo-ch diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/rm/pledarigrond.xml b/mobile/ios/Client/Assets/Search/SearchPlugins/rm/pledarigrond.xml new file mode 100644 index 0000000000..eef1b941be --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/rm/pledarigrond.xml @@ -0,0 +1,11 @@ + + + +Pledari Grond +UTF-8 +data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAYAAADimHc4AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAA2hpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMC1jMDYxIDY0LjE0MDk0OSwgMjAxMC8xMi8wNy0xMDo1NzowMSAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtcE1NOk9yaWdpbmFsRG9jdW1lbnRJRD0ieG1wLmRpZDowMTgwMTE3NDA3MjA2ODExOEE2REI2ODhGMkQ2RTdCRSIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDo2QjU0QkNFNkI2Q0IxMUU1QTYzQUIxQzY5MzE4QzQ4OCIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDo2QjU0QkNFNUI2Q0IxMUU1QTYzQUIxQzY5MzE4QzQ4OCIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ1M1LjEgTWFjaW50b3NoIj4gPHhtcE1NOkRlcml2ZWRGcm9tIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6MDE4MDExNzQwNzIwNjgxMTkyQjBDOTk3OUU1NzBCODIiIHN0UmVmOmRvY3VtZW50SUQ9InhtcC5kaWQ6MDE4MDExNzQwNzIwNjgxMThBNkRCNjg4RjJENkU3QkUiLz4gPC9yZGY6RGVzY3Jp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 + +http://www.pledarigrond.ch/ + diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/rm/wikipedia-rm.xml b/mobile/ios/Client/Assets/Search/SearchPlugins/rm/wikipedia-rm.xml new file mode 100644 index 0000000000..64377a395b --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/rm/wikipedia-rm.xml @@ -0,0 +1,23 @@ + + + +Wikipedia (rm) +UTF-8 +data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAQAAABIkb+zAAAEXElEQVR4Ae3cA4xs2QJG4b9aj41n27Zt27Zt27Zt27Zt+2KM5lXP924qOzs56VPjKSRnrfhv7NUsp08v98+PsxwT4nJ+nPunl8KZ8/WYQL+eMydJrx5/EhN603lAHpVJ5bzZNp035uyZXM7Uy3LmM7ms9CITzVRGQRfQBXQBXUAX0AV0AV1AF9AFdAFdQBfQBXQBXcBMjgU7M5+1vqtZyXqmc/IsZDEL+53LP3KRPCCv62+rWd7vnpx6v/M5Vd935OF5R+6RtRyeQ3NYNjKTxZw2p8tiduR8uWfemY3+dkjWM5fFLPU/8q9yrRwbHLP39WkHamPD5z1WnNMb/dpW/uAdbipu413+qcl2H3N3cXFv9DNNtvmg68uxMY6dPfewTZNvOauouqEdULlTY532IgqHua+ZxnoHm4ANj63biRNQPZedVPY5nTR1BXsVbNuyTlsDqy4tW/wK4NYiJ01AXMlm+wGrL1cDnWbLejC4m7T4DvAOOSkD4v0qztGyLzpUweOk4UXAp6XV72DNmU7qgPPao+DJ0uLTFPzXXGN5I9acU1o8vb14jZzUAfFeBdvMtuwLDlfwIKme2248T1p9MvY59zACLqcy4Kf5eQr+ZbYRfrAFaXHaf/FRGUZAfE/Bzwf8OKwpuJ/0vYRNPEpavT248rACbqfimtLiqxT8w4yIz+Of5qTV7+LHMqyAaf9R8Clp8ex2K7iXuAa4q7R6aXCH4QXE4xVsOq+0+DYFfzXth/ilnrT6Tvzb9DADlqwqDPjTd377FHwI3EBaPb0NPFaGGRBvoLBiUVr8EFS+KgN8KpYtDjvgQo6i8Hhp8ZJUjnIZaXXGNrxKhh0QX6TwHzPS4uco/FtPWr0T9jnPKAJuOOiCc/UqVG4jrf4AH5dRBPT8UWHg3/BvUfhl635ZcNXRBMSDqFxFWvwklVu07O/BT2RUAad0GIWPtexXgMpPt+xntAt3Gl1AvJjCPufasn4d/6NyY2n4DPzXzCgDzmEvhVdIw+tj02X8nsIPGvus7Xi8jDIgPkLhSPON5ad4t7gbletJ9S71n+BIA65C5dGal1g3nENM+weF70j1R3iNjDogfkrhn6br5dU/4UXS94EtF7+vUC8IjjzgrlRuJ33v27j2dTLbKXxd+r4Pn5RRB9RfxsL3y4H/i0dI9TFUrirOZDeuPh4B8TQqVyjH/ZtZqZ7KIRS+JJ6Fn8k4BNRL9IUPmncwbr/1b37lanbiLuMTEG+nsNfb8cOWq0DLFA7E/8yMU8AlNLnaoP/alSfKOAXEN6h8Qlo8k3UKq5bGLeAWFPa6oLT6Ogqvk3ELmPJ3wOtlgOe0B2w6//gFxCPBsjPIQN8JPi3jGDDvSDxVDPaCNnFNOdGczrNzIrEnRyR5XPZlMIdmKofnpTnx6B783QV0ARNOF9AFdAFdQBfQBXQBXUAX0AV0AV3A6OgCVjLJrEzlj5lk/jiVt2eSefvEvzTJxL84zMS/PM//Af1pBRpQAkXBAAAAAElFTkSuQmCC + + + + + + + + + + + + + +https://rm.wikipedia.org/wiki/Spezial:Search + diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/rm/yahoo-ch.xml b/mobile/ios/Client/Assets/Search/SearchPlugins/rm/yahoo-ch.xml new file mode 100644 index 0000000000..479089b989 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/rm/yahoo-ch.xml @@ -0,0 +1,21 @@ + + + +Yahoo +UTF-8 +data:image/png;base64,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 + + + + + + + + + + + +https://ch.search.yahoo.com/ + diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/ro/default.txt b/mobile/ios/Client/Assets/Search/SearchPlugins/ro/default.txt new file mode 100644 index 0000000000..ced95a5cb9 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/ro/default.txt @@ -0,0 +1 @@ +Google \ No newline at end of file diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/ro/list.txt b/mobile/ios/Client/Assets/Search/SearchPlugins/ro/list.txt new file mode 100644 index 0000000000..116691e2b6 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/ro/list.txt @@ -0,0 +1,3 @@ +google +twitter +wikipedia-ro diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/ro/wikipedia-ro.xml b/mobile/ios/Client/Assets/Search/SearchPlugins/ro/wikipedia-ro.xml new file mode 100644 index 0000000000..2e1f73079e --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/ro/wikipedia-ro.xml @@ -0,0 +1,23 @@ + + + +Wikipedia (ro) +UTF-8 +data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAQAAABIkb+zAAAEXElEQVR4Ae3cA4xs2QJG4b9aj41n27Zt27Zt27Zt27Zt+2KM5lXP924qOzs56VPjKSRnrfhv7NUsp08v98+PsxwT4nJ+nPunl8KZ8/WYQL+eMydJrx5/EhN603lAHpVJ5bzZNp035uyZXM7Uy3LmM7ms9CITzVRGQRfQBXQBXUAX0AV0AV1AF9AFdAFdQBfQBXQBXcBMjgU7M5+1vqtZyXqmc/IsZDEL+53LP3KRPCCv62+rWd7vnpx6v/M5Vd935OF5R+6RtRyeQ3NYNjKTxZw2p8tiduR8uWfemY3+dkjWM5fFLPU/8q9yrRwbHLP39WkHamPD5z1WnNMb/dpW/uAdbipu413+qcl2H3N3cXFv9DNNtvmg68uxMY6dPfewTZNvOauouqEdULlTY532IgqHua+ZxnoHm4ANj63biRNQPZedVPY5nTR1BXsVbNuyTlsDqy4tW/wK4NYiJ01AXMlm+wGrL1cDnWbLejC4m7T4DvAOOSkD4v0qztGyLzpUweOk4UXAp6XV72DNmU7qgPPao+DJ0uLTFPzXXGN5I9acU1o8vb14jZzUAfFeBdvMtuwLDlfwIKme2248T1p9MvY59zACLqcy4Kf5eQr+ZbYRfrAFaXHaf/FRGUZAfE/Bzwf8OKwpuJ/0vYRNPEpavT248rACbqfimtLiqxT8w4yIz+Of5qTV7+LHMqyAaf9R8Clp8ex2K7iXuAa4q7R6aXCH4QXE4xVsOq+0+DYFfzXth/ilnrT6Tvzb9DADlqwqDPjTd377FHwI3EBaPb0NPFaGGRBvoLBiUVr8EFS+KgN8KpYtDjvgQo6i8Hhp8ZJUjnIZaXXGNrxKhh0QX6TwHzPS4uco/FtPWr0T9jnPKAJuOOiCc/UqVG4jrf4AH5dRBPT8UWHg3/BvUfhl635ZcNXRBMSDqFxFWvwklVu07O/BT2RUAad0GIWPtexXgMpPt+xntAt3Gl1AvJjCPufasn4d/6NyY2n4DPzXzCgDzmEvhVdIw+tj02X8nsIPGvus7Xi8jDIgPkLhSPON5ad4t7gbletJ9S71n+BIA65C5dGal1g3nENM+weF70j1R3iNjDogfkrhn6br5dU/4UXS94EtF7+vUC8IjjzgrlRuJ33v27j2dTLbKXxd+r4Pn5RRB9RfxsL3y4H/i0dI9TFUrirOZDeuPh4B8TQqVyjH/ZtZqZ7KIRS+JJ6Fn8k4BNRL9IUPmncwbr/1b37lanbiLuMTEG+nsNfb8cOWq0DLFA7E/8yMU8AlNLnaoP/alSfKOAXEN6h8Qlo8k3UKq5bGLeAWFPa6oLT6Ogqvk3ELmPJ3wOtlgOe0B2w6//gFxCPBsjPIQN8JPi3jGDDvSDxVDPaCNnFNOdGczrNzIrEnRyR5XPZlMIdmKofnpTnx6B783QV0ARNOF9AFdAFdQBfQBXQBXUAX0AV0AV3A6OgCVjLJrEzlj5lk/jiVt2eSefvEvzTJxL84zMS/PM//Af1pBRpQAkXBAAAAAElFTkSuQmCC + + + + + + + + + + + + + +https://ro.wikipedia.org/wiki/Special:Căutare + diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/ru/default.txt b/mobile/ios/Client/Assets/Search/SearchPlugins/ru/default.txt new file mode 100644 index 0000000000..daaaf0e061 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/ru/default.txt @@ -0,0 +1 @@ +Яндекс \ No newline at end of file diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/ru/list.txt b/mobile/ios/Client/Assets/Search/SearchPlugins/ru/list.txt new file mode 100644 index 0000000000..6ce1a87b51 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/ru/list.txt @@ -0,0 +1,5 @@ +google:hidden +google-nocodes +twitter +wikipedia-ru +yandex-ru diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/ru/wikipedia-ru.xml b/mobile/ios/Client/Assets/Search/SearchPlugins/ru/wikipedia-ru.xml new file mode 100644 index 0000000000..4988bdd954 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/ru/wikipedia-ru.xml @@ -0,0 +1,23 @@ + + + +Википедия (ru) +UTF-8 +data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAQAAABIkb+zAAAEXElEQVR4Ae3cA4xs2QJG4b9aj41n27Zt27Zt27Zt27Zt+2KM5lXP924qOzs56VPjKSRnrfhv7NUsp08v98+PsxwT4nJ+nPunl8KZ8/WYQL+eMydJrx5/EhN603lAHpVJ5bzZNp035uyZXM7Uy3LmM7ms9CITzVRGQRfQBXQBXUAX0AV0AV1AF9AFdAFdQBfQBXQBXcBMjgU7M5+1vqtZyXqmc/IsZDEL+53LP3KRPCCv62+rWd7vnpx6v/M5Vd935OF5R+6RtRyeQ3NYNjKTxZw2p8tiduR8uWfemY3+dkjWM5fFLPU/8q9yrRwbHLP39WkHamPD5z1WnNMb/dpW/uAdbipu413+qcl2H3N3cXFv9DNNtvmg68uxMY6dPfewTZNvOauouqEdULlTY532IgqHua+ZxnoHm4ANj63biRNQPZedVPY5nTR1BXsVbNuyTlsDqy4tW/wK4NYiJ01AXMlm+wGrL1cDnWbLejC4m7T4DvAOOSkD4v0qztGyLzpUweOk4UXAp6XV72DNmU7qgPPao+DJ0uLTFPzXXGN5I9acU1o8vb14jZzUAfFeBdvMtuwLDlfwIKme2248T1p9MvY59zACLqcy4Kf5eQr+ZbYRfrAFaXHaf/FRGUZAfE/Bzwf8OKwpuJ/0vYRNPEpavT248rACbqfimtLiqxT8w4yIz+Of5qTV7+LHMqyAaf9R8Clp8ex2K7iXuAa4q7R6aXCH4QXE4xVsOq+0+DYFfzXth/ilnrT6Tvzb9DADlqwqDPjTd377FHwI3EBaPb0NPFaGGRBvoLBiUVr8EFS+KgN8KpYtDjvgQo6i8Hhp8ZJUjnIZaXXGNrxKhh0QX6TwHzPS4uco/FtPWr0T9jnPKAJuOOiCc/UqVG4jrf4AH5dRBPT8UWHg3/BvUfhl635ZcNXRBMSDqFxFWvwklVu07O/BT2RUAad0GIWPtexXgMpPt+xntAt3Gl1AvJjCPufasn4d/6NyY2n4DPzXzCgDzmEvhVdIw+tj02X8nsIPGvus7Xi8jDIgPkLhSPON5ad4t7gbletJ9S71n+BIA65C5dGal1g3nENM+weF70j1R3iNjDogfkrhn6br5dU/4UXS94EtF7+vUC8IjjzgrlRuJ33v27j2dTLbKXxd+r4Pn5RRB9RfxsL3y4H/i0dI9TFUrirOZDeuPh4B8TQqVyjH/ZtZqZ7KIRS+JJ6Fn8k4BNRL9IUPmncwbr/1b37lanbiLuMTEG+nsNfb8cOWq0DLFA7E/8yMU8AlNLnaoP/alSfKOAXEN6h8Qlo8k3UKq5bGLeAWFPa6oLT6Ogqvk3ELmPJ3wOtlgOe0B2w6//gFxCPBsjPIQN8JPi3jGDDvSDxVDPaCNnFNOdGczrNzIrEnRyR5XPZlMIdmKofnpTnx6B783QV0ARNOF9AFdAFdQBfQBXQBXUAX0AV0AV3A6OgCVjLJrEzlj5lk/jiVt2eSefvEvzTJxL84zMS/PM//Af1pBRpQAkXBAAAAAElFTkSuQmCC + + + + + + + + + + + + + +https://ru.wikipedia.org/wiki/Служебная:Поиск + diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/ru/yandex-ru.xml b/mobile/ios/Client/Assets/Search/SearchPlugins/ru/yandex-ru.xml new file mode 100644 index 0000000000..bab34585e4 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/ru/yandex-ru.xml @@ -0,0 +1,21 @@ + + + +Яндекс +UTF-8 +data:image/png;base64,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 + + + + + + + + + + + +https://www.yandex.ru/ + diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/sk/azet-sk.xml b/mobile/ios/Client/Assets/Search/SearchPlugins/sk/azet-sk.xml new file mode 100644 index 0000000000..bf83bc2ba5 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/sk/azet-sk.xml @@ -0,0 +1,14 @@ + + + +Azet.sk - firmy +UTF-8 +data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAYAAADimHc4AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAA2hpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMC1jMDYxIDY0LjE0MDk0OSwgMjAxMC8xMi8wNy0xMDo1NzowMSAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtcE1NOk9yaWdpbmFsRG9jdW1lbnRJRD0ieG1wLmRpZDowMTgwMTE3NDA3MjA2ODExOEE2REI2ODhGMkQ2RTdCRSIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDo1MDc5QUNFRkUyMkIxMUU1QjE0QUUxNkJFNEIwNTQwQiIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDo1MDc5QUNFRUUyMkIxMUU1QjE0QUUxNkJFNEIwNTQwQiIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ1M1LjEgTWFjaW50b3NoIj4gPHhtcE1NOkRlcml2ZWRGcm9tIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6RkI3RjExNzQwNzIwNjgxMUI2MEJEOTMwMkE4QTkwQjMiIHN0UmVmOmRvY3VtZW50SUQ9InhtcC5kaWQ6MDE4MDExNzQwNzIwNjgxMThBNkRCNjg4RjJENkU3QkUiLz4gPC9yZGY6RGVzY3Jp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 + + + + +http://www.azet.sk/katalog/ + diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/sk/default.txt b/mobile/ios/Client/Assets/Search/SearchPlugins/sk/default.txt new file mode 100644 index 0000000000..ced95a5cb9 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/sk/default.txt @@ -0,0 +1 @@ +Google \ No newline at end of file diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/sk/list.txt b/mobile/ios/Client/Assets/Search/SearchPlugins/sk/list.txt new file mode 100644 index 0000000000..97d5f9dfeb --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/sk/list.txt @@ -0,0 +1,5 @@ +azet-sk +google +slovnik-sk +twitter +wikipedia-sk diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/sk/slovnik-sk.xml b/mobile/ios/Client/Assets/Search/SearchPlugins/sk/slovnik-sk.xml new file mode 100644 index 0000000000..a923c7570b --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/sk/slovnik-sk.xml @@ -0,0 +1,15 @@ + + + +Slovnik.sk +UTF-8 +data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAYAAADimHc4AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAA2hpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMC1jMDYxIDY0LjE0MDk0OSwgMjAxMC8xMi8wNy0xMDo1NzowMSAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtcE1NOk9yaWdpbmFsRG9jdW1lbnRJRD0ieG1wLmRpZDowMTgwMTE3NDA3MjA2ODExOEE2REI2ODhGMkQ2RTdCRSIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDo1MDc5QUNGM0UyMkIxMUU1QjE0QUUxNkJFNEIwNTQwQiIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDo1MDc5QUNGMkUyMkIxMUU1QjE0QUUxNkJFNEIwNTQwQiIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ1M1LjEgTWFjaW50b3NoIj4gPHhtcE1NOkRlcml2ZWRGcm9tIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6RkM3RjExNzQwNzIwNjgxMUI2MEJEOTMwMkE4QTkwQjMiIHN0UmVmOmRvY3VtZW50SUQ9InhtcC5kaWQ6MDE4MDExNzQwNzIwNjgxMThBNkRCNjg4RjJENkU3QkUiLz4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+IDw/eHBhY2tldCBlbmQ9InIiPz6h3RNCAAARi0lEQVR42uxda3Bc9XX//e+9+15Ju3ralmU9/BC2bANWDIEQY4ZAGjupm6SBMm0z6ZB+oPmSmX7qtP3Spu2k03zppKSdSZomk0kmFMKj4AkxhJeNMZjYGFsPG0dvW9KuLGnfj/voOf+7KwsCxiv7riTnnpm/7mpfuvf8zvuc/5UoFAooUZjWD2jdRysKl5ygWVqHaD1MK8VPiBIAvbReoxVweVQVytL6NK23GQCW/Bgtv8uXqlKOVpNSMjsu86tPzPMfsAawXYq4/FgWmmMALJcPy0eKywIXABcAl1wAXABccgFwAXDJBcAFwCUXABcAl1wAXABcqg5pq+lkxVW+z3IBuEZGi8vMFiWGmpaFvCHoCOim+FAwFGFBJZ3m5VHe/3kJjLXywFlRAGiKzfyCDqSLwHRaw2QaGJqzcCEpMJ0CZrMWLft9i8kwgVo/0BQSaA4B0QDQGQFaa4CGgEG/WxIUj2ojopsrA4wV0ZCR0kqMmcsIDM4IvDWhoH8GuDBPK0lSLWxwFMV2WuIjbBFLuMnaQj+MEpPDXqCDgNhQB3Q1CGxvNOmxhYawrQ66vrxALCsACjFSJR1M5hQcmxA4eE7BqUkTybxtOnz0mld9vxmpxF8wULoBMl1ATrelv9YHdDcJ7G038YlWBsaUqBXN3zMAlJIUvzam4pkBgRMXLWTI7PhUm/GixPXrcXKi9IPNVNGwmc3f31VvA7F/C7A2bEqwrN8HANicJIsqfvIO8OQAawAQ8JR8gMNRzGWnbmsFU0cU+LOdAvd1GtKRG9YNDACbgfGEgu8dF3hpyLb/bGaWixiIrG6fx5e3CfzFLQb5DUv6jxsOAGb+dFrBPx9W8PqoJR2kKrDsxKfApilPTP98t8A3bzPIDFoSnBsmE+bYPKcL/OiUiqPE/JoVwvxy4qaRcATJDB4ctPD0oAqB6pycUk0pe5ainKf6THmhK4X5HzxHFpSfn7ZwblaRj28IADjiGZqznS4/Znt7rdptURhjCeV9C9dBajkQiFGid2zckufqtJxo1QLg4DmBWNqS0r8k5jOTJeN5UXJm6FAsc5EZUWBQUsFAyByAQ02rck9aLoNMkq8q6FzasGSCt2oBYObHMgqOX7CWbHZMSoEVkxxjYQbBuX54k0Pw5GeIUcZCYGkJDwzFi2JoPXJ13UjXdMJUfUsCQhDHU6ks5Q0aAaCsbg1gO9ofF5hI2Oq9FOb78rOIjB9EcOoonXABmllAIBiEx+Ml6bQWPGkhl0Zu5jiK8CAd2Y70hs8iVdsNUzCAVwcCw2nSz1w2A1MPQHj8cFIFnDdBdEXj85zlCgQq/GtsTjQjh4bB/0Y0cQLrO7vR3rUZLS1rEa2PwqN5FgAgHSCmZRGLxTA8dB7n+09h9uS7CHbsw6XW+1HUQlCMjweh/G21SgZeoRIYAUdTQ0cBYHvKghfLCLu0ICpDzqLQMDT5BjYFZ3HP5x/Gtu07UFdXR5LvueInk8kUxsbH8OqLz+PUO0+SyRpGvOsBZAOtH6sJDLpXn8VacxiKuuuyhq1KADjBoZRyPm2QXfVUhgAhxjliVMvhTx58AN09O+XThmGgWCxe4WMC4XAI27ZuRfuGdrz55lEcevYpqH2PIr7pq0iRf1AkBuaHMp/ZvS7+MjZ0eqD6wnT++uoNQ5kZum4gnU6Ru6wstWHJ0ykK6ereQczogmXajDc/ToLl53T53kDAj71778VXv/5X2Fgv0Hzmu4hOHYalGBQxKeSkFelj5JJBv47GsefQnj6K7m3bZRPI8bqY41lmqQ0lKtYeCxqFMKPmWiQp2AkQ00SF1UrWFvqJLd3d+NrXH8EvHn8Mp/t+DN/8IHKNvcgHWqCTH9GKOrzZCwhNH0PN3AnsPfBHWLeurfT5VVwLUikGzWTz+NahDA7HG2TFsxIySl0rrs98aauJznr7CavUcKlEQDVNQyKRxK9+eRCvvnxIBgXwhilC8kE1cxDFJMJegbvv24fP7f+CDD+tKmiAowAodBGFbBr/9EIaL8WaCYDKKyxcEMuTGW6PArevs3DrOmBbk0DEZ0JV7RYYg3E1hTN23myeTp48gTOnTmBq8qJ02MFgAK3rN+DmXbvR09MjTadpVqcc6igAggAQ+QS+80Icz0y2w+/1SNNSuRkDcoYt+WEf0BYR2N5kYXODhfW1Am11Fur9BoEgsCgt+NCGDguFqqrIUpyfTmelr9A0FaFwGMFAQJqdajHfcR/AKqz5/GgPZeHR07C80SUBwCEs5xD8SdaGgZiF/mlO7ATqKUxvqRFYG9awMSqwowXoqNNl6durXE7+rJKWMHN5eb0++P0BKe18nvzclaKr1emE+aq1ANb6SM1zF5ANRa8pp2HzxYz1lCScv342B8QzwOkp4BXFglfjRryKTfVAT7OCzeQ3WmtMCVTIK+sMUpP0EhDLTY5HQSaJXVtzHTbofejTt5L6X5+mY7npXq6ulokbK5eywOtjwEtDpuwvt0dUdBH2veQ/tkQNqTHRoK0SRWN5AXC8I8YqzsnM9/7r+3hB34Ncy06oVer3iVK6xb3fbAEIkgY0BAmEBoFPtVnYRU69LUI2XxdV7QNXPQ/w+Xy4a/fN6H/qIEbru6Cr4asujl3T3y6BENTsbhdLeywNTCYtvDUBrK9TcG8nsG8z0Bg0Zdmk2jhUpSFjUmSx/ZZbsWONgob3/peYYsrss1pU9hfskEMeO5JiGp618MOTAt/8pcBz76koWsqSKrYrHgDWAr8/iP1fOIDO4ik0Dv5IRkWc/tudLFQVDNYKZrSfNINSE4zNA/92GPj2EQUXU0I6enEjAWA7Y8pkN27B/gNfRvOlI2g8812EkiMgoYOuKbLFuCxOsBTianR8/pyFf3xVRV9MUG5wgzjhDyZBTMeOHcP/Pf5TXEgCha59SDT0Ihtoke1G1VhaK/F6UbpgD2r93R6TMm7no6SqD2aVM9Ezff14/rmnMDgwCNS1I9WwC8loDzKhTWSa6H2GbTCEVf3wJJHnkFXgH+4x0BBwdkhrWUYTOTRlEObm5nD8rTdx5OUXEYtPo+BrgB5qRa6mC6noLuQDa8hEeWCWzJOwsAgQ506bQ9IsJcUP7RT4xm5T1kCcGtJa1uloBoH5OTc/i7eOvYG+06dxYWIEqUyetMCPoq8ZuYadyNd0oOhtpN8jKHpCC5MPssy9AMr1vQzOHaJBgW/dY2LnWi5T3IAAfFAj0ukMRkeGcH5oCGf7+xCbmkQmnUQxn0Pe3wwzuAbFcBvy4U7kQ20ESJjC2QDlFV7bvFllUK7dZjCmSWL6n+5U8EivDtWhod1lA6BcSli8bagMBB/TqRTi8TjeO/9bTIyPYiYew8z0BGYvzaAIH0wtBNPPJmstcrVbUOCjvxUFb2iRdizdh/B38Lj8hqjAv37GwPpaZxxy1QGQI+jkZC05py/gVayF2f3FdrbsrMvWPjGfQHwmjumpKYyPDmNk+LeYiU0hl00jm8mhqIVhRTaiUNuBbHgjMjWbUPDUyTBXNSo3UQwAl8A5i/72/WSGmp1xxlUDQEimCpy/JHBySsgQlId1o34TbXUCvWRnm0Nkaw3xkSZKlJxxvlBAijQknUrLpsr42CgmL05gfOQ90pBLMDy1EDXrkKq9SYa4mVBXxREV/6UCCQi3iv9+D7Cn3ZCFRWs1AsAXw9LzeL+GJ/stTKYuJ0HMEz5urgf+stfCnW3m72jDh4WyvBgQbqAYlOTlcznMzMwQEBdxdrAPA31nJEB54Ues80HMNd8JxaxME2Qhr2DikdsEHuwxHdk8UhUAuFz8WJ+G/3jTnrNc2JCxaBsSRx0NFHX89Z0m7u4gEAxcdehX1ozykacZkokETr97CsePHsZgTMfI5m9ADzRV5KBJZ5AkR/C1m4t4eLdHavD1Tkucnw0l5o+nFDw9YKuvT/td9WC2cZFsPmfhO68r0tnd02mBgtSrijwWpuMWcae2thZ77t6L3t5P4IknnsDEbAzFYJMMWysKFMhZqUauxKrrXy5xvBbENvTIKDA8Z7cIr1Qk4w16vA/4X14TeKKPbb6y5IFeBoNbjNzr/ey9e9BYF0SxwhkriyRegQ6tkHBsPtTx0UR2qmMJBVczHMSXyBVKDv/+87glR84f6CFNMJceg5tGEYVgK4Wr9OeTlTlh9lteTaA+aMnNe05sIXR2Mo7TerqKdM5Y2JZ6NcTNE2b4o+Qz/v2YilTRLhErFWoDv1+hMPfIhAfjaQ/5osqYxz6Iw2Ru1qjCGSVwfDTRIAlMJ+ckQyvhH5srZuDP37Xwt79W8c6kIhni0WynLneviMsbKsqPZY9Ytd/H45DPDNo7c2RSVsEJWKWaUF1hAg1++qyiOVJ/cnYshRjgQR4hkZOMsSp0Y5y0aV7ITdx/Q/nDvi0q7qIwtaPOvi/Egn8Q5XTaHsGOkx8ZmRX41ZCK58+Z0h9UuhWW602KoaOlOIRITQ8hS6wy9dUGAJsAFWvMcYok6mF5gkuq03A2mikAPzkJPDugYFsLsCYs0BS0GRv1235jPs+lZAVDsxYG4kAyX9oQuIQ9aSZn0MkR9ESTqKutcWxQ11kAKEHSfDVYr00hMD+IQuDWJWkxf4QZzaaFywNHRrh0YUmHbZsc+zY2ObIZBfI5/L6AZt+oYymFa9mdI9PZMncMN+9ei2BNhCIoZzozzrckKRFo39CGjsRLUAupJTfjrUVJXZ0PctCKcwre38tg8MRdSLOfr/XiGprrBCahGrjwBnpDI9jWs51MmHAsDHUcAJ1i8c039eDmtRqaRx6XOxuvdSKiXFCQd1spOV1VuXwbm2thlUFf6E+Mon3mED55+22I1DdB150bWaxKU55Hwz9z3/1Yn30HDed/CtUqyA0SK4uEPCd/NoaW9/4Ht21bj9vv/LTjewSqMxdEvmDT5m4c+OKX0BJ/BY1930cgF4ehKVWdD/poh6vQuQiEk8NoPPMotjZZ2PeHX5SC4/T8aNVuWcYXcgdJVLGg4xeP/RjeU2NIbfwKEpEdKGoeWbNfjgY8Sz3nZ9HpNxA99zO0N/rwlYceQXNTU1Wmpas+lsJ1/ZMnfoOnH/8ZJuIJFJtuwXzTHUhGtsOgWFs1nG+6yw1QPJZOyhdIjaH+4osITr6GTZ0d+OOH/hwdHR1VG1VflrEUXly35+1Cp35zDBnTi0ztZiQbP4lMdAdJZWDBNJWrl0ufhhALQ1+WuFzh9GUmUTf1CgLxEwgYc7jjU3fhD/YfQH00InfRWFXSxuW7ZRnbXXJwZ8+exeFXX8bZgT6ksgXovnrk6neiEL0JuUArit46FDmBW4jRsTAN8b4LsUqvLX7fotcUswhffgb+9Bgx/W14L51GgDL09o4u3HPvfbj11l0LprKqrn+5pyJ43xare39/P9499Q4GB/qRuDSFbDaPnK8ZiHTACLciT2DoHgLDW0PHgLw3RLn0YCpeGU8IMy8Hf5lUIw2tkJGl5GBmBEp2Gsqls/CkJ1BTE8a6ti7s2r0bu3b1IhKJLMvumBUBQLloxxEH7ymenJzE2NgohoeGcHF8CNMTo0gm5qFz012lDEzliYggTG8t8VyVd1HRPRF5VAvzJO0FuT1fpShLGFlaRSjFJHweDWvaOtDetQXdN23Fxo0b5a57lvhqbEdd0QB80EnL6ISYMp9IYGYmjnhsBhNjw5idmUZsegqpVFKWue07RNi78S2LSxDqwvyp1+dFY1Mz6usbsKFrM5roMa8o2XgGu7yhe9mFbyX+Jz3WCF7lxjtTPp8nM8E74AuyCc9zQ7bZsJAiDSkUDfs+El6vNGt8NxUvHZnZ/kCAsubLm/FWwt6wFQ3AlUARi4r6iyOV8nRF+b2LX+ejZa3My1w1d0+/GiZaq/C/Yrr/P8AFwAXAJRcAFwCXXABcAFxyAXABcKnK+SUDkHD5sGyUZAAOuXxYNjrExbgwPYjR8rv8qCrxro8m1gDesXVX6QmXqsd85nmq7ITfZjRoPUFr1uWPYzRX4nFTief4fwEGAHBg1fZNXOIsAAAAAElFTkSuQmCC + + + + + +http://slovnik.azet.sk/ + diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/sk/wikipedia-sk.xml b/mobile/ios/Client/Assets/Search/SearchPlugins/sk/wikipedia-sk.xml new file mode 100644 index 0000000000..2877773a9d --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/sk/wikipedia-sk.xml @@ -0,0 +1,23 @@ + + + +Wikipédia (sk) +UTF-8 +data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAQAAABIkb+zAAAEXElEQVR4Ae3cA4xs2QJG4b9aj41n27Zt27Zt27Zt27Zt+2KM5lXP924qOzs56VPjKSRnrfhv7NUsp08v98+PsxwT4nJ+nPunl8KZ8/WYQL+eMydJrx5/EhN603lAHpVJ5bzZNp035uyZXM7Uy3LmM7ms9CITzVRGQRfQBXQBXUAX0AV0AV1AF9AFdAFdQBfQBXQBXcBMjgU7M5+1vqtZyXqmc/IsZDEL+53LP3KRPCCv62+rWd7vnpx6v/M5Vd935OF5R+6RtRyeQ3NYNjKTxZw2p8tiduR8uWfemY3+dkjWM5fFLPU/8q9yrRwbHLP39WkHamPD5z1WnNMb/dpW/uAdbipu413+qcl2H3N3cXFv9DNNtvmg68uxMY6dPfewTZNvOauouqEdULlTY532IgqHua+ZxnoHm4ANj63biRNQPZedVPY5nTR1BXsVbNuyTlsDqy4tW/wK4NYiJ01AXMlm+wGrL1cDnWbLejC4m7T4DvAOOSkD4v0qztGyLzpUweOk4UXAp6XV72DNmU7qgPPao+DJ0uLTFPzXXGN5I9acU1o8vb14jZzUAfFeBdvMtuwLDlfwIKme2248T1p9MvY59zACLqcy4Kf5eQr+ZbYRfrAFaXHaf/FRGUZAfE/Bzwf8OKwpuJ/0vYRNPEpavT248rACbqfimtLiqxT8w4yIz+Of5qTV7+LHMqyAaf9R8Clp8ex2K7iXuAa4q7R6aXCH4QXE4xVsOq+0+DYFfzXth/ilnrT6Tvzb9DADlqwqDPjTd377FHwI3EBaPb0NPFaGGRBvoLBiUVr8EFS+KgN8KpYtDjvgQo6i8Hhp8ZJUjnIZaXXGNrxKhh0QX6TwHzPS4uco/FtPWr0T9jnPKAJuOOiCc/UqVG4jrf4AH5dRBPT8UWHg3/BvUfhl635ZcNXRBMSDqFxFWvwklVu07O/BT2RUAad0GIWPtexXgMpPt+xntAt3Gl1AvJjCPufasn4d/6NyY2n4DPzXzCgDzmEvhVdIw+tj02X8nsIPGvus7Xi8jDIgPkLhSPON5ad4t7gbletJ9S71n+BIA65C5dGal1g3nENM+weF70j1R3iNjDogfkrhn6br5dU/4UXS94EtF7+vUC8IjjzgrlRuJ33v27j2dTLbKXxd+r4Pn5RRB9RfxsL3y4H/i0dI9TFUrirOZDeuPh4B8TQqVyjH/ZtZqZ7KIRS+JJ6Fn8k4BNRL9IUPmncwbr/1b37lanbiLuMTEG+nsNfb8cOWq0DLFA7E/8yMU8AlNLnaoP/alSfKOAXEN6h8Qlo8k3UKq5bGLeAWFPa6oLT6Ogqvk3ELmPJ3wOtlgOe0B2w6//gFxCPBsjPIQN8JPi3jGDDvSDxVDPaCNnFNOdGczrNzIrEnRyR5XPZlMIdmKofnpTnx6B783QV0ARNOF9AFdAFdQBfQBXQBXUAX0AV0AV3A6OgCVjLJrEzlj5lk/jiVt2eSefvEvzTJxL84zMS/PM//Af1pBRpQAkXBAAAAAElFTkSuQmCC + + + + + + + + + + + + + +https://sk.wikipedia.org/wiki/Špeciálne:Hľadanie + diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/sl/ceneje.xml b/mobile/ios/Client/Assets/Search/SearchPlugins/sl/ceneje.xml new file mode 100644 index 0000000000..d1ef5e30a2 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/sl/ceneje.xml @@ -0,0 +1,15 @@ + + + +m.Ceneje.si +Mobilni iskalnik Ceneje.si +UTF-8 +data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAYAAADimHc4AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAABJ9JREFUeNrsnb9rFEEUx9/M7p3BSEyh+AsxhTZRNEEttJGAhVqJoP4FgqWgtb8aGwvtBBFsRZR0ChZiYyGiEfyB2EQEEbS4qBE1tzO+2dsLuejtnLgzs7d+H7zs3eXgbr+ffW/fmx9JTJn9vLl6hA9n2A+yDxPMhTXYJ9nP1Q9/mDYviEz8MT7ch/BeQUwwhCmRXflPIX4QCOMySzsQ378Zzc/ILOfDwthBias/bBRIaBDWAAAAAAAGAAAAAwAAgAEAAMAAAABgAAAAMAAAABgAAAAMAKptse8PTOprKBlYS0KIzKl1pPRB+pxaz2jBD382N0O68aKaAMTwZvq+9Rp9+SGpFscU12oURRHF7JLdPJZSznsbkveL5PEJUtM3qpWCjPjxnttE9eWlTwvRjkskR45WB8C8+LWhvsnNviBIiB8WgoT4YSFIiB8WgoT4YSFIiB8WQuF9gE38WM1S/fOTtOaPYnZT82f1v2An41z761aHlh59dgKz8QZqisHub9h4lgY+TVH09XVJGzHLlT+YvKUlr47nvkdnHsJmR6/Q3ND2/FOUyyjq16GI0o/NcDTqOF+WIrtzAFic5wEgrEnZGpsCgGARwMWANQIIANxFQFaN5QEgRIBTANqSgggpyC0AskQAIQW5s/QGawGgkYJ6EHJwPYm1+0jUlpP+9q7nGa50Fs4CQBXYKFYSgGTho52XO7pyufEYNR8cIpr7bAdgy/EFpqDqrYpg0ReLn2o2vJmisfO9hMCCBQN/9iIJVA6AGN7SdTxKbrCPZPpeAoB1QaHTZeXOiG+4XauXjw+rD0BrbXG3J6RnueJ5c/X3X/DNN3l2unQAYhcA8mto9yP9Rmg984Jz/pHWZ35rQfG54q28AJSfqRZT9/ta3VYqAEopGyHceUMCEFqRgO4OASRJPgAGFEF3dwASSwRIAHAMoJkfATpRVIPu7gA0k2b+B6oEqruNgHwAIgEApwDmbACaAOAUQGxZURANjVA0esrbCbY777RB1J1DJYpLYtMYmtK57QPRShqwXmUz5QWw4t2FdBFrd1vFvtXrVdYxFqVU6gmnQuOmbG4a58g1LuRgLgCzf6zIIY3CB+NM+2++ZBXNxeY9J8PRVYTgaueks/mAKkFwuW3V6YRMFSC43jPsfEasnyH42LDtZUqyHyF0E99sUYoPPEq9iHLa27qg9snkl6jlFl+s3N3x/cXoydb7X14sP4A2BN14TlQL8OcK9HxLRkKbE9cUZY1ZPW3STGPGr6mvpLrs/5IM4Hcou/ojAuZ1KMG8rFh07Pm7/6ED1paVdqW4B1TF0gheuOyFxVf/kH6CREBfGwvevLeX5Lr96eo7/f5uugwGADxDKLI0RQoqsqbfdIziPbf+ajc9IqAo8Vn0aFtr9XVkqqWfM6Q4RSECfFVWS9d3PjertJGCfJbXzzsrph4XApt/5omlakVFAace06yp93d67ncAIPS9AxIAAADAAAAAYAAAADAAAAAYAPx3ABqQIZg1DIBJ6BDMJg2Ac4iCMFe/0V7WD3+Y5gcTgOBd/AmjfXoT5gdTfBhnvw4QzoU3Go9nmtMvAQYANBXJumEWXjoAAAAASUVORK5CYII= + + + + +http://ceneje.si + diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/sl/default.txt b/mobile/ios/Client/Assets/Search/SearchPlugins/sl/default.txt new file mode 100644 index 0000000000..ced95a5cb9 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/sl/default.txt @@ -0,0 +1 @@ +Google \ No newline at end of file diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/sl/list.txt b/mobile/ios/Client/Assets/Search/SearchPlugins/sl/list.txt new file mode 100644 index 0000000000..f29ffff33a --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/sl/list.txt @@ -0,0 +1,5 @@ +ceneje +google +odpiralni +twitter +wikipedia-sl diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/sl/odpiralni.xml b/mobile/ios/Client/Assets/Search/SearchPlugins/sl/odpiralni.xml new file mode 100644 index 0000000000..094bccaea5 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/sl/odpiralni.xml @@ -0,0 +1,15 @@ + + + +Odpiralni Časi +Odpiralni Časi v Sloveniji +UTF-8 +data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAYAAADimHc4AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAA3hpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNS1jMDIxIDc5LjE1NTc3MiwgMjAxNC8wMS8xMy0xOTo0NDowMCAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtcE1NOk9yaWdpbmFsRG9jdW1lbnRJRD0ieG1wLmRpZDozNmQ4NzgxNS1jY2RiLTQyOGMtOTk0MC1jNTg0YzA3OGMxN2IiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6NkI2QTREN0EyQTA1MTFFNTg4NzM5RjgxODBENTVDNTYiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6NkI2QTRENzkyQTA1MTFFNTg4NzM5RjgxODBENTVDNTYiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENDIDIwMTQgKE1hY2ludG9zaCkiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDozNmQ4NzgxNS1jY2RiLTQyOGMtOTk0MC1jNTg0YzA3OGMxN2IiIHN0UmVmOmRvY3VtZW50SUQ9InhtcC5kaWQ6MzZkODc4MTUtY2NkYi00MjhjLTk5NDAtYzU4NGMwNzhjMTdiIi8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+61WF3wAALH5JREFUeNrEfXmQHFeZ5+9lHd1dVX0faqkl67BkyWAj+TYGjE9s8ADjWXMTMNjm2AFmFsdsxMRubMRG7Pw3A+ZYYGCWw1weYAcM2APGhMPGNsdYsiXZli3Jsq5uqe+ruu6qfPu9l9d7L19Wt8xubCtSXZ2VWZX5nb/vesnOvvABoDMDxpDlHJ8A8D7aLqKtwGgnp53RD6NN/XutP6/2vHP8Fu16/+9/J308ff6r/twV2p6nT7mfzv8nel1HtQF2dvJu8dFj9N+DtO3xPpzJM7xX0d/Rj+0mmfKe7SIZoH2W+bm278E5HMv9vWyV89DmGmzfqdKDK5+/2jW1/ez99N+f0fsTaZTqWZ7LPEjs3RO/UHgSFX4n98QgfA39dSh9PBCX6HixTz3H+J7Y39q56vn+56rfoR6lXod8341em5+l8UI9R3kv+C7/PW6eq36/+TfMewi/S9D6IVZuXJWmQz7OheTzNmrF+bm9Vv9O2r+WH9ux7b7zXK5vtetey7Wv5d6S73c3vfmxNEn4+7khiUlWjrUxMH+KF2AJ+sAshiPFxNaSppGx6B1hm1tyc+Aq35Z0zavdw5/iQdZsmIn2afp1acQo/zDVytBNipuTmhkcx0yt5f5rpmmxp33cvwAG9XuYoprcIiwp+sxcuo6sU4XDXEnwhuug2sqg6mbQbGTQaHjnpIgrmWwTHak6CukGsqmmtNUuXXeDZ1FudqHJmW5lYFo7xdZbzDc3rCq3WBfV/3Ou0zK4T+O8S4QJynLDbsW1kK9RM3kb62PYauVNDkcSuZMIl0+X6HULlWYWJyYKOHWyGytFB8USx9ICx+ISR7XC0WxyuC4PCZpOd6CjsxPdBYa+fobeboauPDC6voEd20sYyFSk9pSbOZRbWakpCEGGKoCrW5Mk62qjxypWqSPNfd8eV5Ikw4BzOK6NeffP7SKp7U6vSAmdXSrgyQPDOH6MYXHBxeRUC8VijSRZENsnNFHRERvT8YDY3BZHi45ruVy+J47p7HIwMlzA4EAvNmwkw7u7ivPGVtDh1FFp5bFC2qHjPqwBOZ0LHdqjQ3bq6Ac48llVftucyBJgmgf/VF8S/R0/R5i03kyJpH0ZUyuDePaZHA48C0xOuphbaErTks0wdHY6yGS8zxEa02q15NZstogprnYjDhOMSSGTThGT0pI74p9gSLXiot7w0ExfXwrDgw5ee3EKu19XxraNC/RZGczXu+m3p4n0aTHArF5/e5Hj/r3rNNLp4+1zyk3BgPdz7jPAxK866tTtts12Jh0XqZ0g/ArZ9mW8dHIUv/9dJ154vonpORecpLeLpLWjw5EX2iAuVGs1IriLlOOQiUnT+x0o5PIk1V3IZrNyv7S3JPH1egO1WpVMVQnlcgWNVlMyy4FgZAcxMiOPr9c5ysQQYb2Emdq5M4MrLm/g8j3T9K0ZLNR7JCMiB98epCX5g9XP9RlwUjAgl/mTI0cLJNdkopCu0LaMoxND+M3DXTh0qIX5hRbyOQf5fEpKdIkIV6vX0UHEzedyWD86ivM2bUJ3by96eroxNDyCwaFBdNF76UxWSr1gtji3RVpRqZQxPzeHuZlZLC4uorRSxMT4BMYnJlBcKUmGpklDCrkuctxpYpiL5WKLGMSw/fwMrr22iasvnSTH3YVF0oi10qPdvbc9r9wQDHjfn8iAOOSLuExmweEY7pjBQrmAB37aj6f3NbG85KKnO0WEdIgIDSwtF6VEryPibt6yBRdffDFGN2zA+rEx9PT1o+mS6WnUUa9W0SAGSRNE+zQT5KSIqGSy6HOy5IxTxKAMEblcLmHqzATOTpzB4UOH8PKxY5iamZEM6S7kkevqJG0Tzr1J5zp4zc40bntHFRdumcJSo5+cdgcxmr8qKV8N0EoGnDgSMCCCh6oXj/6OpxECiGoL4QVw7CE735Uq4tEnN+FXvyQbP9Wkm04hR4SvVGtYXlkhM9BN0nc+9uy5BJdeeQX6hoboM12UiSnVcpmITQ6BOWGeRwvEY6+ZgcZIzcnsdHblkCsUSGs6sbI0j0PPPYd9e/fi8JEjkhkF0qhCPhcyIpd38Pqr03jXHVPkrFuYqQ36ehzct03iTdopUEO5djWXxIQJOnHkvSEDeELgkux6k3k90jlHUp/Hd+7rx/79delUe3rS0sQsFYnwRJA9u3fjDde+CRdfcjnBwhYWZ2dQJzPhXawTQxzRTUSESMwiMMTOFZ+XTmfQO9CPAjHlyJHDeOq3T2Dfvn2YnJ4hs9clt0rFM02bN2fw7ndXsXvXJOZqw6i7aTiGK0WbLNlqWSGpAcclA9LaYa/WGAnnlXUaGOqYxt7nx3D//VlMk9QPDKTJPIAQzpJ0hpddsge3vPWtuPC1F5FDLEmbrUqQjZBJ+814xQMAzMfgOqN0DE9mkHzLQP8AxsfH8fBD/4bf/eH30hwO9PXS9aYwN9+QZultt6Zw+23HUWz2odjIUTzhtiXtWunHynWwVw6/h1BQOoF3a8lKeucI4udSFTI7i/jZw1vx0IMtiWz6BzIkUVVpbrZv3Yp33n47rnnDG8gGr5DDnA1VNAyJQilHjHBxdMUt7/M12GTvhADe9vT0kJAM4qWXXsIDP/4xnjl4kDSWNKW3mxx5EyslF5dfkcXdd47Td6QIKfX5cHW1rGd7WM9KLcGAd/kMYFoka3c49hts8RQFU8LeL+O++7fjt4/VyKamCN04UuqFNN10ww141wc/QDfWgcmJ0zKKDQjKGFsl6NHtv0tRmUAx/eQvOju75N8LZL6EwxXfFSaNw+xrZIKEaRPS75k4hPvFr8HhIQIGBfzqFz/Dz3/+CywsLmGwv08GgXOzDZy/swOf/tQ0Cp1VzNWHRJSxJhiqvx/dGyuRDzh2+A7ukgliCVl+u/xEQYWQ/J6MiCzL+Oo3zse+P9TQP5imgAiE7+exfmQEH/zQh3D161+PqekpVMixCsdok9jE/Irh9aRjJaTzPEnq8VdekRJ85euvlqmICkFZx2FedO8Hax7RI4abzlxNGwiYunHjJpw8dRJf+/JX8OKRoxjq75W+Y3amgXUbMvhPn5nHUO8ypqsjEiEJyMHXlCsQTHcjfRFO+OhLd1AknPITbmuz/MGxgviFdBmdhHS+/PUd2P90jbB6WkrY7MIiLtq1E5++5x5iyBAm6IakeXFY7DJ1+64SKu5Mxc/QuhE8+JOf4of/+1/lnhoFbVfs2Y1PfeYzksEiAEty4DZ/YuJ5AXGHKQZxSMu+8eUv4/GnnpIoKUcBoGDCAN3jPffMYqivSAhpRGZn10ozNSh1Kk3BEq6oobrB2KJ94liRaewkm9+VWsTXvnmBJP7wSIZC/6Yk/jUEKf/L//h7KZWnT7zipQYcppgDBY6xiADMWsCAEtLTKyLM8ePHpekZHhwgKFvAtMD2y0sENdMJzlkntFrGDLYwE0tmbHZqiqDwMj5NTL3j9j8nP1YjX1DC8HAG8zNNfOHzQ1gu5zCYnaV7ZgaddPq5SkJT1zjuMSC5OuUGSVQ1oerBOdZAb2YG3/vxLuz9fZUkn4jfamB+cRk3vPla/O3f/R1KSwuEcKalJDEz5R3LjqpSznSY6aOW8KqaTXQRYzPptCfZIqlHf4tgLCCAnh7xt/CzofgeHqIilTnCzK0QUDhz5gze+4EP4oMfeD/q9aYM4EZI0GbONvClL42iQdQtZIohkY3ksxI/2DeHt3mTh5XQ6F+wfzA7gYce3YXHHyGbTzCTk0MSxL/xumvxqb/5G5LIaYp4l6SzZKGW2SFktC9KYgVSFDAiQEoMyVVMpvzPoWoVYqlnHrM/TkwIHJk7qhFMPY23v/OdkgkijlkpVTBETDhxrI5vfGsbuhyC104dXKMdT/jbDf/2vYJOYJFXkenfwCwFJ0gJcdGk3QPZSex/maDmT4n73SnC9sDsPJmdq67EJ//aI3655CESKNFjFK0yewUwgIcWCJqYg2EmLzxzo+f5WYzpzKgRM+h+SE1xCJ9y+vRp/Nk73kHR8R3EgDIxguKdoTT2/3sNDzx8IdHkjGSqyyOaBfRTGeHyiBHiPceTNDfcItMT7OPhfoL15HSLWK7n8ePv5SXIEFBzem4BF1+4C5/+2/9MwcucJL6Qnjj2tceOgWlgWqWMhSZCJUwAimDU5lerPqj+S0VDsWJMLJDzjheOeWJiHHe8+924+cYbsEDaLZxqd08KjzzYxL4Xz8dgxxmP3BpN4/SN9sMzQa7PJa5wjBt7xDEOmZmu1Bx+9C+bMHW2hT4yPXMLyxgdGZYIpFopS7MjpCZytuoNcsOnwGpWAsLH7TnTmizA2hGfxfIwcaZbkJByfQy6xjYaTandf/nxv8LrXvMazMwvIJdLyTv42Y/zWKEoOUeI0FVoxlf550hpt3BMmCIemB75t0Nqdha/278L+//YILufkhGuyLF/6MMfJmYMSYfrmZ14biapNBfUjTnXHX3wnmkqOJI7SsxyZyTxjhZxQ4m4Vf9jq+2aaKlEjrlWWcFf/fWnMTI0KIM1IYhnx5v46U+2IZ+ej0y2QldX0YJIGwITFJN3HqmKb9G6UisoNgt45OcdFJQwmdtZLq7glptvwlVXX40zp0+Ekm9KeWhfbVLHESeOht/hR67M2jqU1CGnmRqtBq1nNDVfYzGTIUT370vmiCigHBlZh/e9931oimJQrYaevhT2PtXAwZe3oy8z7WMfE7wY/7hEQZHT9TYPt3Lof+dTM3j44W2YPNNCTy9dBGH9Hdu24j+8/32YpgtKMgOMRbCHmfVQ7X65UrLkBjGjfSqqQWIhhOlFcW50PMSQF1PgYvvWRxknEPw9ffoUrr3+eryRIvD5pSLFO4ycNSN/0E1MEea6HjpcHholHtI22OdEXWJK5AA3sr8kffn0Es4sjuGZp1zpdGu1OmHwDG4nRJDJdKBETjeSKot4J3UbcFPSo5JmYLv1FIKOY5lpJiwdF6oZCz6LGzaGKd0LYbxhaKaJqkSpdGFxAe9673vJBw6hWCyhrz+F40ea+OOzF6CfUJHreuVS34r7XRyuArGFBsgD/CPgETzYZI5DSv8knnh0HZYWXFmsWCI7eMXll0rTM0nIQJgeewdevNFMizq1Yo/Z5eDGpM9MH5idjtwCMSOmwrD51kAk/B6N4ZwbaXDv85YWF7F+/Xpcf/0NKFdrEhWlyDz/8TESymYemVQ99KFeDsh7zV2Pzr4J8u2TzwjJHdfruRG/OynKOzmzDS/sJ0YUHFTpi0Qx5Za33SZTzJ591gnFuU5stWnLptZxs6AHYJxzvVnA5atkzrkSTTMN2tqiVe/jXQ1tRekLbi8+MS9Qm5yaxI23vAXbztuERTJFApaOn2zhmf1b0J2e9FMRAX19uvpMEK+deA4jMD2B7Z/EM0+vw/yMK2u4i+R4L7nkEuzcdSEW5mZ9hBG/cJMp6g2ZpsmW97dlLiM3bnfEzNItrTdI6fmYID0RmSlu7dwz70f9zmqlgoG+frzx2jdRnOT6MRRw4A+iG09YBjfUIK4kiAIz6ISwSPHM3I/m0uka5msbcPg5v4BOYXhfT7csI5bKJVu/Wyh5Kvbn3F7AjFICTKtk2ZLhUT4HXseVrRbCIjZFzpe37WaIAwOmpSMC7Y0Yx4zKHCMtmMIb3nQtxkbXy1xRodvBxGmOIye2IpeZD+mp0dgPdB3pFrjiKHzJF00HufQcXj48gplJ3/aT9O/Yvh0X7b4U87MznvQrBQ2ziSlOd6ZJoG4euIbZ1VhA7TmVL13epiyp6KPW02SvFSdn8bklZWHrmyLBrFbQNziEyy69BI1miwQXKBU5ntvbi6yzKDPHrnTCvnuVLoDJ23BcF5GdUk2Q49mqF54toNX0nGI2k8Ull14qC+hJ4T64SUhoJsTaG5ow1aLGBszSjpbci8OteSdTyuPTP0jsX1VBgO7UvWNWlpdw2ZVXoqeQl3mizi6GiZMMC9VRYkg9NDsajT0TFNklN7T/XJ60XB/C1ARDljBumWzd6PAQ9lx2GRYp4tXtuu/sYrn2uAkJLlyz6ywhQ6nkYzRGOGzVjjWumB+VkTzBLHGLQdWjdG7knXRoLGoRm7ZuxXayEKLBrCvHMHMWOHxoA5mhuZDoaszloSAemR21gNCRWsTLh4ZkR3JnB0OVsP95mzejd3AQdfIF1pwwQ0K6l7Ulls3RqSgmnlBrl462pTDUyhvThEHtZAUSsrVRPjsRqYm4INfZhQt2XiDTDmK/6DOaGu+gYKsse1TN4pawPl4k7LoSEnm/vc1BBcePFsi+iSt1Zbvg63bvVgIkaI7JJHQs3WtNnPEEiTKlVS/QSAmJf6U+Z8BVwrtarl//TouNN3A/S2zFV2sWDEtLC9h6/vkY6O1FvSFaWhjGj6dRrA3ASTVDCOr5gzAXFIehoICi7oouMiYDC+FYRE103egoSjINy0KbqNrBKM/CtFqsLrUsVgGzB2PccsN6NtQkjpYE1NAUMwRAj9r1cqvBVG67FvW6FT9AIOX8HRdg08aNqFSr6CDLMT8LTE0NIZsthcQPkp7SCXMFowYXkclWKagYopPJ/hMXxYeNEvHXbxyT7YKmmbD1gqmOxsw2qk7O3k1tohZuwfa2VHKSNtr8iq1IY5QFDACga048uhZSLdog+/p6pdCK3KSw1ksLnUixqqXOHjphV2dAagWnT/RjeVEwA2iSATtv83no6e2n181Yfl/F86okq6VEe/CV1A/EYhCUJwS/th5NM9CCVhNWnauaFIyaw6Lz42hKG7vicYAhftZvGEM65aVnGsSAmclOMunVCCWGPp17DAgdsC8dDISAFrMQ3R0iv5FOOejt6ZUcNnMyKrJJDKASgqrwPFWVE2CmtVuC2X1ArA4c8tlNBgYhfNaFJB7fWBqClWRheaWIbTu2o6e74DUW08/UmSyZ9Iy0+RECcmXqx1EJL/MVfpRWq7IwPhATJ93d3eTV63FcDlhVy7zaOMNYYhYnlDwW1xyeEBMkVcPU8qaa2o5Mn5mpZcnX1ub+gu8TQyKDFJQV8nlphoQPLa9QsNbIgznNEFgEQp8OCK832ZLdL6XktYmCtJhMGV43EnYux5NpPLH8p9+khaDGzDOLTWjq9pclZP7tqQ5umfDhMSSk+ir1XqCkr5mvVrpZ0v2VtCZkrkUDsghaRR2ZjAeEIjSqWfIJLTompZk4JwoQgi930eBpCqW9nvxms0ncLKB/YBAN4q5q2+0NVTyEgPHJQ25UvtgapNcMpJI7pHUm6/4pIG58lkDp7TcSc6aQhZ+jfQfTEJEguhwSyWak9RAxo2RAQzSMtUI6cy0QcxUzQZi/XsmiWnGkF2+SBoiWvK5cl5xM0VWQxboKmIIN41lOHYVELSRoW8QxnStznCigivFOJZwT1xVzyQJEmsGsHRLccL7MUjnjGsLKEPFF76r0mQ6kL63XUpK2CIMwHqEgafX9AExwqUncata9UVBxlFCpDKmULNzYOr+MaNXM6djiAD2EZZaCjM2EeRKWIwkTLY9m0cZmmMy8DzOSaXowydukNXSGqmZE1SpvKiflX58nYCKXVq+LgcJW1DvEeeCEA8ertBK1nLB9nPttesHryPTYZ0CS8z9MU/e49piFnHj1SxD8vM1b8dzBg9i/f78c5JOpIZIUoaluqxVep2om4vkne10ifv3caCjDqultSS85LpvWkFSzoTRmKfQO44AoEeRqTXW6avNYd4EdF/JYTRWWiXSzK0GHs3ppUEi+IP5Lh17A5++9VzZGCdMoPr9cqcrryuXy0gkmhrVKiiOqPccdt5rM49a0iFIrboPoVH55VUgdKipOGFCdMfEL3oiWL0cuj8axGWvTi2OzjfH+f+t6EZxb68hBLmfzlm2S+P/4D/8oWwP7e3u9LGSpJGHy2267DR3EEAGVk+rTccFQCy88FiQGGV572ptbmwyY/1leizxCqyEmalxX7ZrjfkHGaJ2TGTpHQCmPGeJkMTTdos1xmFEo4bEqkdZlrBVXeEJ9gBlBD9dwvvBLQvJfeP45Iv4/oFwuY6CvT74tatJiPvgjH/kw3nLrrVicn9PTHxwWopqzAWaLvGNE6qYZ4pr9V/1AYABahBxr9ZrX8ycXExF+tEH3wox2xbAkGcGiVpMhlXGR7XBl1UbYs2q1KsdKg643NTw3I0cVkjEDVsazkKxtC0lLIf69n/2snIAf6O+TxwniC1R2551/KYkv+nRiSTMWT5rFNcAGZ5npGSxVPqb5lnDAULSsEO6sklkU8xByNRdyB6lMk/Y7MOntBP0q0vy4fobO4ejKezkfkdMQgwlLiwsS32oXYym76tlMbs2ABk4oaT4g0KTNW4j4zx3UiK9K/l13fgQ333IrTp065SM4s3JlL7BzRZLNjo0IlMThpg4Q/C4KQ5O8lvaGrJmItk3XZ4CTaviZUK51SUQoyFV/M8L9DW8Qg7y5qIbNTE4h29mlO9A20E+t7eoDy/77CkIx16QI0M7zBw/gc5/9XIz4zZD4t+CUlHw31toYR2tMK77Euy+gOWZzliCuRUxfzs1/4VDo2yRzLRoY5GyaK1L6oqWRzHuLhc1aYUVMLUcidMQiDe3ZfMf3AUtLi0iTBsRSA8zsvUzG0zaYGsfkAdQ8gHs/9zlUiPn9qs0n53b3XXfiJiK+NDt+gtCsdsXXOIraHrliOtUINz4c7l2wXvuAMt2pIz/xI+IlUZ4UQiMG/tymWDKniTSZddG6qCIgyQAXemeil/9h6MxXZCQstEH0OpZWCG04KU0D1JYSta7LVdhptInbiiuRY/SIf/DAfnxeEJ98j2rzBfHvuusu3PSWt4RmRyeGDVryGK5PFor4rCOzILlQc41ZSPHdue4evPLyMSwXi3LWWLzbP1QmWnKpDVztBPXigHgms15NYWjdAvIFVzplseSLmCYvl1bCDujkMSOj14YnrRqlh+8B2jl44Fl8gYgvOvD6eyPJF/j+bkH8m2+WxFdnfc2gLl7zNTMQ3Fo75tyMkrmlnSZpts2fnSODL66v3qjL7n9RT+kfLlKg6Cg19+jzHNdl/o5oa9QcFPoa6B1oyoKCWG9HMGDq7Bm5Vo+tvYTHImFmxA3x2Vy1HLqFHO7B/c+Q5N8rGwD6JM7nvuS7JPl34kYi/mnF5puFf1sbixppx/2NieeZFZHZG7dgDN95e+r1quwZFYIqVvBKE5wvFMoytRPRGOFrB3qRxhtQcoXndiUSaraYzOyJgGeaHLFYdcQm0QxxoqqpgFhHmsIYQfxn9u2lCPfzck2f/r5eeYz4TqEZH7v7Lkn8U6dOhmZHBwHcyE0hFmmbtenofGZck72725xxi+KfKD+Uy+UwQdI/ceYMukhoBdG7e1voGahIoXa5MW8qNMC7GL/4IrTB9ZeAJJUZGZsnOya6er3lZQ49/4J0MnrZkcXqv7YOY91BRgHJli1biPj78MUvfFG2vYsgSxwjbGhgdq6/6Sbf5kcr11rnOYwatdoYbO2MY/EUha2DT9dYFb1xqEvjiO64Iy++iOnZOXR2dMg09PCGMjryYpk1x0Jn6QM8RXK5olD0X62cwobz5lDoEcuHMenRX375KFaWF+XYvr21BEZwxYwcf3SOHLIeHcXep58m4n9BwjYh+TK9sFKUqOxukvwbBPFVqMmYFUUlJcv0qpitNYVbujC4BlHt6wOpc8we48U9vXLsWJg1Fvi/f2hZ+tHAyriaI2ZeIMa1nsUge8ck53oH62hUIZf5mpyZwYukBb0DA4lj/ma20+yhCS66lyT92OHD+KevftUzO35uZ4kkXxz3sY/e7RH/5EkfajoxUxJvLbENBdqK/vYUth1B6YUa67p5dESeTLNYjuEluqcc0UqUdHsHW9iweQ7VUgpBB7ylK0LZoSdF5fVuOn9GwlExcC0mx/eRxBbIEXOjwJ28Yog9VV3oIQYcPYqZ+Xn09fQoxOf4KBH/uhtu9MyOVrxJrteqUFg1d/qUJbdM5KsAwT7IzdQYwkBvgRD09g9iP5lSMbolQEudGDA4UkWuRwAZx6Ot6m91BjDrViPOjW5aQHdfC7UK0E1cfpE4fOTIEbnYURyaMUPiubURVrycn5nCNde+CRfu2IFxcu5np2c8yf/YR4n4N8RwfiISYbr0M9j6QmFJf/NYSj2xa9tcLs0omwroWa2U5DJocqZYAJcujo3bJskRB5rL/DhA+a36gNhG/zVqTHJwbEtRqpRY4G6KCPWHJ5+UK00F0zHmMsW2DKdZgBcJvmy2A5/45Cfxptdfje1bt+ATn/g43njdDQrOZzHElDRr0N6xcqNj2h6bxNrdLc1d+kyBZ/eH12/A3t//HsfJXIqFAKtlhoHhBtafV0RlJaVLPVQNYKIror2prJcdnLfjLF55qRd1YohoUfx3MkM33noLunt6sVJcjuYEmLmWKNeSWFFdVhSrHSzMz0lf8Kl77iE1rUnsPH7yuFbssFeseExibc43PqBtDwiD03ms9hsNWDLYB75FrkysEPP4Y49R8NVEj8ge03Gbd8zI6DdY8ReutQWVAjGuT59wRD5AvFclNDS4vkpaUEKlJBjQReZiGr/+t19iUDpjntA7iYSnb0SOTfoVsvvzM9Mo0e+Fubmw/JmMouwO3kQn0URkvIaraicz+l3MyFZNTmulVDkpSU52bBN+99jjcmGn/r4eSaOB4SY2XzCDSjkVm+7h/vCLUpL0JzfEFEfLJ3xLb1WvVRxsf+0pCsxcTwso4HiCzNDRo0cwODziOWQWJK70Vg99/orr7PclVeB7Ee2qkE5HIe2GJpx4ZYvr04wqoZmV0GZJ1OZjDL8hxnfzBYLMy/jVL38pk5Yp0mCB/bfumiEf4EoTHtDTlWta+9lQBDRXnHDQ8OD5hMgRi58amaGB0Tqp1TJx1ZFaIKbk//WHP0JXLi/VMEpc2Zt1oZkhszIVlzb9XDfBBPFYqpjF2hWZBeFEnOBJLeraGJK9dX5kZAQP/fQBHDtxAn1igb8iMLS+ga0XTqO8nNLpGfaosvB2vfkAJTGkJonMTfQJ7br0FHr6mygLNSN123fgAH790INyjbWW6L0ICzUsqTYNfS0gvYHXLMjrkTMS0IodAETtntzqbJmxrE2S9unBpkc5ca8jG8Zw6IXn8fAjj6A7n5fIRxyya884nAwFr3VmbdcM6Bzk35wkgptbjTx7Z6GFna+bJNUSlZ80OjIZ/OyBB+QCd8PrRsNCNIM9yWVOw8R7LeOzAsycuFNsvD3i5lpcYlvcNanAzoyhC/N4z1y6KHT3SK38zre+LRcsEYu9looMm3aUMLZ9UZF+fXNtgRgUh2uOKmkn0geWllLY8poZjG0tY2VZdEx3Y35xCV//ylfhkBkSF+a6bqJDTEIkEZFc68QMMx8QFKsxJxT5YxKd1FXKjObcuKYGwZlYomF4aAj3/fP/wpFXjhMQ6fPASW8LF13xCupkKeRQI+JjX2E6IlhHTkVBQfc298cpQ24h9DloNSDzGq+75hiF3mSKViAv4NDhI/jmV74iL0xg+2i9hXhbn8rYdmVDs7rGYgV2FutDiuXrFR/BwCxL5zCj8w0x1GUyeWzjRvyCtP7Rxx/HQG+PpIdQ/NdeOY7Oblf6S2XFTB/5sKg3SF0NAgED1DyFMWsVMsPfX1lh6OpxcfHV4/KLWw0Hg/29eOyJJ/HDH/wAGzZskPBSb2SNI6N4HyiPrw0UriEd76AwE2jm9AqMjjwea0FJGuJOeCyDaBLYvAVPPfEE7hfgw38mQbnoYNtrFrBxB5keshByDRQ3aMTyEm48mMPmEeHF1nIDFKQWZVxvC1Om6uYfU1p0sIFs3c49s6R+IhTPygDtJyQZD/785+SUN0o87yrqFZ/1Ypa4wY316kS2nidX4Jit7KmnjqOab4R+uDJnpjt8HQUJsypqFs8++yy+/vWvS18hHO/KkoN1myq46KrTqJYcKZAh/Vpe5rPVgk5LDoWeRLtAwh0nMEM+igmWb4xFiR6urZAP2HXZBErLWZw62ovuvi45nvm97/9AYuK3vf3tmJiY8DvVHGv0afbu25/QgYTFPGzLX/J4o2YsELS0qzCutZeYq3gFxP/S578gpx8HSOOLiyBEWMel1x3xup8r3sWG9OIsMkWqIw/CCF+m0lHXsY7Eg8mellJ5Uuc4RKlStGDsufYEatXtmDqdpwsiqeAl3Pfd78oOtjve8x65xppY5kuYJQT9QNp6nTxhNRMWDmjYhuTUEM02axxPBOoTMSoj1WWUg/OE9mYyaYyNbcSTZHb+mSRfEr+vV06PiradK248ikyHACfME2Bu0MuFRku5z48HghUXUn/xFxv/e9NhYRcfa9vdqf8065AjOBu2LWBxugeLsxlCQl6x5pkDBzE/PY1r3ny97JcXjxNh8kEMiE0iJvWbRsRy9I47pi+gZGqPLUcUH7YzE23RfgGnC93dcjL0Zz/5Cb59332yJiEKRivLYuWABq68+SgKfU1JfDA9txpPjsT3yY452ZhlUDkWPBiLaam1A/HJlaL39xV0QcNjZVJNihc6utDXncevH30Uf//f/iuq5RK2bt2mJch0+80TUYdZ8rTVd9Ub0BnCjWXvOWzL5kTdFF6lbpR8WI4g9le++EV8//5/kdXAPkI8xSWgU0r+MXQPNDziGyVpbkkt2/YF56VuJw1oOa/q4Sghe0VgJhZvHds+T74hj7kpMaJDcUGhEydOj2PvH/+IHDlp8ZgSYYTEGjsh3lZsotknpOZv1LySuT/Wo6fNgjFri2R8pRVXtrcLk/PiCy/gf37uXux9dr9cnqezo5OI75CJ9cxOoZ8kf5HZ3EvyxJTlR0DQ1J/fvmnNDGj3PYIJ4pGCG7YvkJPuwMyZTtISh9S2CwtLy9j79D6MnzqFnRdeiE2bNqFaq8qOOzDb4w/VITsF66trjDKWPJittIWbps30J0Li5ZL1mzbLVWB+RFD6+7TNzMxSXNNPB6Ql1Bweq+DyG46iI+cS8Y0I2WY+/YtgljHd4D3RFce+/d2reSPNoqfDJhFceTRi7JmMikqls6SmBWD86CBe2rcO9Voa+W4u12BeLBaxbngI1735Otz81luJOf2YnDyLWq1mtBfGJxHNmWH1mPi0PW8zJhVNPook4vDoeiI8x5O/+Q0e+fWvcezESQmp5bNkykz29mzZtYydl52S+Z7qStBLguQH6SB5TVNV2DL02ezb37mKN1KOXcTX+jRDZV8gxbk+oDiXxuF92zB7tkOihY5OLleUqhDBt5AWXHPNNXjT9dfL5wuI9XaWaYsK8HGEEzIIMFY3sQVSzDJM4Z2ZL+TR2z9E6K0kq3tP/Pa3OHz0Zalffb0F2UZepYg219PABXvOYP3WJcL5npY7bBVzsNpzrZVjMk2612/d52vAOf60e2xH4KjzPUzOaBx7fj3Gj/TRTaXRmRedwi0sLq9I9R8jpCHWoNt92aXYtuMCWfAXy0GKQo3rcku+yLasTbzvX1vslY4RTVO9A94jsk6dOI4Dz+zDM3v34cSp0xJe9pOTFfn8MgVUYkBldHMJ23cflw8ELS15kNFhq9PEVnNIolO6Qdf4zW9dzesZlsysVZ4S1+7h5dIkkXPu6gaWZlM4+eJmTE90odVIESNcuaCd0AgxUS5WmhLPEzt/xw5sp+38nTvl82HEjxj/r1Wrck7MdV29CqcInShzithEjLGKB8N1EZR0WIYkuorxkyfx0guH5CNPXjpyBPMLizJg7C7kZIwi6rgCt/eN1LB55yTWnVeUEl8r83N7rhHW/uzibINM0De+9eo04Fw0RWy5bgepDMfsRB7jL49ifqpTqnq2g8s2SOGQi+WKlFDRI7Rxwxj6+/uwfsMGYsp2DA4Ne+Oy2YwcFBEPhZArtIsYRjzks9mSkyliMEL05y8SgU8QsUWBX/RqinbBmbk5Kcm5zk50dYqx25RsNhAX2DtUx+iWeYppZmVQVV7m56z55/qTaXKvKO+aayi70XjXag8wtg7E+TGCqj3lopdq7h8tYWD9McydyWHq1DosTJOzE2rPOtHfnaXv5dIkiPaXBhFWTOj0UEyRJ4jY0dEhxz+7yEyJQWixhLD4ejGiKpy8mCWo1+ryfBGJi0UGRZeyMC0iedYvW2mYLBWKpGKms4WBdQ2MbJqlbUH6KUF4uUhJIL1OfBDPLEMkPehZTUPYaOevFWFfgELtu1ptBfQk1bQ9+Lm05I3v949WiBHHUVxwMDO+DsX5Ar3OoF5JyWd15bs65GSJ+KAGSfbc/II0Py1/lZFYVU10sKYc/wGfjv/Qzhy9LsjclXSs5Egd0rbu/jpFsVUMbZim6/BWvK2sgN7naiYmRoekxGnbBz2vQrs0T8jIcqbg6XaAgxndxvFSsIHNPY0rLXmPc+rocrHt4rMyr75IAVxxqZ/e6yJmiO7ilNzvtjrlohfptL5aoW09IR40GDQhVzYXE4qCkd0DJQqgagSJl8nOL5MPYmjVSROKrpR4hxnm3HiyR6LZMRKVGh2S6MMVH6mmFRCvZ0S71N4Wc9ETFjEtaJCIwVOL+sphkAqXXXfieVw9QzUMjE1JiS0tiv051KtpYkiBpLOTbHvKT+uKAErPsArTJYYLhckQKKujq0YwsoyuQp18RgX53haZHI8xtTJB5Hl9tfS1P6bc7l1jj2IJs5zJNHT9bGhdPFe+zfoTbbO+sWNtHn8NdQ9xMRUyAcxHHekOUYMuy2QfY4RI6CpbDR4ufioZ4AbS5xFfBEjiceci8Zrt8MRAlAdbTbHGv+hxco3nBaxWhlkNYZzbexbTtJKmG3+G3rj6VX/R/wPUJL6vSQRv1pUvZ96aENIEiWbhNDcW12NhkrBR5V5+Hufgs/4//BDtnxcm6H7BgKQgGKsEx+1K3nyNeSXWpmTOlVxLy+XtukLXnA9LClZZgsK3+45EzK+mbpLTO/eLroiv0XbA7MjTMqc8Dp+s9QIzBbuaNtlMFo9/Lm9j+tR2yoTnqCWuWZdkhmLoDUZ6fjVrzKGjKW55n+Mg/fqaU0+xGgnWbbRjv6s4DDfhIXpcaakIyplhp5fSfuEmPR9OKfaHMI9HDXBaIwCPujRU5KD219i+G2ofpnqMqx9r3oOrLGCuXo96TtK9mw8igXFtxj0eoH1vaxDt07kqBTFpZ4IYdhW9/x/psA/S6wvpd36tPsjeS5YcwSe9Z3ta8WpPreYJv9tdX9L38ASTyhK+F2v4HGW/WO//Rdq+R39/lf6ui2Tc/xFgAOJvtb2CZnneAAAAAElFTkSuQmCC +http://www.odpiralnicasi.com/opensearch/description.xml + + + + + diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/sl/wikipedia-sl.xml b/mobile/ios/Client/Assets/Search/SearchPlugins/sl/wikipedia-sl.xml new file mode 100644 index 0000000000..6fbbdd4ebb --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/sl/wikipedia-sl.xml @@ -0,0 +1,23 @@ + + + +Wikipedija (sl) +UTF-8 +data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAQAAABIkb+zAAAEXElEQVR4Ae3cA4xs2QJG4b9aj41n27Zt27Zt27Zt27Zt+2KM5lXP924qOzs56VPjKSRnrfhv7NUsp08v98+PsxwT4nJ+nPunl8KZ8/WYQL+eMydJrx5/EhN603lAHpVJ5bzZNp035uyZXM7Uy3LmM7ms9CITzVRGQRfQBXQBXUAX0AV0AV1AF9AFdAFdQBfQBXQBXcBMjgU7M5+1vqtZyXqmc/IsZDEL+53LP3KRPCCv62+rWd7vnpx6v/M5Vd935OF5R+6RtRyeQ3NYNjKTxZw2p8tiduR8uWfemY3+dkjWM5fFLPU/8q9yrRwbHLP39WkHamPD5z1WnNMb/dpW/uAdbipu413+qcl2H3N3cXFv9DNNtvmg68uxMY6dPfewTZNvOauouqEdULlTY532IgqHua+ZxnoHm4ANj63biRNQPZedVPY5nTR1BXsVbNuyTlsDqy4tW/wK4NYiJ01AXMlm+wGrL1cDnWbLejC4m7T4DvAOOSkD4v0qztGyLzpUweOk4UXAp6XV72DNmU7qgPPao+DJ0uLTFPzXXGN5I9acU1o8vb14jZzUAfFeBdvMtuwLDlfwIKme2248T1p9MvY59zACLqcy4Kf5eQr+ZbYRfrAFaXHaf/FRGUZAfE/Bzwf8OKwpuJ/0vYRNPEpavT248rACbqfimtLiqxT8w4yIz+Of5qTV7+LHMqyAaf9R8Clp8ex2K7iXuAa4q7R6aXCH4QXE4xVsOq+0+DYFfzXth/ilnrT6Tvzb9DADlqwqDPjTd377FHwI3EBaPb0NPFaGGRBvoLBiUVr8EFS+KgN8KpYtDjvgQo6i8Hhp8ZJUjnIZaXXGNrxKhh0QX6TwHzPS4uco/FtPWr0T9jnPKAJuOOiCc/UqVG4jrf4AH5dRBPT8UWHg3/BvUfhl635ZcNXRBMSDqFxFWvwklVu07O/BT2RUAad0GIWPtexXgMpPt+xntAt3Gl1AvJjCPufasn4d/6NyY2n4DPzXzCgDzmEvhVdIw+tj02X8nsIPGvus7Xi8jDIgPkLhSPON5ad4t7gbletJ9S71n+BIA65C5dGal1g3nENM+weF70j1R3iNjDogfkrhn6br5dU/4UXS94EtF7+vUC8IjjzgrlRuJ33v27j2dTLbKXxd+r4Pn5RRB9RfxsL3y4H/i0dI9TFUrirOZDeuPh4B8TQqVyjH/ZtZqZ7KIRS+JJ6Fn8k4BNRL9IUPmncwbr/1b37lanbiLuMTEG+nsNfb8cOWq0DLFA7E/8yMU8AlNLnaoP/alSfKOAXEN6h8Qlo8k3UKq5bGLeAWFPa6oLT6Ogqvk3ELmPJ3wOtlgOe0B2w6//gFxCPBsjPIQN8JPi3jGDDvSDxVDPaCNnFNOdGczrNzIrEnRyR5XPZlMIdmKofnpTnx6B783QV0ARNOF9AFdAFdQBfQBXQBXUAX0AV0AV3A6OgCVjLJrEzlj5lk/jiVt2eSefvEvzTJxL84zMS/PM//Af1pBRpQAkXBAAAAAElFTkSuQmCC + + + + + + + + + + + + + +https://sl.wikipedia.org/wiki/Posebno:Iskanje + diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/sq/amazon-en-GB.xml b/mobile/ios/Client/Assets/Search/SearchPlugins/sq/amazon-en-GB.xml new file mode 100644 index 0000000000..3a3fbabb19 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/sq/amazon-en-GB.xml @@ -0,0 +1,16 @@ + + + +Amazon.co.uk +ISO-8859-1 +data:image/png;base64,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 + + + + + + +https://www.amazon.co.uk/ + diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/sq/default.txt b/mobile/ios/Client/Assets/Search/SearchPlugins/sq/default.txt new file mode 100644 index 0000000000..ced95a5cb9 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/sq/default.txt @@ -0,0 +1 @@ +Google \ No newline at end of file diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/sq/list.txt b/mobile/ios/Client/Assets/Search/SearchPlugins/sq/list.txt new file mode 100644 index 0000000000..56a92fd0b0 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/sq/list.txt @@ -0,0 +1,6 @@ +amazon-en-GB +bing +google +twitter +wikipedia-sq +yahoo diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/sq/wikipedia-sq.xml b/mobile/ios/Client/Assets/Search/SearchPlugins/sq/wikipedia-sq.xml new file mode 100644 index 0000000000..b0d228cc15 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/sq/wikipedia-sq.xml @@ -0,0 +1,23 @@ + + + +Wikipedia (sq) +UTF-8 +data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAQAAABIkb+zAAAEXElEQVR4Ae3cA4xs2QJG4b9aj41n27Zt27Zt27Zt27Zt+2KM5lXP924qOzs56VPjKSRnrfhv7NUsp08v98+PsxwT4nJ+nPunl8KZ8/WYQL+eMydJrx5/EhN603lAHpVJ5bzZNp035uyZXM7Uy3LmM7ms9CITzVRGQRfQBXQBXUAX0AV0AV1AF9AFdAFdQBfQBXQBXcBMjgU7M5+1vqtZyXqmc/IsZDEL+53LP3KRPCCv62+rWd7vnpx6v/M5Vd935OF5R+6RtRyeQ3NYNjKTxZw2p8tiduR8uWfemY3+dkjWM5fFLPU/8q9yrRwbHLP39WkHamPD5z1WnNMb/dpW/uAdbipu413+qcl2H3N3cXFv9DNNtvmg68uxMY6dPfewTZNvOauouqEdULlTY532IgqHua+ZxnoHm4ANj63biRNQPZedVPY5nTR1BXsVbNuyTlsDqy4tW/wK4NYiJ01AXMlm+wGrL1cDnWbLejC4m7T4DvAOOSkD4v0qztGyLzpUweOk4UXAp6XV72DNmU7qgPPao+DJ0uLTFPzXXGN5I9acU1o8vb14jZzUAfFeBdvMtuwLDlfwIKme2248T1p9MvY59zACLqcy4Kf5eQr+ZbYRfrAFaXHaf/FRGUZAfE/Bzwf8OKwpuJ/0vYRNPEpavT248rACbqfimtLiqxT8w4yIz+Of5qTV7+LHMqyAaf9R8Clp8ex2K7iXuAa4q7R6aXCH4QXE4xVsOq+0+DYFfzXth/ilnrT6Tvzb9DADlqwqDPjTd377FHwI3EBaPb0NPFaGGRBvoLBiUVr8EFS+KgN8KpYtDjvgQo6i8Hhp8ZJUjnIZaXXGNrxKhh0QX6TwHzPS4uco/FtPWr0T9jnPKAJuOOiCc/UqVG4jrf4AH5dRBPT8UWHg3/BvUfhl635ZcNXRBMSDqFxFWvwklVu07O/BT2RUAad0GIWPtexXgMpPt+xntAt3Gl1AvJjCPufasn4d/6NyY2n4DPzXzCgDzmEvhVdIw+tj02X8nsIPGvus7Xi8jDIgPkLhSPON5ad4t7gbletJ9S71n+BIA65C5dGal1g3nENM+weF70j1R3iNjDogfkrhn6br5dU/4UXS94EtF7+vUC8IjjzgrlRuJ33v27j2dTLbKXxd+r4Pn5RRB9RfxsL3y4H/i0dI9TFUrirOZDeuPh4B8TQqVyjH/ZtZqZ7KIRS+JJ6Fn8k4BNRL9IUPmncwbr/1b37lanbiLuMTEG+nsNfb8cOWq0DLFA7E/8yMU8AlNLnaoP/alSfKOAXEN6h8Qlo8k3UKq5bGLeAWFPa6oLT6Ogqvk3ELmPJ3wOtlgOe0B2w6//gFxCPBsjPIQN8JPi3jGDDvSDxVDPaCNnFNOdGczrNzIrEnRyR5XPZlMIdmKofnpTnx6B783QV0ARNOF9AFdAFdQBfQBXQBXUAX0AV0AV3A6OgCVjLJrEzlj5lk/jiVt2eSefvEvzTJxL84zMS/PM//Af1pBRpQAkXBAAAAAElFTkSuQmCC + + + + + + + + + + + + + +https://sq.wikipedia.org/wiki/Speciale:Kërkim + diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/sr/default.txt b/mobile/ios/Client/Assets/Search/SearchPlugins/sr/default.txt new file mode 100644 index 0000000000..ced95a5cb9 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/sr/default.txt @@ -0,0 +1 @@ +Google \ No newline at end of file diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/sr/list.txt b/mobile/ios/Client/Assets/Search/SearchPlugins/sr/list.txt new file mode 100644 index 0000000000..53ad1108e0 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/sr/list.txt @@ -0,0 +1,6 @@ +bing +duckduckgo +google +twitter +wikipedia-sr +yahoo diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/sr/wikipedia-sr.xml b/mobile/ios/Client/Assets/Search/SearchPlugins/sr/wikipedia-sr.xml new file mode 100644 index 0000000000..f6fd5f22e2 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/sr/wikipedia-sr.xml @@ -0,0 +1,23 @@ + + + +Википедија (sr) +UTF-8 +data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAQAAABIkb+zAAAEXElEQVR4Ae3cA4xs2QJG4b9aj41n27Zt27Zt27Zt27Zt+2KM5lXP924qOzs56VPjKSRnrfhv7NUsp08v98+PsxwT4nJ+nPunl8KZ8/WYQL+eMydJrx5/EhN603lAHpVJ5bzZNp035uyZXM7Uy3LmM7ms9CITzVRGQRfQBXQBXUAX0AV0AV1AF9AFdAFdQBfQBXQBXcBMjgU7M5+1vqtZyXqmc/IsZDEL+53LP3KRPCCv62+rWd7vnpx6v/M5Vd935OF5R+6RtRyeQ3NYNjKTxZw2p8tiduR8uWfemY3+dkjWM5fFLPU/8q9yrRwbHLP39WkHamPD5z1WnNMb/dpW/uAdbipu413+qcl2H3N3cXFv9DNNtvmg68uxMY6dPfewTZNvOauouqEdULlTY532IgqHua+ZxnoHm4ANj63biRNQPZedVPY5nTR1BXsVbNuyTlsDqy4tW/wK4NYiJ01AXMlm+wGrL1cDnWbLejC4m7T4DvAOOSkD4v0qztGyLzpUweOk4UXAp6XV72DNmU7qgPPao+DJ0uLTFPzXXGN5I9acU1o8vb14jZzUAfFeBdvMtuwLDlfwIKme2248T1p9MvY59zACLqcy4Kf5eQr+ZbYRfrAFaXHaf/FRGUZAfE/Bzwf8OKwpuJ/0vYRNPEpavT248rACbqfimtLiqxT8w4yIz+Of5qTV7+LHMqyAaf9R8Clp8ex2K7iXuAa4q7R6aXCH4QXE4xVsOq+0+DYFfzXth/ilnrT6Tvzb9DADlqwqDPjTd377FHwI3EBaPb0NPFaGGRBvoLBiUVr8EFS+KgN8KpYtDjvgQo6i8Hhp8ZJUjnIZaXXGNrxKhh0QX6TwHzPS4uco/FtPWr0T9jnPKAJuOOiCc/UqVG4jrf4AH5dRBPT8UWHg3/BvUfhl635ZcNXRBMSDqFxFWvwklVu07O/BT2RUAad0GIWPtexXgMpPt+xntAt3Gl1AvJjCPufasn4d/6NyY2n4DPzXzCgDzmEvhVdIw+tj02X8nsIPGvus7Xi8jDIgPkLhSPON5ad4t7gbletJ9S71n+BIA65C5dGal1g3nENM+weF70j1R3iNjDogfkrhn6br5dU/4UXS94EtF7+vUC8IjjzgrlRuJ33v27j2dTLbKXxd+r4Pn5RRB9RfxsL3y4H/i0dI9TFUrirOZDeuPh4B8TQqVyjH/ZtZqZ7KIRS+JJ6Fn8k4BNRL9IUPmncwbr/1b37lanbiLuMTEG+nsNfb8cOWq0DLFA7E/8yMU8AlNLnaoP/alSfKOAXEN6h8Qlo8k3UKq5bGLeAWFPa6oLT6Ogqvk3ELmPJ3wOtlgOe0B2w6//gFxCPBsjPIQN8JPi3jGDDvSDxVDPaCNnFNOdGczrNzIrEnRyR5XPZlMIdmKofnpTnx6B783QV0ARNOF9AFdAFdQBfQBXQBXUAX0AV0AV3A6OgCVjLJrEzlj5lk/jiVt2eSefvEvzTJxL84zMS/PM//Af1pBRpQAkXBAAAAAElFTkSuQmCC + + + + + + + + + + + + + +https://sr.wikipedia.org/wiki/Посебно:Претражи + diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/sv-SE/default.txt b/mobile/ios/Client/Assets/Search/SearchPlugins/sv-SE/default.txt new file mode 100644 index 0000000000..ced95a5cb9 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/sv-SE/default.txt @@ -0,0 +1 @@ +Google \ No newline at end of file diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/sv-SE/list.txt b/mobile/ios/Client/Assets/Search/SearchPlugins/sv-SE/list.txt new file mode 100644 index 0000000000..eb0a30f8dd --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/sv-SE/list.txt @@ -0,0 +1,4 @@ +google +prisjakt-sv-SE +twitter +wikipedia-sv-SE diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/sv-SE/prisjakt-sv-SE.xml b/mobile/ios/Client/Assets/Search/SearchPlugins/sv-SE/prisjakt-sv-SE.xml new file mode 100644 index 0000000000..06f5e65312 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/sv-SE/prisjakt-sv-SE.xml @@ -0,0 +1,12 @@ + + + +Prisjakt +UTF-8 +data:image/png;base64,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 + + + + diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/sv-SE/wikipedia-sv-SE.xml b/mobile/ios/Client/Assets/Search/SearchPlugins/sv-SE/wikipedia-sv-SE.xml new file mode 100644 index 0000000000..83df4fa315 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/sv-SE/wikipedia-sv-SE.xml @@ -0,0 +1,23 @@ + + + +Wikipedia (sv) +UTF-8 +data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAQAAABIkb+zAAAEXElEQVR4Ae3cA4xs2QJG4b9aj41n27Zt27Zt27Zt27Zt+2KM5lXP924qOzs56VPjKSRnrfhv7NUsp08v98+PsxwT4nJ+nPunl8KZ8/WYQL+eMydJrx5/EhN603lAHpVJ5bzZNp035uyZXM7Uy3LmM7ms9CITzVRGQRfQBXQBXUAX0AV0AV1AF9AFdAFdQBfQBXQBXcBMjgU7M5+1vqtZyXqmc/IsZDEL+53LP3KRPCCv62+rWd7vnpx6v/M5Vd935OF5R+6RtRyeQ3NYNjKTxZw2p8tiduR8uWfemY3+dkjWM5fFLPU/8q9yrRwbHLP39WkHamPD5z1WnNMb/dpW/uAdbipu413+qcl2H3N3cXFv9DNNtvmg68uxMY6dPfewTZNvOauouqEdULlTY532IgqHua+ZxnoHm4ANj63biRNQPZedVPY5nTR1BXsVbNuyTlsDqy4tW/wK4NYiJ01AXMlm+wGrL1cDnWbLejC4m7T4DvAOOSkD4v0qztGyLzpUweOk4UXAp6XV72DNmU7qgPPao+DJ0uLTFPzXXGN5I9acU1o8vb14jZzUAfFeBdvMtuwLDlfwIKme2248T1p9MvY59zACLqcy4Kf5eQr+ZbYRfrAFaXHaf/FRGUZAfE/Bzwf8OKwpuJ/0vYRNPEpavT248rACbqfimtLiqxT8w4yIz+Of5qTV7+LHMqyAaf9R8Clp8ex2K7iXuAa4q7R6aXCH4QXE4xVsOq+0+DYFfzXth/ilnrT6Tvzb9DADlqwqDPjTd377FHwI3EBaPb0NPFaGGRBvoLBiUVr8EFS+KgN8KpYtDjvgQo6i8Hhp8ZJUjnIZaXXGNrxKhh0QX6TwHzPS4uco/FtPWr0T9jnPKAJuOOiCc/UqVG4jrf4AH5dRBPT8UWHg3/BvUfhl635ZcNXRBMSDqFxFWvwklVu07O/BT2RUAad0GIWPtexXgMpPt+xntAt3Gl1AvJjCPufasn4d/6NyY2n4DPzXzCgDzmEvhVdIw+tj02X8nsIPGvus7Xi8jDIgPkLhSPON5ad4t7gbletJ9S71n+BIA65C5dGal1g3nENM+weF70j1R3iNjDogfkrhn6br5dU/4UXS94EtF7+vUC8IjjzgrlRuJ33v27j2dTLbKXxd+r4Pn5RRB9RfxsL3y4H/i0dI9TFUrirOZDeuPh4B8TQqVyjH/ZtZqZ7KIRS+JJ6Fn8k4BNRL9IUPmncwbr/1b37lanbiLuMTEG+nsNfb8cOWq0DLFA7E/8yMU8AlNLnaoP/alSfKOAXEN6h8Qlo8k3UKq5bGLeAWFPa6oLT6Ogqvk3ELmPJ3wOtlgOe0B2w6//gFxCPBsjPIQN8JPi3jGDDvSDxVDPaCNnFNOdGczrNzIrEnRyR5XPZlMIdmKofnpTnx6B783QV0ARNOF9AFdAFdQBfQBXQBXUAX0AV0AV3A6OgCVjLJrEzlj5lk/jiVt2eSefvEvzTJxL84zMS/PM//Af1pBRpQAkXBAAAAAElFTkSuQmCC + + + + + + + + + + + + + +https://sv.wikipedia.org/wiki/Special:Sök + diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/ta/amazon-in.xml b/mobile/ios/Client/Assets/Search/SearchPlugins/ta/amazon-in.xml new file mode 100644 index 0000000000..05c5311e31 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/ta/amazon-in.xml @@ -0,0 +1,15 @@ + + + +Amazon.in +utf-8 +data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAYAAADimHc4AAAAAXNSR0IArs4c6QAACixJREFUeAHtXQ1MldcZfuRXlIr4A1MoYAf+xKBswlKmdTJ1Q7RZdc5kM7XWsXaGWk2WTDPc0qlzOmZqnHGZoxrntuiydB1UXZSRqS1h03WTVdDa+osov/I3qAicve93ufTC/eHc673f+ejOmxzu933n/c77nuc5/+fcC/CJjKDLXArlFFopCB38igFjytgyxoz1APkM3ZVQ0KCbgwFjzZgbwmxo8M0B3rGAM+YjgunPdyhspKDFXASeInPVTMABCvHm2tbW+hCI5eaHO4cnNCRKEGhjArhd0qIIgSBFdrXZPgQ0AYqLgiZAE6AYAcXmdQ3QBChGQLF5XQM0AYoRUGxe1wBNgGIEFJvXNUAToBgBxeZ1DdAEKEZAsfkQxfZ9Nh8SEoKwsDD09PTg4cOHPqej+sVhQcC4ceOwdOlSZGRkICUlxQhJSUkIDuYNPaCpqQm3bt3CzZs3UVlZiVOnTqG8vNwgRzXAMvYdN4otdT1//nxBYIpHjx4Jb6W2tlbk5+eLqKgoS+WJCBnsj9ODwQqm38fHx4uSkhJvMXep39zcLNauXWt6HlwA7c4HaxGwZMkS0dDQ4BLMx3m4b98+dwCofm4dAhYuXOhTcyNLzObNm1WD7cq+NQiYNm2a4OYikNLZ2SmSk5NdgaDymTUIOHnyZCCx70/70KFDKsF2sm2JYyk02sHZs2dlRmyoqanB4cOHcfnyZbS2tmLq1KlYs2YN0tLSpN5vbGxEbGyspYaoTqx40YP75d2jR4/2l1BPFzt37hQjR450shkUFCR27drl6dUBcenp6U5pmJ1nB3tqmyAGr76+fgBArm4KCgqGBI0mX65edXq2YsWKIdNyACigusoX41JTUzFhwgTKr3uhzhnbtm1zr9AXc+TIkSF1WIHmGVJ6ZigpJ+DatWtG+9/d3e02v8eOHUNbW5vbeHtERUWF/dLjJy9tWEWUrwV1dHRgwYIFxrpOXFwcEhMTMWXKFCPwek9oaCi2b98uhVd7e7uUHi/kWUUs4wmvat6+fdsI58+f9wkfT7XIMUHqdxxvlV5bxxM/wEC9rVQqsnpSiT2mkmVqgEw+uDnicf/MmTMxY8YMozOdPHkyOEyaNAkTJ06UScZSOpYnIDMzEzk5OcjOzjYmW1Zqv/3BpCUJCA8Px+rVq7Fp0ybwMPXTLJYjYN68eSgsLAQtzn2ace/Pm6U64S1btuDcuXP/N+AzC5apAVu3bpUe77PjPJLh/V9enKOlDNAmjjFnWL9+PUd7FCuNgtjRgK51yKS/aNEiwkRO6urqxMaNG0VMTIyT39OnT5dKZMeOHU7vyvgZCB3lNSAiIgIHDx6kvA0tpaWlWL58ubEM7UrbfkrCVZxVnyknYOXKlcayw1AAXbhwAcuWLQPtarlVHY4EKO+E161b5xZQxwg6YuIRfNYdjnMEpQSMHj0avBs2lNy5cwdnzpwZSm1YzoSVEsCTLJmFMdllZj41JyPR0dEyaqboKCUgISFBKpOe2n3HBPj4oozwsrdVRCkBtL8rhYNMieWZ8+LFi6XSmzNnDkaM4PMI6kUpAXTmUwoB2kQ3JlmelGnDvv+wric9juMtSVmyhkrLH/HKJiVZWVlSEydW2rBhg1s/aQlDOh274qVLlwQtb7tNk4A1K840Q04Zos140dvba8fE42dXV5egFVJBTUd/OrQPIPbu3evxPU+Re/bs6U/LRMAH21RHAGeaRjieMHKKq6qqErRJL06fPi3oixlO8d4+sMAZIbUEcNOiUmjkNLhEmn2vloDIyEhx7949v3Fw8eJFo4bIJLh7926zwXZlTy0B3AzRdqN0X+AJ2OLiYkGza8GkXr161a0qnZ4QtGztCgwVz9QTwCTk5uYKBsZX2b9/v6DFuH4A6UyRqK6udkqODngJCzQ7/X5S3q1BAPvB+wJXrlxxAs3Tg7KyMsHfJXOVDzo5MeDcKa0pidmzZ7vUdXyfjg2J1DiI59IgXsmCePXLEHn0mZMKkTDOv3jxdJAd8lqSY4DrDUBvr9evenyBj56sWrXKCPSNGfCCnaPQsBXUvIAPbxUVFeHEiROO0U7XPOs9cOCAsXOWl5dnfDop9T1IiQW+/xXga3TSfWKkOy3gj+8B3/iV+3hvYnwm4P3XbL9Anf8W8Na/vTHpnS4f3OXzPnzssKWlxTgjSjXCu0QktHetAL5HKxkhEmsDnTSBH/WKRKISKj4TkDge+BNtv37uSeDvN4D8PwN/rZKwaFGVl2lVvPVj4E4Tfe+4A2im0Eb3wUTIdPqZ7Re/CLz0jM35FtoTGrvJPxnxmQA2HxEGvLEG+GaGzZmKu8AvSoHf/QPo7PKPg1ZJZewo4MHrNm/OXQO+9HP/eCZR4dwbYpC/VQhsfpP6AmoVZtEq76+fB+7uBn72dSDJ87F/9wkrjmG/f5ADHH8JGN/XF0QTAXZ59yP71eN/PlYNcDSfReeo3ngBmEJNk12YlGI6sv8b+rcFpyuBdqrSVpXYMcCqdFttznzK5iX7n7ETeO828PzTlI8Xbc+X7gdO/sc/OfEbAezOKGqSfvIcQMM2BHHKDtLVA5z9AHibCHmbnL9e7xCp6DKaBljL0wj0LwBcgIIdfL7RCLz8W+AMFRyWQmpqvz0XuEnPU34IdFN+/CF+JcDu0NNUgn65Gkjz8E2gqvs2Mt75EPgnlbC7D+xvB+6TxvCYmwzMozD3swCN9Z0KSncv8HoJ8Fox0NHXj4XSb4LcK6DmiAh79bitn/OXlwEhgJ3jDSeu0j9+FphG4+uhpLaNiLhlq+4f1gFcAm80ADXNQA+B4o3EUHOSNJ5KaozNNo9iuFA86WErmFobFF0CflQEVFQPtEYTMmPEV9NCaW79hJiBWr7dBYwAuzs8jHshkzrqbGAqAeKtcDvc9F+gjgh60GHLvL1kcjMXFgJEhtM/QKDdzbER9I9ZougZlVhZ6aH0j18AfvoX4H0axbkSHm7TLNgY+ZRfd6Xh+7OAE2B3jWsEt7PfnQ9wieJqrVLutwJ/uAjsKwU+qnfvCfcT9wuAvN9TP/COez1fY0wjwNFBHnFwx/fsLOAZao/NIqORatKb/wKOUYn/Gw0IZJZRRoYCcxKBd6mvCoQoIcAxI1HUbHx1JpBNIZ0yOmOS3HKAYxrurhvagTJqMspo3M4Alt/w3+jFnU1vnysnYLDDXOJmxQOfT7B1oHFjgTjqPGOfAMYQWWOorQ+ndp/7ho8f0XIB/VxcIwFdT4E7bW5OOHCH/kHt4NStd285AmQg4o7d25GRTLoqdIYlASqACpRNKktaVCKgCVCJPtnWBGgCFCOg2LyuAZoAxQgoNq9rgCZAMQKKzesaoAlQjIBi87oGaAIUI6DYvK4BmgDFCCg2zzWAtru1KEKgjQmoVGRcmyXsmYBCjYQyBAzseVeshALtsupgIgaMOWNvCJ0d0yQQBmYVQgafMR8gzEYuhXIKdGzJNGfMyrRqO4wpY8sY95f8/wEKvBLprcz3zwAAAABJRU5ErkJggg== + + + + + +https://www.amazon.in/ + diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/ta/default.txt b/mobile/ios/Client/Assets/Search/SearchPlugins/ta/default.txt new file mode 100644 index 0000000000..ced95a5cb9 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/ta/default.txt @@ -0,0 +1 @@ +Google \ No newline at end of file diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/ta/eBay-in.xml b/mobile/ios/Client/Assets/Search/SearchPlugins/ta/eBay-in.xml new file mode 100644 index 0000000000..8d27d340f9 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/ta/eBay-in.xml @@ -0,0 +1,17 @@ + + + +eBay India +UTF-8 +data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAAEEfUpiAAAEWklEQVRYw8WXfUxVdRjHP79z7guXBBTEEBXEraktCydjWi7RZW82lr340kZNXaX+41p/tF42Xakw3ZqNyeZMnZVYtjnLNLXMzJFLA1FrgiIioveCIvcyLvdyzz3n6Q+7FxIM5YJ8/jovv/2e7/Oc5+X8FICIbCKCiCyTLmgArcUbmb2ugirP3u4rlIjkA3P+3aFIiYjQBa21eCMARv2XFB5Mo9sKRORZ+R80IMv0NAJQ7Q4CYDYdJmwGehZ+O0pEXEA7d0B1i0UnbUqpd7urvg2NXtAAQhWnmF5Ug9tnQDiIZYUBCBo+NPf0WQTLfqc1aDEyyU77kWyCB9Kpa/6NOHsS3Mm9tQcejLoZk0gzEgehb2zp9VP0Ro8S522qv2sJGkDLR6uwmpvBsvA8n8+3f/oA+Oakly3HbhI6swKA9u9dFB5MA6DKs5drvtO3Noh/4Tm8a9fRunUbzkmTGJGgs3ZfIx/sbiT/YQPTdxbjUgnKkcL7z3goPJjG0PhM0pMeo19iYBIrIvJaH4x3iIgeyYNUYAWQepc2jyqlSiPWX+mr/wAqhiwEWKPFGL6sWDfoXm1Ltjf0bYNQ+SnEMPB4b7WLX6vbOmu2uQyrrZqg4Ys+izRGDaBxzlwcUybjXbkaEYuSIzfIGz8E9eZZQudWoidPQ+mJ/F25nNrrRwBY/3MWADYAfXgyrcUb0UeOQNpDLJ85HIAnH4rHrP8CQ9nQEibyiH0In1Ys5KXsLSyadqhL1ztfgy1rHP5du8HpJOu9Kjb8dJ3aGyHE9KMSJhA6XwhiMT/na3ZXLiEt8dFoGt8TJy5vFrfvTOR2R6yJVNr/eXCPhJSINN1DFd7O5EggvX0oxg97aipeGXjCIrJaRByRduACGoBk7j85GlAwSMYBVmiAzuChawwydyVgzmd1TC+qGRABtsiF5fXSsmoNofIKbBPGI01NWIEAQws/AZJpCVjMWHeRKy0GLodGlbuDd55KYf2r6VjXD9Nxcj44UtDiM7C8FcgDmWxTJgnOUbwxdX+XarPYXJZHYtwoFuTsRInIMqDEnfc09swxOPPyOgdBXS3tPxxg8YubONfmoK5oAkp1qs/++AJpiXZ+XJ5IuGEXVls1yriBWCFMz3600QvZ7j+Hy5bI61P3IWLxedlMklzpzJuyE6A0GgHb2AyU3UHCooKogY7ySvSx49B8Q7jW4Oe8J8j4kXEA1N80uNDYweKJlQT2L8D5+CHs45be8jLgJnBtD0op3nriKFuPz2bHyZcxwn4SnGkR40T6wDKgBMC8epXgsTLCl6+AaeLIzcE1K499f/nRFIxJtvPVcS++YJjMFAdvz0hhWLyO+C8Sqi1Ggo3orrHoGQVI4DKixWFLnYlpGWz4ZSKjh+Uyf0rpf6ZBzD93vWFZYfnij3zZc3ppT6932Aa6zJTSKcj9bsDGWb/0gUuDaL8mMgnnyv2nMHpc7TKSU4FcIBvI6GdvbwLlwAmlVPQU9g9eOPBg6mUX4QAAAABJRU5ErkJggg== + + + + + + + +http://search.ebay.in/ + diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/ta/list.txt b/mobile/ios/Client/Assets/Search/SearchPlugins/ta/list.txt new file mode 100644 index 0000000000..5c8c024bee --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/ta/list.txt @@ -0,0 +1,8 @@ +amazon-in +bing +duckduckgo +eBay-in +google +wikipedia-ta +wiktionary-ta +yahoo-in diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/ta/wikipedia-ta.xml b/mobile/ios/Client/Assets/Search/SearchPlugins/ta/wikipedia-ta.xml new file mode 100644 index 0000000000..fde6a35718 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/ta/wikipedia-ta.xml @@ -0,0 +1,23 @@ + + + +விக்கிப்பீடியா (ta) +UTF-8 +data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAQAAABIkb+zAAAEXElEQVR4Ae3cA4xs2QJG4b9aj41n27Zt27Zt27Zt27Zt+2KM5lXP924qOzs56VPjKSRnrfhv7NUsp08v98+PsxwT4nJ+nPunl8KZ8/WYQL+eMydJrx5/EhN603lAHpVJ5bzZNp035uyZXM7Uy3LmM7ms9CITzVRGQRfQBXQBXUAX0AV0AV1AF9AFdAFdQBfQBXQBXcBMjgU7M5+1vqtZyXqmc/IsZDEL+53LP3KRPCCv62+rWd7vnpx6v/M5Vd935OF5R+6RtRyeQ3NYNjKTxZw2p8tiduR8uWfemY3+dkjWM5fFLPU/8q9yrRwbHLP39WkHamPD5z1WnNMb/dpW/uAdbipu413+qcl2H3N3cXFv9DNNtvmg68uxMY6dPfewTZNvOauouqEdULlTY532IgqHua+ZxnoHm4ANj63biRNQPZedVPY5nTR1BXsVbNuyTlsDqy4tW/wK4NYiJ01AXMlm+wGrL1cDnWbLejC4m7T4DvAOOSkD4v0qztGyLzpUweOk4UXAp6XV72DNmU7qgPPao+DJ0uLTFPzXXGN5I9acU1o8vb14jZzUAfFeBdvMtuwLDlfwIKme2248T1p9MvY59zACLqcy4Kf5eQr+ZbYRfrAFaXHaf/FRGUZAfE/Bzwf8OKwpuJ/0vYRNPEpavT248rACbqfimtLiqxT8w4yIz+Of5qTV7+LHMqyAaf9R8Clp8ex2K7iXuAa4q7R6aXCH4QXE4xVsOq+0+DYFfzXth/ilnrT6Tvzb9DADlqwqDPjTd377FHwI3EBaPb0NPFaGGRBvoLBiUVr8EFS+KgN8KpYtDjvgQo6i8Hhp8ZJUjnIZaXXGNrxKhh0QX6TwHzPS4uco/FtPWr0T9jnPKAJuOOiCc/UqVG4jrf4AH5dRBPT8UWHg3/BvUfhl635ZcNXRBMSDqFxFWvwklVu07O/BT2RUAad0GIWPtexXgMpPt+xntAt3Gl1AvJjCPufasn4d/6NyY2n4DPzXzCgDzmEvhVdIw+tj02X8nsIPGvus7Xi8jDIgPkLhSPON5ad4t7gbletJ9S71n+BIA65C5dGal1g3nENM+weF70j1R3iNjDogfkrhn6br5dU/4UXS94EtF7+vUC8IjjzgrlRuJ33v27j2dTLbKXxd+r4Pn5RRB9RfxsL3y4H/i0dI9TFUrirOZDeuPh4B8TQqVyjH/ZtZqZ7KIRS+JJ6Fn8k4BNRL9IUPmncwbr/1b37lanbiLuMTEG+nsNfb8cOWq0DLFA7E/8yMU8AlNLnaoP/alSfKOAXEN6h8Qlo8k3UKq5bGLeAWFPa6oLT6Ogqvk3ELmPJ3wOtlgOe0B2w6//gFxCPBsjPIQN8JPi3jGDDvSDxVDPaCNnFNOdGczrNzIrEnRyR5XPZlMIdmKofnpTnx6B783QV0ARNOF9AFdAFdQBfQBXQBXUAX0AV0AV3A6OgCVjLJrEzlj5lk/jiVt2eSefvEvzTJxL84zMS/PM//Af1pBRpQAkXBAAAAAElFTkSuQmCC + + + + + + + + + + + + + +https://ta.wikipedia.org/wiki/சிறப்பு:Search + diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/ta/wiktionary-ta.xml b/mobile/ios/Client/Assets/Search/SearchPlugins/ta/wiktionary-ta.xml new file mode 100644 index 0000000000..01d859dec0 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/ta/wiktionary-ta.xml @@ -0,0 +1,17 @@ + + + +விக்சனரி (ta) +data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAYAAADimHc4AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAA2hpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMC1jMDYxIDY0LjE0MDk0OSwgMjAxMC8xMi8wNy0xMDo1NzowMSAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtcE1NOk9yaWdpbmFsRG9jdW1lbnRJRD0ieG1wLmRpZDpGRTdGMTE3NDA3MjA2ODExOTJCMEU1MkZGMDQ2RkQ0QyIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDozNURGRTgzRTMwMzMxMUU1OTlBMUM2MDY0MDI4RjgwMSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDozNURGRTgzRDMwMzMxMUU1OTlBMUM2MDY0MDI4RjgwMSIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ1M1LjEgTWFjaW50b3NoIj4gPHhtcE1NOkRlcml2ZWRGcm9tIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6MDA4MDExNzQwNzIwNjgxMTkyQjBFNTJGRjA0NkZENEMiIHN0UmVmOmRvY3VtZW50SUQ9InhtcC5kaWQ6RkU3RjExNzQwNzIwNjgxMTkyQjBFNTJGRjA0NkZENEMiLz4gPC9yZGY6RGVzY3Jp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 + + + + + + + + +https://ta.wiktionary.org/wiki/சிறப்பு:Search + diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/ta/yahoo-in.xml b/mobile/ios/Client/Assets/Search/SearchPlugins/ta/yahoo-in.xml new file mode 100644 index 0000000000..67e95519df --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/ta/yahoo-in.xml @@ -0,0 +1,22 @@ + + + +Yahoo +UTF-8 +data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAYAAADimHc4AAAAAXNSR0IArs4c6QAACJ5JREFUeAHtXWlsVFUUPm9mOt2gLWKXIFtLKRZIGoIS/CEJa7ASEExdECPYVhYjiAlq0CAgIJBIABUFWkqKEaGiEKRAoAIKBEGBmNJCW2ihQGnL0oW2dJmO9wyly3DPw/bN2+SehPS9c+bee873vbvf95CgSZzglOIgPZ7dJjgB+rO/nR/YxF+PIFApAWSxnJLSIDZZAonBDMB0AHFwKMwJNd8zEkbivRB1EWDgZ0jgOyUNht+w4JMvwFcXcPfc8UFvwlySXob0RADnBvcfiXv1EWDNT6JFAie2+0L0QSCBNUGuDlef4h/zUhF7C8NAjHb0exA6IwFCdERAEKAj+Fi0IEAQoDMCOhcvaoAgQGcEdC5e1ABBgM4I6Fy8qAGCAJ0R0Ll4UQN0JsCmdvmDYoMh8tlAuH6hCipK61xbQE9084GQcF8IjfCDAxsK4cKxO2q7wc1/8LgQwH/Fl6qhtKAaHA1OsNkt4O1vheCevvBPxk3I/l1d31Qn4OKpcpidGgOdu9q5IPSOCYAPBx+FRgfXrJrS5m2B+K/6Q0hvP24ZJYyQX1Zc5No8qVS9CcKnPnXeedJnJGBUYk/SrpZh7Lu9SPCxzOTZWVBX3ahW8c35qk4AlnQo5SpkHr7VXKj7xWufR4F/F9UrY3OxASF2iFsQ2XzvfnFyVzH8vbvEXa3KvSYEoOfrp2dCfS2/nQl40g6vLIxSJUBepm980Q/8A714JqitdsCm2ee4NjWUmhFQlFMFO5bRberYWT2he/9OasTYJs/IIYEwYlr3NrrWN2mLc+HmlXutVapea0YARrFz+UW4dv4uNyCrzQLTVkdzbZ5SSizahK8HgCS5TuM8lG1hViXsXpX/kF5NhaYENNQ5Yf2MTDKemNHB8Mz4ENKu1DDi7R5sSBxEZrNhZiY46l3npcjfeNqgKQHofNaR23BkyzUyjqmrogGHiJ4W7OSx7afkcOpV1cf8vLI9HymvFDdd6rxsqCqvd9Pevw3r4w8T5oVzbUqUk5f1A+zseXL3Tr3sUJmXxlM6XQgoL66DrZ/kkDFMmh8Jwb19SXt7Ddjxjn6Hnmv8MP8CVJSwWboOogsBGOf+by/DpdPl3JC9fa2sQ8bzwcoFO97EdQPBwo6g8ST3ZBlbDrnCM2mi040AJ5tkbpyVCU4nv9MbMiEUcB1JqYyZ0RP6DA7kZuNwOAE7XvRFL9GNAAw4989yOJhUSMYev7Y/ePl03MWAYDtMXkp3vPvXXYb80xVk+VoYOh6dh7zDvqC6gu6QX/ooosMlTVnBZrxB/Blvxc06+HEB3Q91uNB2JtSdAFys+2lJHun2xI/7QEhE+zvkqOeCYPhUesa79dMLUF3WQJarlUF3AjDQ9DUFUJRXxY3Z7mOF+LUDuDZKabGyGe839Iw3/2wFHNxIN31UvmroDUEAzpA3f5BNxjf4xRAYMjGUtLsbxszsBRGD+B0v/nbTnHO6dryt/TUEAegQLv+e2Vfa2rc219gh+3Rij/YjJCjMDq8voVdWj20v0mXGS7ltGALQwZT3s6Chnj8m7NrdF15d1JeKo1n/1pfR9FJzjQO2sFm4kcRQBOC+cfraAhKf2Dnh0CuGfp1h4Iiu8Pzkp8j0u1Ze0nSpmXSklcFQBKBfaYvzoKy4tpWLLZdWqwTTvxvY9G5nix6vrF6Sa6m5rbbl7mZhDexcSe9HtPxS2yvDEVBT0QC4NkNJ1NAu3OHluLnh0D2a3tDBfWkt9ngpvym94QhAR39je8h5p8oon13Lyn6BLXvIXXv4yO7xZv1xG45vKyLz09NgSAKALQ/hqQRKgkK94c2VTzebceHOx7+FkGYDu2hsdELKHDqv1r/V49qYBDAkck+UwYmfb5CYjErsAdHDurgW7IZOCiN/l5FcCPln9F3vIZ1jBvai9h7+cqRcKo1suASxJmsYeHnzx//Xc+6yztcCoeH8w1W46fNe3yP3T+Rp5HN7izFsDcBASi7VwK+rC8iYukV1IsHHRGmLcg0NPvpoaALQwR1L6WEp2inB2rH368uU2TB6wxNwr9IhOyylkMS1Ja1POFC+yOkNTwA6f2jzVXL7khfc2f2lcHoPva7ES6OXzhQE4Jbhpv84lHQ0NELKXOMOO92JNgUB6PT5o3fgeNqjJ1P71l2Ba9n8vQX34I1wbxoCEKxtn+W4JlYUcJW36mD7Qv23GSn/eHpTEYBP9il2dJwSPPxbdUf/bUbKP57eVARgAJmH6PcMSvKreTEaWmc6Aupr+Rs2iLKFLVebTUxHAHXCDYG32gQBqj+AeNSQEkEAhYwH9RJxxhOLsIga4EGkiaxsbOuREnzLxmxiOo99OvM3XhB4PJBlNjEdAXLtvKgBGjx+ckNN0QfoTIBc7dDAtQ4V8b9qguRqR4fQ0SCR6QiQA5l620YDHDtchOkIwBNwlIhOmELGg3rq/A8WYbPT5HjQBY9mZboagB9TosTGjqiYTUznsUXGY7nmyajEyIRjTJeJt1pdzuIhLbOJ6TyWW4wTfYAGj5/cMFT0ARoQILsaKjNE1cC1DhVhuibIzr4jQYnoAyhkPKi3+9EE4Dc/zSam89hL5mNOcs2TUYkxHQFyK55mnAfQ20tGfWSa/MJPzdRWNbhe0MBagaclzDgKMh0By8f/BfeqHFDJvnbS/J0ftgQUFOYN3jL9g1GfJ9MRUJJf8zCW7CWrsiL+u8UP/9hYGtP1AcaCT7k3ggDlGCrKQRCgCD7liQUByjFUlIMgQBF8yhMLApRjqCgHQYAi+JQnFgQox1BRDoIARfApTywIUI6hohyQgEpFOYjEShCoxPdNzPNauZJQDZgWsccakGRA3x4Xl5IkJzilONh7gP0d+bhEbYQ4JZAy0uCF0awJkpwS+E5BhREcexx8QKybMGfYN8n9mpAez24T2PI6/vcV9BdSHyQSf9uDQGVTf5uUBrHJ+OBj4n8BJ5EPp7ErQgIAAAAASUVORK5CYII= + + + + + + + + + + + + +https://in.search.yahoo.com/ + diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/te/amazon-in.xml b/mobile/ios/Client/Assets/Search/SearchPlugins/te/amazon-in.xml new file mode 100644 index 0000000000..05c5311e31 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/te/amazon-in.xml @@ -0,0 +1,15 @@ + + + +Amazon.in +utf-8 +data:image/png;base64,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 + + + + + +https://www.amazon.in/ + diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/te/default.txt b/mobile/ios/Client/Assets/Search/SearchPlugins/te/default.txt new file mode 100644 index 0000000000..ced95a5cb9 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/te/default.txt @@ -0,0 +1 @@ +Google \ No newline at end of file diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/te/eBay-in.xml b/mobile/ios/Client/Assets/Search/SearchPlugins/te/eBay-in.xml new file mode 100644 index 0000000000..8d27d340f9 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/te/eBay-in.xml @@ -0,0 +1,17 @@ + + + +eBay India +UTF-8 +data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAAEEfUpiAAAEWklEQVRYw8WXfUxVdRjHP79z7guXBBTEEBXEraktCydjWi7RZW82lr340kZNXaX+41p/tF42Xakw3ZqNyeZMnZVYtjnLNLXMzJFLA1FrgiIioveCIvcyLvdyzz3n6Q+7FxIM5YJ8/jovv/2e7/Oc5+X8FICIbCKCiCyTLmgArcUbmb2ugirP3u4rlIjkA3P+3aFIiYjQBa21eCMARv2XFB5Mo9sKRORZ+R80IMv0NAJQ7Q4CYDYdJmwGehZ+O0pEXEA7d0B1i0UnbUqpd7urvg2NXtAAQhWnmF5Ug9tnQDiIZYUBCBo+NPf0WQTLfqc1aDEyyU77kWyCB9Kpa/6NOHsS3Mm9tQcejLoZk0gzEgehb2zp9VP0Ro8S522qv2sJGkDLR6uwmpvBsvA8n8+3f/oA+Oakly3HbhI6swKA9u9dFB5MA6DKs5drvtO3Noh/4Tm8a9fRunUbzkmTGJGgs3ZfIx/sbiT/YQPTdxbjUgnKkcL7z3goPJjG0PhM0pMeo19iYBIrIvJaH4x3iIgeyYNUYAWQepc2jyqlSiPWX+mr/wAqhiwEWKPFGL6sWDfoXm1Ltjf0bYNQ+SnEMPB4b7WLX6vbOmu2uQyrrZqg4Ys+izRGDaBxzlwcUybjXbkaEYuSIzfIGz8E9eZZQudWoidPQ+mJ/F25nNrrRwBY/3MWADYAfXgyrcUb0UeOQNpDLJ85HIAnH4rHrP8CQ9nQEibyiH0In1Ys5KXsLSyadqhL1ztfgy1rHP5du8HpJOu9Kjb8dJ3aGyHE9KMSJhA6XwhiMT/na3ZXLiEt8dFoGt8TJy5vFrfvTOR2R6yJVNr/eXCPhJSINN1DFd7O5EggvX0oxg97aipeGXjCIrJaRByRduACGoBk7j85GlAwSMYBVmiAzuChawwydyVgzmd1TC+qGRABtsiF5fXSsmoNofIKbBPGI01NWIEAQws/AZJpCVjMWHeRKy0GLodGlbuDd55KYf2r6VjXD9Nxcj44UtDiM7C8FcgDmWxTJgnOUbwxdX+XarPYXJZHYtwoFuTsRInIMqDEnfc09swxOPPyOgdBXS3tPxxg8YubONfmoK5oAkp1qs/++AJpiXZ+XJ5IuGEXVls1yriBWCFMz3600QvZ7j+Hy5bI61P3IWLxedlMklzpzJuyE6A0GgHb2AyU3UHCooKogY7ySvSx49B8Q7jW4Oe8J8j4kXEA1N80uNDYweKJlQT2L8D5+CHs45be8jLgJnBtD0op3nriKFuPz2bHyZcxwn4SnGkR40T6wDKgBMC8epXgsTLCl6+AaeLIzcE1K499f/nRFIxJtvPVcS++YJjMFAdvz0hhWLyO+C8Sqi1Ggo3orrHoGQVI4DKixWFLnYlpGWz4ZSKjh+Uyf0rpf6ZBzD93vWFZYfnij3zZc3ppT6932Aa6zJTSKcj9bsDGWb/0gUuDaL8mMgnnyv2nMHpc7TKSU4FcIBvI6GdvbwLlwAmlVPQU9g9eOPBg6mUX4QAAAABJRU5ErkJggg== + + + + + + + +http://search.ebay.in/ + diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/te/list.txt b/mobile/ios/Client/Assets/Search/SearchPlugins/te/list.txt new file mode 100644 index 0000000000..ca3fc758d5 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/te/list.txt @@ -0,0 +1,7 @@ +amazon-in +bing +eBay-in +google +wikipedia-te +wiktionary-te +yahoo-in diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/te/wikipedia-te.xml b/mobile/ios/Client/Assets/Search/SearchPlugins/te/wikipedia-te.xml new file mode 100644 index 0000000000..241b698ed6 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/te/wikipedia-te.xml @@ -0,0 +1,23 @@ + + + +వికీపీడియా (te) +UTF-8 +data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAQAAABIkb+zAAAEXElEQVR4Ae3cA4xs2QJG4b9aj41n27Zt27Zt27Zt27Zt+2KM5lXP924qOzs56VPjKSRnrfhv7NUsp08v98+PsxwT4nJ+nPunl8KZ8/WYQL+eMydJrx5/EhN603lAHpVJ5bzZNp035uyZXM7Uy3LmM7ms9CITzVRGQRfQBXQBXUAX0AV0AV1AF9AFdAFdQBfQBXQBXcBMjgU7M5+1vqtZyXqmc/IsZDEL+53LP3KRPCCv62+rWd7vnpx6v/M5Vd935OF5R+6RtRyeQ3NYNjKTxZw2p8tiduR8uWfemY3+dkjWM5fFLPU/8q9yrRwbHLP39WkHamPD5z1WnNMb/dpW/uAdbipu413+qcl2H3N3cXFv9DNNtvmg68uxMY6dPfewTZNvOauouqEdULlTY532IgqHua+ZxnoHm4ANj63biRNQPZedVPY5nTR1BXsVbNuyTlsDqy4tW/wK4NYiJ01AXMlm+wGrL1cDnWbLejC4m7T4DvAOOSkD4v0qztGyLzpUweOk4UXAp6XV72DNmU7qgPPao+DJ0uLTFPzXXGN5I9acU1o8vb14jZzUAfFeBdvMtuwLDlfwIKme2248T1p9MvY59zACLqcy4Kf5eQr+ZbYRfrAFaXHaf/FRGUZAfE/Bzwf8OKwpuJ/0vYRNPEpavT248rACbqfimtLiqxT8w4yIz+Of5qTV7+LHMqyAaf9R8Clp8ex2K7iXuAa4q7R6aXCH4QXE4xVsOq+0+DYFfzXth/ilnrT6Tvzb9DADlqwqDPjTd377FHwI3EBaPb0NPFaGGRBvoLBiUVr8EFS+KgN8KpYtDjvgQo6i8Hhp8ZJUjnIZaXXGNrxKhh0QX6TwHzPS4uco/FtPWr0T9jnPKAJuOOiCc/UqVG4jrf4AH5dRBPT8UWHg3/BvUfhl635ZcNXRBMSDqFxFWvwklVu07O/BT2RUAad0GIWPtexXgMpPt+xntAt3Gl1AvJjCPufasn4d/6NyY2n4DPzXzCgDzmEvhVdIw+tj02X8nsIPGvus7Xi8jDIgPkLhSPON5ad4t7gbletJ9S71n+BIA65C5dGal1g3nENM+weF70j1R3iNjDogfkrhn6br5dU/4UXS94EtF7+vUC8IjjzgrlRuJ33v27j2dTLbKXxd+r4Pn5RRB9RfxsL3y4H/i0dI9TFUrirOZDeuPh4B8TQqVyjH/ZtZqZ7KIRS+JJ6Fn8k4BNRL9IUPmncwbr/1b37lanbiLuMTEG+nsNfb8cOWq0DLFA7E/8yMU8AlNLnaoP/alSfKOAXEN6h8Qlo8k3UKq5bGLeAWFPa6oLT6Ogqvk3ELmPJ3wOtlgOe0B2w6//gFxCPBsjPIQN8JPi3jGDDvSDxVDPaCNnFNOdGczrNzIrEnRyR5XPZlMIdmKofnpTnx6B783QV0ARNOF9AFdAFdQBfQBXQBXUAX0AV0AV3A6OgCVjLJrEzlj5lk/jiVt2eSefvEvzTJxL84zMS/PM//Af1pBRpQAkXBAAAAAElFTkSuQmCC + + + + + + + + + + + + + +https://te.wikipedia.org/wiki/ప్రత్యేక:అన్వేషణ + diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/te/wiktionary-te.xml b/mobile/ios/Client/Assets/Search/SearchPlugins/te/wiktionary-te.xml new file mode 100644 index 0000000000..e556ec7344 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/te/wiktionary-te.xml @@ -0,0 +1,17 @@ + + + +విక్షనరీ (te) +data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAYAAADimHc4AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAA2hpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMC1jMDYxIDY0LjE0MDk0OSwgMjAxMC8xMi8wNy0xMDo1NzowMSAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtcE1NOk9yaWdpbmFsRG9jdW1lbnRJRD0ieG1wLmRpZDpGRTdGMTE3NDA3MjA2ODExOTJCMEU1MkZGMDQ2RkQ0QyIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDozNURGRTgzRTMwMzMxMUU1OTlBMUM2MDY0MDI4RjgwMSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDozNURGRTgzRDMwMzMxMUU1OTlBMUM2MDY0MDI4RjgwMSIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ1M1LjEgTWFjaW50b3NoIj4gPHhtcE1NOkRlcml2ZWRGcm9tIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6MDA4MDExNzQwNzIwNjgxMTkyQjBFNTJGRjA0NkZENEMiIHN0UmVmOmRvY3VtZW50SUQ9InhtcC5kaWQ6RkU3RjExNzQwNzIwNjgxMTkyQjBFNTJGRjA0NkZENEMiLz4gPC9yZGY6RGVzY3Jp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 + + + + + + + + +https://te.wiktionary.org/wiki/ప్రత్యేక:అన్వేషణ + diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/te/yahoo-in.xml b/mobile/ios/Client/Assets/Search/SearchPlugins/te/yahoo-in.xml new file mode 100644 index 0000000000..67e95519df --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/te/yahoo-in.xml @@ -0,0 +1,22 @@ + + + +Yahoo +UTF-8 +data:image/png;base64,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 + + + + + + + + + + + + +https://in.search.yahoo.com/ + diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/th/default.txt b/mobile/ios/Client/Assets/Search/SearchPlugins/th/default.txt new file mode 100644 index 0000000000..ced95a5cb9 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/th/default.txt @@ -0,0 +1 @@ +Google \ No newline at end of file diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/th/list.txt b/mobile/ios/Client/Assets/Search/SearchPlugins/th/list.txt new file mode 100644 index 0000000000..ecbd34be3c --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/th/list.txt @@ -0,0 +1,3 @@ +google +twitter +wikipedia-th diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/th/wikipedia-th.xml b/mobile/ios/Client/Assets/Search/SearchPlugins/th/wikipedia-th.xml new file mode 100644 index 0000000000..fa0cabb0f9 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/th/wikipedia-th.xml @@ -0,0 +1,23 @@ + + + +วิกิพีเดีย +UTF-8 +data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAQAAABIkb+zAAAEXElEQVR4Ae3cA4xs2QJG4b9aj41n27Zt27Zt27Zt27Zt+2KM5lXP924qOzs56VPjKSRnrfhv7NUsp08v98+PsxwT4nJ+nPunl8KZ8/WYQL+eMydJrx5/EhN603lAHpVJ5bzZNp035uyZXM7Uy3LmM7ms9CITzVRGQRfQBXQBXUAX0AV0AV1AF9AFdAFdQBfQBXQBXcBMjgU7M5+1vqtZyXqmc/IsZDEL+53LP3KRPCCv62+rWd7vnpx6v/M5Vd935OF5R+6RtRyeQ3NYNjKTxZw2p8tiduR8uWfemY3+dkjWM5fFLPU/8q9yrRwbHLP39WkHamPD5z1WnNMb/dpW/uAdbipu413+qcl2H3N3cXFv9DNNtvmg68uxMY6dPfewTZNvOauouqEdULlTY532IgqHua+ZxnoHm4ANj63biRNQPZedVPY5nTR1BXsVbNuyTlsDqy4tW/wK4NYiJ01AXMlm+wGrL1cDnWbLejC4m7T4DvAOOSkD4v0qztGyLzpUweOk4UXAp6XV72DNmU7qgPPao+DJ0uLTFPzXXGN5I9acU1o8vb14jZzUAfFeBdvMtuwLDlfwIKme2248T1p9MvY59zACLqcy4Kf5eQr+ZbYRfrAFaXHaf/FRGUZAfE/Bzwf8OKwpuJ/0vYRNPEpavT248rACbqfimtLiqxT8w4yIz+Of5qTV7+LHMqyAaf9R8Clp8ex2K7iXuAa4q7R6aXCH4QXE4xVsOq+0+DYFfzXth/ilnrT6Tvzb9DADlqwqDPjTd377FHwI3EBaPb0NPFaGGRBvoLBiUVr8EFS+KgN8KpYtDjvgQo6i8Hhp8ZJUjnIZaXXGNrxKhh0QX6TwHzPS4uco/FtPWr0T9jnPKAJuOOiCc/UqVG4jrf4AH5dRBPT8UWHg3/BvUfhl635ZcNXRBMSDqFxFWvwklVu07O/BT2RUAad0GIWPtexXgMpPt+xntAt3Gl1AvJjCPufasn4d/6NyY2n4DPzXzCgDzmEvhVdIw+tj02X8nsIPGvus7Xi8jDIgPkLhSPON5ad4t7gbletJ9S71n+BIA65C5dGal1g3nENM+weF70j1R3iNjDogfkrhn6br5dU/4UXS94EtF7+vUC8IjjzgrlRuJ33v27j2dTLbKXxd+r4Pn5RRB9RfxsL3y4H/i0dI9TFUrirOZDeuPh4B8TQqVyjH/ZtZqZ7KIRS+JJ6Fn8k4BNRL9IUPmncwbr/1b37lanbiLuMTEG+nsNfb8cOWq0DLFA7E/8yMU8AlNLnaoP/alSfKOAXEN6h8Qlo8k3UKq5bGLeAWFPa6oLT6Ogqvk3ELmPJ3wOtlgOe0B2w6//gFxCPBsjPIQN8JPi3jGDDvSDxVDPaCNnFNOdGczrNzIrEnRyR5XPZlMIdmKofnpTnx6B783QV0ARNOF9AFdAFdQBfQBXQBXUAX0AV0AV3A6OgCVjLJrEzlj5lk/jiVt2eSefvEvzTJxL84zMS/PM//Af1pBRpQAkXBAAAAAElFTkSuQmCC + + + + + + + + + + + + + +https://th.wikipedia.org/wiki/พิเศษ:ค้นหา + diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/tr/default.txt b/mobile/ios/Client/Assets/Search/SearchPlugins/tr/default.txt new file mode 100644 index 0000000000..c4735ea8a6 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/tr/default.txt @@ -0,0 +1 @@ +Yandex \ No newline at end of file diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/tr/list.txt b/mobile/ios/Client/Assets/Search/SearchPlugins/tr/list.txt new file mode 100644 index 0000000000..a222fe9cf4 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/tr/list.txt @@ -0,0 +1,5 @@ +google:hidden +google-nocodes +yandex-tr +twitter +wikipedia-tr diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/tr/wikipedia-tr.xml b/mobile/ios/Client/Assets/Search/SearchPlugins/tr/wikipedia-tr.xml new file mode 100644 index 0000000000..6db21a8eb0 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/tr/wikipedia-tr.xml @@ -0,0 +1,23 @@ + + + +Wikipedia (tr) +UTF-8 +data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAQAAABIkb+zAAAEXElEQVR4Ae3cA4xs2QJG4b9aj41n27Zt27Zt27Zt27Zt+2KM5lXP924qOzs56VPjKSRnrfhv7NUsp08v98+PsxwT4nJ+nPunl8KZ8/WYQL+eMydJrx5/EhN603lAHpVJ5bzZNp035uyZXM7Uy3LmM7ms9CITzVRGQRfQBXQBXUAX0AV0AV1AF9AFdAFdQBfQBXQBXcBMjgU7M5+1vqtZyXqmc/IsZDEL+53LP3KRPCCv62+rWd7vnpx6v/M5Vd935OF5R+6RtRyeQ3NYNjKTxZw2p8tiduR8uWfemY3+dkjWM5fFLPU/8q9yrRwbHLP39WkHamPD5z1WnNMb/dpW/uAdbipu413+qcl2H3N3cXFv9DNNtvmg68uxMY6dPfewTZNvOauouqEdULlTY532IgqHua+ZxnoHm4ANj63biRNQPZedVPY5nTR1BXsVbNuyTlsDqy4tW/wK4NYiJ01AXMlm+wGrL1cDnWbLejC4m7T4DvAOOSkD4v0qztGyLzpUweOk4UXAp6XV72DNmU7qgPPao+DJ0uLTFPzXXGN5I9acU1o8vb14jZzUAfFeBdvMtuwLDlfwIKme2248T1p9MvY59zACLqcy4Kf5eQr+ZbYRfrAFaXHaf/FRGUZAfE/Bzwf8OKwpuJ/0vYRNPEpavT248rACbqfimtLiqxT8w4yIz+Of5qTV7+LHMqyAaf9R8Clp8ex2K7iXuAa4q7R6aXCH4QXE4xVsOq+0+DYFfzXth/ilnrT6Tvzb9DADlqwqDPjTd377FHwI3EBaPb0NPFaGGRBvoLBiUVr8EFS+KgN8KpYtDjvgQo6i8Hhp8ZJUjnIZaXXGNrxKhh0QX6TwHzPS4uco/FtPWr0T9jnPKAJuOOiCc/UqVG4jrf4AH5dRBPT8UWHg3/BvUfhl635ZcNXRBMSDqFxFWvwklVu07O/BT2RUAad0GIWPtexXgMpPt+xntAt3Gl1AvJjCPufasn4d/6NyY2n4DPzXzCgDzmEvhVdIw+tj02X8nsIPGvus7Xi8jDIgPkLhSPON5ad4t7gbletJ9S71n+BIA65C5dGal1g3nENM+weF70j1R3iNjDogfkrhn6br5dU/4UXS94EtF7+vUC8IjjzgrlRuJ33v27j2dTLbKXxd+r4Pn5RRB9RfxsL3y4H/i0dI9TFUrirOZDeuPh4B8TQqVyjH/ZtZqZ7KIRS+JJ6Fn8k4BNRL9IUPmncwbr/1b37lanbiLuMTEG+nsNfb8cOWq0DLFA7E/8yMU8AlNLnaoP/alSfKOAXEN6h8Qlo8k3UKq5bGLeAWFPa6oLT6Ogqvk3ELmPJ3wOtlgOe0B2w6//gFxCPBsjPIQN8JPi3jGDDvSDxVDPaCNnFNOdGczrNzIrEnRyR5XPZlMIdmKofnpTnx6B783QV0ARNOF9AFdAFdQBfQBXQBXUAX0AV0AV3A6OgCVjLJrEzlj5lk/jiVt2eSefvEvzTJxL84zMS/PM//Af1pBRpQAkXBAAAAAElFTkSuQmCC + + + + + + + + + + + + + +https://tr.wikipedia.org/wiki/Özel:Ara + diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/tr/yandex-tr.xml b/mobile/ios/Client/Assets/Search/SearchPlugins/tr/yandex-tr.xml new file mode 100644 index 0000000000..1fd00bcb7f --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/tr/yandex-tr.xml @@ -0,0 +1,22 @@ + + + +Yandex +Yandex Türkiye arama motoru +UTF-8 +data:image/png;base64,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 + + + + + + + + + + + + + diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/uk/default.txt b/mobile/ios/Client/Assets/Search/SearchPlugins/uk/default.txt new file mode 100644 index 0000000000..ced95a5cb9 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/uk/default.txt @@ -0,0 +1 @@ +Google \ No newline at end of file diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/uk/list.txt b/mobile/ios/Client/Assets/Search/SearchPlugins/uk/list.txt new file mode 100644 index 0000000000..ade00315aa --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/uk/list.txt @@ -0,0 +1,5 @@ +google:hidden +google-nocodes +twitter +wikipedia-uk +yandex-market diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/uk/wikipedia-uk.xml b/mobile/ios/Client/Assets/Search/SearchPlugins/uk/wikipedia-uk.xml new file mode 100644 index 0000000000..631bb644b3 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/uk/wikipedia-uk.xml @@ -0,0 +1,23 @@ + + + +Вікіпедія +UTF-8 +data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAQAAABIkb+zAAAEXElEQVR4Ae3cA4xs2QJG4b9aj41n27Zt27Zt27Zt27Zt+2KM5lXP924qOzs56VPjKSRnrfhv7NUsp08v98+PsxwT4nJ+nPunl8KZ8/WYQL+eMydJrx5/EhN603lAHpVJ5bzZNp035uyZXM7Uy3LmM7ms9CITzVRGQRfQBXQBXUAX0AV0AV1AF9AFdAFdQBfQBXQBXcBMjgU7M5+1vqtZyXqmc/IsZDEL+53LP3KRPCCv62+rWd7vnpx6v/M5Vd935OF5R+6RtRyeQ3NYNjKTxZw2p8tiduR8uWfemY3+dkjWM5fFLPU/8q9yrRwbHLP39WkHamPD5z1WnNMb/dpW/uAdbipu413+qcl2H3N3cXFv9DNNtvmg68uxMY6dPfewTZNvOauouqEdULlTY532IgqHua+ZxnoHm4ANj63biRNQPZedVPY5nTR1BXsVbNuyTlsDqy4tW/wK4NYiJ01AXMlm+wGrL1cDnWbLejC4m7T4DvAOOSkD4v0qztGyLzpUweOk4UXAp6XV72DNmU7qgPPao+DJ0uLTFPzXXGN5I9acU1o8vb14jZzUAfFeBdvMtuwLDlfwIKme2248T1p9MvY59zACLqcy4Kf5eQr+ZbYRfrAFaXHaf/FRGUZAfE/Bzwf8OKwpuJ/0vYRNPEpavT248rACbqfimtLiqxT8w4yIz+Of5qTV7+LHMqyAaf9R8Clp8ex2K7iXuAa4q7R6aXCH4QXE4xVsOq+0+DYFfzXth/ilnrT6Tvzb9DADlqwqDPjTd377FHwI3EBaPb0NPFaGGRBvoLBiUVr8EFS+KgN8KpYtDjvgQo6i8Hhp8ZJUjnIZaXXGNrxKhh0QX6TwHzPS4uco/FtPWr0T9jnPKAJuOOiCc/UqVG4jrf4AH5dRBPT8UWHg3/BvUfhl635ZcNXRBMSDqFxFWvwklVu07O/BT2RUAad0GIWPtexXgMpPt+xntAt3Gl1AvJjCPufasn4d/6NyY2n4DPzXzCgDzmEvhVdIw+tj02X8nsIPGvus7Xi8jDIgPkLhSPON5ad4t7gbletJ9S71n+BIA65C5dGal1g3nENM+weF70j1R3iNjDogfkrhn6br5dU/4UXS94EtF7+vUC8IjjzgrlRuJ33v27j2dTLbKXxd+r4Pn5RRB9RfxsL3y4H/i0dI9TFUrirOZDeuPh4B8TQqVyjH/ZtZqZ7KIRS+JJ6Fn8k4BNRL9IUPmncwbr/1b37lanbiLuMTEG+nsNfb8cOWq0DLFA7E/8yMU8AlNLnaoP/alSfKOAXEN6h8Qlo8k3UKq5bGLeAWFPa6oLT6Ogqvk3ELmPJ3wOtlgOe0B2w6//gFxCPBsjPIQN8JPi3jGDDvSDxVDPaCNnFNOdGczrNzIrEnRyR5XPZlMIdmKofnpTnx6B783QV0ARNOF9AFdAFdQBfQBXQBXUAX0AV0AV3A6OgCVjLJrEzlj5lk/jiVt2eSefvEvzTJxL84zMS/PM//Af1pBRpQAkXBAAAAAElFTkSuQmCC + + + + + + + + + + + + + +https://uk.wikipedia.org/wiki/Спеціальна:Пошук + diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/uk/yandex-market.xml b/mobile/ios/Client/Assets/Search/SearchPlugins/uk/yandex-market.xml new file mode 100644 index 0000000000..aac55155d4 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/uk/yandex-market.xml @@ -0,0 +1,20 @@ + + + +Яндекс.Маркет +Вибір моделей та пошук товарів. +UTF-8 +data:image/png;base64,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 + + + + + + + + + +http://m.market.yandex.ua + diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/ur/amazon-in.xml b/mobile/ios/Client/Assets/Search/SearchPlugins/ur/amazon-in.xml new file mode 100644 index 0000000000..05c5311e31 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/ur/amazon-in.xml @@ -0,0 +1,15 @@ + + + +Amazon.in +utf-8 +data:image/png;base64,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 + + + + + +https://www.amazon.in/ + diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/ur/default.txt b/mobile/ios/Client/Assets/Search/SearchPlugins/ur/default.txt new file mode 100644 index 0000000000..ced95a5cb9 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/ur/default.txt @@ -0,0 +1 @@ +Google \ No newline at end of file diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/ur/list.txt b/mobile/ios/Client/Assets/Search/SearchPlugins/ur/list.txt new file mode 100644 index 0000000000..1d848c8110 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/ur/list.txt @@ -0,0 +1,7 @@ +amazon-in +bing +duckduckgo +google +twitter +wikipedia-ur +yahoo-in diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/ur/wikipedia-ur.xml b/mobile/ios/Client/Assets/Search/SearchPlugins/ur/wikipedia-ur.xml new file mode 100644 index 0000000000..9eb978609f --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/ur/wikipedia-ur.xml @@ -0,0 +1,23 @@ + + + +ویکیپیڈیا (ur) +UTF-8 +data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAQAAABIkb+zAAAEXElEQVR4Ae3cA4xs2QJG4b9aj41n27Zt27Zt27Zt27Zt+2KM5lXP924qOzs56VPjKSRnrfhv7NUsp08v98+PsxwT4nJ+nPunl8KZ8/WYQL+eMydJrx5/EhN603lAHpVJ5bzZNp035uyZXM7Uy3LmM7ms9CITzVRGQRfQBXQBXUAX0AV0AV1AF9AFdAFdQBfQBXQBXcBMjgU7M5+1vqtZyXqmc/IsZDEL+53LP3KRPCCv62+rWd7vnpx6v/M5Vd935OF5R+6RtRyeQ3NYNjKTxZw2p8tiduR8uWfemY3+dkjWM5fFLPU/8q9yrRwbHLP39WkHamPD5z1WnNMb/dpW/uAdbipu413+qcl2H3N3cXFv9DNNtvmg68uxMY6dPfewTZNvOauouqEdULlTY532IgqHua+ZxnoHm4ANj63biRNQPZedVPY5nTR1BXsVbNuyTlsDqy4tW/wK4NYiJ01AXMlm+wGrL1cDnWbLejC4m7T4DvAOOSkD4v0qztGyLzpUweOk4UXAp6XV72DNmU7qgPPao+DJ0uLTFPzXXGN5I9acU1o8vb14jZzUAfFeBdvMtuwLDlfwIKme2248T1p9MvY59zACLqcy4Kf5eQr+ZbYRfrAFaXHaf/FRGUZAfE/Bzwf8OKwpuJ/0vYRNPEpavT248rACbqfimtLiqxT8w4yIz+Of5qTV7+LHMqyAaf9R8Clp8ex2K7iXuAa4q7R6aXCH4QXE4xVsOq+0+DYFfzXth/ilnrT6Tvzb9DADlqwqDPjTd377FHwI3EBaPb0NPFaGGRBvoLBiUVr8EFS+KgN8KpYtDjvgQo6i8Hhp8ZJUjnIZaXXGNrxKhh0QX6TwHzPS4uco/FtPWr0T9jnPKAJuOOiCc/UqVG4jrf4AH5dRBPT8UWHg3/BvUfhl635ZcNXRBMSDqFxFWvwklVu07O/BT2RUAad0GIWPtexXgMpPt+xntAt3Gl1AvJjCPufasn4d/6NyY2n4DPzXzCgDzmEvhVdIw+tj02X8nsIPGvus7Xi8jDIgPkLhSPON5ad4t7gbletJ9S71n+BIA65C5dGal1g3nENM+weF70j1R3iNjDogfkrhn6br5dU/4UXS94EtF7+vUC8IjjzgrlRuJ33v27j2dTLbKXxd+r4Pn5RRB9RfxsL3y4H/i0dI9TFUrirOZDeuPh4B8TQqVyjH/ZtZqZ7KIRS+JJ6Fn8k4BNRL9IUPmncwbr/1b37lanbiLuMTEG+nsNfb8cOWq0DLFA7E/8yMU8AlNLnaoP/alSfKOAXEN6h8Qlo8k3UKq5bGLeAWFPa6oLT6Ogqvk3ELmPJ3wOtlgOe0B2w6//gFxCPBsjPIQN8JPi3jGDDvSDxVDPaCNnFNOdGczrNzIrEnRyR5XPZlMIdmKofnpTnx6B783QV0ARNOF9AFdAFdQBfQBXQBXUAX0AV0AV3A6OgCVjLJrEzlj5lk/jiVt2eSefvEvzTJxL84zMS/PM//Af1pBRpQAkXBAAAAAElFTkSuQmCC + + + + + + + + + + + + + +https://ur.wikipedia.org/wiki/خاص:تلاش + diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/ur/yahoo-in.xml b/mobile/ios/Client/Assets/Search/SearchPlugins/ur/yahoo-in.xml new file mode 100644 index 0000000000..67e95519df --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/ur/yahoo-in.xml @@ -0,0 +1,22 @@ + + + +Yahoo +UTF-8 +data:image/png;base64,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 + + + + + + + + + + + + +https://in.search.yahoo.com/ + diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/uz/default.txt b/mobile/ios/Client/Assets/Search/SearchPlugins/uz/default.txt new file mode 100644 index 0000000000..ced95a5cb9 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/uz/default.txt @@ -0,0 +1 @@ +Google \ No newline at end of file diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/uz/list.txt b/mobile/ios/Client/Assets/Search/SearchPlugins/uz/list.txt new file mode 100644 index 0000000000..c4a0ab5085 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/uz/list.txt @@ -0,0 +1,6 @@ +amazondotcom +bing +google +twitter +wikipedia-uz +yahoo diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/uz/wikipedia-uz.xml b/mobile/ios/Client/Assets/Search/SearchPlugins/uz/wikipedia-uz.xml new file mode 100644 index 0000000000..217879fa32 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/uz/wikipedia-uz.xml @@ -0,0 +1,23 @@ + + + +Vikipediya (uz) +UTF-8 +data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAQAAABIkb+zAAAEXElEQVR4Ae3cA4xs2QJG4b9aj41n27Zt27Zt27Zt27Zt+2KM5lXP924qOzs56VPjKSRnrfhv7NUsp08v98+PsxwT4nJ+nPunl8KZ8/WYQL+eMydJrx5/EhN603lAHpVJ5bzZNp035uyZXM7Uy3LmM7ms9CITzVRGQRfQBXQBXUAX0AV0AV1AF9AFdAFdQBfQBXQBXcBMjgU7M5+1vqtZyXqmc/IsZDEL+53LP3KRPCCv62+rWd7vnpx6v/M5Vd935OF5R+6RtRyeQ3NYNjKTxZw2p8tiduR8uWfemY3+dkjWM5fFLPU/8q9yrRwbHLP39WkHamPD5z1WnNMb/dpW/uAdbipu413+qcl2H3N3cXFv9DNNtvmg68uxMY6dPfewTZNvOauouqEdULlTY532IgqHua+ZxnoHm4ANj63biRNQPZedVPY5nTR1BXsVbNuyTlsDqy4tW/wK4NYiJ01AXMlm+wGrL1cDnWbLejC4m7T4DvAOOSkD4v0qztGyLzpUweOk4UXAp6XV72DNmU7qgPPao+DJ0uLTFPzXXGN5I9acU1o8vb14jZzUAfFeBdvMtuwLDlfwIKme2248T1p9MvY59zACLqcy4Kf5eQr+ZbYRfrAFaXHaf/FRGUZAfE/Bzwf8OKwpuJ/0vYRNPEpavT248rACbqfimtLiqxT8w4yIz+Of5qTV7+LHMqyAaf9R8Clp8ex2K7iXuAa4q7R6aXCH4QXE4xVsOq+0+DYFfzXth/ilnrT6Tvzb9DADlqwqDPjTd377FHwI3EBaPb0NPFaGGRBvoLBiUVr8EFS+KgN8KpYtDjvgQo6i8Hhp8ZJUjnIZaXXGNrxKhh0QX6TwHzPS4uco/FtPWr0T9jnPKAJuOOiCc/UqVG4jrf4AH5dRBPT8UWHg3/BvUfhl635ZcNXRBMSDqFxFWvwklVu07O/BT2RUAad0GIWPtexXgMpPt+xntAt3Gl1AvJjCPufasn4d/6NyY2n4DPzXzCgDzmEvhVdIw+tj02X8nsIPGvus7Xi8jDIgPkLhSPON5ad4t7gbletJ9S71n+BIA65C5dGal1g3nENM+weF70j1R3iNjDogfkrhn6br5dU/4UXS94EtF7+vUC8IjjzgrlRuJ33v27j2dTLbKXxd+r4Pn5RRB9RfxsL3y4H/i0dI9TFUrirOZDeuPh4B8TQqVyjH/ZtZqZ7KIRS+JJ6Fn8k4BNRL9IUPmncwbr/1b37lanbiLuMTEG+nsNfb8cOWq0DLFA7E/8yMU8AlNLnaoP/alSfKOAXEN6h8Qlo8k3UKq5bGLeAWFPa6oLT6Ogqvk3ELmPJ3wOtlgOe0B2w6//gFxCPBsjPIQN8JPi3jGDDvSDxVDPaCNnFNOdGczrNzIrEnRyR5XPZlMIdmKofnpTnx6B783QV0ARNOF9AFdAFdQBfQBXQBXUAX0AV0AV3A6OgCVjLJrEzlj5lk/jiVt2eSefvEvzTJxL84zMS/PM//Af1pBRpQAkXBAAAAAElFTkSuQmCC + + + + + + + + + + + + + +https://uz.wikipedia.org/wiki/Maxsus:Search + diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/zh-CN/baidu.xml b/mobile/ios/Client/Assets/Search/SearchPlugins/zh-CN/baidu.xml new file mode 100644 index 0000000000..eb806c6b5b --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/zh-CN/baidu.xml @@ -0,0 +1,28 @@ + + + + 百度 + UTF-8 + data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAYAAADimHc4AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAA2hpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMC1jMDYxIDY0LjE0MDk0OSwgMjAxMC8xMi8wNy0xMDo1NzowMSAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtcE1NOk9yaWdpbmFsRG9jdW1lbnRJRD0ieG1wLmRpZDowMzgwMTE3NDA3MjA2ODExOEE2REI2ODhGMkQ2RTdCRSIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDozQTZEQkQzRTNBODUxMUU1OUE0RDhCNjkxRkYwMTQwNCIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDozQTZEQkQzRDNBODUxMUU1OUE0RDhCNjkxRkYwMTQwNCIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ1M1LjEgTWFjaW50b3NoIj4gPHhtcE1NOkRlcml2ZWRGcm9tIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6Rjc3RjExNzQwNzIwNjgxMTk0RkZFQURGMTU5RDlDM0MiIHN0UmVmOmRvY3VtZW50SUQ9InhtcC5kaWQ6MDM4MDExNzQwNzIwNjgxMThBNkRCNjg4RjJENkU3QkUiLz4gPC9yZGY6RGVzY3Jp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 + + + + + + + + + + + + + + + + https://m.baidu.com/ + diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/zh-CN/default.txt b/mobile/ios/Client/Assets/Search/SearchPlugins/zh-CN/default.txt new file mode 100644 index 0000000000..1ff2bf5542 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/zh-CN/default.txt @@ -0,0 +1 @@ +百度 \ No newline at end of file diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/zh-CN/list.txt b/mobile/ios/Client/Assets/Search/SearchPlugins/zh-CN/list.txt new file mode 100644 index 0000000000..bd8014fbd3 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/zh-CN/list.txt @@ -0,0 +1,6 @@ +baidu +bing +google:hidden +google-nocodes +taobao +wikipedia-zh-CN diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/zh-CN/taobao.xml b/mobile/ios/Client/Assets/Search/SearchPlugins/zh-CN/taobao.xml new file mode 100644 index 0000000000..a43e873a85 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/zh-CN/taobao.xml @@ -0,0 +1,23 @@ + + + +淘宝 +UTF-8 +data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAYAAADimHc4AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAA2hpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMC1jMDYxIDY0LjE0MDk0OSwgMjAxMC8xMi8wNy0xMDo1NzowMSAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtcE1NOk9yaWdpbmFsRG9jdW1lbnRJRD0ieG1wLmRpZDowMTgwMTE3NDA3MjA2ODExOEE2REI2ODhGMkQ2RTdCRSIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDo0QkUwRjAxREQ4NkIxMUU1ODBCNDk3NEYxRTREODQyOCIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDo0QkUwRjAxQ0Q4NkIxMUU1ODBCNDk3NEYxRTREODQyOCIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ1M1LjEgTWFjaW50b3NoIj4gPHhtcE1NOkRlcml2ZWRGcm9tIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6MDE4MDExNzQwNzIwNjgxMUIxREJDMDZDRDM1QUE2REEiIHN0UmVmOmRvY3VtZW50SUQ9InhtcC5kaWQ6MDE4MDExNzQwNzIwNjgxMThBNkRCNjg4RjJENkU3QkUiLz4gPC9yZGY6RGVzY3Jp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 + + + + + + + + + + + + + +http://r.m.taobao.com/ + diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/zh-CN/wikipedia-zh-CN.xml b/mobile/ios/Client/Assets/Search/SearchPlugins/zh-CN/wikipedia-zh-CN.xml new file mode 100644 index 0000000000..2f3eead054 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/zh-CN/wikipedia-zh-CN.xml @@ -0,0 +1,23 @@ + + + +维基百科 +UTF-8 +data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAQAAABIkb+zAAAEXElEQVR4Ae3cA4xs2QJG4b9aj41n27Zt27Zt27Zt27Zt+2KM5lXP924qOzs56VPjKSRnrfhv7NUsp08v98+PsxwT4nJ+nPunl8KZ8/WYQL+eMydJrx5/EhN603lAHpVJ5bzZNp035uyZXM7Uy3LmM7ms9CITzVRGQRfQBXQBXUAX0AV0AV1AF9AFdAFdQBfQBXQBXcBMjgU7M5+1vqtZyXqmc/IsZDEL+53LP3KRPCCv62+rWd7vnpx6v/M5Vd935OF5R+6RtRyeQ3NYNjKTxZw2p8tiduR8uWfemY3+dkjWM5fFLPU/8q9yrRwbHLP39WkHamPD5z1WnNMb/dpW/uAdbipu413+qcl2H3N3cXFv9DNNtvmg68uxMY6dPfewTZNvOauouqEdULlTY532IgqHua+ZxnoHm4ANj63biRNQPZedVPY5nTR1BXsVbNuyTlsDqy4tW/wK4NYiJ01AXMlm+wGrL1cDnWbLejC4m7T4DvAOOSkD4v0qztGyLzpUweOk4UXAp6XV72DNmU7qgPPao+DJ0uLTFPzXXGN5I9acU1o8vb14jZzUAfFeBdvMtuwLDlfwIKme2248T1p9MvY59zACLqcy4Kf5eQr+ZbYRfrAFaXHaf/FRGUZAfE/Bzwf8OKwpuJ/0vYRNPEpavT248rACbqfimtLiqxT8w4yIz+Of5qTV7+LHMqyAaf9R8Clp8ex2K7iXuAa4q7R6aXCH4QXE4xVsOq+0+DYFfzXth/ilnrT6Tvzb9DADlqwqDPjTd377FHwI3EBaPb0NPFaGGRBvoLBiUVr8EFS+KgN8KpYtDjvgQo6i8Hhp8ZJUjnIZaXXGNrxKhh0QX6TwHzPS4uco/FtPWr0T9jnPKAJuOOiCc/UqVG4jrf4AH5dRBPT8UWHg3/BvUfhl635ZcNXRBMSDqFxFWvwklVu07O/BT2RUAad0GIWPtexXgMpPt+xntAt3Gl1AvJjCPufasn4d/6NyY2n4DPzXzCgDzmEvhVdIw+tj02X8nsIPGvus7Xi8jDIgPkLhSPON5ad4t7gbletJ9S71n+BIA65C5dGal1g3nENM+weF70j1R3iNjDogfkrhn6br5dU/4UXS94EtF7+vUC8IjjzgrlRuJ33v27j2dTLbKXxd+r4Pn5RRB9RfxsL3y4H/i0dI9TFUrirOZDeuPh4B8TQqVyjH/ZtZqZ7KIRS+JJ6Fn8k4BNRL9IUPmncwbr/1b37lanbiLuMTEG+nsNfb8cOWq0DLFA7E/8yMU8AlNLnaoP/alSfKOAXEN6h8Qlo8k3UKq5bGLeAWFPa6oLT6Ogqvk3ELmPJ3wOtlgOe0B2w6//gFxCPBsjPIQN8JPi3jGDDvSDxVDPaCNnFNOdGczrNzIrEnRyR5XPZlMIdmKofnpTnx6B783QV0ARNOF9AFdAFdQBfQBXQBXUAX0AV0AV3A6OgCVjLJrEzlj5lk/jiVt2eSefvEvzTJxL84zMS/PM//Af1pBRpQAkXBAAAAAElFTkSuQmCC + + + + + + + + + + + + + +https://zh.wikipedia.org/wiki/Special:搜索 + diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/zh-TW/default.txt b/mobile/ios/Client/Assets/Search/SearchPlugins/zh-TW/default.txt new file mode 100644 index 0000000000..ced95a5cb9 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/zh-TW/default.txt @@ -0,0 +1 @@ +Google \ No newline at end of file diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/zh-TW/list.txt b/mobile/ios/Client/Assets/Search/SearchPlugins/zh-TW/list.txt new file mode 100644 index 0000000000..088c61428c --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/zh-TW/list.txt @@ -0,0 +1,5 @@ +bing +duckduckgo +google-nocodes:hidden +google +wikipedia-zh-TW diff --git a/mobile/ios/Client/Assets/Search/SearchPlugins/zh-TW/wikipedia-zh-TW.xml b/mobile/ios/Client/Assets/Search/SearchPlugins/zh-TW/wikipedia-zh-TW.xml new file mode 100644 index 0000000000..589ab4ad63 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/SearchPlugins/zh-TW/wikipedia-zh-TW.xml @@ -0,0 +1,25 @@ + + + +Wikipedia (zh) +UTF-8 +data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAQAAABIkb+zAAAEXElEQVR4Ae3cA4xs2QJG4b9aj41n27Zt27Zt27Zt27Zt+2KM5lXP924qOzs56VPjKSRnrfhv7NUsp08v98+PsxwT4nJ+nPunl8KZ8/WYQL+eMydJrx5/EhN603lAHpVJ5bzZNp035uyZXM7Uy3LmM7ms9CITzVRGQRfQBXQBXUAX0AV0AV1AF9AFdAFdQBfQBXQBXcBMjgU7M5+1vqtZyXqmc/IsZDEL+53LP3KRPCCv62+rWd7vnpx6v/M5Vd935OF5R+6RtRyeQ3NYNjKTxZw2p8tiduR8uWfemY3+dkjWM5fFLPU/8q9yrRwbHLP39WkHamPD5z1WnNMb/dpW/uAdbipu413+qcl2H3N3cXFv9DNNtvmg68uxMY6dPfewTZNvOauouqEdULlTY532IgqHua+ZxnoHm4ANj63biRNQPZedVPY5nTR1BXsVbNuyTlsDqy4tW/wK4NYiJ01AXMlm+wGrL1cDnWbLejC4m7T4DvAOOSkD4v0qztGyLzpUweOk4UXAp6XV72DNmU7qgPPao+DJ0uLTFPzXXGN5I9acU1o8vb14jZzUAfFeBdvMtuwLDlfwIKme2248T1p9MvY59zACLqcy4Kf5eQr+ZbYRfrAFaXHaf/FRGUZAfE/Bzwf8OKwpuJ/0vYRNPEpavT248rACbqfimtLiqxT8w4yIz+Of5qTV7+LHMqyAaf9R8Clp8ex2K7iXuAa4q7R6aXCH4QXE4xVsOq+0+DYFfzXth/ilnrT6Tvzb9DADlqwqDPjTd377FHwI3EBaPb0NPFaGGRBvoLBiUVr8EFS+KgN8KpYtDjvgQo6i8Hhp8ZJUjnIZaXXGNrxKhh0QX6TwHzPS4uco/FtPWr0T9jnPKAJuOOiCc/UqVG4jrf4AH5dRBPT8UWHg3/BvUfhl635ZcNXRBMSDqFxFWvwklVu07O/BT2RUAad0GIWPtexXgMpPt+xntAt3Gl1AvJjCPufasn4d/6NyY2n4DPzXzCgDzmEvhVdIw+tj02X8nsIPGvus7Xi8jDIgPkLhSPON5ad4t7gbletJ9S71n+BIA65C5dGal1g3nENM+weF70j1R3iNjDogfkrhn6br5dU/4UXS94EtF7+vUC8IjjzgrlRuJ33v27j2dTLbKXxd+r4Pn5RRB9RfxsL3y4H/i0dI9TFUrirOZDeuPh4B8TQqVyjH/ZtZqZ7KIRS+JJ6Fn8k4BNRL9IUPmncwbr/1b37lanbiLuMTEG+nsNfb8cOWq0DLFA7E/8yMU8AlNLnaoP/alSfKOAXEN6h8Qlo8k3UKq5bGLeAWFPa6oLT6Ogqvk3ELmPJ3wOtlgOe0B2w6//gFxCPBsjPIQN8JPi3jGDDvSDxVDPaCNnFNOdGczrNzIrEnRyR5XPZlMIdmKofnpTnx6B783QV0ARNOF9AFdAFdQBfQBXQBXUAX0AV0AV3A6OgCVjLJrEzlj5lk/jiVt2eSefvEvzTJxL84zMS/PM//Af1pBRpQAkXBAAAAAElFTkSuQmCC + + + + + + + + + + + + + + + +https://zh.wikipedia.org/wiki/Special:搜索 + diff --git a/mobile/ios/Client/Assets/Search/Tests/Base/testplugin.xml b/mobile/ios/Client/Assets/Search/Tests/Base/testplugin.xml new file mode 100644 index 0000000000..b0fd8f057b --- /dev/null +++ b/mobile/ios/Client/Assets/Search/Tests/Base/testplugin.xml @@ -0,0 +1,17 @@ + + + + TestPlugin + Test Plugin + UTF-8 + + + + + + + + + diff --git a/mobile/ios/Client/Assets/Search/Tests/Expected/append.xml b/mobile/ios/Client/Assets/Search/Tests/Expected/append.xml new file mode 100644 index 0000000000..ab7f87a2e8 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/Tests/Expected/append.xml @@ -0,0 +1,19 @@ + + + TestPlugin + Test Plugin + UTF-8 + + + + + + + + + + + + diff --git a/mobile/ios/Client/Assets/Search/Tests/Expected/replace.xml b/mobile/ios/Client/Assets/Search/Tests/Expected/replace.xml new file mode 100644 index 0000000000..f5a53b992c --- /dev/null +++ b/mobile/ios/Client/Assets/Search/Tests/Expected/replace.xml @@ -0,0 +1,15 @@ + + + TestPlugin + Test Plugin + UTF-8 + + + + + + Child of replaced element. + + diff --git a/mobile/ios/Client/Assets/Search/Tests/Overlays/append.xml b/mobile/ios/Client/Assets/Search/Tests/Overlays/append.xml new file mode 100644 index 0000000000..5bc874342a --- /dev/null +++ b/mobile/ios/Client/Assets/Search/Tests/Overlays/append.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/mobile/ios/Client/Assets/Search/Tests/Overlays/replace.xml b/mobile/ios/Client/Assets/Search/Tests/Overlays/replace.xml new file mode 100644 index 0000000000..42c7998130 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/Tests/Overlays/replace.xml @@ -0,0 +1,7 @@ + + + + Child of replaced element. + + + diff --git a/mobile/ios/Client/Assets/Search/get_supported_locales.swift b/mobile/ios/Client/Assets/Search/get_supported_locales.swift new file mode 100644 index 0000000000..dc643f8736 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/get_supported_locales.swift @@ -0,0 +1,13 @@ +#!/usr/bin/env swift + +import Foundation + +var allLocales = NSLocale.availableLocaleIdentifiers() + +// iOS doesn't directly support zh-CN/zh-TW; instead, it supports zh-Hans-CN and zh-Hant-TW, respectively. +// These transformations are done in SearchEngines.swift. Add them to the list of supported engines so +// that we download them. +allLocales.append("zh_CN") +allLocales.append("zh_TW") + +print(allLocales) diff --git a/mobile/ios/Client/Assets/Search/run_tests.py b/mobile/ios/Client/Assets/Search/run_tests.py new file mode 100644 index 0000000000..63a59d41ed --- /dev/null +++ b/mobile/ios/Client/Assets/Search/run_tests.py @@ -0,0 +1,34 @@ +#!/usr/bin/env python + +# 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/. + +from lxml import etree +import sys +import unittest + +from scrape_plugins import Overlay + +class TestOverlays(unittest.TestCase): + def setUp(self): + self.plugin = etree.parse("Tests/Base/testplugin.xml") + + def testAppend(self): + overlay = Overlay("Tests/Overlays/append.xml") + overlay.apply(self.plugin) + self.assertEqualsExpectedXML(plugin=self.plugin, expectedPath="Tests/Expected/append.xml") + + def testReplace(self): + overlay = Overlay("Tests/Overlays/replace.xml") + overlay.apply(self.plugin) + self.assertEqualsExpectedXML(plugin=self.plugin, expectedPath="Tests/Expected/replace.xml") + + def assertEqualsExpectedXML(self, plugin, expectedPath): + actual = etree.tostring(plugin, pretty_print=True) + with open(expectedPath, "r") as file: + expected = file.read() + self.assertEqual(actual, expected, "\nExpected:\n%s\n\nActual:\n%s" % (expected, actual)) + +if __name__ == '__main__': + unittest.main() diff --git a/mobile/ios/Client/Assets/Search/scrape_plugins.py b/mobile/ios/Client/Assets/Search/scrape_plugins.py new file mode 100644 index 0000000000..30d0cd8d96 --- /dev/null +++ b/mobile/ios/Client/Assets/Search/scrape_plugins.py @@ -0,0 +1,214 @@ +#!/usr/bin/env python + +# 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/. + +from lxml import html +from lxml import etree +import copy +import json +import os +import requests +import shutil +import subprocess +import urllib + +# Paths for en-US plugins included in the core Android repo. +EN_PLUGINS_DIR_URL = "https://hg.mozilla.org/releases/mozilla-aurora/file/default/mobile/locales/en-US/searchplugins" +EN_PLUGINS_FILE_URL = "https://hg.mozilla.org/releases/mozilla-aurora/raw-file/default/mobile/locales/en-US/searchplugins/%s" +EN_PREFS_URL = "https://hg.mozilla.org/releases/mozilla-aurora/raw-file/default/mobile/locales/en-US/chrome/region.properties" + +# Paths for plugins in the l10n repos. +L10N_LOCALE_LIST_URL = "https://hg.mozilla.org/releases/mozilla-aurora/raw-file/default/mobile/android/locales/all-locales" +L10N_PLUGINS_DIR_URL = "https://hg.mozilla.org/releases/l10n/mozilla-aurora/%s/file/default/mobile/searchplugins" +L10N_PLUGINS_FILE_URL = "https://hg.mozilla.org/releases/l10n/mozilla-aurora/%s/raw-file/default/mobile/searchplugins/%%s" +L10N_PREFS_URL = "https://hg.mozilla.org/releases/l10n/mozilla-aurora/%s/raw-file/default/mobile/chrome/region.properties" + +MOZ_HEADER = """\ + + +""" + +ns = { "search": "http://www.mozilla.org/2006/browser/search/" } + +def main(): + # Remove and recreate the SearchPlugins directory. + if os.path.exists("SearchPlugins"): + shutil.rmtree("SearchPlugins") + os.makedirs("SearchPlugins") + + # Import en-US engines from the core repo. + downloadLocale("en", EnScraper()) + + # Import engines from the l10n repos. + response = requests.get(L10N_LOCALE_LIST_URL) + locales = response.text.strip().split("\n") + supportedLocales = getSupportedLocales() + for locale in locales: + if (locale not in supportedLocales): + print("skipping unsupported locale: %s" % locale) + continue + + downloadLocale(locale, L10nScraper(locale)) + + verifyEngines() + +def downloadLocale(locale, scraper): + print("scraping: %s..." % locale) + files = scraper.getFileList() + if files == None: + print("no files for locale: %s" % locale) + return + + print(" found search plugins") + + directory = os.path.join("SearchPlugins", locale) + if not os.path.exists(directory): + os.makedirs(directory) + + # Get the default search engine for this locale. + default = scraper.getDefault() + print(" default: %s" % default) + saveDefault(locale, default) + + for file in files: + path = os.path.join(directory, file) + print(" downloading: %s..." % file) + downloadedFile = scraper.getFile(file) + name, extension = os.path.splitext(file) + + # Apply iOS-specific overlays for this engine if they are defined. + if extension == ".xml": + engine = name.split("-")[0] + overlay = overlayForEngine(engine) + if overlay: + plugin = etree.parse(downloadedFile) + overlay.apply(plugin) + contents = MOZ_HEADER + etree.tostring(plugin.getroot(), encoding="utf-8", pretty_print=True) + with open(path, "w") as outfile: + outfile.write(contents) + continue + + # Otherwise, just use the downloaded file as is. + shutil.move(downloadedFile, path) + +def verifyEngines(): + print("verifying engines...") + enDir = os.path.join("SearchPlugins", "en") + for locale in os.listdir("SearchPlugins"): + if locale.startswith("."): continue + localeDir = os.path.join("SearchPlugins", locale) + with open(os.path.join(localeDir, "list.txt")) as f: + engineList = f.read().splitlines() + + for engine in engineList: + if engine.endswith(":hidden"): continue + path = os.path.join(localeDir, engine + ".xml") + enPath = os.path.join(enDir, engine + ".xml") + if not os.path.exists(path) and not os.path.exists(enPath): + print(" ERROR: missing engine %s for locale %s" % (engine, locale)) + +def getSupportedLocales(): + supportedLocales = subprocess.Popen("./get_supported_locales.swift", stdout=subprocess.PIPE).communicate()[0] + return json.loads(supportedLocales.replace("_", "-")) + +def overlayForEngine(engine): + path = os.path.join("SearchOverlays", "%s.xml" % engine) + if not os.path.exists(path): + return None + return Overlay(path) + +def saveDefault(locale, default): + directory = os.path.join("SearchPlugins", locale, "default.txt") + file = open(directory, "w") + file.write(default.encode("UTF-8")) + + +class Scraper: + def pluginsDirURL(self): pass + def pluginsFileURL(self): pass + def prefsURL(self): pass + def defaultPrefName(self): pass + + def getFileList(self): + response = requests.get(self.pluginsDirURL) + if not response.ok: + raise Exception("error: could not read plugins directory") + + tree = html.fromstring(response.content) + return tree.xpath('//a[@class="list"]/text()') + + def getFile(self, file): + result = urllib.urlretrieve(self.pluginsFileURL % file) + return result[0] + + def getDefault(self): + response = requests.get(self.prefsURL) + if not response.ok: + raise Exception("error: could not read prefs file") + + lines = response.text.strip().split("\n") + for line in lines: + values = line.strip().split("=") + if len(values) == 2 and values[0].strip() == self.defaultPrefName: + default = values[1].strip() + return default + + raise Exception("error: no default pref found") + + +class L10nScraper(Scraper): + def __init__(self, locale): + self.pluginsDirURL = L10N_PLUGINS_DIR_URL % locale + self.pluginsFileURL = L10N_PLUGINS_FILE_URL % locale + self.prefsURL = L10N_PREFS_URL % locale + self.defaultPrefName = "browser.search.defaultenginename" + + +class EnScraper(Scraper): + def __init__(self): + self.pluginsDirURL = EN_PLUGINS_DIR_URL + self.pluginsFileURL = EN_PLUGINS_FILE_URL + self.prefsURL = EN_PREFS_URL + self.defaultPrefName = "browser.search.defaultenginename.US" + +class Overlay: + def __init__(self, path): + overlay = etree.parse(path) + self.actions = overlay.getroot().getchildren() + + def apply(self, doc): + for action in self.actions: + if action.tag == "replace": + self.replace(target=action.get("target"), replacement=action[0], doc=doc) + elif action.tag == "append": + self.append(parent=action.get("parent"), child=action[0], doc=doc) + + def replace(self, target, replacement, doc): + for element in doc.xpath(target, namespaces=ns): + replacementCopy = copy.deepcopy(replacement) + element.getparent().replace(element, replacementCopy) + + # Try to preserve indentation. + replacementCopy.tail = element.tail + + def append(self, parent, child, doc): + for element in doc.xpath(parent, namespaces=ns): + childCopy = copy.deepcopy(child) + element.append(childCopy) + + # Try to preserve indentation. + childCopy.tail = "\n" + previous = childCopy.getprevious() + if previous is not None: + childCopy.tail = previous.tail + prevPrevious = previous.getprevious() + if prevPrevious is not None: + previous.tail = prevPrevious.tail + + +if __name__ == "__main__": + main() diff --git a/mobile/ios/Client/Assets/SessionRestore.html b/mobile/ios/Client/Assets/SessionRestore.html new file mode 100644 index 0000000000..19f4de7376 --- /dev/null +++ b/mobile/ios/Client/Assets/SessionRestore.html @@ -0,0 +1,52 @@ + + + + + + + + + + diff --git a/mobile/ios/Client/Assets/SpotlightHelper.js b/mobile/ios/Client/Assets/SpotlightHelper.js new file mode 100644 index 0000000000..4ce8923c41 --- /dev/null +++ b/mobile/ios/Client/Assets/SpotlightHelper.js @@ -0,0 +1,31 @@ +/* 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/. */ + +(function() { +"use strict"; + +var selectors = { + title: "title", + description: "meta[name='description'], body p", +}; + +function collectText(selector) { + var el = document.querySelector(selector); + return el ? el.getAttribute("content") || el.innerText || "" : ""; +} + +function assemblePayload(selectors) { + var payload = {}; + for (var key in selectors) { + payload[key] = collectText(selectors[key]); + } + return payload; +} + +window.addEventListener("load", function() { + var payload = assemblePayload(selectors); + webkit.messageHandlers.spotlightMessageHandler.postMessage(payload); +}); + +})(); diff --git a/mobile/ios/Client/Assets/TopSites/aa-com.png b/mobile/ios/Client/Assets/TopSites/aa-com.png new file mode 100644 index 0000000000..c96e95fd43 Binary files /dev/null and b/mobile/ios/Client/Assets/TopSites/aa-com.png differ diff --git a/mobile/ios/Client/Assets/TopSites/abcnews-go-com.png b/mobile/ios/Client/Assets/TopSites/abcnews-go-com.png new file mode 100644 index 0000000000..62b5b1b3d1 Binary files /dev/null and b/mobile/ios/Client/Assets/TopSites/abcnews-go-com.png differ diff --git a/mobile/ios/Client/Assets/TopSites/about-com.png b/mobile/ios/Client/Assets/TopSites/about-com.png new file mode 100644 index 0000000000..d61a22fff5 Binary files /dev/null and b/mobile/ios/Client/Assets/TopSites/about-com.png differ diff --git a/mobile/ios/Client/Assets/TopSites/accuweather-com.png b/mobile/ios/Client/Assets/TopSites/accuweather-com.png new file mode 100644 index 0000000000..0f14e31a26 Binary files /dev/null and b/mobile/ios/Client/Assets/TopSites/accuweather-com.png differ diff --git a/mobile/ios/Client/Assets/TopSites/adobe-com.png b/mobile/ios/Client/Assets/TopSites/adobe-com.png new file mode 100644 index 0000000000..ca8cde6ec9 Binary files /dev/null and b/mobile/ios/Client/Assets/TopSites/adobe-com.png differ diff --git a/mobile/ios/Client/Assets/TopSites/adp-com.png b/mobile/ios/Client/Assets/TopSites/adp-com.png new file mode 100644 index 0000000000..f5bf112321 Binary files /dev/null and b/mobile/ios/Client/Assets/TopSites/adp-com.png differ diff --git a/mobile/ios/Client/Assets/TopSites/airbnb-com.png b/mobile/ios/Client/Assets/TopSites/airbnb-com.png new file mode 100644 index 0000000000..5e775cafbd Binary files /dev/null and b/mobile/ios/Client/Assets/TopSites/airbnb-com.png differ diff --git a/mobile/ios/Client/Assets/TopSites/allrecipes-com.png b/mobile/ios/Client/Assets/TopSites/allrecipes-com.png new file mode 100644 index 0000000000..ef1f12d154 Binary files /dev/null and b/mobile/ios/Client/Assets/TopSites/allrecipes-com.png differ diff --git a/mobile/ios/Client/Assets/TopSites/amazon-com.png b/mobile/ios/Client/Assets/TopSites/amazon-com.png new file mode 100644 index 0000000000..9bba9e5c54 Binary files /dev/null and b/mobile/ios/Client/Assets/TopSites/amazon-com.png differ diff --git a/mobile/ios/Client/Assets/TopSites/amazonaws-com.png b/mobile/ios/Client/Assets/TopSites/amazonaws-com.png new file mode 100644 index 0000000000..8086e5e227 Binary files /dev/null and b/mobile/ios/Client/Assets/TopSites/amazonaws-com.png differ diff --git a/mobile/ios/Client/Assets/TopSites/americanexpress-com.png b/mobile/ios/Client/Assets/TopSites/americanexpress-com.png new file mode 100644 index 0000000000..344120b49c Binary files /dev/null and b/mobile/ios/Client/Assets/TopSites/americanexpress-com.png differ diff --git a/mobile/ios/Client/Assets/TopSites/ancestry-com.png b/mobile/ios/Client/Assets/TopSites/ancestry-com.png new file mode 100644 index 0000000000..5fe61b2547 Binary files /dev/null and b/mobile/ios/Client/Assets/TopSites/ancestry-com.png differ diff --git a/mobile/ios/Client/Assets/TopSites/answers-com.png b/mobile/ios/Client/Assets/TopSites/answers-com.png new file mode 100644 index 0000000000..5ed923423b Binary files /dev/null and b/mobile/ios/Client/Assets/TopSites/answers-com.png differ diff --git a/mobile/ios/Client/Assets/TopSites/aol-com.png b/mobile/ios/Client/Assets/TopSites/aol-com.png new file mode 100644 index 0000000000..5e7a26cf00 Binary files /dev/null and b/mobile/ios/Client/Assets/TopSites/aol-com.png differ diff --git a/mobile/ios/Client/Assets/TopSites/apple-com.png b/mobile/ios/Client/Assets/TopSites/apple-com.png new file mode 100644 index 0000000000..e688081c26 Binary files /dev/null and b/mobile/ios/Client/Assets/TopSites/apple-com.png differ diff --git a/mobile/ios/Client/Assets/TopSites/ask-com.png b/mobile/ios/Client/Assets/TopSites/ask-com.png new file mode 100644 index 0000000000..2f9ab71007 Binary files /dev/null and b/mobile/ios/Client/Assets/TopSites/ask-com.png differ diff --git a/mobile/ios/Client/Assets/TopSites/att-com.png b/mobile/ios/Client/Assets/TopSites/att-com.png new file mode 100644 index 0000000000..7fa9febd08 Binary files /dev/null and b/mobile/ios/Client/Assets/TopSites/att-com.png differ diff --git a/mobile/ios/Client/Assets/TopSites/baidu-com.png b/mobile/ios/Client/Assets/TopSites/baidu-com.png new file mode 100644 index 0000000000..a72237ae8e Binary files /dev/null and b/mobile/ios/Client/Assets/TopSites/baidu-com.png differ diff --git a/mobile/ios/Client/Assets/TopSites/bankofamerica-com.png b/mobile/ios/Client/Assets/TopSites/bankofamerica-com.png new file mode 100644 index 0000000000..ddb7ef5019 Binary files /dev/null and b/mobile/ios/Client/Assets/TopSites/bankofamerica-com.png differ diff --git a/mobile/ios/Client/Assets/TopSites/bbc-com.png b/mobile/ios/Client/Assets/TopSites/bbc-com.png new file mode 100644 index 0000000000..b90feff3fe Binary files /dev/null and b/mobile/ios/Client/Assets/TopSites/bbc-com.png differ diff --git a/mobile/ios/Client/Assets/TopSites/bestbuy-com.png b/mobile/ios/Client/Assets/TopSites/bestbuy-com.png new file mode 100644 index 0000000000..d72238386d Binary files /dev/null and b/mobile/ios/Client/Assets/TopSites/bestbuy-com.png differ diff --git a/mobile/ios/Client/Assets/TopSites/bing-com.png b/mobile/ios/Client/Assets/TopSites/bing-com.png new file mode 100644 index 0000000000..6c09543646 Binary files /dev/null and b/mobile/ios/Client/Assets/TopSites/bing-com.png differ diff --git a/mobile/ios/Client/Assets/TopSites/blackboard-com.png b/mobile/ios/Client/Assets/TopSites/blackboard-com.png new file mode 100644 index 0000000000..3692d392e6 Binary files /dev/null and b/mobile/ios/Client/Assets/TopSites/blackboard-com.png differ diff --git a/mobile/ios/Client/Assets/TopSites/bleacherreport-com.png b/mobile/ios/Client/Assets/TopSites/bleacherreport-com.png new file mode 100644 index 0000000000..088a242322 Binary files /dev/null and b/mobile/ios/Client/Assets/TopSites/bleacherreport-com.png differ diff --git a/mobile/ios/Client/Assets/TopSites/blogger-com.png b/mobile/ios/Client/Assets/TopSites/blogger-com.png new file mode 100644 index 0000000000..33947ead81 Binary files /dev/null and b/mobile/ios/Client/Assets/TopSites/blogger-com.png differ diff --git a/mobile/ios/Client/Assets/TopSites/box-com.png b/mobile/ios/Client/Assets/TopSites/box-com.png new file mode 100644 index 0000000000..7f5d62bdb8 Binary files /dev/null and b/mobile/ios/Client/Assets/TopSites/box-com.png differ diff --git a/mobile/ios/Client/Assets/TopSites/businessinsider-com.png b/mobile/ios/Client/Assets/TopSites/businessinsider-com.png new file mode 100644 index 0000000000..f1c960031a Binary files /dev/null and b/mobile/ios/Client/Assets/TopSites/businessinsider-com.png differ diff --git a/mobile/ios/Client/Assets/TopSites/buzzfeed-com.png b/mobile/ios/Client/Assets/TopSites/buzzfeed-com.png new file mode 100644 index 0000000000..a0af65eefe Binary files /dev/null and b/mobile/ios/Client/Assets/TopSites/buzzfeed-com.png differ diff --git a/mobile/ios/Client/Assets/TopSites/buzzlie-com.png b/mobile/ios/Client/Assets/TopSites/buzzlie-com.png new file mode 100644 index 0000000000..cf1a880577 Binary files /dev/null and b/mobile/ios/Client/Assets/TopSites/buzzlie-com.png differ diff --git a/mobile/ios/Client/Assets/TopSites/ca-gov.png b/mobile/ios/Client/Assets/TopSites/ca-gov.png new file mode 100644 index 0000000000..10479e67e3 Binary files /dev/null and b/mobile/ios/Client/Assets/TopSites/ca-gov.png differ diff --git a/mobile/ios/Client/Assets/TopSites/capitalone-com.png b/mobile/ios/Client/Assets/TopSites/capitalone-com.png new file mode 100644 index 0000000000..bbae9002bb Binary files /dev/null and b/mobile/ios/Client/Assets/TopSites/capitalone-com.png differ diff --git a/mobile/ios/Client/Assets/TopSites/cbsnews-com.png b/mobile/ios/Client/Assets/TopSites/cbsnews-com.png new file mode 100644 index 0000000000..60afaf6ea3 Binary files /dev/null and b/mobile/ios/Client/Assets/TopSites/cbsnews-com.png differ diff --git a/mobile/ios/Client/Assets/TopSites/cbssports-com.png b/mobile/ios/Client/Assets/TopSites/cbssports-com.png new file mode 100644 index 0000000000..c11b56b252 Binary files /dev/null and b/mobile/ios/Client/Assets/TopSites/cbssports-com.png differ diff --git a/mobile/ios/Client/Assets/TopSites/chase-com.png b/mobile/ios/Client/Assets/TopSites/chase-com.png new file mode 100644 index 0000000000..61a7e9d21f Binary files /dev/null and b/mobile/ios/Client/Assets/TopSites/chase-com.png differ diff --git a/mobile/ios/Client/Assets/TopSites/citi-com.png b/mobile/ios/Client/Assets/TopSites/citi-com.png new file mode 100644 index 0000000000..415b8f8190 Binary files /dev/null and b/mobile/ios/Client/Assets/TopSites/citi-com.png differ diff --git a/mobile/ios/Client/Assets/TopSites/cnet-com.png b/mobile/ios/Client/Assets/TopSites/cnet-com.png new file mode 100644 index 0000000000..8dd8438b19 Binary files /dev/null and b/mobile/ios/Client/Assets/TopSites/cnet-com.png differ diff --git a/mobile/ios/Client/Assets/TopSites/cnn-com.png b/mobile/ios/Client/Assets/TopSites/cnn-com.png new file mode 100644 index 0000000000..64fff1a8e2 Binary files /dev/null and b/mobile/ios/Client/Assets/TopSites/cnn-com.png differ diff --git a/mobile/ios/Client/Assets/TopSites/conservativetribune-com.png b/mobile/ios/Client/Assets/TopSites/conservativetribune-com.png new file mode 100644 index 0000000000..b808aeeb7c Binary files /dev/null and b/mobile/ios/Client/Assets/TopSites/conservativetribune-com.png differ diff --git a/mobile/ios/Client/Assets/TopSites/costco-com.png b/mobile/ios/Client/Assets/TopSites/costco-com.png new file mode 100644 index 0000000000..42827a58b1 Binary files /dev/null and b/mobile/ios/Client/Assets/TopSites/costco-com.png differ diff --git a/mobile/ios/Client/Assets/TopSites/craigslist-org.png b/mobile/ios/Client/Assets/TopSites/craigslist-org.png new file mode 100644 index 0000000000..e1825ae0e2 Binary files /dev/null and b/mobile/ios/Client/Assets/TopSites/craigslist-org.png differ diff --git a/mobile/ios/Client/Assets/TopSites/dailybeast-com.png b/mobile/ios/Client/Assets/TopSites/dailybeast-com.png new file mode 100644 index 0000000000..5102bef549 Binary files /dev/null and b/mobile/ios/Client/Assets/TopSites/dailybeast-com.png differ diff --git a/mobile/ios/Client/Assets/TopSites/dailymail-co-uk.png b/mobile/ios/Client/Assets/TopSites/dailymail-co-uk.png new file mode 100644 index 0000000000..9c3374d77c Binary files /dev/null and b/mobile/ios/Client/Assets/TopSites/dailymail-co-uk.png differ diff --git a/mobile/ios/Client/Assets/TopSites/delta-com.png b/mobile/ios/Client/Assets/TopSites/delta-com.png new file mode 100644 index 0000000000..eea4df5e0e Binary files /dev/null and b/mobile/ios/Client/Assets/TopSites/delta-com.png differ diff --git a/mobile/ios/Client/Assets/TopSites/deviantart-com.png b/mobile/ios/Client/Assets/TopSites/deviantart-com.png new file mode 100644 index 0000000000..41347d31ad Binary files /dev/null and b/mobile/ios/Client/Assets/TopSites/deviantart-com.png differ diff --git a/mobile/ios/Client/Assets/TopSites/digg-com.png b/mobile/ios/Client/Assets/TopSites/digg-com.png new file mode 100644 index 0000000000..cd547b82e3 Binary files /dev/null and b/mobile/ios/Client/Assets/TopSites/digg-com.png differ diff --git a/mobile/ios/Client/Assets/TopSites/diply-com.png b/mobile/ios/Client/Assets/TopSites/diply-com.png new file mode 100644 index 0000000000..349407a691 Binary files /dev/null and b/mobile/ios/Client/Assets/TopSites/diply-com.png differ diff --git a/mobile/ios/Client/Assets/TopSites/discovercard-com.png b/mobile/ios/Client/Assets/TopSites/discovercard-com.png new file mode 100644 index 0000000000..b9feba07c0 Binary files /dev/null and b/mobile/ios/Client/Assets/TopSites/discovercard-com.png differ diff --git a/mobile/ios/Client/Assets/TopSites/dropbox-com.png b/mobile/ios/Client/Assets/TopSites/dropbox-com.png new file mode 100644 index 0000000000..22fd13b491 Binary files /dev/null and b/mobile/ios/Client/Assets/TopSites/dropbox-com.png differ diff --git a/mobile/ios/Client/Assets/TopSites/drudgereport-com.png b/mobile/ios/Client/Assets/TopSites/drudgereport-com.png new file mode 100644 index 0000000000..fbdd4ef0f1 Binary files /dev/null and b/mobile/ios/Client/Assets/TopSites/drudgereport-com.png differ diff --git a/mobile/ios/Client/Assets/TopSites/ebates-com.png b/mobile/ios/Client/Assets/TopSites/ebates-com.png new file mode 100644 index 0000000000..4fe8c7ead5 Binary files /dev/null and b/mobile/ios/Client/Assets/TopSites/ebates-com.png differ diff --git a/mobile/ios/Client/Assets/TopSites/ebay-com.png b/mobile/ios/Client/Assets/TopSites/ebay-com.png new file mode 100644 index 0000000000..d6f8e6acb2 Binary files /dev/null and b/mobile/ios/Client/Assets/TopSites/ebay-com.png differ diff --git a/mobile/ios/Client/Assets/TopSites/espn-go-com.png b/mobile/ios/Client/Assets/TopSites/espn-go-com.png new file mode 100644 index 0000000000..0322d3d134 Binary files /dev/null and b/mobile/ios/Client/Assets/TopSites/espn-go-com.png differ diff --git a/mobile/ios/Client/Assets/TopSites/etsy-com.png b/mobile/ios/Client/Assets/TopSites/etsy-com.png new file mode 100644 index 0000000000..a8cfe2daa4 Binary files /dev/null and b/mobile/ios/Client/Assets/TopSites/etsy-com.png differ diff --git a/mobile/ios/Client/Assets/TopSites/eventbrite-com.png b/mobile/ios/Client/Assets/TopSites/eventbrite-com.png new file mode 100644 index 0000000000..01b74c3202 Binary files /dev/null and b/mobile/ios/Client/Assets/TopSites/eventbrite-com.png differ diff --git a/mobile/ios/Client/Assets/TopSites/expedia-com.png b/mobile/ios/Client/Assets/TopSites/expedia-com.png new file mode 100644 index 0000000000..48125f8985 Binary files /dev/null and b/mobile/ios/Client/Assets/TopSites/expedia-com.png differ diff --git a/mobile/ios/Client/Assets/TopSites/facebook-com.png b/mobile/ios/Client/Assets/TopSites/facebook-com.png new file mode 100644 index 0000000000..a820630ea8 Binary files /dev/null and b/mobile/ios/Client/Assets/TopSites/facebook-com.png differ diff --git a/mobile/ios/Client/Assets/TopSites/faithtap-com.png b/mobile/ios/Client/Assets/TopSites/faithtap-com.png new file mode 100644 index 0000000000..7ade921c99 Binary files /dev/null and b/mobile/ios/Client/Assets/TopSites/faithtap-com.png differ diff --git a/mobile/ios/Client/Assets/TopSites/fedex-com.png b/mobile/ios/Client/Assets/TopSites/fedex-com.png new file mode 100644 index 0000000000..4711a9ace7 Binary files /dev/null and b/mobile/ios/Client/Assets/TopSites/fedex-com.png differ diff --git a/mobile/ios/Client/Assets/TopSites/feedly-com.png b/mobile/ios/Client/Assets/TopSites/feedly-com.png new file mode 100644 index 0000000000..87beb5cb8e Binary files /dev/null and b/mobile/ios/Client/Assets/TopSites/feedly-com.png differ diff --git a/mobile/ios/Client/Assets/TopSites/fitbit-com.png b/mobile/ios/Client/Assets/TopSites/fitbit-com.png new file mode 100644 index 0000000000..9ea8afa9d1 Binary files /dev/null and b/mobile/ios/Client/Assets/TopSites/fitbit-com.png differ diff --git a/mobile/ios/Client/Assets/TopSites/flickr-com.png b/mobile/ios/Client/Assets/TopSites/flickr-com.png new file mode 100644 index 0000000000..41db127f9e Binary files /dev/null and b/mobile/ios/Client/Assets/TopSites/flickr-com.png differ diff --git a/mobile/ios/Client/Assets/TopSites/foodnetwork-com.png b/mobile/ios/Client/Assets/TopSites/foodnetwork-com.png new file mode 100644 index 0000000000..ba3cee06dd Binary files /dev/null and b/mobile/ios/Client/Assets/TopSites/foodnetwork-com.png differ diff --git a/mobile/ios/Client/Assets/TopSites/forbes-com.png b/mobile/ios/Client/Assets/TopSites/forbes-com.png new file mode 100644 index 0000000000..e813d38800 Binary files /dev/null and b/mobile/ios/Client/Assets/TopSites/forbes-com.png differ diff --git a/mobile/ios/Client/Assets/TopSites/foxnews-com.png b/mobile/ios/Client/Assets/TopSites/foxnews-com.png new file mode 100644 index 0000000000..0c1cfc983f Binary files /dev/null and b/mobile/ios/Client/Assets/TopSites/foxnews-com.png differ diff --git a/mobile/ios/Client/Assets/TopSites/gap-com.png b/mobile/ios/Client/Assets/TopSites/gap-com.png new file mode 100644 index 0000000000..11f24e1268 Binary files /dev/null and b/mobile/ios/Client/Assets/TopSites/gap-com.png differ diff --git a/mobile/ios/Client/Assets/TopSites/gawker-com.png b/mobile/ios/Client/Assets/TopSites/gawker-com.png new file mode 100644 index 0000000000..cb2ba8a90d Binary files /dev/null and b/mobile/ios/Client/Assets/TopSites/gawker-com.png differ diff --git a/mobile/ios/Client/Assets/TopSites/gfycat-com.png b/mobile/ios/Client/Assets/TopSites/gfycat-com.png new file mode 100644 index 0000000000..7305cdee70 Binary files /dev/null and b/mobile/ios/Client/Assets/TopSites/gfycat-com.png differ diff --git a/mobile/ios/Client/Assets/TopSites/github-com.png b/mobile/ios/Client/Assets/TopSites/github-com.png new file mode 100644 index 0000000000..369315c603 Binary files /dev/null and b/mobile/ios/Client/Assets/TopSites/github-com.png differ diff --git a/mobile/ios/Client/Assets/TopSites/gizmodo-com.png b/mobile/ios/Client/Assets/TopSites/gizmodo-com.png new file mode 100644 index 0000000000..e332f8194b Binary files /dev/null and b/mobile/ios/Client/Assets/TopSites/gizmodo-com.png differ diff --git a/mobile/ios/Client/Assets/TopSites/glassdoor-com.png b/mobile/ios/Client/Assets/TopSites/glassdoor-com.png new file mode 100644 index 0000000000..c92b84b5ed Binary files /dev/null and b/mobile/ios/Client/Assets/TopSites/glassdoor-com.png differ diff --git a/mobile/ios/Client/Assets/TopSites/go-com.png b/mobile/ios/Client/Assets/TopSites/go-com.png new file mode 100644 index 0000000000..86b97210d8 Binary files /dev/null and b/mobile/ios/Client/Assets/TopSites/go-com.png differ diff --git a/mobile/ios/Client/Assets/TopSites/goodreads-com.png b/mobile/ios/Client/Assets/TopSites/goodreads-com.png new file mode 100644 index 0000000000..a07b21073a Binary files /dev/null and b/mobile/ios/Client/Assets/TopSites/goodreads-com.png differ diff --git a/mobile/ios/Client/Assets/TopSites/google-admin.png b/mobile/ios/Client/Assets/TopSites/google-admin.png new file mode 100644 index 0000000000..666a1ca647 Binary files /dev/null and b/mobile/ios/Client/Assets/TopSites/google-admin.png differ diff --git a/mobile/ios/Client/Assets/TopSites/google-calendar.png b/mobile/ios/Client/Assets/TopSites/google-calendar.png new file mode 100644 index 0000000000..cde34eff31 Binary files /dev/null and b/mobile/ios/Client/Assets/TopSites/google-calendar.png differ diff --git a/mobile/ios/Client/Assets/TopSites/google-com.png b/mobile/ios/Client/Assets/TopSites/google-com.png new file mode 100644 index 0000000000..6b3c2b2e01 Binary files /dev/null and b/mobile/ios/Client/Assets/TopSites/google-com.png differ diff --git a/mobile/ios/Client/Assets/TopSites/google-contacts.png b/mobile/ios/Client/Assets/TopSites/google-contacts.png new file mode 100644 index 0000000000..968c7e0e51 Binary files /dev/null and b/mobile/ios/Client/Assets/TopSites/google-contacts.png differ diff --git a/mobile/ios/Client/Assets/TopSites/google-docs.png b/mobile/ios/Client/Assets/TopSites/google-docs.png new file mode 100644 index 0000000000..f75ce6b7b8 Binary files /dev/null and b/mobile/ios/Client/Assets/TopSites/google-docs.png differ diff --git a/mobile/ios/Client/Assets/TopSites/google-drive.png b/mobile/ios/Client/Assets/TopSites/google-drive.png new file mode 100644 index 0000000000..6f43fc7dc5 Binary files /dev/null and b/mobile/ios/Client/Assets/TopSites/google-drive.png differ diff --git a/mobile/ios/Client/Assets/TopSites/google-forms.png b/mobile/ios/Client/Assets/TopSites/google-forms.png new file mode 100644 index 0000000000..daf8fe6e5d Binary files /dev/null and b/mobile/ios/Client/Assets/TopSites/google-forms.png differ diff --git a/mobile/ios/Client/Assets/TopSites/google-gmail.png b/mobile/ios/Client/Assets/TopSites/google-gmail.png new file mode 100644 index 0000000000..fd9cec2c57 Binary files /dev/null and b/mobile/ios/Client/Assets/TopSites/google-gmail.png differ diff --git a/mobile/ios/Client/Assets/TopSites/google-groups.png b/mobile/ios/Client/Assets/TopSites/google-groups.png new file mode 100644 index 0000000000..8691079905 Binary files /dev/null and b/mobile/ios/Client/Assets/TopSites/google-groups.png differ diff --git a/mobile/ios/Client/Assets/TopSites/google-hangouts.png b/mobile/ios/Client/Assets/TopSites/google-hangouts.png new file mode 100644 index 0000000000..de2e88bdae Binary files /dev/null and b/mobile/ios/Client/Assets/TopSites/google-hangouts.png differ diff --git a/mobile/ios/Client/Assets/TopSites/google-photos.png b/mobile/ios/Client/Assets/TopSites/google-photos.png new file mode 100644 index 0000000000..367751a8d2 Binary files /dev/null and b/mobile/ios/Client/Assets/TopSites/google-photos.png differ diff --git a/mobile/ios/Client/Assets/TopSites/google-plus.png b/mobile/ios/Client/Assets/TopSites/google-plus.png new file mode 100644 index 0000000000..ea8e8d030f Binary files /dev/null and b/mobile/ios/Client/Assets/TopSites/google-plus.png differ diff --git a/mobile/ios/Client/Assets/TopSites/google-sheets.png b/mobile/ios/Client/Assets/TopSites/google-sheets.png new file mode 100644 index 0000000000..69f1e23f27 Binary files /dev/null and b/mobile/ios/Client/Assets/TopSites/google-sheets.png differ diff --git a/mobile/ios/Client/Assets/TopSites/google-sites.png b/mobile/ios/Client/Assets/TopSites/google-sites.png new file mode 100644 index 0000000000..bb6ead5d8d Binary files /dev/null and b/mobile/ios/Client/Assets/TopSites/google-sites.png differ diff --git a/mobile/ios/Client/Assets/TopSites/google-slides.png b/mobile/ios/Client/Assets/TopSites/google-slides.png new file mode 100644 index 0000000000..508375aa4f Binary files /dev/null and b/mobile/ios/Client/Assets/TopSites/google-slides.png differ diff --git a/mobile/ios/Client/Assets/TopSites/groupon-com.png b/mobile/ios/Client/Assets/TopSites/groupon-com.png new file mode 100644 index 0000000000..10e04e5d81 Binary files /dev/null and b/mobile/ios/Client/Assets/TopSites/groupon-com.png differ diff --git a/mobile/ios/Client/Assets/TopSites/homedepot-com.png b/mobile/ios/Client/Assets/TopSites/homedepot-com.png new file mode 100644 index 0000000000..273fee4216 Binary files /dev/null and b/mobile/ios/Client/Assets/TopSites/homedepot-com.png differ diff --git a/mobile/ios/Client/Assets/TopSites/houzz-com.png b/mobile/ios/Client/Assets/TopSites/houzz-com.png new file mode 100644 index 0000000000..aff94af116 Binary files /dev/null and b/mobile/ios/Client/Assets/TopSites/houzz-com.png differ diff --git a/mobile/ios/Client/Assets/TopSites/huffingtonpost-com.png b/mobile/ios/Client/Assets/TopSites/huffingtonpost-com.png new file mode 100644 index 0000000000..38a2153826 Binary files /dev/null and b/mobile/ios/Client/Assets/TopSites/huffingtonpost-com.png differ diff --git a/mobile/ios/Client/Assets/TopSites/hulu-com.png b/mobile/ios/Client/Assets/TopSites/hulu-com.png new file mode 100644 index 0000000000..593d18d8fb Binary files /dev/null and b/mobile/ios/Client/Assets/TopSites/hulu-com.png differ diff --git a/mobile/ios/Client/Assets/TopSites/ign-com.png b/mobile/ios/Client/Assets/TopSites/ign-com.png new file mode 100644 index 0000000000..2cbc573762 Binary files /dev/null and b/mobile/ios/Client/Assets/TopSites/ign-com.png differ diff --git a/mobile/ios/Client/Assets/TopSites/ikea-com.png b/mobile/ios/Client/Assets/TopSites/ikea-com.png new file mode 100644 index 0000000000..08a5c017c9 Binary files /dev/null and b/mobile/ios/Client/Assets/TopSites/ikea-com.png differ diff --git a/mobile/ios/Client/Assets/TopSites/images-google-com.png b/mobile/ios/Client/Assets/TopSites/images-google-com.png new file mode 100644 index 0000000000..ef4c566931 Binary files /dev/null and b/mobile/ios/Client/Assets/TopSites/images-google-com.png differ diff --git a/mobile/ios/Client/Assets/TopSites/imdb-com.png b/mobile/ios/Client/Assets/TopSites/imdb-com.png new file mode 100644 index 0000000000..8c06de0eb3 Binary files /dev/null and b/mobile/ios/Client/Assets/TopSites/imdb-com.png differ diff --git a/mobile/ios/Client/Assets/TopSites/imgur-com.png b/mobile/ios/Client/Assets/TopSites/imgur-com.png new file mode 100644 index 0000000000..ec963f908d Binary files /dev/null and b/mobile/ios/Client/Assets/TopSites/imgur-com.png differ diff --git a/mobile/ios/Client/Assets/TopSites/instagram-com.png b/mobile/ios/Client/Assets/TopSites/instagram-com.png new file mode 100644 index 0000000000..7197f1c59b Binary files /dev/null and b/mobile/ios/Client/Assets/TopSites/instagram-com.png differ diff --git a/mobile/ios/Client/Assets/TopSites/instructure-com.png b/mobile/ios/Client/Assets/TopSites/instructure-com.png new file mode 100644 index 0000000000..e874d6c5da Binary files /dev/null and b/mobile/ios/Client/Assets/TopSites/instructure-com.png differ diff --git a/mobile/ios/Client/Assets/TopSites/intuit-com.png b/mobile/ios/Client/Assets/TopSites/intuit-com.png new file mode 100644 index 0000000000..45599f9f03 Binary files /dev/null and b/mobile/ios/Client/Assets/TopSites/intuit-com.png differ diff --git a/mobile/ios/Client/Assets/TopSites/invision-com.png b/mobile/ios/Client/Assets/TopSites/invision-com.png new file mode 100644 index 0000000000..3ff2b533d9 Binary files /dev/null and b/mobile/ios/Client/Assets/TopSites/invision-com.png differ diff --git a/mobile/ios/Client/Assets/TopSites/irs-gov.png b/mobile/ios/Client/Assets/TopSites/irs-gov.png new file mode 100644 index 0000000000..2c8986235a Binary files /dev/null and b/mobile/ios/Client/Assets/TopSites/irs-gov.png differ diff --git a/mobile/ios/Client/Assets/TopSites/jcpenney-com.png b/mobile/ios/Client/Assets/TopSites/jcpenney-com.png new file mode 100644 index 0000000000..5ccf7c86f0 Binary files /dev/null and b/mobile/ios/Client/Assets/TopSites/jcpenney-com.png differ diff --git a/mobile/ios/Client/Assets/TopSites/jd-com.png b/mobile/ios/Client/Assets/TopSites/jd-com.png new file mode 100644 index 0000000000..6169213af4 Binary files /dev/null and b/mobile/ios/Client/Assets/TopSites/jd-com.png differ diff --git a/mobile/ios/Client/Assets/TopSites/kayak-com.png b/mobile/ios/Client/Assets/TopSites/kayak-com.png new file mode 100644 index 0000000000..c0c7938b6b Binary files /dev/null and b/mobile/ios/Client/Assets/TopSites/kayak-com.png differ diff --git a/mobile/ios/Client/Assets/TopSites/kohls-com.png b/mobile/ios/Client/Assets/TopSites/kohls-com.png new file mode 100644 index 0000000000..ad486a5541 Binary files /dev/null and b/mobile/ios/Client/Assets/TopSites/kohls-com.png differ diff --git a/mobile/ios/Client/Assets/TopSites/latimes-com.png b/mobile/ios/Client/Assets/TopSites/latimes-com.png new file mode 100644 index 0000000000..4c3e94b2d7 Binary files /dev/null and b/mobile/ios/Client/Assets/TopSites/latimes-com.png differ diff --git a/mobile/ios/Client/Assets/TopSites/lifehacker-com.png b/mobile/ios/Client/Assets/TopSites/lifehacker-com.png new file mode 100644 index 0000000000..d06b35d0e2 Binary files /dev/null and b/mobile/ios/Client/Assets/TopSites/lifehacker-com.png differ diff --git a/mobile/ios/Client/Assets/TopSites/linkedin-com.png b/mobile/ios/Client/Assets/TopSites/linkedin-com.png new file mode 100644 index 0000000000..5f005fddc8 Binary files /dev/null and b/mobile/ios/Client/Assets/TopSites/linkedin-com.png differ diff --git a/mobile/ios/Client/Assets/TopSites/live-com.png b/mobile/ios/Client/Assets/TopSites/live-com.png new file mode 100644 index 0000000000..8255e40097 Binary files /dev/null and b/mobile/ios/Client/Assets/TopSites/live-com.png differ diff --git a/mobile/ios/Client/Assets/TopSites/lowes-com.png b/mobile/ios/Client/Assets/TopSites/lowes-com.png new file mode 100644 index 0000000000..503534a388 Binary files /dev/null and b/mobile/ios/Client/Assets/TopSites/lowes-com.png differ diff --git a/mobile/ios/Client/Assets/TopSites/macys-com.png b/mobile/ios/Client/Assets/TopSites/macys-com.png new file mode 100644 index 0000000000..a75ef3e4b6 Binary files /dev/null and b/mobile/ios/Client/Assets/TopSites/macys-com.png differ diff --git a/mobile/ios/Client/Assets/TopSites/mapquest-com.png b/mobile/ios/Client/Assets/TopSites/mapquest-com.png new file mode 100644 index 0000000000..84185b3712 Binary files /dev/null and b/mobile/ios/Client/Assets/TopSites/mapquest-com.png differ diff --git a/mobile/ios/Client/Assets/TopSites/mashable-com.png b/mobile/ios/Client/Assets/TopSites/mashable-com.png new file mode 100644 index 0000000000..42d62dc371 Binary files /dev/null and b/mobile/ios/Client/Assets/TopSites/mashable-com.png differ diff --git a/mobile/ios/Client/Assets/TopSites/microsoft-com.png b/mobile/ios/Client/Assets/TopSites/microsoft-com.png new file mode 100644 index 0000000000..32db2a40c8 Binary files /dev/null and b/mobile/ios/Client/Assets/TopSites/microsoft-com.png differ diff --git a/mobile/ios/Client/Assets/TopSites/microsoftonline-com.png b/mobile/ios/Client/Assets/TopSites/microsoftonline-com.png new file mode 100644 index 0000000000..f50d1986bb Binary files /dev/null and b/mobile/ios/Client/Assets/TopSites/microsoftonline-com.png differ diff --git a/mobile/ios/Client/Assets/TopSites/mlb-com.png b/mobile/ios/Client/Assets/TopSites/mlb-com.png new file mode 100644 index 0000000000..a3e9c96658 Binary files /dev/null and b/mobile/ios/Client/Assets/TopSites/mlb-com.png differ diff --git a/mobile/ios/Client/Assets/TopSites/mozilla-com.png b/mobile/ios/Client/Assets/TopSites/mozilla-com.png new file mode 100644 index 0000000000..41bb981cdb Binary files /dev/null and b/mobile/ios/Client/Assets/TopSites/mozilla-com.png differ diff --git a/mobile/ios/Client/Assets/TopSites/msn-com.png b/mobile/ios/Client/Assets/TopSites/msn-com.png new file mode 100644 index 0000000000..5bb3403bff Binary files /dev/null and b/mobile/ios/Client/Assets/TopSites/msn-com.png differ diff --git a/mobile/ios/Client/Assets/TopSites/nbcnews-com.png b/mobile/ios/Client/Assets/TopSites/nbcnews-com.png new file mode 100644 index 0000000000..4f7c467cdb Binary files /dev/null and b/mobile/ios/Client/Assets/TopSites/nbcnews-com.png differ diff --git a/mobile/ios/Client/Assets/TopSites/netflix-com.png b/mobile/ios/Client/Assets/TopSites/netflix-com.png new file mode 100644 index 0000000000..708f2102fc Binary files /dev/null and b/mobile/ios/Client/Assets/TopSites/netflix-com.png differ diff --git a/mobile/ios/Client/Assets/TopSites/newegg-com.png b/mobile/ios/Client/Assets/TopSites/newegg-com.png new file mode 100644 index 0000000000..a98d2fae0f Binary files /dev/null and b/mobile/ios/Client/Assets/TopSites/newegg-com.png differ diff --git a/mobile/ios/Client/Assets/TopSites/news-ycombinator-com.png b/mobile/ios/Client/Assets/TopSites/news-ycombinator-com.png new file mode 100644 index 0000000000..8ede606794 Binary files /dev/null and b/mobile/ios/Client/Assets/TopSites/news-ycombinator-com.png differ diff --git a/mobile/ios/Client/Assets/TopSites/nih-gov.png b/mobile/ios/Client/Assets/TopSites/nih-gov.png new file mode 100644 index 0000000000..237b1038c6 Binary files /dev/null and b/mobile/ios/Client/Assets/TopSites/nih-gov.png differ diff --git a/mobile/ios/Client/Assets/TopSites/nordstrom-com.png b/mobile/ios/Client/Assets/TopSites/nordstrom-com.png new file mode 100644 index 0000000000..247be3953e Binary files /dev/null and b/mobile/ios/Client/Assets/TopSites/nordstrom-com.png differ diff --git a/mobile/ios/Client/Assets/TopSites/npr-org.png b/mobile/ios/Client/Assets/TopSites/npr-org.png new file mode 100644 index 0000000000..8944be7270 Binary files /dev/null and b/mobile/ios/Client/Assets/TopSites/npr-org.png differ diff --git a/mobile/ios/Client/Assets/TopSites/nypost-com.png b/mobile/ios/Client/Assets/TopSites/nypost-com.png new file mode 100644 index 0000000000..d9ca6fd43e Binary files /dev/null and b/mobile/ios/Client/Assets/TopSites/nypost-com.png differ diff --git a/mobile/ios/Client/Assets/TopSites/nytimes-com.png b/mobile/ios/Client/Assets/TopSites/nytimes-com.png new file mode 100644 index 0000000000..3d316e1743 Binary files /dev/null and b/mobile/ios/Client/Assets/TopSites/nytimes-com.png differ diff --git a/mobile/ios/Client/Assets/TopSites/office-com.png b/mobile/ios/Client/Assets/TopSites/office-com.png new file mode 100644 index 0000000000..d7ad7649a5 Binary files /dev/null and b/mobile/ios/Client/Assets/TopSites/office-com.png differ diff --git a/mobile/ios/Client/Assets/TopSites/overstock-com.png b/mobile/ios/Client/Assets/TopSites/overstock-com.png new file mode 100644 index 0000000000..469659b8e8 Binary files /dev/null and b/mobile/ios/Client/Assets/TopSites/overstock-com.png differ diff --git a/mobile/ios/Client/Assets/TopSites/pandora-com.png b/mobile/ios/Client/Assets/TopSites/pandora-com.png new file mode 100644 index 0000000000..20d2d13c77 Binary files /dev/null and b/mobile/ios/Client/Assets/TopSites/pandora-com.png differ diff --git a/mobile/ios/Client/Assets/TopSites/patch-com.png b/mobile/ios/Client/Assets/TopSites/patch-com.png new file mode 100644 index 0000000000..bdf24ac327 Binary files /dev/null and b/mobile/ios/Client/Assets/TopSites/patch-com.png differ diff --git a/mobile/ios/Client/Assets/TopSites/paypal-com.png b/mobile/ios/Client/Assets/TopSites/paypal-com.png new file mode 100644 index 0000000000..4783328a1b Binary files /dev/null and b/mobile/ios/Client/Assets/TopSites/paypal-com.png differ diff --git a/mobile/ios/Client/Assets/TopSites/people-com.png b/mobile/ios/Client/Assets/TopSites/people-com.png new file mode 100644 index 0000000000..51bc55c432 Binary files /dev/null and b/mobile/ios/Client/Assets/TopSites/people-com.png differ diff --git a/mobile/ios/Client/Assets/TopSites/pinterest-com.png b/mobile/ios/Client/Assets/TopSites/pinterest-com.png new file mode 100644 index 0000000000..5b4a427ff7 Binary files /dev/null and b/mobile/ios/Client/Assets/TopSites/pinterest-com.png differ diff --git a/mobile/ios/Client/Assets/TopSites/politico-com.png b/mobile/ios/Client/Assets/TopSites/politico-com.png new file mode 100644 index 0000000000..561ca25390 Binary files /dev/null and b/mobile/ios/Client/Assets/TopSites/politico-com.png differ diff --git a/mobile/ios/Client/Assets/TopSites/qq-com.png b/mobile/ios/Client/Assets/TopSites/qq-com.png new file mode 100644 index 0000000000..764c93cfb2 Binary files /dev/null and b/mobile/ios/Client/Assets/TopSites/qq-com.png differ diff --git a/mobile/ios/Client/Assets/TopSites/quora-com.png b/mobile/ios/Client/Assets/TopSites/quora-com.png new file mode 100644 index 0000000000..99b8364016 Binary files /dev/null and b/mobile/ios/Client/Assets/TopSites/quora-com.png differ diff --git a/mobile/ios/Client/Assets/TopSites/realtor-com.png b/mobile/ios/Client/Assets/TopSites/realtor-com.png new file mode 100644 index 0000000000..4c4948cd57 Binary files /dev/null and b/mobile/ios/Client/Assets/TopSites/realtor-com.png differ diff --git a/mobile/ios/Client/Assets/TopSites/reddit-com.png b/mobile/ios/Client/Assets/TopSites/reddit-com.png new file mode 100644 index 0000000000..782b44561f Binary files /dev/null and b/mobile/ios/Client/Assets/TopSites/reddit-com.png differ diff --git a/mobile/ios/Client/Assets/TopSites/salesforce-com.png b/mobile/ios/Client/Assets/TopSites/salesforce-com.png new file mode 100644 index 0000000000..86819ac168 Binary files /dev/null and b/mobile/ios/Client/Assets/TopSites/salesforce-com.png differ diff --git a/mobile/ios/Client/Assets/TopSites/sears-com.png b/mobile/ios/Client/Assets/TopSites/sears-com.png new file mode 100644 index 0000000000..03faa333bd Binary files /dev/null and b/mobile/ios/Client/Assets/TopSites/sears-com.png differ diff --git a/mobile/ios/Client/Assets/TopSites/sina-com.png b/mobile/ios/Client/Assets/TopSites/sina-com.png new file mode 100644 index 0000000000..dd0177b1d7 Binary files /dev/null and b/mobile/ios/Client/Assets/TopSites/sina-com.png differ diff --git a/mobile/ios/Client/Assets/TopSites/slate-com.png b/mobile/ios/Client/Assets/TopSites/slate-com.png new file mode 100644 index 0000000000..0688958e42 Binary files /dev/null and b/mobile/ios/Client/Assets/TopSites/slate-com.png differ diff --git a/mobile/ios/Client/Assets/TopSites/slickdeals-net.png b/mobile/ios/Client/Assets/TopSites/slickdeals-net.png new file mode 100644 index 0000000000..2683ca9adc Binary files /dev/null and b/mobile/ios/Client/Assets/TopSites/slickdeals-net.png differ diff --git a/mobile/ios/Client/Assets/TopSites/soundcloud-com.png b/mobile/ios/Client/Assets/TopSites/soundcloud-com.png new file mode 100644 index 0000000000..d12f24f56c Binary files /dev/null and b/mobile/ios/Client/Assets/TopSites/soundcloud-com.png differ diff --git a/mobile/ios/Client/Assets/TopSites/southwest-com.png b/mobile/ios/Client/Assets/TopSites/southwest-com.png new file mode 100644 index 0000000000..ef14ad7682 Binary files /dev/null and b/mobile/ios/Client/Assets/TopSites/southwest-com.png differ diff --git a/mobile/ios/Client/Assets/TopSites/spotify-com.png b/mobile/ios/Client/Assets/TopSites/spotify-com.png new file mode 100644 index 0000000000..b1d0b364de Binary files /dev/null and b/mobile/ios/Client/Assets/TopSites/spotify-com.png differ diff --git a/mobile/ios/Client/Assets/TopSites/stackexchange-com.png b/mobile/ios/Client/Assets/TopSites/stackexchange-com.png new file mode 100644 index 0000000000..b5c50b5069 Binary files /dev/null and b/mobile/ios/Client/Assets/TopSites/stackexchange-com.png differ diff --git a/mobile/ios/Client/Assets/TopSites/stackoverflow-com.png b/mobile/ios/Client/Assets/TopSites/stackoverflow-com.png new file mode 100644 index 0000000000..8b97dba3d0 Binary files /dev/null and b/mobile/ios/Client/Assets/TopSites/stackoverflow-com.png differ diff --git a/mobile/ios/Client/Assets/TopSites/staples-com.png b/mobile/ios/Client/Assets/TopSites/staples-com.png new file mode 100644 index 0000000000..2571d42cc3 Binary files /dev/null and b/mobile/ios/Client/Assets/TopSites/staples-com.png differ diff --git a/mobile/ios/Client/Assets/TopSites/strava-com.png b/mobile/ios/Client/Assets/TopSites/strava-com.png new file mode 100644 index 0000000000..76bf338339 Binary files /dev/null and b/mobile/ios/Client/Assets/TopSites/strava-com.png differ diff --git a/mobile/ios/Client/Assets/TopSites/surveymonkey-com.png b/mobile/ios/Client/Assets/TopSites/surveymonkey-com.png new file mode 100644 index 0000000000..4d48bf62ca Binary files /dev/null and b/mobile/ios/Client/Assets/TopSites/surveymonkey-com.png differ diff --git a/mobile/ios/Client/Assets/TopSites/swagbucks-com.png b/mobile/ios/Client/Assets/TopSites/swagbucks-com.png new file mode 100644 index 0000000000..3e1fc6df7e Binary files /dev/null and b/mobile/ios/Client/Assets/TopSites/swagbucks-com.png differ diff --git a/mobile/ios/Client/Assets/TopSites/t-mobile-com.png b/mobile/ios/Client/Assets/TopSites/t-mobile-com.png new file mode 100644 index 0000000000..221e66c359 Binary files /dev/null and b/mobile/ios/Client/Assets/TopSites/t-mobile-com.png differ diff --git a/mobile/ios/Client/Assets/TopSites/taboola-com.png b/mobile/ios/Client/Assets/TopSites/taboola-com.png new file mode 100644 index 0000000000..f485c184c4 Binary files /dev/null and b/mobile/ios/Client/Assets/TopSites/taboola-com.png differ diff --git a/mobile/ios/Client/Assets/TopSites/talkingpointsmemo-com.png b/mobile/ios/Client/Assets/TopSites/talkingpointsmemo-com.png new file mode 100644 index 0000000000..781ab88f8c Binary files /dev/null and b/mobile/ios/Client/Assets/TopSites/talkingpointsmemo-com.png differ diff --git a/mobile/ios/Client/Assets/TopSites/taobao-com.png b/mobile/ios/Client/Assets/TopSites/taobao-com.png new file mode 100644 index 0000000000..2fb36ef86f Binary files /dev/null and b/mobile/ios/Client/Assets/TopSites/taobao-com.png differ diff --git a/mobile/ios/Client/Assets/TopSites/target-com.png b/mobile/ios/Client/Assets/TopSites/target-com.png new file mode 100644 index 0000000000..60db330959 Binary files /dev/null and b/mobile/ios/Client/Assets/TopSites/target-com.png differ diff --git a/mobile/ios/Client/Assets/TopSites/thedailybeast-com.png b/mobile/ios/Client/Assets/TopSites/thedailybeast-com.png new file mode 100644 index 0000000000..eac04ef598 Binary files /dev/null and b/mobile/ios/Client/Assets/TopSites/thedailybeast-com.png differ diff --git a/mobile/ios/Client/Assets/TopSites/theguardian-com.png b/mobile/ios/Client/Assets/TopSites/theguardian-com.png new file mode 100644 index 0000000000..8712caf44d Binary files /dev/null and b/mobile/ios/Client/Assets/TopSites/theguardian-com.png differ diff --git a/mobile/ios/Client/Assets/TopSites/thesaurus-com.png b/mobile/ios/Client/Assets/TopSites/thesaurus-com.png new file mode 100644 index 0000000000..e204efc44c Binary files /dev/null and b/mobile/ios/Client/Assets/TopSites/thesaurus-com.png differ diff --git a/mobile/ios/Client/Assets/TopSites/ticketmaster-com.png b/mobile/ios/Client/Assets/TopSites/ticketmaster-com.png new file mode 100644 index 0000000000..8924d6e2f8 Binary files /dev/null and b/mobile/ios/Client/Assets/TopSites/ticketmaster-com.png differ diff --git a/mobile/ios/Client/Assets/TopSites/tripadvisor-com.png b/mobile/ios/Client/Assets/TopSites/tripadvisor-com.png new file mode 100644 index 0000000000..edcfae006d Binary files /dev/null and b/mobile/ios/Client/Assets/TopSites/tripadvisor-com.png differ diff --git a/mobile/ios/Client/Assets/TopSites/trulia-com.png b/mobile/ios/Client/Assets/TopSites/trulia-com.png new file mode 100644 index 0000000000..386dce7102 Binary files /dev/null and b/mobile/ios/Client/Assets/TopSites/trulia-com.png differ diff --git a/mobile/ios/Client/Assets/TopSites/tumblr-com.png b/mobile/ios/Client/Assets/TopSites/tumblr-com.png new file mode 100644 index 0000000000..01b2e8daa1 Binary files /dev/null and b/mobile/ios/Client/Assets/TopSites/tumblr-com.png differ diff --git a/mobile/ios/Client/Assets/TopSites/twitch-tv.png b/mobile/ios/Client/Assets/TopSites/twitch-tv.png new file mode 100644 index 0000000000..2021463da9 Binary files /dev/null and b/mobile/ios/Client/Assets/TopSites/twitch-tv.png differ diff --git a/mobile/ios/Client/Assets/TopSites/twitter-com.png b/mobile/ios/Client/Assets/TopSites/twitter-com.png new file mode 100644 index 0000000000..de2f9eb1de Binary files /dev/null and b/mobile/ios/Client/Assets/TopSites/twitter-com.png differ diff --git a/mobile/ios/Client/Assets/TopSites/ups-com.png b/mobile/ios/Client/Assets/TopSites/ups-com.png new file mode 100644 index 0000000000..62b0ff38b5 Binary files /dev/null and b/mobile/ios/Client/Assets/TopSites/ups-com.png differ diff --git a/mobile/ios/Client/Assets/TopSites/usaa-com.png b/mobile/ios/Client/Assets/TopSites/usaa-com.png new file mode 100644 index 0000000000..3f175e6946 Binary files /dev/null and b/mobile/ios/Client/Assets/TopSites/usaa-com.png differ diff --git a/mobile/ios/Client/Assets/TopSites/usatoday-com.png b/mobile/ios/Client/Assets/TopSites/usatoday-com.png new file mode 100644 index 0000000000..090a26ed7e Binary files /dev/null and b/mobile/ios/Client/Assets/TopSites/usatoday-com.png differ diff --git a/mobile/ios/Client/Assets/TopSites/usbank-com.png b/mobile/ios/Client/Assets/TopSites/usbank-com.png new file mode 100644 index 0000000000..f1f254d3bf Binary files /dev/null and b/mobile/ios/Client/Assets/TopSites/usbank-com.png differ diff --git a/mobile/ios/Client/Assets/TopSites/usps-com.png b/mobile/ios/Client/Assets/TopSites/usps-com.png new file mode 100644 index 0000000000..d709c6e72f Binary files /dev/null and b/mobile/ios/Client/Assets/TopSites/usps-com.png differ diff --git a/mobile/ios/Client/Assets/TopSites/verizon-com.png b/mobile/ios/Client/Assets/TopSites/verizon-com.png new file mode 100644 index 0000000000..001bc86f03 Binary files /dev/null and b/mobile/ios/Client/Assets/TopSites/verizon-com.png differ diff --git a/mobile/ios/Client/Assets/TopSites/verizonwireless-com.png b/mobile/ios/Client/Assets/TopSites/verizonwireless-com.png new file mode 100644 index 0000000000..2bc56995c1 Binary files /dev/null and b/mobile/ios/Client/Assets/TopSites/verizonwireless-com.png differ diff --git a/mobile/ios/Client/Assets/TopSites/vice-com.png b/mobile/ios/Client/Assets/TopSites/vice-com.png new file mode 100644 index 0000000000..c281d1afc6 Binary files /dev/null and b/mobile/ios/Client/Assets/TopSites/vice-com.png differ diff --git a/mobile/ios/Client/Assets/TopSites/vimeo-com.png b/mobile/ios/Client/Assets/TopSites/vimeo-com.png new file mode 100644 index 0000000000..52eb2cd427 Binary files /dev/null and b/mobile/ios/Client/Assets/TopSites/vimeo-com.png differ diff --git a/mobile/ios/Client/Assets/TopSites/walmart-com.png b/mobile/ios/Client/Assets/TopSites/walmart-com.png new file mode 100644 index 0000000000..ac9400b1f7 Binary files /dev/null and b/mobile/ios/Client/Assets/TopSites/walmart-com.png differ diff --git a/mobile/ios/Client/Assets/TopSites/washingtonpost-com.png b/mobile/ios/Client/Assets/TopSites/washingtonpost-com.png new file mode 100644 index 0000000000..bc14e4cbe5 Binary files /dev/null and b/mobile/ios/Client/Assets/TopSites/washingtonpost-com.png differ diff --git a/mobile/ios/Client/Assets/TopSites/wayfair-com.png b/mobile/ios/Client/Assets/TopSites/wayfair-com.png new file mode 100644 index 0000000000..1c467d0be6 Binary files /dev/null and b/mobile/ios/Client/Assets/TopSites/wayfair-com.png differ diff --git a/mobile/ios/Client/Assets/TopSites/weather-com.png b/mobile/ios/Client/Assets/TopSites/weather-com.png new file mode 100644 index 0000000000..866138a42a Binary files /dev/null and b/mobile/ios/Client/Assets/TopSites/weather-com.png differ diff --git a/mobile/ios/Client/Assets/TopSites/webmd-com.png b/mobile/ios/Client/Assets/TopSites/webmd-com.png new file mode 100644 index 0000000000..db3f46094c Binary files /dev/null and b/mobile/ios/Client/Assets/TopSites/webmd-com.png differ diff --git a/mobile/ios/Client/Assets/TopSites/wellsfargo-com.png b/mobile/ios/Client/Assets/TopSites/wellsfargo-com.png new file mode 100644 index 0000000000..3675c67901 Binary files /dev/null and b/mobile/ios/Client/Assets/TopSites/wellsfargo-com.png differ diff --git a/mobile/ios/Client/Assets/TopSites/wikia-com.png b/mobile/ios/Client/Assets/TopSites/wikia-com.png new file mode 100644 index 0000000000..7630eeb187 Binary files /dev/null and b/mobile/ios/Client/Assets/TopSites/wikia-com.png differ diff --git a/mobile/ios/Client/Assets/TopSites/wikihow-com.png b/mobile/ios/Client/Assets/TopSites/wikihow-com.png new file mode 100644 index 0000000000..7560695b3c Binary files /dev/null and b/mobile/ios/Client/Assets/TopSites/wikihow-com.png differ diff --git a/mobile/ios/Client/Assets/TopSites/wikipedia-org.png b/mobile/ios/Client/Assets/TopSites/wikipedia-org.png new file mode 100644 index 0000000000..de6627270e Binary files /dev/null and b/mobile/ios/Client/Assets/TopSites/wikipedia-org.png differ diff --git a/mobile/ios/Client/Assets/TopSites/wired-com.png b/mobile/ios/Client/Assets/TopSites/wired-com.png new file mode 100644 index 0000000000..63fb545a1a Binary files /dev/null and b/mobile/ios/Client/Assets/TopSites/wired-com.png differ diff --git a/mobile/ios/Client/Assets/TopSites/wittyfeed-com.png b/mobile/ios/Client/Assets/TopSites/wittyfeed-com.png new file mode 100644 index 0000000000..6f3a98183d Binary files /dev/null and b/mobile/ios/Client/Assets/TopSites/wittyfeed-com.png differ diff --git a/mobile/ios/Client/Assets/TopSites/wordpress-com.png b/mobile/ios/Client/Assets/TopSites/wordpress-com.png new file mode 100644 index 0000000000..5403f48a70 Binary files /dev/null and b/mobile/ios/Client/Assets/TopSites/wordpress-com.png differ diff --git a/mobile/ios/Client/Assets/TopSites/wsj-com.png b/mobile/ios/Client/Assets/TopSites/wsj-com.png new file mode 100644 index 0000000000..a53adcfd40 Binary files /dev/null and b/mobile/ios/Client/Assets/TopSites/wsj-com.png differ diff --git a/mobile/ios/Client/Assets/TopSites/wunderground-com.png b/mobile/ios/Client/Assets/TopSites/wunderground-com.png new file mode 100644 index 0000000000..d444c58b90 Binary files /dev/null and b/mobile/ios/Client/Assets/TopSites/wunderground-com.png differ diff --git a/mobile/ios/Client/Assets/TopSites/xfinity-com.png b/mobile/ios/Client/Assets/TopSites/xfinity-com.png new file mode 100644 index 0000000000..1b8c732868 Binary files /dev/null and b/mobile/ios/Client/Assets/TopSites/xfinity-com.png differ diff --git a/mobile/ios/Client/Assets/TopSites/yahoo-com.png b/mobile/ios/Client/Assets/TopSites/yahoo-com.png new file mode 100644 index 0000000000..50320c7a16 Binary files /dev/null and b/mobile/ios/Client/Assets/TopSites/yahoo-com.png differ diff --git a/mobile/ios/Client/Assets/TopSites/yelp-com.png b/mobile/ios/Client/Assets/TopSites/yelp-com.png new file mode 100644 index 0000000000..74214d9832 Binary files /dev/null and b/mobile/ios/Client/Assets/TopSites/yelp-com.png differ diff --git a/mobile/ios/Client/Assets/TopSites/youtube-com.png b/mobile/ios/Client/Assets/TopSites/youtube-com.png new file mode 100644 index 0000000000..9e54c0a698 Binary files /dev/null and b/mobile/ios/Client/Assets/TopSites/youtube-com.png differ diff --git a/mobile/ios/Client/Assets/TopSites/zillow-com.png b/mobile/ios/Client/Assets/TopSites/zillow-com.png new file mode 100644 index 0000000000..930abb4bc7 Binary files /dev/null and b/mobile/ios/Client/Assets/TopSites/zillow-com.png differ diff --git a/mobile/ios/Client/Assets/top_sites.json b/mobile/ios/Client/Assets/top_sites.json new file mode 100644 index 0000000000..fd4f2c3ee4 --- /dev/null +++ b/mobile/ios/Client/Assets/top_sites.json @@ -0,0 +1,1384 @@ +[ + { + "title": "aa", + "url": "https://www.aa.com/", + "image_url": "aa-com.png", + "background_color": "#FAFAFA", + "domain": "aa.com" + }, + { + "title": "abcnews.go", + "url": "http://abcnews.go.com/", + "image_url": "abcnews-go-com.png", + "background_color": "#FFF", + "domain": "abcnews.go.com" + }, + { + "title": "about", + "url": "http://www.about.com/", + "image_url": "about-com.png", + "background_color": "#FFF", + "domain": "about.com" + }, + { + "title": "accuweather", + "url": "http://www.accuweather.com/", + "image_url": "accuweather-com.png", + "background_color": "#f56b17", + "domain": "accuweather.com" + }, + { + "title": "adobe", + "url": "http://www.adobe.com/", + "image_url": "adobe-com.png", + "background_color": "#e22919", + "domain": "adobe.com" + }, + { + "title": "adp", + "url": "http://www.adp.com/", + "image_url": "adp-com.png", + "background_color": "#f02311", + "domain": "adp.com" + }, + { + "title": "airbnb", + "url": "https://www.airbnb.com/", + "image_url": "airbnb-com.png", + "background_color": "#ff585a", + "domain": "airbnb.com" + }, + { + "title": "allrecipes", + "url": "http://allrecipes.com/", + "image_url": "allrecipes-com.png", + "background_color": "#ffb333", + "domain": "allrecipes.com" + }, + { + "title": "amazon", + "url": "http://www.amazon.com/", + "image_url": "amazon-com.png", + "background_color": "#FFF", + "is_multi_region_domain": "true", + "domain": "amazon" + }, + { + "title": "americanexpress", + "url": "https://www.americanexpress.com", + "image_url": "americanexpress-com.png", + "background_color": "#e0e0e0", + "domain": "americanexpress.com" + }, + { + "title": "ancestry", + "url": "http://www.ancestry.com/", + "image_url": "ancestry-com.png", + "background_color": "#9bbf2f", + "domain": "ancestry.com" + }, + { + "title": "answers", + "url": "http://www.answers.com", + "image_url": "answers-com.png", + "background_color": "#3c67d5", + "domain": "answers.com" + }, + { + "title": "aol", + "url": "http://www.aol.com/", + "image_url": "aol-com.png", + "background_color": "#e0e0e0", + "domain": "aol.com" + }, + { + "title": "apple", + "url": "http://www.apple.com/", + "image_url": "apple-com.png", + "background_color": "#6d6e71", + "domain": "apple.com" + }, + { + "title": "ask.com", + "url": "http://www.ask.com", + "image_url": "ask-com.png", + "background_color": "#cf0000", + "domain": "ask.com" + }, + { + "title": "att", + "url": "https://www.att.com/", + "image_url": "att-com.png", + "background_color": "#5ba1ca", + "domain": "att.com" + }, + { + "title": "aws.amazon", + "url": "https://aws.amazon.com/", + "image_url": "amazonaws-com.png", + "background_color": "#FFF", + "domain": "aws.amazon.com" + }, + { + "title": "baidu", + "url": "http://baidu.com/", + "image_url": "baidu-com.png", + "background_color": "#c33302", + "domain": "baidu.com" + }, + { + "title": "bankofamerica", + "url": "https://www.bankofamerica.com", + "image_url": "bankofamerica-com.png", + "background_color": "#eb3146", + "domain": "bankofamerica.com" + }, + { + "title": "bbc", + "url": "http://www.bbc.com/", + "image_url": "bbc-com.png", + "background_color": "#000000", + "domain": "bbc.com" + }, + { + "title": "bestbuy", + "url": "http://www.bestbuy.com", + "image_url": "bestbuy-com.png", + "background_color": "#003a65", + "domain": "bestbuy.com" + }, + { + "title": "bing", + "url": "http://www.bing.com/", + "image_url": "bing-com.png", + "background_color": "#138484", + "domain": "bing.com" + }, + { + "title": "blackboard", + "url": "http://www.blackboard.com/", + "image_url": "blackboard-com.png", + "background_color": "#e6e6e6", + "domain": "blackboard.com" + }, + { + "title": "bleacherreport", + "url": "http://bleacherreport.com/", + "image_url": "bleacherreport-com.png", + "background_color": "#ec412e", + "domain": "bleacherreport.com" + }, + { + "title": "blogger", + "url": "https://www.blogger.com/home", + "image_url": "blogger-com.png", + "background_color": "#ff8822", + "domain": "blogger.com" + }, + { + "title": "box", + "url": "https://www.box.com/", + "image_url": "box-com.png", + "background_color": "#4daee8", + "domain": "box.com" + }, + { + "title": "businessinsider", + "url": "http://www.businessinsider.com", + "image_url": "businessinsider-com.png", + "background_color": "#1d5b7d", + "domain": "businessinsider.com" + }, + { + "title": "buzzfeed", + "url": "http://www.buzzfeed.com/index", + "image_url": "buzzfeed-com.png", + "background_color": "#ee3322", + "domain": "buzzfeed.com" + }, + { + "title": "buzzlie", + "url": "http://buzzlie.com/", + "image_url": "buzzlie-com.png", + "background_color": "#ff68b6", + "domain": "buzzlie.com" + }, + { + "title": "California", + "url": "http://ca.gov/", + "image_url": "ca-gov.png", + "background_color": "#000201", + "domain": "ca.gov" + }, + { + "title": "capitalone", + "url": "https://www.capitalone.com/", + "image_url": "capitalone-com.png", + "background_color": "#303e4f", + "domain": "capitalone.com" + }, + { + "title": "cbsnews", + "url": "http://www.cbsnews.com/", + "image_url": "cbsnews-com.png", + "background_color": "#000", + "domain": "cbsnews.com" + }, + { + "title": "cbssports", + "url": "http://www.cbssports.com/", + "image_url": "cbssports-com.png", + "background_color": "#014a8f", + "domain": "cbssports.com" + }, + { + "title": "chase", + "url": "https://www.chase.com", + "image_url": "chase-com.png", + "background_color": "#0d68c1", + "domain": "chase.com" + }, + { + "title": "cnet", + "url": "http://www.cnet.com/", + "image_url": "cnet-com.png", + "background_color": "#FFF", + "domain": "cnet.com" + }, + { + "title": "cnn", + "url": "http://www.cnn.com", + "image_url": "cnn-com.png", + "background_color": "#d41c1e", + "domain": "cnn.com" + }, + { + "title": "comcast", + "urls": ["http://www.comcast.net/", "http://www.xfinity.com/"], + "image_url": "xfinity-com.png", + "background_color": "#000", + "domain": "comcast.net" + }, + { + "title": "conservativetribune", + "url": "http://conservativetribune.com/", + "image_url": "conservativetribune-com.png", + "background_color": "#ae0001", + "domain": "conservativetribune.com" + }, + { + "title": "costco", + "url": "http://www.costco.com/", + "image_url": "costco-com.png", + "background_color": "#005bad", + "domain": "costco.com" + }, + { + "title": "craigslist", + "url": "http://craigslist.org/", + "image_url": "craigslist-org.png", + "background_color": "#652892", + "is_multi_region_domain": "true", + "domain": "craigslist" + }, + { + "title": "dailymail", + "url": "http://www.dailymail.co.uk/", + "image_url": "dailymail-co-uk.png", + "background_color": "#0064c1", + "domain": "dailymail.co.uk" + }, + { + "title": "dailybeast", + "url": "https://www.thedailybeast.com/", + "image_url": "dailybeast-com.png", + "background_color": "#f12c15", + "domain": "dailybeast.com" + }, + { + "title": "delta", + "url": "https://www.delta.com/", + "image_url": "delta-com.png", + "background_color": "#1d649e", + "domain": "delta.com" + }, + { + "title": "deviantart", + "url": "http://www.deviantart.com/", + "image_url": "deviantart-com.png", + "background_color": "#00ce3e", + "domain": "deviantart.com" + }, + { + "title": "digg", + "url": "http://digg.com/", + "image_url": "digg-com.png", + "background_color": "#000", + "domain": "digg.com" + }, + { + "title": "diply", + "url": "http://diply.com/", + "image_url": "diply-com.png", + "background_color": "#2168b3", + "domain": "diply.com" + }, + { + "title": "discover", + "urls": ["https://www.discover.com/", "https://www.discovercard.com/"], + "image_url": "discovercard-com.png", + "background_color": "#d6d6d6", + "domain": "discover.com" + }, + { + "title": "dropbox", + "url": "https://www.dropbox.com/", + "image_url": "dropbox-com.png", + "background_color": "#007fe2", + "domain": "dropbox.com" + }, + { + "title": "drudgereport", + "url": "http://drudgereport.com/", + "image_url": "drudgereport-com.png", + "background_color": "#FFF", + "domain": "drudgereport.com" + }, + { + "title": "ebates", + "url": "http://www.ebates.com/", + "image_url": "ebates-com.png", + "background_color": "#14af44", + "domain": "ebates.com" + }, + { + "title": "ebay", + "url": "http://www.ebay.com", + "image_url": "ebay-com.png", + "background_color": "#ededed", + "domain": "ebay.com" + }, + { + "title": "espn.go", + "url": "http://espn.go.com", + "image_url": "espn-go-com.png", + "background_color": "#4b4b4b", + "domain": "espn.go.com" + }, + { + "title": "etsy", + "url": "https://www.etsy.com/", + "image_url": "etsy-com.png", + "background_color": "#f76300", + "domain": "etsy.com" + }, + { + "title": "eventbrite", + "url": "https://www.eventbrite.com/", + "image_url": "eventbrite-com.png", + "background_color": "#ff8000", + "domain": "eventbrite.com" + }, + { + "title": "expedia", + "url": "https://www.expedia.com/", + "image_url": "expedia-com.png", + "background_color": "#003460", + "domain": "expedia.com" + }, + { + "title": "facebook", + "url": "https://www.facebook.com/", + "image_url": "facebook-com.png", + "background_color": "#3b5998", + "domain": "facebook.com" + }, + { + "title": "faithtap", + "url": "http://faithtap.com/", + "image_url": "faithtap-com.png", + "background_color": "#4c286f", + "domain": "faithtap.com" + }, + { + "title": "fedex", + "url": "http://www.fedex.com/", + "image_url": "fedex-com.png", + "background_color": "#391675", + "domain": "fedex.com" + }, + { + "title": "feedly", + "url": "http://feedly.com/", + "image_url": "feedly-com.png", + "background_color": "#20b447", + "domain": "feedly.com" + }, + { + "title": "fitbit", + "url": "https://www.fitbit.com/", + "image_url": "fitbit-com.png", + "background_color": "#00b0ba", + "domain": "fitbit.com" + }, + { + "title": "flickr", + "url": "https://www.flickr.com", + "image_url": "flickr-com.png", + "background_color": "#dcdcdc", + "domain": "flickr.com" + }, + { + "title": "foodnetwork", + "url": "http://www.foodnetwork.com/", + "image_url": "foodnetwork-com.png", + "background_color": "#f50024", + "domain": "foodnetwork.com" + }, + { + "title": "forbes", + "url": "http://www.forbes.com/", + "image_url": "forbes-com.png", + "background_color": "#4177ab", + "domain": "forbes.com" + }, + { + "title": "foxnews", + "url": "http://www.foxnews.com", + "image_url": "foxnews-com.png", + "background_color": "#9e0b0f", + "domain": "foxnews.com" + }, + { + "title": "gap", + "url": "http://www.gap.com/", + "image_url": "gap-com.png", + "background_color": "#002861", + "domain": "gap.com" + }, + { + "title": "gawker", + "url": "http://gawker.com/", + "image_url": "gawker-com.png", + "background_color": "#d75343", + "domain": "gawker.com" + }, + { + "title": "gfycat", + "url": "http://gfycat.com/", + "image_url": "gfycat-com.png", + "background_color": "#eaeaea", + "domain": "gfycat.com" + }, + { + "title": "GitHub", + "url": "https://github.com/", + "image_url": "github-com.png", + "background_color": "#000", + "domain": "github.com" + }, + { + "title": "gizmodo", + "url": "http://gizmodo.com/", + "image_url": "gizmodo-com.png", + "background_color": "#000", + "domain": "gizmodo.com" + }, + { + "title": "glassdoor", + "url": "https://www.glassdoor.com/", + "image_url": "glassdoor-com.png", + "background_color": "#7aad28", + "domain": "glassdoor.com" + }, + { + "title": "go", + "url": "http://go.com", + "image_url": "go-com.png", + "background_color": "#000", + "domain": "go.com" + }, + { + "title": "goodreads", + "url": "http://www.goodreads.com/", + "image_url": "goodreads-com.png", + "background_color": "#382110", + "domain": "goodreads.com" + }, + { + "title": "google", + "url": "https://www.google.com/", + "image_url": "google-com.png", + "background_color": "#FFF", + "is_multi_region_domain": "true", + "domain": "google" + }, + { + "title": "admin.google", + "url": "https://admin.google.com/", + "image_url": "google-admin.png", + "background_color": "#FFF", + "domain": "admin.google.com" + }, + { + "title": "calendar.google", + "url": "https://calendar.google.com/", + "image_url": "google-calendar.png", + "background_color": "#FFF", + "domain": "calendar.google.com" + }, + { + "title": "contacts.google", + "url": "https://contacts.google.com/", + "image_url": "google-contacts.png", + "background_color": "#FFF", + "domain": "contacts.google.com" + }, + { + "title": "docs.google", + "url": "https://docs.google.com/", + "image_url": "google-docs.png", + "background_color": "#FFF", + "domain": "docs.google.com" + }, + { + "title": "drive.google", + "url": "https://drive.google.com/", + "image_url": "google-drive.png", + "background_color": "#FFF", + "domain": "drive.google.com" + }, + { + "title": "forms.google", + "url": "https://forms.google.com/", + "image_url": "google-forms.png", + "background_color": "#FFF", + "domain": "forms.google.com" + }, + { + "title": "gmail", + "urls": ["https://mail.google.com/", "https://gmail.com"], + "image_url": "google-gmail.png", + "background_color": "#FFF", + "domain": "mail.google.com" + }, + { + "title": "groups.google", + "url": "https://groups.google.com/", + "image_url": "google-groups.png", + "background_color": "#FFF", + "domain": "groups.google.com" + }, + { + "title": "hangouts.google", + "url": "https://hangouts.google.com/", + "image_url": "google-hangouts.png", + "background_color": "#FFF", + "domain": "hangouts.google.com" + }, + { + "title": "plus.google", + "url": "https://plus.google.com/", + "image_url": "google-plus.png", + "background_color": "#FFF", + "domain": "plus.google.com" + }, + { + "title": "sheets.google", + "url": "https://sheets.google.com/", + "image_url": "google-sheets.png", + "background_color": "#FFF", + "domain": "sheets.google.com" + }, + { + "title": "sites.google", + "url": "https://sites.google.com/", + "image_url": "google-sites.png", + "background_color": "#FFF", + "domain": "sites.google.com" + }, + { + "title": "slides.google", + "url": "https://slides.google.com/", + "image_url": "google-slides.png", + "background_color": "#FFF", + "domain": "slides.google.com" + }, + { + "title": "photos.google", + "url": "https://photos.google.com/", + "image_url": "google-photos.png", + "background_color": "#FFF", + "domain": "photos.google.com" + }, + { + "title": "images.google", + "url": "https://images.google.com/", + "image_url": "images-google-com.png", + "background_color": "#FFF", + "domain": "images.google.com" + }, + { + "title": "groupon", + "url": "https://www.groupon.com/", + "image_url": "groupon-com.png", + "background_color": "#53a318", + "domain": "groupon.com" + }, + { + "title": "homedepot", + "url": "http://www.homedepot.com/", + "image_url": "homedepot-com.png", + "background_color": "#f7d5a4", + "domain": "homedepot.com" + }, + { + "title": "houzz", + "url": "https://www.houzz.com/", + "image_url": "houzz-com.png", + "background_color": "#52a02a", + "domain": "houzz.com" + }, + { + "title": "huffingtonpost", + "url": "http://www.huffingtonpost.com/", + "image_url": "huffingtonpost-com.png", + "background_color": "#7dbdb8", + "domain": "huffingtonpost.com" + }, + { + "title": "hulu", + "url": "http://www.hulu.com/", + "image_url": "hulu-com.png", + "background_color": "#97c64f", + "domain": "hulu.com" + }, + { + "title": "ign", + "url": "http://www.ign.com/", + "image_url": "ign-com.png", + "background_color": "#ff0000", + "domain": "ign.com" + }, + { + "title": "ikea", + "url": "http://www.ikea.com/", + "image_url": "ikea-com.png", + "background_color": "#00329c", + "domain": "ikea.com" + }, + { + "title": "imdb", + "url": "http://www.imdb.com/", + "image_url": "imdb-com.png", + "background_color": "#ffd100", + "domain": "imdb.com" + }, + { + "title": "imgur", + "url": "http://imgur.com/", + "image_url": "imgur-com.png", + "background_color": "#2a2c25", + "domain": "imgur.com" + }, + { + "title": "instagram", + "url": "https://www.instagram.com/", + "image_url": "instagram-com.png", + "background_color": "#0b558a", + "domain": "instagram.com" + }, + { + "title": "instructure", + "url": "https://www.instructure.com/", + "image_url": "instructure-com.png", + "background_color": "#efefef", + "domain": "instructure.com" + }, + { + "title": "intuit", + "url": "http://www.intuit.com/", + "image_url": "intuit-com.png", + "background_color": "#f6f6f6", + "domain": "intuit.com" + }, + { + "title": "irs", + "url": "https://www.irs.gov/", + "image_url": "irs-gov.png", + "background_color": "#efefef", + "domain": "irs.gov" + }, + { + "title": "invision", + "urls": ["https://www.invisionapp.com/", "https://mozilla.invisionapp.com/"], + "image_url": "invision-com.png", + "background_color": "#ff2e63", + "domain": "invisionapp.com" + }, + { + "title": "jcpenney", + "url": "http://www.jcpenney.com/", + "image_url": "jcpenney-com.png", + "background_color": "#fa0026", + "domain": "jcpenney.com" + }, + { + "title": "jd", + "url": "http://www.jd.com/", + "image_url": "jd-com.png", + "background_color": "#e50000", + "domain": "jd.com" + }, + { + "title": "kayak", + "url": "https://www.kayak.com/", + "image_url": "kayak-com.png", + "background_color": "#fff", + "domain": "kayak.com" + }, + { + "title": "kohl's", + "url": "http://www.kohls.com", + "image_url": "kohls-com.png", + "background_color": "#000", + "domain": "kohls.com" + }, + { + "title": "latimes", + "url": "http://www.latimes.com/", + "image_url": "latimes-com.png", + "background_color": "#FFF", + "domain": "latimes.com" + }, + { + "title": "lifehacker", + "url": "http://lifehacker.com/", + "image_url": "lifehacker-com.png", + "background_color": "#94b330", + "domain": "lifehacker.com" + }, + { + "title": "linkedin", + "url": "https://www.linkedin.com/", + "image_url": "linkedin-com.png", + "background_color": "#00659b", + "domain": "linkedin.com" + }, + { + "title": "lowes", + "url": "http://www.lowes.com/", + "image_url": "lowes-com.png", + "background_color": "#004793", + "domain": "lowes.com" + }, + { + "title": "macys", + "url": "http://www.macys.com/", + "image_url": "macys-com.png", + "background_color": "#ea0000", + "domain": "macys.com" + }, + { + "title": "login.microsoftonline", + "url": "https://login.microsoftonline.com/", + "image_url": "microsoftonline-com.png", + "background_color": "#ce4f00", + "domain": "login.microsoftonline.com" + }, + { + "title": "mail.live", + "url": "https://mail.live.com", + "image_url": "live-com.png", + "background_color": "#0070c9", + "domain": "mail.live.com" + }, + { + "title": "mapquest", + "url": "http://www.mapquest.com/", + "image_url": "mapquest-com.png", + "background_color": "#373737", + "domain": "mapquest.com" + }, + { + "title": "mashable", + "url": "http://mashable.com/stories/", + "image_url": "mashable-com.png", + "background_color": "#00aef0", + "domain": "mashable.com" + }, + { + "title": "microsoft", + "url": "http://www.microsoft.com/", + "image_url": "microsoft-com.png", + "background_color": "#FFF", + "domain": "microsoft.com" + }, + { + "title": "mlb", + "url": "http://mlb.mlb.com/", + "image_url": "mlb-com.png", + "background_color": "#ffffff", + "domain": "mlb.com" + }, + { + "title": "msn", + "url": "http://www.msn.com/", + "image_url": "msn-com.png", + "background_color": "#000", + "domain": "msn.com" + }, + { + "title": "mozilla", + "url": "http://www.mozilla.com.cn/", + "image_url": "mozilla-com.png", + "background_color": "#000", + "domain": "mozilla.com.cn" + }, + { + "title": "nbcnews", + "url": "http://www.nbcnews.com/", + "image_url": "nbcnews-com.png", + "background_color": "#003a51", + "domain": "nbcnews.com" + }, + { + "title": "netflix", + "url": "https://www.netflix.com/", + "image_url": "netflix-com.png", + "background_color": "#000", + "domain": "netflix.com" + }, + { + "title": "newegg", + "url": "http://www.newegg.com/", + "image_url": "newegg-com.png", + "background_color": "#cecece", + "domain": "newegg.com" + }, + { + "title": "news.ycombinator", + "url": "https://news.ycombinator.com/", + "image_url": "news-ycombinator-com.png", + "background_color": "#D46D1D", + "domain": "news.ycombinator.com" + }, + { + "title": "nih", + "url": "http://www.nih.gov/", + "image_url": "nih-gov.png", + "background_color": "#efefef", + "domain": "nih.gov" + }, + { + "title": "nordstrom", + "url": "http://shop.nordstrom.com/", + "image_url": "nordstrom-com.png", + "background_color": "#7f7d7a", + "domain": "nordstrom.com" + }, + { + "title": "npr", + "url": "http://www.npr.org/", + "image_url": "npr-org.png", + "background_color": "#FFF", + "domain": "npr.org" + }, + { + "title": "nypost", + "url": "http://nypost.com/", + "image_url": "nypost-com.png", + "background_color": "#FFF", + "domain": "nypost.com" + }, + { + "title": "nytimes", + "url": "http://www.nytimes.com", + "image_url": "nytimes-com.png", + "background_color": "#FFF", + "domain": "nytimes.com" + }, + { + "title": "office", + "url": "https://www.office.com/", + "image_url": "office-com.png", + "background_color": "#000", + "domain": "office.com" + }, + { + "title": "online.citi", + "url": "https://online.citi.com/", + "image_url": "citi-com.png", + "background_color": "#FFF", + "domain": "online.citi.com" + }, + { + "title": "overstock", + "url": "http://www.overstock.com/", + "image_url": "overstock-com.png", + "background_color": "#fff", + "domain": "overstock.com" + }, + { + "title": "pandora", + "url": "http://www.pandora.com/", + "image_url": "pandora-com.png", + "background_color": "#efefef", + "domain": "pandora.com" + }, + { + "title": "Patch", + "url": "http://patch.com", + "image_url": "patch-com.png", + "background_color": "#519442", + "domain": "patch.com" + }, + { + "title": "paypal", + "url": "https://www.paypal.com/home", + "image_url": "paypal-com.png", + "background_color": "#009cde", + "domain": "paypal.com" + }, + { + "title": "people.com", + "url": "http://www.people.com/", + "image_url": "people-com.png", + "background_color": "#27c4ff", + "domain": "people.com" + }, + { + "title": "pinterest", + "url": "https://www.pinterest.com/", + "image_url": "pinterest-com.png", + "background_color": "#ba212b", + "domain": "pinterest.com" + }, + { + "title": "politico", + "url": "http://www.politico.com/", + "image_url": "politico-com.png", + "background_color": "#9f0000", + "domain": "politico.com" + }, + { + "title": "quora", + "url": "https://www.quora.com/", + "image_url": "quora-com.png", + "background_color": "#bb2920", + "domain": "quora.com" + }, + { + "title": "qq", + "url": "https://www.qq.com/", + "image_url": "qq-com.png", + "background_color": "#2d91da", + "domain": "qq.com" + }, + { + "title": "realtor", + "url": "http://www.realtor.com/", + "image_url": "realtor-com.png", + "background_color": "#fcfcfc", + "domain": "realtor.com" + }, + { + "title": "reddit", + "url": "https://www.reddit.com/", + "image_url": "reddit-com.png", + "background_color": "#cee3f8", + "domain": "reddit.com" + }, + { + "title": "salesforce", + "url": "http://www.salesforce.com/", + "image_url": "salesforce-com.png", + "background_color": "#efefef", + "domain": "salesforce.com" + }, + { + "title": "sears", + "url": "http://www.sears.com/", + "image_url": "sears-com.png", + "background_color": "#00265a", + "domain": "sears.com" + }, + { + "title": "sina", + "url": "http://www.sina.cn/", + "image_url": "sina-com.png", + "background_color": "#ff0000", + "domain": "sina.cn" + }, + { + "title": "sina", + "url": "http://www.sina.com/", + "image_url": "sina-com.png", + "background_color": "#ff0000", + "domain": "sina.com" + }, + { + "title": "slate", + "url": "http://www.slate.com/", + "image_url": "slate-com.png", + "background_color": "#670033", + "domain": "slate.com" + }, + { + "title": "slickdeals", + "url": "http://slickdeals.net", + "image_url": "slickdeals-net.png", + "background_color": "#0072bd", + "domain": "slickdeals.net" + }, + { + "title": "soundcloud", + "url": "https://soundcloud.com/", + "image_url": "soundcloud-com.png", + "background_color": "#F95300", + "domain": "soundcloud.com" + }, + { + "title": "southwest", + "url": "https://www.southwest.com/", + "image_url": "southwest-com.png", + "background_color": "#3452c1", + "domain": "southwest.com" + }, + { + "title": "spotify", + "url": "https://www.spotify.com/", + "image_url": "spotify-com.png", + "background_color": "#dd08a7", + "domain": "spotify.com" + }, + { + "title": "stackexchange", + "url": "http://stackexchange.com/", + "image_url": "stackexchange-com.png", + "background_color": "#fff", + "domain": "stackexchange.com" + }, + { + "title": "stackoverflow", + "url": "http://stackoverflow.com/", + "image_url": "stackoverflow-com.png", + "background_color": "#f48024", + "domain": "stackoverflow.com" + }, + { + "title": "staples", + "url": "http://www.staples.com/", + "image_url": "staples-com.png", + "background_color": "#F3F1F3", + "domain": "staples.com" + }, + { + "title": "strava", + "url": "http://www.strava.com/", + "image_url": "strava-com.png", + "background_color": "#ff4b00", + "domain": "strava.com" + }, + { + "title": "surveymonkey", + "url": "https://www.surveymonkey.com/", + "image_url": "surveymonkey-com.png", + "background_color": "#a6c32f", + "domain": "surveymonkey.com" + }, + { + "title": "swagbucks", + "url": "http://www.swagbucks.com/", + "image_url": "swagbucks-com.png", + "background_color": "#5fb5d6", + "domain": "swagbucks.com" + }, + { + "title": "talkingpointsmemo", + "url": "http://talkingpointsmemo.com/", + "image_url": "talkingpointsmemo-com.png", + "background_color": "#FFF", + "domain": "talkingpointsmemo.com" + }, + { + "title": "t-mobile", + "url": "http://www.t-mobile.com/", + "image_url": "t-mobile-com.png", + "background_color": "#f32f9d", + "domain": "t-mobile.com" + }, + { + "title": "taboola", + "url": "https://www.taboola.com/", + "image_url": "taboola-com.png", + "background_color": "#1761a8", + "domain": "taboola.com" + }, + { + "title": "taobao", + "url": "https://www.taobao.com/", + "image_url": "taobao-com.png", + "background_color": "#ff8300", + "domain": "taobao.com" + }, + { + "title": "target", + "url": "http://www.target.com", + "image_url": "target-com.png", + "background_color": "#e81530", + "domain": "target.com" + }, + { + "title": "thedailybeast", + "url": "http://www.thedailybeast.com/", + "image_url": "thedailybeast-com.png", + "background_color": "#f12c15", + "domain": "thedailybeast.com" + }, + { + "title": "theguardian", + "url": "http://www.theguardian.com/", + "image_url": "theguardian-com.png", + "background_color": "#005E91", + "domain": "theguardian.com" + }, + { + "title": "thesaurus", + "url": "http://www.thesaurus.com/", + "image_url": "thesaurus-com.png", + "background_color": "#ffce80", + "domain": "thesaurus.com" + }, + { + "title": "ticketmaster", + "url": "http://www.ticketmaster.com/", + "image_url": "ticketmaster-com.png", + "background_color": "#fff", + "domain": "ticketmaster.com" + }, + { + "title": "tripadvisor", + "url": "https://www.tripadvisor.com/", + "image_url": "tripadvisor-com.png", + "background_color": "#5ba443", + "domain": "tripadvisor.com" + }, + { + "title": "trulia", + "url": "http://www.trulia.com/", + "image_url": "trulia-com.png", + "background_color": "#62be06", + "domain": "trulia.com" + }, + { + "title": "tumblr", + "url": "https://www.tumblr.com/", + "image_url": "tumblr-com.png", + "background_color": "#4ebd89", + "domain": "tumblr.com" + }, + { + "title": "twitch", + "url": "https://www.twitch.tv/", + "image_url": "twitch-tv.png", + "background_color": "#5A43A9", + "domain": "twitch.tv" + }, + { + "title": "twitter", + "url": "https://twitter.com/", + "image_url": "twitter-com.png", + "background_color": "#049ff5", + "domain": "twitter.com" + }, + { + "title": "ups", + "url": "https://www.ups.com/", + "image_url": "ups-com.png", + "background_color": "#281704", + "domain": "ups.com" + }, + { + "title": "usaa", + "url": "https://www.usaa.com/", + "image_url": "usaa-com.png", + "background_color": "#002a41", + "domain": "usaa.com" + }, + { + "title": "usatoday", + "url": "http://www.usatoday.com/", + "image_url": "usatoday-com.png", + "background_color": "#000", + "domain": "usatoday.com" + }, + { + "title": "usbank", + "url": "https://www.usbank.com/", + "image_url": "usbank-com.png", + "background_color": "#ff0022", + "domain": "usbank.com" + }, + { + "title": "usps", + "url": "https://www.usps.com/", + "image_url": "usps-com.png", + "background_color": "#f5f5f5", + "domain": "usps.com" + }, + { + "title": "verizon", + "url": "http://www.verizon.com/", + "image_url": "verizon-com.png", + "background_color": "#f00000", + "domain": "verizon.com" + }, + { + "title": "verizonwireless", + "url": "http://www.verizonwireless.com/", + "image_url": "verizonwireless-com.png", + "background_color": "#fff", + "domain": "verizonwireless.com" + }, + { + "title": "vice", + "url": "http://www.vice.com/", + "image_url": "vice-com.png", + "background_color": "#000", + "domain": "vice.com" + }, + { + "title": "vimeo", + "url": "https://vimeo.com/", + "image_url": "vimeo-com.png", + "background_color": "#00b1f2", + "domain": "vimeo.com" + }, + { + "title": "walmart", + "url": "http://www.walmart.com/", + "image_url": "walmart-com.png", + "background_color": "#fff", + "domain": "walmart.com" + }, + { + "title": "washingtonpost", + "url": "https://www.washingtonpost.com/regional/", + "image_url": "washingtonpost-com.png", + "background_color": "#fff", + "domain": "washingtonpost.com" + }, + { + "title": "wayfair", + "url": "http://www.wayfair.com/", + "image_url": "wayfair-com.png", + "background_color": "#ffffff", + "domain": "wayfair.com" + }, + { + "title": "weather", + "url": "https://weather.com/", + "image_url": "weather-com.png", + "background_color": "#2147a8", + "domain": "weather.com" + }, + { + "title": "webmd", + "url": "http://www.webmd.com/default.htm", + "image_url": "webmd-com.png", + "background_color": "#00639a", + "domain": "webmd.com" + }, + { + "title": "wellsfargo", + "url": "https://www.wellsfargo.com", + "image_url": "wellsfargo-com.png", + "background_color": "#ba1613", + "domain": "wellsfargo.com" + }, + { + "title": "wikia", + "url": "http://www.wikia.com/fandom", + "image_url": "wikia-com.png", + "background_color": "#f1f1f1", + "domain": "wikia.com" + }, + { + "title": "wikihow", + "url": "http://www.wikihow.com/", + "image_url": "wikihow-com.png", + "background_color": "#455046", + "domain": "wikihow.com" + }, + { + "title": "wikipedia", + "url": "https://www.wikipedia.org/", + "image_url": "wikipedia-org.png", + "background_color": "#fff", + "domain": "wikipedia.org" + }, + { + "title": "wired", + "url": "https://www.wired.com/", + "image_url": "wired-com.png", + "background_color": "#000", + "domain": "wired.com" + }, + { + "title": "wittyfeed", + "url": "http://www.wittyfeed.com", + "image_url": "wittyfeed-com.png", + "background_color": "#d83633", + "domain": "wittyfeed.com" + }, + { + "title": "wordpress", + "url": "https://wordpress.com", + "image_url": "wordpress-com.png", + "background_color": "#00739c", + "domain": "wordpress.com" + }, + { + "title": "wsj", + "url": "http://www.wsj.com/", + "image_url": "wsj-com.png", + "background_color": "#000000", + "domain": "wsj.com" + }, + { + "title": "wunderground", + "url": "https://www.wunderground.com/", + "image_url": "wunderground-com.png", + "background_color": "#000000", + "domain": "wunderground.com" + }, + { + "title": "yahoo", + "url": "https://www.yahoo.com/", + "image_url": "yahoo-com.png", + "background_color": "#5009a7", + "domain": "yahoo.com" + }, + { + "title": "yelp", + "url": "http://yelp.com/", + "image_url": "yelp-com.png", + "background_color": "#d83633", + "domain": "yelp.com" + }, + { + "title": "youtube", + "url": "https://www.youtube.com/", + "image_url": "youtube-com.png", + "background_color": "#db4338", + "domain": "youtube.com" + }, + { + "title": "zillow", + "url": "http://www.zillow.com/", + "image_url": "zillow-com.png", + "background_color": "#98c554", + "domain": "zillow.com" + } +] diff --git a/mobile/ios/Client/Assets/topdomains.txt b/mobile/ios/Client/Assets/topdomains.txt new file mode 100644 index 0000000000..de1c19f4b3 --- /dev/null +++ b/mobile/ios/Client/Assets/topdomains.txt @@ -0,0 +1,455 @@ +google.com +facebook.com +amazon.com +youtube.com +yahoo.com +ebay.com +wikipedia.org +twitter.com +reddit.com +go.com +craigslist.org +live.com +netflix.com +pinterest.com +bing.com +linkedin.com +imgur.com +espn.go.com +walmart.com +tumblr.com +target.com +paypal.com +cnn.com +chase.com +instagram.com +bestbuy.com +blogspot.com +nytimes.com +msn.com +imdb.com +apple.com +bankofamerica.com +diply.com +huffingtonpost.com +yelp.com +wellsfargo.com +etsy.com +weather.com +wordpress.com +buzzfeed.com +zillow.com +kohls.com +aol.com +homedepot.com +foxnews.com +microsoft.com +comcast.net +wikia.com +groupon.com +macys.com +washingtonpost.com +outbrain.com +microsoftonline.com +xfinity.com +usps.com +hulu.com +americanexpress.com +slickdeals.net +pandora.com +office.com +cnet.com +indeed.com +capitalone.com +nfl.com +ups.com +ask.com +verizonwireless.com +newegg.com +usatoday.com +forbes.com +dailymail.co.uk +dropbox.com +att.com +costco.com +gfycat.com +lowes.com +gap.com +about.com +tripadvisor.com +fedex.com +baidu.com +vice.com +nordstrom.com +adobe.com +bbc.com +twitch.tv +allrecipes.com +retailmenot.com +stackoverflow.com +citi.com +sears.com +jcpenney.com +webmd.com +ijreview.com +nih.gov +answers.com +foodnetwork.com +discovercard.com +cbssports.com +overstock.com +businessinsider.com +office365.com +theguardian.com +staples.com +bleacherreport.com +toysrus.com +verizon.com +github.com +wayfair.com +salesforce.com +zulily.com +wsj.com +flickr.com +goodreads.com +realtor.com +nbcnews.com +ebates.com +ancestry.com +wunderground.com +instructure.com +people.com +stackexchange.com +drudgereport.com +fidelity.com +southwest.com +deviantart.com +thesaurus.com +intuit.com +woot.com +pch.com +soundcloud.com +force.com +samsclub.com +ign.com +qvc.com +npr.org +patch.com +dell.com +accuweather.com +vimeo.com +expedia.com +trulia.com +ca.gov +swagbucks.com +spotify.com +bedbathandbeyond.com +nypost.com +aliexpress.com +blackboard.com +ticketmaster.com +ikea.com +feedly.com +usaa.com +tmz.com +quora.com +lifehacker.com +kayak.com +reference.com +zappos.com +gizmodo.com +slate.com +faithtap.com +adp.com +abcnews.go.com +sephora.com +cbs.com +latimes.com +shutterfly.com +t-mobile.com +littlethings.com +glassdoor.com +bloomberg.com +cbsnews.com +wikihow.com +walgreens.com +usbank.com +blogger.com +weebly.com +gamestop.com +food.com +time.com +kickstarter.com +okcupid.com +aa.com +weather.gov +nametests.com +fandango.com +engadget.com +steamcommunity.com +thekitchn.com +nba.com +mashable.com +hp.com +gamefaqs.com +delta.com +breitbart.com +coupons.com +eonline.com +surveymonkey.com +kmart.com +barnesandnoble.com +meetup.com +bhphotovideo.com +fanduel.com +quizlet.com +nydailynews.com +sbnation.com +nbcsports.com +likes.com +bbc.co.uk +ew.com +nike.com +rottentomatoes.com +steampowered.com +reuters.com +qq.com +today.com +mapquest.com +audible.com +priceline.com +whitepages.com +united.com +myfitnesspal.com +icloud.com +forever21.com +theatlantic.com +microsoftstore.com +theverge.com +gawker.com +houzz.com +mayoclinic.org +rei.com +sfgate.com +lifebuzz.com +discover.com +pnc.com +pof.com +iflscience.com +popsugar.com +creditkarma.com +telegraph.co.uk +airbnb.com +buzzlie.com +cnbc.com +deadspin.com +sina.com.cn +legacy.com +thedailybeast.com +samsung.com +nextdoor.com +evite.com +shopify.com +yellowpages.com +pcmag.com +redfin.com +emgn.com +weibo.com +alibaba.com +cabelas.com +battle.net +foxsports.com +taobao.com +eventbrite.com +victoriassecret.com +theblaze.com +dealnews.com +cbslocal.com +cvs.com +dailymotion.com +ecollege.com +gofundme.com +fitbit.com +instructables.com +godaddy.com +babycenter.com +squarespace.com +llbean.com +dickssportinggoods.com +6pm.com +myway.com +hsn.com +wired.com +officedepot.com +ozztube.com +usmagazine.com +match.com +cracked.com +evernote.com +box.com +starbucks.com +kbb.com +mlb.com +marriott.com +si.com +jezebel.com +pbs.org +consumerreports.org +roblox.com +urbandictionary.com +kotaku.com +xbox.com +marketwatch.com +refinery29.com +wikimedia.org +tvguide.com +politico.com +barclaycardus.com +abc.go.com +mint.com +topix.com +theblackfriday.com +aarp.org +hotnewhiphop.com +yourdailydish.com +sprint.com +vox.com +cafemom.com +nbc.com +dailykos.com +azlyrics.com +autotrader.com +hilton.com +irs.gov +monster.com +fatwallet.com +mailchimp.com +webex.com +landsend.com +wix.com +usnews.com +jcrew.com +jet.com +capitalone360.com +sharepoint.com +schwab.com +ulta.com +vistaprint.com +rollingstone.com +biblegateway.com +gamespot.com +io9.com +opentable.com +hm.com +duckduckgo.com +chron.com +photobucket.com +shareasale.com +directv.com +avg.com +oracle.com +hotels.com +timewarnercable.com +chicagotribune.com +ehow.com +primewire.ag +abs-cbnnews.com +salon.com +greatergood.com +epicurious.com +fool.com +patheos.com +custhelp.com +purdue.edu +tickld.com +frys.com +indiatimes.com +amazon.co.uk +zendesk.com +tigerdirect.com +stubhub.com +healthcare.gov +archive.org +qualtrics.com +ravelry.com +cars.com +redbox.com +jalopnik.com +speedtest.net +harvard.edu +slideshare.net +kinja.com +nesn.com +michaels.com +mit.edu +bodybuilding.com +edmunds.com +nhl.com +zergnet.com +terraclicks.com +techcrunch.com +regnok.com +pogo.com +backpage.com +mozilla.org +naver.com +giphy.com +bankrate.com +msnbc.com +digitaltrends.com +fanfiction.net +skype.com +disney.go.com +norton.com +androidcentral.com +tomshardware.com +thefreedictionary.com +liveleak.com +247sports.com +merriam-webster.com +wnd.com +earthlink.net +conservativetribune.com +independent.co.uk +drugs.com +rotoworld.com +nationalgeographic.com +ae.com +noaa.gov +arstechnica.com +thinkgeek.com +stanford.edu +bizjournals.com +hootsuite.com +genius.com +goodhousekeeping.com +vanguard.com +ny.gov +citibankonline.com +booking.com +mic.com +orbitz.com +dominos.com +medium.com +wow.com +urbanoutfitters.com +douban.com +timeanddate.com +draftkings.com +livestrong.com +livingsocial.com +cox.net +theonion.com +marthastewart.com +comenity.net +worldlifestyle.com +disney.com +realsimple.com +vrbo.com +playstation.com +potterybarn.com +zazzle.com +ksl.com +tdbank.com +sourceforge.net +careerbuilder.com diff --git a/mobile/ios/Client/Configuration/Fennec.enterprise.xcconfig b/mobile/ios/Client/Configuration/Fennec.enterprise.xcconfig new file mode 100644 index 0000000000..dcf262bfa3 --- /dev/null +++ b/mobile/ios/Client/Configuration/Fennec.enterprise.xcconfig @@ -0,0 +1,20 @@ +// 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/. + +MOZ_BUNDLE_DISPLAY_NAME = Fennec + +// Bundle Identifier +MOZ_BUNDLE_ID = org.mozilla.ios.FennecEnterprise + +// Flag to indicate if we want to include the debug settings bundle or not +INCLUDE_SETTINGS_BUNDLE = YES + +// Adjust SDK Variables +ADJUST_ENVIRONMENT = +ADJUST_APP_TOKEN = + +// Leanplum SDK Variables +LEANPLUM_ENVIRONMENT = +LEANPLUM_APP_ID = +LEANPLUM_KEY = diff --git a/mobile/ios/Client/Configuration/Fennec.xcconfig b/mobile/ios/Client/Configuration/Fennec.xcconfig new file mode 100644 index 0000000000..5cfd3171b9 --- /dev/null +++ b/mobile/ios/Client/Configuration/Fennec.xcconfig @@ -0,0 +1,20 @@ +// 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/. + +MOZ_BUNDLE_DISPLAY_NAME = Fennec ($(USER)) + +// Bundle Identifier +MOZ_BUNDLE_ID = org.mozilla.ios.Fennec + +// Flag to indicate if we want to include the debug settings bundle or not +INCLUDE_SETTINGS_BUNDLE = YES + +// Adjust SDK Variables +ADJUST_ENVIRONMENT = +ADJUST_APP_TOKEN = + +// Leanplum SDK Variables +LEANPLUM_ENVIRONMENT = development +LEANPLUM_APP_ID = +LEANPLUM_KEY = diff --git a/mobile/ios/Client/Configuration/Firefox.xcconfig b/mobile/ios/Client/Configuration/Firefox.xcconfig new file mode 100644 index 0000000000..8fcd7900e1 --- /dev/null +++ b/mobile/ios/Client/Configuration/Firefox.xcconfig @@ -0,0 +1,20 @@ +// 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/. + +MOZ_BUNDLE_DISPLAY_NAME = Firefox + +// Bundle Identifier +MOZ_BUNDLE_ID = org.mozilla.ios.Firefox + +// Flag to indicate if we want to include the debug settings bundle or not +INCLUDE_SETTINGS_BUNDLE = NO + +// Adjust SDK Variables +ADJUST_ENVIRONMENT = +ADJUST_APP_TOKEN = + +// Leanplum SDK Variables +LEANPLUM_ENVIRONMENT = +LEANPLUM_APP_ID = +LEANPLUM_KEY = diff --git a/mobile/ios/Client/Configuration/FirefoxBeta.xcconfig b/mobile/ios/Client/Configuration/FirefoxBeta.xcconfig new file mode 100644 index 0000000000..6911d23c45 --- /dev/null +++ b/mobile/ios/Client/Configuration/FirefoxBeta.xcconfig @@ -0,0 +1,20 @@ +// 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/. + +MOZ_BUNDLE_DISPLAY_NAME = Firefox Beta + +// Bundle Identifier +MOZ_BUNDLE_ID = org.mozilla.ios.FirefoxBeta + +// Flag to indicate if we want to include the debug settings bundle or not +INCLUDE_SETTINGS_BUNDLE = NO + +// Adjust SDK Variables +ADJUST_ENVIRONMENT = +ADJUST_APP_TOKEN = + +// Leanplum SDK Variables +LEANPLUM_ENVIRONMENT = +LEANPLUM_APP_ID = +LEANPLUM_KEY = diff --git a/mobile/ios/Client/DispatchQueueExtensions.swift b/mobile/ios/Client/DispatchQueueExtensions.swift new file mode 100644 index 0000000000..9f1b73a6a1 --- /dev/null +++ b/mobile/ios/Client/DispatchQueueExtensions.swift @@ -0,0 +1,18 @@ +/* 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 + + +// Only push the task async if we are not already on the main thread. +// Unless you want another event to fire before your work happens. This is better than using DispatchQueue.main.async to ensure main thread +public func ensureMainThread(execute work: @escaping @convention(block) () -> Swift.Void) { + if Thread.isMainThread { + work() + } else { + DispatchQueue.main.async { + work() + } + } +} diff --git a/mobile/ios/Client/Entitlements/FennecApplication.entitlements b/mobile/ios/Client/Entitlements/FennecApplication.entitlements new file mode 100644 index 0000000000..6d3ba2781e --- /dev/null +++ b/mobile/ios/Client/Entitlements/FennecApplication.entitlements @@ -0,0 +1,25 @@ + + + + + aps-environment + development + com.apple.developer.associated-domains + + applinks:accounts.firefox.com + applinks:accounts.stage.mozaws.net + applinks:latest.dev.lcip.org + applinks:nightly.dev.lcip.org + applinks:rzte.adj.st + applinks:nn8g.adj.st + + com.apple.security.application-groups + + group.org.mozilla.ios.Fennec + + keychain-access-groups + + $(AppIdentifierPrefix)org.mozilla.ios.Fennec + + + diff --git a/mobile/ios/Client/Entitlements/FennecEnterpriseApplication.entitlements b/mobile/ios/Client/Entitlements/FennecEnterpriseApplication.entitlements new file mode 100644 index 0000000000..1d9670e369 --- /dev/null +++ b/mobile/ios/Client/Entitlements/FennecEnterpriseApplication.entitlements @@ -0,0 +1,16 @@ + + + + + aps-environment + production + com.apple.security.application-groups + + group.org.mozilla.ios.Fennec.enterprise + + keychain-access-groups + + $(AppIdentifierPrefix)org.mozilla.ios.Fennec.enterprise + + + diff --git a/mobile/ios/Client/Entitlements/FirefoxApplication.entitlements b/mobile/ios/Client/Entitlements/FirefoxApplication.entitlements new file mode 100644 index 0000000000..1f7a3e1ca6 --- /dev/null +++ b/mobile/ios/Client/Entitlements/FirefoxApplication.entitlements @@ -0,0 +1,25 @@ + + + + + com.apple.developer.associated-domains + + applinks:accounts.firefox.com + applinks:accounts.stage.mozaws.net + applinks:latest.dev.lcip.org + applinks:nightly.dev.lcip.org + applinks:rzte.adj.st + applinks:nn8g.adj.st + + aps-environment + production + com.apple.security.application-groups + + group.org.mozilla.ios.Firefox + + keychain-access-groups + + $(AppIdentifierPrefix)org.mozilla.ios.Firefox + + + diff --git a/mobile/ios/Client/Entitlements/FirefoxBetaApplication.entitlements b/mobile/ios/Client/Entitlements/FirefoxBetaApplication.entitlements new file mode 100644 index 0000000000..a9055bfd0d --- /dev/null +++ b/mobile/ios/Client/Entitlements/FirefoxBetaApplication.entitlements @@ -0,0 +1,25 @@ + + + + + com.apple.developer.associated-domains + + applinks:accounts.firefox.com + applinks:accounts.stage.mozaws.net + applinks:latest.dev.lcip.org + applinks:nightly.dev.lcip.org + applinks:rzte.adj.st + applinks:nn8g.adj.st + + aps-environment + production + com.apple.security.application-groups + + group.org.mozilla.ios.FirefoxBeta + + keychain-access-groups + + $(AppIdentifierPrefix)org.mozilla.ios.FirefoxBeta + + + diff --git a/mobile/ios/Client/Extensions/GeometryExtensions.swift b/mobile/ios/Client/Extensions/GeometryExtensions.swift new file mode 100644 index 0000000000..85ba16d50b --- /dev/null +++ b/mobile/ios/Client/Extensions/GeometryExtensions.swift @@ -0,0 +1,42 @@ +/* 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 UIKit + +extension CGRect { + var center: CGPoint { + get { + return CGPoint(x: size.width / 2, y: size.height / 2) + } + set { + self.origin = CGPoint(x: newValue.x - size.width / 2, y: newValue.y - size.height / 2) + } + } +} + +extension UIEdgeInsets { + init(equalInset inset: CGFloat) { + top = inset + left = inset + right = inset + bottom = inset + } +} + +/** +Generates the affine transform for transforming the first CGRect into the second one + +- parameter frame: CGRect to transform from +- parameter toFrame: CGRect to transform to + +- returns: CGAffineTransform that transforms the first CGRect into the second +*/ +func CGAffineTransformMakeRectToRect(_ frame: CGRect, toFrame: CGRect) -> CGAffineTransform { + let scale = toFrame.size.width / frame.size.width + let tx = toFrame.origin.x + toFrame.width / 2 - (frame.origin.x + frame.width / 2) + let ty = toFrame.origin.y - frame.origin.y * scale * 2 + let translation = CGAffineTransform(translationX: tx, y: ty) + let scaledAndTranslated = translation.scaledBy(x: scale, y: scale) + return scaledAndTranslated +} diff --git a/mobile/ios/Client/Extensions/NSAttributedStringExtensions.swift b/mobile/ios/Client/Extensions/NSAttributedStringExtensions.swift new file mode 100644 index 0000000000..ae9eccb0b7 --- /dev/null +++ b/mobile/ios/Client/Extensions/NSAttributedStringExtensions.swift @@ -0,0 +1,13 @@ +/* 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 + +// MARK: - Common UITableView text styling +extension NSAttributedString { + static func tableRowTitle(_ string: String, enabled: Bool) -> NSAttributedString { + let color = enabled ? [NSForegroundColorAttributeName: UIConstants.TableViewRowTextColor] : [NSForegroundColorAttributeName: UIConstants.TableViewDisabledRowTextColor] + return NSAttributedString(string: string, attributes: color) + } +} diff --git a/mobile/ios/Client/Extensions/NSURLExtensionsMailTo.swift b/mobile/ios/Client/Extensions/NSURLExtensionsMailTo.swift new file mode 100644 index 0000000000..2e36998fa9 --- /dev/null +++ b/mobile/ios/Client/Extensions/NSURLExtensionsMailTo.swift @@ -0,0 +1,55 @@ +/* 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 + +/** + * Data structure containing metadata associated with a mailto: link. For additional details, + * see RFC 2368 https://tools.ietf.org/html/rfc2368 + */ +public struct MailToMetadata { + public let to: String + public let headers: [String: String] +} + +public extension URL { + + /** + Extracts the metadata associated with a mailto: URL according to RFC 2368 + https://tools.ietf.org/html/rfc2368 + */ + func mailToMetadata() -> MailToMetadata? { + guard scheme == "mailto" else { + return nil + } + let urlString = absoluteString + + // Extract 'to' value + let toStart = urlString.characters.index(urlString.startIndex, offsetBy: "mailto:".characters.count) + let toEnd = urlString.characters.index(of: "?") ?? urlString.endIndex + + let to = urlString.substring(with: toStart.. UIAlertController { + let alert = UIAlertController( + title: NSLocalizedString("Well, this is embarrassing.", comment: "Restore Tabs Prompt Title"), + message: NSLocalizedString("Looks like Firefox crashed previously. Would you like to restore your tabs?", comment: "Restore Tabs Prompt Description"), + preferredStyle: UIAlertControllerStyle.alert + ) + + let noOption = UIAlertAction( + title: NSLocalizedString("No", comment: "Restore Tabs Negative Action"), + style: UIAlertActionStyle.cancel, + handler: noCallback + ) + + let okayOption = UIAlertAction( + title: NSLocalizedString("Okay", comment: "Restore Tabs Affirmative Action"), + style: UIAlertActionStyle.default, + handler: okayCallback + ) + + alert.addAction(okayOption) + alert.addAction(noOption) + return alert + } + + class func clearPrivateDataAlert(okayCallback: @escaping (UIAlertAction) -> Void) -> UIAlertController { + let alert = UIAlertController( + title: "", + message: NSLocalizedString("This action will clear all of your private data. It cannot be undone.", tableName: "ClearPrivateDataConfirm", comment: "Description of the confirmation dialog shown when a user tries to clear their private data."), + preferredStyle: UIAlertControllerStyle.alert + ) + + let noOption = UIAlertAction( + title: NSLocalizedString("Cancel", tableName: "ClearPrivateDataConfirm", comment: "The cancel button when confirming clear private data."), + style: UIAlertActionStyle.cancel, + handler: nil + ) + + let okayOption = UIAlertAction( + title: NSLocalizedString("OK", tableName: "ClearPrivateDataConfirm", comment: "The button that clears private data."), + style: UIAlertActionStyle.destructive, + handler: okayCallback + ) + + alert.addAction(okayOption) + alert.addAction(noOption) + return alert + } + + /** + Builds the Alert view that asks if the users wants to also delete history stored on their other devices. + + - parameter okayCallback: Okay option handler. + + - returns: UIAlertController for asking the user to restore tabs after a crash + */ + + class func clearSyncedHistoryAlert(okayCallback: @escaping (UIAlertAction) -> Void) -> UIAlertController { + let alert = UIAlertController( + title: "", + message: NSLocalizedString("This action will clear all of your private data, including history from your synced devices.", tableName: "ClearHistoryConfirm", comment: "Description of the confirmation dialog shown when a user tries to clear history that's synced to another device."), + preferredStyle: UIAlertControllerStyle.alert + ) + + let noOption = UIAlertAction( + title: NSLocalizedString("Cancel", tableName: "ClearHistoryConfirm", comment: "The cancel button when confirming clear history."), + style: UIAlertActionStyle.cancel, + handler: nil + ) + + let okayOption = UIAlertAction( + title: NSLocalizedString("OK", tableName: "ClearHistoryConfirm", comment: "The confirmation button that clears history even when Sync is connected."), + style: UIAlertActionStyle.destructive, + handler: okayCallback + ) + + alert.addAction(okayOption) + alert.addAction(noOption) + return alert + } + + /** + Creates an alert view to warn the user that their logins will either be completely deleted in the + case of local-only logins or deleted across synced devices in synced account logins. + + - parameter deleteCallback: Block to run when delete is tapped. + - parameter hasSyncedLogins: Boolean indicating the user has logins that have been synced. + + - returns: UIAlertController instance + */ + class func deleteLoginAlertWithDeleteCallback( + _ deleteCallback: @escaping UIAlertActionCallback, + hasSyncedLogins: Bool) -> UIAlertController { + + let areYouSureTitle = NSLocalizedString("Are you sure?", + tableName: "LoginManager", + comment: "Prompt title when deleting logins") + let deleteLocalMessage = NSLocalizedString("Logins will be permanently removed.", + tableName: "LoginManager", + comment: "Prompt message warning the user that deleting non-synced logins will permanently remove them") + let deleteSyncedDevicesMessage = NSLocalizedString("Logins will be removed from all connected devices.", + tableName: "LoginManager", + comment: "Prompt message warning the user that deleted logins will remove logins from all connected devices") + let cancelActionTitle = NSLocalizedString("Cancel", + tableName: "LoginManager", + comment: "Prompt option for cancelling out of deletion") + let deleteActionTitle = NSLocalizedString("Delete", + tableName: "LoginManager", + comment: "Label for the button used to delete the current login.") + + let deleteAlert: UIAlertController + if hasSyncedLogins { + deleteAlert = UIAlertController(title: areYouSureTitle, message: deleteSyncedDevicesMessage, preferredStyle: .alert) + } else { + deleteAlert = UIAlertController(title: areYouSureTitle, message: deleteLocalMessage, preferredStyle: .alert) + } + + let cancelAction = UIAlertAction(title: cancelActionTitle, style: .cancel, handler: nil) + let deleteAction = UIAlertAction(title: deleteActionTitle, style: .destructive, handler: deleteCallback) + + deleteAlert.addAction(cancelAction) + deleteAlert.addAction(deleteAction) + + return deleteAlert + } +} diff --git a/mobile/ios/Client/Extensions/UIImageViewExtensions.swift b/mobile/ios/Client/Extensions/UIImageViewExtensions.swift new file mode 100644 index 0000000000..68c8d5759d --- /dev/null +++ b/mobile/ios/Client/Extensions/UIImageViewExtensions.swift @@ -0,0 +1,87 @@ +/* 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 UIKit +import Storage +import SDWebImage +import Shared + +public extension UIImageView { + + public func setIcon(_ icon: Favicon?, forURL url: URL?, completed completionBlock: ((UIColor, URL?) -> Void)? = nil ) { + if let url = url, let defaultIcon = FaviconFetcher.getDefaultIconForURL(url: url) { + self.image = UIImage(contentsOfFile: defaultIcon.url) + self.backgroundColor = defaultIcon.color + completionBlock?(defaultIcon.color, url) + } else { + let imageURL = URL(string: icon?.url ?? "") + let defaults = defaultFavicon(url) + self.sd_setImage(with: imageURL, placeholderImage: defaults.image, options: []) {(img, err, _, _) in + guard let image = img, let dUrl = url, err == nil else { + self.backgroundColor = defaults.color + completionBlock?(defaults.color, url) + return + } + self.color(forImage: image, andURL: dUrl, completed: completionBlock) + } + } + } + + /* + * Fetch a background color for a specfic favicon UIImage. It uses the URL to store the UIColor in memory for subsequent requests. + */ + private func color(forImage image: UIImage, andURL url: URL, completed completionBlock: ((UIColor, URL?) -> Void)? = nil) { + guard let domain = url.baseDomain else { + self.backgroundColor = .gray + completionBlock?(UIColor.gray, url) + return + } + + if let color = FaviconFetcher.colors[domain] { + self.backgroundColor = color + completionBlock?(color, url) + } else { + image.getColors(scaleDownSize: CGSize(width: 25, height: 25)) {colors in + let isSame = [colors.primary, colors.secondary, colors.detail].every { $0 == colors.primary } + if isSame { + completionBlock?(UIColor.white, url) + FaviconFetcher.colors[domain] = UIColor.white + } else { + completionBlock?(colors.background, url) + FaviconFetcher.colors[domain] = colors.background + } + } + } + } + + public func setFavicon(forSite site: Site, onCompletion completionBlock: ((UIColor, URL?) -> Void)? = nil ) { + self.setIcon(site.icon, forURL: site.tileURL, completed: completionBlock) + } + + private func defaultFavicon(_ url: URL?) -> (image: UIImage, color: UIColor) { + if let url = url { + return (FaviconFetcher.getDefaultFavicon(url), FaviconFetcher.getDefaultColor(url)) + } else { + return (FaviconFetcher.defaultFavicon, .white) + } + } +} + +open class ImageOperation: NSObject, SDWebImageOperation { + open var cacheOperation: Operation? + + var cancelled: Bool { + if let cacheOperation = cacheOperation { + return cacheOperation.isCancelled + } + return false + } + + @objc open func cancel() { + if let cacheOperation = cacheOperation { + cacheOperation.cancel() + } + } +} + diff --git a/mobile/ios/Client/Extensions/UIPasteboardExtensions.swift b/mobile/ios/Client/Extensions/UIPasteboardExtensions.swift new file mode 100644 index 0000000000..0cc815124c --- /dev/null +++ b/mobile/ios/Client/Extensions/UIPasteboardExtensions.swift @@ -0,0 +1,63 @@ +/* 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 Deferred +import Foundation +import MobileCoreServices +import Shared +import UIKit + +extension UIPasteboard { + func addImageWithData(_ data: Data, forURL url: URL) { + let isGIF = UIImage.dataIsGIF(data) + + // Setting pasteboard.items allows us to set multiple representations for the same item. + items = [[ + kUTTypeURL as String: url, + imageTypeKey(isGIF): data + ]] + } + + fileprivate func imageTypeKey(_ isGIF: Bool) -> String { + return (isGIF ? kUTTypeGIF : kUTTypePNG) as String + } + + private var syncURL: URL? { + if let string = UIPasteboard.general.string, + let url = URL(string: string), url.isWebPage() { + return url + } else { + return nil + } + } + + /// Preferred method to get strings out of the clipboard. + /// When iCloud pasteboards are enabled, the usually fast, synchronous calls + /// become slow and synchronous causing very slow start up times. + func asyncString() -> Deferred> { + return fetchAsync() { + return UIPasteboard.general.string + } + } + + /// Preferred method to get URLs out of the clipboard. + /// We use Deferred> to fit in to the rest of the Deferred tools + /// we already use; but use optionals instead of errorTypes, because not having a URL + /// on the clipboard isn't an error. + func asyncURL() -> Deferred> { + return fetchAsync() { + return self.syncURL + } + } + + // Converts the potentially long running synchronous operation into an asynchronous one. + private func fetchAsync(getter: @escaping () -> T) -> Deferred> { + let deferred = Deferred>() + DispatchQueue.global().async { + let value = getter() + deferred.fill(Maybe(success: value)) + } + return deferred + } +} diff --git a/mobile/ios/Client/Extensions/UIViewExtensions.swift b/mobile/ios/Client/Extensions/UIViewExtensions.swift new file mode 100644 index 0000000000..a7b87ed526 --- /dev/null +++ b/mobile/ios/Client/Extensions/UIViewExtensions.swift @@ -0,0 +1,94 @@ +/* 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 + +extension UIView { + /** + * Takes a screenshot of the view with the given size. + */ + func screenshot(_ size: CGSize, offset: CGPoint? = nil, quality: CGFloat = 1) -> UIImage? { + assert(0...1 ~= quality) + + let offset = offset ?? CGPoint(x: 0, y: 0) + + UIGraphicsBeginImageContextWithOptions(size, false, UIScreen.main.scale * quality) + drawHierarchy(in: CGRect(origin: offset, size: frame.size), afterScreenUpdates: false) + let image = UIGraphicsGetImageFromCurrentImageContext() + UIGraphicsEndImageContext() + + return image + } + + /** + * Takes a screenshot of the view with the given aspect ratio. + * An aspect ratio of 0 means capture the entire view. + */ + func screenshot(_ aspectRatio: CGFloat = 0, offset: CGPoint? = nil, quality: CGFloat = 1) -> UIImage? { + assert(aspectRatio >= 0) + + var size: CGSize + if aspectRatio > 0 { + size = CGSize() + let viewAspectRatio = frame.width / frame.height + if viewAspectRatio > aspectRatio { + size.height = frame.height + size.width = size.height * aspectRatio + } else { + size.width = frame.width + size.height = size.width / aspectRatio + } + } else { + size = frame.size + } + + return screenshot(size, offset: offset, quality: quality) + } + + /* + * Performs a deep copy of the view. Does not copy constraints. + */ + func clone() -> UIView { + let data = NSKeyedArchiver.archivedData(withRootObject: self) + return NSKeyedUnarchiver.unarchiveObject(with: data) as! UIView + } + + /** + * rounds the requested corners of a view with the provided radius + */ + func addRoundedCorners(_ cornersToRound: UIRectCorner, cornerRadius: CGSize, color: UIColor) { + let rect = bounds + let maskPath = UIBezierPath(roundedRect: rect, byRoundingCorners: cornersToRound, cornerRadii: cornerRadius) + + // Create the shape layer and set its path + let maskLayer = CAShapeLayer() + maskLayer.frame = rect + maskLayer.path = maskPath.cgPath + + let roundedLayer = CALayer() + roundedLayer.backgroundColor = color.cgColor + roundedLayer.frame = rect + roundedLayer.mask = maskLayer + + layer.insertSublayer(roundedLayer, at: 0) + backgroundColor = UIColor.clear + } + + /** + This allows us to find the view in a current view hierarchy that is currently the first responder + */ + static func findSubViewWithFirstResponder(_ view: UIView) -> UIView? { + let subviews = view.subviews + if subviews.count == 0 { + return nil + } + for subview: UIView in subviews { + if subview.isFirstResponder { + return subview + } + return findSubViewWithFirstResponder(subview) + } + return nil + } +} diff --git a/mobile/ios/Client/Frontend/Accessors/HomePageAccessors.swift b/mobile/ios/Client/Frontend/Accessors/HomePageAccessors.swift new file mode 100644 index 0000000000..23d8227701 --- /dev/null +++ b/mobile/ios/Client/Frontend/Accessors/HomePageAccessors.swift @@ -0,0 +1,25 @@ +/* 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 XCGLogger + +/// Accessors for homepage details from the app state. +/// These are pure functions, so it's quite ok to have them +/// as static. +class HomePageAccessors { + + static func getHomePage(_ prefs: Prefs) -> URL? { + let string = prefs.stringForKey(HomePageConstants.HomePageURLPrefKey) ?? getDefaultHomePageString(prefs) + guard let urlString = string else { + return nil + } + return NSURL(string: urlString) as URL? + } + + static func getDefaultHomePageString(_ prefs: Prefs) -> String? { + return prefs.stringForKey(HomePageConstants.DefaultHomePageURLPrefKey) + } +} diff --git a/mobile/ios/Client/Frontend/Accessors/NewTabAccessors.swift b/mobile/ios/Client/Frontend/Accessors/NewTabAccessors.swift new file mode 100644 index 0000000000..42912713f0 --- /dev/null +++ b/mobile/ios/Client/Frontend/Accessors/NewTabAccessors.swift @@ -0,0 +1,77 @@ +/* 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 XCGLogger + +/// Accessors to find what a new tab should do when created without a URL. +struct NewTabAccessors { + static let PrefKey = PrefsKeys.KeyNewTab + static let Default = NewTabPage.topSites + + static func getNewTabPage(_ prefs: Prefs) -> NewTabPage { + guard let raw = prefs.stringForKey(PrefKey) else { + return Default + } + let option = NewTabPage(rawValue: raw) ?? Default + // Check if the user has chosen to open a homepage, but no homepage is set, + // then use the default. + if option == .homePage && HomePageAccessors.getHomePage(prefs) == nil { + return Default + } + return option + } +} + +/// Enum to encode what should happen when the user opens a new tab without a URL. +enum NewTabPage: String { + case blankPage = "Blank" + case homePage = "HomePage" + case topSites = "TopSites" + case bookmarks = "Bookmarks" + case history = "History" + case readingList = "ReadingList" + + var settingTitle: String { + switch self { + case .blankPage: + return Strings.SettingsNewTabBlankPage + case .homePage: + return Strings.SettingsNewTabHomePage + case .topSites: + return Strings.SettingsNewTabTopSites + case .bookmarks: + return Strings.SettingsNewTabBookmarks + case .history: + return Strings.SettingsNewTabHistory + case .readingList: + return Strings.SettingsNewTabReadingList + } + } + + var homePanelType: HomePanelType? { + switch self { + case .topSites: + return HomePanelType.topSites + case .bookmarks: + return HomePanelType.bookmarks + case .history: + return HomePanelType.history + case .readingList: + return HomePanelType.readingList + default: + return nil + } + } + + var url: URL? { + guard let homePanel = self.homePanelType else { + return nil + } + return homePanel.localhostURL as URL + } + + static let allValues = [blankPage, topSites, bookmarks, history, readingList, homePage] +} diff --git a/mobile/ios/Client/Frontend/Animators/Animatable.swift b/mobile/ios/Client/Frontend/Animators/Animatable.swift new file mode 100644 index 0000000000..642c35ac54 --- /dev/null +++ b/mobile/ios/Client/Frontend/Animators/Animatable.swift @@ -0,0 +1,9 @@ +/* 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 UIKit + +protocol Animatable { + func animateFromView(_ view: UIView, offset: CGFloat?, completion: ((Bool) -> Void)?) +} diff --git a/mobile/ios/Client/Frontend/Animators/JumpAndSpinAnimator.swift b/mobile/ios/Client/Frontend/Animators/JumpAndSpinAnimator.swift new file mode 100644 index 0000000000..3026dcd010 --- /dev/null +++ b/mobile/ios/Client/Frontend/Animators/JumpAndSpinAnimator.swift @@ -0,0 +1,35 @@ +/* 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 UIKit + +class JumpAndSpinAnimator: Animatable { + fileprivate let AnimationDuration: Double = 0.5 + fileprivate let AnimationOffset: CGFloat = -80 + + static func animateFromView(_ view: UIView, offset: CGFloat, completion: ((Bool) -> Void)?) { + let animator = JumpAndSpinAnimator() + animator.animateFromView(view, offset: offset, completion: completion) + } + + func animateFromView(_ viewToAnimate: UIView, offset: CGFloat? = nil, completion: ((Bool) -> Void)?) { + let offset = offset ?? AnimationOffset + let offToolbar = CGAffineTransform(translationX: 0, y: offset) + + UIView.animate(withDuration: AnimationDuration, delay: 0.0, usingSpringWithDamping: 0.6, initialSpringVelocity: 2.0, options: [], animations: { () -> Void in + viewToAnimate.transform = offToolbar + let rotation = CABasicAnimation(keyPath: "transform.rotation") + rotation.toValue = CGFloat(2.0 * Double.pi) + rotation.isCumulative = true + rotation.duration = self.AnimationDuration + 0.075 + rotation.repeatCount = 1.0 + rotation.timingFunction = CAMediaTimingFunction(controlPoints: 0.32, 0.70, 0.18, 1.00) + viewToAnimate.layer.add(rotation, forKey: "rotateStar") + }, completion: { finished in + UIView.animate(withDuration: self.AnimationDuration, delay: 0.15, usingSpringWithDamping: 0.7, initialSpringVelocity: 0, options: [], animations: { () -> Void in + viewToAnimate.transform = CGAffineTransform.identity + }, completion: completion) + }) + } +} diff --git a/mobile/ios/Client/Frontend/AuthenticationManager/AppAuthenticator.swift b/mobile/ios/Client/Frontend/AuthenticationManager/AppAuthenticator.swift new file mode 100644 index 0000000000..ac4ea06684 --- /dev/null +++ b/mobile/ios/Client/Frontend/AuthenticationManager/AppAuthenticator.swift @@ -0,0 +1,55 @@ +/* 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 SwiftKeychainWrapper +import LocalAuthentication + +class AppAuthenticator { + static func presentAuthenticationUsingInfo(_ authenticationInfo: AuthenticationKeychainInfo, touchIDReason: String, success: (() -> Void)?, cancel: (() -> Void)?, fallback: (() -> Void)?) { + if authenticationInfo.useTouchID { + let localAuthContext = LAContext() + localAuthContext.localizedFallbackTitle = AuthenticationStrings.enterPasscode + localAuthContext.evaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, localizedReason: touchIDReason) { didSucceed, error in + if didSucceed { + // Update our authentication info's last validation timestamp so we don't ask again based + // on the set required interval + authenticationInfo.recordValidation() + KeychainWrapper.sharedAppContainerKeychain.setAuthenticationInfo(authenticationInfo) + DispatchQueue.main.async { + success?() + } + return + } + + guard let authError = error, + let code = LAError.Code(rawValue: authError._code) else { + return + } + + DispatchQueue.main.async { + switch code { + case .userFallback, .touchIDNotEnrolled, .touchIDNotAvailable, .touchIDLockout: + fallback?() + case .userCancel: + cancel?() + default: + cancel?() + } + } + } + } else { + fallback?() + } + } + + static func presentPasscodeAuthentication(_ presentingNavController: UINavigationController?, delegate: PasscodeEntryDelegate?) { + let passcodeVC = PasscodeEntryViewController() + passcodeVC.delegate = delegate + let navController = UINavigationController(rootViewController: passcodeVC) + navController.modalPresentationStyle = .formSheet + presentingNavController?.present(navController, animated: true, completion: nil) + } +} diff --git a/mobile/ios/Client/Frontend/AuthenticationManager/AuthenticationManagerConstants.swift b/mobile/ios/Client/Frontend/AuthenticationManager/AuthenticationManagerConstants.swift new file mode 100644 index 0000000000..1fd79ece2a --- /dev/null +++ b/mobile/ios/Client/Frontend/AuthenticationManager/AuthenticationManagerConstants.swift @@ -0,0 +1,107 @@ +/* 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 + +// Strings for the passcode intervals. +extension PasscodeInterval { + var settingTitle: String { + switch self { + case .immediately: return AuthenticationStrings.immediately + case .oneMinute: return AuthenticationStrings.oneMinute + case .fiveMinutes: return AuthenticationStrings.fiveMinutes + case .tenMinutes: return AuthenticationStrings.tenMinutes + case .fifteenMinutes: return AuthenticationStrings.fifteenMinutes + case .oneHour: return AuthenticationStrings.oneHour + } + } +} + +// Strings used in multiple areas within the Authentication Manager +struct AuthenticationStrings { + static let passcode = + NSLocalizedString("Passcode For Logins", tableName: "AuthenticationManager", comment: "Label for the Passcode item in Settings") + + static let touchIDPasscodeSetting = + NSLocalizedString("Touch ID & Passcode", tableName: "AuthenticationManager", comment: "Label for the Touch ID/Passcode item in Settings") + + static let faceIDPasscodeSetting = + NSLocalizedString("Face ID & Passcode", tableName: "AuthenticationManager", comment: "Label for the Face ID/Passcode item in Settings") + + static let requirePasscode = + NSLocalizedString("Require Passcode", tableName: "AuthenticationManager", comment: "Text displayed in the 'Interval' section, followed by the current interval setting, e.g. 'Immediately'") + + static let enterAPasscode = + NSLocalizedString("Enter a passcode", tableName: "AuthenticationManager", comment: "Text displayed above the input field when entering a new passcode") + + static let enterPasscodeTitle = + NSLocalizedString("Enter Passcode", tableName: "AuthenticationManager", comment: "Title of the dialog used to request the passcode") + + static let enterPasscode = + NSLocalizedString("Enter passcode", tableName: "AuthenticationManager", comment: "Text displayed above the input field when changing the existing passcode") + + static let reenterPasscode = + NSLocalizedString("Re-enter passcode", tableName: "AuthenticationManager", comment: "Text displayed above the input field when confirming a passcode") + + static let setPasscode = + NSLocalizedString("Set Passcode", tableName: "AuthenticationManager", comment: "Title of the dialog used to set a passcode") + + static let turnOffPasscode = + NSLocalizedString("Turn Passcode Off", tableName: "AuthenticationManager", comment: "Label used as a setting item to turn off passcode") + + static let turnOnPasscode = + NSLocalizedString("Turn Passcode On", tableName: "AuthenticationManager", comment: "Label used as a setting item to turn on passcode") + + static let changePasscode = + NSLocalizedString("Change Passcode", tableName: "AuthenticationManager", comment: "Label used as a setting item and title of the following screen to change the current passcode") + + static let enterNewPasscode = + NSLocalizedString("Enter a new passcode", tableName: "AuthenticationManager", comment: "Text displayed above the input field when changing the existing passcode") + + static let immediately = + NSLocalizedString("Immediately", tableName: "AuthenticationManager", comment: "'Immediately' interval item for selecting when to require passcode") + + static let oneMinute = + NSLocalizedString("After 1 minute", tableName: "AuthenticationManager", comment: "'After 1 minute' interval item for selecting when to require passcode") + + static let fiveMinutes = + NSLocalizedString("After 5 minutes", tableName: "AuthenticationManager", comment: "'After 5 minutes' interval item for selecting when to require passcode") + + static let tenMinutes = + NSLocalizedString("After 10 minutes", tableName: "AuthenticationManager", comment: "'After 10 minutes' interval item for selecting when to require passcode") + + static let fifteenMinutes = + NSLocalizedString("After 15 minutes", tableName: "AuthenticationManager", comment: "'After 15 minutes' interval item for selecting when to require passcode") + + static let oneHour = + NSLocalizedString("After 1 hour", tableName: "AuthenticationManager", comment: "'After 1 hour' interval item for selecting when to require passcode") + + static let loginsTouchReason = + NSLocalizedString("Use your fingerprint to access Logins now.", tableName: "AuthenticationManager", comment: "Touch ID prompt subtitle when accessing logins") + + static let requirePasscodeTouchReason = + NSLocalizedString("touchid.require.passcode.reason.label", + tableName: "AuthenticationManager", + value: "Use your fingerprint to access configuring your required passcode interval.", + comment: "Touch ID prompt subtitle when accessing the require passcode setting") + + static let disableTouchReason = + NSLocalizedString("touchid.disable.reason.label", + tableName: "AuthenticationManager", + value: "Use your fingerprint to disable Touch ID.", + comment: "Touch ID prompt subtitle when disabling Touch ID") + + static let wrongPasscodeError = + NSLocalizedString("Incorrect passcode. Try again.", tableName: "AuthenticationManager", comment: "Error message displayed when user enters incorrect passcode when trying to enter a protected section of the app") + + static let incorrectAttemptsRemaining = + NSLocalizedString("Incorrect passcode. Try again (Attempts remaining: %d).", tableName: "AuthenticationManager", comment: "Error message displayed when user enters incorrect passcode when trying to enter a protected section of the app with attempts remaining") + + static let maximumAttemptsReached = + NSLocalizedString("Maximum attempts reached. Please try again in an hour.", tableName: "AuthenticationManager", comment: "Error message displayed when user enters incorrect passcode and has reached the maximum number of attempts.") + + static let maximumAttemptsReachedNoTime = + NSLocalizedString("Maximum attempts reached. Please try again later.", tableName: "AuthenticationManager", comment: "Error message displayed when user enters incorrect passcode and has reached the maximum number of attempts.") +} diff --git a/mobile/ios/Client/Frontend/AuthenticationManager/AuthenticationSettingsViewController.swift b/mobile/ios/Client/Frontend/AuthenticationManager/AuthenticationSettingsViewController.swift new file mode 100644 index 0000000000..7c49cee4d0 --- /dev/null +++ b/mobile/ios/Client/Frontend/AuthenticationManager/AuthenticationSettingsViewController.swift @@ -0,0 +1,351 @@ +/* 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 UIKit +import Shared +import SwiftKeychainWrapper +import LocalAuthentication + +private let logger = Logger.browserLogger + +private func presentNavAsFormSheet(_ presented: UINavigationController, presenter: UINavigationController?, settings: AuthenticationSettingsViewController?) { + presented.modalPresentationStyle = .formSheet + presenter?.present(presented, animated: true, completion: { + if let selectedRow = settings?.tableView.indexPathForSelectedRow { + settings?.tableView.deselectRow(at: selectedRow, animated: false) + } + }) +} + +class TurnPasscodeOnSetting: Setting { + weak var settings: AuthenticationSettingsViewController? + + init(settings: SettingsTableViewController, delegate: SettingsDelegate? = nil) { + self.settings = settings as? AuthenticationSettingsViewController + super.init(title: NSAttributedString.tableRowTitle(AuthenticationStrings.turnOnPasscode, enabled: true), + delegate: delegate) + } + + override func onClick(_ navigationController: UINavigationController?) { + presentNavAsFormSheet(UINavigationController(rootViewController: SetupPasscodeViewController()), + presenter: navigationController, + settings: settings) + } +} + +class TurnPasscodeOffSetting: Setting { + weak var settings: AuthenticationSettingsViewController? + + init(settings: SettingsTableViewController, delegate: SettingsDelegate? = nil) { + self.settings = settings as? AuthenticationSettingsViewController + super.init(title: NSAttributedString.tableRowTitle(AuthenticationStrings.turnOffPasscode, enabled: true), + delegate: delegate) + } + + override func onClick(_ navigationController: UINavigationController?) { + presentNavAsFormSheet(UINavigationController(rootViewController: RemovePasscodeViewController()), + presenter: navigationController, + settings: settings) + } +} + +class ChangePasscodeSetting: Setting { + weak var settings: AuthenticationSettingsViewController? + + init(settings: SettingsTableViewController, delegate: SettingsDelegate? = nil, enabled: Bool) { + self.settings = settings as? AuthenticationSettingsViewController + + let attributedTitle: NSAttributedString = NSAttributedString.tableRowTitle(AuthenticationStrings.changePasscode, enabled: enabled) + + super.init(title: attributedTitle, + delegate: delegate, + enabled: enabled) + } + + override func onClick(_ navigationController: UINavigationController?) { + presentNavAsFormSheet(UINavigationController(rootViewController: ChangePasscodeViewController()), + presenter: navigationController, + settings: settings) + } +} + +class RequirePasscodeSetting: Setting { + weak var settings: AuthenticationSettingsViewController? + + fileprivate weak var navigationController: UINavigationController? + + override var accessoryType: UITableViewCellAccessoryType { return .disclosureIndicator } + + override var style: UITableViewCellStyle { return .value1 } + + override var status: NSAttributedString { + // Only show the interval if we are enabled and have an interval set. + let authenticationInterval = KeychainWrapper.sharedAppContainerKeychain.authenticationInfo() + if let interval = authenticationInterval?.requiredPasscodeInterval, enabled { + return NSAttributedString.tableRowTitle(interval.settingTitle, enabled: false) + } + return NSAttributedString(string: "") + } + + init(settings: SettingsTableViewController, delegate: SettingsDelegate? = nil, enabled: Bool? = nil) { + self.navigationController = settings.navigationController + self.settings = settings as? AuthenticationSettingsViewController + + let title = AuthenticationStrings.requirePasscode + let attributedTitle = NSAttributedString.tableRowTitle(title, enabled: enabled ?? true) + super.init(title: attributedTitle, + delegate: delegate, + enabled: enabled) + } + + func deselectRow () { + if let selectedRow = self.settings?.tableView.indexPathForSelectedRow { + self.settings?.tableView.deselectRow(at: selectedRow, animated: true) + } + } + + override func onClick(_: UINavigationController?) { + guard let authInfo = KeychainWrapper.sharedAppContainerKeychain.authenticationInfo() else { + navigateToRequireInterval() + return + } + + if authInfo.requiresValidation() { + AppAuthenticator.presentAuthenticationUsingInfo(authInfo, + touchIDReason: AuthenticationStrings.requirePasscodeTouchReason, + success: { + self.navigateToRequireInterval() + }, + cancel: nil, + fallback: { + AppAuthenticator.presentPasscodeAuthentication(self.navigationController, delegate: self) + self.deselectRow() + }) + } else { + self.navigateToRequireInterval() + } + } + + fileprivate func navigateToRequireInterval() { + navigationController?.pushViewController(RequirePasscodeIntervalViewController(), animated: true) + } +} + +extension RequirePasscodeSetting: PasscodeEntryDelegate { + @objc func passcodeValidationDidSucceed() { + navigationController?.dismiss(animated: true) { + self.navigateToRequireInterval() + } + } +} + +class TouchIDSetting: Setting { + fileprivate let authInfo: AuthenticationKeychainInfo? + + fileprivate weak var navigationController: UINavigationController? + fileprivate weak var switchControl: UISwitch? + + fileprivate var touchIDSuccess: (() -> Void)? + fileprivate var touchIDFallback: (() -> Void)? + + init( + title: NSAttributedString?, + navigationController: UINavigationController? = nil, + delegate: SettingsDelegate? = nil, + enabled: Bool? = nil, + touchIDSuccess: (() -> Void)? = nil, + touchIDFallback: (() -> Void)? = nil) { + self.touchIDSuccess = touchIDSuccess + self.touchIDFallback = touchIDFallback + self.navigationController = navigationController + self.authInfo = KeychainWrapper.sharedAppContainerKeychain.authenticationInfo() + super.init(title: title, delegate: delegate, enabled: enabled) + } + + override func onConfigureCell(_ cell: UITableViewCell) { + super.onConfigureCell(cell) + cell.selectionStyle = .none + + // In order for us to recognize a tap gesture without toggling the switch, + // the switch is wrapped in a UIView which has a tap gesture recognizer. This way + // we can disable interaction of the switch and still handle tap events. + let control = UISwitch() + control.onTintColor = UIConstants.ControlTintColor + control.isOn = authInfo?.useTouchID ?? false + control.isUserInteractionEnabled = false + switchControl = control + + let accessoryContainer = UIView(frame: control.frame) + accessoryContainer.addSubview(control) + + let gesture = UITapGestureRecognizer(target: self, action: #selector(TouchIDSetting.switchTapped)) + accessoryContainer.addGestureRecognizer(gesture) + + cell.accessoryView = accessoryContainer + } + + @objc fileprivate func switchTapped() { + guard let authInfo = authInfo else { + logger.error("Authentication info should always be present when modifying Touch ID preference.") + return + } + + if authInfo.useTouchID { + AppAuthenticator.presentAuthenticationUsingInfo( + authInfo, + touchIDReason: AuthenticationStrings.disableTouchReason, + success: self.touchIDSuccess, + cancel: nil, + fallback: self.touchIDFallback + ) + } else { + toggleTouchID(true) + } + } + + func toggleTouchID(_ enabled: Bool) { + authInfo?.useTouchID = enabled + KeychainWrapper.sharedAppContainerKeychain.setAuthenticationInfo(authInfo) + switchControl?.setOn(enabled, animated: true) + } +} + +class AuthenticationSettingsViewController: SettingsTableViewController { + override func viewDidLoad() { + super.viewDidLoad() + updateTitleForTouchIDState() + + let notificationCenter = NotificationCenter.default + notificationCenter.addObserver(self, selector: #selector(AuthenticationSettingsViewController.refreshSettings(_:)), name: NSNotification.Name(rawValue: NotificationPasscodeDidRemove), object: nil) + notificationCenter.addObserver(self, selector: #selector(AuthenticationSettingsViewController.refreshSettings(_:)), name: NSNotification.Name(rawValue: NotificationPasscodeDidCreate), object: nil) + notificationCenter.addObserver(self, selector: #selector(AuthenticationSettingsViewController.refreshSettings(_:)), name: NSNotification.Name.UIApplicationDidBecomeActive, object: nil) + + tableView.accessibilityIdentifier = "AuthenticationManager.settingsTableView" + } + + deinit { + let notificationCenter = NotificationCenter.default + notificationCenter.removeObserver(self, name: NSNotification.Name(rawValue: NotificationPasscodeDidRemove), object: nil) + notificationCenter.removeObserver(self, name: NSNotification.Name(rawValue: NotificationPasscodeDidCreate), object: nil) + notificationCenter.removeObserver(self, name: NSNotification.Name.UIApplicationDidBecomeActive, object: nil) + } + + override func generateSettings() -> [SettingSection] { + if let _ = KeychainWrapper.sharedAppContainerKeychain.authenticationInfo() { + return passcodeEnabledSettings() + } else { + return passcodeDisabledSettings() + } + } + + fileprivate func updateTitleForTouchIDState() { + let localAuthContext = LAContext() + if localAuthContext.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: nil) { + let title: String + if #available(iOS 11.0, *), localAuthContext.biometryType == .faceID { + title = AuthenticationStrings.faceIDPasscodeSetting + } else { + title = AuthenticationStrings.touchIDPasscodeSetting + } + navigationItem.title = title + } else { + navigationItem.title = AuthenticationStrings.passcode + } + } + + fileprivate func passcodeEnabledSettings() -> [SettingSection] { + var settings = [SettingSection]() + + let passcodeSectionTitle = NSAttributedString(string: AuthenticationStrings.passcode) + let passcodeSection = SettingSection(title: passcodeSectionTitle, children: [ + TurnPasscodeOffSetting(settings: self), + ChangePasscodeSetting(settings: self, delegate: nil, enabled: true) + ]) + + var requirePasscodeSectionChildren: [Setting] = [RequirePasscodeSetting(settings: self)] + let localAuthContext = LAContext() + if localAuthContext.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: nil) { + let title: String + if #available(iOS 11.0, *), localAuthContext.biometryType == .faceID { + title = Strings.UseFaceID + } else { + title = Strings.UseTouchID + } + requirePasscodeSectionChildren.append( + TouchIDSetting( + title: NSAttributedString.tableRowTitle(title, enabled: true), + navigationController: self.navigationController, + delegate: nil, + enabled: true, + touchIDSuccess: { [unowned self] in + self.touchIDAuthenticationSucceeded() + }, + touchIDFallback: { [unowned self] in + self.fallbackOnTouchIDFailure() + } + ) + ) + } + + let requirePasscodeSection = SettingSection(title: nil, children: requirePasscodeSectionChildren) + settings += [ + passcodeSection, + requirePasscodeSection, + ] + + return settings + } + + fileprivate func passcodeDisabledSettings() -> [SettingSection] { + var settings = [SettingSection]() + + let passcodeSectionTitle = NSAttributedString(string: AuthenticationStrings.passcode) + let passcodeSection = SettingSection(title: passcodeSectionTitle, children: [ + TurnPasscodeOnSetting(settings: self), + ChangePasscodeSetting(settings: self, delegate: nil, enabled: false) + ]) + + let requirePasscodeSection = SettingSection(title: nil, children: [ + RequirePasscodeSetting(settings: self, delegate: nil, enabled: false), + ]) + + settings += [ + passcodeSection, + requirePasscodeSection, + ] + + return settings + } +} + +extension AuthenticationSettingsViewController { + func refreshSettings(_ notification: Notification) { + updateTitleForTouchIDState() + settings = generateSettings() + tableView.reloadData() + } +} + +extension AuthenticationSettingsViewController: PasscodeEntryDelegate { + fileprivate func getTouchIDSetting() -> TouchIDSetting? { + guard settings.count >= 2 && settings[1].count >= 2 else { + return nil + } + return settings[1][1] as? TouchIDSetting + } + + func touchIDAuthenticationSucceeded() { + getTouchIDSetting()?.toggleTouchID(false) + } + + func fallbackOnTouchIDFailure() { + AppAuthenticator.presentPasscodeAuthentication(self.navigationController, delegate: self) + } + + @objc func passcodeValidationDidSucceed() { + getTouchIDSetting()?.toggleTouchID(false) + navigationController?.dismiss(animated: true, completion: nil) + } +} diff --git a/mobile/ios/Client/Frontend/AuthenticationManager/BasePasscodeViewController.swift b/mobile/ios/Client/Frontend/AuthenticationManager/BasePasscodeViewController.swift new file mode 100644 index 0000000000..62dd46e8a0 --- /dev/null +++ b/mobile/ios/Client/Frontend/AuthenticationManager/BasePasscodeViewController.swift @@ -0,0 +1,92 @@ +/* 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 SwiftKeychainWrapper + +/// Base UIViewController subclass containing methods for displaying common error messaging +/// for the various Passcode configuration screens. +class BasePasscodeViewController: UIViewController { + var authenticationInfo: AuthenticationKeychainInfo? + + var errorToast: ErrorToast? + let errorPadding: CGFloat = 10 + + init() { + self.authenticationInfo = KeychainWrapper.sharedAppContainerKeychain.authenticationInfo() + super.init(nibName: nil, bundle: nil) + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + view.backgroundColor = UIConstants.TableViewHeaderBackgroundColor + navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .cancel, target: self, action: #selector(self.dismissAnimated)) + automaticallyAdjustsScrollViewInsets = false + } + + func dismissAnimated() { + self.dismiss(animated: true, completion: nil) + } +} + +// MARK: - Error Helpers +extension BasePasscodeViewController { + fileprivate func displayError(_ text: String) { + errorToast?.removeFromSuperview() + errorToast = { + let toast = ErrorToast() + toast.textLabel.text = text + view.addSubview(toast) + toast.snp.makeConstraints { make in + make.center.equalTo(self.view) + make.left.greaterThanOrEqualTo(self.view).offset(errorPadding) + make.right.lessThanOrEqualTo(self.view).offset(-errorPadding) + } + return toast + }() + } + + func displayLockoutError() { + displayError(AuthenticationStrings.maximumAttemptsReachedNoTime) + } + + func failMismatchPasscode() { + let mismatchPasscodeError + = NSLocalizedString("Passcodes didn’t match. Try again.", + tableName: "AuthenticationManager", + comment: "Error message displayed to user when their confirming passcode doesn't match the first code.") + displayError(mismatchPasscodeError) + } + + func failMustBeDifferent() { + let useNewPasscodeError + = NSLocalizedString("New passcode must be different than existing code.", + tableName: "AuthenticationManager", + comment: "Error message displayed when user tries to enter the same passcode as their existing code when changing it.") + displayError(useNewPasscodeError) + } + + func failIncorrectPasscode(_ inputView: PasscodeInputView) { + authenticationInfo?.recordFailedAttempt() + let numberOfAttempts = authenticationInfo?.failedAttempts ?? 0 + if numberOfAttempts == AllowedPasscodeFailedAttempts { + authenticationInfo?.lockOutUser() + displayError(AuthenticationStrings.maximumAttemptsReachedNoTime) + inputView.isUserInteractionEnabled = false + resignFirstResponder() + } else { + displayError(String(format: AuthenticationStrings.incorrectAttemptsRemaining, (AllowedPasscodeFailedAttempts - numberOfAttempts))) + } + + inputView.resetCode() + + // Store mutations on authentication info object + KeychainWrapper.sharedAppContainerKeychain.setAuthenticationInfo(authenticationInfo) + } +} diff --git a/mobile/ios/Client/Frontend/AuthenticationManager/ChangePasscodeViewController.swift b/mobile/ios/Client/Frontend/AuthenticationManager/ChangePasscodeViewController.swift new file mode 100644 index 0000000000..486ed2c34f --- /dev/null +++ b/mobile/ios/Client/Frontend/AuthenticationManager/ChangePasscodeViewController.swift @@ -0,0 +1,93 @@ +/* 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 SwiftKeychainWrapper +import Shared + +let NotificationPasscodeDidChange = "NotificationPasscodeDidChange" + +/// Displayed to the user when changing an existing passcode. +class ChangePasscodeViewController: PagingPasscodeViewController, PasscodeInputViewDelegate { + fileprivate var newPasscode: String? + fileprivate var oldPasscode: String? + + override init() { + super.init() + self.title = AuthenticationStrings.changePasscode + self.panes = [ + PasscodePane(title: AuthenticationStrings.enterPasscode, passcodeSize: authenticationInfo?.passcode?.count ?? 6), + PasscodePane(title: AuthenticationStrings.enterNewPasscode, passcodeSize: 6), + PasscodePane(title: AuthenticationStrings.reenterPasscode, passcodeSize: 6), + ] + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + panes.forEach { $0.codeInputView.delegate = self } + + // Don't show the keyboard or allow typing if we're locked out. Also display the error. + if authenticationInfo?.isLocked() ?? false { + displayLockoutError() + panes.first?.codeInputView.isUserInteractionEnabled = false + } else { + panes.first?.codeInputView.becomeFirstResponder() + } + } + + func passcodeInputView(_ inputView: PasscodeInputView, didFinishEnteringCode code: String) { + switch currentPaneIndex { + case 0: + // Constraint: We need to make sure that the first passcode they've entered matches the one stored in the keychain + if code != authenticationInfo?.passcode { + panes[currentPaneIndex].shakePasscode() + failIncorrectPasscode(inputView) + return + } + oldPasscode = code + authenticationInfo?.recordValidation() + + // Clear out any previous errors if we are allowed to proceed + errorToast?.removeFromSuperview() + scrollToNextAndSelect() + case 1: + // Constraint: The new passcode cannot match their old passcode. + if oldPasscode == code { + failMustBeDifferent() + + // Scroll back and reset the input fields + resetAllInputFields() + return + } + newPasscode = code + errorToast?.removeFromSuperview() + scrollToNextAndSelect() + case 2: + if newPasscode != code { + failMismatchPasscode() + + // Scroll back and reset input fields + resetAllInputFields() + scrollToPreviousAndSelect() + newPasscode = nil + return + } + changePasscodeToCode(code) + dismissAnimated() + default: + break + } + } + + fileprivate func changePasscodeToCode(_ code: String) { + authenticationInfo?.updatePasscode(code) + KeychainWrapper.sharedAppContainerKeychain.setAuthenticationInfo(authenticationInfo) + let notificationCenter = NotificationCenter.default + notificationCenter.post(name: Notification.Name(rawValue: NotificationPasscodeDidChange), object: nil) + } +} diff --git a/mobile/ios/Client/Frontend/AuthenticationManager/PagingPasscodeViewController.swift b/mobile/ios/Client/Frontend/AuthenticationManager/PagingPasscodeViewController.swift new file mode 100644 index 0000000000..5016ac6427 --- /dev/null +++ b/mobile/ios/Client/Frontend/AuthenticationManager/PagingPasscodeViewController.swift @@ -0,0 +1,89 @@ +/* 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 +private let PaneSwipeDuration: TimeInterval = 0.3 + +/// Base class for implementing a Passcode configuration screen with multiple 'panes'. +class PagingPasscodeViewController: BasePasscodeViewController { + fileprivate lazy var pager: UIScrollView = { + let scrollView = UIScrollView() + scrollView.isPagingEnabled = true + scrollView.isUserInteractionEnabled = false + scrollView.showsHorizontalScrollIndicator = false + scrollView.showsVerticalScrollIndicator = false + return scrollView + }() + + var panes = [PasscodePane]() + var currentPaneIndex: Int = 0 + + override func viewDidLoad() { + super.viewDidLoad() + view.addSubview(pager) + panes.forEach { pager.addSubview($0) } + pager.snp.makeConstraints { make in + make.bottom.left.right.equalTo(self.view) + make.top.equalTo(self.topLayoutGuide.snp.bottom) + } + } + + override func viewDidLayoutSubviews() { + super.viewDidLayoutSubviews() + panes.enumerated().forEach { index, pane in + pane.frame = CGRect(origin: CGPoint(x: CGFloat(index) * pager.frame.width, y: 0), size: pager.frame.size) + } + pager.contentSize = CGSize(width: CGFloat(panes.count) * pager.frame.width, height: pager.frame.height) + scrollToPaneAtIndex(currentPaneIndex) + if self.authenticationInfo?.isLocked() ?? false { + return + } + panes[currentPaneIndex].codeInputView.becomeFirstResponder() + } + + override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + self.view.endEditing(true) + } +} + +extension PagingPasscodeViewController { + @discardableResult func scrollToNextAndSelect() -> PasscodePane { + scrollToNextPane() + panes[currentPaneIndex].codeInputView.becomeFirstResponder() + return panes[currentPaneIndex] + } + + @discardableResult func scrollToPreviousAndSelect() -> PasscodePane { + scrollToPreviousPane() + panes[currentPaneIndex].codeInputView.becomeFirstResponder() + return panes[currentPaneIndex] + } + + func resetAllInputFields() { + panes.forEach { $0.codeInputView.resetCode() } + } + + func scrollToNextPane() { + guard (currentPaneIndex + 1) < panes.count else { + return + } + currentPaneIndex += 1 + scrollToPaneAtIndex(currentPaneIndex) + } + + func scrollToPreviousPane() { + guard (currentPaneIndex - 1) >= 0 else { + return + } + currentPaneIndex -= 1 + scrollToPaneAtIndex(currentPaneIndex) + } + + func scrollToPaneAtIndex(_ index: Int) { + UIView.animate(withDuration: PaneSwipeDuration, delay: 0, options: UIViewAnimationOptions(), animations: { + self.pager.contentOffset = CGPoint(x: CGFloat(self.currentPaneIndex) * self.pager.frame.width, y: 0) + }, completion: nil) + } +} diff --git a/mobile/ios/Client/Frontend/AuthenticationManager/PasscodeEntryViewController.swift b/mobile/ios/Client/Frontend/AuthenticationManager/PasscodeEntryViewController.swift new file mode 100644 index 0000000000..c9a8b009dd --- /dev/null +++ b/mobile/ios/Client/Frontend/AuthenticationManager/PasscodeEntryViewController.swift @@ -0,0 +1,92 @@ +/* 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 SnapKit +import Shared +import SwiftKeychainWrapper + +/// Delegate available for PasscodeEntryViewController consumers to be notified of the validation of a passcode. +@objc protocol PasscodeEntryDelegate: class { + func passcodeValidationDidSucceed() + @objc optional func userDidCancelValidation() +} + +/// Presented to the to user when asking for their passcode to validate entry into a part of the app. +class PasscodeEntryViewController: BasePasscodeViewController { + weak var delegate: PasscodeEntryDelegate? + fileprivate var passcodePane: PasscodePane + + override init() { + let authInfo = KeychainWrapper.sharedAppContainerKeychain.authenticationInfo() + passcodePane = PasscodePane(title:nil, passcodeSize:authInfo?.passcode?.count ?? 6) + + super.init() + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + title = AuthenticationStrings.enterPasscodeTitle + view.addSubview(passcodePane) + passcodePane.snp.makeConstraints { make in + make.bottom.left.right.equalTo(self.view) + make.top.equalTo(self.topLayoutGuide.snp.bottom) + } + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + passcodePane.codeInputView.delegate = self + + // Don't show the keyboard or allow typing if we're locked out. Also display the error. + if authenticationInfo?.isLocked() ?? false { + displayLockoutError() + passcodePane.codeInputView.isUserInteractionEnabled = false + } else { + passcodePane.codeInputView.becomeFirstResponder() + } + } + + override func viewDidLayoutSubviews() { + super.viewDidLayoutSubviews() + + if authenticationInfo?.isLocked() ?? false { + passcodePane.codeInputView.isUserInteractionEnabled = false + passcodePane.codeInputView.resignFirstResponder() + } else { + passcodePane.codeInputView.becomeFirstResponder() + } + } + + override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + self.view.endEditing(true) + } + + override func dismissAnimated() { + delegate?.userDidCancelValidation?() + super.dismissAnimated() + } +} + +extension PasscodeEntryViewController: PasscodeInputViewDelegate { + func passcodeInputView(_ inputView: PasscodeInputView, didFinishEnteringCode code: String) { + if let passcode = authenticationInfo?.passcode, passcode == code { + authenticationInfo?.recordValidation() + KeychainWrapper.sharedAppContainerKeychain.setAuthenticationInfo(authenticationInfo) + delegate?.passcodeValidationDidSucceed() + } else { + passcodePane.shakePasscode() + failIncorrectPasscode(inputView) + passcodePane.codeInputView.resetCode() + + // Store mutations on authentication info object + KeychainWrapper.sharedAppContainerKeychain.setAuthenticationInfo(authenticationInfo) + } + } +} diff --git a/mobile/ios/Client/Frontend/AuthenticationManager/PasscodeViews.swift b/mobile/ios/Client/Frontend/AuthenticationManager/PasscodeViews.swift new file mode 100644 index 0000000000..2283fd7da7 --- /dev/null +++ b/mobile/ios/Client/Frontend/AuthenticationManager/PasscodeViews.swift @@ -0,0 +1,206 @@ +/* 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 SnapKit + +private struct PasscodeUX { + static let TitleVerticalSpacing: CGFloat = 32 + static let DigitSize: CGFloat = 30 + static let TopMargin: CGFloat = 80 + static let PasscodeFieldSize: CGSize = CGSize(width: 160, height: 32) +} + +@objc protocol PasscodeInputViewDelegate: class { + func passcodeInputView(_ inputView: PasscodeInputView, didFinishEnteringCode code: String) +} + +/// A custom, keyboard-able view that displays the blank/filled digits when entrering a passcode. +class PasscodeInputView: UIView, UIKeyInput { + weak var delegate: PasscodeInputViewDelegate? + + var digitFont: UIFont = UIConstants.PasscodeEntryFont + + let blankCharacter: Character = "-" + + let filledCharacter: Character = "•" + + fileprivate let passcodeSize: Int + + fileprivate var inputtedCode: String = "" + + fileprivate var blankDigitString: NSAttributedString { + return NSAttributedString(string: "\(blankCharacter)", attributes: [NSFontAttributeName: digitFont]) + } + + fileprivate var filledDigitString: NSAttributedString { + return NSAttributedString(string: "\(filledCharacter)", attributes: [NSFontAttributeName: digitFont]) + } + + @objc var keyboardType: UIKeyboardType = .numberPad + + init(frame: CGRect, passcodeSize: Int) { + self.passcodeSize = passcodeSize + super.init(frame: frame) + isOpaque = false + } + + convenience init(passcodeSize: Int) { + self.init(frame: CGRect.zero, passcodeSize: passcodeSize) + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override var canBecomeFirstResponder: Bool { + return true + } + + func resetCode() { + inputtedCode = "" + setNeedsDisplay() + } + + @objc var hasText: Bool { + return inputtedCode.characters.count > 0 + } + + @objc func insertText(_ text: String) { + guard inputtedCode.characters.count < passcodeSize else { + return + } + + inputtedCode += text + setNeedsDisplay() + if inputtedCode.characters.count == passcodeSize { + delegate?.passcodeInputView(self, didFinishEnteringCode: inputtedCode) + } + } + + // Required for implementing UIKeyInput + @objc func deleteBackward() { + guard inputtedCode.characters.count > 0 else { + return + } + + inputtedCode.remove(at: inputtedCode.characters.index(before: inputtedCode.endIndex)) + setNeedsDisplay() + } + + override func draw(_ rect: CGRect) { + let circleSize = CGSize(width: 14, height: 14) + + guard let context = UIGraphicsGetCurrentContext() else { return } + + context.setLineWidth(1) + context.setStrokeColor(UIConstants.PasscodeDotColor.cgColor) + context.setFillColor(UIConstants.PasscodeDotColor.cgColor) + + (0.. Int { + return 1 + } + + override func accessibilityElement(at index: Int) -> Any? { + switch index { + case 0: return titleLabel + default: return nil + } + } + + init(title: String? = nil, passcodeSize: Int = 4) { + codeInputView = PasscodeInputView(passcodeSize: passcodeSize) + super.init(frame: CGRect.zero) + backgroundColor = UIConstants.TableViewHeaderBackgroundColor + + titleLabel.text = title + centerContainer.addSubview(titleLabel) + centerContainer.addSubview(codeInputView) + addSubview(centerContainer) + + centerContainer.snp.makeConstraints { make in + make.centerX.equalTo(self) + containerCenterConstraint = make.centerY.equalTo(self).constraint + } + + titleLabel.snp.makeConstraints { make in + make.centerX.equalTo(centerContainer) + make.top.equalTo(centerContainer) + make.bottom.equalTo(codeInputView.snp.top).offset(-PasscodeUX.TitleVerticalSpacing) + } + + codeInputView.snp.makeConstraints { make in + codeViewCenterConstraint = make.centerX.equalTo(centerContainer).constraint + make.bottom.equalTo(centerContainer) + make.size.equalTo(PasscodeUX.PasscodeFieldSize) + } + layoutIfNeeded() + NotificationCenter.default.addObserver(self, selector: #selector(PasscodePane.keyboardWillHide(_:)), name: NSNotification.Name.UIKeyboardWillHide, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(PasscodePane.keyboardWillShow(_:)), name: NSNotification.Name.UIKeyboardWillShow, object: nil) + } + + func shakePasscode() { + UIView.animate(withDuration: 0.1, animations: { + self.codeViewCenterConstraint?.update(offset: -10) + self.layoutIfNeeded() + }, completion: { complete in + UIView.animate(withDuration: 0.1, animations: { + self.codeViewCenterConstraint?.update(offset: 0) + self.layoutIfNeeded() + }) + }) + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func keyboardWillShow(_ sender: Notification) { + guard let keyboardFrame = (sender.userInfo?[UIKeyboardFrameEndUserInfoKey] as AnyObject).cgRectValue else { + return + } + + UIView.animate(withDuration: 0.1, animations: { + self.containerCenterConstraint?.update(offset: -keyboardFrame.height/2) + self.layoutIfNeeded() + }) + } + + func keyboardWillHide(_ sender: Notification) { + UIView.animate(withDuration: 0.1, animations: { + self.containerCenterConstraint?.update(offset: 0) + self.layoutIfNeeded() + }) + } +} diff --git a/mobile/ios/Client/Frontend/AuthenticationManager/RemovePasscodeViewController.swift b/mobile/ios/Client/Frontend/AuthenticationManager/RemovePasscodeViewController.swift new file mode 100644 index 0000000000..6141a0aaa5 --- /dev/null +++ b/mobile/ios/Client/Frontend/AuthenticationManager/RemovePasscodeViewController.swift @@ -0,0 +1,56 @@ +/* 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 SwiftKeychainWrapper +import Shared + +let NotificationPasscodeDidRemove = "NotificationPasscodeDidRemove" + +/// Displayed to the user when removing a passcode. +class RemovePasscodeViewController: PagingPasscodeViewController, PasscodeInputViewDelegate { + override init() { + super.init() + self.title = AuthenticationStrings.turnOffPasscode + self.panes = [ + PasscodePane(title: AuthenticationStrings.enterPasscode, passcodeSize: authenticationInfo?.passcode?.count ?? 6), + ] + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + panes.forEach { $0.codeInputView.delegate = self } + + // Don't show the keyboard or allow typing if we're locked out. Also display the error. + if authenticationInfo?.isLocked() ?? false { + displayLockoutError() + panes.first?.codeInputView.isUserInteractionEnabled = false + } else { + panes.first?.codeInputView.becomeFirstResponder() + } + } + + func passcodeInputView(_ inputView: PasscodeInputView, didFinishEnteringCode code: String) { + if code != authenticationInfo?.passcode { + panes[currentPaneIndex].shakePasscode() + failIncorrectPasscode(inputView) + return + } + + authenticationInfo?.recordValidation() + errorToast?.removeFromSuperview() + removePasscode() + dismissAnimated() + } + + fileprivate func removePasscode() { + KeychainWrapper.sharedAppContainerKeychain.setAuthenticationInfo(nil) + let notificationCenter = NotificationCenter.default + notificationCenter.post(name: Notification.Name(rawValue: NotificationPasscodeDidRemove), object: nil) + } +} diff --git a/mobile/ios/Client/Frontend/AuthenticationManager/RequirePasscodeIntervalViewController.swift b/mobile/ios/Client/Frontend/AuthenticationManager/RequirePasscodeIntervalViewController.swift new file mode 100644 index 0000000000..182dcc496c --- /dev/null +++ b/mobile/ios/Client/Frontend/AuthenticationManager/RequirePasscodeIntervalViewController.swift @@ -0,0 +1,77 @@ +/* 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 SwiftKeychainWrapper + +/// Screen presented to the user when selecting the time interval before requiring a passcode +class RequirePasscodeIntervalViewController: UITableViewController { + let intervalOptions: [PasscodeInterval] = [ + .immediately, + .oneMinute, + .fiveMinutes, + .tenMinutes, + .fifteenMinutes, + .oneHour + ] + + fileprivate let BasicCheckmarkCell = "BasicCheckmarkCell" + fileprivate var authenticationInfo: AuthenticationKeychainInfo? + + init() { + super.init(nibName: nil, bundle: nil) + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + title = AuthenticationStrings.requirePasscode + + tableView.accessibilityIdentifier = "AuthenticationManager.passcodeIntervalTableView" + + tableView.register(UITableViewCell.self, forCellReuseIdentifier: BasicCheckmarkCell) + tableView.backgroundColor = UIConstants.TableViewHeaderBackgroundColor + + let headerFooterFrame = CGRect(origin: CGPoint.zero, size: CGSize(width: self.view.frame.width, height: UIConstants.TableViewHeaderFooterHeight)) + let headerView = SettingsTableSectionHeaderFooterView(frame: headerFooterFrame) + headerView.showTopBorder = false + headerView.showBottomBorder = true + + let footerView = SettingsTableSectionHeaderFooterView(frame: headerFooterFrame) + footerView.showTopBorder = true + footerView.showBottomBorder = false + + tableView.tableHeaderView = headerView + tableView.tableFooterView = footerView + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + self.authenticationInfo = KeychainWrapper.sharedAppContainerKeychain.authenticationInfo() + tableView.reloadData() + } + + override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let cell = tableView.dequeueReusableCell(withIdentifier: BasicCheckmarkCell, for: indexPath) + let option = intervalOptions[indexPath.row] + let intervalTitle = NSAttributedString.tableRowTitle(option.settingTitle, enabled: true) + cell.textLabel?.attributedText = intervalTitle + cell.accessoryType = authenticationInfo?.requiredPasscodeInterval == option ? .checkmark : .none + return cell + } + + override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return intervalOptions.count + } + + override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + authenticationInfo?.updateRequiredPasscodeInterval(intervalOptions[indexPath.row]) + KeychainWrapper.sharedAppContainerKeychain.setAuthenticationInfo(authenticationInfo) + tableView.reloadData() + } +} diff --git a/mobile/ios/Client/Frontend/AuthenticationManager/SensitiveViewController.swift b/mobile/ios/Client/Frontend/AuthenticationManager/SensitiveViewController.swift new file mode 100644 index 0000000000..1f33ade48c --- /dev/null +++ b/mobile/ios/Client/Frontend/AuthenticationManager/SensitiveViewController.swift @@ -0,0 +1,115 @@ +/* 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 SnapKit +import SwiftKeychainWrapper + +enum AuthenticationState { + case notAuthenticating + case presenting +} + +class SensitiveViewController: UIViewController { + var promptingForTouchID: Bool = false + var backgroundedBlur: UIImageView? + var authState: AuthenticationState = .notAuthenticating + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + let notificationCenter = NotificationCenter.default + notificationCenter.addObserver(self, selector: #selector(SensitiveViewController.checkIfUserRequiresValidation), name: NSNotification.Name.UIApplicationWillEnterForeground, object: nil) + notificationCenter.addObserver(self, selector: #selector(SensitiveViewController.checkIfUserRequiresValidation), name: NSNotification.Name.UIApplicationDidBecomeActive, object: nil) + notificationCenter.addObserver(self, selector: #selector(SensitiveViewController.blurContents), name: NSNotification.Name.UIApplicationWillResignActive, object: nil) + notificationCenter.addObserver(self, selector: #selector(SensitiveViewController.hideLogins), name: NSNotification.Name.UIApplicationDidEnterBackground, object: nil) + + } + + override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + let notificationCenter = NotificationCenter.default + notificationCenter.removeObserver(self, name: NSNotification.Name.UIApplicationWillEnterForeground, object: nil) + notificationCenter.removeObserver(self, name: NSNotification.Name.UIApplicationWillResignActive, object: nil) + notificationCenter.removeObserver(self, name: NSNotification.Name.UIApplicationDidBecomeActive, object: nil) + notificationCenter.removeObserver(self, name: NSNotification.Name.UIApplicationDidEnterBackground, object: nil) + } + + func checkIfUserRequiresValidation() { + guard authState != .presenting else { + return + } + + presentedViewController?.dismiss(animated: false, completion: nil) + guard let authInfo = KeychainWrapper.sharedAppContainerKeychain.authenticationInfo(), authInfo.requiresValidation() else { + removeBackgroundedBlur() + return + } + + promptingForTouchID = true + AppAuthenticator.presentAuthenticationUsingInfo(authInfo, + touchIDReason: AuthenticationStrings.loginsTouchReason, + success: { + self.promptingForTouchID = false + self.authState = .notAuthenticating + self.removeBackgroundedBlur() + }, + cancel: { + self.promptingForTouchID = false + self.authState = .notAuthenticating + _ = self.navigationController?.popToRootViewController(animated: true) + }, + fallback: { + self.promptingForTouchID = false + AppAuthenticator.presentPasscodeAuthentication(self.navigationController, delegate: self) + } + ) + authState = .presenting + } + + func hideLogins() { + _ = self.navigationController?.popToRootViewController(animated: true) + } + + func blurContents() { + if backgroundedBlur == nil { + backgroundedBlur = addBlurredContent() + } + } + + func removeBackgroundedBlur() { + if !promptingForTouchID { + backgroundedBlur?.removeFromSuperview() + backgroundedBlur = nil + } + } + + fileprivate func addBlurredContent() -> UIImageView? { + guard let snapshot = view.screenshot() else { + return nil + } + + let blurredSnapshot = snapshot.applyBlur(withRadius: 10, blurType: BOXFILTER, tintColor: UIColor(white: 1, alpha: 0.3), saturationDeltaFactor: 1.8, maskImage: nil) + let blurView = UIImageView(image: blurredSnapshot) + view.addSubview(blurView) + blurView.snp.makeConstraints { $0.edges.equalTo(self.view) } + view.layoutIfNeeded() + + return blurView + } +} + +// MARK: - PasscodeEntryDelegate +extension SensitiveViewController: PasscodeEntryDelegate { + func passcodeValidationDidSucceed() { + removeBackgroundedBlur() + self.navigationController?.dismiss(animated: true, completion: nil) + self.authState = .notAuthenticating + } + + func userDidCancelValidation() { + _ = self.navigationController?.popToRootViewController(animated: false) + self.authState = .notAuthenticating + } +} + diff --git a/mobile/ios/Client/Frontend/AuthenticationManager/SetupPasscodeViewController.swift b/mobile/ios/Client/Frontend/AuthenticationManager/SetupPasscodeViewController.swift new file mode 100644 index 0000000000..1b08a1dea1 --- /dev/null +++ b/mobile/ios/Client/Frontend/AuthenticationManager/SetupPasscodeViewController.swift @@ -0,0 +1,68 @@ +/* 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 SwiftKeychainWrapper + +let NotificationPasscodeDidCreate = "NotificationPasscodeDidCreate" + +/// Displayed to the user when setting up a passcode. +class SetupPasscodeViewController: PagingPasscodeViewController, PasscodeInputViewDelegate { + fileprivate var confirmCode: String? + + override init() { + super.init() + self.title = AuthenticationStrings.setPasscode + self.panes = [ + PasscodePane(title: AuthenticationStrings.enterAPasscode, passcodeSize: 6), + PasscodePane(title: AuthenticationStrings.reenterPasscode, passcodeSize: 6), + ] + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + panes.forEach { $0.codeInputView.delegate = self } + + // Don't show the keyboard or allow typing if we're locked out. Also display the error. + if authenticationInfo?.isLocked() ?? false { + displayLockoutError() + panes.first?.codeInputView.isUserInteractionEnabled = false + } else { + panes.first?.codeInputView.becomeFirstResponder() + } + } + + func passcodeInputView(_ inputView: PasscodeInputView, didFinishEnteringCode code: String) { + switch currentPaneIndex { + case 0: + confirmCode = code + scrollToNextAndSelect() + case 1: + // Constraint: The first and confirmation codes must match. + if confirmCode != code { + failMismatchPasscode() + resetAllInputFields() + scrollToPreviousAndSelect() + confirmCode = nil + return + } + + createPasscodeWithCode(code) + dismissAnimated() + default: + break + } + } + + fileprivate func createPasscodeWithCode(_ code: String) { + KeychainWrapper.sharedAppContainerKeychain.setAuthenticationInfo(AuthenticationKeychainInfo(passcode: code)) + let notificationCenter = NotificationCenter.default + notificationCenter.post(name: Notification.Name(rawValue: NotificationPasscodeDidCreate), object: nil) + } +} diff --git a/mobile/ios/Client/Frontend/Browser/AboutHomeHandler.swift b/mobile/ios/Client/Frontend/Browser/AboutHomeHandler.swift new file mode 100644 index 0000000000..463530a70f --- /dev/null +++ b/mobile/ios/Client/Frontend/Browser/AboutHomeHandler.swift @@ -0,0 +1,38 @@ +/* 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/. */ + +/** + * Handles the page request to about/home/ so that the page loads and does not throw an error (404) on initialization + */ +import GCDWebServers + +struct AboutHomeHandler { + static func register(_ webServer: WebServer) { + webServer.registerHandlerForMethod("GET", module: "about", resource: "home") { (request: GCDWebServerRequest?) -> GCDWebServerResponse! in + return GCDWebServerResponse(statusCode: 200) + } + } +} + +struct AboutLicenseHandler { + static func register(_ webServer: WebServer) { + webServer.registerHandlerForMethod("GET", module: "about", resource: "license") { (request: GCDWebServerRequest?) -> GCDWebServerResponse! in + let path = Bundle.main.path(forResource: "Licenses", ofType: "html") + do { + let html = try NSString(contentsOfFile: path!, encoding: String.Encoding.utf8.rawValue) as String + return GCDWebServerDataResponse(html: html) + } catch { + print("Unable to register webserver \(error)") + } + return GCDWebServerResponse(statusCode: 200) + } + } +} + +extension GCDWebServerDataResponse { + convenience init(XHTML: String) { + let data = XHTML.data(using: String.Encoding.utf8, allowLossyConversion: false) + self.init(data: data, contentType: "application/xhtml+xml; charset=utf-8") + } +} diff --git a/mobile/ios/Client/Frontend/Browser/Authenticator.swift b/mobile/ios/Client/Frontend/Browser/Authenticator.swift new file mode 100644 index 0000000000..28c4db7ead --- /dev/null +++ b/mobile/ios/Client/Frontend/Browser/Authenticator.swift @@ -0,0 +1,153 @@ +/* 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 Deferred + +private let CancelButtonTitle = NSLocalizedString("Cancel", comment: "Label for Cancel button") +private let LogInButtonTitle = NSLocalizedString("Log in", comment: "Authentication prompt log in button") +private let log = Logger.browserLogger + +class Authenticator { + fileprivate static let MaxAuthenticationAttempts = 3 + + static func handleAuthRequest(_ viewController: UIViewController, challenge: URLAuthenticationChallenge, loginsHelper: LoginsHelper?) -> Deferred> { + // If there have already been too many login attempts, we'll just fail. + if challenge.previousFailureCount >= Authenticator.MaxAuthenticationAttempts { + return deferMaybe(LoginDataError(description: "Too many attempts to open site")) + } + + var credential = challenge.proposedCredential + + // If we were passed an initial set of credentials from iOS, try and use them. + if let proposed = credential { + if !(proposed.user?.isEmpty ?? true) { + if challenge.previousFailureCount == 0 { + return deferMaybe(Login.createWithCredential(credential!, protectionSpace: challenge.protectionSpace)) + } + } else { + credential = nil + } + } + + // If we have some credentials, we'll show a prompt with them. + if let credential = credential { + return promptForUsernamePassword(viewController, credentials: credential, protectionSpace: challenge.protectionSpace, loginsHelper: loginsHelper) + } + + // Otherwise, try to look them up and show the prompt. + if let loginsHelper = loginsHelper { + return findMatchingCredentialsForChallenge(challenge, fromLoginsProvider: loginsHelper.logins).bindQueue(DispatchQueue.main) { result in + guard let credentials = result.successValue else { + return deferMaybe(result.failureValue ?? LoginDataError(description: "Unknown error when finding credentials")) + } + return self.promptForUsernamePassword(viewController, credentials: credentials, protectionSpace: challenge.protectionSpace, loginsHelper: loginsHelper) + } + } + + // No credentials, so show an empty prompt. + return self.promptForUsernamePassword(viewController, credentials: nil, protectionSpace: challenge.protectionSpace, loginsHelper: nil) + } + + static func findMatchingCredentialsForChallenge(_ challenge: URLAuthenticationChallenge, fromLoginsProvider loginsProvider: BrowserLogins) -> Deferred> { + return loginsProvider.getLoginsForProtectionSpace(challenge.protectionSpace) >>== { cursor in + guard cursor.count >= 1 else { + return deferMaybe(nil) + } + + let logins = cursor.asArray() + var credentials: URLCredential? = nil + + // It is possible that we might have duplicate entries since we match against host and scheme://host. + // This is a side effect of https://bugzilla.mozilla.org/show_bug.cgi?id=1238103. + if logins.count > 1 { + credentials = (logins.find { login in + (login.protectionSpace.`protocol` == challenge.protectionSpace.`protocol`) && !login.hasMalformedHostname + })?.credentials + + let malformedGUIDs: [GUID] = logins.flatMap { login in + if login.hasMalformedHostname { + return login.guid + } + return nil + } + loginsProvider.removeLoginsWithGUIDs(malformedGUIDs).upon { log.debug("Removed malformed logins. Success :\($0.isSuccess)") } + } + + // Found a single entry but the schemes don't match. This is a result of a schemeless entry that we + // saved in a previous iteration of the app so we need to migrate it. We only care about the + // the username/password so we can rewrite the scheme to be correct. + else if logins.count == 1 && logins[0].protectionSpace.`protocol` != challenge.protectionSpace.`protocol` { + let login = logins[0] + credentials = login.credentials + let new = Login(credential: login.credentials, protectionSpace: challenge.protectionSpace) + return loginsProvider.updateLoginByGUID(login.guid, new: new, significant: true) + >>> { deferMaybe(credentials) } + } + + // Found a single entry that matches the scheme and host - good to go. + else { + credentials = logins[0].credentials + } + + return deferMaybe(credentials) + } + } + + fileprivate static func promptForUsernamePassword(_ viewController: UIViewController, credentials: URLCredential?, protectionSpace: URLProtectionSpace, loginsHelper: LoginsHelper?) -> Deferred> { + if protectionSpace.host.isEmpty { + print("Unable to show a password prompt without a hostname") + return deferMaybe(LoginDataError(description: "Unable to show a password prompt without a hostname")) + } + + let deferred = Deferred>() + let alert: UIAlertController + let title = NSLocalizedString("Authentication required", comment: "Authentication prompt title") + if !(protectionSpace.realm?.isEmpty ?? true) { + let msg = NSLocalizedString("A username and password are being requested by %@. The site says: %@", comment: "Authentication prompt message with a realm. First parameter is the hostname. Second is the realm string") + let formatted = NSString(format: msg as NSString, protectionSpace.host, protectionSpace.realm ?? "") as String + alert = UIAlertController(title: title, message: formatted, preferredStyle: UIAlertControllerStyle.alert) + } else { + let msg = NSLocalizedString("A username and password are being requested by %@.", comment: "Authentication prompt message with no realm. Parameter is the hostname of the site") + let formatted = NSString(format: msg as NSString, protectionSpace.host) as String + alert = UIAlertController(title: title, message: formatted, preferredStyle: UIAlertControllerStyle.alert) + } + + // Add a button to log in. + let action = UIAlertAction(title: LogInButtonTitle, + style: UIAlertActionStyle.default) { (action) -> Void in + guard let user = alert.textFields?[0].text, let pass = alert.textFields?[1].text else { deferred.fill(Maybe(failure: LoginDataError(description: "Username and Password required"))); return } + + let login = Login.createWithCredential(URLCredential(user: user, password: pass, persistence: .forSession), protectionSpace: protectionSpace) + deferred.fill(Maybe(success: login)) + loginsHelper?.setCredentials(login) + } + alert.addAction(action) + + // Add a cancel button. + let cancel = UIAlertAction(title: CancelButtonTitle, style: UIAlertActionStyle.cancel) { (action) -> Void in + deferred.fill(Maybe(failure: LoginDataError(description: "Save password cancelled"))) + } + alert.addAction(cancel) + + // Add a username textfield. + alert.addTextField { (textfield) -> Void in + textfield.placeholder = NSLocalizedString("Username", comment: "Username textbox in Authentication prompt") + textfield.text = credentials?.user + } + + // Add a password textfield. + alert.addTextField { (textfield) -> Void in + textfield.placeholder = NSLocalizedString("Password", comment: "Password textbox in Authentication prompt") + textfield.isSecureTextEntry = true + textfield.text = credentials?.password + } + + viewController.present(alert, animated: true) { () -> Void in } + return deferred + } + +} diff --git a/mobile/ios/Client/Frontend/Browser/BackForwardListAnimator.swift b/mobile/ios/Client/Frontend/Browser/BackForwardListAnimator.swift new file mode 100644 index 0000000000..d7c1672a0c --- /dev/null +++ b/mobile/ios/Client/Frontend/Browser/BackForwardListAnimator.swift @@ -0,0 +1,83 @@ +/* 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 UIKit + +class BackForwardListAnimator: NSObject, UIViewControllerAnimatedTransitioning { + + var presenting: Bool = false + let animationDuration = 0.4 + + func animateTransition(using transitionContext: UIViewControllerContextTransitioning) { + let screens = (from: transitionContext.viewController(forKey: UITransitionContextViewControllerKey.from)!, to: transitionContext.viewController(forKey: UITransitionContextViewControllerKey.to)!) + + guard let backForwardViewController = !self.presenting ? screens.from as? BackForwardListViewController : screens.to as? BackForwardListViewController else { + return + } + + var bottomViewController = !self.presenting ? screens.to as UIViewController : screens.from as UIViewController + + if let navController = bottomViewController as? UINavigationController { + bottomViewController = navController.viewControllers.last ?? bottomViewController + } + + if let browserViewController = bottomViewController as? BrowserViewController { + animateWithBackForward(backForwardViewController, browserViewController: browserViewController, transitionContext: transitionContext) + } + } + + func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval { + return animationDuration + } +} + +extension BackForwardListAnimator: UIViewControllerTransitioningDelegate { + func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? { + self.presenting = true + return self + } + + func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? { + self.presenting = false + return self + } +} + +extension BackForwardListAnimator { + fileprivate func animateWithBackForward(_ backForward: BackForwardListViewController, browserViewController bvc: BrowserViewController, transitionContext: UIViewControllerContextTransitioning) { + let containerView = transitionContext.containerView + + if presenting { + backForward.view.frame = bvc.view.frame + backForward.view.alpha = 0 + containerView.addSubview(backForward.view) + backForward.view.snp.updateConstraints { make in + make.edges.equalTo(containerView) + } + backForward.view.layoutIfNeeded() + + UIView.animate(withDuration: transitionDuration(using: transitionContext), delay: 0, usingSpringWithDamping: 0.8, initialSpringVelocity: 0.3, options: [], animations: { () -> Void in + backForward.view.alpha = 1 + backForward.tableView.snp.updateConstraints { make in + make.height.equalTo(backForward.tableHeight) + } + backForward.view.layoutIfNeeded() + }, completion: { (completed) -> Void in + transitionContext.completeTransition(completed) + }) + + } else { + UIView.animate(withDuration: transitionDuration(using: transitionContext), delay: 0, usingSpringWithDamping: 1.2, initialSpringVelocity: 0.0, options: [], animations: { () -> Void in + backForward.view.alpha = 0 + backForward.tableView.snp.updateConstraints { make in + make.height.equalTo(0) + } + backForward.view.layoutIfNeeded() + }, completion: { (completed) -> Void in + backForward.view.removeFromSuperview() + transitionContext.completeTransition(completed) + }) + } + } +} diff --git a/mobile/ios/Client/Frontend/Browser/BackForwardListViewController.swift b/mobile/ios/Client/Frontend/Browser/BackForwardListViewController.swift new file mode 100644 index 0000000000..00a70b7ab8 --- /dev/null +++ b/mobile/ios/Client/Frontend/Browser/BackForwardListViewController.swift @@ -0,0 +1,249 @@ +/* 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 UIKit +import Shared +import WebKit +import Storage +import SnapKit + +struct BackForwardViewUX { + static let RowHeight: CGFloat = 50 + static let BackgroundColor = UIColor(rgb: 0xf9f9fa).withAlphaComponent(0.4) +} + +class BackForwardListViewController: UIViewController, UITableViewDataSource, UITableViewDelegate, UIGestureRecognizerDelegate { + + fileprivate let BackForwardListCellIdentifier = "BackForwardListViewController" + fileprivate var profile: Profile + fileprivate lazy var sites = [String: Site]() + fileprivate var dismissing = false + fileprivate var currentRow = 0 + fileprivate var verticalConstraints: [Constraint] = [] + + lazy var tableView: UITableView = { + let tableView = UITableView() + tableView.separatorStyle = .none + tableView.dataSource = self + tableView.delegate = self + tableView.alwaysBounceVertical = false + tableView.register(BackForwardTableViewCell.self, forCellReuseIdentifier: self.BackForwardListCellIdentifier) + tableView.backgroundColor = BackForwardViewUX.BackgroundColor + let blurEffect = UIBlurEffect(style: .extraLight) + let blurEffectView = UIVisualEffectView(effect: blurEffect) + tableView.backgroundView = blurEffectView + + return tableView + }() + + lazy var shadow: UIView = { + let shadow = UIView() + shadow.backgroundColor = UIColor(white: 0, alpha: 0.2) + return shadow + }() + + var tabManager: TabManager! + weak var bvc: BrowserViewController? + var currentItem: WKBackForwardListItem? + var listData = [WKBackForwardListItem]() + + var tableHeight: CGFloat { + get { + assert(Thread.isMainThread, "tableHeight interacts with UIKit components - cannot call from background thread.") + return min(BackForwardViewUX.RowHeight * CGFloat(listData.count), self.view.frame.height/2) + } + } + + var backForwardTransitionDelegate: UIViewControllerTransitioningDelegate? { + didSet { + self.transitioningDelegate = backForwardTransitionDelegate + } + } + + var snappedToBottom: Bool = true + + init(profile: Profile, backForwardList: WKBackForwardList) { + self.profile = profile + super.init(nibName: nil, bundle: nil) + + loadSites(backForwardList) + loadSitesFromProfile() + } + + override func viewDidLoad() { + super.viewDidLoad() + view.addSubview(shadow) + view.addSubview(tableView) + snappedToBottom = bvc?.toolbar != nil + tableView.snp.makeConstraints { make in + make.height.equalTo(0) + make.left.right.equalTo(self.view) + } + shadow.snp.makeConstraints { make in + make.left.right.equalTo(self.view) + } + remakeVerticalConstraints() + view.layoutIfNeeded() + scrollTableViewToIndex(currentRow) + setupDismissTap() + } + + func loadSitesFromProfile() { + let sql = profile.favicons as! SQLiteHistory + let urls = listData.flatMap {$0.url.isLocal ? $0.url.getQuery()["url"]?.unescape() : $0.url.absoluteString} + + sql.getSitesForURLs(urls).uponQueue(.main) { result in + guard let results = result.successValue else { + return + } + // Add all results into the sites dictionary + results.flatMap({$0}).forEach({site in + if let url = site?.url { + self.sites[url] = site + } + }) + self.tableView.reloadData() + } + } + + func homeAndNormalPagesOnly(_ bfList: WKBackForwardList) { + let items = bfList.forwardList.reversed() + [bfList.currentItem].flatMap({$0}) + bfList.backList.reversed() + + //error url's are OK as they are used to populate history on session restore. + listData = items.filter({return !($0.url.isLocal && ($0.url.originalURLFromErrorURL?.isLocal ?? true)) || $0.url.isAboutHomeURL}) + } + + func loadSites(_ bfList: WKBackForwardList) { + currentItem = bfList.currentItem + + homeAndNormalPagesOnly(bfList) + } + + func scrollTableViewToIndex(_ index: Int) { + guard index > 1 else { + return + } + let moveToIndexPath = IndexPath(row: index-2, section: 0) + tableView.reloadRows(at: [moveToIndexPath], with: .none) + tableView.scrollToRow(at: moveToIndexPath, at: UITableViewScrollPosition.middle, animated: false) + } + + override func willTransition(to newCollection: UITraitCollection, with coordinator: UIViewControllerTransitionCoordinator) { + super.willTransition(to: newCollection, with: coordinator) + guard let bvc = self.bvc else { + return + } + if bvc.shouldShowFooterForTraitCollection(newCollection) != snappedToBottom { + tableView.snp.updateConstraints { make in + if snappedToBottom { + make.bottom.equalTo(self.view).offset(0) + } else { + make.top.equalTo(self.view).offset(0) + } + make.height.equalTo(0) + } + snappedToBottom = !snappedToBottom + } + } + + override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { + super.viewWillTransition(to: size, with: coordinator) + let correctHeight = { + self.tableView.snp.updateConstraints { make in + make.height.equalTo(min(BackForwardViewUX.RowHeight * CGFloat(self.listData.count), size.height / 2)) + } + } + coordinator.animate(alongsideTransition: nil) { _ in + self.remakeVerticalConstraints() + correctHeight() + } + } + + func remakeVerticalConstraints() { + guard let bvc = self.bvc else { + return + } + for constraint in self.verticalConstraints { + constraint.deactivate() + } + self.verticalConstraints = [] + tableView.snp.makeConstraints { make in + if snappedToBottom { + verticalConstraints += [make.bottom.equalTo(self.view).offset(-bvc.footer.frame.height).constraint] + } else { + verticalConstraints += [make.top.equalTo(self.view).offset(bvc.header.frame.height + UIApplication.shared.statusBarFrame.size.height).constraint] + } + } + shadow.snp.makeConstraints() { make in + if snappedToBottom { + verticalConstraints += [ + make.bottom.equalTo(tableView.snp.top).constraint, + make.top.equalTo(self.view).constraint + ] + + } else { + verticalConstraints += [ + make.top.equalTo(tableView.snp.bottom).constraint, + make.bottom.equalTo(self.view).constraint + ] + } + } + } + + func setupDismissTap() { + let tap = UITapGestureRecognizer(target: self, action: #selector(BackForwardListViewController.handleTap)) + tap.cancelsTouchesInView = false + tap.delegate = self + view.addGestureRecognizer(tap) + } + + func handleTap() { + dismiss(animated: true, completion: nil) + } + + func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool { + if touch.view?.isDescendant(of: tableView) ?? true { + return false + } + return true + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: - Table view + func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return listData.count + } + + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let cell = self.tableView.dequeueReusableCell(withIdentifier: BackForwardListCellIdentifier, for: indexPath) as! BackForwardTableViewCell + let item = listData[indexPath.item] + let urlString = item.url.isLocal ? item.url.getQuery()["url"]?.unescape() : item.url.absoluteString + + cell.isCurrentTab = listData[indexPath.item] == self.currentItem + cell.connectingBackwards = indexPath.item != listData.count-1 + cell.connectingForwards = indexPath.item != 0 + + guard let url = urlString, !item.url.isAboutHomeURL else { + cell.site = Site(url: item.url.absoluteString, title: Strings.FirefoxHomePage) + return cell + } + + cell.site = sites[url] ?? Site(url: url, title: item.title ?? "") + cell.setNeedsDisplay() + + return cell + } + + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + tabManager.selectedTab?.goToBackForwardListItem(listData[indexPath.item]) + dismiss(animated: true, completion: nil) + } + + func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { + return BackForwardViewUX.RowHeight + } +} diff --git a/mobile/ios/Client/Frontend/Browser/BackForwardTableViewCell.swift b/mobile/ios/Client/Frontend/Browser/BackForwardTableViewCell.swift new file mode 100644 index 0000000000..f18d2f903e --- /dev/null +++ b/mobile/ios/Client/Frontend/Browser/BackForwardTableViewCell.swift @@ -0,0 +1,142 @@ +/* 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 UIKit +import Storage + +class BackForwardTableViewCell: UITableViewCell { + + struct BackForwardViewCellUX { + static let bgColor = UIColor.gray + static let faviconWidth = 29 + static let faviconPadding: CGFloat = 20 + static let labelPadding = 20 + static let borderSmall = 2 + static let borderBold = 5 + static let IconSize = 23 + static let fontSize: CGFloat = 12.0 + } + + lazy var faviconView: UIImageView = { + let faviconView = UIImageView(image: FaviconFetcher.defaultFavicon) + faviconView.backgroundColor = UIColor.white + faviconView.layer.cornerRadius = 6 + faviconView.layer.borderWidth = 0.5 + faviconView.layer.borderColor = UIColor(white: 0, alpha: 0.1).cgColor + faviconView.layer.masksToBounds = true + faviconView.contentMode = .center + return faviconView + }() + + lazy var label: UILabel = { + let label = UILabel() + label.text = " " + label.font = label.font.withSize(BackForwardViewCellUX.fontSize) + label.textColor = UIColor(rgb: 0x272727) + return label + }() + + var connectingForwards = true + var connectingBackwards = true + + var isCurrentTab = false { + didSet { + if isCurrentTab { + label.font = UIFont(name: "HelveticaNeue-Bold", size: BackForwardViewCellUX.fontSize) + } + } + } + + var site: Site? { + didSet { + if let s = site { + faviconView.setFavicon(forSite: s, onCompletion: { [weak self] (color, url) in + if s.tileURL.isLocal { + self?.faviconView.image = UIImage(named: "faviconFox") + self?.faviconView.image = self?.faviconView.image?.createScaled(CGSize(width: BackForwardViewCellUX.IconSize, height: BackForwardViewCellUX.IconSize)) + self?.faviconView.backgroundColor = UIColor.white + return + } + + self?.faviconView.image = self?.faviconView.image?.createScaled(CGSize(width: BackForwardViewCellUX.IconSize, height: BackForwardViewCellUX.IconSize)) + self?.faviconView.backgroundColor = color == .clear ? .white : color + }) + var title = s.title + if title.isEmpty { + title = s.url + } + label.text = title + setNeedsLayout() + } + } + } + + override init(style: UITableViewCellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + backgroundColor = UIColor.clear + selectionStyle = .none + + contentView.addSubview(faviconView) + contentView.addSubview(label) + + faviconView.snp.makeConstraints { make in + make.height.equalTo(BackForwardViewCellUX.faviconWidth) + make.width.equalTo(BackForwardViewCellUX.faviconWidth) + make.centerY.equalTo(self) + make.leading.equalTo(self.snp.leading).offset(BackForwardViewCellUX.faviconPadding) + } + + label.snp.makeConstraints { make in + make.centerY.equalTo(self) + make.leading.equalTo(faviconView.snp.trailing).offset(BackForwardViewCellUX.labelPadding) + make.trailing.equalTo(self.snp.trailing).offset(-BackForwardViewCellUX.labelPadding) + } + + } + + required init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func draw(_ rect: CGRect) { + super.draw(rect) + guard let context = UIGraphicsGetCurrentContext() else { return } + + var startPoint = CGPoint(x: rect.origin.x + BackForwardViewCellUX.faviconPadding + CGFloat(Double(BackForwardViewCellUX.faviconWidth)*0.5), + y: rect.origin.y + (connectingForwards ? 0 : rect.size.height/2)) + var endPoint = CGPoint(x: rect.origin.x + BackForwardViewCellUX.faviconPadding + CGFloat(Double(BackForwardViewCellUX.faviconWidth)*0.5), + y: rect.origin.y + rect.size.height - (connectingBackwards ? 0 : rect.size.height/2)) + + // flip the x component if RTL + if UIApplication.shared.userInterfaceLayoutDirection == .rightToLeft { + startPoint.x = rect.origin.x - startPoint.x + rect.size.width + endPoint.x = rect.origin.x - endPoint.x + rect.size.width + } + + context.saveGState() + context.setLineCap(CGLineCap.square) + context.setStrokeColor(BackForwardViewCellUX.bgColor.cgColor) + context.setLineWidth(1.0) + context.move(to: CGPoint(x: startPoint.x, y: startPoint.y)) + context.addLine(to: CGPoint(x: endPoint.x, y: endPoint.y)) + context.strokePath() + context.restoreGState() + } + + override func setHighlighted(_ highlighted: Bool, animated: Bool) { + if highlighted { + self.backgroundColor = UIColor(white: 0, alpha: 0.1) + } else { + self.backgroundColor = UIColor.clear + } + } + + override func prepareForReuse() { + super.prepareForReuse() + connectingForwards = true + connectingBackwards = true + isCurrentTab = false + label.font = UIFont(name: "HelveticaNeue", size: BackForwardViewCellUX.fontSize) + } +} diff --git a/mobile/ios/Client/Frontend/Browser/BrowserPrompts.swift b/mobile/ios/Client/Frontend/Browser/BrowserPrompts.swift new file mode 100644 index 0000000000..a627e812b5 --- /dev/null +++ b/mobile/ios/Client/Frontend/Browser/BrowserPrompts.swift @@ -0,0 +1,135 @@ +/* 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 WebKit + +@objc protocol JSPromptAlertControllerDelegate: class { + func promptAlertControllerDidDismiss(_ alertController: JSPromptAlertController) +} + +/// A simple version of UIAlertController that attaches a delegate to the viewDidDisappear method +/// to allow forwarding the event. The reason this is needed for prompts from Javascript is we +/// need to invoke the completionHandler passed to us from the WKWebView delegate or else +/// a runtime exception is thrown. +class JSPromptAlertController: UIAlertController { + var alertInfo: JSAlertInfo? + + weak var delegate: JSPromptAlertControllerDelegate? + + override func viewDidDisappear(_ animated: Bool) { + super.viewDidDisappear(animated) + delegate?.promptAlertControllerDidDismiss(self) + } +} + +/** + * An JSAlertInfo is used to store information about an alert we want to show either immediately or later. + * Since alerts are generated by web pages and have no upper limit it would be unwise to allocate a + * UIAlertController instance for each generated prompt which could potentially be queued in the background. + * Instead, the JSAlertInfo structure retains the relevant data needed for the prompt along with a copy + * of the provided completionHandler to let us generate the UIAlertController when needed. + */ +protocol JSAlertInfo { + func alertController() -> JSPromptAlertController + func cancel() +} + +struct MessageAlert: JSAlertInfo { + let message: String + let frame: WKFrameInfo + let completionHandler: () -> Void + + func alertController() -> JSPromptAlertController { + let alertController = JSPromptAlertController(title: titleForJavaScriptPanelInitiatedByFrame(frame), + message: message, + preferredStyle: UIAlertControllerStyle.alert) + alertController.addAction(UIAlertAction(title: UIConstants.OKString, style: UIAlertActionStyle.default) { _ in + self.completionHandler() + }) + alertController.alertInfo = self + return alertController + } + + func cancel() { + completionHandler() + } +} + +struct ConfirmPanelAlert: JSAlertInfo { + let message: String + let frame: WKFrameInfo + let completionHandler: (Bool) -> Void + + init(message: String, frame: WKFrameInfo, completionHandler: @escaping (Bool) -> Void) { + self.message = message + self.frame = frame + self.completionHandler = completionHandler + } + + func alertController() -> JSPromptAlertController { + // Show JavaScript confirm dialogs. + let alertController = JSPromptAlertController(title: titleForJavaScriptPanelInitiatedByFrame(frame), message: message, preferredStyle: UIAlertControllerStyle.alert) + alertController.addAction(UIAlertAction(title: UIConstants.OKString, style: UIAlertActionStyle.default) { _ in + self.completionHandler(true) + }) + alertController.addAction(UIAlertAction(title: UIConstants.CancelString, style: UIAlertActionStyle.cancel) { _ in + self.cancel() + }) + alertController.alertInfo = self + return alertController + } + + func cancel() { + completionHandler(false) + } +} + +struct TextInputAlert: JSAlertInfo { + let message: String + let frame: WKFrameInfo + let completionHandler: (String?) -> Void + let defaultText: String? + + var input: UITextField! + + init(message: String, frame: WKFrameInfo, completionHandler: @escaping (String?) -> Void, defaultText: String?) { + self.message = message + self.frame = frame + self.completionHandler = completionHandler + self.defaultText = defaultText + } + + func alertController() -> JSPromptAlertController { + let alertController = JSPromptAlertController(title: titleForJavaScriptPanelInitiatedByFrame(frame), message: message, preferredStyle: UIAlertControllerStyle.alert) + var input: UITextField! + alertController.addTextField(configurationHandler: { (textField: UITextField) in + input = textField + input.text = self.defaultText + }) + alertController.addAction(UIAlertAction(title: UIConstants.OKString, style: UIAlertActionStyle.default) { _ in + self.completionHandler(input.text) + }) + alertController.addAction(UIAlertAction(title: UIConstants.CancelString, style: UIAlertActionStyle.cancel) { _ in + self.cancel() + }) + alertController.alertInfo = self + return alertController + } + + func cancel() { + completionHandler(nil) + } +} + +/// Show a title for a JavaScript Panel (alert) based on the WKFrameInfo. On iOS9 we will use the new securityOrigin +/// and on iOS 8 we will fall back to the request URL. If the request URL is nil, which happens for JavaScript pages, +/// we fall back to "JavaScript" as a title. +private func titleForJavaScriptPanelInitiatedByFrame(_ frame: WKFrameInfo) -> String { + var title = "\(frame.securityOrigin.`protocol`)://\(frame.securityOrigin.host)" + if frame.securityOrigin.port != 0 { + title += ":\(frame.securityOrigin.port)" + } + return title +} diff --git a/mobile/ios/Client/Frontend/Browser/BrowserTrayAnimators.swift b/mobile/ios/Client/Frontend/Browser/BrowserTrayAnimators.swift new file mode 100644 index 0000000000..6c22a2228e --- /dev/null +++ b/mobile/ios/Client/Frontend/Browser/BrowserTrayAnimators.swift @@ -0,0 +1,322 @@ +/* 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 UIKit +import Shared + +class TrayToBrowserAnimator: NSObject, UIViewControllerAnimatedTransitioning { + func animateTransition(using transitionContext: UIViewControllerContextTransitioning) { + if let bvc = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.to) as? BrowserViewController, + let tabTray = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.from) as? TabTrayController { + transitionFromTray(tabTray, toBrowser: bvc, usingContext: transitionContext) + } + } + + func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval { + return 0.4 + } +} + +private extension TrayToBrowserAnimator { + func transitionFromTray(_ tabTray: TabTrayController, toBrowser bvc: BrowserViewController, usingContext transitionContext: UIViewControllerContextTransitioning) { + let container = transitionContext.containerView + guard let selectedTab = bvc.tabManager.selectedTab else { return } + + let tabManager = bvc.tabManager + let displayedTabs = selectedTab.isPrivate ? tabManager.privateTabs : tabManager.normalTabs + guard let expandFromIndex = displayedTabs.index(of: selectedTab) else { return } + + bvc.view.frame = transitionContext.finalFrame(for: bvc) + + // Hide browser components + bvc.toggleSnackBarVisibility(show: false) + toggleWebViewVisibility(false, usingTabManager: bvc.tabManager) + bvc.homePanelController?.view.isHidden = true + bvc.webViewContainerBackdrop.isHidden = true + bvc.statusBarOverlay.isHidden = false + if let url = selectedTab.url, !url.isReaderModeURL { + bvc.hideReaderModeBar(animated: false) + } + + // Take a snapshot of the collection view that we can scale/fade out. We don't need to wait for screen updates since it's already rendered on the screen + let tabCollectionViewSnapshot = tabTray.collectionView.snapshotView(afterScreenUpdates: false)! + tabTray.collectionView.alpha = 0 + tabCollectionViewSnapshot.frame = tabTray.collectionView.frame + container.insertSubview(tabCollectionViewSnapshot, at: 0) + + // Create a fake cell to use for the upscaling animation + let startingFrame = calculateCollapsedCellFrameUsingCollectionView(tabTray.collectionView, atIndex: expandFromIndex) + let cell = createTransitionCellFromTab(bvc.tabManager.selectedTab, withFrame: startingFrame) + cell.backgroundHolder.layer.cornerRadius = 0 + + container.insertSubview(bvc.view, aboveSubview: tabCollectionViewSnapshot) + container.insertSubview(cell, aboveSubview: bvc.view) + + // Flush any pending layout/animation code in preperation of the animation call + container.layoutIfNeeded() + + let finalFrame = calculateExpandedCellFrameFromBVC(bvc) + bvc.footer.alpha = shouldDisplayFooterForBVC(bvc) ? 1 : 0 + bvc.urlBar.isTransitioning = true + + // Re-calculate the starting transforms for header/footer views in case we switch orientation + resetTransformsForViews([bvc.header, bvc.readerModeBar, bvc.footer]) + transformHeaderFooterForBVC(bvc, toFrame: startingFrame, container: container) + + UIView.animate(withDuration: self.transitionDuration(using: transitionContext), + delay: 0, usingSpringWithDamping: 1, + initialSpringVelocity: 0, + options: UIViewAnimationOptions(), + animations: { + // Scale up the cell and reset the transforms for the header/footers + cell.frame = finalFrame + container.layoutIfNeeded() + cell.title.transform = CGAffineTransform(translationX: 0, y: -cell.title.frame.height) + + bvc.tabTrayDidDismiss(tabTray) + UIApplication.shared.windows.first?.backgroundColor = UIConstants.AppBackgroundColor + tabTray.navigationController?.setNeedsStatusBarAppearanceUpdate() + tabTray.toolbar.transform = CGAffineTransform(translationX: 0, y: UIConstants.BottomToolbarHeight) + tabCollectionViewSnapshot.transform = CGAffineTransform(scaleX: 0.9, y: 0.9) + tabCollectionViewSnapshot.alpha = 0 + }, completion: { finished in + // Remove any of the views we used for the animation + cell.removeFromSuperview() + tabCollectionViewSnapshot.removeFromSuperview() + bvc.footer.alpha = 1 + bvc.toggleSnackBarVisibility(show: true) + toggleWebViewVisibility(true, usingTabManager: bvc.tabManager) + bvc.webViewContainerBackdrop.isHidden = false + bvc.homePanelController?.view.isHidden = false + bvc.urlBar.isTransitioning = false + transitionContext.completeTransition(true) + }) + } +} + +class BrowserToTrayAnimator: NSObject, UIViewControllerAnimatedTransitioning { + func animateTransition(using transitionContext: UIViewControllerContextTransitioning) { + if let bvc = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.from) as? BrowserViewController, + let tabTray = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.to) as? TabTrayController { + transitionFromBrowser(bvc, toTabTray: tabTray, usingContext: transitionContext) + } + } + + func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval { + return 0.4 + } +} + +private extension BrowserToTrayAnimator { + func transitionFromBrowser(_ bvc: BrowserViewController, toTabTray tabTray: TabTrayController, usingContext transitionContext: UIViewControllerContextTransitioning) { + + let container = transitionContext.containerView + guard let selectedTab = bvc.tabManager.selectedTab else { return } + + let tabManager = bvc.tabManager + let displayedTabs = selectedTab.isPrivate ? tabManager.privateTabs : tabManager.normalTabs + guard let scrollToIndex = displayedTabs.index(of: selectedTab) else { return } + + tabTray.view.frame = transitionContext.finalFrame(for: tabTray) + + // Insert tab tray below the browser and force a layout so the collection view can get it's frame right + container.insertSubview(tabTray.view, belowSubview: bvc.view) + + // Force subview layout on the collection view so we can calculate the correct end frame for the animation + tabTray.view.layoutSubviews() + + tabTray.collectionView.scrollToItem(at: IndexPath(item: scrollToIndex, section: 0), at: .centeredVertically, animated: false) + + // Build a tab cell that we will use to animate the scaling of the browser to the tab + let expandedFrame = calculateExpandedCellFrameFromBVC(bvc) + let cell = createTransitionCellFromTab(bvc.tabManager.selectedTab, withFrame: expandedFrame) + cell.backgroundHolder.layer.cornerRadius = TabTrayControllerUX.CornerRadius + + // Take a snapshot of the collection view to perform the scaling/alpha effect + let tabCollectionViewSnapshot = tabTray.collectionView.snapshotView(afterScreenUpdates: true)! + tabCollectionViewSnapshot.frame = tabTray.collectionView.frame + tabCollectionViewSnapshot.transform = CGAffineTransform(scaleX: 0.9, y: 0.9) + tabCollectionViewSnapshot.alpha = 0 + tabTray.view.insertSubview(tabCollectionViewSnapshot, belowSubview: tabTray.toolbar) + + if let toast = bvc.clipboardBarDisplayHandler?.clipboardToast { + toast.removeFromSuperview() + } + + container.addSubview(cell) + cell.layoutIfNeeded() + cell.title.transform = CGAffineTransform(translationX: 0, y: -cell.title.frame.size.height) + + // Hide views we don't want to show during the animation in the BVC + bvc.homePanelController?.view.isHidden = true + bvc.statusBarOverlay.isHidden = true + bvc.toggleSnackBarVisibility(show: false) + toggleWebViewVisibility(false, usingTabManager: bvc.tabManager) + bvc.urlBar.isTransitioning = true + + // Since we are hiding the collection view and the snapshot API takes the snapshot after the next screen update, + // the screenshot ends up being blank unless we set the collection view hidden after the screen update happens. + // To work around this, we dispatch the setting of collection view to hidden after the screen update is completed. + + DispatchQueue.main.async { + tabTray.collectionView.isHidden = true + let finalFrame = calculateCollapsedCellFrameUsingCollectionView(tabTray.collectionView, + atIndex: scrollToIndex) + tabTray.toolbar.transform = CGAffineTransform(translationX: 0, y: UIConstants.BottomToolbarHeight) + + UIView.animate(withDuration: self.transitionDuration(using: transitionContext), + delay: 0, usingSpringWithDamping: 1, + initialSpringVelocity: 0, + options: UIViewAnimationOptions(), + animations: { + cell.frame = finalFrame + cell.title.transform = CGAffineTransform.identity + cell.layoutIfNeeded() + + UIApplication.shared.windows.first?.backgroundColor = TabTrayControllerUX.BackgroundColor + tabTray.navigationController?.setNeedsStatusBarAppearanceUpdate() + + transformHeaderFooterForBVC(bvc, toFrame: finalFrame, container: container) + + bvc.urlBar.updateAlphaForSubviews(0) + bvc.footer.alpha = 0 + tabCollectionViewSnapshot.alpha = 1 + + tabTray.toolbar.transform = CGAffineTransform.identity + resetTransformsForViews([tabCollectionViewSnapshot]) + }, completion: { finished in + // Remove any of the views we used for the animation + cell.removeFromSuperview() + tabCollectionViewSnapshot.removeFromSuperview() + tabTray.collectionView.isHidden = false + + bvc.toggleSnackBarVisibility(show: true) + toggleWebViewVisibility(true, usingTabManager: bvc.tabManager) + bvc.homePanelController?.view.isHidden = false + + bvc.urlBar.isTransitioning = false + transitionContext.completeTransition(true) + }) + } + } +} + +private func transformHeaderFooterForBVC(_ bvc: BrowserViewController, toFrame finalFrame: CGRect, container: UIView) { + let footerForTransform = footerTransform(bvc.footer.frame, toFrame: finalFrame, container: container) + let headerForTransform = headerTransform(bvc.header.frame, toFrame: finalFrame, container: container) + + bvc.footer.transform = footerForTransform + bvc.header.transform = headerForTransform + bvc.readerModeBar?.transform = headerForTransform +} + +private func footerTransform( _ frame: CGRect, toFrame finalFrame: CGRect, container: UIView) -> CGAffineTransform { + let frame = container.convert(frame, to: container) + let endY = finalFrame.maxY - (frame.size.height / 2) + let endX = finalFrame.midX + let translation = CGPoint(x: endX - frame.midX, y: endY - frame.midY) + + let scaleX = finalFrame.width / frame.width + + var transform = CGAffineTransform.identity + transform = transform.translatedBy(x: translation.x, y: translation.y) + transform = transform.scaledBy(x: scaleX, y: scaleX) + return transform +} + +private func headerTransform(_ frame: CGRect, toFrame finalFrame: CGRect, container: UIView) -> CGAffineTransform { + let frame = container.convert(frame, to: container) + let endY = finalFrame.minY + (frame.size.height / 2) + let endX = finalFrame.midX + let translation = CGPoint(x: endX - frame.midX, y: endY - frame.midY) + + let scaleX = finalFrame.width / frame.width + + var transform = CGAffineTransform.identity + transform = transform.translatedBy(x: translation.x, y: translation.y) + transform = transform.scaledBy(x: scaleX, y: scaleX) + return transform +} + +//MARK: Private Helper Methods +private func calculateCollapsedCellFrameUsingCollectionView(_ collectionView: UICollectionView, atIndex index: Int) -> CGRect { + if let attr = collectionView.collectionViewLayout.layoutAttributesForItem(at: IndexPath(item: index, section: 0)) { + return collectionView.convert(attr.frame, to: collectionView.superview) + } else { + return CGRect.zero + } +} + +private func calculateExpandedCellFrameFromBVC(_ bvc: BrowserViewController) -> CGRect { + var frame = bvc.webViewContainer.frame + + // If we're navigating to a home panel and we were expecting to show the toolbar, add more height to end frame since + // there is no toolbar for home panels + if !bvc.shouldShowFooterForTraitCollection(bvc.traitCollection) { + return frame + } else if let url = bvc.tabManager.selectedTab?.url, url.isAboutURL && bvc.toolbar == nil { + frame.size.height += UIConstants.BottomToolbarHeight + } + + return frame +} + +private func shouldDisplayFooterForBVC(_ bvc: BrowserViewController) -> Bool { + if bvc.shouldShowFooterForTraitCollection(bvc.traitCollection) { + if let url = bvc.tabManager.selectedTab?.url { + return !url.isAboutURL + } + } + return false +} + +private func toggleWebViewVisibility(_ show: Bool, usingTabManager tabManager: TabManager) { + for i in 0.. TabCell { + let cell = TabCell(frame: frame) + cell.screenshotView.image = tab?.screenshot + cell.titleText.text = tab?.displayTitle + + if let tab = tab, tab.isPrivate { + cell.style = .dark + } + + if let favIcon = tab?.displayFavicon { + cell.favicon.sd_setImage(with: URL(string: favIcon.url)!) + } else { + let defaultFavicon = UIImage(named: "defaultFavicon") + if tab?.isPrivate ?? false { + cell.favicon.image = defaultFavicon + cell.favicon.tintColor = (tab?.isPrivate ?? false) ? UIColor.white : UIColor.darkGray + } else { + cell.favicon.image = defaultFavicon + } + } + return cell +} diff --git a/mobile/ios/Client/Frontend/Browser/BrowserViewController.swift b/mobile/ios/Client/Frontend/Browser/BrowserViewController.swift new file mode 100644 index 0000000000..4f041003e2 --- /dev/null +++ b/mobile/ios/Client/Frontend/Browser/BrowserViewController.swift @@ -0,0 +1,2986 @@ +/* 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 Photos +import UIKit +import WebKit +import Shared +import Storage +import SnapKit +import XCGLogger +import Alamofire +import Account +import ReadingList +import MobileCoreServices +import SDWebImage +import SwiftyJSON +import Telemetry +import Sentry + +private let KVOs: [KVOConstants] = [ + .estimatedProgress, + .loading, + .canGoBack, + .canGoForward, + .URL, + .title, +] + +private let ActionSheetTitleMaxLength = 120 + +private struct BrowserViewControllerUX { + fileprivate static let BackgroundColor = UIConstants.AppBackgroundColor + fileprivate static let ShowHeaderTapAreaHeight: CGFloat = 32 + fileprivate static let BookmarkStarAnimationDuration: Double = 0.5 + fileprivate static let BookmarkStarAnimationOffset: CGFloat = 80 +} + +class BrowserViewController: UIViewController { + var homePanelController: HomePanelViewController? + var webViewContainer: UIView! + var urlBar: URLBarView! + var clipboardBarDisplayHandler: ClipboardBarDisplayHandler? + var readerModeBar: ReaderModeBarView? + var readerModeCache: ReaderModeCache + let webViewContainerToolbar = UIView() + var statusBarOverlay: UIView! + fileprivate(set) var toolbar: TabToolbar? + fileprivate var searchController: SearchViewController? + fileprivate var screenshotHelper: ScreenshotHelper! + fileprivate var homePanelIsInline = false + fileprivate var searchLoader: SearchLoader? + fileprivate let snackBars = UIView() + fileprivate var findInPageBar: FindInPageBar? + fileprivate let findInPageContainer = UIView() + + lazy var mailtoLinkHandler: MailtoLinkHandler = MailtoLinkHandler() + + lazy fileprivate var customSearchEngineButton: UIButton = { + let searchButton = UIButton() + searchButton.setImage(UIImage(named: "AddSearch")?.withRenderingMode(.alwaysTemplate), for: UIControlState()) + searchButton.addTarget(self, action: #selector(BrowserViewController.addCustomSearchEngineForFocusedElement), for: .touchUpInside) + searchButton.accessibilityIdentifier = "BrowserViewController.customSearchEngineButton" + return searchButton + }() + + fileprivate var customSearchBarButton: UIBarButtonItem? + + // popover rotation handling + fileprivate var displayedPopoverController: UIViewController? + fileprivate var updateDisplayedPopoverProperties: (() -> Void)? + + var openInHelper: OpenInHelper? + + // location label actions + fileprivate var pasteGoAction: AccessibleAction! + fileprivate var pasteAction: AccessibleAction! + fileprivate var copyAddressAction: AccessibleAction! + + fileprivate weak var tabTrayController: TabTrayController! + let profile: Profile + let tabManager: TabManager + + // These views wrap the urlbar and toolbar to provide background effects on them + var header: UIView! + var footer: UIView! + fileprivate var topTouchArea: UIButton! + let urlBarTopTabsContainer = UIView(frame: CGRect.zero) + var topTabsVisible: Bool { + return topTabsViewController != nil + } + // Backdrop used for displaying greyed background for private tabs + var webViewContainerBackdrop: UIView! + + var scrollController = TabScrollingController() + + fileprivate var keyboardState: KeyboardState? + + var didRemoveAllTabs: Bool = false + var pendingToast: ButtonToast? // A toast that might be waiting for BVC to appear before displaying + + let WhiteListedUrls = ["\\/\\/itunes\\.apple\\.com\\/"] + + // Tracking navigation items to record history types. + // TODO: weak references? + var ignoredNavigation = Set() + var typedNavigation = [WKNavigation: VisitType]() + var navigationToolbar: TabToolbarProtocol { + return toolbar ?? urlBar + } + + var topTabsViewController: TopTabsViewController? + let topTabsContainer = UIView(frame: CGRect.zero) + + init(profile: Profile, tabManager: TabManager) { + self.profile = profile + self.tabManager = tabManager + self.readerModeCache = DiskReaderModeCache.sharedInstance + super.init(nibName: nil, bundle: nil) + didInit() + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override var supportedInterfaceOrientations: UIInterfaceOrientationMask { + if UIDevice.current.userInterfaceIdiom == .phone { + return UIInterfaceOrientationMask.allButUpsideDown + } else { + return UIInterfaceOrientationMask.all + } + } + + override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { + super.viewWillTransition(to: size, with: coordinator) + + displayedPopoverController?.dismiss(animated: true) { + self.displayedPopoverController = nil + } + + if let _ = self.presentedViewController as? PhotonActionSheet { + self.presentedViewController?.dismiss(animated: true, completion: nil) + } + + coordinator.animate(alongsideTransition: { context in + self.scrollController.updateMinimumZoom() + self.topTabsViewController?.scrollToCurrentTab(false, centerCell: false) + if let popover = self.displayedPopoverController { + self.updateDisplayedPopoverProperties?() + self.present(popover, animated: true, completion: nil) + } + }, completion: { _ in + self.scrollController.setMinimumZoom() + }) + } + + override func didReceiveMemoryWarning() { + super.didReceiveMemoryWarning() + } + + fileprivate func didInit() { + screenshotHelper = ScreenshotHelper(controller: self) + tabManager.addDelegate(self) + tabManager.addNavigationDelegate(self) + } + + override var preferredStatusBarStyle: UIStatusBarStyle { + let isPrivate = tabManager.selectedTab?.isPrivate ?? false + let isIpad = shouldShowTopTabsForTraitCollection(traitCollection) + return (isPrivate || isIpad) ? UIStatusBarStyle.lightContent : UIStatusBarStyle.default + } + + func shouldShowFooterForTraitCollection(_ previousTraitCollection: UITraitCollection) -> Bool { + return previousTraitCollection.verticalSizeClass != .compact && previousTraitCollection.horizontalSizeClass != .regular + } + + func shouldShowTopTabsForTraitCollection(_ newTraitCollection: UITraitCollection) -> Bool { + return newTraitCollection.verticalSizeClass == .regular && newTraitCollection.horizontalSizeClass == .regular + } + + func toggleSnackBarVisibility(show: Bool) { + if show { + UIView.animate(withDuration: 0.1, animations: { self.snackBars.isHidden = false }) + } else { + snackBars.isHidden = true + } + } + + fileprivate func updateToolbarStateForTraitCollection(_ newCollection: UITraitCollection, withTransitionCoordinator coordinator: UIViewControllerTransitionCoordinator? = nil) { + let showToolbar = shouldShowFooterForTraitCollection(newCollection) + let showTopTabs = shouldShowTopTabsForTraitCollection(newCollection) + + urlBar.topTabsIsShowing = showTopTabs + urlBar.setShowToolbar(!showToolbar) + toolbar?.removeFromSuperview() + toolbar?.tabToolbarDelegate = nil + toolbar = nil + + if showToolbar { + toolbar = TabToolbar() + footer.addSubview(toolbar!) + toolbar?.tabToolbarDelegate = self + let theme = (tabManager.selectedTab?.isPrivate ?? false) ? Theme.PrivateMode : Theme.NormalMode + toolbar?.applyTheme(theme) + updateTabCountUsingTabManager(self.tabManager) + } + + if showTopTabs { + if topTabsViewController == nil { + let topTabsViewController = TopTabsViewController(tabManager: tabManager) + topTabsViewController.delegate = self + addChildViewController(topTabsViewController) + topTabsViewController.view.frame = topTabsContainer.frame + topTabsContainer.addSubview(topTabsViewController.view) + topTabsViewController.view.snp.makeConstraints { make in + make.edges.equalTo(topTabsContainer) + make.height.equalTo(TopTabsUX.TopTabsViewHeight) + } + self.topTabsViewController = topTabsViewController + } + topTabsContainer.snp.updateConstraints { make in + make.height.equalTo(TopTabsUX.TopTabsViewHeight) + } + } else { + topTabsContainer.snp.updateConstraints { make in + make.height.equalTo(0) + } + topTabsViewController?.view.removeFromSuperview() + topTabsViewController?.removeFromParentViewController() + topTabsViewController = nil + } + + view.setNeedsUpdateConstraints() + if let home = homePanelController { + home.view.setNeedsUpdateConstraints() + } + + if let tab = tabManager.selectedTab, + let webView = tab.webView { + updateURLBarDisplayURL(tab) + navigationToolbar.updateBackStatus(webView.canGoBack) + navigationToolbar.updateForwardStatus(webView.canGoForward) + navigationToolbar.updateReloadStatus(tab.loading) + } + } + + override func willTransition(to newCollection: UITraitCollection, with coordinator: UIViewControllerTransitionCoordinator) { + super.willTransition(to: newCollection, with: coordinator) + + // During split screen launching on iPad, this callback gets fired before viewDidLoad gets a chance to + // set things up. Make sure to only update the toolbar state if the view is ready for it. + if isViewLoaded { + updateToolbarStateForTraitCollection(newCollection, withTransitionCoordinator: coordinator) + } + + displayedPopoverController?.dismiss(animated: true, completion: nil) + coordinator.animate(alongsideTransition: { context in + self.scrollController.showToolbars(animated: false) + if self.isViewLoaded { + self.statusBarOverlay.backgroundColor = self.shouldShowTopTabsForTraitCollection(self.traitCollection) ? UIColor(rgb: 0x272727) : self.urlBar.backgroundColor + self.setNeedsStatusBarAppearanceUpdate() + } + }, completion: nil) + } + + func SELappDidEnterBackgroundNotification() { + displayedPopoverController?.dismiss(animated: false) { + self.displayedPopoverController = nil + } + } + + func SELtappedTopArea() { + scrollController.showToolbars(animated: true) + } + + func SELappWillResignActiveNotification() { + // Dismiss any popovers that might be visible + displayedPopoverController?.dismiss(animated: false) { + self.displayedPopoverController = nil + } + + // If we are displying a private tab, hide any elements in the tab that we wouldn't want shown + // when the app is in the home switcher + guard let privateTab = tabManager.selectedTab, privateTab.isPrivate else { + return + } + + webViewContainerBackdrop.alpha = 1 + webViewContainer.alpha = 0 + urlBar.locationContainer.alpha = 0 + topTabsViewController?.switchForegroundStatus(isInForeground: false) + presentedViewController?.popoverPresentationController?.containerView?.alpha = 0 + presentedViewController?.view.alpha = 0 + } + + func SELappDidBecomeActiveNotification() { + // Re-show any components that might have been hidden because they were being displayed + // as part of a private mode tab + UIView.animate(withDuration: 0.2, delay: 0, options: UIViewAnimationOptions(), animations: { + self.webViewContainer.alpha = 1 + self.urlBar.locationContainer.alpha = 1 + self.topTabsViewController?.switchForegroundStatus(isInForeground: true) + self.presentedViewController?.popoverPresentationController?.containerView?.alpha = 1 + self.presentedViewController?.view.alpha = 1 + self.view.backgroundColor = UIColor.clear + }, completion: { _ in + self.webViewContainerBackdrop.alpha = 0 + }) + + // Re-show toolbar which might have been hidden during scrolling (prior to app moving into the background) + scrollController.showToolbars(animated: false) + } + + override func viewDidLoad() { + super.viewDidLoad() + NotificationCenter.default.addObserver(self, selector: #selector(BrowserViewController.SELBookmarkStatusDidChange(_:)), name: NSNotification.Name(rawValue: BookmarkStatusChangedNotification), object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(BrowserViewController.SELappWillResignActiveNotification), name: NSNotification.Name.UIApplicationWillResignActive, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(BrowserViewController.SELappDidBecomeActiveNotification), name: NSNotification.Name.UIApplicationDidBecomeActive, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(BrowserViewController.SELappDidEnterBackgroundNotification), name: NSNotification.Name.UIApplicationDidEnterBackground, object: nil) + KeyboardHelper.defaultHelper.addDelegate(self) + + webViewContainerBackdrop = UIView() + webViewContainerBackdrop.backgroundColor = UIColor.gray + webViewContainerBackdrop.alpha = 0 + view.addSubview(webViewContainerBackdrop) + + webViewContainer = UIView() + webViewContainer.addSubview(webViewContainerToolbar) + view.addSubview(webViewContainer) + + // Temporary work around for covering the non-clipped web view content + statusBarOverlay = UIView() + view.addSubview(statusBarOverlay) + + topTouchArea = UIButton() + topTouchArea.isAccessibilityElement = false + topTouchArea.addTarget(self, action: #selector(BrowserViewController.SELtappedTopArea), for: UIControlEvents.touchUpInside) + view.addSubview(topTouchArea) + + // Setup the URL bar, wrapped in a view to get transparency effect + urlBar = URLBarView() + urlBar.translatesAutoresizingMaskIntoConstraints = false + urlBar.delegate = self + urlBar.tabToolbarDelegate = self + header = urlBarTopTabsContainer + urlBarTopTabsContainer.addSubview(urlBar) + urlBarTopTabsContainer.addSubview(topTabsContainer) + view.addSubview(header) + + // UIAccessibilityCustomAction subclass holding an AccessibleAction instance does not work, thus unable to generate AccessibleActions and UIAccessibilityCustomActions "on-demand" and need to make them "persistent" e.g. by being stored in BVC + pasteGoAction = AccessibleAction(name: NSLocalizedString("Paste & Go", comment: "Paste the URL into the location bar and visit"), handler: { () -> Bool in + if let pasteboardContents = UIPasteboard.general.string { + self.urlBar(self.urlBar, didSubmitText: pasteboardContents) + return true + } + return false + }) + pasteAction = AccessibleAction(name: NSLocalizedString("Paste", comment: "Paste the URL into the location bar"), handler: { () -> Bool in + if let pasteboardContents = UIPasteboard.general.string { + // Enter overlay mode and make the search controller appear. + self.urlBar.enterOverlayMode(pasteboardContents, pasted: true, search: true) + + return true + } + return false + }) + copyAddressAction = AccessibleAction(name: NSLocalizedString("Copy Address", comment: "Copy the URL from the location bar"), handler: { () -> Bool in + if let url = self.urlBar.currentURL { + UIPasteboard.general.url = url as URL + } + return true + }) + + searchLoader = SearchLoader(profile: profile, urlBar: urlBar) + + footer = UIView() + self.view.addSubview(footer) + self.view.addSubview(snackBars) + snackBars.backgroundColor = UIColor.clear + self.view.addSubview(findInPageContainer) + + if AppConstants.MOZ_CLIPBOARD_BAR { + clipboardBarDisplayHandler = ClipboardBarDisplayHandler(prefs: profile.prefs, tabManager: tabManager) + clipboardBarDisplayHandler?.delegate = self + } + + scrollController.urlBar = urlBar + scrollController.header = header + scrollController.footer = footer + scrollController.snackBars = snackBars + scrollController.webViewContainerToolbar = webViewContainerToolbar + + self.updateToolbarStateForTraitCollection(self.traitCollection) + + setupConstraints() + } + + fileprivate func setupConstraints() { + topTabsContainer.snp.makeConstraints { make in + make.leading.trailing.equalTo(self.header) + make.top.equalTo(urlBarTopTabsContainer) + } + + urlBar.snp.makeConstraints { make in + make.leading.trailing.bottom.equalTo(urlBarTopTabsContainer) + make.height.equalTo(UIConstants.TopToolbarHeight) + make.top.equalTo(topTabsContainer.snp.bottom) + } + + header.snp.makeConstraints { make in + scrollController.headerTopConstraint = make.top.equalTo(self.topLayoutGuide.snp.bottom).constraint + make.left.right.equalTo(self.view) + } + + webViewContainerBackdrop.snp.makeConstraints { make in + make.edges.equalTo(webViewContainer) + } + + webViewContainerToolbar.snp.makeConstraints { make in + make.left.right.top.equalTo(webViewContainer) + make.height.equalTo(0) + } + } + + override func viewDidLayoutSubviews() { + super.viewDidLayoutSubviews() + statusBarOverlay.snp.remakeConstraints { make in + make.top.left.right.equalTo(self.view) + make.height.equalTo(self.topLayoutGuide.length) + } + } + + override var canBecomeFirstResponder: Bool { + return true + } + + override func becomeFirstResponder() -> Bool { + // Make the web view the first responder so that it can show the selection menu. + return tabManager.selectedTab?.webView?.becomeFirstResponder() ?? false + } + + func loadQueuedTabs(receivedURLs: [URL]? = nil) { + // Chain off of a trivial deferred in order to run on the background queue. + succeed().upon() { res in + self.dequeueQueuedTabs(receivedURLs: receivedURLs ?? []) + } + } + + fileprivate func dequeueQueuedTabs(receivedURLs: [URL]) { + assert(!Thread.current.isMainThread, "This must be called in the background.") + self.profile.queue.getQueuedTabs() >>== { cursor in + + // This assumes that the DB returns rows in some kind of sane order. + // It does in practice, so WFM. + if cursor.count > 0 { + + // Filter out any tabs received by a push notification to prevent dupes. + let urls = cursor.flatMap { $0?.url.asURL }.filter { !receivedURLs.contains($0) } + if !urls.isEmpty { + DispatchQueue.main.async { + self.tabManager.addTabsForURLs(urls, zombie: false) + } + } + + // Clear *after* making an attempt to open. We're making a bet that + // it's better to run the risk of perhaps opening twice on a crash, + // rather than losing data. + self.profile.queue.clearQueuedTabs() + } + + // Then, open any received URLs from push notifications. + if !receivedURLs.isEmpty { + DispatchQueue.main.async { + let unopenedReceivedURLs = receivedURLs.filter { self.tabManager.getTabForURL($0) == nil } + + self.tabManager.addTabsForURLs(unopenedReceivedURLs, zombie: false) + + if let lastURL = receivedURLs.last, let tab = self.tabManager.getTabForURL(lastURL) { + self.tabManager.selectTab(tab) + } + } + } + } + } + + // Because crashedLastLaunch is sticky, it does not get reset, we need to remember its + // value so that we do not keep asking the user to restore their tabs. + var displayedRestoreTabsAlert = false + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + + // On iPhone, if we are about to show the On-Boarding, blank out the tab so that it does + // not flash before we present. This change of alpha also participates in the animation when + // the intro view is dismissed. + if UIDevice.current.userInterfaceIdiom == .phone { + self.view.alpha = (profile.prefs.intForKey(IntroViewControllerSeenProfileKey) != nil) ? 1.0 : 0.0 + } + + if !displayedRestoreTabsAlert && !cleanlyBackgrounded() && crashedLastLaunch() { + displayedRestoreTabsAlert = true + showRestoreTabsAlert() + } else { + tabManager.restoreTabs() + } + + updateTabCountUsingTabManager(tabManager, animated: false) + clipboardBarDisplayHandler?.checkIfShouldDisplayBar() + } + + fileprivate func crashedLastLaunch() -> Bool { + return Sentry.crashedLastLaunch + } + + fileprivate func cleanlyBackgrounded() -> Bool { + guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else { + return false + } + return appDelegate.applicationCleanlyBackgrounded + } + + fileprivate func showRestoreTabsAlert() { + if !canRestoreTabs() { + self.tabManager.addTabAndSelect() + return + } + + let alert = UIAlertController.restoreTabsAlert( + okayCallback: { _ in + self.tabManager.restoreTabs() + self.updateTabCountUsingTabManager(self.tabManager, animated: false) + }, + noCallback: { _ in + self.tabManager.addTabAndSelect() + self.updateTabCountUsingTabManager(self.tabManager, animated: false) + } + ) + + self.present(alert, animated: true, completion: nil) + } + + fileprivate func canRestoreTabs() -> Bool { + guard let tabsToRestore = TabManager.tabsToRestore() else { return false } + return tabsToRestore.count > 0 + } + + override func viewDidAppear(_ animated: Bool) { + presentIntroViewController() + self.webViewContainerToolbar.isHidden = false + + screenshotHelper.viewIsVisible = true + screenshotHelper.takePendingScreenshots(tabManager.tabs) + + super.viewDidAppear(animated) + + if shouldShowWhatsNewTab() { + // Only display if the SUMO topic has been configured in the Info.plist (present and not empty) + if let whatsNewTopic = AppInfo.whatsNewTopic, whatsNewTopic != "" { + if let whatsNewURL = SupportUtils.URLForTopic(whatsNewTopic) { + self.openURLInNewTab(whatsNewURL, isPrivileged: false) + profile.prefs.setString(AppInfo.appVersion, forKey: LatestAppVersionProfileKey) + } + } + } + + if let toast = self.pendingToast { + self.pendingToast = nil + show(buttonToast: toast, afterWaiting: ButtonToastUX.ToastDelay) + } + showQueuedAlertIfAvailable() + } + + // THe logic for shouldShowWhatsNewTab is as follows: If we do not have the LatestAppVersionProfileKey in + // the profile, that means that this is a fresh install and we do not show the What's New. If we do have + // that value, we compare it to the major version of the running app. If it is different then this is an + // upgrade, downgrades are not possible, so we can show the What's New page. + + fileprivate func shouldShowWhatsNewTab() -> Bool { + guard let latestMajorAppVersion = profile.prefs.stringForKey(LatestAppVersionProfileKey)?.components(separatedBy: ".").first else { + return false // Clean install, never show What's New + } + + return latestMajorAppVersion != AppInfo.majorAppVersion && DeviceInfo.hasConnectivity() + } + + fileprivate func showQueuedAlertIfAvailable() { + if let queuedAlertInfo = tabManager.selectedTab?.dequeueJavascriptAlertPrompt() { + let alertController = queuedAlertInfo.alertController() + alertController.delegate = self + present(alertController, animated: true, completion: nil) + } + } + + override func viewWillDisappear(_ animated: Bool) { + screenshotHelper.viewIsVisible = false + super.viewWillDisappear(animated) + } + + override func viewDidDisappear(_ animated: Bool) { + super.viewDidDisappear(animated) + } + + func resetBrowserChrome() { + // animate and reset transform for tab chrome + urlBar.updateAlphaForSubviews(1) + footer.alpha = 1 + + [header, footer, readerModeBar].forEach { view in + view?.transform = CGAffineTransform.identity + } + statusBarOverlay.isHidden = false + } + + override func updateViewConstraints() { + super.updateViewConstraints() + + topTouchArea.snp.remakeConstraints { make in + make.top.left.right.equalTo(self.view) + make.height.equalTo(BrowserViewControllerUX.ShowHeaderTapAreaHeight) + } + + readerModeBar?.snp.remakeConstraints { make in + make.top.equalTo(self.header.snp.bottom) + make.height.equalTo(UIConstants.ToolbarHeight) + make.leading.trailing.equalTo(self.view) + } + + webViewContainer.snp.remakeConstraints { make in + make.left.right.equalTo(self.view) + + if let readerModeBarBottom = readerModeBar?.snp.bottom { + make.top.equalTo(readerModeBarBottom) + } else { + make.top.equalTo(self.header.snp.bottom) + } + + let findInPageHeight = (findInPageBar == nil) ? 0 : UIConstants.ToolbarHeight + if let toolbar = self.toolbar { + make.bottom.equalTo(toolbar.snp.top).offset(-findInPageHeight) + } else { + make.bottom.equalTo(self.view).offset(-findInPageHeight) + } + } + + // Setup the bottom toolbar + toolbar?.snp.remakeConstraints { make in + make.edges.equalTo(self.footer) + make.height.equalTo(UIConstants.BottomToolbarHeight) + } + + footer.snp.remakeConstraints { make in + scrollController.footerBottomConstraint = make.bottom.equalTo(self.view.snp.bottom).constraint + make.leading.trailing.equalTo(self.view) + } + + updateSnackBarConstraints() + urlBar.setNeedsUpdateConstraints() + + // Remake constraints even if we're already showing the home controller. + // The home controller may change sizes if we tap the URL bar while on about:home. + homePanelController?.view.snp.remakeConstraints { make in + make.top.equalTo(self.urlBar.snp.bottom) + make.left.right.equalTo(self.view) + if self.homePanelIsInline { + make.bottom.equalTo(self.toolbar?.snp.top ?? self.view.snp.bottom) + } else { + make.bottom.equalTo(self.view.snp.bottom) + } + } + + findInPageContainer.snp.remakeConstraints { make in + make.left.right.equalTo(self.view) + + if let keyboardHeight = keyboardState?.intersectionHeightForView(self.view), keyboardHeight > 0 { + make.bottom.equalTo(self.view).offset(-keyboardHeight) + } else if let toolbar = self.toolbar { + make.bottom.equalTo(toolbar.snp.top) + } else { + make.bottom.equalTo(self.view) + } + } + } + + fileprivate func showHomePanelController(inline: Bool) { + homePanelIsInline = inline + + if homePanelController == nil { + let homePanelController = HomePanelViewController() + homePanelController.profile = profile + homePanelController.delegate = self + homePanelController.url = tabManager.selectedTab?.url?.displayURL + homePanelController.view.alpha = 0 + self.homePanelController = homePanelController + + addChildViewController(homePanelController) + view.addSubview(homePanelController.view) + homePanelController.didMove(toParentViewController: self) + } + guard let homePanelController = self.homePanelController else { + assertionFailure("homePanelController is still nil after assignment.") + return + } + let isPrivate = tabManager.selectedTab?.isPrivate ?? false + homePanelController.applyTheme(isPrivate ? Theme.PrivateMode : Theme.NormalMode) + let panelNumber = tabManager.selectedTab?.url?.fragment + + // splitting this out to see if we can get better crash reports when this has a problem + var newSelectedButtonIndex = 0 + if let numberArray = panelNumber?.components(separatedBy: "=") { + if let last = numberArray.last, let lastInt = Int(last) { + newSelectedButtonIndex = lastInt + } + } + homePanelController.selectedPanel = HomePanelType(rawValue: newSelectedButtonIndex) + + // We have to run this animation, even if the view is already showing because there may be a hide animation running + // and we want to be sure to override its results. + UIView.animate(withDuration: 0.2, animations: { () -> Void in + homePanelController.view.alpha = 1 + }, completion: { finished in + if finished { + self.webViewContainer.accessibilityElementsHidden = true + UIAccessibilityPostNotification(UIAccessibilityScreenChangedNotification, nil) + } + }) + view.setNeedsUpdateConstraints() + } + + fileprivate func hideHomePanelController() { + if let controller = homePanelController { + self.homePanelController = nil + UIView.animate(withDuration: 0.2, delay: 0, options: .beginFromCurrentState, animations: { () -> Void in + controller.view.alpha = 0 + }, completion: { _ in + controller.willMove(toParentViewController: nil) + controller.view.removeFromSuperview() + controller.removeFromParentViewController() + self.webViewContainer.accessibilityElementsHidden = false + UIAccessibilityPostNotification(UIAccessibilityScreenChangedNotification, nil) + + // Refresh the reading view toolbar since the article record may have changed + if let readerMode = self.tabManager.selectedTab?.getContentScript(name: ReaderMode.name()) as? ReaderMode, readerMode.state == .active { + self.showReaderModeBar(animated: false) + } + }) + } + } + + fileprivate func updateInContentHomePanel(_ url: URL?) { + if !urlBar.inOverlayMode { + guard let url = url else { + hideHomePanelController() + return + } + if url.isAboutHomeURL { + showHomePanelController(inline: true) + } else if !url.isLocalUtility || url.isReaderModeURL { + hideHomePanelController() + } + } + } + + fileprivate func showSearchController() { + if searchController != nil { + return + } + + let isPrivate = tabManager.selectedTab?.isPrivate ?? false + searchController = SearchViewController(isPrivate: isPrivate) + searchController!.searchEngines = profile.searchEngines + searchController!.searchDelegate = self + searchController!.profile = self.profile + + searchLoader = SearchLoader(profile: profile, urlBar: urlBar) + searchLoader?.addListener(searchController!) + + addChildViewController(searchController!) + view.addSubview(searchController!.view) + searchController!.view.snp.makeConstraints { make in + make.top.equalTo(self.urlBar.snp.bottom) + make.left.right.bottom.equalTo(self.view) + return + } + + homePanelController?.view?.isHidden = true + + searchController!.didMove(toParentViewController: self) + } + + fileprivate func hideSearchController() { + if let searchController = searchController { + searchController.willMove(toParentViewController: nil) + searchController.view.removeFromSuperview() + searchController.removeFromParentViewController() + self.searchController = nil + homePanelController?.view?.isHidden = false + searchLoader = nil + } + } + + fileprivate func finishEditingAndSubmit(_ url: URL, visitType: VisitType) { + urlBar.currentURL = url + urlBar.leaveOverlayMode() + + guard let tab = tabManager.selectedTab else { + return + } + + if let webView = tab.webView { + resetSpoofedUserAgentIfRequired(webView, newURL: url) + } + + if let nav = tab.loadRequest(PrivilegedRequest(url: url) as URLRequest) { + self.recordNavigationInTab(tab, navigation: nav, visitType: visitType) + } + } + + func addBookmark(_ tabState: TabState) { + guard let url = tabState.url else { return } + let absoluteString = url.absoluteString + let shareItem = ShareItem(url: absoluteString, title: tabState.title, favicon: tabState.favicon) + _ = profile.bookmarks.shareItem(shareItem) + var userData = [QuickActions.TabURLKey: shareItem.url] + if let title = shareItem.title { + userData[QuickActions.TabTitleKey] = title + } + QuickActions.sharedInstance.addDynamicApplicationShortcutItemOfType(.openLastBookmark, + withUserData: userData, + toApplication: UIApplication.shared) + if let tab = tabManager.getTabForURL(url) { + tab.isBookmarked = true + } + } + + func SELBookmarkStatusDidChange(_ notification: Notification) { + if let bookmark = notification.object as? BookmarkItem { + if bookmark.url == urlBar.currentURL?.absoluteString { + if let userInfo = notification.userInfo as? Dictionary { + if userInfo["added"] != nil { + if let tab = self.tabManager.getTabForURL(urlBar.currentURL!) { + tab.isBookmarked = false + } + } + } + } + } + } + + override func accessibilityPerformEscape() -> Bool { + if urlBar.inOverlayMode { + urlBar.SELdidClickCancel() + return true + } else if let selectedTab = tabManager.selectedTab, selectedTab.canGoBack { + selectedTab.goBack() + return true + } + return false + } + + override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey: Any]?, context: UnsafeMutableRawPointer?) { + let webView = object as! WKWebView + guard let kp = keyPath, let path = KVOConstants(rawValue: kp) else { + assertionFailure("Unhandled KVO key: \(keyPath ?? "nil")") + return + } + + switch path { + case .estimatedProgress: + guard webView == tabManager.selectedTab?.webView, + let progress = change?[NSKeyValueChangeKey.newKey] as? Float else { break } + if !(webView.url?.isLocalUtility ?? false) { + urlBar.updateProgressBar(progress) + } else { + urlBar.hideProgressBar() + } + case .loading: + guard let loading = change?[NSKeyValueChangeKey.newKey] as? Bool else { break } + + if webView == tabManager.selectedTab?.webView { + navigationToolbar.updateReloadStatus(loading) + } + + if !loading { + runScriptsOnWebView(webView) + } + case .URL: + guard let tab = tabManager[webView] else { break } + + // To prevent spoofing, only change the URL immediately if the new URL is on + // the same origin as the current URL. Otherwise, do nothing and wait for + // didCommitNavigation to confirm the page load. + if tab.url?.origin == webView.url?.origin { + tab.url = webView.url + + if tab === tabManager.selectedTab && !tab.restoring { + updateUIForReaderHomeStateForTab(tab) + } + } + case .title: + guard let tab = tabManager[webView] else { break } + + // Ensure that the tab title *actually* changed to prevent repeated calls + // to navigateInTab(tab:). + guard let title = tab.title else { break } + if !title.isEmpty && title != tab.lastTitle { + navigateInTab(tab: tab) + } + case .canGoBack: + guard webView == tabManager.selectedTab?.webView, + let canGoBack = change?[NSKeyValueChangeKey.newKey] as? Bool else { break } + + navigationToolbar.updateBackStatus(canGoBack) + case .canGoForward: + guard webView == tabManager.selectedTab?.webView, + let canGoForward = change?[NSKeyValueChangeKey.newKey] as? Bool else { break } + + navigationToolbar.updateForwardStatus(canGoForward) + default: + assertionFailure("Unhandled KVO key: \(keyPath ?? "nil")") + } + } + + fileprivate func runScriptsOnWebView(_ webView: WKWebView) { + guard let url = webView.url, url.isWebPage(), !url.isLocal else { + return + } + webView.evaluateJavaScript("__firefox__.favicons.getFavicons()", completionHandler: nil) + webView.evaluateJavaScript("__firefox__.metadata.extractMetadata()", completionHandler: nil) + + if #available(iOS 11, *) { + if NoImageModeHelper.isActivated(profile.prefs) { + webView.evaluateJavaScript("__firefox__.NoImageMode.setEnabled(true)", completionHandler: nil) + } + } + } + + func updateUIForReaderHomeStateForTab(_ tab: Tab) { + updateURLBarDisplayURL(tab) + scrollController.showToolbars(animated: false) + + if let url = tab.url { + if url.isReaderModeURL { + showReaderModeBar(animated: false) + NotificationCenter.default.addObserver(self, selector: #selector(BrowserViewController.SELDynamicFontChanged(_:)), name: NotificationDynamicFontChanged, object: nil) + } else { + hideReaderModeBar(animated: false) + NotificationCenter.default.removeObserver(self, name: NotificationDynamicFontChanged, object: nil) + } + + updateInContentHomePanel(url as URL) + } + } + + fileprivate func isWhitelistedUrl(_ url: URL) -> Bool { + for entry in WhiteListedUrls { + if let _ = url.absoluteString.range(of: entry, options: .regularExpression) { + return UIApplication.shared.canOpenURL(url) + } + } + return false + } + + /// Updates the URL bar text and button states. + /// Call this whenever the page URL changes. + fileprivate func updateURLBarDisplayURL(_ tab: Tab) { + urlBar.currentURL = tab.url?.displayURL + + let isPage = tab.url?.displayURL?.isWebPage() ?? false + navigationToolbar.updatePageStatus(isPage) + + guard let url = tab.url?.displayURL?.absoluteString else { + return + } + + profile.bookmarks.modelFactory >>== { + $0.isBookmarked(url).uponQueue(DispatchQueue.main) { [weak tab] result in + guard let bookmarked = result.successValue else { + print("Error getting bookmark status: \(result.failureValue ??? "nil").") + return + } + tab?.isBookmarked = bookmarked + } + } + } + + // MARK: Opening New Tabs + + func switchToPrivacyMode(isPrivate: Bool ) { +// applyTheme(isPrivate ? Theme.PrivateMode : Theme.NormalMode) + + let tabTrayController = self.tabTrayController ?? TabTrayController(tabManager: tabManager, profile: profile, tabTrayDelegate: self) + if tabTrayController.privateMode != isPrivate { + tabTrayController.changePrivacyMode(isPrivate) + } + self.tabTrayController = tabTrayController + } + + func switchToTabForURLOrOpen(_ url: URL, isPrivate: Bool = false, isPrivileged: Bool) { + popToBVC() + if let tab = tabManager.getTabForURL(url) { + tabManager.selectTab(tab) + } else { + openURLInNewTab(url, isPrivate: isPrivate, isPrivileged: isPrivileged) + } + } + + func openURLInNewTab(_ url: URL?, isPrivate: Bool = false, isPrivileged: Bool) { + if let selectedTab = tabManager.selectedTab { + screenshotHelper.takeScreenshot(selectedTab) + } + let request: URLRequest? + if let url = url { + request = isPrivileged ? PrivilegedRequest(url: url) as URLRequest : URLRequest(url: url) + } else { + request = nil + } + + switchToPrivacyMode(isPrivate: isPrivate) + _ = tabManager.addTabAndSelect(request, isPrivate: isPrivate) + if url == nil && NewTabAccessors.getNewTabPage(profile.prefs) == .blankPage { + urlBar.tabLocationViewDidTapLocation(urlBar.locationView) + } + } + + func openBlankNewTab(focusLocationField: Bool, isPrivate: Bool = false) { + popToBVC() + openURLInNewTab(nil, isPrivate: isPrivate, isPrivileged: true) + + if focusLocationField { + DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(300)) { + // Without a delay, the text field fails to become first responder + self.urlBar.tabLocationViewDidTapLocation(self.urlBar.locationView) + } + } + } + + fileprivate func popToBVC() { + guard let currentViewController = navigationController?.topViewController else { + return + } + currentViewController.dismiss(animated: true, completion: nil) + if currentViewController != self { + _ = self.navigationController?.popViewController(animated: true) + } else if urlBar.inOverlayMode { + urlBar.SELdidClickCancel() + } + } + + // MARK: User Agent Spoofing + + func resetSpoofedUserAgentIfRequired(_ webView: WKWebView, newURL: URL) { + // Reset the UA when a different domain is being loaded + if webView.url?.host != newURL.host { + webView.customUserAgent = nil + } + } + + func restoreSpoofedUserAgentIfRequired(_ webView: WKWebView, newRequest: URLRequest) { + // Restore any non-default UA from the request's header + let ua = newRequest.value(forHTTPHeaderField: "User-Agent") + webView.customUserAgent = ua != UserAgent.defaultUserAgent() ? ua : nil + } + + fileprivate func presentActivityViewController(_ url: URL, tab: Tab? = nil, sourceView: UIView?, sourceRect: CGRect, arrowDirection: UIPopoverArrowDirection) { + let helper = ShareExtensionHelper(url: url, tab: tab) + + let controller = helper.createActivityViewController({ [unowned self] completed, _ in + // After dismissing, check to see if there were any prompts we queued up + self.showQueuedAlertIfAvailable() + + // Usually the popover delegate would handle nil'ing out the references we have to it + // on the BVC when displaying as a popover but the delegate method doesn't seem to be + // invoked on iOS 10. See Bug 1297768 for additional details. + self.displayedPopoverController = nil + self.updateDisplayedPopoverProperties = nil + }) + + if let popoverPresentationController = controller.popoverPresentationController { + popoverPresentationController.sourceView = sourceView + popoverPresentationController.sourceRect = sourceRect + popoverPresentationController.permittedArrowDirections = arrowDirection + popoverPresentationController.delegate = self + } + + present(controller, animated: true, completion: nil) + LeanPlumClient.shared.track(event: .userSharedWebpage) + } + + func updateFindInPageVisibility(visible: Bool) { + if visible { + if findInPageBar == nil { + let findInPageBar = FindInPageBar() + self.findInPageBar = findInPageBar + findInPageBar.delegate = self + findInPageContainer.addSubview(findInPageBar) + + findInPageBar.snp.makeConstraints { make in + make.edges.equalTo(findInPageContainer) + make.height.equalTo(UIConstants.ToolbarHeight) + } + + updateViewConstraints() + + // We make the find-in-page bar the first responder below, causing the keyboard delegates + // to fire. This, in turn, will animate the Find in Page container since we use the same + // delegate to slide the bar up and down with the keyboard. We don't want to animate the + // constraints added above, however, so force a layout now to prevent these constraints + // from being lumped in with the keyboard animation. + findInPageBar.layoutIfNeeded() + } + + self.findInPageBar?.becomeFirstResponder() + } else if let findInPageBar = self.findInPageBar { + findInPageBar.endEditing(true) + guard let webView = tabManager.selectedTab?.webView else { return } + webView.evaluateJavaScript("__firefox__.findDone()", completionHandler: nil) + findInPageBar.removeFromSuperview() + self.findInPageBar = nil + updateViewConstraints() + } + } + + @objc fileprivate func openSettings() { + assert(Thread.isMainThread, "Opening settings requires being invoked on the main thread") + + let settingsTableViewController = AppSettingsTableViewController() + settingsTableViewController.profile = profile + settingsTableViewController.tabManager = tabManager + settingsTableViewController.settingsDelegate = self + + let controller = SettingsNavigationController(rootViewController: settingsTableViewController) + controller.popoverDelegate = self + controller.modalPresentationStyle = UIModalPresentationStyle.formSheet + self.present(controller, animated: true, completion: nil) + } + + fileprivate func postLocationChangeNotificationForTab(_ tab: Tab, navigation: WKNavigation?) { + let notificationCenter = NotificationCenter.default + var info = [AnyHashable: Any]() + info["url"] = tab.url?.displayURL + info["title"] = tab.title + if let visitType = self.getVisitTypeForTab(tab, navigation: navigation)?.rawValue { + info["visitType"] = visitType + } + info["isPrivate"] = tab.isPrivate + notificationCenter.post(name: NotificationOnLocationChange, object: self, userInfo: info) + } + + func navigateInTab(tab: Tab, to navigation: WKNavigation? = nil) { + tabManager.expireSnackbars() + + guard let webView = tab.webView else { + print("Cannot navigate in tab without a webView") + return + } + + if let url = webView.url, !url.isErrorPageURL && !url.isAboutHomeURL { + tab.lastExecutedTime = Date.now() + + postLocationChangeNotificationForTab(tab, navigation: navigation) + + // Fire the readability check. This is here and not in the pageShow event handler in ReaderMode.js anymore + // because that event wil not always fire due to unreliable page caching. This will either let us know that + // the currently loaded page can be turned into reading mode or if the page already is in reading mode. We + // ignore the result because we are being called back asynchronous when the readermode status changes. + webView.evaluateJavaScript("\(ReaderModeNamespace).checkReadability()", completionHandler: nil) + + // Re-run additional scripts in webView to extract updated favicons and metadata. + runScriptsOnWebView(webView) + } + + if tab === tabManager.selectedTab { + UIAccessibilityPostNotification(UIAccessibilityScreenChangedNotification, nil) + // must be followed by LayoutChanged, as ScreenChanged will make VoiceOver + // cursor land on the correct initial element, but if not followed by LayoutChanged, + // VoiceOver will sometimes be stuck on the element, not allowing user to move + // forward/backward. Strange, but LayoutChanged fixes that. + UIAccessibilityPostNotification(UIAccessibilityLayoutChangedNotification, nil) + } else { + // To Screenshot a tab that is hidden we must add the webView, + // then wait enough time for the webview to render. + if let webView = tab.webView { + view.insertSubview(webView, at: 0) + DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(500)) { + self.screenshotHelper.takeScreenshot(tab) + if webView.superview == self.view { + webView.removeFromSuperview() + } + } + } + } + + // Remember whether or not a desktop site was requested + tab.desktopSite = webView.customUserAgent?.isEmpty == false + } + + // MARK: open in helper utils + func addViewForOpenInHelper(_ openInHelper: OpenInHelper) { + guard let view = openInHelper.openInView else { return } + webViewContainerToolbar.addSubview(view) + webViewContainerToolbar.snp.updateConstraints { make in + make.height.equalTo(OpenInViewUX.ViewHeight) + } + view.snp.makeConstraints { make in + make.edges.equalTo(webViewContainerToolbar) + } + + self.openInHelper = openInHelper + } + + func removeOpenInView() { + guard let _ = self.openInHelper else { return } + webViewContainerToolbar.subviews.forEach { $0.removeFromSuperview() } + + webViewContainerToolbar.snp.updateConstraints { make in + make.height.equalTo(0) + } + + self.openInHelper = nil + } +} + +extension BrowserViewController: ClipboardBarDisplayHandlerDelegate { + func shouldDisplay(clipboardBar bar: ButtonToast) { + show(buttonToast: bar, duration: ClipboardBarToastUX.ToastDelay) + } +} + +extension BrowserViewController: QRCodeViewControllerDelegate { + func didScanQRCodeWithURL(_ url: URL) { + openBlankNewTab(focusLocationField: false) + finishEditingAndSubmit(url, visitType: VisitType.typed) + UnifiedTelemetry.recordEvent(category: .action, method: .scan, object: .qrCodeURL) + } + + func didScanQRCodeWithText(_ text: String) { + openBlankNewTab(focusLocationField: false) + submitSearchText(text) + UnifiedTelemetry.recordEvent(category: .action, method: .scan, object: .qrCodeText) + } +} + +extension BrowserViewController: SettingsDelegate { + func settingsOpenURLInNewTab(_ url: URL) { + self.openURLInNewTab(url, isPrivileged: false) + } +} + +extension BrowserViewController: PresentingModalViewControllerDelegate { + func dismissPresentedModalViewController(_ modalViewController: UIViewController, animated: Bool) { + self.dismiss(animated: animated, completion: nil) + } +} + +/** + * History visit management. + * TODO: this should be expanded to track various visit types; see Bug 1166084. + */ +extension BrowserViewController { + func ignoreNavigationInTab(_ tab: Tab, navigation: WKNavigation) { + self.ignoredNavigation.insert(navigation) + } + + func recordNavigationInTab(_ tab: Tab, navigation: WKNavigation, visitType: VisitType) { + self.typedNavigation[navigation] = visitType + } + + /** + * Untrack and do the right thing. + */ + func getVisitTypeForTab(_ tab: Tab, navigation: WKNavigation?) -> VisitType? { + guard let navigation = navigation else { + // See https://github.com/WebKit/webkit/blob/master/Source/WebKit2/UIProcess/Cocoa/NavigationState.mm#L390 + return VisitType.link + } + + if let _ = self.ignoredNavigation.remove(navigation) { + return nil + } + + return self.typedNavigation.removeValue(forKey: navigation) ?? VisitType.link + } +} + +extension BrowserViewController: URLBarDelegate { + + func showTabTray() { + webViewContainerToolbar.isHidden = true + updateFindInPageVisibility(visible: false) + + let tabTrayController = TabTrayController(tabManager: tabManager, profile: profile, tabTrayDelegate: self) + + if let tab = tabManager.selectedTab { + screenshotHelper.takeScreenshot(tab) + } + + navigationController?.pushViewController(tabTrayController, animated: true) + self.tabTrayController = tabTrayController + } + + func urlBarDidPressReload(_ urlBar: URLBarView) { + tabManager.selectedTab?.reload() + } + + func urlBarDidPressQRButton(_ urlBar: URLBarView) { + let qrCodeViewController = QRCodeViewController() + qrCodeViewController.qrCodeDelegate = self + let controller = QRCodeNavigationController(rootViewController: qrCodeViewController) + self.present(controller, animated: true, completion: nil) + } + + func urlBarDidPressPageOptions(_ urlBar: URLBarView, from button: UIButton) { + + let actionMenuPresenter: (URL, Tab, UIView, UIPopoverArrowDirection) -> Void = { (url, tab, view, _) in + self.presentActivityViewController(url, tab: tab, sourceView: view, sourceRect: view.bounds, arrowDirection: .up) + } + + let findInPageAction = { + self.updateFindInPageVisibility(visible: true) + } + + let successCallback: (String) -> Void = { (successMessage) in + SimpleToast().showAlertWithText(successMessage, bottomContainer: self.webViewContainer) + } + + guard let tab = tabManager.selectedTab, tab.url != nil else { return } + + // The logic of which actions appear when isnt final. + let pageActions = getTabActions(tab: tab, buttonView: button, presentShareMenu: actionMenuPresenter, + findInPage: findInPageAction, presentableVC: self, success: successCallback) + + presentSheetWith(actions: pageActions, on: self, from: button) + } + + func urlBarDidLongPressPageOptions(_ urlBar: URLBarView, from button: UIButton) { + guard let tab = tabManager.selectedTab else { return } + guard let url = tab.canonicalURL?.displayURL else { return } + presentActivityViewController(url, tab: tab, sourceView: button, sourceRect: button.bounds, arrowDirection: .up) + } + + func urlBarDidPressStop(_ urlBar: URLBarView) { + tabManager.selectedTab?.stop() + } + + func urlBarDidPressTabs(_ urlBar: URLBarView) { + showTabTray() + } + + func urlBarDidPressReaderMode(_ urlBar: URLBarView) { + if let tab = tabManager.selectedTab { + if let readerMode = tab.getContentScript(name: "ReaderMode") as? ReaderMode { + switch readerMode.state { + case .available: + enableReaderMode() + UnifiedTelemetry.recordEvent(category: .action, method: .tap, object: .readerModeOpenButton) + LeanPlumClient.shared.track(event: .useReaderView) + case .active: + disableReaderMode() + UnifiedTelemetry.recordEvent(category: .action, method: .tap, object: .readerModeCloseButton) + case .unavailable: + break + } + } + } + } + + func urlBarDidLongPressReaderMode(_ urlBar: URLBarView) -> Bool { + guard let tab = tabManager.selectedTab, + let url = tab.url?.displayURL, + let result = profile.readingList?.createRecordWithURL(url.absoluteString, title: tab.title ?? "", addedBy: UIDevice.current.name) + else { + UIAccessibilityPostNotification(UIAccessibilityAnnouncementNotification, NSLocalizedString("Could not add page to Reading list", comment: "Accessibility message e.g. spoken by VoiceOver after adding current webpage to the Reading List failed.")) + return false + } + + switch result { + case .success: + UIAccessibilityPostNotification(UIAccessibilityAnnouncementNotification, NSLocalizedString("Added page to Reading List", comment: "Accessibility message e.g. spoken by VoiceOver after the current page gets added to the Reading List using the Reader View button, e.g. by long-pressing it or by its accessibility custom action.")) + // TODO: https://bugzilla.mozilla.org/show_bug.cgi?id=1158503 provide some form of 'this has been added' visual feedback? + case .failure(let error): + UIAccessibilityPostNotification(UIAccessibilityAnnouncementNotification, NSLocalizedString("Could not add page to Reading List. Maybe it’s already there?", comment: "Accessibility message e.g. spoken by VoiceOver after the user wanted to add current page to the Reading List and this was not done, likely because it already was in the Reading List, but perhaps also because of real failures.")) + print("readingList.createRecordWithURL(url: \"\(url.absoluteString)\", ...) failed with error: \(error)") + } + return true + } + + func locationActionsForURLBar(_ urlBar: URLBarView) -> [AccessibleAction] { + if UIPasteboard.general.string != nil { + return [pasteGoAction, pasteAction, copyAddressAction] + } else { + return [copyAddressAction] + } + } + + func urlBarDisplayTextForURL(_ url: URL?) -> (String?, Bool) { + // use the initial value for the URL so we can do proper pattern matching with search URLs + var searchURL = self.tabManager.selectedTab?.currentInitialURL + if searchURL?.isErrorPageURL ?? true { + searchURL = url + } + if let query = profile.searchEngines.queryForSearchURL(searchURL as URL?) { + return (query, true) + } else { + return (url?.absoluteString, false) + } + } + + func urlBarDidLongPressLocation(_ urlBar: URLBarView) { + let longPressAlertController = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet) + + for action in locationActionsForURLBar(urlBar) { + longPressAlertController.addAction(action.alertAction(style: .default)) + } + + let cancelAction = UIAlertAction(title: NSLocalizedString("Cancel", comment: "Label for Cancel button"), style: .cancel, handler: { (alert: UIAlertAction) -> Void in + }) + longPressAlertController.addAction(cancelAction) + + let setupPopover = { [unowned self] in + if let popoverPresentationController = longPressAlertController.popoverPresentationController { + popoverPresentationController.sourceView = urlBar + popoverPresentationController.sourceRect = urlBar.frame + popoverPresentationController.permittedArrowDirections = .any + popoverPresentationController.delegate = self + } + } + + setupPopover() + + if longPressAlertController.popoverPresentationController != nil { + displayedPopoverController = longPressAlertController + updateDisplayedPopoverProperties = setupPopover + } + + self.present(longPressAlertController, animated: true, completion: nil) + } + + func urlBarDidPressScrollToTop(_ urlBar: URLBarView) { + if let selectedTab = tabManager.selectedTab { + // Only scroll to top if we are not showing the home view controller + if homePanelController == nil { + selectedTab.webView?.scrollView.setContentOffset(CGPoint.zero, animated: true) + } + } + } + + func urlBarLocationAccessibilityActions(_ urlBar: URLBarView) -> [UIAccessibilityCustomAction]? { + return locationActionsForURLBar(urlBar).map { $0.accessibilityCustomAction } + } + + func urlBar(_ urlBar: URLBarView, didEnterText text: String) { + if text.isEmpty { + hideSearchController() + } else { + showSearchController() + searchController?.searchQuery = text + searchLoader?.query = text + } + } + + func urlBar(_ urlBar: URLBarView, didSubmitText text: String) { + if let fixupURL = URIFixup.getURL(text) { + // The user entered a URL, so use it. + finishEditingAndSubmit(fixupURL, visitType: VisitType.typed) + return + } + + // We couldn't build a URL, so check for a matching search keyword. + let trimmedText = text.trimmingCharacters(in: CharacterSet.whitespaces) + guard let possibleKeywordQuerySeparatorSpace = trimmedText.characters.index(of: " ") else { + submitSearchText(text) + return + } + + let possibleKeyword = trimmedText.substring(to: possibleKeywordQuerySeparatorSpace) + let possibleQuery = trimmedText.substring(from: trimmedText.index(after: possibleKeywordQuerySeparatorSpace)) + + profile.bookmarks.getURLForKeywordSearch(possibleKeyword).uponQueue(DispatchQueue.main) { result in + if var urlString = result.successValue, + let escapedQuery = possibleQuery.addingPercentEncoding(withAllowedCharacters: NSCharacterSet.urlQueryAllowed), + let range = urlString.range(of: "%s") { + urlString.replaceSubrange(range, with: escapedQuery) + + if let url = URL(string: urlString) { + self.finishEditingAndSubmit(url, visitType: VisitType.typed) + return + } + } + + self.submitSearchText(text) + } + } + + fileprivate func submitSearchText(_ text: String) { + let engine = profile.searchEngines.defaultEngine + + if let searchURL = engine.searchURLForQuery(text) { + // We couldn't find a matching search keyword, so do a search query. + Telemetry.default.recordSearch(location: .actionBar, searchEngine: engine.engineID ?? "other") + finishEditingAndSubmit(searchURL, visitType: VisitType.typed) + } else { + // We still don't have a valid URL, so something is broken. Give up. + print("Error handling URL entry: \"\(text)\".") + assertionFailure("Couldn't generate search URL: \(text)") + } + } + + func urlBarDidEnterOverlayMode(_ urlBar: URLBarView) { + guard let profile = profile as? BrowserProfile else { + return + } + + if .blankPage == NewTabAccessors.getNewTabPage(profile.prefs) { + UIAccessibilityPostNotification(UIAccessibilityScreenChangedNotification, nil) + } else { + if let toast = clipboardBarDisplayHandler?.clipboardToast { + toast.removeFromSuperview() + } + showHomePanelController(inline: false) + } + + LeanPlumClient.shared.track(event: .interactWithURLBar) + } + + func urlBarDidLeaveOverlayMode(_ urlBar: URLBarView) { + hideSearchController() + updateInContentHomePanel(tabManager.selectedTab?.url as URL?) + } +} + +extension BrowserViewController: TabToolbarDelegate, PhotonActionSheetProtocol { + func tabToolbarDidPressBack(_ tabToolbar: TabToolbarProtocol, button: UIButton) { + tabManager.selectedTab?.goBack() + } + + func tabToolbarDidLongPressBack(_ tabToolbar: TabToolbarProtocol, button: UIButton) { + showBackForwardList() + } + + func tabToolbarDidPressReload(_ tabToolbar: TabToolbarProtocol, button: UIButton) { + tabManager.selectedTab?.reload() + } + + func tabToolbarDidLongPressReload(_ tabToolbar: TabToolbarProtocol, button: UIButton) { + guard let tab = tabManager.selectedTab, tab.webView?.url != nil && (tab.getContentScript(name: ReaderMode.name()) as? ReaderMode)?.state != .active else { + return + } + + let toggleActionTitle: String + if tab.desktopSite { + toggleActionTitle = NSLocalizedString("Request Mobile Site", comment: "Action Sheet Button for Requesting the Mobile Site") + } else { + toggleActionTitle = NSLocalizedString("Request Desktop Site", comment: "Action Sheet Button for Requesting the Desktop Site") + } + + let controller = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet) + controller.addAction(UIAlertAction(title: toggleActionTitle, style: .default, handler: { _ in tab.toggleDesktopSite() })) + controller.addAction(UIAlertAction(title: NSLocalizedString("Cancel", comment: "Label for Cancel button"), style: .cancel, handler: nil)) + controller.popoverPresentationController?.sourceView = toolbar ?? urlBar + controller.popoverPresentationController?.sourceRect = button.frame + present(controller, animated: true, completion: nil) + } + + func tabToolbarDidPressStop(_ tabToolbar: TabToolbarProtocol, button: UIButton) { + tabManager.selectedTab?.stop() + } + + func tabToolbarDidPressForward(_ tabToolbar: TabToolbarProtocol, button: UIButton) { + tabManager.selectedTab?.goForward() + } + + func tabToolbarDidLongPressForward(_ tabToolbar: TabToolbarProtocol, button: UIButton) { + showBackForwardList() + } + + func tabToolbarDidPressMenu(_ tabToolbar: TabToolbarProtocol, button: UIButton) { + // ensure that any keyboards or spinners are dismissed before presenting the menu + UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil) + var actions: [[PhotonActionSheetItem]] = [] + + actions.append(getHomePanelActions()) + actions.append(getOtherPanelActions(vcDelegate: self)) + // force a modal if the menu is being displayed in compact split screen + let shouldSupress = !topTabsVisible && UIDevice.current.userInterfaceIdiom == .pad + presentSheetWith(actions: actions, on: self, from: button, supressPopover: shouldSupress) + } + + func tabToolbarDidPressTabs(_ tabToolbar: TabToolbarProtocol, button: UIButton) { + showTabTray() + } + + func tabToolbarDidLongPressTabs(_ tabToolbar: TabToolbarProtocol, button: UIButton) { + let controller = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet) + controller.addAction(UIAlertAction(title: Strings.NewTabTitle, style: .default, handler: { _ in + self.tabManager.addTabAndSelect(isPrivate: false) + })) + controller.addAction(UIAlertAction(title: Strings.NewPrivateTabTitle, style: .default, handler: { _ in + self.tabManager.addTabAndSelect(isPrivate: true) + })) + controller.addAction(UIAlertAction(title: Strings.CloseTabTitle, style: .destructive, handler: { _ in + if let tab = self.tabManager.selectedTab { + self.tabManager.removeTab(tab) + } + })) + controller.addAction(UIAlertAction(title: NSLocalizedString("Cancel", comment: "Label for Cancel button"), style: .cancel, handler: nil)) + controller.popoverPresentationController?.sourceView = toolbar ?? urlBar + controller.popoverPresentationController?.sourceRect = button.frame + present(controller, animated: true, completion: nil) + } + + func showBackForwardList() { + if let backForwardList = tabManager.selectedTab?.webView?.backForwardList { + let backForwardViewController = BackForwardListViewController(profile: profile, backForwardList: backForwardList) + backForwardViewController.tabManager = tabManager + backForwardViewController.bvc = self + backForwardViewController.modalPresentationStyle = UIModalPresentationStyle.overCurrentContext + backForwardViewController.backForwardTransitionDelegate = BackForwardListAnimator() + self.present(backForwardViewController, animated: true, completion: nil) + } + } +} + +extension BrowserViewController: TabDelegate { + + func tab(_ tab: Tab, didCreateWebView webView: WKWebView) { + webView.frame = webViewContainer.frame + // Observers that live as long as the tab. Make sure these are all cleared in willDeleteWebView below! + KVOs.forEach { webView.addObserver(self, forKeyPath: $0.rawValue, options: .new, context: nil) } + webView.scrollView.addObserver(self.scrollController, forKeyPath: KVOConstants.contentSize.rawValue, options: .new, context: nil) + + webView.uiDelegate = self + + let readerMode = ReaderMode(tab: tab) + readerMode.delegate = self + tab.addContentScript(readerMode, name: ReaderMode.name()) + + let favicons = FaviconManager(tab: tab, profile: profile) + tab.addContentScript(favicons, name: FaviconManager.name()) + + // only add the logins helper if the tab is not a private browsing tab + if !tab.isPrivate { + let logins = LoginsHelper(tab: tab, profile: profile) + tab.addContentScript(logins, name: LoginsHelper.name()) + } + + let contextMenuHelper = ContextMenuHelper(tab: tab) + contextMenuHelper.delegate = self + tab.addContentScript(contextMenuHelper, name: ContextMenuHelper.name()) + + let errorHelper = ErrorPageHelper() + tab.addContentScript(errorHelper, name: ErrorPageHelper.name()) + + let sessionRestoreHelper = SessionRestoreHelper(tab: tab) + sessionRestoreHelper.delegate = self + tab.addContentScript(sessionRestoreHelper, name: SessionRestoreHelper.name()) + + let findInPageHelper = FindInPageHelper(tab: tab) + findInPageHelper.delegate = self + tab.addContentScript(findInPageHelper, name: FindInPageHelper.name()) + + let noImageModeHelper = NoImageModeHelper(tab: tab) + tab.addContentScript(noImageModeHelper, name: NoImageModeHelper.name()) + + let printHelper = PrintHelper(tab: tab) + tab.addContentScript(printHelper, name: PrintHelper.name()) + + let customSearchHelper = CustomSearchHelper(tab: tab) + tab.addContentScript(customSearchHelper, name: CustomSearchHelper.name()) + + let nightModeHelper = NightModeHelper(tab: tab) + tab.addContentScript(nightModeHelper, name: NightModeHelper.name()) + + // XXX: Bug 1390200 - Disable NSUserActivity/CoreSpotlight temporarily + // let spotlightHelper = SpotlightHelper(tab: tab) + // tab.addHelper(spotlightHelper, name: SpotlightHelper.name()) + + tab.addContentScript(LocalRequestHelper(), name: LocalRequestHelper.name()) + + let historyStateHelper = HistoryStateHelper(tab: tab) + historyStateHelper.delegate = self + tab.addContentScript(historyStateHelper, name: HistoryStateHelper.name()) + + if #available(iOS 11, *) { + (tab.contentBlocker as? ContentBlockerHelper)?.setupForWebView() + } + + let metadataHelper = MetadataParserHelper(tab: tab, profile: profile) + tab.addContentScript(metadataHelper, name: MetadataParserHelper.name()) + } + + func tab(_ tab: Tab, willDeleteWebView webView: WKWebView) { + tab.cancelQueuedAlerts() + KVOs.forEach { webView.removeObserver(self, forKeyPath: $0.rawValue) } + webView.scrollView.removeObserver(self.scrollController, forKeyPath: KVOConstants.contentSize.rawValue) + webView.uiDelegate = nil + webView.scrollView.delegate = nil + webView.removeFromSuperview() + } + + fileprivate func findSnackbar(_ barToFind: SnackBar) -> Int? { + let bars = snackBars.subviews + for (index, bar) in bars.enumerated() where bar === barToFind { + return index + } + return nil + } + + fileprivate func updateSnackBarConstraints() { + snackBars.snp.remakeConstraints { make in + make.bottom.equalTo(findInPageContainer.snp.top) + + let bars = self.snackBars.subviews + if bars.count > 0 { + let view = bars[bars.count-1] + make.top.equalTo(view.snp.top) + } else { + make.height.equalTo(0) + } + + if traitCollection.horizontalSizeClass != .regular { + make.leading.trailing.equalTo(self.view) + self.snackBars.layer.borderWidth = 0 + } else { + make.centerX.equalTo(self.view) + make.width.equalTo(SnackBarUX.MaxWidth) + self.snackBars.layer.borderColor = UIConstants.BorderColor.cgColor + self.snackBars.layer.borderWidth = 1 + } + } + } + + // This removes the bar from its superview and updates constraints appropriately + fileprivate func finishRemovingBar(_ bar: SnackBar) { + // If there was a bar above this one, we need to remake its constraints. + if let index = findSnackbar(bar) { + // If the bar being removed isn't on the top of the list + let bars = snackBars.subviews + if index < bars.count-1 { + // Move the bar above this one + let nextbar = bars[index+1] as! SnackBar + nextbar.snp.makeConstraints { make in + // If this wasn't the bottom bar, attach to the bar below it + if index > 0 { + let bar = bars[index-1] as! SnackBar + nextbar.bottom = make.bottom.equalTo(bar.snp.top).constraint + } else { + // Otherwise, we attach it to the bottom of the snackbars + nextbar.bottom = make.bottom.equalTo(self.snackBars.snp.bottom).constraint + } + } + } + } + + // Really remove the bar + bar.removeFromSuperview() + } + + fileprivate func finishAddingBar(_ bar: SnackBar) { + snackBars.addSubview(bar) + bar.snp.makeConstraints { make in + // If there are already bars showing, add this on top of them + let bars = self.snackBars.subviews + + // Add the bar on top of the stack + // We're the new top bar in the stack, so make sure we ignore ourself + if bars.count > 1 { + let view = bars[bars.count - 2] + bar.bottom = make.bottom.equalTo(view.snp.top).offset(0).constraint + } else { + bar.bottom = make.bottom.equalTo(self.snackBars.snp.bottom).offset(0).constraint + } + make.leading.trailing.equalTo(self.snackBars) + } + } + + func showBar(_ bar: SnackBar, animated: Bool) { + finishAddingBar(bar) + updateSnackBarConstraints() + + bar.hide() + view.layoutIfNeeded() + UIView.animate(withDuration: animated ? 0.25 : 0, animations: { () -> Void in + bar.show() + self.view.layoutIfNeeded() + }) + } + + func removeBar(_ bar: SnackBar, animated: Bool) { + if let _ = findSnackbar(bar) { + UIView.animate(withDuration: animated ? 0.25 : 0, animations: { () -> Void in + bar.hide() + self.view.layoutIfNeeded() + }, completion: { success in + // Really remove the bar + self.finishRemovingBar(bar) + self.updateSnackBarConstraints() + }) + } + } + + func removeAllBars() { + let bars = snackBars.subviews + for bar in bars { + if let bar = bar as? SnackBar { + bar.removeFromSuperview() + } + } + self.updateSnackBarConstraints() + } + + func tab(_ tab: Tab, didAddSnackbar bar: SnackBar) { + showBar(bar, animated: true) + } + + func tab(_ tab: Tab, didRemoveSnackbar bar: SnackBar) { + removeBar(bar, animated: true) + } + + func tab(_ tab: Tab, didSelectFindInPageForSelection selection: String) { + updateFindInPageVisibility(visible: true) + findInPageBar?.text = selection + } +} + +extension BrowserViewController: HomePanelViewControllerDelegate { + func homePanelViewController(_ homePanelViewController: HomePanelViewController, didSelectURL url: URL, visitType: VisitType) { + finishEditingAndSubmit(url, visitType: visitType) + } + + func homePanelViewController(_ homePanelViewController: HomePanelViewController, didSelectPanel panel: Int) { + if let url = tabManager.selectedTab?.url, url.isAboutHomeURL { + tabManager.selectedTab?.webView?.evaluateJavaScript("history.replaceState({}, '', '#panel=\(panel)')", completionHandler: nil) + } + } + + func homePanelViewControllerDidRequestToCreateAccount(_ homePanelViewController: HomePanelViewController) { + presentSignInViewController() // TODO UX Right now the flow for sign in and create account is the same + } + + func homePanelViewControllerDidRequestToSignIn(_ homePanelViewController: HomePanelViewController) { + presentSignInViewController() // TODO UX Right now the flow for sign in and create account is the same + } + + func homePanelViewControllerDidRequestToOpenInNewTab(_ url: URL, isPrivate: Bool) { + let tab = self.tabManager.addTab(PrivilegedRequest(url: url) as URLRequest, afterTab: self.tabManager.selectedTab, isPrivate: isPrivate) + // If we are showing toptabs a user can just use the top tab bar + // If in overlay mode switching doesnt correctly dismiss the homepanels + guard !topTabsVisible, !self.urlBar.inOverlayMode else { + return + } + // We're not showing the top tabs; show a toast to quick switch to the fresh new tab. + let toast = ButtonToast(labelText: Strings.ContextMenuButtonToastNewTabOpenedLabelText, buttonText: Strings.ContextMenuButtonToastNewTabOpenedButtonText, completion: { buttonPressed in + if buttonPressed { + self.tabManager.selectTab(tab) + } + }) + self.show(buttonToast: toast) + } +} + +extension BrowserViewController: SearchViewControllerDelegate { + func searchViewController(_ searchViewController: SearchViewController, didSelectURL url: URL) { + finishEditingAndSubmit(url, visitType: VisitType.typed) + } + + func searchViewController(_ searchViewController: SearchViewController, didLongPressSuggestion suggestion: String) { + self.urlBar.setLocation(suggestion, search: true) + } + + func presentSearchSettingsController() { + let settingsNavigationController = SearchSettingsTableViewController() + settingsNavigationController.model = self.profile.searchEngines + settingsNavigationController.profile = self.profile + let navController = ModalSettingsNavigationController(rootViewController: settingsNavigationController) + + self.present(navController, animated: true, completion: nil) + } +} + +extension BrowserViewController: TabManagerDelegate { + + func tabManager(_ tabManager: TabManager, didSelectedTabChange selected: Tab?, previous: Tab?) { + // Remove the old accessibilityLabel. Since this webview shouldn't be visible, it doesn't need it + // and having multiple views with the same label confuses tests. + if let wv = previous?.webView { + removeOpenInView() + wv.endEditing(true) + wv.accessibilityLabel = nil + wv.accessibilityElementsHidden = true + wv.accessibilityIdentifier = nil + wv.removeFromSuperview() + } + + if let tab = selected, let webView = tab.webView { + updateURLBarDisplayURL(tab) + if tab.isPrivate != previous?.isPrivate { + applyTheme(tab.isPrivate ? Theme.PrivateMode : Theme.NormalMode) + } + if tab.isPrivate { + readerModeCache = MemoryReaderModeCache.sharedInstance + + } else { + readerModeCache = DiskReaderModeCache.sharedInstance + } + if let privateModeButton = topTabsViewController?.privateModeButton, previous != nil && previous?.isPrivate != tab.isPrivate { + privateModeButton.setSelected(tab.isPrivate, animated: true) + } + ReaderModeHandlers.readerModeCache = readerModeCache + + scrollController.tab = selected + webViewContainer.addSubview(webView) + webView.snp.makeConstraints { make in + make.top.equalTo(webViewContainerToolbar.snp.bottom) + make.left.right.bottom.equalTo(self.webViewContainer) + } + webView.accessibilityLabel = NSLocalizedString("Web content", comment: "Accessibility label for the main web content view") + webView.accessibilityIdentifier = "contentView" + webView.accessibilityElementsHidden = false + + if let url = webView.url { + let absoluteString = url.absoluteString + // Don't bother fetching bookmark state for about/sessionrestore and about/home. + if url.isAboutURL { + // Indeed, because we don't show the toolbar at all, don't even blank the star. + } else { + profile.bookmarks.modelFactory >>== { [weak tab] in + $0.isBookmarked(absoluteString) + .uponQueue(DispatchQueue.main) { + guard let isBookmarked = $0.successValue else { + print("Error getting bookmark status: \($0.failureValue ??? "nil").") + return + } + + tab?.isBookmarked = isBookmarked + } + } + } + } else { + // The web view can go gray if it was zombified due to memory pressure. + // When this happens, the URL is nil, so try restoring the page upon selection. + tab.reload() + } + } + + if let selected = selected, let previous = previous, selected.isPrivate != previous.isPrivate { + updateTabCountUsingTabManager(tabManager) + } + + removeAllBars() + if let bars = selected?.bars { + for bar in bars { + showBar(bar, animated: true) + } + } + + updateFindInPageVisibility(visible: false) + + navigationToolbar.updateReloadStatus(selected?.loading ?? false) + navigationToolbar.updateBackStatus(selected?.canGoBack ?? false) + navigationToolbar.updateForwardStatus(selected?.canGoForward ?? false) + if !(selected?.webView?.url?.isLocalUtility ?? false) { + self.urlBar.updateProgressBar(Float(selected?.estimatedProgress ?? 0)) + } + + if let readerMode = selected?.getContentScript(name: ReaderMode.name()) as? ReaderMode { + urlBar.updateReaderModeState(readerMode.state) + if readerMode.state == .active { + showReaderModeBar(animated: false) + } else { + hideReaderModeBar(animated: false) + } + } else { + urlBar.updateReaderModeState(ReaderModeState.unavailable) + } + + updateInContentHomePanel(selected?.url as URL?) + } + + func tabManager(_ tabManager: TabManager, willAddTab tab: Tab) { + } + + func tabManager(_ tabManager: TabManager, didAddTab tab: Tab) { + // If we are restoring tabs then we update the count once at the end + if !tabManager.isRestoring { + updateTabCountUsingTabManager(tabManager) + } + tab.tabDelegate = self + } + + func tabManager(_ tabManager: TabManager, willRemoveTab tab: Tab) { + } + + func tabManager(_ tabManager: TabManager, didRemoveTab tab: Tab) { + updateTabCountUsingTabManager(tabManager) + // tabDelegate is a weak ref (and the tab's webView may not be destroyed yet) + // so we don't expcitly unset it. + + if let url = tab.url, !url.isAboutURL && !tab.isPrivate { + profile.recentlyClosedTabs.addTab(url as URL, title: tab.title, faviconURL: tab.displayFavicon?.url) + } + } + + func tabManagerDidAddTabs(_ tabManager: TabManager) { + updateTabCountUsingTabManager(tabManager) + } + + func tabManagerDidRestoreTabs(_ tabManager: TabManager) { + updateTabCountUsingTabManager(tabManager) + } + + func show(buttonToast: ButtonToast, afterWaiting delay: DispatchTimeInterval = SimpleToastUX.ToastDelayBefore, duration: DispatchTimeInterval = SimpleToastUX.ToastDismissAfter) { + // If BVC isnt visible hold on to this toast until viewDidAppear + if self.view.window == nil { + self.pendingToast = buttonToast + return + } + + DispatchQueue.main.asyncAfter(deadline: .now() + delay) { + self.view.addSubview(buttonToast) + buttonToast.snp.makeConstraints { make in + make.left.right.equalTo(self.view) + make.bottom.equalTo(self.webViewContainer) + } + buttonToast.showToast(duration: duration) + } + } + + func tabManagerDidRemoveAllTabs(_ tabManager: TabManager, toast: ButtonToast?) { + guard let toast = toast, !tabTrayController.privateMode else { + return + } + show(buttonToast: toast, afterWaiting: ButtonToastUX.ToastDelay) + } + + fileprivate func updateTabCountUsingTabManager(_ tabManager: TabManager, animated: Bool = true) { + if let selectedTab = tabManager.selectedTab { + let count = selectedTab.isPrivate ? tabManager.privateTabs.count : tabManager.normalTabs.count + toolbar?.updateTabCount(count, animated: animated) + urlBar.updateTabCount(count, animated: !urlBar.inOverlayMode) + topTabsViewController?.updateTabCount(count, animated: animated) + } + } +} + +/// List of schemes that are allowed to open a popup window +private let SchemesAllowedToOpenPopups = ["http", "https", "javascript", "data"] + +extension BrowserViewController: WKUIDelegate { + func webView(_ webView: WKWebView, createWebViewWith configuration: WKWebViewConfiguration, for navigationAction: WKNavigationAction, windowFeatures: WKWindowFeatures) -> WKWebView? { + guard let parentTab = tabManager[webView] else { return nil } + + if !navigationAction.isAllowed { + print("Denying unprivileged request: \(navigationAction.request)") + return nil + } + + if let currentTab = tabManager.selectedTab { + screenshotHelper.takeScreenshot(currentTab) + } + + // If the page uses window.open() or target="_blank", open the page in a new tab. + let newTab = tabManager.addTab(navigationAction.request, configuration: configuration, afterTab: parentTab, isPrivate: parentTab.isPrivate) + tabManager.selectTab(newTab) + + // If the page we just opened has a bad scheme, we return nil here so that JavaScript does not + // get a reference to it which it can return from window.open() - this will end up as a + // CFErrorHTTPBadURL being presented. + guard let scheme = (navigationAction.request as NSURLRequest).url?.scheme?.lowercased(), SchemesAllowedToOpenPopups.contains(scheme) else { + return nil + } + + return newTab.webView + } + + fileprivate func canDisplayJSAlertForWebView(_ webView: WKWebView) -> Bool { + // Only display a JS Alert if we are selected and there isn't anything being shown + return ((tabManager.selectedTab == nil ? false : tabManager.selectedTab!.webView == webView)) && (self.presentedViewController == nil) + } + + func webView(_ webView: WKWebView, runJavaScriptAlertPanelWithMessage message: String, initiatedByFrame frame: WKFrameInfo, completionHandler: @escaping () -> Void) { + let messageAlert = MessageAlert(message: message, frame: frame, completionHandler: completionHandler) + if canDisplayJSAlertForWebView(webView) { + present(messageAlert.alertController(), animated: true, completion: nil) + } else if let promptingTab = tabManager[webView] { + promptingTab.queueJavascriptAlertPrompt(messageAlert) + } else { + // This should never happen since an alert needs to come from a web view but just in case call the handler + // since not calling it will result in a runtime exception. + completionHandler() + } + } + + func webView(_ webView: WKWebView, runJavaScriptConfirmPanelWithMessage message: String, initiatedByFrame frame: WKFrameInfo, completionHandler: @escaping (Bool) -> Void) { + let confirmAlert = ConfirmPanelAlert(message: message, frame: frame, completionHandler: completionHandler) + if canDisplayJSAlertForWebView(webView) { + present(confirmAlert.alertController(), animated: true, completion: nil) + } else if let promptingTab = tabManager[webView] { + promptingTab.queueJavascriptAlertPrompt(confirmAlert) + } else { + completionHandler(false) + } + } + + func webView(_ webView: WKWebView, runJavaScriptTextInputPanelWithPrompt prompt: String, defaultText: String?, initiatedByFrame frame: WKFrameInfo, completionHandler: @escaping (String?) -> Void) { + let textInputAlert = TextInputAlert(message: prompt, frame: frame, completionHandler: completionHandler, defaultText: defaultText) + if canDisplayJSAlertForWebView(webView) { + present(textInputAlert.alertController(), animated: true, completion: nil) + } else if let promptingTab = tabManager[webView] { + promptingTab.queueJavascriptAlertPrompt(textInputAlert) + } else { + completionHandler(nil) + } + } + + /// Invoked when an error occurs while starting to load data for the main frame. + func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error) { + // Ignore the "Frame load interrupted" error that is triggered when we cancel a request + // to open an external application and hand it over to UIApplication.openURL(). The result + // will be that we switch to the external app, for example the app store, while keeping the + // original web page in the tab instead of replacing it with an error page. + let error = error as NSError + if error.domain == "WebKitErrorDomain" && error.code == 102 { + return + } + + if checkIfWebContentProcessHasCrashed(webView, error: error as NSError) { + return + } + + if error.code == Int(CFNetworkErrors.cfurlErrorCancelled.rawValue) { + if let tab = tabManager[webView], tab === tabManager.selectedTab { + urlBar.currentURL = tab.url?.displayURL + } + return + } + + if let url = error.userInfo[NSURLErrorFailingURLErrorKey] as? URL { + ErrorPageHelper().showPage(error, forUrl: url, inWebView: webView) + + // If the local web server isn't working for some reason (Firefox cellular data is + // disabled in settings, for example), we'll fail to load the session restore URL. + // We rely on loading that page to get the restore callback to reset the restoring + // flag, so if we fail to load that page, reset it here. + if url.aboutComponent == "sessionrestore" { + tabManager.tabs.filter { $0.webView == webView }.first?.restoring = false + } + } + } + + fileprivate func checkIfWebContentProcessHasCrashed(_ webView: WKWebView, error: NSError) -> Bool { + if error.code == WKError.webContentProcessTerminated.rawValue && error.domain == "WebKitErrorDomain" { + print("WebContent process has crashed. Trying to reloadFromOrigin to restart it.") + webView.reloadFromOrigin() + return true + } + + return false + } + + func webView(_ webView: WKWebView, decidePolicyFor navigationResponse: WKNavigationResponse, decisionHandler: @escaping (WKNavigationResponsePolicy) -> Void) { + let helperForURL = OpenIn.helperForResponse(navigationResponse.response) + if navigationResponse.canShowMIMEType { + if let openInHelper = helperForURL { + addViewForOpenInHelper(openInHelper) + } + decisionHandler(WKNavigationResponsePolicy.allow) + return + } + + guard var openInHelper = helperForURL else { + let error = NSError(domain: ErrorPageHelper.MozDomain, code: Int(ErrorPageHelper.MozErrorDownloadsNotEnabled), userInfo: [NSLocalizedDescriptionKey: Strings.UnableToDownloadError]) + ErrorPageHelper().showPage(error, forUrl: navigationResponse.response.url!, inWebView: webView) + return decisionHandler(WKNavigationResponsePolicy.allow) + } + + if openInHelper.openInView == nil { + openInHelper.openInView = navigationToolbar.menuButton + } + + openInHelper.open() + decisionHandler(WKNavigationResponsePolicy.cancel) + } + + func webViewDidClose(_ webView: WKWebView) { + if let tab = tabManager[webView] { + self.tabManager.removeTab(tab) + } + } +} + +extension BrowserViewController: ReaderModeDelegate { + func readerMode(_ readerMode: ReaderMode, didChangeReaderModeState state: ReaderModeState, forTab tab: Tab) { + // If this reader mode availability state change is for the tab that we currently show, then update + // the button. Otherwise do nothing and the button will be updated when the tab is made active. + if tabManager.selectedTab === tab { + urlBar.updateReaderModeState(state) + } + } + + func readerMode(_ readerMode: ReaderMode, didDisplayReaderizedContentForTab tab: Tab) { + self.showReaderModeBar(animated: true) + tab.showContent(true) + } +} + +// MARK: - UIPopoverPresentationControllerDelegate + +extension BrowserViewController: UIPopoverPresentationControllerDelegate { + func popoverPresentationControllerDidDismissPopover(_ popoverPresentationController: UIPopoverPresentationController) { + displayedPopoverController = nil + updateDisplayedPopoverProperties = nil + } +} + +extension BrowserViewController: UIAdaptivePresentationControllerDelegate { + // Returning None here makes sure that the Popover is actually presented as a Popover and + // not as a full-screen modal, which is the default on compact device classes. + func adaptivePresentationStyle(for controller: UIPresentationController, traitCollection: UITraitCollection) -> UIModalPresentationStyle { + return UIModalPresentationStyle.none + } +} + +// MARK: - ReaderModeStyleViewControllerDelegate + +extension BrowserViewController: ReaderModeStyleViewControllerDelegate { + func readerModeStyleViewController(_ readerModeStyleViewController: ReaderModeStyleViewController, didConfigureStyle style: ReaderModeStyle) { + // Persist the new style to the profile + let encodedStyle: [String: Any] = style.encodeAsDictionary() + profile.prefs.setObject(encodedStyle, forKey: ReaderModeProfileKeyStyle) + // Change the reader mode style on all tabs that have reader mode active + for tabIndex in 0.. 1 && backList.last?.url == readerModeURL { + webView.go(to: backList.last!) + } else if forwardList.count > 0 && forwardList.first?.url == readerModeURL { + webView.go(to: forwardList.first!) + } else { + // Store the readability result in the cache and load it. This will later move to the ReadabilityHelper. + webView.evaluateJavaScript("\(ReaderModeNamespace).readerize()", completionHandler: { (object, error) -> Void in + if let readabilityResult = ReadabilityResult(object: object as AnyObject?) { + do { + try self.readerModeCache.put(currentURL, readabilityResult) + } catch _ { + } + if let nav = webView.load(PrivilegedRequest(url: readerModeURL) as URLRequest) { + self.ignoreNavigationInTab(tab, navigation: nav) + } + } + }) + } + } + + /// Disabling reader mode can mean two things. In the simplest case we were opened from the reading list, which + /// means that there is nothing in the BackForwardList except the internal url for the reader mode page. In that + /// case we simply open a new page with the original url. In the more complicated page, the non-readerized version + /// of the page is either to the left or right in the BackForwardList. If that is the case, we navigate there. + + func disableReaderMode() { + if let tab = tabManager.selectedTab, + let webView = tab.webView { + let backList = webView.backForwardList.backList + let forwardList = webView.backForwardList.forwardList + + if let currentURL = webView.backForwardList.currentItem?.url { + if let originalURL = currentURL.decodeReaderModeURL { + if backList.count > 1 && backList.last?.url == originalURL { + webView.go(to: backList.last!) + } else if forwardList.count > 0 && forwardList.first?.url == originalURL { + webView.go(to: forwardList.first!) + } else { + if let nav = webView.load(URLRequest(url: originalURL)) { + self.ignoreNavigationInTab(tab, navigation: nav) + } + } + } + } + } + } + + func SELDynamicFontChanged(_ notification: Notification) { + guard notification.name == NotificationDynamicFontChanged else { return } + + var readerModeStyle = DefaultReaderModeStyle + if let dict = profile.prefs.dictionaryForKey(ReaderModeProfileKeyStyle) { + if let style = ReaderModeStyle(dict: dict as [String: AnyObject]) { + readerModeStyle = style + } + } + readerModeStyle.fontSize = ReaderModeFontSize.defaultSize + self.readerModeStyleViewController(ReaderModeStyleViewController(), didConfigureStyle: readerModeStyle) + } +} + +extension BrowserViewController: ReaderModeBarViewDelegate { + func readerModeBar(_ readerModeBar: ReaderModeBarView, didSelectButton buttonType: ReaderModeBarButtonType) { + switch buttonType { + case .settings: + if let readerMode = tabManager.selectedTab?.getContentScript(name: "ReaderMode") as? ReaderMode, readerMode.state == ReaderModeState.active { + var readerModeStyle = DefaultReaderModeStyle + if let dict = profile.prefs.dictionaryForKey(ReaderModeProfileKeyStyle) { + if let style = ReaderModeStyle(dict: dict as [String: AnyObject]) { + readerModeStyle = style + } + } + + let readerModeStyleViewController = ReaderModeStyleViewController() + readerModeStyleViewController.delegate = self + readerModeStyleViewController.readerModeStyle = readerModeStyle + readerModeStyleViewController.modalPresentationStyle = UIModalPresentationStyle.popover + + let setupPopover = { [unowned self] in + if let popoverPresentationController = readerModeStyleViewController.popoverPresentationController { + popoverPresentationController.backgroundColor = UIColor.white + popoverPresentationController.delegate = self + popoverPresentationController.sourceView = readerModeBar + popoverPresentationController.sourceRect = CGRect(x: readerModeBar.frame.width/2, y: UIConstants.ToolbarHeight, width: 1, height: 1) + popoverPresentationController.permittedArrowDirections = UIPopoverArrowDirection.up + } + } + + setupPopover() + + if readerModeStyleViewController.popoverPresentationController != nil { + displayedPopoverController = readerModeStyleViewController + updateDisplayedPopoverProperties = setupPopover + } + + self.present(readerModeStyleViewController, animated: true, completion: nil) + } + + case .markAsRead: + if let url = self.tabManager.selectedTab?.url?.displayURL?.absoluteString, let result = profile.readingList?.getRecordWithURL(url) { + if let successValue = result.successValue, let record = successValue { + profile.readingList?.updateRecord(record, unread: false) // TODO Check result, can this fail? + readerModeBar.unread = false + } + } + + case .markAsUnread: + if let url = self.tabManager.selectedTab?.url?.displayURL?.absoluteString, let result = profile.readingList?.getRecordWithURL(url) { + if let successValue = result.successValue, let record = successValue { + profile.readingList?.updateRecord(record, unread: true) // TODO Check result, can this fail? + readerModeBar.unread = true + } + } + + case .addToReadingList: + if let tab = tabManager.selectedTab, + let rawURL = tab.url, rawURL.isReaderModeURL, + let url = rawURL.decodeReaderModeURL { + profile.readingList?.createRecordWithURL(url.absoluteString, title: tab.title ?? "", addedBy: UIDevice.current.name) // TODO Check result, can this fail? + readerModeBar.added = true + readerModeBar.unread = true + } + + case .removeFromReadingList: + if let url = self.tabManager.selectedTab?.url?.displayURL?.absoluteString, + let result = profile.readingList?.getRecordWithURL(url), + let successValue = result.successValue, + let record = successValue { + profile.readingList?.deleteRecord(record) // TODO Check result, can this fail? + readerModeBar.added = false + readerModeBar.unread = false + } + } + } +} + +extension BrowserViewController: IntroViewControllerDelegate { + @discardableResult func presentIntroViewController(_ force: Bool = false, animated: Bool = true) -> Bool { + if let deeplink = self.profile.prefs.stringForKey("AdjustDeeplinkKey"), let url = URL(string: deeplink) { + self.launchFxAFromDeeplinkURL(url) + return true + } + + if force || profile.prefs.intForKey(IntroViewControllerSeenProfileKey) == nil { + let introViewController = IntroViewController() + introViewController.delegate = self + // On iPad we present it modally in a controller + if topTabsVisible { + introViewController.preferredContentSize = CGSize(width: IntroViewControllerUX.Width, height: IntroViewControllerUX.Height) + introViewController.modalPresentationStyle = UIModalPresentationStyle.formSheet + } + present(introViewController, animated: animated) { + // On first run (and forced) open up the homepage in the background. + if let homePageURL = HomePageAccessors.getHomePage(self.profile.prefs), let tab = self.tabManager.selectedTab, DeviceInfo.hasConnectivity() { + tab.loadRequest(URLRequest(url: homePageURL)) + } + } + + return true + } + + return false + } + + func launchFxAFromDeeplinkURL(_ url: URL) { + self.profile.prefs.removeObjectForKey("AdjustDeeplinkKey") + var query = url.getQuery() + query["entrypoint"] = "adjust_deepklink_ios" + let fxaParams: FxALaunchParams + fxaParams = FxALaunchParams(query: query) + self.presentSignInViewController(fxaParams) + } + + func introViewControllerDidFinish(_ introViewController: IntroViewController, requestToLogin: Bool) { + self.profile.prefs.setInt(1, forKey: IntroViewControllerSeenProfileKey) + introViewController.dismiss(animated: true) { finished in + if self.navigationController?.viewControllers.count ?? 0 > 1 { + _ = self.navigationController?.popToRootViewController(animated: true) + } + + if requestToLogin { + self.presentSignInViewController() + } + } + } + + func presentSignInViewController(_ fxaOptions: FxALaunchParams? = nil) { + // Show the settings page if we have already signed in. If we haven't then show the signin page + let vcToPresent: UIViewController + if profile.hasAccount() { + let settingsTableViewController = AppSettingsTableViewController() + settingsTableViewController.profile = profile + settingsTableViewController.tabManager = tabManager + vcToPresent = settingsTableViewController + } else { + let signInVC = FxAContentViewController(profile: profile, fxaOptions: fxaOptions) + signInVC.delegate = self + signInVC.navigationItem.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem: UIBarButtonSystemItem.done, target: self, action: #selector(BrowserViewController.dismissSignInViewController)) + vcToPresent = signInVC + } + + let settingsNavigationController = SettingsNavigationController(rootViewController: vcToPresent) + settingsNavigationController.modalPresentationStyle = .formSheet + settingsNavigationController.navigationBar.isTranslucent = false + self.present(settingsNavigationController, animated: true, completion: nil) + } + + func dismissSignInViewController() { + self.dismiss(animated: true, completion: nil) + } + +} + +extension BrowserViewController: FxAContentViewControllerDelegate { + func contentViewControllerDidSignIn(_ viewController: FxAContentViewController, withFlags flags: FxALoginFlags) { + if flags.verified { + self.dismiss(animated: true, completion: nil) + } + } + + func contentViewControllerDidCancel(_ viewController: FxAContentViewController) { + self.dismiss(animated: true, completion: nil) + } +} + +extension BrowserViewController: ContextMenuHelperDelegate { + func contextMenuHelper(_ contextMenuHelper: ContextMenuHelper, didLongPressElements elements: ContextMenuHelper.Elements, gestureRecognizer: UIGestureRecognizer) { + // locationInView can return (0, 0) when the long press is triggered in an invalid page + // state (e.g., long pressing a link before the document changes, then releasing after a + // different page loads). + let touchPoint = gestureRecognizer.location(in: view) + guard touchPoint != CGPoint.zero else { return } + + let touchSize = CGSize(width: 0, height: 16) + + let actionSheetController = UIAlertController(title: nil, message: nil, preferredStyle: UIAlertControllerStyle.actionSheet) + var dialogTitle: String? + + if let url = elements.link, let currentTab = tabManager.selectedTab { + dialogTitle = url.absoluteString + let isPrivate = currentTab.isPrivate + + let addTab = { (rURL: URL, isPrivate: Bool) in + let tab = self.tabManager.addTab(URLRequest(url: rURL as URL), afterTab: currentTab, isPrivate: isPrivate) + LeanPlumClient.shared.track(event: .openedNewTab, withParameters: ["Source": "Long Press Context Menu" as AnyObject]) + guard !self.topTabsVisible else { + return + } + // We're not showing the top tabs; show a toast to quick switch to the fresh new tab. + let toast = ButtonToast(labelText: Strings.ContextMenuButtonToastNewTabOpenedLabelText, buttonText: Strings.ContextMenuButtonToastNewTabOpenedButtonText, completion: { buttonPressed in + if buttonPressed { + self.tabManager.selectTab(tab) + } + }) + self.show(buttonToast: toast) + } + + if !isPrivate { + let newTabTitle = NSLocalizedString("Open in New Tab", comment: "Context menu item for opening a link in a new tab") + let openNewTabAction = UIAlertAction(title: newTabTitle, style: UIAlertActionStyle.default) { (action: UIAlertAction) in + addTab(url, false) + } + actionSheetController.addAction(openNewTabAction) + } + + let openNewPrivateTabTitle = NSLocalizedString("Open in New Private Tab", tableName: "PrivateBrowsing", comment: "Context menu option for opening a link in a new private tab") + let openNewPrivateTabAction = UIAlertAction(title: openNewPrivateTabTitle, style: UIAlertActionStyle.default) { (action: UIAlertAction) in + addTab(url, true) + } + actionSheetController.addAction(openNewPrivateTabAction) + + let copyTitle = NSLocalizedString("Copy Link", comment: "Context menu item for copying a link URL to the clipboard") + let copyAction = UIAlertAction(title: copyTitle, style: UIAlertActionStyle.default) { (action: UIAlertAction) -> Void in + UIPasteboard.general.url = url as URL + } + actionSheetController.addAction(copyAction) + + let shareTitle = NSLocalizedString("Share Link", comment: "Context menu item for sharing a link URL") + let shareAction = UIAlertAction(title: shareTitle, style: UIAlertActionStyle.default) { _ in + self.presentActivityViewController(url as URL, sourceView: self.view, sourceRect: CGRect(origin: touchPoint, size: touchSize), arrowDirection: .any) + } + actionSheetController.addAction(shareAction) + } + + if let url = elements.image { + if dialogTitle == nil { + dialogTitle = url.absoluteString + } + + let photoAuthorizeStatus = PHPhotoLibrary.authorizationStatus() + let saveImageTitle = NSLocalizedString("Save Image", comment: "Context menu item for saving an image") + let saveImageAction = UIAlertAction(title: saveImageTitle, style: UIAlertActionStyle.default) { (action: UIAlertAction) -> Void in + if photoAuthorizeStatus == PHAuthorizationStatus.authorized || photoAuthorizeStatus == PHAuthorizationStatus.notDetermined { + self.getImage(url as URL) { + UIImageWriteToSavedPhotosAlbum($0, self, #selector(self.image(_:didFinishSavingWithError:contextInfo:)), nil) + } + } else { + let accessDenied = UIAlertController(title: NSLocalizedString("Firefox would like to access your Photos", comment: "See http://mzl.la/1G7uHo7"), message: NSLocalizedString("This allows you to save the image to your Camera Roll.", comment: "See http://mzl.la/1G7uHo7"), preferredStyle: UIAlertControllerStyle.alert) + let dismissAction = UIAlertAction(title: UIConstants.CancelString, style: UIAlertActionStyle.default, handler: nil) + accessDenied.addAction(dismissAction) + let settingsAction = UIAlertAction(title: NSLocalizedString("Open Settings", comment: "See http://mzl.la/1G7uHo7"), style: UIAlertActionStyle.default ) { (action: UIAlertAction!) -> Void in + UIApplication.shared.openURL(URL(string: UIApplicationOpenSettingsURLString)!) + } + accessDenied.addAction(settingsAction) + self.present(accessDenied, animated: true, completion: nil) + } + } + actionSheetController.addAction(saveImageAction) + + let copyImageTitle = NSLocalizedString("Copy Image", comment: "Context menu item for copying an image to the clipboard") + let copyAction = UIAlertAction(title: copyImageTitle, style: UIAlertActionStyle.default) { (action: UIAlertAction) -> Void in + // put the actual image on the clipboard + // do this asynchronously just in case we're in a low bandwidth situation + let pasteboard = UIPasteboard.general + pasteboard.url = url as URL + let changeCount = pasteboard.changeCount + let application = UIApplication.shared + var taskId: UIBackgroundTaskIdentifier = 0 + taskId = application.beginBackgroundTask (expirationHandler: { _ in + application.endBackgroundTask(taskId) + }) + + Alamofire.request(url) + .validate(statusCode: 200..<300) + .response { response in + // Only set the image onto the pasteboard if the pasteboard hasn't changed since + // fetching the image; otherwise, in low-bandwidth situations, + // we might be overwriting something that the user has subsequently added. + if changeCount == pasteboard.changeCount, let imageData = response.data, response.error == nil { + pasteboard.addImageWithData(imageData, forURL: url) + } + + application.endBackgroundTask(taskId) + } + } + actionSheetController.addAction(copyAction) + } + + // If we're showing an arrow popup, set the anchor to the long press location. + if let popoverPresentationController = actionSheetController.popoverPresentationController { + popoverPresentationController.sourceView = view + popoverPresentationController.sourceRect = CGRect(origin: touchPoint, size: touchSize) + popoverPresentationController.permittedArrowDirections = .any + popoverPresentationController.delegate = self + } + + if actionSheetController.popoverPresentationController != nil { + displayedPopoverController = actionSheetController + } + + actionSheetController.title = dialogTitle?.ellipsize(maxLength: ActionSheetTitleMaxLength) + let cancelAction = UIAlertAction(title: UIConstants.CancelString, style: UIAlertActionStyle.cancel, handler: nil) + actionSheetController.addAction(cancelAction) + self.present(actionSheetController, animated: true, completion: nil) + } + + fileprivate func getImage(_ url: URL, success: @escaping (UIImage) -> Void) { + Alamofire.request(url) + .validate(statusCode: 200..<300) + .response { response in + if let data = response.data, + let image = UIImage.dataIsGIF(data) ? UIImage.imageFromGIFDataThreadSafe(data) : UIImage.imageFromDataThreadSafe(data) { + success(image) + } + } + } + + func contextMenuHelper(_ contextMenuHelper: ContextMenuHelper, didCancelGestureRecognizer: UIGestureRecognizer) { + displayedPopoverController?.dismiss(animated: true) { + self.displayedPopoverController = nil + } + } +} + +extension BrowserViewController { + func image(_ image: UIImage, didFinishSavingWithError error: NSError?, contextInfo: UnsafeRawPointer) { + if error == nil { + LeanPlumClient.shared.track(event: .saveImage) + } + } +} + +extension BrowserViewController: HistoryStateHelperDelegate { + func historyStateHelper(_ historyStateHelper: HistoryStateHelper, didPushOrReplaceStateInTab tab: Tab) { + navigateInTab(tab: tab) + tabManager.storeChanges() + } +} + +/** + A third party search engine Browser extension +**/ +extension BrowserViewController { + + func addCustomSearchButtonToWebView(_ webView: WKWebView) { + //check if the search engine has already been added. + let domain = webView.url?.domainURL.host + let matches = self.profile.searchEngines.orderedEngines.filter {$0.shortName == domain} + if !matches.isEmpty { + self.customSearchEngineButton.tintColor = UIColor.gray + self.customSearchEngineButton.isUserInteractionEnabled = false + } else { + self.customSearchEngineButton.tintColor = UIConstants.SystemBlueColor + self.customSearchEngineButton.isUserInteractionEnabled = true + } + + /* + This is how we access hidden views in the WKContentView + Using the public headers we can find the keyboard accessoryView which is not usually available. + Specific values here are from the WKContentView headers. + https://github.com/JaviSoto/iOS9-Runtime-Headers/blob/master/Frameworks/WebKit.framework/WKContentView.h + */ + guard let webContentView = UIView.findSubViewWithFirstResponder(webView) else { + /* + In some cases the URL bar can trigger the keyboard notification. In that case the webview isnt the first responder + and a search button should not be added. + */ + return + } + + guard let input = webContentView.perform(#selector(getter: UIResponder.inputAccessoryView)), + let inputView = input.takeUnretainedValue() as? UIInputView, + let nextButton = inputView.value(forKey: "_nextItem") as? UIBarButtonItem, + let nextButtonView = nextButton.value(forKey: "view") as? UIView else { + //failed to find the inputView instead lets use the inputAssistant + addCustomSearchButtonToInputAssistant(webContentView) + return + } + inputView.addSubview(self.customSearchEngineButton) + self.customSearchEngineButton.snp.remakeConstraints { make in + make.leading.equalTo(nextButtonView.snp.trailing).offset(20) + make.width.equalTo(inputView.snp.height) + make.top.equalTo(nextButtonView.snp.top) + make.height.equalTo(inputView.snp.height) + } + } + + /** + This adds the customSearchButton to the inputAssistant + for cases where the inputAccessoryView could not be found for example + on the iPad where it does not exist. However this only works on iOS9 + **/ + func addCustomSearchButtonToInputAssistant(_ webContentView: UIView) { + guard customSearchBarButton == nil else { + return //The searchButton is already on the keyboard + } + let inputAssistant = webContentView.inputAssistantItem + let item = UIBarButtonItem(customView: customSearchEngineButton) + customSearchBarButton = item + _ = Try(withTry: { + inputAssistant.trailingBarButtonGroups.last?.barButtonItems.append(item) + }) { (exception) in + Sentry.shared.send(message: "Failed adding custom search button to input assistant", tag: .general, severity: .error, description: "\(exception ??? "nil")") + } + } + + func addCustomSearchEngineForFocusedElement() { + guard let webView = tabManager.selectedTab?.webView else { + return + } + webView.evaluateJavaScript("__firefox__.searchQueryForField()") { (result, _) in + guard let searchQuery = result as? String, let favicon = self.tabManager.selectedTab!.displayFavicon else { + //Javascript responded with an incorrectly formatted message. Show an error. + let alert = ThirdPartySearchAlerts.failedToAddThirdPartySearch() + self.present(alert, animated: true, completion: nil) + return + } + self.addSearchEngine(searchQuery, favicon: favicon) + self.customSearchEngineButton.tintColor = UIColor.gray + self.customSearchEngineButton.isUserInteractionEnabled = false + } + } + + func addSearchEngine(_ searchQuery: String, favicon: Favicon) { + guard searchQuery != "", + let iconURL = URL(string: favicon.url), + let url = URL(string: searchQuery.addingPercentEncoding(withAllowedCharacters: CharacterSet.urlFragmentAllowed)!), + let shortName = url.domainURL.host else { + let alert = ThirdPartySearchAlerts.failedToAddThirdPartySearch() + self.present(alert, animated: true, completion: nil) + return + } + + let alert = ThirdPartySearchAlerts.addThirdPartySearchEngine { alert in + self.customSearchEngineButton.tintColor = UIColor.gray + self.customSearchEngineButton.isUserInteractionEnabled = false + SDWebImageManager.shared().loadImage(with: iconURL, options: .continueInBackground, progress: nil) { (image, _, _, _, _, _) in + guard let image = image else { + let alert = ThirdPartySearchAlerts.failedToAddThirdPartySearch() + self.present(alert, animated: true, completion: nil) + return + } + + self.profile.searchEngines.addSearchEngine(OpenSearchEngine(engineID: nil, shortName: shortName, image: image, searchTemplate: searchQuery, suggestTemplate: nil, isCustomEngine: true)) + let Toast = SimpleToast() + Toast.showAlertWithText(Strings.ThirdPartySearchEngineAdded, bottomContainer: self.webViewContainer) + } + } + + self.present(alert, animated: true, completion: {}) + } +} + +extension BrowserViewController: KeyboardHelperDelegate { + func keyboardHelper(_ keyboardHelper: KeyboardHelper, keyboardWillShowWithState state: KeyboardState) { + keyboardState = state + updateViewConstraints() + + UIView.animate(withDuration: state.animationDuration) { + UIView.setAnimationCurve(state.animationCurve) + self.findInPageContainer.layoutIfNeeded() + self.snackBars.layoutIfNeeded() + } + + if let webView = tabManager.selectedTab?.webView { + webView.evaluateJavaScript("__firefox__.searchQueryForField()") { (result, _) in + guard let _ = result as? String else { + return + } + self.addCustomSearchButtonToWebView(webView) + } + } + } + + func keyboardHelper(_ keyboardHelper: KeyboardHelper, keyboardDidShowWithState state: KeyboardState) { + } + + func keyboardHelper(_ keyboardHelper: KeyboardHelper, keyboardWillHideWithState state: KeyboardState) { + keyboardState = nil + updateViewConstraints() + //If the searchEngineButton exists remove it form the keyboard + if let buttonGroup = customSearchBarButton?.buttonGroup { + buttonGroup.barButtonItems = buttonGroup.barButtonItems.filter { $0 != customSearchBarButton } + customSearchBarButton = nil + } + + if self.customSearchEngineButton.superview != nil { + self.customSearchEngineButton.removeFromSuperview() + } + + UIView.animate(withDuration: state.animationDuration) { + UIView.setAnimationCurve(state.animationCurve) + self.findInPageContainer.layoutIfNeeded() + self.snackBars.layoutIfNeeded() + } + } +} + +extension BrowserViewController: SessionRestoreHelperDelegate { + func sessionRestoreHelper(_ helper: SessionRestoreHelper, didRestoreSessionForTab tab: Tab) { + tab.restoring = false + + if let tab = tabManager.selectedTab, tab.webView === tab.webView { + updateUIForReaderHomeStateForTab(tab) + } + } +} + +extension BrowserViewController: TabTrayDelegate { + // This function animates and resets the tab chrome transforms when + // the tab tray dismisses. + func tabTrayDidDismiss(_ tabTray: TabTrayController) { + resetBrowserChrome() + } + + func tabTrayDidAddBookmark(_ tab: Tab) { + guard let url = tab.url?.absoluteString, url.characters.count > 0 else { return } + self.addBookmark(tab.tabState) + } + + func tabTrayDidAddToReadingList(_ tab: Tab) -> ReadingListClientRecord? { + guard let url = tab.url?.absoluteString, url.characters.count > 0 else { return nil } + return profile.readingList?.createRecordWithURL(url, title: tab.title ?? url, addedBy: UIDevice.current.name).successValue + } + + func tabTrayRequestsPresentationOf(_ viewController: UIViewController) { + self.present(viewController, animated: false, completion: nil) + } +} + +// MARK: Browser Chrome Theming +extension BrowserViewController: Themeable { + + func applyTheme(_ themeName: String) { + let ui: [Themeable?] = [urlBar, toolbar, readerModeBar, topTabsViewController] + ui.forEach { $0?.applyTheme(themeName) } + statusBarOverlay.backgroundColor = shouldShowTopTabsForTraitCollection(traitCollection) ? UIColor(rgb: 0x272727) : urlBar.backgroundColor + setNeedsStatusBarAppearanceUpdate() + } +} + +protocol Themeable { + func applyTheme(_ themeName: String) +} + +extension BrowserViewController: FindInPageBarDelegate, FindInPageHelperDelegate { + func findInPage(_ findInPage: FindInPageBar, didTextChange text: String) { + find(text, function: "find") + } + + func findInPage(_ findInPage: FindInPageBar, didFindNextWithText text: String) { + findInPageBar?.endEditing(true) + find(text, function: "findNext") + } + + func findInPage(_ findInPage: FindInPageBar, didFindPreviousWithText text: String) { + findInPageBar?.endEditing(true) + find(text, function: "findPrevious") + } + + func findInPageDidPressClose(_ findInPage: FindInPageBar) { + updateFindInPageVisibility(visible: false) + } + + fileprivate func find(_ text: String, function: String) { + guard let webView = tabManager.selectedTab?.webView else { return } + + let escaped = text.replacingOccurrences(of: "\\", with: "\\\\") + .replacingOccurrences(of: "\"", with: "\\\"") + + webView.evaluateJavaScript("__firefox__.\(function)(\"\(escaped)\")", completionHandler: nil) + } + + func findInPageHelper(_ findInPageHelper: FindInPageHelper, didUpdateCurrentResult currentResult: Int) { + findInPageBar?.currentResult = currentResult + } + + func findInPageHelper(_ findInPageHelper: FindInPageHelper, didUpdateTotalResults totalResults: Int) { + findInPageBar?.totalResults = totalResults + } +} + +extension BrowserViewController: JSPromptAlertControllerDelegate { + func promptAlertControllerDidDismiss(_ alertController: JSPromptAlertController) { + showQueuedAlertIfAvailable() + } +} + +extension BrowserViewController: TopTabsDelegate { + func topTabsDidPressTabs() { + urlBar.leaveOverlayMode(didCancel: true) + self.urlBarDidPressTabs(urlBar) + } + + func topTabsDidPressNewTab(_ isPrivate: Bool) { + openBlankNewTab(focusLocationField: false, isPrivate: isPrivate) + } + + func topTabsDidTogglePrivateMode() { + guard let selectedTab = tabManager.selectedTab else { + return + } + urlBar.leaveOverlayMode() + } + + func topTabsDidChangeTab() { + urlBar.leaveOverlayMode(didCancel: true) + } +} + +extension BrowserViewController: ClientPickerViewControllerDelegate, InstructionsViewControllerDelegate { + func instructionsViewControllerDidClose(_ instructionsViewController: InstructionsViewController) { + self.popToBVC() + } + + func clientPickerViewControllerDidCancel(_ clientPickerViewController: ClientPickerViewController) { + self.popToBVC() + } + + func clientPickerViewController(_ clientPickerViewController: ClientPickerViewController, didPickClients clients: [RemoteClient]) { + guard let tab = tabManager.selectedTab, + let url = tab.canonicalURL?.displayURL?.absoluteString else { return } + let shareItem = ShareItem(url: url, title: tab.title, favicon: tab.displayFavicon) + guard shareItem.isShareable else { + let alert = UIAlertController(title: Strings.SendToErrorTitle, message: Strings.SendToErrorMessage, preferredStyle: .alert) + alert.addAction(UIAlertAction(title: Strings.SendToErrorOKButton, style: .default) { _ in self.popToBVC()}) + present(alert, animated: true, completion: nil) + return + } + profile.sendItems([shareItem], toClients: clients).uponQueue(DispatchQueue.main) { _ in + self.popToBVC() + } + } +} + diff --git a/mobile/ios/Client/Frontend/Browser/BrowserViewController/BrowserViewController+KeyCommands.swift b/mobile/ios/Client/Frontend/Browser/BrowserViewController/BrowserViewController+KeyCommands.swift new file mode 100644 index 0000000000..e13d08f434 --- /dev/null +++ b/mobile/ios/Client/Frontend/Browser/BrowserViewController/BrowserViewController+KeyCommands.swift @@ -0,0 +1,100 @@ +/* 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 Shared + +// Naming functions: use the suffix 'KeyCommand' for an additional level of namespacing (bug 1415830) + +extension BrowserViewController { + + @objc private func reloadTabKeyCommand() { + if homePanelController == nil { + tabManager.selectedTab?.reload() + } + } + + @objc private func goBackKeyCommand() { + if tabManager.selectedTab?.canGoBack == true && homePanelController == nil { + tabManager.selectedTab?.goBack() + } + } + + @objc private func goForwardKeyCommand() { + if tabManager.selectedTab?.canGoForward == true && homePanelController == nil { + tabManager.selectedTab?.goForward() + } + } + + @objc private func findOnPageKeyCommand() { + if homePanelController == nil { + tab( (tabManager.selectedTab)!, didSelectFindInPageForSelection: "") + } + } + + @objc private func selectLocationBarKeyCommand() { + scrollController.showToolbars(animated: true) + urlBar.tabLocationViewDidTapLocation(urlBar.locationView) + } + + @objc private func newTabKeyCommand() { + openBlankNewTab(focusLocationField: false, isPrivate: false) + } + + @objc private func newPrivateTabKeyCommand() { + openBlankNewTab(focusLocationField: false, isPrivate: true) + } + + @objc private func closeTabKeyCommand() { + guard let currentTab = tabManager.selectedTab else { + return + } + tabManager.removeTab(currentTab) + } + + @objc private func nextTabKeyCommand() { + guard let currentTab = tabManager.selectedTab else { + return + } + + let tabs = currentTab.isPrivate ? tabManager.privateTabs : tabManager.normalTabs + if let index = tabs.index(of: currentTab), index + 1 < tabs.count { + tabManager.selectTab(tabs[index + 1]) + } else if let firstTab = tabs.first { + tabManager.selectTab(firstTab) + } + } + + @objc private func previousTabKeyCommand() { + guard let currentTab = tabManager.selectedTab else { + return + } + + let tabs = currentTab.isPrivate ? tabManager.privateTabs : tabManager.normalTabs + if let index = tabs.index(of: currentTab), index - 1 < tabs.count && index != 0 { + tabManager.selectTab(tabs[index - 1]) + } else if let lastTab = tabs.last { + tabManager.selectTab(lastTab) + } + } + + override var keyCommands: [UIKeyCommand]? { + return [ + UIKeyCommand(input: "r", modifierFlags: .command, action: #selector(BrowserViewController.reloadTabKeyCommand), discoverabilityTitle: Strings.ReloadPageTitle), + UIKeyCommand(input: "[", modifierFlags: .command, action: #selector(BrowserViewController.goBackKeyCommand), discoverabilityTitle: Strings.BackTitle), + UIKeyCommand(input: UIKeyInputLeftArrow, modifierFlags: .command, action: #selector(BrowserViewController.goBackKeyCommand), discoverabilityTitle: Strings.BackTitle), + UIKeyCommand(input: "]", modifierFlags: .command, action: #selector(BrowserViewController.goForwardKeyCommand), discoverabilityTitle: Strings.ForwardTitle), + UIKeyCommand(input: UIKeyInputRightArrow, modifierFlags: .command, action: #selector(BrowserViewController.goForwardKeyCommand), discoverabilityTitle: Strings.ForwardTitle), + + UIKeyCommand(input: "f", modifierFlags: .command, action: #selector(BrowserViewController.findOnPageKeyCommand), discoverabilityTitle: Strings.FindTitle), + UIKeyCommand(input: "l", modifierFlags: .command, action: #selector(BrowserViewController.selectLocationBarKeyCommand), discoverabilityTitle: Strings.SelectLocationBarTitle), + UIKeyCommand(input: "t", modifierFlags: .command, action: #selector(BrowserViewController.newTabKeyCommand), discoverabilityTitle: Strings.NewTabTitle), + UIKeyCommand(input: "p", modifierFlags: [.command, .shift], action: #selector(BrowserViewController.newPrivateTabKeyCommand), discoverabilityTitle: Strings.NewPrivateTabTitle), + UIKeyCommand(input: "w", modifierFlags: .command, action: #selector(BrowserViewController.closeTabKeyCommand), discoverabilityTitle: Strings.CloseTabTitle), + UIKeyCommand(input: "\t", modifierFlags: .control, action: #selector(BrowserViewController.nextTabKeyCommand), discoverabilityTitle: Strings.ShowNextTabTitle), + UIKeyCommand(input: UIKeyInputRightArrow, modifierFlags: [.command, .shift], action: #selector(BrowserViewController.nextTabKeyCommand), discoverabilityTitle: Strings.ShowNextTabTitle), + UIKeyCommand(input: "\t", modifierFlags: [.control, .shift], action: #selector(BrowserViewController.previousTabKeyCommand), discoverabilityTitle: Strings.ShowPreviousTabTitle), + UIKeyCommand(input: UIKeyInputLeftArrow, modifierFlags: [.command, .shift], action: #selector(BrowserViewController.previousTabKeyCommand), discoverabilityTitle: Strings.ShowPreviousTabTitle), + ] + } +} diff --git a/mobile/ios/Client/Frontend/Browser/BrowserViewController/BrowserViewController+WKNavigationDelegate.swift b/mobile/ios/Client/Frontend/Browser/BrowserViewController/BrowserViewController+WKNavigationDelegate.swift new file mode 100644 index 0000000000..aaeb4e5978 --- /dev/null +++ b/mobile/ios/Client/Frontend/Browser/BrowserViewController/BrowserViewController+WKNavigationDelegate.swift @@ -0,0 +1,212 @@ +/* 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 WebKit +import Shared + +private let log = Logger.browserLogger + +extension WKNavigationAction { + /// Allow local requests only if the request is privileged. + var isAllowed: Bool { + guard let url = request.url else { + return true + } + + return !url.isWebPage(includeDataURIs: false) || !url.isLocal || request.isPrivileged + } +} + +extension BrowserViewController: WKNavigationDelegate { + func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) { + if tabManager.selectedTab?.webView !== webView { + return + } + + updateFindInPageVisibility(visible: false) + + // If we are going to navigate to a new page, hide the reader mode button. Unless we + // are going to a about:reader page. Then we keep it on screen: it will change status + // (orange color) as soon as the page has loaded. + if let url = webView.url { + if !url.isReaderModeURL { + urlBar.updateReaderModeState(ReaderModeState.unavailable) + hideReaderModeBar(animated: false) + } + + // remove the open in overlay view if it is present + removeOpenInView() + } + } + + // Recognize an Apple Maps URL. This will trigger the native app. But only if a search query is present. Otherwise + // it could just be a visit to a regular page on maps.apple.com. + fileprivate func isAppleMapsURL(_ url: URL) -> Bool { + if url.scheme == "http" || url.scheme == "https" { + if url.host == "maps.apple.com" && url.query != nil { + return true + } + } + return false + } + + // Recognize a iTunes Store URL. These all trigger the native apps. Note that appstore.com and phobos.apple.com + // used to be in this list. I have removed them because they now redirect to itunes.apple.com. If we special case + // them then iOS will actually first open Safari, which then redirects to the app store. This works but it will + // leave a 'Back to Safari' button in the status bar, which we do not want. + fileprivate func isStoreURL(_ url: URL) -> Bool { + if url.scheme == "http" || url.scheme == "https" { + if url.host == "itunes.apple.com" { + return true + } + } + return false + } + + // This is the place where we decide what to do with a new navigation action. There are a number of special schemes + // and http(s) urls that need to be handled in a different way. All the logic for that is inside this delegate + // method. + + func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) { + guard let url = navigationAction.request.url else { + decisionHandler(WKNavigationActionPolicy.cancel) + return + } + + if url.scheme == "about" { + decisionHandler(WKNavigationActionPolicy.allow) + return + } + + if !navigationAction.isAllowed && navigationAction.navigationType != .backForward { + log.warning("Denying unprivileged request: \(navigationAction.request)") + decisionHandler(WKNavigationActionPolicy.cancel) + return + } + + // First special case are some schemes that are about Calling. We prompt the user to confirm this action. This + // gives us the exact same behaviour as Safari. + if url.scheme == "tel" || url.scheme == "facetime" || url.scheme == "facetime-audio" { + UIApplication.shared.openURL(url) + decisionHandler(WKNavigationActionPolicy.cancel) + return + } + + // Second special case are a set of URLs that look like regular http links, but should be handed over to iOS + // instead of being loaded in the webview. Note that there is no point in calling canOpenURL() here, because + // iOS will always say yes. TODO Is this the same as isWhitelisted? + + if isAppleMapsURL(url) { + UIApplication.shared.openURL(url) + decisionHandler(WKNavigationActionPolicy.cancel) + return + } + + if let tab = tabManager.selectedTab, isStoreURL(url) { + decisionHandler(WKNavigationActionPolicy.cancel) + + let alreadyShowingSnackbarOnThisTab = tab.bars.count > 0 + if !alreadyShowingSnackbarOnThisTab { + TimerSnackBar.showAppStoreConfirmationBar(forTab: tab, appStoreURL: url) + } + + return + } + + // Handles custom mailto URL schemes. + if url.scheme == "mailto" { + if let mailToMetadata = url.mailToMetadata(), let mailScheme = self.profile.prefs.stringForKey(PrefsKeys.KeyMailToOption), mailScheme != "mailto" { + self.mailtoLinkHandler.launchMailClientForScheme(mailScheme, metadata: mailToMetadata, defaultMailtoURL: url) + } else { + UIApplication.shared.openURL(url) + } + + LeanPlumClient.shared.track(event: .openedMailtoLink) + decisionHandler(WKNavigationActionPolicy.cancel) + return + } + + // This is the normal case, opening a http or https url, which we handle by loading them in this WKWebView. We + // always allow this. Additionally, data URIs are also handled just like normal web pages. + + if url.scheme == "http" || url.scheme == "https" || url.scheme == "data" || url.scheme == "blob" { + if navigationAction.navigationType == .linkActivated { + resetSpoofedUserAgentIfRequired(webView, newURL: url) + } else if navigationAction.navigationType == .backForward { + restoreSpoofedUserAgentIfRequired(webView, newRequest: navigationAction.request) + } + decisionHandler(WKNavigationActionPolicy.allow) + return + } + + // Ignore JS navigated links, the intention is to match Safari and native WKWebView behaviour. + if navigationAction.navigationType == .linkActivated { + UIApplication.shared.open(url, options: [:]) { openedURL in + if !openedURL { + let alert = UIAlertController(title: Strings.UnableToOpenURLErrorTitle, message: Strings.UnableToOpenURLError, preferredStyle: .alert) + alert.addAction(UIAlertAction(title: UIConstants.OKString, style: UIAlertActionStyle.default, handler: nil)) + self.present(alert, animated: true, completion: nil) + } + } + } + decisionHandler(WKNavigationActionPolicy.cancel) + } + + func webView(_ webView: WKWebView, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) { + + // If this is a certificate challenge, see if the certificate has previously been + // accepted by the user. + let origin = "\(challenge.protectionSpace.host):\(challenge.protectionSpace.port)" + if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust, + let trust = challenge.protectionSpace.serverTrust, + let cert = SecTrustGetCertificateAtIndex(trust, 0), profile.certStore.containsCertificate(cert, forOrigin: origin) { + completionHandler(URLSession.AuthChallengeDisposition.useCredential, URLCredential(trust: trust)) + return + } + + guard challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodHTTPBasic || + challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodHTTPDigest || + challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodNTLM, + let tab = tabManager[webView] else { + completionHandler(URLSession.AuthChallengeDisposition.performDefaultHandling, nil) + return + } + + // If this is a request to our local web server, use our private credentials. + if challenge.protectionSpace.host == "localhost" && challenge.protectionSpace.port == Int(WebServer.sharedInstance.server.port) { + completionHandler(.useCredential, WebServer.sharedInstance.credentials) + return + } + + // The challenge may come from a background tab, so ensure it's the one visible. + tabManager.selectTab(tab) + + let loginsHelper = tab.getContentScript(name: LoginsHelper.name()) as? LoginsHelper + Authenticator.handleAuthRequest(self, challenge: challenge, loginsHelper: loginsHelper).uponQueue(DispatchQueue.main) { res in + if let credentials = res.successValue { + completionHandler(.useCredential, credentials.credentials) + } else { + completionHandler(URLSession.AuthChallengeDisposition.rejectProtectionSpace, nil) + } + } + } + + func webView(_ webView: WKWebView, didCommit navigation: WKNavigation!) { + guard let tab = tabManager[webView] else { return } + + tab.url = webView.url + self.scrollController.resetZoomState() + + if tabManager.selectedTab === tab { + updateUIForReaderHomeStateForTab(tab) + } + } + + func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { + if let tab = tabManager[webView] { + navigateInTab(tab: tab, to: navigation) + } + } +} diff --git a/mobile/ios/Client/Frontend/Browser/ButtonToast.swift b/mobile/ios/Client/Frontend/Browser/ButtonToast.swift new file mode 100644 index 0000000000..2d6b9f90e3 --- /dev/null +++ b/mobile/ios/Client/Frontend/Browser/ButtonToast.swift @@ -0,0 +1,181 @@ +/* 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 SnapKit + +struct ButtonToastUX { + static let ToastPadding = 15.0 + static let TitleSpacing = 2.0 + static let ToastButtonPadding: CGFloat = 10.0 + static let TitleButtonPadding: CGFloat = 5.0 + static let ToastDelay = DispatchTimeInterval.milliseconds(900) + static let ToastButtonBorderRadius: CGFloat = 5 + static let ToastButtonBorderWidth: CGFloat = 1 +} + +private class HighlightableButton: UIButton { + override var isHighlighted: Bool { + didSet { + if isHighlighted { + self.backgroundColor = UIColor.white + } else { + self.backgroundColor = UIColor.clear + } + } + } +} + +class ButtonToast: UIView { + + fileprivate var dismissed = false + fileprivate var completionHandler: ((Bool) -> Void)? + fileprivate lazy var toast: UIView = { + let toast = UIView() + toast.backgroundColor = SimpleToastUX.ToastDefaultColor + return toast + }() + fileprivate var animationConstraint: Constraint? + fileprivate lazy var gestureRecognizer: UITapGestureRecognizer = { + let gestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(ButtonToast.handleTap(_:))) + gestureRecognizer.cancelsTouchesInView = false + return gestureRecognizer + }() + + init(labelText: String, descriptionText: String? = nil, buttonText: String, completion:@escaping (_ buttonPressed: Bool) -> Void) { + super.init(frame: CGRect.zero) + completionHandler = completion + + self.clipsToBounds = true + self.addSubview(createView(labelText, descriptionText: descriptionText, buttonText: buttonText)) + + toast.snp.makeConstraints { make in + make.left.right.height.equalTo(self) + animationConstraint = make.top.equalTo(self).offset(SimpleToastUX.ToastHeight).constraint + } + self.snp.makeConstraints { make in + make.height.equalTo(SimpleToastUX.ToastHeight) + } + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + fileprivate func createView(_ labelText: String, descriptionText: String?, buttonText: String) -> UIView { + let label = UILabel() + label.textColor = UIColor.white + label.font = SimpleToastUX.ToastFont + label.text = labelText + label.lineBreakMode = .byWordWrapping + label.numberOfLines = 0 + toast.addSubview(label) + + let button = HighlightableButton() + button.layer.cornerRadius = ButtonToastUX.ToastButtonBorderRadius + button.layer.borderWidth = ButtonToastUX.ToastButtonBorderWidth + button.layer.borderColor = UIColor.white.cgColor + button.setTitle(buttonText, for: UIControlState()) + button.setTitleColor(self.toast.backgroundColor, for: .highlighted) + button.titleLabel?.font = SimpleToastUX.ToastFont + button.titleLabel?.numberOfLines = 1 + button.titleLabel?.lineBreakMode = .byClipping + button.titleLabel?.adjustsFontSizeToFitWidth = true + button.titleLabel?.minimumScaleFactor = 0.1 + + let recognizer = UITapGestureRecognizer(target: self, action: #selector(ButtonToast.buttonPressed(_:))) + button.addGestureRecognizer(recognizer) + toast.addSubview(button) + var descriptionLabel: UILabel? + + if let text = descriptionText { + let textLabel = UILabel() + textLabel.textColor = UIColor.white + textLabel.font = SimpleToastUX.ToastFont + textLabel.text = text + textLabel.lineBreakMode = .byTruncatingTail + toast.addSubview(textLabel) + descriptionLabel = textLabel + } + + if let description = descriptionLabel { + label.numberOfLines = 1 // if showing a description we cant wrap to the second line + label.lineBreakMode = .byClipping + label.adjustsFontSizeToFitWidth = true + label.snp.makeConstraints { (make) in + make.leading.equalTo(toast).offset(ButtonToastUX.ToastPadding) + make.top.equalTo(toast).offset(ButtonToastUX.TitleButtonPadding) + make.trailing.equalTo(button.snp.leading).offset(-ButtonToastUX.TitleButtonPadding) + } + description.snp.makeConstraints { (make) in + make.leading.equalTo(toast).offset(ButtonToastUX.ToastPadding) + make.top.equalTo(label.snp.bottom).offset(ButtonToastUX.TitleSpacing) + make.trailing.equalTo(button.snp.leading).offset(-ButtonToastUX.TitleButtonPadding) + } + } else { + label.snp.makeConstraints { (make) in + make.leading.equalTo(toast).offset(ButtonToastUX.ToastPadding) + make.centerY.equalTo(toast) + make.trailing.equalTo(button.snp.leading).offset(-ButtonToastUX.TitleButtonPadding) + } + } + + button.snp.makeConstraints { (make) in + make.trailing.equalTo(toast).offset(-ButtonToastUX.ToastPadding) + make.centerY.equalTo(toast) + make.width.equalTo(button.titleLabel!.intrinsicContentSize.width + 2*ButtonToastUX.ToastButtonPadding) + } + + return toast + } + + fileprivate func dismiss(_ buttonPressed: Bool) { + guard dismissed == false else { + return + } + dismissed = true + superview?.removeGestureRecognizer(gestureRecognizer) + + UIView.animate(withDuration: SimpleToastUX.ToastAnimationDuration, animations: { + self.animationConstraint?.update(offset: SimpleToastUX.ToastHeight) + self.layoutIfNeeded() + }, + completion: { finished in + self.removeFromSuperview() + if !buttonPressed { + self.completionHandler?(false) + } + } + ) + } + + func showToast(duration: DispatchTimeInterval = SimpleToastUX.ToastDismissAfter) { + layoutIfNeeded() + UIView.animate(withDuration: SimpleToastUX.ToastAnimationDuration, animations: { + self.animationConstraint?.update(offset: 0) + self.layoutIfNeeded() + }, + completion: { finished in + DispatchQueue.main.asyncAfter(deadline: .now() + duration) { + self.dismiss(false) + } + } + ) + } + + @objc func buttonPressed(_ gestureRecognizer: UIGestureRecognizer) { + self.completionHandler?(true) + self.dismiss(true) + } + + override func didMoveToSuperview() { + super.didMoveToSuperview() + superview?.addGestureRecognizer(gestureRecognizer) + } + + func handleTap(_ gestureRecognizer: UIGestureRecognizer) { + dismiss(false) + } +} diff --git a/mobile/ios/Client/Frontend/Browser/ClipboardBarDisplayHandler.swift b/mobile/ios/Client/Frontend/Browser/ClipboardBarDisplayHandler.swift new file mode 100644 index 0000000000..794b27f828 --- /dev/null +++ b/mobile/ios/Client/Frontend/Browser/ClipboardBarDisplayHandler.swift @@ -0,0 +1,162 @@ +/* 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 + +public struct ClipboardBarToastUX { + static let ToastDelay = DispatchTimeInterval.milliseconds(4000) +} + +protocol ClipboardBarDisplayHandlerDelegate: class { + func shouldDisplay(clipboardBar bar: ButtonToast) +} + +class ClipboardBarDisplayHandler: NSObject, URLChangeDelegate { + weak var delegate: (ClipboardBarDisplayHandlerDelegate & SettingsDelegate)? + weak var settingsDelegate: SettingsDelegate? + weak var tabManager: TabManager? + private var sessionStarted = true + private var sessionRestored = false + private var firstTabLoaded = false + private var prefs: Prefs + private var lastDisplayedURL: String? + private weak var firstTab: Tab? + var clipboardToast: ButtonToast? + + init(prefs: Prefs, tabManager: TabManager) { + self.prefs = prefs + self.tabManager = tabManager + + super.init() + + NotificationCenter.default.addObserver(self, selector: #selector(SELUIPasteboardChanged), name: NSNotification.Name.UIPasteboardChanged, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(SELAppWillEnterForegroundNotification), name: NSNotification.Name.UIApplicationWillEnterForeground, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(SELDidRestoreSession), name: NotificationDidRestoreSession, object: nil) + } + + @objc private func SELUIPasteboardChanged() { + // UIPasteboardChanged gets triggered when calling UIPasteboard.general. + NotificationCenter.default.removeObserver(self, name: NSNotification.Name.UIPasteboardChanged, object: nil) + + UIPasteboard.general.asyncURL().uponQueue(.main) { res in + defer { + NotificationCenter.default.addObserver(self, selector: #selector(self.SELUIPasteboardChanged), name: NSNotification.Name.UIPasteboardChanged, object: nil) + } + + guard let copiedURL: URL? = res.successValue, + let url = copiedURL else { + return + } + self.lastDisplayedURL = url.absoluteString + } + } + + @objc private func SELAppWillEnterForegroundNotification() { + sessionStarted = true + checkIfShouldDisplayBar() + } + + private func observeURLForFirstTab(firstTab: Tab) { + if firstTab.webView == nil { + // Nothing to do; bail out. + firstTabLoaded = true + return + } + self.firstTab = firstTab + firstTab.observeURLChanges(delegate: self) + } + + @objc private func SELDidRestoreSession() { + DispatchQueue.main.sync { + if let tabManager = self.tabManager, + let firstTab = tabManager.selectedTab { + self.observeURLForFirstTab(firstTab: firstTab) + } else { + firstTabLoaded = true + } + + NotificationCenter.default.removeObserver(self, name: NotificationDidRestoreSession, object: nil) + + sessionRestored = true + checkIfShouldDisplayBar() + } + } + + func tab(_ tab: Tab, urlDidChangeTo url: URL) { + // Ugly hack to ensure we wait until we're finished restoring the session on the first tab + // before checking if we should display the clipboard bar. + guard sessionRestored, + !url.absoluteString.startsWith("\(WebServer.sharedInstance.base)/about/sessionrestore?history=") else { + return + } + + tab.removeURLChangeObserver(delegate: self) + firstTabLoaded = true + checkIfShouldDisplayBar() + } + + private func shouldDisplayBar(_ copiedURL: String) -> Bool { + if !sessionStarted || + !sessionRestored || + !firstTabLoaded || + isClipboardURLAlreadyDisplayed(copiedURL) || + self.prefs.intForKey(IntroViewControllerSeenProfileKey) == nil { + return false + } + sessionStarted = false + return true + } + + // If we already displayed this URL on the previous session, or in an already open + // tab, we shouldn't display it again + private func isClipboardURLAlreadyDisplayed(_ clipboardURL: String) -> Bool { + if lastDisplayedURL == clipboardURL { + return true + } + + if let url = URL(string: clipboardURL), + let _ = tabManager?.getTabFor(url) { + return true + } + return false + } + + func checkIfShouldDisplayBar() { + guard self.prefs.boolForKey("showClipboardBar") ?? false else { + // There's no point in doing any of this work unless the + // user has asked for it in settings. + return + } + UIPasteboard.general.asyncURL().uponQueue(.main) { res in + guard let copiedURL: URL? = res.successValue, + let url = copiedURL else { + return + } + + let absoluteString = url.absoluteString + + guard self.shouldDisplayBar(absoluteString) else { + return + } + + self.lastDisplayedURL = absoluteString + + self.clipboardToast = + ButtonToast( + labelText: Strings.GoToCopiedLink, + descriptionText: url.absoluteDisplayString, + buttonText: Strings.GoButtonTittle, + completion: { buttonPressed in + if buttonPressed { + self.delegate?.settingsOpenURLInNewTab(url) + } + }) + + if let toast = self.clipboardToast { + self.delegate?.shouldDisplay(clipboardBar: toast) + } + } + } +} diff --git a/mobile/ios/Client/Frontend/Browser/ContextMenuHelper.swift b/mobile/ios/Client/Frontend/Browser/ContextMenuHelper.swift new file mode 100644 index 0000000000..b3873facf9 --- /dev/null +++ b/mobile/ios/Client/Frontend/Browser/ContextMenuHelper.swift @@ -0,0 +1,108 @@ +/* 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 WebKit + +protocol ContextMenuHelperDelegate: class { + func contextMenuHelper(_ contextMenuHelper: ContextMenuHelper, didLongPressElements elements: ContextMenuHelper.Elements, gestureRecognizer: UIGestureRecognizer) + func contextMenuHelper(_ contextMenuHelper: ContextMenuHelper, didCancelGestureRecognizer: UIGestureRecognizer) +} + +class ContextMenuHelper: NSObject { + struct Elements { + let link: URL? + let image: URL? + } + + fileprivate weak var tab: Tab? + + weak var delegate: ContextMenuHelperDelegate? + + fileprivate var nativeHighlightLongPressRecognizer: UILongPressGestureRecognizer? + fileprivate var elements: Elements? + + required init(tab: Tab) { + super.init() + + self.tab = tab + + guard let path = Bundle.main.path(forResource: "ContextMenu", ofType: "js"), + let source = try? NSString(contentsOfFile: path, encoding: String.Encoding.utf8.rawValue) as String, + let webView = tab.webView else { + return + } + + let userScript = WKUserScript(source: source, injectionTime: WKUserScriptInjectionTime.atDocumentEnd, forMainFrameOnly: false) + webView.configuration.userContentController.addUserScript(userScript) + + nativeHighlightLongPressRecognizer = gestureRecognizerWithDescriptionFragment("action=_highlightLongPressRecognized:") as? UILongPressGestureRecognizer + + if let nativeLongPressRecognizer = gestureRecognizerWithDescriptionFragment("action=_longPressRecognized:") as? UILongPressGestureRecognizer { + nativeLongPressRecognizer.removeTarget(nil, action: nil) + nativeLongPressRecognizer.addTarget(self, action: #selector(longPressGestureDetected(_:))) + } + } + + func gestureRecognizerWithDescriptionFragment(_ descriptionFragment: String) -> UIGestureRecognizer? { + return tab?.webView?.scrollView.subviews.flatMap({ $0.gestureRecognizers }).joined().first(where: { $0.description.contains(descriptionFragment) }) + } + + func longPressGestureDetected(_ sender: UIGestureRecognizer) { + if sender.state == .cancelled { + delegate?.contextMenuHelper(self, didCancelGestureRecognizer: sender) + return + } + + guard sender.state == .began, let elements = self.elements else { + return + } + + delegate?.contextMenuHelper(self, didLongPressElements: elements, gestureRecognizer: sender) + + // To prevent the tapped link from proceeding with navigation, "cancel" the native WKWebView + // `_highlightLongPressRecognizer`. This preserves the original behavior as seen here: + // https://github.com/WebKit/webkit/blob/d591647baf54b4b300ca5501c21a68455429e182/Source/WebKit/UIProcess/ios/WKContentViewInteraction.mm#L1600-L1614 + if let nativeHighlightLongPressRecognizer = self.nativeHighlightLongPressRecognizer, + nativeHighlightLongPressRecognizer.isEnabled { + nativeHighlightLongPressRecognizer.isEnabled = false + nativeHighlightLongPressRecognizer.isEnabled = true + } + + self.elements = nil + } +} + +extension ContextMenuHelper: TabContentScript { + class func name() -> String { + return "ContextMenuHelper" + } + + func scriptMessageHandlerName() -> String? { + return "contextMenuMessageHandler" + } + + func userContentController(_ userContentController: WKUserContentController, didReceiveScriptMessage message: WKScriptMessage) { + guard let data = message.body as? [String: AnyObject] else { + return + } + + var linkURL: URL? + if let urlString = data["link"] as? String, + let escapedURLString = urlString.addingPercentEncoding(withAllowedCharacters: CharacterSet.URLAllowedCharacterSet()) { + linkURL = URL(string: escapedURLString) + } + + var imageURL: URL? + if let urlString = data["image"] as? String, + let escapedURLString = urlString.addingPercentEncoding(withAllowedCharacters: CharacterSet.URLAllowedCharacterSet()) { + imageURL = URL(string: escapedURLString) + } + + if linkURL != nil || imageURL != nil { + elements = Elements(link: linkURL, image: imageURL) + } else { + elements = nil + } + } +} diff --git a/mobile/ios/Client/Frontend/Browser/CustomSearchHandler.swift b/mobile/ios/Client/Frontend/Browser/CustomSearchHandler.swift new file mode 100644 index 0000000000..b2de348309 --- /dev/null +++ b/mobile/ios/Client/Frontend/Browser/CustomSearchHandler.swift @@ -0,0 +1,32 @@ +/* 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 WebKit + +class CustomSearchHelper: TabContentScript { + fileprivate weak var tab: Tab? + + required init(tab: Tab) { + self.tab = tab + if let path = Bundle.main.path(forResource: "CustomSearchHelper", ofType: "js") { + if let source = try? NSString(contentsOfFile: path, encoding: String.Encoding.utf8.rawValue) as String { + let userScript = WKUserScript(source: source, injectionTime: WKUserScriptInjectionTime.atDocumentEnd, forMainFrameOnly: true) + tab.webView!.configuration.userContentController.addUserScript(userScript) + } + } + } + + func scriptMessageHandlerName() -> String? { + return "customSearchHelper" + } + + func userContentController(_ userContentController: WKUserContentController, didReceiveScriptMessage message: WKScriptMessage) { + //We don't listen to messages because the BVC calls the searchHelper script by itself. + } + + class func name() -> String { + return "CustomSearchHelper" + } +} diff --git a/mobile/ios/Client/Frontend/Browser/ErrorPageHelper.swift b/mobile/ios/Client/Frontend/Browser/ErrorPageHelper.swift new file mode 100644 index 0000000000..9dfbe936b0 --- /dev/null +++ b/mobile/ios/Client/Frontend/Browser/ErrorPageHelper.swift @@ -0,0 +1,298 @@ +/* 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 WebKit +import GCDWebServers +import Shared +import Storage + +class ErrorPageHelper { + static let MozDomain = "mozilla" + static let MozErrorDownloadsNotEnabled = 100 + + fileprivate static let MessageOpenInSafari = "openInSafari" + fileprivate static let MessageCertVisitOnce = "certVisitOnce" + + // When an error page is intentionally loaded, its added to this set. If its in the set, we show + // it as an error page. If its not, we assume someone is trying to reload this page somehow, and + // we'll instead redirect back to the original URL. + fileprivate static var redirecting = [URL]() + + fileprivate static weak var certStore: CertStore? + + // Regardless of cause, NSURLErrorServerCertificateUntrusted is currently returned in all cases. + // Check the other cases in case this gets fixed in the future. + fileprivate static let CertErrors = [ + NSURLErrorServerCertificateUntrusted, + NSURLErrorServerCertificateHasBadDate, + NSURLErrorServerCertificateHasUnknownRoot, + NSURLErrorServerCertificateNotYetValid + ] + + // Error codes copied from Gecko. The ints corresponding to these codes were determined + // by inspecting the NSError in each of these cases. + fileprivate static let CertErrorCodes = [ + -9813: "SEC_ERROR_UNKNOWN_ISSUER", + -9814: "SEC_ERROR_EXPIRED_CERTIFICATE", + -9843: "SSL_ERROR_BAD_CERT_DOMAIN", + ] + + class func cfErrorToName(_ err: CFNetworkErrors) -> String { + switch err { + case .cfHostErrorHostNotFound: return "CFHostErrorHostNotFound" + case .cfHostErrorUnknown: return "CFHostErrorUnknown" + case .cfsocksErrorUnknownClientVersion: return "CFSOCKSErrorUnknownClientVersion" + case .cfsocksErrorUnsupportedServerVersion: return "CFSOCKSErrorUnsupportedServerVersion" + case .cfsocks4ErrorRequestFailed: return "CFSOCKS4ErrorRequestFailed" + case .cfsocks4ErrorIdentdFailed: return "CFSOCKS4ErrorIdentdFailed" + case .cfsocks4ErrorIdConflict: return "CFSOCKS4ErrorIdConflict" + case .cfsocks4ErrorUnknownStatusCode: return "CFSOCKS4ErrorUnknownStatusCode" + case .cfsocks5ErrorBadState: return "CFSOCKS5ErrorBadState" + case .cfsocks5ErrorBadResponseAddr: return "CFSOCKS5ErrorBadResponseAddr" + case .cfsocks5ErrorBadCredentials: return "CFSOCKS5ErrorBadCredentials" + case .cfsocks5ErrorUnsupportedNegotiationMethod: return "CFSOCKS5ErrorUnsupportedNegotiationMethod" + case .cfsocks5ErrorNoAcceptableMethod: return "CFSOCKS5ErrorNoAcceptableMethod" + case .cfftpErrorUnexpectedStatusCode: return "CFFTPErrorUnexpectedStatusCode" + case .cfErrorHTTPAuthenticationTypeUnsupported: return "CFErrorHTTPAuthenticationTypeUnsupported" + case .cfErrorHTTPBadCredentials: return "CFErrorHTTPBadCredentials" + case .cfErrorHTTPConnectionLost: return "CFErrorHTTPConnectionLost" + case .cfErrorHTTPParseFailure: return "CFErrorHTTPParseFailure" + case .cfErrorHTTPRedirectionLoopDetected: return "CFErrorHTTPRedirectionLoopDetected" + case .cfErrorHTTPBadURL: return "CFErrorHTTPBadURL" + case .cfErrorHTTPProxyConnectionFailure: return "CFErrorHTTPProxyConnectionFailure" + case .cfErrorHTTPBadProxyCredentials: return "CFErrorHTTPBadProxyCredentials" + case .cfErrorPACFileError: return "CFErrorPACFileError" + case .cfErrorPACFileAuth: return "CFErrorPACFileAuth" + case .cfErrorHTTPSProxyConnectionFailure: return "CFErrorHTTPSProxyConnectionFailure" + case .cfStreamErrorHTTPSProxyFailureUnexpectedResponseToCONNECTMethod: return "CFStreamErrorHTTPSProxyFailureUnexpectedResponseToCONNECTMethod" + + case .cfurlErrorBackgroundSessionInUseByAnotherProcess: return "CFURLErrorBackgroundSessionInUseByAnotherProcess" + case .cfurlErrorBackgroundSessionWasDisconnected: return "CFURLErrorBackgroundSessionWasDisconnected" + case .cfurlErrorUnknown: return "CFURLErrorUnknown" + case .cfurlErrorCancelled: return "CFURLErrorCancelled" + case .cfurlErrorBadURL: return "CFURLErrorBadURL" + case .cfurlErrorTimedOut: return "CFURLErrorTimedOut" + case .cfurlErrorUnsupportedURL: return "CFURLErrorUnsupportedURL" + case .cfurlErrorCannotFindHost: return "CFURLErrorCannotFindHost" + case .cfurlErrorCannotConnectToHost: return "CFURLErrorCannotConnectToHost" + case .cfurlErrorNetworkConnectionLost: return "CFURLErrorNetworkConnectionLost" + case .cfurlErrorDNSLookupFailed: return "CFURLErrorDNSLookupFailed" + case .cfurlErrorHTTPTooManyRedirects: return "CFURLErrorHTTPTooManyRedirects" + case .cfurlErrorResourceUnavailable: return "CFURLErrorResourceUnavailable" + case .cfurlErrorNotConnectedToInternet: return "CFURLErrorNotConnectedToInternet" + case .cfurlErrorRedirectToNonExistentLocation: return "CFURLErrorRedirectToNonExistentLocation" + case .cfurlErrorBadServerResponse: return "CFURLErrorBadServerResponse" + case .cfurlErrorUserCancelledAuthentication: return "CFURLErrorUserCancelledAuthentication" + case .cfurlErrorUserAuthenticationRequired: return "CFURLErrorUserAuthenticationRequired" + case .cfurlErrorZeroByteResource: return "CFURLErrorZeroByteResource" + case .cfurlErrorCannotDecodeRawData: return "CFURLErrorCannotDecodeRawData" + case .cfurlErrorCannotDecodeContentData: return "CFURLErrorCannotDecodeContentData" + case .cfurlErrorCannotParseResponse: return "CFURLErrorCannotParseResponse" + case .cfurlErrorInternationalRoamingOff: return "CFURLErrorInternationalRoamingOff" + case .cfurlErrorCallIsActive: return "CFURLErrorCallIsActive" + case .cfurlErrorDataNotAllowed: return "CFURLErrorDataNotAllowed" + case .cfurlErrorRequestBodyStreamExhausted: return "CFURLErrorRequestBodyStreamExhausted" + case .cfurlErrorFileDoesNotExist: return "CFURLErrorFileDoesNotExist" + case .cfurlErrorFileIsDirectory: return "CFURLErrorFileIsDirectory" + case .cfurlErrorNoPermissionsToReadFile: return "CFURLErrorNoPermissionsToReadFile" + case .cfurlErrorDataLengthExceedsMaximum: return "CFURLErrorDataLengthExceedsMaximum" + case .cfurlErrorSecureConnectionFailed: return "CFURLErrorSecureConnectionFailed" + case .cfurlErrorServerCertificateHasBadDate: return "CFURLErrorServerCertificateHasBadDate" + case .cfurlErrorServerCertificateUntrusted: return "CFURLErrorServerCertificateUntrusted" + case .cfurlErrorServerCertificateHasUnknownRoot: return "CFURLErrorServerCertificateHasUnknownRoot" + case .cfurlErrorServerCertificateNotYetValid: return "CFURLErrorServerCertificateNotYetValid" + case .cfurlErrorClientCertificateRejected: return "CFURLErrorClientCertificateRejected" + case .cfurlErrorClientCertificateRequired: return "CFURLErrorClientCertificateRequired" + case .cfurlErrorCannotLoadFromNetwork: return "CFURLErrorCannotLoadFromNetwork" + case .cfurlErrorCannotCreateFile: return "CFURLErrorCannotCreateFile" + case .cfurlErrorCannotOpenFile: return "CFURLErrorCannotOpenFile" + case .cfurlErrorCannotCloseFile: return "CFURLErrorCannotCloseFile" + case .cfurlErrorCannotWriteToFile: return "CFURLErrorCannotWriteToFile" + case .cfurlErrorCannotRemoveFile: return "CFURLErrorCannotRemoveFile" + case .cfurlErrorCannotMoveFile: return "CFURLErrorCannotMoveFile" + case .cfurlErrorDownloadDecodingFailedMidStream: return "CFURLErrorDownloadDecodingFailedMidStream" + case .cfurlErrorDownloadDecodingFailedToComplete: return "CFURLErrorDownloadDecodingFailedToComplete" + + case .cfhttpCookieCannotParseCookieFile: return "CFHTTPCookieCannotParseCookieFile" + case .cfNetServiceErrorUnknown: return "CFNetServiceErrorUnknown" + case .cfNetServiceErrorCollision: return "CFNetServiceErrorCollision" + case .cfNetServiceErrorNotFound: return "CFNetServiceErrorNotFound" + case .cfNetServiceErrorInProgress: return "CFNetServiceErrorInProgress" + case .cfNetServiceErrorBadArgument: return "CFNetServiceErrorBadArgument" + case .cfNetServiceErrorCancel: return "CFNetServiceErrorCancel" + case .cfNetServiceErrorInvalid: return "CFNetServiceErrorInvalid" + case .cfNetServiceErrorTimeout: return "CFNetServiceErrorTimeout" + case .cfNetServiceErrorDNSServiceFailure: return "CFNetServiceErrorDNSServiceFailure" + default: return "Unknown" + } + } + + class func register(_ server: WebServer, certStore: CertStore?) { + self.certStore = certStore + + server.registerHandlerForMethod("GET", module: "errors", resource: "error.html", handler: { (request) -> GCDWebServerResponse! in + guard let url = request?.url.originalURLFromErrorURL else { + return GCDWebServerResponse(statusCode: 404) + } + + guard let index = self.redirecting.index(of: url) else { + return GCDWebServerDataResponse(redirect: url, permanent: false) + } + + self.redirecting.remove(at: index) + + guard let code = request?.query["code"] as? String, + let errCode = Int(code), + let errDescription = request?.query["description"] as? String, + let errURLString = request?.query["url"] as? String, + let errURLDomain = URL(string: errURLString)?.host, + var errDomain = request?.query["domain"] as? String else { + return GCDWebServerResponse(statusCode: 404) + } + + var asset = Bundle.main.path(forResource: "NetError", ofType: "html") + var variables = [ + "error_code": "\(errCode)", + "error_title": errDescription, + "short_description": errDomain, + ] + + let tryAgain = NSLocalizedString("Try again", tableName: "ErrorPages", comment: "Shown in error pages on a button that will try to load the page again") + var actions = "" + + if errDomain == kCFErrorDomainCFNetwork as String { + if let code = CFNetworkErrors(rawValue: Int32(errCode)) { + errDomain = self.cfErrorToName(code) + } + } else if errDomain == ErrorPageHelper.MozDomain { + if errCode == ErrorPageHelper.MozErrorDownloadsNotEnabled { + let downloadInSafari = NSLocalizedString("Open in Safari", tableName: "ErrorPages", comment: "Shown in error pages for files that can't be shown and need to be downloaded.") + + // Overwrite the normal try-again action. + actions = "" + } + errDomain = "" + } else if CertErrors.contains(errCode) { + guard let certError = request?.query["certerror"] as? String else { + return GCDWebServerResponse(statusCode: 404) + } + + asset = Bundle.main.path(forResource: "CertError", ofType: "html") + actions = "" + variables["error_title"] = Strings.ErrorPagesCertWarningTitle + variables["cert_error"] = certError + variables["long_description"] = String(format: Strings.ErrorPagesCertWarningDescription, "\(errURLDomain)") + variables["advanced_button"] = Strings.ErrorPagesAdvancedButton + variables["warning_description"] = Strings.ErrorPagesCertWarningDescription + variables["warning_advanced1"] = Strings.ErrorPagesAdvancedWarning1 + variables["warning_advanced2"] = Strings.ErrorPagesAdvancedWarning2 + variables["warning_actions"] = + "

\(Strings.ErrorPagesVisitOnceButton)

" + } + + variables["actions"] = actions + + let response = GCDWebServerDataResponse(htmlTemplate: asset, variables: variables) + response?.setValue("no cache", forAdditionalHeader: "Pragma") + response?.setValue("no-cache,must-revalidate", forAdditionalHeader: "Cache-Control") + response?.setValue(Date().description, forAdditionalHeader: "Expires") + return response + }) + + server.registerHandlerForMethod("GET", module: "errors", resource: "NetError.css", handler: { (request) -> GCDWebServerResponse! in + let path = Bundle(for: self).path(forResource: "NetError", ofType: "css")! + return GCDWebServerDataResponse(data: try? Data(contentsOf: URL(fileURLWithPath: path)), contentType: "text/css") + }) + + server.registerHandlerForMethod("GET", module: "errors", resource: "CertError.css", handler: { (request) -> GCDWebServerResponse! in + let path = Bundle(for: self).path(forResource: "CertError", ofType: "css")! + return GCDWebServerDataResponse(data: try? Data(contentsOf: URL(fileURLWithPath: path)), contentType: "text/css") + }) + } + + func showPage(_ error: NSError, forUrl url: URL, inWebView webView: WKWebView) { + // Don't show error pages for error pages. + if url.isErrorPageURL { + if let previousURL = url.originalURLFromErrorURL, + let index = ErrorPageHelper.redirecting.index(of: previousURL) { + ErrorPageHelper.redirecting.remove(at: index) + } + return + } + + // Add this page to the redirecting list. This will cause the server to actually show the error page + // (instead of redirecting to the original URL). + ErrorPageHelper.redirecting.append(url) + + var components = URLComponents(string: WebServer.sharedInstance.base + "/errors/error.html")! + var queryItems = [ + URLQueryItem(name: "url", value: url.absoluteString), + URLQueryItem(name: "code", value: String(error.code)), + URLQueryItem(name: "domain", value: error.domain), + URLQueryItem(name: "description", value: error.localizedDescription) + ] + + // If this is an invalid certificate, show a certificate error allowing the + // user to go back or continue. The certificate itself is encoded and added as + // a query parameter to the error page URL; we then read the certificate from + // the URL if the user wants to continue. + if ErrorPageHelper.CertErrors.contains(error.code), + let certChain = error.userInfo["NSErrorPeerCertificateChainKey"] as? [SecCertificate], + let cert = certChain.first, + let underlyingError = error.userInfo[NSUnderlyingErrorKey] as? NSError, + let certErrorCode = underlyingError.userInfo["_kCFStreamErrorCodeKey"] as? Int { + let encodedCert = (SecCertificateCopyData(cert) as Data).base64EncodedString + queryItems.append(URLQueryItem(name: "badcert", value: encodedCert)) + + let certError = ErrorPageHelper.CertErrorCodes[certErrorCode] ?? "" + queryItems.append(URLQueryItem(name: "certerror", value: String(certError))) + } + + components.queryItems = queryItems + webView.load(PrivilegedRequest(url: components.url!) as URLRequest) + } +} + +extension ErrorPageHelper: TabContentScript { + static func name() -> String { + return "ErrorPageHelper" + } + + func scriptMessageHandlerName() -> String? { + return "errorPageHelperMessageManager" + } + + func userContentController(_ userContentController: WKUserContentController, didReceiveScriptMessage message: WKScriptMessage) { + if let errorURL = message.frameInfo.request.url, errorURL.isErrorPageURL, + let res = message.body as? [String: String], + let originalURL = errorURL.originalURLFromErrorURL, + let type = res["type"] { + + switch type { + case ErrorPageHelper.MessageOpenInSafari: + UIApplication.shared.openURL(originalURL) + case ErrorPageHelper.MessageCertVisitOnce: + if let cert = certFromErrorURL(errorURL), + let host = originalURL.host { + let origin = "\(host):\(originalURL.port ?? 443)" + ErrorPageHelper.certStore?.addCertificate(cert, forOrigin: origin) + _ = message.webView?.reload() + } + default: + assertionFailure("Unknown error message") + } + } + } + + fileprivate func certFromErrorURL(_ url: URL) -> SecCertificate? { + let components = URLComponents(url: url, resolvingAgainstBaseURL: false) + if let encodedCert = components?.queryItems?.filter({ $0.name == "badcert" }).first?.value, + let certData = Data(base64Encoded: encodedCert, options: []) { + return SecCertificateCreateWithData(nil, certData as CFData) + } + + return nil + } +} diff --git a/mobile/ios/Client/Frontend/Browser/FaviconManager.swift b/mobile/ios/Client/Frontend/Browser/FaviconManager.swift new file mode 100644 index 0000000000..edbff93413 --- /dev/null +++ b/mobile/ios/Client/Frontend/Browser/FaviconManager.swift @@ -0,0 +1,145 @@ +/* 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 WebKit +import Shared +import Storage +import SDWebImage +import Deferred +import Sync + +class FaviconManager: TabContentScript { + static let FaviconDidLoad = "FaviconManagerFaviconDidLoad" + + let profile: Profile! + weak var tab: Tab? + + static let maximumFaviconSize = 1 * 1024 * 1024 // 1 MiB file size limit + + init(tab: Tab, profile: Profile) { + self.profile = profile + self.tab = tab + + if let path = Bundle.main.path(forResource: "Favicons", ofType: "js") { + if let source = try? NSString(contentsOfFile: path, encoding: String.Encoding.utf8.rawValue) as String { + let userScript = WKUserScript(source: source, injectionTime: WKUserScriptInjectionTime.atDocumentEnd, forMainFrameOnly: true) + tab.webView!.configuration.userContentController.addUserScript(userScript) + } + } + } + + class func name() -> String { + return "FaviconsManager" + } + + func scriptMessageHandlerName() -> String? { + return "faviconsMessageHandler" + } + + fileprivate func loadFavicons(_ tab: Tab, profile: Profile, favicons: [Favicon]) -> Deferred<[Maybe]> { + var deferreds: [() -> Deferred>] + deferreds = favicons.map { favicon in + return { [weak tab] () -> Deferred> in + if let tab = tab, + let url = URL(string: favicon.url), + let currentURL = tab.url { + return self.getFavicon(tab, iconUrl: url, currentURL: currentURL, icon: favicon, profile: profile) + } else { + return deferMaybe(FaviconError()) + } + } + } + return all(deferreds.map({$0()})) + } + + func getFavicon(_ tab: Tab, iconUrl: URL, currentURL: URL, icon: Favicon, profile: Profile) -> Deferred> { + let deferred = Deferred>() + let manager = SDWebImageManager.shared() + let options: [SDWebImageOptions] = tab.isPrivate ? [.lowPriority, .cacheMemoryOnly] : [.lowPriority] + let url = currentURL.absoluteString + let site = Site(url: url, title: "") + + weak var tab = tab + + func loadImageCompleted(_ img: UIImage?, _ url: URL?) { + guard let tab = tab, let img = img, let urlString = url?.absoluteString else { + deferred.fill(Maybe(failure: FaviconError())) + return + } + + let fav = Favicon(url: urlString, date: Date(), type: icon.type) + fav.width = Int(img.size.width) + fav.height = Int(img.size.height) + + if !tab.isPrivate { + if tab.favicons.isEmpty { + self.makeFaviconAvailable(tab, atURL: currentURL, favicon: fav, withImage: img) + } + tab.favicons.append(fav) + self.profile.favicons.addFavicon(fav, forSite: site).upon { _ in + deferred.fill(Maybe(success: fav)) + } + } else { + tab.favicons.append(fav) + deferred.fill(Maybe(success: fav)) + } + } + + var fetch: SDWebImageOperation? = nil + fetch = manager.loadImage(with: iconUrl, options: SDWebImageOptions(options), + progress: { (receivedSize, expectedSize, _) in + if receivedSize > FaviconManager.maximumFaviconSize || expectedSize > FaviconManager.maximumFaviconSize { + fetch?.cancel() + } + }, + completed: { (img, _, _, _, _, url) in + loadImageCompleted(img, url) + }) + return deferred + } + + func userContentController(_ userContentController: WKUserContentController, didReceiveScriptMessage message: WKScriptMessage) { + self.tab?.favicons.removeAll(keepingCapacity: false) + if let tab = self.tab, let currentURL = tab.url { + var favicons = [Favicon]() + if let icons = message.body as? [String: Int] { + for icon in icons { + if let _ = URL(string: icon.0), let iconType = IconType(rawValue: icon.1) { + let favicon = Favicon(url: icon.0, date: Date(), type: iconType) + favicons.append(favicon) + } + } + } + loadFavicons(tab, profile: profile, favicons: favicons).uponQueue(DispatchQueue.main) { result in + let results = result.flatMap({ $0.successValue }) + let faviconsReadOnly = favicons + if results.count == 1 && faviconsReadOnly[0].type == .guess { + // No favicon is indicated in the HTML + self.noFaviconAvailable(tab, atURL: currentURL as URL) + } + + NotificationCenter.default.post(name: NSNotification.Name(rawValue: FaviconManager.FaviconDidLoad), object: tab) + } + } + } + + func makeFaviconAvailable(_ tab: Tab, atURL url: URL, favicon: Favicon, withImage image: UIImage) { + // XXX: Bug 1390200 - Disable NSUserActivity/CoreSpotlight temporarily + // let helper = tab.getHelper(name: "SpotlightHelper") as? SpotlightHelper + // helper?.updateImage(image, forURL: url) + } + + func noFaviconAvailable(_ tab: Tab, atURL url: URL) { + // XXX: Bug 1390200 - Disable NSUserActivity/CoreSpotlight temporarily + // let helper = tab.getHelper(name: "SpotlightHelper") as? SpotlightHelper + // helper?.updateImage(forURL: url) + } +} + +class FaviconError: MaybeErrorType { + internal var description: String { + return "No Image Loaded" + } +} diff --git a/mobile/ios/Client/Frontend/Browser/FindInPageBar.swift b/mobile/ios/Client/Frontend/Browser/FindInPageBar.swift new file mode 100644 index 0000000000..8edf49516a --- /dev/null +++ b/mobile/ios/Client/Frontend/Browser/FindInPageBar.swift @@ -0,0 +1,167 @@ +/* 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 UIKit + +protocol FindInPageBarDelegate: class { + func findInPage(_ findInPage: FindInPageBar, didTextChange text: String) + func findInPage(_ findInPage: FindInPageBar, didFindPreviousWithText text: String) + func findInPage(_ findInPage: FindInPageBar, didFindNextWithText text: String) + func findInPageDidPressClose(_ findInPage: FindInPageBar) +} + +private struct FindInPageUX { + static let ButtonColor = UIColor.black + static let MatchCountColor = UIColor.lightGray + static let MatchCountFont = UIConstants.DefaultChromeFont + static let SearchTextColor = UIColor(rgb: 0xe66000) + static let SearchTextFont = UIConstants.DefaultChromeFont + static let TopBorderColor = UIColor(rgb: 0xEEEEEE) +} + +class FindInPageBar: UIView { + weak var delegate: FindInPageBarDelegate? + fileprivate let searchText = UITextField() + fileprivate let matchCountView = UILabel() + fileprivate let previousButton = UIButton() + fileprivate let nextButton = UIButton() + + var currentResult = 0 { + didSet { + matchCountView.text = "\(currentResult)/\(totalResults)" + } + } + + var totalResults = 0 { + didSet { + matchCountView.text = "\(currentResult)/\(totalResults)" + previousButton.isEnabled = totalResults > 1 + nextButton.isEnabled = previousButton.isEnabled + } + } + + var text: String? { + get { + return searchText.text + } + + set { + searchText.text = newValue + SELdidTextChange(searchText) + } + } + + override init(frame: CGRect) { + super.init(frame: frame) + + backgroundColor = UIColor.white + + searchText.addTarget(self, action: #selector(FindInPageBar.SELdidTextChange(_:)), for: UIControlEvents.editingChanged) + searchText.textColor = FindInPageUX.SearchTextColor + searchText.font = FindInPageUX.SearchTextFont + searchText.autocapitalizationType = UITextAutocapitalizationType.none + searchText.autocorrectionType = UITextAutocorrectionType.no + searchText.inputAssistantItem.leadingBarButtonGroups = [] + searchText.inputAssistantItem.trailingBarButtonGroups = [] + searchText.enablesReturnKeyAutomatically = true + searchText.returnKeyType = .search + searchText.accessibilityIdentifier = "FindInPage.searchField" + addSubview(searchText) + + matchCountView.textColor = FindInPageUX.MatchCountColor + matchCountView.font = FindInPageUX.MatchCountFont + matchCountView.isHidden = true + matchCountView.accessibilityIdentifier = "FindInPage.matchCount" + addSubview(matchCountView) + + previousButton.setImage(UIImage(named: "find_previous"), for: UIControlState()) + previousButton.setTitleColor(FindInPageUX.ButtonColor, for: UIControlState()) + previousButton.accessibilityLabel = NSLocalizedString("Previous in-page result", tableName: "FindInPage", comment: "Accessibility label for previous result button in Find in Page Toolbar.") + previousButton.addTarget(self, action: #selector(FindInPageBar.SELdidFindPrevious(_:)), for: UIControlEvents.touchUpInside) + previousButton.accessibilityIdentifier = "FindInPage.find_previous" + addSubview(previousButton) + + nextButton.setImage(UIImage(named: "find_next"), for: UIControlState()) + nextButton.setTitleColor(FindInPageUX.ButtonColor, for: UIControlState()) + nextButton.accessibilityLabel = NSLocalizedString("Next in-page result", tableName: "FindInPage", comment: "Accessibility label for next result button in Find in Page Toolbar.") + nextButton.addTarget(self, action: #selector(FindInPageBar.SELdidFindNext(_:)), for: UIControlEvents.touchUpInside) + nextButton.accessibilityIdentifier = "FindInPage.find_next" + addSubview(nextButton) + + let closeButton = UIButton() + closeButton.setImage(UIImage(named: "find_close"), for: UIControlState()) + closeButton.setTitleColor(FindInPageUX.ButtonColor, for: UIControlState()) + closeButton.accessibilityLabel = NSLocalizedString("Done", tableName: "FindInPage", comment: "Done button in Find in Page Toolbar.") + closeButton.addTarget(self, action: #selector(FindInPageBar.SELdidPressClose(_:)), for: UIControlEvents.touchUpInside) + closeButton.accessibilityIdentifier = "FindInPage.close" + addSubview(closeButton) + + let topBorder = UIView() + topBorder.backgroundColor = FindInPageUX.TopBorderColor + addSubview(topBorder) + + searchText.snp.makeConstraints { make in + make.leading.top.bottom.equalTo(self).inset(UIEdgeInsets(top: 0, left: 8, bottom: 0, right: 0)) + } + searchText.setContentHuggingPriority(UILayoutPriorityDefaultLow, for: UILayoutConstraintAxis.horizontal) + searchText.setContentCompressionResistancePriority(UILayoutPriorityDefaultLow, for: UILayoutConstraintAxis.horizontal) + + matchCountView.snp.makeConstraints { make in + make.leading.equalTo(searchText.snp.trailing) + make.centerY.equalTo(self) + } + matchCountView.setContentHuggingPriority(UILayoutPriorityDefaultHigh, for: UILayoutConstraintAxis.horizontal) + matchCountView.setContentCompressionResistancePriority(UILayoutPriorityDefaultHigh, for: UILayoutConstraintAxis.horizontal) + + previousButton.snp.makeConstraints { make in + make.leading.equalTo(matchCountView.snp.trailing) + make.size.equalTo(self.snp.height) + make.centerY.equalTo(self) + } + + nextButton.snp.makeConstraints { make in + make.leading.equalTo(previousButton.snp.trailing) + make.size.equalTo(self.snp.height) + make.centerY.equalTo(self) + } + + closeButton.snp.makeConstraints { make in + make.leading.equalTo(nextButton.snp.trailing) + make.size.equalTo(self.snp.height) + make.trailing.centerY.equalTo(self) + } + + topBorder.snp.makeConstraints { make in + make.height.equalTo(1) + make.left.right.top.equalTo(self) + } + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + @discardableResult override func becomeFirstResponder() -> Bool { + searchText.becomeFirstResponder() + return super.becomeFirstResponder() + } + + @objc fileprivate func SELdidFindPrevious(_ sender: UIButton) { + delegate?.findInPage(self, didFindPreviousWithText: searchText.text ?? "") + } + + @objc fileprivate func SELdidFindNext(_ sender: UIButton) { + delegate?.findInPage(self, didFindNextWithText: searchText.text ?? "") + } + + @objc fileprivate func SELdidTextChange(_ sender: UITextField) { + matchCountView.isHidden = searchText.text?.isEmpty ?? true + delegate?.findInPage(self, didTextChange: searchText.text ?? "") + } + + @objc fileprivate func SELdidPressClose(_ sender: UIButton) { + delegate?.findInPageDidPressClose(self) + } +} diff --git a/mobile/ios/Client/Frontend/Browser/FindInPageHelper.swift b/mobile/ios/Client/Frontend/Browser/FindInPageHelper.swift new file mode 100644 index 0000000000..db7d7a63f2 --- /dev/null +++ b/mobile/ios/Client/Frontend/Browser/FindInPageHelper.swift @@ -0,0 +1,46 @@ +/* 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 WebKit + +protocol FindInPageHelperDelegate: class { + func findInPageHelper(_ findInPageHelper: FindInPageHelper, didUpdateCurrentResult currentResult: Int) + func findInPageHelper(_ findInPageHelper: FindInPageHelper, didUpdateTotalResults totalResults: Int) +} + +class FindInPageHelper: TabContentScript { + weak var delegate: FindInPageHelperDelegate? + fileprivate weak var tab: Tab? + + class func name() -> String { + return "FindInPage" + } + + required init(tab: Tab) { + self.tab = tab + + if let path = Bundle.main.path(forResource: "FindInPage", ofType: "js"), let source = try? NSString(contentsOfFile: path, encoding: String.Encoding.utf8.rawValue) as String { + let userScript = WKUserScript(source: source, injectionTime: WKUserScriptInjectionTime.atDocumentEnd, forMainFrameOnly: true) + tab.webView!.configuration.userContentController.addUserScript(userScript) + } + } + + func scriptMessageHandlerName() -> String? { + return "findInPageHandler" + } + + func userContentController(_ userContentController: WKUserContentController, didReceiveScriptMessage message: WKScriptMessage) { + let data = message.body as! [String: Int] + + if let currentResult = data["currentResult"] { + delegate?.findInPageHelper(self, didUpdateCurrentResult: currentResult) + } + + if let totalResults = data["totalResults"] { + delegate?.findInPageHelper(self, didUpdateTotalResults: totalResults) + } + } +} diff --git a/mobile/ios/Client/Frontend/Browser/HistoryStateHelper.swift b/mobile/ios/Client/Frontend/Browser/HistoryStateHelper.swift new file mode 100644 index 0000000000..931247bbeb --- /dev/null +++ b/mobile/ios/Client/Frontend/Browser/HistoryStateHelper.swift @@ -0,0 +1,45 @@ +/* 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 WebKit + +protocol HistoryStateHelperDelegate: class { + func historyStateHelper(_ historyStateHelper: HistoryStateHelper, didPushOrReplaceStateInTab tab: Tab) +} + +// This tab helper is needed for injecting a user script into the +// WKWebView that intercepts calls to `history.pushState()` and +// `history.replaceState()` so that the BrowserViewController is +// notified when the user navigates a single-page web application. +class HistoryStateHelper: TabContentScript { + weak var delegate: HistoryStateHelperDelegate? + fileprivate weak var tab: Tab? + + required init(tab: Tab) { + self.tab = tab + if let path = Bundle.main.path(forResource: "HistoryStateHelper", ofType: "js") { + if let source = try? NSString(contentsOfFile: path, encoding: String.Encoding.utf8.rawValue) as String { + let userScript = WKUserScript(source: source, injectionTime: WKUserScriptInjectionTime.atDocumentEnd, forMainFrameOnly: true) + tab.webView!.configuration.userContentController.addUserScript(userScript) + } + } + } + + func scriptMessageHandlerName() -> String? { + return "historyStateHelper" + } + + func userContentController(_ userContentController: WKUserContentController, didReceiveScriptMessage message: WKScriptMessage) { + if let tab = tab { + DispatchQueue.main.async { + self.delegate?.historyStateHelper(self, didPushOrReplaceStateInTab: tab) + } + } + } + + class func name() -> String { + return "HistoryStateHelper" + } +} diff --git a/mobile/ios/Client/Frontend/Browser/HomePageHelper.swift b/mobile/ios/Client/Frontend/Browser/HomePageHelper.swift new file mode 100644 index 0000000000..8940c20807 --- /dev/null +++ b/mobile/ios/Client/Frontend/Browser/HomePageHelper.swift @@ -0,0 +1,71 @@ +/* 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 XCGLogger + +private let log = Logger.browserLogger + +struct HomePageConstants { + static let HomePageURLPrefKey = "HomePageURLPref" + static let DefaultHomePageURLPrefKey = PrefsKeys.KeyDefaultHomePageURL +} + +class HomePageHelper { + + let prefs: Prefs + + var currentURL: URL? { + get { + return HomePageAccessors.getHomePage(prefs) + } + set { + if let url = newValue, url.isWebPage(includeDataURIs: false) && !url.isLocal { + prefs.setString(url.absoluteString, forKey: HomePageConstants.HomePageURLPrefKey) + } else { + prefs.removeObjectForKey(HomePageConstants.HomePageURLPrefKey) + } + } + } + + var defaultURLString: String? { + return HomePageAccessors.getDefaultHomePageString(prefs) + } + + var isHomePageAvailable: Bool { return currentURL != nil } + + init(prefs: Prefs) { + self.prefs = prefs + } + + func openHomePage(_ tab: Tab) { + guard let url = currentURL else { + // this should probably never happen. + log.error("User requested a homepage that wasn't a valid URL") + return + } + tab.loadRequest(URLRequest(url: url)) + } + + func openHomePage(inTab tab: Tab, presentAlertOn viewController: UIViewController?) { + if isHomePageAvailable { + openHomePage(tab) + } else { + setHomePage(toTab: tab, presentAlertOn: viewController) + } + } + + func setHomePage(toTab tab: Tab, presentAlertOn viewController: UIViewController?) { + let alertController = UIAlertController( + title: Strings.SetHomePageDialogTitle, + message: Strings.SetHomePageDialogMessage, + preferredStyle: UIAlertControllerStyle.alert) + alertController.addAction(UIAlertAction(title: Strings.SetHomePageDialogNo, style: .cancel, handler: nil)) + alertController.addAction(UIAlertAction(title: Strings.SetHomePageDialogYes, style: .default) { _ in + self.currentURL = tab.url as URL? + }) + viewController?.present(alertController, animated: true, completion: nil) + } +} diff --git a/mobile/ios/Client/Frontend/Browser/LocalRequestHelper.swift b/mobile/ios/Client/Frontend/Browser/LocalRequestHelper.swift new file mode 100644 index 0000000000..b01fefd6ae --- /dev/null +++ b/mobile/ios/Client/Frontend/Browser/LocalRequestHelper.swift @@ -0,0 +1,32 @@ +/* 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 WebKit + +class LocalRequestHelper: TabContentScript { + func scriptMessageHandlerName() -> String? { + return "localRequestHelper" + } + + func userContentController(_ userContentController: WKUserContentController, didReceiveScriptMessage message: WKScriptMessage) { + guard message.frameInfo.request.url?.isLocal ?? false else { return } + + let params = message.body as! [String: String] + + if params["type"] == "load", + let urlString = params["url"], + let url = URL(string: urlString) { + _ = message.webView?.load(PrivilegedRequest(url: url) as URLRequest) + } else if params["type"] == "reload" { + _ = message.webView?.reload() + } else { + assertionFailure("Invalid message: \(message.body)") + } + } + + class func name() -> String { + return "LocalRequestHelper" + } +} diff --git a/mobile/ios/Client/Frontend/Browser/LoginsHelper.swift b/mobile/ios/Client/Frontend/Browser/LoginsHelper.swift new file mode 100644 index 0000000000..6605e3a138 --- /dev/null +++ b/mobile/ios/Client/Frontend/Browser/LoginsHelper.swift @@ -0,0 +1,234 @@ +/* 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 +import WebKit +import Deferred +import SwiftyJSON + +private let log = Logger.browserLogger + +class LoginsHelper: TabContentScript { + fileprivate weak var tab: Tab? + fileprivate let profile: Profile + fileprivate var snackBar: SnackBar? + + // Exposed for mocking purposes + var logins: BrowserLogins { + return profile.logins + } + + class func name() -> String { + return "LoginsHelper" + } + + required init(tab: Tab, profile: Profile) { + self.tab = tab + self.profile = profile + + if let path = Bundle.main.path(forResource: "LoginsHelper", ofType: "js"), let source = try? NSString(contentsOfFile: path, encoding: String.Encoding.utf8.rawValue) as String { + let userScript = WKUserScript(source: source, injectionTime: WKUserScriptInjectionTime.atDocumentEnd, forMainFrameOnly: false) + tab.webView!.configuration.userContentController.addUserScript(userScript) + } + } + + func scriptMessageHandlerName() -> String? { + return "loginsManagerMessageHandler" + } + + func userContentController(_ userContentController: WKUserContentController, didReceiveScriptMessage message: WKScriptMessage) { + guard var res = message.body as? [String: AnyObject] else { return } + guard let type = res["type"] as? String else { return } + + // Check to see that we're in the foreground before trying to check the logins. We want to + // make sure we don't try accessing the logins database while we're backgrounded to avoid + // the system from terminating our app due to background disk access. + // + // See https://bugzilla.mozilla.org/show_bug.cgi?id=1307822 for details. + guard UIApplication.shared.applicationState == .active && !profile.isShutdown else { + return + } + + // We don't use the WKWebView's URL since the page can spoof the URL by using document.location + // right before requesting login data. See bug 1194567 for more context. + if let url = message.frameInfo.request.url { + // Since responses go to the main frame, make sure we only listen for main frame requests + // to avoid XSS attacks. + if message.frameInfo.isMainFrame && type == "request" { + res["username"] = "" as AnyObject? + res["password"] = "" as AnyObject? + if let login = Login.fromScript(url, script: res), + let requestId = res["requestId"] as? String { + requestLogins(login, requestId: requestId) + } + } else if type == "submit" { + if self.profile.prefs.boolForKey("saveLogins") ?? true { + if let login = Login.fromScript(url, script: res) { + setCredentials(login) + } + } + } + } + } + + class func replace(_ base: String, keys: [String], replacements: [String]) -> NSMutableAttributedString { + var ranges = [NSRange]() + var string = base + for (index, key) in keys.enumerated() { + let replace = replacements[index] + let range = string.range(of: key, + options: NSString.CompareOptions.literal, + range: nil, + locale: nil)! + string.replaceSubrange(range, with: replace) + let nsRange = NSRange(location: string.characters.distance(from: string.startIndex, to: range.lowerBound), + length: replace.characters.count) + ranges.append(nsRange) + } + + var attributes = [String: AnyObject]() + attributes[NSFontAttributeName] = UIFont.systemFont(ofSize: 13, weight: UIFontWeightRegular) + attributes[NSForegroundColorAttributeName] = UIColor.darkGray + let attr = NSMutableAttributedString(string: string, attributes: attributes) + let font: UIFont = UIFont.systemFont(ofSize: 13, weight: UIFontWeightMedium) + for range in ranges { + attr.addAttribute(NSFontAttributeName, value: font, range: range) + } + return attr + } + + func getLoginsForProtectionSpace(_ protectionSpace: URLProtectionSpace) -> Deferred>> { + return profile.logins.getLoginsForProtectionSpace(protectionSpace) + } + + func updateLoginByGUID(_ guid: GUID, new: LoginData, significant: Bool) -> Success { + return profile.logins.updateLoginByGUID(guid, new: new, significant: significant) + } + + func setCredentials(_ login: LoginData) { + if login.password.isEmpty { + log.debug("Empty password") + return + } + + profile.logins + .getLoginsForProtectionSpace(login.protectionSpace, withUsername: login.username) + .uponQueue(DispatchQueue.main) { res in + if let data = res.successValue { + log.debug("Found \(data.count) logins.") + for saved in data { + if let saved = saved { + if saved.password == login.password { + self.profile.logins.addUseOfLoginByGUID(saved.guid) + return + } + + self.promptUpdateFromLogin(login: saved, toLogin: login) + return + } + } + } + + self.promptSave(login) + } + } + + fileprivate func promptSave(_ login: LoginData) { + guard login.isValid.isSuccess else { + return + } + + let promptMessage: NSAttributedString + if let username = login.username { + let promptStringFormat = NSLocalizedString("LoginsHelper.PromptSaveLogin.Title", value: "Save login %@ for %@?", comment: "Prompt for saving a login. The first parameter is the username being saved. The second parameter is the hostname of the site.") + promptMessage = NSAttributedString(string: String(format: promptStringFormat, username, login.hostname)) + } else { + let promptStringFormat = NSLocalizedString("LoginsHelper.PromptSavePassword.Title", value: "Save password for %@?", comment: "Prompt for saving a password with no username. The parameter is the hostname of the site.") + promptMessage = NSAttributedString(string: String(format: promptStringFormat, login.hostname)) + } + + if snackBar != nil { + tab?.removeSnackbar(snackBar!) + } + + snackBar = TimerSnackBar(attrText: promptMessage, + img: UIImage(named: "key"), + buttons: [ + SnackButton(title: Strings.LoginsHelperDontSaveButtonTitle, accessibilityIdentifier: "SaveLoginPrompt.dontSaveButton", callback: { (bar: SnackBar) -> Void in + self.tab?.removeSnackbar(bar) + self.snackBar = nil + return + }), + + SnackButton(title: Strings.LoginsHelperSaveLoginButtonTitle, accessibilityIdentifier: "SaveLoginPrompt.saveLoginButton", callback: { (bar: SnackBar) -> Void in + self.tab?.removeSnackbar(bar) + self.snackBar = nil + self.profile.logins.addLogin(login) + + LeanPlumClient.shared.track(event: .savedLoginAndPassword) + }) + ]) + tab?.addSnackbar(snackBar!) + } + + fileprivate func promptUpdateFromLogin(login old: LoginData, toLogin new: LoginData) { + guard new.isValid.isSuccess else { + return + } + + let guid = old.guid + + let formatted: String + if let username = new.username { + let promptStringFormat = NSLocalizedString("LoginsHelper.PromptUpdateLogin.Title", value: "Update login %@ for %@?", comment: "Prompt for updating a login. The first parameter is the username for which the password will be updated for. The second parameter is the hostname of the site.") + formatted = String(format: promptStringFormat, username, new.hostname) + } else { + let promptStringFormat = NSLocalizedString("LoginsHelper.PromptUpdatePassword.Title", value: "Update password for %@?", comment: "Prompt for updating a password with no username. The parameter is the hostname of the site.") + formatted = String(format: promptStringFormat, new.hostname) + } + let promptMessage = NSAttributedString(string: formatted) + + if snackBar != nil { + tab?.removeSnackbar(snackBar!) + } + + snackBar = TimerSnackBar(attrText: promptMessage, + img: UIImage(named: "key"), + buttons: [ + SnackButton(title: Strings.LoginsHelperDontSaveButtonTitle, accessibilityIdentifier: "UpdateLoginPrompt.dontSaveButton", callback: { (bar: SnackBar) -> Void in + self.tab?.removeSnackbar(bar) + self.snackBar = nil + return + }), + + SnackButton(title: Strings.LoginsHelperUpdateButtonTitle, accessibilityIdentifier: "UpdateLoginPrompt.updateButton", callback: { (bar: SnackBar) -> Void in + self.tab?.removeSnackbar(bar) + self.snackBar = nil + self.profile.logins.updateLoginByGUID(guid, new: new, + significant: new.isSignificantlyDifferentFrom(old)) + }) + ]) + tab?.addSnackbar(snackBar!) + } + + fileprivate func requestLogins(_ login: LoginData, requestId: String) { + profile.logins.getLoginsForProtectionSpace(login.protectionSpace).uponQueue(DispatchQueue.main) { res in + var jsonObj = [String: Any]() + if let cursor = res.successValue { + log.debug("Found \(cursor.count) logins.") + jsonObj["requestId"] = requestId + jsonObj["name"] = "RemoteLogins:loginsFound" + jsonObj["logins"] = cursor.map { $0!.toDict() } + } + + let json = JSON(jsonObj) + let src = "window.__firefox__.logins.inject(\(json.stringValue()!))" + self.tab?.webView?.evaluateJavaScript(src, completionHandler: { (obj, err) -> Void in + }) + } + } +} diff --git a/mobile/ios/Client/Frontend/Browser/MailProviders.swift b/mobile/ios/Client/Frontend/Browser/MailProviders.swift new file mode 100644 index 0000000000..c140182ab8 --- /dev/null +++ b/mobile/ios/Client/Frontend/Browser/MailProviders.swift @@ -0,0 +1,156 @@ +/* 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 + +// mailto headers: subject, body, cc, bcc + +protocol MailProvider { + var beginningScheme: String {get set} + var supportedHeaders: [String] {get set} + func newEmailURLFromMetadata(_ metadata: MailToMetadata) -> URL? +} + +private func constructEmailURLString(_ beginningURLString: String, metadata: MailToMetadata, supportedHeaders: [String], bodyHName: String = "body", toHName: String = "to") -> String { + var lowercasedHeaders = [String: String]() + metadata.headers.forEach { (hname, hvalue) in + lowercasedHeaders[hname.lowercased()] = hvalue + } + + var toParam: String + if let toHValue = lowercasedHeaders["to"] { + let value = metadata.to.isEmpty ? toHValue : [metadata.to, toHValue].joined(separator: "%2C%20") + lowercasedHeaders.removeValue(forKey: "to") + toParam = "\(toHName)=\(value)" + } else { + toParam = "\(toHName)=\(metadata.to)" + } + + var queryParams: [String] = [] + lowercasedHeaders.forEach({ (hname, hvalue) in + if supportedHeaders.contains(hname) { + queryParams.append("\(hname)=\(hvalue)") + } else if hname == "body" { + queryParams.append("\(bodyHName)=\(hvalue)") + } + }) + let stringParams = queryParams.joined(separator: "&") + let finalURLString = beginningURLString + (stringParams.isEmpty ? toParam : [toParam, stringParams].joined(separator: "&")) + return finalURLString +} + +class ReaddleSparkIntegration: MailProvider { + var beginningScheme = "readdle-spark://compose?" + var supportedHeaders = [ + "subject", + "recipient", + "textbody", + "html", + "cc", + "bcc" + ] + + func newEmailURLFromMetadata(_ metadata: MailToMetadata) -> URL? { + return constructEmailURLString(beginningScheme, metadata: metadata, supportedHeaders: supportedHeaders, bodyHName: "textbody", toHName: "recipient").asURL + } +} + +class AirmailIntegration: MailProvider { + var beginningScheme = "airmail://compose?" + var supportedHeaders = [ + "subject", + "from", + "to", + "cc", + "bcc", + "plainBody", + "htmlBody" + ] + + func newEmailURLFromMetadata(_ metadata: MailToMetadata) -> URL? { + return constructEmailURLString(beginningScheme, metadata: metadata, supportedHeaders: supportedHeaders, bodyHName: "htmlBody").asURL + } +} + +class MyMailIntegration: MailProvider { + var beginningScheme = "mymail-mailto://?" + var supportedHeaders = [ + "to", + "subject", + "body", + "cc", + "bcc" + ] + + func newEmailURLFromMetadata(_ metadata: MailToMetadata) -> URL? { + return constructEmailURLString(beginningScheme, metadata: metadata, supportedHeaders: supportedHeaders).asURL + } +} + +class MailRuIntegration: MyMailIntegration { + override init() { + super.init() + self.beginningScheme = "mailru-mailto://?" + } +} + +class MSOutlookIntegration: MailProvider { + var beginningScheme = "ms-outlook://emails/new?" + var supportedHeaders = [ + "to", + "cc", + "bcc", + "subject", + "body" + ] + + func newEmailURLFromMetadata(_ metadata: MailToMetadata) -> URL? { + return constructEmailURLString(beginningScheme, metadata: metadata, supportedHeaders: supportedHeaders).asURL + } +} + +class YMailIntegration: MailProvider { + var beginningScheme = "ymail://mail/any/compose?" + var supportedHeaders = [ + "to", + "cc", + "subject", + "body" + ] + + func newEmailURLFromMetadata(_ metadata: MailToMetadata) -> URL? { + return constructEmailURLString(beginningScheme, metadata: metadata, supportedHeaders: supportedHeaders).asURL + } +} + +class GoogleGmailIntegration: MailProvider { + var beginningScheme = "googlegmail:///co?" + var supportedHeaders = [ + "to", + "cc", + "bcc", + "subject", + "body" + ] + + func newEmailURLFromMetadata(_ metadata: MailToMetadata) -> URL? { + return constructEmailURLString(beginningScheme, metadata: metadata, supportedHeaders: supportedHeaders).asURL + } +} + +class GoogleInboxIntegration: MailProvider { + var beginningScheme = "inbox-gmail://co?" + var supportedHeaders = [ + "to", + "cc", + "bcc", + "subject", + "body" + ] + + func newEmailURLFromMetadata(_ metadata: MailToMetadata) -> URL? { + return constructEmailURLString(beginningScheme, metadata: metadata, supportedHeaders: supportedHeaders).asURL + } +} diff --git a/mobile/ios/Client/Frontend/Browser/MailtoLinkHandler.swift b/mobile/ios/Client/Frontend/Browser/MailtoLinkHandler.swift new file mode 100644 index 0000000000..cd9097b14b --- /dev/null +++ b/mobile/ios/Client/Frontend/Browser/MailtoLinkHandler.swift @@ -0,0 +1,52 @@ +/* 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 UIKit + +open class MailtoLinkHandler { + + lazy var mailSchemeProviders: [String: MailProvider] = self.fetchMailSchemeProviders() + + func launchMailClientForScheme(_ scheme: String, metadata: MailToMetadata, defaultMailtoURL: URL) { + guard let provider = mailSchemeProviders[scheme], let mailURL = provider.newEmailURLFromMetadata(metadata) else { + UIApplication.shared.openURL(defaultMailtoURL) + return + } + + if UIApplication.shared.canOpenURL(mailURL) { + UIApplication.shared.openURL(mailURL) + } else { + UIApplication.shared.openURL(defaultMailtoURL) + } + } + + func fetchMailSchemeProviders() -> [String: MailProvider] { + var providerDict = [String: MailProvider]() + if let path = Bundle.main.path(forResource: "MailSchemes", ofType: "plist"), let dictRoot = NSArray(contentsOfFile: path) { + dictRoot.forEach({ dict in + if let schemeDict = dict as? [String: Any], let scheme = schemeDict["scheme"] as? String { + if scheme == "readdle-spark://" { + providerDict[scheme] = ReaddleSparkIntegration() + } else if scheme == "mymail-mailto://" { + providerDict[scheme] = MyMailIntegration() + } else if scheme == "mailru-mailto://" { + providerDict[scheme] = MailRuIntegration() + } else if scheme == "airmail://" { + providerDict[scheme] = AirmailIntegration() + } else if scheme == "ms-outlook://" { + providerDict[scheme] = MSOutlookIntegration() + } else if scheme == "ymail://" { + providerDict[scheme] = YMailIntegration() + } else if scheme == "googlegmail://" { + providerDict[scheme] = GoogleGmailIntegration() + } else if scheme == "inbox-gmail://" { + providerDict[scheme] = GoogleInboxIntegration() + } + } + }) + } + return providerDict + } +} diff --git a/mobile/ios/Client/Frontend/Browser/NightModeHelper.swift b/mobile/ios/Client/Frontend/Browser/NightModeHelper.swift new file mode 100644 index 0000000000..4ed325561f --- /dev/null +++ b/mobile/ios/Client/Frontend/Browser/NightModeHelper.swift @@ -0,0 +1,58 @@ +/* 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 WebKit +import Shared + +struct NightModePrefsKey { + static let NightModeButtonIsInMenu = PrefsKeys.KeyNightModeButtonIsInMenu + static let NightModeStatus = PrefsKeys.KeyNightModeStatus +} + +class NightModeHelper: TabContentScript { + fileprivate weak var tab: Tab? + + required init(tab: Tab) { + self.tab = tab + if let path = Bundle.main.path(forResource: "NightModeHelper", ofType: "js"), let source = try? NSString(contentsOfFile: path, encoding: String.Encoding.utf8.rawValue) as String { + let userScript = WKUserScript(source: source, injectionTime: WKUserScriptInjectionTime.atDocumentStart, forMainFrameOnly: true) + tab.webView!.configuration.userContentController.addUserScript(userScript) + } + } + + static func name() -> String { + return "NightMode" + } + + func scriptMessageHandlerName() -> String? { + return "NightMode" + } + + func userContentController(_ userContentController: WKUserContentController, didReceiveScriptMessage message: WKScriptMessage) { + // Do nothing. + } + + static func toggle(_ prefs: Prefs, tabManager: TabManager) { + let isActive = prefs.boolForKey(NightModePrefsKey.NightModeStatus) ?? false + setNightMode(prefs, tabManager: tabManager, enabled: !isActive) + } + + static func setNightMode(_ prefs: Prefs, tabManager: TabManager, enabled: Bool) { + prefs.setBool(enabled, forKey: NightModePrefsKey.NightModeStatus) + for tab in tabManager.tabs { + tab.setNightMode(enabled) + } + } + + static func isActivated(_ prefs: Prefs) -> Bool { + return prefs.boolForKey(NightModePrefsKey.NightModeStatus) ?? false + } +} + +class NightModeAccessors { + static func isNightMode(_ prefs: Prefs) -> Bool { + return prefs.boolForKey(NightModePrefsKey.NightModeStatus) ?? false + } +} diff --git a/mobile/ios/Client/Frontend/Browser/NoImageModeHelper.swift b/mobile/ios/Client/Frontend/Browser/NoImageModeHelper.swift new file mode 100644 index 0000000000..8c20f36b78 --- /dev/null +++ b/mobile/ios/Client/Frontend/Browser/NoImageModeHelper.swift @@ -0,0 +1,47 @@ +/* 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 WebKit +import Shared + +struct NoImageModePrefsKey { + static let NoImageModeStatus = PrefsKeys.KeyNoImageModeStatus +} + +class NoImageModeHelper: TabContentScript { + fileprivate weak var tab: Tab? + + required init(tab: Tab) { + self.tab = tab + if let path = Bundle.main.path(forResource: "NoImageModeHelper", ofType: "js"), let source = try? NSString(contentsOfFile: path, encoding: String.Encoding.utf8.rawValue) as String { + let userScript = WKUserScript(source: source, injectionTime: WKUserScriptInjectionTime.atDocumentStart, forMainFrameOnly: true) + tab.webView!.configuration.userContentController.addUserScript(userScript) + } + } + + static func name() -> String { + return "NoImageMode" + } + + func scriptMessageHandlerName() -> String? { + return "NoImageMode" + } + + func userContentController(_ userContentController: WKUserContentController, didReceiveScriptMessage message: WKScriptMessage) { + // Do nothing. + } + + static func isActivated(_ prefs: Prefs) -> Bool { + return prefs.boolForKey(NoImageModePrefsKey.NoImageModeStatus) ?? false + } + + static func toggle(profile: Profile, tabManager: TabManager) { + if #available(iOS 11, *) { + let enabled = !isActivated(profile.prefs) + profile.prefs.setBool(enabled, forKey: PrefsKeys.KeyNoImageModeStatus) + tabManager.tabs.forEach { $0.noImageMode = enabled } + } + } +} diff --git a/mobile/ios/Client/Frontend/Browser/OpenInHelper.swift b/mobile/ios/Client/Frontend/Browser/OpenInHelper.swift new file mode 100644 index 0000000000..3b1fe73e83 --- /dev/null +++ b/mobile/ios/Client/Frontend/Browser/OpenInHelper.swift @@ -0,0 +1,232 @@ +/* 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 PassKit +import WebKit +import SnapKit + +import Shared + +import XCGLogger + +private let log = Logger.browserLogger + +struct OpenInViewUX { + static let ViewHeight: CGFloat = 40.0 + static let TextFont = UIFont.systemFont(ofSize: 16) + static let TextColor = UIColor(red: 74.0/255.0, green: 144.0/255.0, blue: 226.0/255.0, alpha: 1.0) + static let TextOffset = -15 + static let OpenInString = NSLocalizedString("Open in…", comment: "String indicating that the file can be opened in another application on the device") +} + +enum MimeType: String { + case PDF = "application/pdf" + case PASS = "application/vnd.apple.pkpass" +} + +protocol OpenInHelper { + init?(response: URLResponse) + var openInView: UIView? { get set } + func open() +} + +struct OpenIn { + static let helpers: [OpenInHelper.Type] = [OpenPdfInHelper.self, OpenPassBookHelper.self, ShareFileHelper.self] + + static func helperForResponse(_ response: URLResponse) -> OpenInHelper? { + return helpers.flatMap { $0.init(response: response) }.first + } +} + +class ShareFileHelper: NSObject, OpenInHelper { + var openInView: UIView? + + fileprivate var url: URL + var pathExtension: String? + + required init?(response: URLResponse) { + guard let MIMEType = response.mimeType, !(MIMEType == MimeType.PASS.rawValue || MIMEType == MimeType.PDF.rawValue), + let responseURL = response.url else { return nil } + url = (responseURL as NSURL) as URL + super.init() + } + + func open() { + let alertController = UIAlertController( + title: Strings.OpenInDownloadHelperAlertTitle, + message: Strings.OpenInDownloadHelperAlertMessage, + preferredStyle: UIAlertControllerStyle.alert) + alertController.addAction( UIAlertAction(title: Strings.OpenInDownloadHelperAlertCancel, style: .cancel, handler: nil)) + alertController.addAction(UIAlertAction(title: Strings.OpenInDownloadHelperAlertConfirm, style: .default) { (action) in + let objectsToShare = [self.url] + let activityVC = UIActivityViewController(activityItems: objectsToShare, applicationActivities: nil) + if let sourceView = self.openInView, let popoverController = activityVC.popoverPresentationController { + popoverController.sourceView = sourceView + popoverController.sourceRect = CGRect(origin: CGPoint(x: sourceView.bounds.midX, y: sourceView.bounds.maxY), size: .zero) + popoverController.permittedArrowDirections = .up + } + UIApplication.shared.keyWindow?.rootViewController?.present(activityVC, animated: true, completion: nil) + }) + UIApplication.shared.keyWindow?.rootViewController?.present(alertController, animated: true, completion: nil) + } +} + +class OpenPassBookHelper: NSObject, OpenInHelper { + var openInView: UIView? + + fileprivate var url: URL + + required init?(response: URLResponse) { + guard let MIMEType = response.mimeType, MIMEType == MimeType.PASS.rawValue && PKAddPassesViewController.canAddPasses(), + let responseURL = response.url else { return nil } + url = responseURL + super.init() + } + + func open() { + guard let passData = try? Data(contentsOf: url) else { return } + var error: NSError? = nil + let pass = PKPass(data: passData, error: &error) + if let _ = error { + // display an error + let alertController = UIAlertController( + title: Strings.UnableToAddPassErrorTitle, + message: Strings.UnableToAddPassErrorMessage, + preferredStyle: UIAlertControllerStyle.alert) + alertController.addAction( + UIAlertAction(title: Strings.UnableToAddPassErrorDismiss, style: .cancel) { (action) in + // Do nothing. + }) + UIApplication.shared.keyWindow?.rootViewController?.present(alertController, animated: true, completion: nil) + return + } + let passLibrary = PKPassLibrary() + if passLibrary.containsPass(pass) { + UIApplication.shared.openURL(pass.passURL!) + } else { + let addController = PKAddPassesViewController(pass: pass) + UIApplication.shared.keyWindow?.rootViewController?.present(addController, animated: true, completion: nil) + } + + } +} + +class OpenPdfInHelper: NSObject, OpenInHelper, UIDocumentInteractionControllerDelegate { + fileprivate var url: URL + fileprivate var docController: UIDocumentInteractionController? + fileprivate var openInURL: URL? + + lazy var openInView: UIView? = getOpenInView(self)() + + lazy var documentDirectory: URL = { + return URL(string: NSTemporaryDirectory())!.appendingPathComponent("pdfs") + }() + + fileprivate var filepath: URL? + + required init?(response: URLResponse) { + guard let MIMEType = response.mimeType, MIMEType == MimeType.PDF.rawValue && UIApplication.shared.canOpenURL(URL(string: "itms-books:")!), + let responseURL = response.url else { return nil } + url = responseURL + super.init() + setFilePath(response.suggestedFilename ?? url.lastPathComponent ) + } + + fileprivate func setFilePath(_ suggestedFilename: String) { + var filename = suggestedFilename + let pathExtension = filename.asURL?.pathExtension + if pathExtension == nil { + filename.append(".pdf") + } + filepath = documentDirectory.appendingPathComponent(filename) + } + + deinit { + guard let url = openInURL else { return } + let fileManager = FileManager.default + do { + try fileManager.removeItem(at: url) + } catch { + log.error("failed to delete file at \(url): \(error)") + } + } + + func getOpenInView() -> OpenInView { + let overlayView = OpenInView() + + overlayView.openInButton.addTarget(self, action: #selector(OpenPdfInHelper.open), for: .touchUpInside) + return overlayView + } + + func createDocumentControllerForURL(url: URL) { + docController = UIDocumentInteractionController(url: url) + docController?.delegate = self + self.openInURL = url + } + + func createLocalCopyOfPDF() { + guard let filePath = filepath else { + log.error("failed to create proper URL") + return + } + if docController == nil { + // if we already have a URL but no document controller, just create the document controller + if let url = openInURL { + createDocumentControllerForURL(url: url) + return + } + let contentsOfFile = try? Data(contentsOf: url) + let fileManager = FileManager.default + do { + try fileManager.createDirectory(atPath: documentDirectory.absoluteString, withIntermediateDirectories: true, attributes: nil) + if fileManager.createFile(atPath: filePath.absoluteString, contents: contentsOfFile, attributes: nil) { + let openInURL = URL(fileURLWithPath: filePath.absoluteString) + createDocumentControllerForURL(url: openInURL) + } else { + log.error("Unable to create local version of PDF file at \(filePath)") + } + } catch { + log.error("Error on creating directory at \(self.documentDirectory)") + } + } + } + + func open() { + createLocalCopyOfPDF() + guard let _parentView = self.openInView!.superview, let docController = self.docController else { log.error("view doesn't have a superview so can't open anything"); return } + // iBooks should be installed by default on all devices we care about, so regardless of whether or not there are other pdf-capable + // apps on this device, if we can open in iBooks we can open this PDF + // simulators do not have iBooks so the open in view will not work on the simulator + if UIApplication.shared.canOpenURL(URL(string: "itms-books:")!) { + log.info("iBooks installed: attempting to open pdf") + docController.presentOpenInMenu(from: .zero, in: _parentView, animated: true) + } else { + log.info("iBooks is not installed") + } + } +} + +class OpenInView: UIView { + let openInButton = UIButton() + + init() { + super.init(frame: .zero) + openInButton.setTitleColor(OpenInViewUX.TextColor, for: UIControlState.normal) + openInButton.setTitle(OpenInViewUX.OpenInString, for: UIControlState.normal) + openInButton.titleLabel?.font = OpenInViewUX.TextFont + openInButton.sizeToFit() + self.addSubview(openInButton) + openInButton.snp.makeConstraints { make in + make.centerY.equalTo(self) + make.height.equalTo(self) + make.trailing.equalTo(self).offset(OpenInViewUX.TextOffset) + } + self.backgroundColor = UIColor.white + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} diff --git a/mobile/ios/Client/Frontend/Browser/OpenSearch.swift b/mobile/ios/Client/Frontend/Browser/OpenSearch.swift new file mode 100644 index 0000000000..1af3593a83 --- /dev/null +++ b/mobile/ios/Client/Frontend/Browser/OpenSearch.swift @@ -0,0 +1,281 @@ +/* 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 UIKit +import Shared +import Fuzi + +private let TypeSearch = "text/html" +private let TypeSuggest = "application/x-suggestions+json" + +class OpenSearchEngine: NSObject, NSCoding { + static let PreferredIconSize = 30 + + let shortName: String + let engineID: String? + let image: UIImage + let isCustomEngine: Bool + let searchTemplate: String + fileprivate let suggestTemplate: String? + + fileprivate let SearchTermComponent = "{searchTerms}" + fileprivate let LocaleTermComponent = "{moz:locale}" + + fileprivate lazy var searchQueryComponentKey: String? = self.getQueryArgFromTemplate() + + init(engineID: String?, shortName: String, image: UIImage, searchTemplate: String, suggestTemplate: String?, isCustomEngine: Bool) { + self.shortName = shortName + self.image = image + self.searchTemplate = searchTemplate + self.suggestTemplate = suggestTemplate + self.isCustomEngine = isCustomEngine + self.engineID = engineID + } + + required init?(coder aDecoder: NSCoder) { + // this catches the cases where bool encoded in Swift 2 needs to be decoded with decodeObject, but a Bool encoded in swift 3 needs + // to be decoded using decodeBool. This catches the upgrade case to ensure that we are always able to fetch a keyed valye for isCustomEngine + // http://stackoverflow.com/a/40034694 + let isCustomEngine = aDecoder.decodeAsBool(forKey: "isCustomEngine") + guard let searchTemplate = aDecoder.decodeObject(forKey: "searchTemplate") as? String, + let shortName = aDecoder.decodeObject(forKey: "shortName") as? String, + let image = aDecoder.decodeObject(forKey: "image") as? UIImage else { + assertionFailure() + return nil + } + + self.searchTemplate = searchTemplate + self.shortName = shortName + self.isCustomEngine = isCustomEngine + self.image = image + self.engineID = aDecoder.decodeObject(forKey: "engineID") as? String + self.suggestTemplate = nil + } + + func encode(with aCoder: NSCoder) { + aCoder.encode(searchTemplate, forKey: "searchTemplate") + aCoder.encode(shortName, forKey: "shortName") + aCoder.encode(isCustomEngine, forKey: "isCustomEngine") + aCoder.encode(image, forKey: "image") + aCoder.encode(engineID, forKey: "engineID") + } + + /** + * Returns the search URL for the given query. + */ + func searchURLForQuery(_ query: String) -> URL? { + return getURLFromTemplate(searchTemplate, query: query) + } + + /** + * Return the arg that we use for searching for this engine + * Problem: the search terms may not be a query arg, they may be part of the URL - how to deal with this? + **/ + fileprivate func getQueryArgFromTemplate() -> String? { + // we have the replace the templates SearchTermComponent in order to make the template + // 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 components = URLComponents(string: template) + let searchTerm = components?.queryItems?.filter { item in + return item.value == placeholder + } + guard let term = searchTerm, !term.isEmpty else { return nil } + return term[0].name + } + + /** + * check that the URL host contains the name of the search engine somewhere inside it + **/ + fileprivate func isSearchURLForEngine(_ url: URL?) -> Bool { + guard let urlHost = url?.hostSLD, + let queryEndIndex = searchTemplate.range(of: "?")?.lowerBound, + let templateURL = URL(string: searchTemplate.substring(to: queryEndIndex)) else { return false } + return urlHost == templateURL.hostSLD + } + + /** + * Returns the query that was used to construct a given search URL + **/ + func queryForSearchURL(_ url: URL?) -> String? { + if isSearchURLForEngine(url) { + if let key = searchQueryComponentKey, + let value = url?.getQuery()[key] { + return value.replacingOccurrences(of: "+", with: " ").removingPercentEncoding + } + } + return nil + } + + /** + * Returns the search suggestion URL for the given query. + */ + func suggestURLForQuery(_ query: String) -> URL? { + if let suggestTemplate = suggestTemplate { + return getURLFromTemplate(suggestTemplate, query: query) + } + return nil + } + + fileprivate func getURLFromTemplate(_ searchTemplate: String, query: String) -> URL? { + if let escapedQuery = query.addingPercentEncoding(withAllowedCharacters: CharacterSet.SearchTermsAllowedCharacterSet()) { + // Escape the search template as well in case it contains not-safe characters like symbols + let templateAllowedSet = NSMutableCharacterSet() + templateAllowedSet.formUnion(with: CharacterSet.URLAllowedCharacterSet()) + + // 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: String.CompareOptions.literal, range: nil) + .replacingOccurrences(of: LocaleTermComponent, with: localeString, options: String.CompareOptions.literal, range: nil) + return URL(string: urlString) + } + } + + return nil + } +} + +/** + * OpenSearch XML parser. + * + * This parser accepts standards-compliant OpenSearch 1.1 XML documents in addition to + * the Firefox-specific search plugin format. + * + * OpenSearch spec: http://www.opensearch.org/Specifications/OpenSearch/1.1 + */ +class OpenSearchParser { + fileprivate let pluginMode: Bool + + init(pluginMode: Bool) { + self.pluginMode = pluginMode + } + + func parse(_ file: String, engineID: String) -> OpenSearchEngine? { + guard let data = try? Data(contentsOf: URL(fileURLWithPath: file)) else { + print("Invalid search file") + return nil + } + + guard let indexer = try? XMLDocument(data: data), + let docIndexer = indexer.root else { + print("Invalid XML document") + return nil + } + + let shortNameIndexer = docIndexer.children(tag: "ShortName") + if shortNameIndexer.count != 1 { + print("ShortName must appear exactly once") + return nil + } + + let shortName = shortNameIndexer[0].stringValue + if shortName == "" { + print("ShortName must contain text") + return nil + } + + let urlIndexers = docIndexer.children(tag: "Url") + if urlIndexers.isEmpty { + print("Url must appear at least once") + return nil + } + + var searchTemplate: String! + var suggestTemplate: String? + for urlIndexer in urlIndexers { + let type = urlIndexer.attributes["type"] + if type == nil { + print("Url element requires a type attribute", terminator: "\n") + return nil + } + + if type != TypeSearch && type != TypeSuggest { + // Not a supported search type. + continue + } + + var template = urlIndexer.attributes["template"] + if template == nil { + print("Url element requires a template attribute", terminator: "\n") + return nil + } + + if pluginMode { + let paramIndexers = urlIndexer.children(tag: "Param") + + if !paramIndexers.isEmpty { + template! += "?" + var firstAdded = false + for paramIndexer in paramIndexers { + if firstAdded { + template! += "&" + } else { + firstAdded = true + } + + let name = paramIndexer.attributes["name"] + let value = paramIndexer.attributes["value"] + if name == nil || value == nil { + print("Param element must have name and value attributes", terminator: "\n") + return nil + } + template! += name! + "=" + value! + } + } + } + + if type == TypeSearch { + searchTemplate = template + } else { + suggestTemplate = template + } + } + + if searchTemplate == nil { + print("Search engine must have a text/html type") + return nil + } + + let imageIndexers = docIndexer.children(tag: "Image") + var largestImage = 0 + var largestImageElement: XMLElement? + + // TODO: For now, just use the largest icon. + for imageIndexer in imageIndexers { + let imageWidth = Int(imageIndexer.attributes["width"] ?? "") + let imageHeight = Int(imageIndexer.attributes["height"] ?? "") + + // Only accept square images. + if imageWidth != imageHeight { + continue + } + + if let imageWidth = imageWidth { + if imageWidth > largestImage { + largestImage = imageWidth + largestImageElement = imageIndexer + } + } + } + + let uiImage: UIImage + if let imageElement = largestImageElement, + let imageURL = URL(string: imageElement.stringValue), + let imageData = try? Data(contentsOf: imageURL), + let image = UIImage.imageFromDataThreadSafe(imageData) { + uiImage = image + } else { + print("Error: Invalid search image data") + return nil + } + + return OpenSearchEngine(engineID: engineID, shortName: shortName, image: uiImage, searchTemplate: searchTemplate, suggestTemplate: suggestTemplate, isCustomEngine: false) + } +} diff --git a/mobile/ios/Client/Frontend/Browser/OpenWithSettingsViewController.swift b/mobile/ios/Client/Frontend/Browser/OpenWithSettingsViewController.swift new file mode 100644 index 0000000000..e9de532ccd --- /dev/null +++ b/mobile/ios/Client/Frontend/Browser/OpenWithSettingsViewController.swift @@ -0,0 +1,123 @@ +/* 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 + +class OpenWithSettingsViewController: UITableViewController { + typealias MailtoProviderEntry = (name: String, scheme: String, enabled: Bool) + var mailProviderSource = [MailtoProviderEntry]() + + fileprivate let prefs: Prefs + fileprivate var currentChoice: String = "mailto" + + fileprivate let BasicCheckmarkCell = "BasicCheckmarkCell" + + init(prefs: Prefs) { + self.prefs = prefs + super.init(nibName: nil, bundle: nil) + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + title = Strings.SettingsOpenWithSectionName + + tableView.accessibilityIdentifier = "OpenWithPage.Setting.Options" + + tableView.register(UITableViewCell.self, forCellReuseIdentifier: BasicCheckmarkCell) + tableView.backgroundColor = UIConstants.TableViewHeaderBackgroundColor + + let headerFooterFrame = CGRect(origin: CGPoint.zero, size: CGSize(width: self.view.frame.width, height: UIConstants.TableViewHeaderFooterHeight)) + let headerView = SettingsTableSectionHeaderFooterView(frame: headerFooterFrame) + headerView.titleLabel.text = Strings.SettingsOpenWithPageTitle.uppercased() + headerView.showTopBorder = false + headerView.showBottomBorder = true + + let footerView = SettingsTableSectionHeaderFooterView(frame: headerFooterFrame) + footerView.showTopBorder = true + footerView.showBottomBorder = false + + tableView.tableHeaderView = headerView + tableView.tableFooterView = footerView + + NotificationCenter.default.addObserver(self, selector: #selector(OpenWithSettingsViewController.appDidBecomeActive), name: NSNotification.Name.UIApplicationDidBecomeActive, object: nil) + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + appDidBecomeActive() + } + + override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + self.prefs.setString(currentChoice, forKey: PrefsKeys.KeyMailToOption) + } + + func appDidBecomeActive() { + reloadMailProviderSource() + updateCurrentChoice() + tableView.reloadData() + } + + func updateCurrentChoice() { + var previousChoiceAvailable: Bool = false + if let prefMailtoScheme = self.prefs.stringForKey(PrefsKeys.KeyMailToOption) { + mailProviderSource.forEach({ (name, scheme, enabled) in + if scheme == prefMailtoScheme { + previousChoiceAvailable = enabled + } + }) + } + + if !previousChoiceAvailable { + self.prefs.setString(mailProviderSource[0].scheme, forKey: PrefsKeys.KeyMailToOption) + } + + if let updatedMailToClient = self.prefs.stringForKey(PrefsKeys.KeyMailToOption) { + self.currentChoice = updatedMailToClient + } + } + + func reloadMailProviderSource() { + if let path = Bundle.main.path(forResource: "MailSchemes", ofType: "plist"), let dictRoot = NSArray(contentsOfFile: path) { + mailProviderSource = dictRoot.map { dict in + let nsDict = dict as! NSDictionary + return (name: nsDict["name"] as! String, scheme: nsDict["scheme"] as! String, + enabled: canOpenMailScheme(nsDict["scheme"] as! String)) + } + } + } + + func canOpenMailScheme(_ scheme: String) -> Bool { + if let url = URL(string: scheme) { + return UIApplication.shared.canOpenURL(url) + } + return false + } + + override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let cell = tableView.dequeueReusableCell(withIdentifier: BasicCheckmarkCell, for: indexPath) + + let option = mailProviderSource[indexPath.row] + + cell.textLabel?.attributedText = NSAttributedString.tableRowTitle(option.name, enabled: option.enabled) + cell.accessoryType = (currentChoice == option.scheme && option.enabled) ? .checkmark : .none + cell.isUserInteractionEnabled = option.enabled + + return cell + } + + override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return mailProviderSource.count + } + + override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + self.currentChoice = mailProviderSource[indexPath.row].scheme + tableView.reloadData() + } +} diff --git a/mobile/ios/Client/Frontend/Browser/PrintHelper.swift b/mobile/ios/Client/Frontend/Browser/PrintHelper.swift new file mode 100644 index 0000000000..d56f0a377e --- /dev/null +++ b/mobile/ios/Client/Frontend/Browser/PrintHelper.swift @@ -0,0 +1,35 @@ +/* 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 WebKit + +class PrintHelper: TabContentScript { + fileprivate weak var tab: Tab? + + class func name() -> String { + return "PrintHelper" + } + + required init(tab: Tab) { + self.tab = tab + if let path = Bundle.main.path(forResource: "PrintHelper", ofType: "js"), let source = try? NSString(contentsOfFile: path, encoding: String.Encoding.utf8.rawValue) as String { + let userScript = WKUserScript(source: source, injectionTime: WKUserScriptInjectionTime.atDocumentEnd, forMainFrameOnly: false) + tab.webView!.configuration.userContentController.addUserScript(userScript) + } + } + + func scriptMessageHandlerName() -> String? { + return "printHandler" + } + + func userContentController(_ userContentController: WKUserContentController, didReceiveScriptMessage message: WKScriptMessage) { + if let tab = tab, let webView = tab.webView { + let printController = UIPrintInteractionController.shared + printController.printFormatter = webView.viewPrintFormatter() + printController.present(animated: true, completionHandler: nil) + } + } +} diff --git a/mobile/ios/Client/Frontend/Browser/PrivilegedRequest.swift b/mobile/ios/Client/Frontend/Browser/PrivilegedRequest.swift new file mode 100644 index 0000000000..128b4ea8e4 --- /dev/null +++ b/mobile/ios/Client/Frontend/Browser/PrivilegedRequest.swift @@ -0,0 +1,41 @@ +/* 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 + +private let REQUEST_KEY_PRIVILEGED = "privileged" + +/** + Request that is allowed to load local resources. + + Pages running on the local server have same origin access all resources + on the server, so we need to prevent arbitrary web pages from accessing + these resources. We do so by explicitly requiring "privileged" requests + in our navigation policy when loading local resources. + + Be careful: creating a privileged request for an arbitrary URL provided + by the page will break this model. Only use a privileged request when + needed, and when you are sure the URL is from a trustworthy source! + **/ +class PrivilegedRequest: NSMutableURLRequest { + override init(url URL: URL, cachePolicy: NSURLRequest.CachePolicy, timeoutInterval: TimeInterval) { + super.init(url: URL, cachePolicy: cachePolicy, timeoutInterval: timeoutInterval) + setPrivileged() + } + + required init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + setPrivileged() + } + + fileprivate func setPrivileged() { + URLProtocol.setProperty(true, forKey: REQUEST_KEY_PRIVILEGED, in: self) + } +} + +extension URLRequest { + var isPrivileged: Bool { + return URLProtocol.property(forKey: REQUEST_KEY_PRIVILEGED, in: self) != nil + } +} diff --git a/mobile/ios/Client/Frontend/Browser/Punycode.swift b/mobile/ios/Client/Frontend/Browser/Punycode.swift new file mode 100644 index 0000000000..517019c198 --- /dev/null +++ b/mobile/ios/Client/Frontend/Browser/Punycode.swift @@ -0,0 +1,198 @@ +/* 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 + +private let base = 36 +private let tMin = 1 +private let tMax = 26 +private let initialBias = 72 +private let initialN: Int = 128 // 0x80 +private let delimiter: Character = "-"; // '\x2D' +private let prefixPunycode = "xn--" +private let asciiPunycode = [Character]("abcdefghijklmnopqrstuvwxyz0123456789".characters) + +extension String { + fileprivate func toValue(_ index: Int) -> Character { + return asciiPunycode[index] + } + + fileprivate func toIndex(_ value: Character) -> Int { + return asciiPunycode.index(of: value)! + } + + fileprivate func adapt(_ delta: Int, numPoints: Int, firstTime: Bool) -> Int { + let skew = 38 + let damp = firstTime ? 700 : 2 + var delta = delta + delta = delta / damp + delta += delta / numPoints + var k = 0 + while delta > ((base - tMin) * tMax) / 2 { + delta /= (base - tMin) + k += base + } + return k + ((base - tMin + 1) * delta) / (delta + skew) + } + + fileprivate func encode(_ input: String) -> String { + var output = "" + var d: Int = 0 + var extendedChars = [Int]() + for c in input.unicodeScalars { + if Int(c.value) < initialN { + d += 1 + output.append(String(c)) + } else { + extendedChars.append(Int(c.value)) + } + } + if extendedChars.count == 0 { + return output + } + if d > 0 { + output.append(delimiter) + } + + var n = initialN + var delta = 0 + var bias = initialBias + var h: Int = 0 + var b: Int = 0 + + if d > 0 { + h = output.unicodeScalars.count - 1 + b = output.unicodeScalars.count - 1 + } else { + h = output.unicodeScalars.count + b = output.unicodeScalars.count + } + + while h < input.unicodeScalars.count { + var char = Int(0x7fffffff) + for c in input.unicodeScalars { + let ci = Int(c.value) + if char > ci && ci >= n { + char = ci + } + } + delta = delta + (char - n) * (h + 1) + if delta < 0 { + print("error: invalid char:") + output = "" + return output + } + n = char + for c in input.unicodeScalars { + let ci = Int(c.value) + if ci < n || ci < initialN { + delta += 1 + continue + } + if ci > n { + continue + } + var q = delta + var k = base + while true { + let t = max(min(k - bias, tMax), tMin) + if q < t { + break + } + let code = t + ((q - t) % (base - t)) + output.append(toValue(code)) + q = (q - t) / (base - t) + k += base + } + output.append(toValue(q)) + bias = self.adapt(delta, numPoints: h + 1, firstTime: h == b) + delta = 0 + h += 1 + } + delta += 1 + n += 1 + } + return output + } + + fileprivate func decode(_ punycode: String) -> String { + var input = [Character](punycode.characters) + var output = [Character]() + var i = 0 + var n = initialN + var bias = initialBias + var pos = 0 + if let ipos = input.index(of: delimiter) { + pos = ipos + output.append(contentsOf: input[0 ..< pos]) + pos += 1 + } + var outputLength = output.count + let inputLength = input.count + while pos < inputLength { + let oldi = i + var w = 1 + var k = base + while true { + let digit = toIndex(input[pos]) + pos += 1 + i += digit * w + let t = max(min(k - bias, tMax), tMin) + if digit < t { + break + } + w = w * (base - t) + k += base + } + outputLength += 1 + bias = adapt(i - oldi, numPoints: outputLength, firstTime: (oldi == 0)) + n = n + i / outputLength + i = i % outputLength + output.insert(Character(UnicodeScalar(n)!), at: i) + i += 1 + } + return String(output) + } + + fileprivate func isValidUnicodeScala(_ s: String) -> Bool { + for c in s.unicodeScalars { + let ci = Int(c.value) + if ci >= initialN { + return false + } + } + return true + } + + fileprivate func isValidPunycodeScala(_ s: String) -> Bool { + return s.hasPrefix(prefixPunycode) + } + + public func utf8HostToAscii() -> String { + if isValidUnicodeScala(self) { + return self + } + var labels = self.components(separatedBy: ".") + for (i, part) in labels.enumerated() { + if !isValidUnicodeScala(part) { + let a = encode(part) + labels[i] = prefixPunycode + a + } + } + let resultString = labels.joined(separator: ".") + return resultString + } + + public func asciiHostToUTF8() -> String { + var labels = self.components(separatedBy: ".") + for (index, part) in labels.enumerated() { + if isValidPunycodeScala(part) { + let changeStr = part.substring(from: part.characters.index(part.startIndex, offsetBy: 4)) + labels[index] = decode(changeStr) + } + } + let resultString = labels.joined(separator: ".") + return resultString + } +} diff --git a/mobile/ios/Client/Frontend/Browser/QRCode.xcassets/Contents.json b/mobile/ios/Client/Frontend/Browser/QRCode.xcassets/Contents.json new file mode 100644 index 0000000000..da4a164c91 --- /dev/null +++ b/mobile/ios/Client/Frontend/Browser/QRCode.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/mobile/ios/Client/Frontend/Browser/QRCode.xcassets/qrcode-goBack.imageset/Contents.json b/mobile/ios/Client/Frontend/Browser/QRCode.xcassets/qrcode-goBack.imageset/Contents.json new file mode 100644 index 0000000000..acc9878d2e --- /dev/null +++ b/mobile/ios/Client/Frontend/Browser/QRCode.xcassets/qrcode-goBack.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "qrcode-goBack.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "qrcode-goBack@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "qrcode-goBack@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/mobile/ios/Client/Frontend/Browser/QRCode.xcassets/qrcode-goBack.imageset/qrcode-goBack.png b/mobile/ios/Client/Frontend/Browser/QRCode.xcassets/qrcode-goBack.imageset/qrcode-goBack.png new file mode 100644 index 0000000000..8ee322cf77 Binary files /dev/null and b/mobile/ios/Client/Frontend/Browser/QRCode.xcassets/qrcode-goBack.imageset/qrcode-goBack.png differ diff --git a/mobile/ios/Client/Frontend/Browser/QRCode.xcassets/qrcode-goBack.imageset/qrcode-goBack@2x.png b/mobile/ios/Client/Frontend/Browser/QRCode.xcassets/qrcode-goBack.imageset/qrcode-goBack@2x.png new file mode 100644 index 0000000000..36b8c21eca Binary files /dev/null and b/mobile/ios/Client/Frontend/Browser/QRCode.xcassets/qrcode-goBack.imageset/qrcode-goBack@2x.png differ diff --git a/mobile/ios/Client/Frontend/Browser/QRCode.xcassets/qrcode-goBack.imageset/qrcode-goBack@3x.png b/mobile/ios/Client/Frontend/Browser/QRCode.xcassets/qrcode-goBack.imageset/qrcode-goBack@3x.png new file mode 100644 index 0000000000..88ec112fd5 Binary files /dev/null and b/mobile/ios/Client/Frontend/Browser/QRCode.xcassets/qrcode-goBack.imageset/qrcode-goBack@3x.png differ diff --git a/mobile/ios/Client/Frontend/Browser/QRCode.xcassets/qrcode-isLighting.imageset/Contents.json b/mobile/ios/Client/Frontend/Browser/QRCode.xcassets/qrcode-isLighting.imageset/Contents.json new file mode 100644 index 0000000000..77e2fb8723 --- /dev/null +++ b/mobile/ios/Client/Frontend/Browser/QRCode.xcassets/qrcode-isLighting.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "qrcode-isLighting.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "qrcode-isLighting@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "qrcode-isLighting@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/mobile/ios/Client/Frontend/Browser/QRCode.xcassets/qrcode-isLighting.imageset/qrcode-isLighting.png b/mobile/ios/Client/Frontend/Browser/QRCode.xcassets/qrcode-isLighting.imageset/qrcode-isLighting.png new file mode 100644 index 0000000000..a3ea881dc3 Binary files /dev/null and b/mobile/ios/Client/Frontend/Browser/QRCode.xcassets/qrcode-isLighting.imageset/qrcode-isLighting.png differ diff --git a/mobile/ios/Client/Frontend/Browser/QRCode.xcassets/qrcode-isLighting.imageset/qrcode-isLighting@2x.png b/mobile/ios/Client/Frontend/Browser/QRCode.xcassets/qrcode-isLighting.imageset/qrcode-isLighting@2x.png new file mode 100644 index 0000000000..0c49495b64 Binary files /dev/null and b/mobile/ios/Client/Frontend/Browser/QRCode.xcassets/qrcode-isLighting.imageset/qrcode-isLighting@2x.png differ diff --git a/mobile/ios/Client/Frontend/Browser/QRCode.xcassets/qrcode-isLighting.imageset/qrcode-isLighting@3x.png b/mobile/ios/Client/Frontend/Browser/QRCode.xcassets/qrcode-isLighting.imageset/qrcode-isLighting@3x.png new file mode 100644 index 0000000000..2ca1265d6f Binary files /dev/null and b/mobile/ios/Client/Frontend/Browser/QRCode.xcassets/qrcode-isLighting.imageset/qrcode-isLighting@3x.png differ diff --git a/mobile/ios/Client/Frontend/Browser/QRCode.xcassets/qrcode-light.imageset/Contents.json b/mobile/ios/Client/Frontend/Browser/QRCode.xcassets/qrcode-light.imageset/Contents.json new file mode 100644 index 0000000000..923eee6e64 --- /dev/null +++ b/mobile/ios/Client/Frontend/Browser/QRCode.xcassets/qrcode-light.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "qrcode-light.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "qrcode-light@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "qrcode-light@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/mobile/ios/Client/Frontend/Browser/QRCode.xcassets/qrcode-light.imageset/qrcode-light.png b/mobile/ios/Client/Frontend/Browser/QRCode.xcassets/qrcode-light.imageset/qrcode-light.png new file mode 100644 index 0000000000..7da9401738 Binary files /dev/null and b/mobile/ios/Client/Frontend/Browser/QRCode.xcassets/qrcode-light.imageset/qrcode-light.png differ diff --git a/mobile/ios/Client/Frontend/Browser/QRCode.xcassets/qrcode-light.imageset/qrcode-light@2x.png b/mobile/ios/Client/Frontend/Browser/QRCode.xcassets/qrcode-light.imageset/qrcode-light@2x.png new file mode 100644 index 0000000000..9da67c1888 Binary files /dev/null and b/mobile/ios/Client/Frontend/Browser/QRCode.xcassets/qrcode-light.imageset/qrcode-light@2x.png differ diff --git a/mobile/ios/Client/Frontend/Browser/QRCode.xcassets/qrcode-light.imageset/qrcode-light@3x.png b/mobile/ios/Client/Frontend/Browser/QRCode.xcassets/qrcode-light.imageset/qrcode-light@3x.png new file mode 100644 index 0000000000..094ba8f7be Binary files /dev/null and b/mobile/ios/Client/Frontend/Browser/QRCode.xcassets/qrcode-light.imageset/qrcode-light@3x.png differ diff --git a/mobile/ios/Client/Frontend/Browser/QRCode.xcassets/qrcode-scanBorder.imageset/Contents.json b/mobile/ios/Client/Frontend/Browser/QRCode.xcassets/qrcode-scanBorder.imageset/Contents.json new file mode 100644 index 0000000000..31465003e5 --- /dev/null +++ b/mobile/ios/Client/Frontend/Browser/QRCode.xcassets/qrcode-scanBorder.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "qrcode-scanBorder.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/mobile/ios/Client/Frontend/Browser/QRCode.xcassets/qrcode-scanBorder.imageset/qrcode-scanBorder.png b/mobile/ios/Client/Frontend/Browser/QRCode.xcassets/qrcode-scanBorder.imageset/qrcode-scanBorder.png new file mode 100644 index 0000000000..2a61593d44 Binary files /dev/null and b/mobile/ios/Client/Frontend/Browser/QRCode.xcassets/qrcode-scanBorder.imageset/qrcode-scanBorder.png differ diff --git a/mobile/ios/Client/Frontend/Browser/QRCode.xcassets/qrcode-scanLine.imageset/Contents.json b/mobile/ios/Client/Frontend/Browser/QRCode.xcassets/qrcode-scanLine.imageset/Contents.json new file mode 100644 index 0000000000..7ede851e8a --- /dev/null +++ b/mobile/ios/Client/Frontend/Browser/QRCode.xcassets/qrcode-scanLine.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "qrcode-scanLine.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/mobile/ios/Client/Frontend/Browser/QRCode.xcassets/qrcode-scanLine.imageset/qrcode-scanLine.png b/mobile/ios/Client/Frontend/Browser/QRCode.xcassets/qrcode-scanLine.imageset/qrcode-scanLine.png new file mode 100644 index 0000000000..8e990f29a1 Binary files /dev/null and b/mobile/ios/Client/Frontend/Browser/QRCode.xcassets/qrcode-scanLine.imageset/qrcode-scanLine.png differ diff --git a/mobile/ios/Client/Frontend/Browser/QRCodeViewController.swift b/mobile/ios/Client/Frontend/Browser/QRCodeViewController.swift new file mode 100644 index 0000000000..bf0a2c7eab --- /dev/null +++ b/mobile/ios/Client/Frontend/Browser/QRCodeViewController.swift @@ -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 AVFoundation +import SnapKit +import Shared + +private struct QRCodeViewControllerUX { + static let navigationBarBackgroundColor = UIColor.black + static let navigationBarTitleColor = UIColor.white + static let maskViewBackgroungColor = UIColor(colorLiteralRed: 0, green: 0, blue: 0, alpha: 0.5) + static let isLightingNavigationItemColor = UIColor(colorLiteralRed: 0.45, green: 0.67, blue: 0.84, alpha: 1) +} + +protocol QRCodeViewControllerDelegate { + func didScanQRCodeWithURL(_ url: URL) + func didScanQRCodeWithText(_ text: String) +} + +class QRCodeViewController: UIViewController { + var qrCodeDelegate: QRCodeViewControllerDelegate? + + fileprivate lazy var captureSession: AVCaptureSession = { + let session = AVCaptureSession() + session.sessionPreset = AVCaptureSessionPresetHigh + return session + }() + + private lazy var captureDevice: AVCaptureDevice? = { + return AVCaptureDevice.defaultDevice(withMediaType: AVMediaTypeVideo) + }() + + private var videoPreviewLayer: AVCaptureVideoPreviewLayer? + private let scanLine: UIImageView = UIImageView(image: UIImage(named: "qrcode-scanLine")) + private let scanBorder: UIImageView = UIImageView(image: UIImage(named: "qrcode-scanBorder")) + private lazy var instructionsLabel: UILabel = { + let label = UILabel() + label.text = Strings.ScanQRCodeInstructionsLabel + label.textColor = UIColor.white + label.textAlignment = NSTextAlignment.center + label.numberOfLines = 0 + return label + }() + private var maskView: UIView = UIView() + private var isAnimationing: Bool = false + private var isLightOn: Bool = false + private var shapeLayer: CAShapeLayer = CAShapeLayer() + + private var scanRange: CGRect { + let size = UIDevice.current.userInterfaceIdiom == .pad ? + CGSize(width: view.frame.width / 2, height: view.frame.width / 2) : + CGSize(width: view.frame.width / 3 * 2, height: view.frame.width / 3 * 2) + var rect = CGRect(x: 0, y: 0, width: size.width, height: size.height) + rect.center = UIScreen.main.bounds.center + return rect + } + + private var scanBorderHeight: CGFloat { + return UIDevice.current.userInterfaceIdiom == .pad ? + view.frame.width / 2 : view.frame.width / 3 * 2 + } + + override func viewDidLoad() { + super.viewDidLoad() + + guard let captureDevice = self.captureDevice else { + dismiss(animated: false) + return + } + + self.navigationItem.title = Strings.ScanQRCodeViewTitle + + // Setup the NavigationBar + self.navigationController?.navigationBar.barTintColor = QRCodeViewControllerUX.navigationBarBackgroundColor + self.navigationController?.navigationBar.titleTextAttributes = [NSForegroundColorAttributeName: QRCodeViewControllerUX.navigationBarTitleColor] + + // Setup the NavigationItem + self.navigationItem.leftBarButtonItem = UIBarButtonItem(image: UIImage(named: "qrcode-goBack"), style: UIBarButtonItemStyle.plain, target: self, action: #selector(goBack)) + self.navigationItem.leftBarButtonItem?.tintColor = UIColor.white + + self.navigationItem.rightBarButtonItem = UIBarButtonItem(image: UIImage(named: "qrcode-light"), style: .plain, target: self, action: #selector(openLight)) + if captureDevice.hasTorch { + self.navigationItem.rightBarButtonItem?.tintColor = UIColor.white + } else { + self.navigationItem.rightBarButtonItem?.tintColor = UIColor.gray + self.navigationItem.rightBarButtonItem?.isEnabled = false + } + + let getAuthorizationStatus = AVCaptureDevice.authorizationStatus(forMediaType: AVMediaTypeVideo) + if getAuthorizationStatus != AVAuthorizationStatus.denied { + setupCamera() + } else { + let alert = UIAlertController(title: "", message: Strings.ScanQRCodePermissionErrorMessage, preferredStyle: .alert) + alert.addAction(UIAlertAction(title: Strings.ScanQRCodeErrorOKButton, style: .default, handler: nil)) + self.present(alert, animated: true, completion: nil) + } + + maskView.backgroundColor = QRCodeViewControllerUX.maskViewBackgroungColor + self.view.addSubview(maskView) + self.view.addSubview(scanBorder) + self.view.addSubview(scanLine) + self.view.addSubview(instructionsLabel) + + setupConstraints() + let rectPath = UIBezierPath(rect: UIScreen.main.bounds) + rectPath.append(UIBezierPath(rect: scanRange).reversing()) + shapeLayer.path = rectPath.cgPath + maskView.layer.mask = shapeLayer + + isAnimationing = true + startScanLineAnimation() + } + + override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + self.captureSession.stopRunning() + stopScanLineAnimation() + } + + private func setupConstraints() { + maskView.snp.makeConstraints { (make) in + make.edges.equalTo(self.view) + } + if UIDevice.current.userInterfaceIdiom == .pad { + scanBorder.snp.makeConstraints { (make) in + make.center.equalTo(self.view) + make.width.height.equalTo(view.frame.width / 2) + } + } else { + scanBorder.snp.makeConstraints { (make) in + make.center.equalTo(self.view) + make.width.height.equalTo(view.frame.width / 3 * 2) + } + } + scanLine.snp.makeConstraints { (make) in + make.left.equalTo(scanBorder.snp.left) + make.top.equalTo(scanBorder.snp.top).offset(6) + make.width.equalTo(scanBorder.snp.width) + make.height.equalTo(6) + } + + instructionsLabel.snp.makeConstraints { (make) in + make.left.right.equalTo(self.view.layoutMarginsGuide) + make.top.equalTo(scanBorder.snp.bottom).offset(30) + } + } + + func startScanLineAnimation() { + if !isAnimationing { + return + } + self.view.layoutIfNeeded() + self.view.setNeedsLayout() + UIView.animate(withDuration: 2.4, animations: { + self.scanLine.snp.updateConstraints({ (make) in + make.top.equalTo(self.scanBorder.snp.top).offset(self.scanBorderHeight - 6) + }) + self.view.layoutIfNeeded() + }) { (value: Bool) in + self.scanLine.snp.updateConstraints({ (make) in + make.top.equalTo(self.scanBorder.snp.top).offset(6) + }) + self.perform(#selector(self.startScanLineAnimation), with: nil, afterDelay: 0) + } + } + + func stopScanLineAnimation() { + isAnimationing = false + } + + func goBack() { + self.dismiss(animated: true, completion: nil) + } + + func openLight() { + guard let captureDevice = self.captureDevice else { + return + } + + if isLightOn { + do { + try captureDevice.lockForConfiguration() + captureDevice.torchMode = AVCaptureTorchMode.off + captureDevice.unlockForConfiguration() + navigationItem.rightBarButtonItem?.image = UIImage(named: "qrcode-light") + navigationItem.rightBarButtonItem?.tintColor = UIColor.white + } catch { + print(error) + } + } else { + do { + try captureDevice.lockForConfiguration() + captureDevice.torchMode = AVCaptureTorchMode.on + captureDevice.unlockForConfiguration() + navigationItem.rightBarButtonItem?.image = UIImage(named: "qrcode-isLighting") + navigationItem.rightBarButtonItem?.tintColor = QRCodeViewControllerUX.isLightingNavigationItemColor + } catch { + print(error) + } + } + isLightOn = !isLightOn + } + + func setupCamera() { + guard let captureDevice = self.captureDevice else { + dismiss(animated: false) + return + } + + do { + let input = try AVCaptureDeviceInput(device: captureDevice) + captureSession.addInput(input) + } catch { + print(error) + } + let output = AVCaptureMetadataOutput() + if captureSession.canAddOutput(output) { + captureSession.addOutput(output) + output.setMetadataObjectsDelegate(self, queue: DispatchQueue.main) + output.metadataObjectTypes = [AVMetadataObjectTypeQRCode] + } + if let videoPreviewLayer = AVCaptureVideoPreviewLayer(session: captureSession) { + videoPreviewLayer.videoGravity = AVLayerVideoGravityResizeAspectFill + videoPreviewLayer.frame = UIScreen.main.bounds + view.layer.addSublayer(videoPreviewLayer) + self.videoPreviewLayer = videoPreviewLayer + captureSession.startRunning() + } + } + + override func willAnimateRotation(to toInterfaceOrientation: UIInterfaceOrientation, duration: TimeInterval) { + shapeLayer.removeFromSuperlayer() + let rectPath = UIBezierPath(rect: UIScreen.main.bounds) + rectPath.append(UIBezierPath(rect: scanRange).reversing()) + shapeLayer.path = rectPath.cgPath + maskView.layer.mask = shapeLayer + + guard let videoPreviewLayer = self.videoPreviewLayer else { + return + } + videoPreviewLayer.frame = UIScreen.main.bounds + switch toInterfaceOrientation { + case .portrait: + videoPreviewLayer.connection.videoOrientation = AVCaptureVideoOrientation.portrait + case .landscapeLeft: + videoPreviewLayer.connection.videoOrientation = AVCaptureVideoOrientation.landscapeLeft + case .landscapeRight: + videoPreviewLayer.connection.videoOrientation = AVCaptureVideoOrientation.landscapeRight + case .portraitUpsideDown: + videoPreviewLayer.connection.videoOrientation = AVCaptureVideoOrientation.portraitUpsideDown + default: + videoPreviewLayer.connection.videoOrientation = AVCaptureVideoOrientation.portrait + } + } +} + +extension QRCodeViewController: AVCaptureMetadataOutputObjectsDelegate { + func captureOutput(_ captureOutput: AVCaptureOutput!, didOutputMetadataObjects metadataObjects: [Any]!, from connection: AVCaptureConnection!) { + if metadataObjects == nil || metadataObjects.count == 0 { + self.captureSession.stopRunning() + let alert = UIAlertController(title: "", message: Strings.ScanQRCodeInvalidDataErrorMessage, preferredStyle: .alert) + alert.addAction(UIAlertAction(title: Strings.ScanQRCodeErrorOKButton, style: .default, handler: { (UIAlertAction) in + self.captureSession.startRunning() + })) + self.present(alert, animated: true, completion: nil) + } else { + self.captureSession.stopRunning() + stopScanLineAnimation() + self.dismiss(animated: true, completion: { + guard let metaData = metadataObjects.first as? AVMetadataMachineReadableCodeObject, let qrCodeDelegate = self.qrCodeDelegate, let text = metaData.stringValue else { + Sentry.shared.sendWithStacktrace(message: "Unable to scan QR code", tag: .general) + return + } + + if let url = URIFixup.getURL(text) { + qrCodeDelegate.didScanQRCodeWithURL(url) + } else { + qrCodeDelegate.didScanQRCodeWithText(text) + } + }) + } + } +} + +class QRCodeNavigationController: UINavigationController { + override open var preferredStatusBarStyle: UIStatusBarStyle { + return .lightContent + } +} diff --git a/mobile/ios/Client/Frontend/Browser/ReaderModeBarView.swift b/mobile/ios/Client/Frontend/Browser/ReaderModeBarView.swift new file mode 100644 index 0000000000..07153f0de7 --- /dev/null +++ b/mobile/ios/Client/Frontend/Browser/ReaderModeBarView.swift @@ -0,0 +1,172 @@ +/* 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 UIKit +import SnapKit +import Shared +import XCGLogger + +private let log = Logger.browserLogger + +enum ReaderModeBarButtonType { + case markAsRead, markAsUnread, settings, addToReadingList, removeFromReadingList + + fileprivate var localizedDescription: String { + switch self { + case .markAsRead: return NSLocalizedString("Mark as Read", comment: "Name for Mark as read button in reader mode") + case .markAsUnread: return NSLocalizedString("Mark as Unread", comment: "Name for Mark as unread button in reader mode") + case .settings: return NSLocalizedString("Display Settings", comment: "Name for display settings button in reader mode. Display in the meaning of presentation, not monitor.") + case .addToReadingList: return NSLocalizedString("Add to Reading List", comment: "Name for button adding current article to reading list in reader mode") + case .removeFromReadingList: return NSLocalizedString("Remove from Reading List", comment: "Name for button removing current article from reading list in reader mode") + } + } + + fileprivate var imageName: String { + switch self { + case .markAsRead: return "MarkAsRead" + case .markAsUnread: return "MarkAsUnread" + case .settings: return "SettingsSerif" + case .addToReadingList: return "addToReadingList" + case .removeFromReadingList: return "removeFromReadingList" + } + } + + fileprivate var image: UIImage? { + let image = UIImage(named: imageName) + image?.accessibilityLabel = localizedDescription + return image + } +} + +protocol ReaderModeBarViewDelegate { + func readerModeBar(_ readerModeBar: ReaderModeBarView, didSelectButton buttonType: ReaderModeBarButtonType) +} + +struct ReaderModeBarViewUX { + + static let Themes: [String: Theme] = { + var themes = [String: Theme]() + var theme = Theme() + theme.backgroundColor = UIColor(rgb: 0x38383D) + theme.buttonTintColor = UIColor(rgb: 0xf9f9fA) + themes[Theme.PrivateMode] = theme + + theme = Theme() + theme.backgroundColor = UIColor(rgb: 0xf9f9fA) + theme.buttonTintColor = UIColor(rgb: 0x272727) + themes[Theme.NormalMode] = theme + + return themes + }() +} + +class ReaderModeBarView: UIView { + var delegate: ReaderModeBarViewDelegate? + + var readStatusButton: UIButton! + var settingsButton: UIButton! + var listStatusButton: UIButton! + + dynamic var buttonTintColor: UIColor = UIColor.clear { + didSet { + readStatusButton.tintColor = self.buttonTintColor + settingsButton.tintColor = self.buttonTintColor + listStatusButton.tintColor = self.buttonTintColor + } + } + + override init(frame: CGRect) { + super.init(frame: frame) + + readStatusButton = createButton(.markAsRead, action: #selector(ReaderModeBarView.SELtappedReadStatusButton(_:))) + readStatusButton.accessibilityIdentifier = "ReaderModeBarView.readStatusButton" + readStatusButton.snp.makeConstraints { (make) -> Void in + make.left.equalTo(self) + make.height.centerY.equalTo(self) + make.width.equalTo(80) + } + + settingsButton = createButton(.settings, action: #selector(ReaderModeBarView.SELtappedSettingsButton(_:))) + settingsButton.accessibilityIdentifier = "ReaderModeBarView.settingsButton" + settingsButton.snp.makeConstraints { (make) -> Void in + make.height.centerX.centerY.equalTo(self) + make.width.equalTo(80) + } + + listStatusButton = createButton(.addToReadingList, action: #selector(ReaderModeBarView.SELtappedListStatusButton(_:))) + listStatusButton.accessibilityIdentifier = "ReaderModeBarView.listStatusButton" + listStatusButton.snp.makeConstraints { (make) -> Void in + make.right.equalTo(self) + make.height.centerY.equalTo(self) + make.width.equalTo(80) + } + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func draw(_ rect: CGRect) { + super.draw(rect) + guard let context = UIGraphicsGetCurrentContext() else { return } + context.setLineWidth(0.5) + context.setStrokeColor(red: 0.1, green: 0.1, blue: 0.1, alpha: 1.0) + context.setStrokeColor(UIColor.gray.cgColor) + context.beginPath() + context.move(to: CGPoint(x: 0, y: frame.height)) + context.addLine(to: CGPoint(x: frame.width, y: frame.height)) + context.strokePath() + } + + fileprivate func createButton(_ type: ReaderModeBarButtonType, action: Selector) -> UIButton { + let button = UIButton() + addSubview(button) + button.setImage(type.image, for: UIControlState()) + button.addTarget(self, action: action, for: .touchUpInside) + return button + } + + func SELtappedReadStatusButton(_ sender: UIButton!) { + UnifiedTelemetry.recordEvent(category: .action, method: .tap, object: .readingListItem, value: unread ? .markAsRead : .markAsUnread, extras: [ "from": "reader-mode-toolbar" ]) + delegate?.readerModeBar(self, didSelectButton: unread ? .markAsRead : .markAsUnread) + } + + func SELtappedSettingsButton(_ sender: UIButton!) { + delegate?.readerModeBar(self, didSelectButton: .settings) + } + + func SELtappedListStatusButton(_ sender: UIButton!) { + UnifiedTelemetry.recordEvent(category: .action, method: added ? .delete : .add, object: .readingListItem, value: .readerModeToolbar) + delegate?.readerModeBar(self, didSelectButton: added ? .removeFromReadingList : .addToReadingList) + } + + var unread: Bool = true { + didSet { + let buttonType: ReaderModeBarButtonType = unread && added ? .markAsRead : .markAsUnread + readStatusButton.setImage(buttonType.image, for: UIControlState()) + readStatusButton.isEnabled = added + readStatusButton.alpha = added ? 1.0 : 0.6 + } + } + + var added: Bool = false { + didSet { + let buttonType: ReaderModeBarButtonType = added ? .removeFromReadingList : .addToReadingList + listStatusButton.setImage(buttonType.image, for: UIControlState()) + } + } +} + +extension ReaderModeBarView: Themeable { + func applyTheme(_ themeName: String) { + guard let theme = ReaderModeBarViewUX.Themes[themeName] else { + log.error("Unable to apply unknown theme \(themeName)") + return + } + + backgroundColor = theme.backgroundColor + buttonTintColor = theme.buttonTintColor! + } +} diff --git a/mobile/ios/Client/Frontend/Browser/ScreenshotHelper.swift b/mobile/ios/Client/Frontend/Browser/ScreenshotHelper.swift new file mode 100644 index 0000000000..83370dba8e --- /dev/null +++ b/mobile/ios/Client/Frontend/Browser/ScreenshotHelper.swift @@ -0,0 +1,59 @@ +/* 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 + +/** + * Handles screenshots for a given tab, including pages with non-webview content. + */ +class ScreenshotHelper { + var viewIsVisible = false + + fileprivate weak var controller: BrowserViewController? + + init(controller: BrowserViewController) { + self.controller = controller + } + + func takeScreenshot(_ tab: Tab) { + var screenshot: UIImage? + + if let url = tab.url { + if url.isAboutHomeURL { + if let homePanel = controller?.homePanelController { + screenshot = homePanel.view.screenshot(quality: UIConstants.ActiveScreenshotQuality) + } + } else { + let offset = CGPoint(x: 0, y: -(tab.webView?.scrollView.contentInset.top ?? 0)) + screenshot = tab.webView?.screenshot(offset: offset, quality: UIConstants.ActiveScreenshotQuality) + } + } + + tab.setScreenshot(screenshot) + } + + /// Takes a screenshot after a small delay. + /// Trying to take a screenshot immediately after didFinishNavigation results in a screenshot + /// of the previous page, presumably due to an iOS bug. Adding a brief delay fixes this. + func takeDelayedScreenshot(_ tab: Tab) { + let time = DispatchTime.now() + Double(Int64(100 * NSEC_PER_MSEC)) / Double(NSEC_PER_SEC) + DispatchQueue.main.asyncAfter(deadline: time) { + // If the view controller isn't visible, the screenshot will be blank. + // Wait until the view controller is visible again to take the screenshot. + guard self.viewIsVisible else { + tab.pendingScreenshot = true + return + } + + self.takeScreenshot(tab) + } + } + + func takePendingScreenshots(_ tabs: [Tab]) { + for tab in tabs where tab.pendingScreenshot { + tab.pendingScreenshot = false + takeDelayedScreenshot(tab) + } + } +} diff --git a/mobile/ios/Client/Frontend/Browser/SearchEngines.swift b/mobile/ios/Client/Frontend/Browser/SearchEngines.swift new file mode 100644 index 0000000000..6482f4d8ad --- /dev/null +++ b/mobile/ios/Client/Frontend/Browser/SearchEngines.swift @@ -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! + } + } +} diff --git a/mobile/ios/Client/Frontend/Browser/SearchLoader.swift b/mobile/ios/Client/Frontend/Browser/SearchLoader.swift new file mode 100644 index 0000000000..ffae17d23f --- /dev/null +++ b/mobile/ios/Client/Frontend/Browser/SearchLoader.swift @@ -0,0 +1,135 @@ +/* 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 URLBeforePathRegex = try! NSRegularExpression(pattern: "^https?://([^/]+)/", options: []) + +// TODO: Swift currently requires that classes extending generic classes must also be generic. +// This is a workaround until that requirement is fixed. +typealias SearchLoader = _SearchLoader + +/** + * Shared data source for the SearchViewController and the URLBar domain completion. + * Since both of these use the same SQL query, we can perform the query once and dispatch the results. + */ +class _SearchLoader: Loader, SearchViewController> { + fileprivate let profile: Profile + fileprivate let urlBar: URLBarView + fileprivate let frecentHistory: FrecentHistory + + init(profile: Profile, urlBar: URLBarView) { + self.profile = profile + self.urlBar = urlBar + frecentHistory = profile.history.getFrecentHistory() + + super.init() + } + + fileprivate lazy var topDomains: [String] = { + let filePath = Bundle.main.path(forResource: "topdomains", ofType: "txt") + return try! String(contentsOfFile: filePath!).components(separatedBy: "\n") + }() + + // `weak` usage here allows deferred queue to be the owner. The deferred is always filled and this set to nil, + // this is defensive against any changes to queue (or cancellation) behaviour in future. + private weak var currentDbQuery: Cancellable? + + var query: String = "" { + didSet { + guard let profile = self.profile as? BrowserProfile else { + assertionFailure("nil profile") + return + } + + if query.isEmpty { + load(Cursor(status: .success, msg: "Empty query")) + return + } + + if let currentDbQuery = currentDbQuery { + profile.db.cancel(databaseOperation: WeakRef(currentDbQuery)) + } + + let deferred = frecentHistory.getSites(whereURLContains: query, historyLimit: 100, bookmarksLimit: 5) + currentDbQuery = deferred as? Cancellable + + deferred.uponQueue(DispatchQueue.main) { result in + defer { + self.currentDbQuery = nil + } + + guard let deferred = deferred as? Cancellable, !deferred.cancelled else { + return + } + + // Failed cursors are excluded in .get(). + if let cursor = result.successValue { + // First, see if the query matches any URLs from the user's search history. + self.load(cursor) + for site in cursor { + if let url = site?.url, + let completion = self.completionForURL(url) { + self.urlBar.setAutocompleteSuggestion(completion) + return + } + } + + // If there are no search history matches, try matching one of the Alexa top domains. + for domain in self.topDomains { + if let completion = self.completionForDomain(domain) { + self.urlBar.setAutocompleteSuggestion(completion) + return + } + } + } + } + } + } + + fileprivate func completionForURL(_ url: String) -> String? { + // Extract the pre-path substring from the URL. This should be more efficient than parsing via + // NSURL since we need to only look at the beginning of the string. + // Note that we won't match non-HTTP(S) URLs. + guard let match = URLBeforePathRegex.firstMatch(in: url, options: NSRegularExpression.MatchingOptions(), range: NSRange(location: 0, length: url.characters.count)) else { + return nil + } + + // If the pre-path component (including the scheme) starts with the query, just use it as is. + var prePathURL = (url as NSString).substring(with: match.rangeAt(0)) + if prePathURL.startsWith(query) { + // Trailing slashes in the autocompleteTextField cause issues with Swype keyboard. Bug 1194714 + if prePathURL.endsWith("/") { + prePathURL.remove(at: prePathURL.index(before: prePathURL.endIndex)) + } + return prePathURL + } + + // Otherwise, find and use any matching domain. + // To simplify the search, prepend a ".", and search the string for ".query". + // For example, for http://en.m.wikipedia.org, domainWithDotPrefix will be ".en.m.wikipedia.org". + // This allows us to use the "." as a separator, so we can match "en", "m", "wikipedia", and "org", + let domain = (url as NSString).substring(with: match.rangeAt(1)) + return completionForDomain(domain) + } + + fileprivate func completionForDomain(_ domain: String) -> String? { + let domainWithDotPrefix: String = ".\(domain)" + if let range = domainWithDotPrefix.range(of: ".\(query)", options: NSString.CompareOptions.caseInsensitive, range: nil, locale: nil) { + // We don't actually want to match the top-level domain ("com", "org", etc.) by itself, so + // so make sure the result includes at least one ".". + let matchedDomain: String = domainWithDotPrefix.substring(from: domainWithDotPrefix.index(range.lowerBound, offsetBy: 1)) + if matchedDomain.contains(".") { + return matchedDomain + } + } + + return nil + } +} diff --git a/mobile/ios/Client/Frontend/Browser/SearchSuggestClient.swift b/mobile/ios/Client/Frontend/Browser/SearchSuggestClient.swift new file mode 100644 index 0000000000..21f05120fa --- /dev/null +++ b/mobile/ios/Client/Frontend/Browser/SearchSuggestClient.swift @@ -0,0 +1,78 @@ +/* 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 Alamofire +import Foundation +import Shared + +let SearchSuggestClientErrorDomain = "org.mozilla.firefox.SearchSuggestClient" +let SearchSuggestClientErrorInvalidEngine = 0 +let SearchSuggestClientErrorInvalidResponse = 1 + +/* + * Clients of SearchSuggestionClient should retain the object during the + * lifetime of the search suggestion query, as requests are canceled during destruction. + * + * Query callbacks that must run even if they are cancelled should wrap their contents in `withExtendendLifetime`. + */ +class SearchSuggestClient { + fileprivate let searchEngine: OpenSearchEngine + fileprivate weak var request: Request? + fileprivate let userAgent: String + + lazy fileprivate var alamofire: SessionManager = { + let configuration = URLSessionConfiguration.ephemeral + var defaultHeaders = SessionManager.default.session.configuration.httpAdditionalHeaders ?? [:] + defaultHeaders["User-Agent"] = self.userAgent + configuration.httpAdditionalHeaders = defaultHeaders + return SessionManager(configuration: configuration) + }() + + init(searchEngine: OpenSearchEngine, userAgent: String) { + self.searchEngine = searchEngine + self.userAgent = userAgent + } + + func query(_ query: String, callback: @escaping (_ response: [String]?, _ error: NSError?) -> Void) { + let url = searchEngine.suggestURLForQuery(query) + if url == nil { + let error = NSError(domain: SearchSuggestClientErrorDomain, code: SearchSuggestClientErrorInvalidEngine, userInfo: nil) + callback(nil, error) + return + } + + request = alamofire.request(url!) + .validate(statusCode: 200..<300) + .responseJSON { response in + if let error = response.result.error { + callback(nil, error as NSError?) + return + } + + // The response will be of the following format: + // ["foobar",["foobar","foobar2000 mac","foobar skins",...]] + // That is, an array of at least two elements: the search term and an array of suggestions. + let array = response.result.value as? NSArray + if array?.count ?? 0 < 2 { + let error = NSError(domain: SearchSuggestClientErrorDomain, code: SearchSuggestClientErrorInvalidResponse, userInfo: nil) + callback(nil, error) + return + } + + let suggestions = array?[1] as? [String] + if suggestions == nil { + let error = NSError(domain: SearchSuggestClientErrorDomain, code: SearchSuggestClientErrorInvalidResponse, userInfo: nil) + callback(nil, error) + return + } + + callback(suggestions!, nil) + } + + } + + func cancelPendingRequest() { + request?.cancel() + } +} diff --git a/mobile/ios/Client/Frontend/Browser/SearchViewController.swift b/mobile/ios/Client/Frontend/Browser/SearchViewController.swift new file mode 100644 index 0000000000..65b1c0daf3 --- /dev/null +++ b/mobile/ios/Client/Frontend/Browser/SearchViewController.swift @@ -0,0 +1,635 @@ +/* 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 UIKit +import Shared +import Storage +import Telemetry + +private enum SearchListSection: Int { + case searchSuggestions + case bookmarksAndHistory + static let Count = 2 +} + +private struct SearchViewControllerUX { + static let SearchEngineScrollViewBackgroundColor = UIColor.white.withAlphaComponent(0.8).cgColor + static let SearchEngineScrollViewBorderColor = UIColor.black.withAlphaComponent(0.2).cgColor + + // TODO: This should use ToolbarHeight in BVC. Fix this when we create a shared theming file. + static let EngineButtonHeight: Float = 44 + static let EngineButtonWidth = EngineButtonHeight * 1.4 + static let EngineButtonBackgroundColor = UIColor.clear.cgColor + + static let SearchImage = "search" + static let SearchEngineTopBorderWidth = 0.5 + static let SearchImageHeight: Float = 44 + static let SearchImageWidth: Float = 24 + + static let SuggestionBackgroundColor = UIColor(red: 1, green: 1, blue: 1, alpha: 0.8) + static let SuggestionBorderColor = UIConstants.HighlightBlue + static let SuggestionBorderWidth: CGFloat = 1 + static let SuggestionCornerRadius: CGFloat = 4 + static let SuggestionInsets = UIEdgeInsets(top: 8, left: 8, bottom: 8, right: 8) + static let SuggestionMargin: CGFloat = 8 + static let SuggestionCellVerticalPadding: CGFloat = 10 + static let SuggestionCellMaxRows = 2 + + static let IconSize: CGFloat = 23 + static let FaviconSize: CGFloat = 29 + static let IconBorderColor = UIColor(white: 0, alpha: 0.1) + static let IconBorderWidth: CGFloat = 0.5 +} + +protocol SearchViewControllerDelegate: class { + func searchViewController(_ searchViewController: SearchViewController, didSelectURL url: URL) + func searchViewController(_ searchViewController: SearchViewController, didLongPressSuggestion suggestion: String) + func presentSearchSettingsController() +} + +class SearchViewController: SiteTableViewController, KeyboardHelperDelegate, LoaderListener { + var searchDelegate: SearchViewControllerDelegate? + + fileprivate let isPrivate: Bool + fileprivate var suggestClient: SearchSuggestClient? + + // Views for displaying the bottom scrollable search engine list. searchEngineScrollView is the + // scrollable container; searchEngineScrollViewContent contains the actual set of search engine buttons. + fileprivate let searchEngineScrollView = ButtonScrollView() + fileprivate let searchEngineScrollViewContent = UIView() + + fileprivate lazy var bookmarkedBadge: UIImage = { + return UIImage(named: "bookmarked_passive")! + }() + + // Cell for the suggestion flow layout. Since heightForHeaderInSection is called *before* + // cellForRowAtIndexPath, we create the cell to find its height before it's added to the table. + fileprivate let suggestionCell = SuggestionCell(style: UITableViewCellStyle.default, reuseIdentifier: nil) + + static var userAgent: String? + + init(isPrivate: Bool) { + self.isPrivate = isPrivate + super.init(nibName: nil, bundle: nil) + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + view.backgroundColor = UIConstants.PanelBackgroundColor + let blur = UIVisualEffectView(effect: UIBlurEffect(style: UIBlurEffectStyle.light)) + view.addSubview(blur) + + super.viewDidLoad() + + KeyboardHelper.defaultHelper.addDelegate(self) + + searchEngineScrollView.layer.backgroundColor = SearchViewControllerUX.SearchEngineScrollViewBackgroundColor + searchEngineScrollView.layer.shadowRadius = 0 + searchEngineScrollView.layer.shadowOpacity = 100 + searchEngineScrollView.layer.shadowOffset = CGSize(width: 0, height: -SearchViewControllerUX.SearchEngineTopBorderWidth) + searchEngineScrollView.layer.shadowColor = SearchViewControllerUX.SearchEngineScrollViewBorderColor + searchEngineScrollView.clipsToBounds = false + + searchEngineScrollView.decelerationRate = UIScrollViewDecelerationRateFast + view.addSubview(searchEngineScrollView) + + searchEngineScrollViewContent.layer.backgroundColor = UIColor.clear.cgColor + searchEngineScrollView.addSubview(searchEngineScrollViewContent) + + layoutTable() + layoutSearchEngineScrollView() + + searchEngineScrollViewContent.snp.makeConstraints { make in + make.center.equalTo(self.searchEngineScrollView).priority(10) + //left-align the engines on iphones, center on ipad + if UIScreen.main.traitCollection.horizontalSizeClass == .compact { + make.left.equalTo(self.searchEngineScrollView).priority(1000) + } else { + make.left.greaterThanOrEqualTo(self.searchEngineScrollView).priority(1000) + } + make.right.lessThanOrEqualTo(self.searchEngineScrollView).priority(1000) + make.top.equalTo(self.searchEngineScrollView) + make.bottom.equalTo(self.searchEngineScrollView) + } + + blur.snp.makeConstraints { make in + make.edges.equalTo(self.view) + } + + suggestionCell.delegate = self + + NotificationCenter.default.addObserver(self, selector: #selector(SearchViewController.SELDynamicFontChanged(_:)), name: NotificationDynamicFontChanged, object: nil) + } + + func SELDynamicFontChanged(_ notification: Notification) { + guard notification.name == NotificationDynamicFontChanged else { return } + + reloadData() + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + reloadSearchEngines() + reloadData() + } + + fileprivate func layoutSearchEngineScrollView() { + let keyboardHeight = KeyboardHelper.defaultHelper.currentState?.intersectionHeightForView(self.view) ?? 0 + searchEngineScrollView.snp.remakeConstraints { make in + make.left.right.equalTo(self.view) + make.bottom.equalTo(self.view).offset(-keyboardHeight) + } + } + + var searchEngines: SearchEngines! { + didSet { + suggestClient?.cancelPendingRequest() + + // Query and reload the table with new search suggestions. + querySuggestClient() + + // Show the default search engine first. + if !isPrivate { + let ua = SearchViewController.userAgent as String! ?? "FxSearch" + suggestClient = SearchSuggestClient(searchEngine: searchEngines.defaultEngine, userAgent: ua) + } + + // Reload the footer list of search engines. + reloadSearchEngines() + } + } + + fileprivate var quickSearchEngines: [OpenSearchEngine] { + var engines = searchEngines.quickSearchEngines + + // If we're not showing search suggestions, the default search engine won't be visible + // at the top of the table. Show it with the others in the bottom search bar. + if isPrivate || !searchEngines.shouldShowSearchSuggestions { + engines?.insert(searchEngines.defaultEngine, at: 0) + } + + return engines! + } + + var searchQuery: String = "" { + didSet { + // Reload the tableView to show the updated text in each engine. + reloadData() + } + } + + override func reloadData() { + querySuggestClient() + } + + fileprivate func layoutTable() { + tableView.snp.remakeConstraints { make in + make.top.equalTo(self.view.snp.top) + make.leading.trailing.equalTo(self.view) + make.bottom.equalTo(self.searchEngineScrollView.snp.top) + } + } + + fileprivate func reloadSearchEngines() { + searchEngineScrollViewContent.subviews.forEach { $0.removeFromSuperview() } + var leftEdge = searchEngineScrollViewContent.snp.left + + //search settings icon + let searchButton = UIButton() + searchButton.setImage(UIImage(named: "quickSearch"), for: UIControlState()) + searchButton.imageView?.contentMode = UIViewContentMode.center + searchButton.layer.backgroundColor = SearchViewControllerUX.EngineButtonBackgroundColor + searchButton.addTarget(self, action: #selector(SearchViewController.SELdidClickSearchButton), for: UIControlEvents.touchUpInside) + searchButton.accessibilityLabel = String(format: NSLocalizedString("Search Settings", tableName: "Search", comment: "Label for search settings button.")) + + searchButton.imageView?.snp.makeConstraints { make in + make.width.height.equalTo(SearchViewControllerUX.SearchImageWidth) + return + } + + searchEngineScrollViewContent.addSubview(searchButton) + searchButton.snp.makeConstraints { make in + make.size.equalTo(SearchViewControllerUX.FaviconSize) + //offset the left edge to align with search results + make.left.equalTo(leftEdge).offset(SearchViewControllerUX.SuggestionMargin * 2) + make.top.equalTo(self.searchEngineScrollViewContent).offset(SearchViewControllerUX.SuggestionMargin) + make.bottom.equalTo(self.searchEngineScrollViewContent).offset(-SearchViewControllerUX.SuggestionMargin) + } + + //search engines + leftEdge = searchButton.snp.right + for engine in quickSearchEngines { + let engineButton = UIButton() + engineButton.setImage(engine.image, for: UIControlState()) + engineButton.imageView?.contentMode = UIViewContentMode.scaleAspectFit + engineButton.layer.backgroundColor = SearchViewControllerUX.EngineButtonBackgroundColor + engineButton.addTarget(self, action: #selector(SearchViewController.SELdidSelectEngine(_:)), for: UIControlEvents.touchUpInside) + engineButton.accessibilityLabel = String(format: NSLocalizedString("%@ search", tableName: "Search", comment: "Label for search engine buttons. The argument corresponds to the name of the search engine."), engine.shortName) + + engineButton.imageView?.snp.makeConstraints { make in + make.width.height.equalTo(SearchViewControllerUX.FaviconSize) + return + } + + searchEngineScrollViewContent.addSubview(engineButton) + engineButton.snp.makeConstraints { make in + make.width.equalTo(SearchViewControllerUX.EngineButtonWidth) + make.height.equalTo(SearchViewControllerUX.EngineButtonHeight) + make.left.equalTo(leftEdge) + make.top.equalTo(self.searchEngineScrollViewContent) + make.bottom.equalTo(self.searchEngineScrollViewContent) + if engine === self.searchEngines.quickSearchEngines.last { + make.right.equalTo(self.searchEngineScrollViewContent) + } + } + leftEdge = engineButton.snp.right + } + } + + func SELdidSelectEngine(_ sender: UIButton) { + // The UIButtons are the same cardinality and order as the array of quick search engines. + // Subtract 1 from index to account for magnifying glass accessory. + guard let index = searchEngineScrollViewContent.subviews.index(of: sender) else { + assertionFailure() + return + } + + let engine = quickSearchEngines[index - 1] + + guard let url = engine.searchURLForQuery(searchQuery) else { + assertionFailure() + return + } + + Telemetry.default.recordSearch(location: .quickSearch, searchEngine: engine.engineID ?? "other") + + searchDelegate?.searchViewController(self, didSelectURL: url) + } + + func SELdidClickSearchButton() { + self.searchDelegate?.presentSearchSettingsController() + } + + func keyboardHelper(_ keyboardHelper: KeyboardHelper, keyboardWillShowWithState state: KeyboardState) { + animateSearchEnginesWithKeyboard(state) + } + + func keyboardHelper(_ keyboardHelper: KeyboardHelper, keyboardDidShowWithState state: KeyboardState) { + } + + func keyboardHelper(_ keyboardHelper: KeyboardHelper, keyboardWillHideWithState state: KeyboardState) { + animateSearchEnginesWithKeyboard(state) + } + + override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { + super.viewWillTransition(to: size, with: coordinator) + // The height of the suggestions row may change, so call reloadData() to recalculate cell heights. + coordinator.animate(alongsideTransition: { _ in + self.tableView.reloadData() + }, completion: nil) + } + + fileprivate func animateSearchEnginesWithKeyboard(_ keyboardState: KeyboardState) { + layoutSearchEngineScrollView() + + UIView.animate(withDuration: keyboardState.animationDuration, animations: { + UIView.setAnimationCurve(keyboardState.animationCurve) + self.view.layoutIfNeeded() + }) + } + + fileprivate func querySuggestClient() { + suggestClient?.cancelPendingRequest() + + if searchQuery.isEmpty || !searchEngines.shouldShowSearchSuggestions || searchQuery.looksLikeAURL() { + suggestionCell.suggestions = [] + tableView.reloadData() + return + } + + suggestClient?.query(searchQuery, callback: { suggestions, error in + if let error = error { + let isSuggestClientError = error.domain == SearchSuggestClientErrorDomain + + switch error.code { + case NSURLErrorCancelled where error.domain == NSURLErrorDomain: + // Request was cancelled. Do nothing. + break + case SearchSuggestClientErrorInvalidEngine where isSuggestClientError: + // Engine does not support search suggestions. Do nothing. + break + case SearchSuggestClientErrorInvalidResponse where isSuggestClientError: + print("Error: Invalid search suggestion data") + default: + print("Error: \(error.description)") + } + } else { + self.suggestionCell.suggestions = suggestions! + } + + // If there are no suggestions, just use whatever the user typed. + if suggestions?.isEmpty ?? true { + self.suggestionCell.suggestions = [self.searchQuery] + } + + // Reload the tableView to show the new list of search suggestions. + self.tableView.reloadData() + }) + } + + func loader(dataLoaded data: Cursor) { + self.data = data + tableView.reloadData() + } + + func tableView(_ tableView: UITableView, didSelectRowAtIndexPath indexPath: IndexPath) { + let section = SearchListSection(rawValue: indexPath.section)! + if section == SearchListSection.bookmarksAndHistory { + if let site = data[indexPath.row] { + if let url = URL(string: site.url) { + searchDelegate?.searchViewController(self, didSelectURL: url) + UnifiedTelemetry.recordEvent(category: .action, method: .open, object: .bookmark, value: .awesomebarResults) + } + } + } + } + + override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { + if let currentSection = SearchListSection(rawValue: indexPath.section) { + switch currentSection { + case .searchSuggestions: + // heightForRowAtIndexPath is called *before* the cell is created, so to get the height, + // force a layout pass first. + suggestionCell.layoutIfNeeded() + return suggestionCell.frame.height + default: + return super.tableView(tableView, heightForRowAt: indexPath) + } + } + + return 0 + } + + override func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { + return 0 + } + + override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + switch SearchListSection(rawValue: indexPath.section)! { + case .searchSuggestions: + suggestionCell.imageView?.image = searchEngines.defaultEngine.image + suggestionCell.imageView?.isAccessibilityElement = true + suggestionCell.imageView?.accessibilityLabel = String(format: NSLocalizedString("Search suggestions from %@", tableName: "Search", comment: "Accessibility label for image of default search engine displayed left to the actual search suggestions from the engine. The parameter substituted for \"%@\" is the name of the search engine. E.g.: Search suggestions from Google"), searchEngines.defaultEngine.shortName) + return suggestionCell + + case .bookmarksAndHistory: + let cell = super.tableView(tableView, cellForRowAt: indexPath) + if let site = data[indexPath.row] { + if let cell = cell as? TwoLineTableViewCell { + let isBookmark = site.bookmarked ?? false + cell.setLines(site.title, detailText: site.url) + cell.setRightBadge(isBookmark ? self.bookmarkedBadge : nil) + cell.imageView!.layer.borderColor = SearchViewControllerUX.IconBorderColor.cgColor + cell.imageView!.layer.borderWidth = SearchViewControllerUX.IconBorderWidth + cell.imageView?.setIcon(site.icon, forURL: site.tileURL, completed: { (color, url) in + if site.tileURL == url { + cell.imageView?.image = cell.imageView?.image?.createScaled(CGSize(width: SearchViewControllerUX.IconSize, height: SearchViewControllerUX.IconSize)) + cell.imageView?.contentMode = .center + cell.imageView?.backgroundColor = color + } + }) + } + } + return cell + } + } + + override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + switch SearchListSection(rawValue: section)! { + case .searchSuggestions: + return searchEngines.shouldShowSearchSuggestions && !searchQuery.looksLikeAURL() && !isPrivate ? 1 : 0 + case .bookmarksAndHistory: + return data.count + } + } + + func numberOfSectionsInTableView(_ tableView: UITableView) -> Int { + return SearchListSection.Count + } +} + +extension SearchViewController: SuggestionCellDelegate { + fileprivate func suggestionCell(_ suggestionCell: SuggestionCell, didSelectSuggestion suggestion: String) { + // Assume that only the default search engine can provide search suggestions. + let engine = searchEngines.defaultEngine + + var url = URIFixup.getURL(suggestion) + if url == nil { + url = engine.searchURLForQuery(suggestion) + } + + Telemetry.default.recordSearch(location: .suggestion, searchEngine: engine.engineID ?? "other") + + if let url = url { + searchDelegate?.searchViewController(self, didSelectURL: url) + } + } + + fileprivate func suggestionCell(_ suggestionCell: SuggestionCell, didLongPressSuggestion suggestion: String) { + searchDelegate?.searchViewController(self, didLongPressSuggestion: suggestion) + } +} + +/** + * Private extension containing string operations specific to this view controller + */ +fileprivate extension String { + func looksLikeAURL() -> Bool { + // The assumption here is that if the user is typing in a forward slash and there are no spaces + // involved, it's going to be a URL. If we type a space, any url would be invalid. + // See https://bugzilla.mozilla.org/show_bug.cgi?id=1192155 for additional details. + return self.contains("/") && !self.contains(" ") + } +} + +/** + * UIScrollView that prevents buttons from interfering with scroll. + */ +fileprivate class ButtonScrollView: UIScrollView { + fileprivate override func touchesShouldCancel(in view: UIView) -> Bool { + return true + } +} + +fileprivate protocol SuggestionCellDelegate: class { + func suggestionCell(_ suggestionCell: SuggestionCell, didSelectSuggestion suggestion: String) + func suggestionCell(_ suggestionCell: SuggestionCell, didLongPressSuggestion suggestion: String) +} + +/** + * Cell that wraps a list of search suggestion buttons. + */ +fileprivate class SuggestionCell: UITableViewCell { + weak var delegate: SuggestionCellDelegate? + let container = UIView() + + override init(style: UITableViewCellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + + isAccessibilityElement = false + accessibilityLabel = nil + layoutMargins = UIEdgeInsets.zero + separatorInset = UIEdgeInsets.zero + selectionStyle = UITableViewCellSelectionStyle.none + + container.backgroundColor = UIColor.clear + contentView.backgroundColor = UIColor.clear + backgroundColor = UIColor.clear + contentView.addSubview(container) + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + var suggestions: [String] = [] { + didSet { + for view in container.subviews { + view.removeFromSuperview() + } + + for suggestion in suggestions { + let button = SuggestionButton() + button.setTitle(suggestion, for: UIControlState()) + button.addTarget(self, action: #selector(SuggestionCell.SELdidSelectSuggestion(_:)), for: UIControlEvents.touchUpInside) + button.addGestureRecognizer(UILongPressGestureRecognizer(target: self, action: #selector(SuggestionCell.SELdidLongPressSuggestion(_:)))) + + // If this is the first image, add the search icon. + if container.subviews.isEmpty { + let image = UIImage(named: SearchViewControllerUX.SearchImage) + button.setImage(image, for: UIControlState()) + button.titleEdgeInsets = UIEdgeInsets(top: 0, left: 8, bottom: 0, right: 0) + } + + container.addSubview(button) + } + + setNeedsLayout() + } + } + + @objc + func SELdidSelectSuggestion(_ sender: UIButton) { + delegate?.suggestionCell(self, didSelectSuggestion: sender.titleLabel!.text!) + } + + @objc + func SELdidLongPressSuggestion(_ recognizer: UILongPressGestureRecognizer) { + if recognizer.state == UIGestureRecognizerState.began { + if let button = recognizer.view as! UIButton? { + delegate?.suggestionCell(self, didLongPressSuggestion: button.titleLabel!.text!) + } + } + } + + fileprivate override func layoutSubviews() { + super.layoutSubviews() + + // The left bounds of the suggestions, aligned with where text would be displayed. + let textLeft: CGFloat = 61 + + // The maximum width of the container, after which suggestions will wrap to the next line. + let maxWidth = contentView.frame.width + + let imageSize = CGFloat(SearchViewControllerUX.FaviconSize) + + // The height of the suggestions container (minus margins), used to determine the frame. + // We set it to imageSize.height as a minimum since we don't want the cell to be shorter than the icon + var height: CGFloat = imageSize + + var currentLeft = textLeft + var currentTop = SearchViewControllerUX.SuggestionCellVerticalPadding + var currentRow = 0 + + for view in container.subviews { + let button = view as! UIButton + var buttonSize = button.intrinsicContentSize + + // Update our base frame height by the max size of either the image or the button so we never + // make the cell smaller than any of the two + if height == imageSize { + height = max(buttonSize.height, imageSize) + } + + var width = currentLeft + buttonSize.width + SearchViewControllerUX.SuggestionMargin + if width > maxWidth { + // Only move to the next row if there's already a suggestion on this row. + // Otherwise, the suggestion is too big to fit and will be resized below. + if currentLeft > textLeft { + currentRow += 1 + if currentRow >= SearchViewControllerUX.SuggestionCellMaxRows { + // Don't draw this button if it doesn't fit on the row. + button.frame = CGRect.zero + continue + } + + currentLeft = textLeft + currentTop += buttonSize.height + SearchViewControllerUX.SuggestionMargin + height += buttonSize.height + SearchViewControllerUX.SuggestionMargin + width = currentLeft + buttonSize.width + SearchViewControllerUX.SuggestionMargin + } + + // If the suggestion is too wide to fit on its own row, shrink it. + if width > maxWidth { + buttonSize.width = maxWidth - currentLeft - SearchViewControllerUX.SuggestionMargin + } + } + + button.frame = CGRect(x: currentLeft, y: currentTop, width: buttonSize.width, height: buttonSize.height) + currentLeft += buttonSize.width + SearchViewControllerUX.SuggestionMargin + } + + frame.size.height = height + 2 * SearchViewControllerUX.SuggestionCellVerticalPadding + contentView.frame = bounds + container.frame = bounds + + let imageX = (textLeft - imageSize) / 2 + let imageY = (frame.size.height - imageSize) / 2 + imageView!.frame = CGRect(x: imageX, y: imageY, width: imageSize, height: imageSize) + } +} + +/** + * Rounded search suggestion button that highlights when selected. + */ +fileprivate class SuggestionButton: InsetButton { + override init(frame: CGRect) { + super.init(frame: frame) + + setTitleColor(UIConstants.HighlightBlue, for: UIControlState()) + setTitleColor(UIColor.white, for: UIControlState.highlighted) + titleLabel?.font = DynamicFontHelper.defaultHelper.DefaultMediumFont + backgroundColor = SearchViewControllerUX.SuggestionBackgroundColor + layer.borderColor = SearchViewControllerUX.SuggestionBorderColor.cgColor + layer.borderWidth = SearchViewControllerUX.SuggestionBorderWidth + layer.cornerRadius = SearchViewControllerUX.SuggestionCornerRadius + contentEdgeInsets = SearchViewControllerUX.SuggestionInsets + + accessibilityHint = NSLocalizedString("Searches for the suggestion", comment: "Accessibility hint describing the action performed when a search suggestion is clicked") + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + @objc + override var isHighlighted: Bool { + didSet { + backgroundColor = isHighlighted ? UIConstants.HighlightBlue : SearchViewControllerUX.SuggestionBackgroundColor + } + } +} diff --git a/mobile/ios/Client/Frontend/Browser/SessionData.swift b/mobile/ios/Client/Frontend/Browser/SessionData.swift new file mode 100644 index 0000000000..b2919e76b6 --- /dev/null +++ b/mobile/ios/Client/Frontend/Browser/SessionData.swift @@ -0,0 +1,50 @@ +/* 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 + +class SessionData: NSObject, NSCoding { + let currentPage: Int + let urls: [URL] + let lastUsedTime: Timestamp + + var jsonDictionary: [String: Any] { + return [ + "currentPage": String(self.currentPage), + "lastUsedTime": String(self.lastUsedTime), + "urls": urls.map { $0.absoluteString } + ] + } + + /** + Creates a new SessionData object representing a serialized tab. + + - parameter currentPage: The active page index. Must be in the range of (-N, 0], + where 1-N is the first page in history, and 0 is the last. + - parameter urls: The sequence of URLs in this tab's session history. + - parameter lastUsedTime: The last time this tab was modified. + **/ + init(currentPage: Int, urls: [URL], lastUsedTime: Timestamp) { + self.currentPage = currentPage + self.urls = urls + self.lastUsedTime = lastUsedTime + + assert(urls.count > 0, "Session has at least one entry") + assert(currentPage > -urls.count && currentPage <= 0, "Session index is valid") + } + + required init?(coder: NSCoder) { + self.currentPage = coder.decodeAsInt(forKey: "currentPage") + self.urls = coder.decodeObject(forKey: "urls") as? [URL] ?? [] + self.lastUsedTime = coder.decodeAsUInt64(forKey: "lastUsedTime") + } + + func encode(with coder: NSCoder) { + coder.encode(currentPage, forKey: "currentPage") + coder.encode(urls, forKey: "urls") + coder.encode(Int64(lastUsedTime), forKey: "lastUsedTime") + } +} diff --git a/mobile/ios/Client/Frontend/Browser/SessionRestoreHandler.swift b/mobile/ios/Client/Frontend/Browser/SessionRestoreHandler.swift new file mode 100644 index 0000000000..554ced8447 --- /dev/null +++ b/mobile/ios/Client/Frontend/Browser/SessionRestoreHandler.swift @@ -0,0 +1,30 @@ +/* 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 WebKit +import GCDWebServers +import Shared + +/// Handles requests to /about/sessionrestore to restore session history. +struct SessionRestoreHandler { + static func register(_ webServer: WebServer) { + // Register the handler that accepts /about/sessionrestore?history=...¤tpage=... requests. + webServer.registerHandlerForMethod("GET", module: "about", resource: "sessionrestore") { _ in + if let sessionRestorePath = Bundle.main.path(forResource: "SessionRestore", ofType: "html") { + do { + let sessionRestoreString = try String(contentsOfFile: sessionRestorePath) + + defer { + NotificationCenter.default.post(name: NotificationDidRestoreSession, object: self) + } + + return GCDWebServerDataResponse(html: sessionRestoreString) + } catch _ {} + } + + return GCDWebServerResponse(statusCode: 404) + } + } +} diff --git a/mobile/ios/Client/Frontend/Browser/SessionRestoreHelper.swift b/mobile/ios/Client/Frontend/Browser/SessionRestoreHelper.swift new file mode 100644 index 0000000000..de8b6213ce --- /dev/null +++ b/mobile/ios/Client/Frontend/Browser/SessionRestoreHelper.swift @@ -0,0 +1,37 @@ +/* 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 WebKit + +protocol SessionRestoreHelperDelegate: class { + func sessionRestoreHelper(_ helper: SessionRestoreHelper, didRestoreSessionForTab tab: Tab) +} + +class SessionRestoreHelper: TabContentScript { + weak var delegate: SessionRestoreHelperDelegate? + fileprivate weak var tab: Tab? + + required init(tab: Tab) { + self.tab = tab + } + + func scriptMessageHandlerName() -> String? { + return "sessionRestoreHelper" + } + + func userContentController(_ userContentController: WKUserContentController, didReceiveScriptMessage message: WKScriptMessage) { + if let tab = tab, let params = message.body as? [String: AnyObject] { + if params["name"] as! String == "didRestoreSession" { + DispatchQueue.main.async { + self.delegate?.sessionRestoreHelper(self, didRestoreSessionForTab: tab) + } + } + } + } + + class func name() -> String { + return "SessionRestoreHelper" + } +} diff --git a/mobile/ios/Client/Frontend/Browser/SimpleToast.swift b/mobile/ios/Client/Frontend/Browser/SimpleToast.swift new file mode 100644 index 0000000000..cec47002dc --- /dev/null +++ b/mobile/ios/Client/Frontend/Browser/SimpleToast.swift @@ -0,0 +1,73 @@ +/* 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 + +struct SimpleToastUX { + static let ToastHeight = BottomToolbarHeight + static let ToastAnimationDuration = 0.5 + static let ToastDefaultColor = UIColor(red: 10 / 255, green: 132 / 255, blue: 255.0 / 255, alpha: 1) + static let ToastFont = UIFont.systemFont(ofSize: 15) + static let ToastDismissAfter = DispatchTimeInterval.milliseconds(4500) // 4.5 seconds. + static let ToastDelayBefore = DispatchTimeInterval.milliseconds(0) // 0 seconds + static let BottomToolbarHeight = CGFloat(45) +} + +struct SimpleToast { + + func showAlertWithText(_ text: String, bottomContainer: UIView) { + let toast = self.createView() + toast.text = text + bottomContainer.addSubview(toast) + toast.snp.makeConstraints { (make) in + make.width.equalTo(bottomContainer) + make.left.equalTo(bottomContainer) + make.height.equalTo(SimpleToastUX.ToastHeight) + make.bottom.equalTo(bottomContainer) + } + animate(toast) + } + + fileprivate func createView() -> UILabel { + let toast = UILabel() + toast.textColor = UIColor.white + toast.backgroundColor = SimpleToastUX.ToastDefaultColor + toast.font = SimpleToastUX.ToastFont + toast.textAlignment = .center + return toast + } + + fileprivate func dismiss(_ toast: UIView) { + UIView.animate(withDuration: SimpleToastUX.ToastAnimationDuration, + animations: { + var frame = toast.frame + frame.origin.y = frame.origin.y + SimpleToastUX.ToastHeight + frame.size.height = 0 + toast.frame = frame + }, + completion: { finished in + toast.removeFromSuperview() + } + ) + } + + fileprivate func animate(_ toast: UIView) { + UIView.animate(withDuration: SimpleToastUX.ToastAnimationDuration, + animations: { + var frame = toast.frame + frame.origin.y = frame.origin.y - SimpleToastUX.ToastHeight + frame.size.height = SimpleToastUX.ToastHeight + toast.frame = frame + }, + completion: { finished in + let dispatchTime = DispatchTime.now() + SimpleToastUX.ToastDismissAfter + + DispatchQueue.main.asyncAfter(deadline: dispatchTime, execute: { + self.dismiss(toast) + }) + } + ) + } +} diff --git a/mobile/ios/Client/Frontend/Browser/SwipeAnimator.swift b/mobile/ios/Client/Frontend/Browser/SwipeAnimator.swift new file mode 100644 index 0000000000..fec61058ea --- /dev/null +++ b/mobile/ios/Client/Frontend/Browser/SwipeAnimator.swift @@ -0,0 +1,156 @@ +/* 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 + +struct SwipeAnimationParameters { + let totalRotationInDegrees: Double + let deleteThreshold: CGFloat + let totalScale: CGFloat + let totalAlpha: CGFloat + let minExitVelocity: CGFloat + let recenterAnimationDuration: TimeInterval +} + +private let DefaultParameters = + SwipeAnimationParameters( + totalRotationInDegrees: 10, + deleteThreshold: 80, + totalScale: 0.9, + totalAlpha: 0, + minExitVelocity: 800, + recenterAnimationDuration: 0.15) + +protocol SwipeAnimatorDelegate: class { + func swipeAnimator(_ animator: SwipeAnimator, viewWillExitContainerBounds: UIView) +} + +class SwipeAnimator: NSObject { + weak var delegate: SwipeAnimatorDelegate? + weak var animatingView: UIView? + + fileprivate var prevOffset: CGPoint? + fileprivate let params: SwipeAnimationParameters + + fileprivate var panGestureRecogniser: UIPanGestureRecognizer! + + var containerCenter: CGPoint { + guard let animatingView = self.animatingView else { + return CGPoint.zero + } + return CGPoint(x: animatingView.frame.width / 2, y: animatingView.frame.height / 2) + } + + init(animatingView: UIView, params: SwipeAnimationParameters = DefaultParameters) { + self.animatingView = animatingView + self.params = params + + super.init() + + self.panGestureRecogniser = UIPanGestureRecognizer(target: self, action: #selector(SwipeAnimator.didPan(_:))) + animatingView.addGestureRecognizer(self.panGestureRecogniser) + self.panGestureRecogniser.delegate = self + } + + func cancelExistingGestures() { + self.panGestureRecogniser.isEnabled = false + self.panGestureRecogniser.isEnabled = true + } +} + +//MARK: Private Helpers +extension SwipeAnimator { + fileprivate func animateBackToCenter() { + UIView.animate(withDuration: params.recenterAnimationDuration, animations: { + self.animatingView?.transform = CGAffineTransform.identity + self.animatingView?.alpha = 1 + }) + } + + fileprivate func animateAwayWithVelocity(_ velocity: CGPoint, speed: CGFloat) { + guard let animatingView = self.animatingView else { + return + } + + // Calculate the edge to calculate distance from + let translation = velocity.x >= 0 ? animatingView.frame.width : -animatingView.frame.width + let timeStep = TimeInterval(abs(translation) / speed) + self.delegate?.swipeAnimator(self, viewWillExitContainerBounds: animatingView) + UIView.animate(withDuration: timeStep, animations: { + animatingView.transform = self.transformForTranslation(translation) + animatingView.alpha = self.alphaForDistanceFromCenter(abs(translation)) + }, completion: { finished in + if finished { + animatingView.alpha = 0 + } + }) + } + + fileprivate func transformForTranslation(_ translation: CGFloat) -> CGAffineTransform { + let swipeWidth = animatingView?.frame.size.width ?? 1 + let totalRotationInRadians = CGFloat(params.totalRotationInDegrees / 180.0 * Double.pi) + + // Determine rotation / scaling amounts by the distance to the edge + let rotation = (translation / swipeWidth) * totalRotationInRadians + let scale = 1 - (abs(translation) / swipeWidth) * (1 - params.totalScale) + + let rotationTransform = CGAffineTransform(rotationAngle: rotation) + let scaleTransform = CGAffineTransform(scaleX: scale, y: scale) + let translateTransform = CGAffineTransform(translationX: translation, y: 0) + return rotationTransform.concatenating(scaleTransform).concatenating(translateTransform) + } + + fileprivate func alphaForDistanceFromCenter(_ distance: CGFloat) -> CGFloat { + let swipeWidth = animatingView?.frame.size.width ?? 1 + return 1 - (distance / swipeWidth) * (1 - params.totalAlpha) + } +} + +//MARK: Selectors +extension SwipeAnimator { + @objc func didPan(_ recognizer: UIPanGestureRecognizer!) { + let translation = recognizer.translation(in: animatingView) + + switch recognizer.state { + case .began: + prevOffset = containerCenter + case .changed: + animatingView?.transform = transformForTranslation(translation.x) + animatingView?.alpha = alphaForDistanceFromCenter(abs(translation.x)) + prevOffset = CGPoint(x: translation.x, y: 0) + case .cancelled: + animateBackToCenter() + case .ended: + let velocity = recognizer.velocity(in: animatingView) + // Bounce back if the velocity is too low or if we have not reached the threshold yet + let speed = max(abs(velocity.x), params.minExitVelocity) + if speed < params.minExitVelocity || abs(prevOffset?.x ?? 0) < params.deleteThreshold { + animateBackToCenter() + } else { + animateAwayWithVelocity(velocity, speed: speed) + } + default: + break + } + } + + func close(right: Bool) { + let direction = CGFloat(right ? -1 : 1) + animateAwayWithVelocity(CGPoint(x: -direction * params.minExitVelocity, y: 0), speed: direction * params.minExitVelocity) + } + + @discardableResult @objc func closeWithoutGesture() -> Bool { + close(right: false) + return true + } +} + +extension SwipeAnimator: UIGestureRecognizerDelegate { + @objc func gestureRecognizerShouldBegin(_ recognizer: UIGestureRecognizer) -> Bool { + let cellView = recognizer.view as UIView! + let panGesture = recognizer as! UIPanGestureRecognizer + let translation = panGesture.translation(in: cellView?.superview) + return fabs(translation.x) > fabs(translation.y) + } +} diff --git a/mobile/ios/Client/Frontend/Browser/Tab.swift b/mobile/ios/Client/Frontend/Browser/Tab.swift new file mode 100644 index 0000000000..1ddf5616cf --- /dev/null +++ b/mobile/ios/Client/Frontend/Browser/Tab.swift @@ -0,0 +1,598 @@ +/* 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 WebKit +import Storage +import Shared +import SwiftyJSON +import XCGLogger + +protocol TabContentScript { + static func name() -> String + func scriptMessageHandlerName() -> String? + func userContentController(_ userContentController: WKUserContentController, didReceiveScriptMessage message: WKScriptMessage) +} + +@objc +protocol TabDelegate { + func tab(_ tab: Tab, didAddSnackbar bar: SnackBar) + func tab(_ tab: Tab, didRemoveSnackbar bar: SnackBar) + func tab(_ tab: Tab, didSelectFindInPageForSelection selection: String) + @objc optional func tab(_ tab: Tab, didCreateWebView webView: WKWebView) + @objc optional func tab(_ tab: Tab, willDeleteWebView webView: WKWebView) +} + +@objc +protocol URLChangeDelegate { + func tab(_ tab: Tab, urlDidChangeTo url: URL) +} + +struct TabState { + var isPrivate: Bool = false + var desktopSite: Bool = false + var isBookmarked: Bool = false + var url: URL? + var title: String? + var favicon: Favicon? +} + +class Tab: NSObject { + fileprivate var _isPrivate: Bool = false + internal fileprivate(set) var isPrivate: Bool { + get { + return _isPrivate + } + set { + if _isPrivate != newValue { + _isPrivate = newValue + } + } + } + + var tabState: TabState { + return TabState(isPrivate: _isPrivate, desktopSite: desktopSite, isBookmarked: isBookmarked, url: url, title: displayTitle, favicon: displayFavicon) + } + + // PageMetadata is derived from the page content itself, and as such lags behind the + // rest of the tab. + var pageMetadata: PageMetadata? + + var canonicalURL: URL? { + if let string = pageMetadata?.siteURL, + let siteURL = URL(string: string) { + return siteURL + } + return self.url + } + + var userActivity: NSUserActivity? = nil + + var webView: WKWebView? + var tabDelegate: TabDelegate? + weak var urlDidChangeDelegate: URLChangeDelegate? // TODO: generalize this. + var bars = [SnackBar]() + var favicons = [Favicon]() + var lastExecutedTime: Timestamp? + var sessionData: SessionData? + fileprivate var lastRequest: URLRequest? + var restoring: Bool = false + var pendingScreenshot = false + var url: URL? + var mimeType: String? + + fileprivate var _noImageMode = false + + /// Returns true if this tab's URL is known, and it's longer than we want to store. + var urlIsTooLong: Bool { + guard let url = self.url else { + return false + } + return url.absoluteString.lengthOfBytes(using: String.Encoding.utf8) > AppConstants.DB_URL_LENGTH_MAX + } + + // Use computed property so @available can be used to guard `noImageMode`. + @available(iOS 11, *) + var noImageMode: Bool { + get { return _noImageMode } + set { + if newValue == _noImageMode { + return + } + _noImageMode = newValue + let helper = (contentBlocker as? ContentBlockerHelper) + helper?.noImageMode(enabled: _noImageMode) + } + } + + // There is no 'available macro' on props, we currently just need to store ownership. + var contentBlocker: AnyObject? + + /// The last title shown by this tab. Used by the tab tray to show titles for zombie tabs. + var lastTitle: String? + + /// Whether or not the desktop site was requested with the last request, reload or navigation. Note that this property needs to + /// be managed by the web view's navigation delegate. + var desktopSite: Bool = false + var isBookmarked: Bool = false + + var readerModeAvailableOrActive: Bool { + if let readerMode = self.getContentScript(name: "ReaderMode") as? ReaderMode { + return readerMode.state != .unavailable + } + return false + } + + fileprivate(set) var screenshot: UIImage? + var screenshotUUID: UUID? + + // If this tab has been opened from another, its parent will point to the tab from which it was opened + var parent: Tab? + + fileprivate var contentScriptManager = TabContentScriptManager() + + fileprivate var configuration: WKWebViewConfiguration? + + /// Any time a tab tries to make requests to display a Javascript Alert and we are not the active + /// tab instance, queue it for later until we become foregrounded. + fileprivate var alertQueue = [JSAlertInfo]() + + init(configuration: WKWebViewConfiguration, isPrivate: Bool = false) { + self.configuration = configuration + super.init() + self.isPrivate = isPrivate + + if #available(iOS 11, *) { + if let appDelegate = UIApplication.shared.delegate as? AppDelegate, let profile = appDelegate.profile { + contentBlocker = ContentBlockerHelper(tab: self, profile: profile) + } + } + } + + class func toTab(_ tab: Tab) -> RemoteTab? { + if let displayURL = tab.url?.displayURL, RemoteTab.shouldIncludeURL(displayURL) { + let history = Array(tab.historyList.filter(RemoteTab.shouldIncludeURL).reversed()) + return RemoteTab(clientGUID: nil, + URL: displayURL, + title: tab.displayTitle, + history: history, + lastUsed: Date.now(), + icon: nil) + } else if let sessionData = tab.sessionData, !sessionData.urls.isEmpty { + let history = Array(sessionData.urls.filter(RemoteTab.shouldIncludeURL).reversed()) + if let displayURL = history.first { + return RemoteTab(clientGUID: nil, + URL: displayURL, + title: tab.displayTitle, + history: history, + lastUsed: sessionData.lastUsedTime, + icon: nil) + } + } + + return nil + } + + weak var navigationDelegate: WKNavigationDelegate? { + didSet { + if let webView = webView { + webView.navigationDelegate = navigationDelegate + } + } + } + + func createWebview() { + if webView == nil { + assert(configuration != nil, "Create webview can only be called once") + configuration!.userContentController = WKUserContentController() + configuration!.preferences = WKPreferences() + configuration!.preferences.javaScriptCanOpenWindowsAutomatically = false + configuration!.allowsInlineMediaPlayback = true + let webView = TabWebView(frame: CGRect.zero, configuration: configuration!) + webView.delegate = self + configuration = nil + + webView.accessibilityLabel = NSLocalizedString("Web content", comment: "Accessibility label for the main web content view") + webView.allowsBackForwardNavigationGestures = true + webView.allowsLinkPreview = false + + // Night mode enables this by toggling WKWebView.isOpaque, otherwise this has no effect. + webView.backgroundColor = .black + + // Turning off masking allows the web content to flow outside of the scrollView's frame + // which allows the content appear beneath the toolbars in the BrowserViewController + webView.scrollView.layer.masksToBounds = false + webView.navigationDelegate = navigationDelegate + + restore(webView) + + self.webView = webView + self.webView?.addObserver(self, forKeyPath: KVOConstants.URL.rawValue, options: .new, context: nil) + tabDelegate?.tab?(self, didCreateWebView: webView) + } + } + + func restore(_ webView: WKWebView) { + // Pulls restored session data from a previous SavedTab to load into the Tab. If it's nil, a session restore + // has already been triggered via custom URL, so we use the last request to trigger it again; otherwise, + // we extract the information needed to restore the tabs and create a NSURLRequest with the custom session restore URL + // to trigger the session restore via custom handlers + if let sessionData = self.sessionData { + restoring = true + + var urls = [String]() + for url in sessionData.urls { + urls.append(url.absoluteString) + } + + let currentPage = sessionData.currentPage + self.sessionData = nil + var jsonDict = [String: AnyObject]() + jsonDict["history"] = urls as AnyObject? + jsonDict["currentPage"] = currentPage as AnyObject? + guard let json = JSON(jsonDict).stringValue() else { + return + } + let escapedJSON = json.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)! + let restoreURL = URL(string: "\(WebServer.sharedInstance.base)/about/sessionrestore?history=\(escapedJSON)") + lastRequest = PrivilegedRequest(url: restoreURL!) as URLRequest + webView.load(lastRequest!) + } else if let request = lastRequest { + webView.load(request) + } else { + print("creating webview with no lastRequest and no session data: \(self.url?.description ?? "nil")") + } + } + + deinit { + if let webView = webView { + webView.removeObserver(self, forKeyPath: KVOConstants.URL.rawValue) + tabDelegate?.tab?(self, willDeleteWebView: webView) + } + } + + var loading: Bool { + return webView?.isLoading ?? false + } + + var estimatedProgress: Double { + return webView?.estimatedProgress ?? 0 + } + + var backList: [WKBackForwardListItem]? { + return webView?.backForwardList.backList + } + + var forwardList: [WKBackForwardListItem]? { + return webView?.backForwardList.forwardList + } + + var historyList: [URL] { + func listToUrl(_ item: WKBackForwardListItem) -> URL { return item.url } + var tabs = self.backList?.map(listToUrl) ?? [URL]() + tabs.append(self.url!) + return tabs + } + + var title: String? { + return webView?.title + } + + var displayTitle: String { + if let title = webView?.title { + if !title.isEmpty { + return title + } + } + + // When picking a display title. Tabs with sessionData are pending a restore so show their old title. + // To prevent flickering of the display title. If a tab is restoring make sure to use its lastTitle. + if let url = self.url, url.isAboutHomeURL, sessionData == nil, !restoring { + return "" + } + + guard let lastTitle = lastTitle, !lastTitle.isEmpty else { + return self.url?.displayURL?.absoluteString ?? "" + } + + return lastTitle + } + + var currentInitialURL: URL? { + get { + let initalURL = self.webView?.backForwardList.currentItem?.initialURL + return initalURL + } + } + + var displayFavicon: Favicon? { + var width = 0 + var largest: Favicon? + for icon in favicons where icon.width! > width { + width = icon.width! + largest = icon + } + return largest + } + + var canGoBack: Bool { + return webView?.canGoBack ?? false + } + + var canGoForward: Bool { + return webView?.canGoForward ?? false + } + + func goBack() { + _ = webView?.goBack() + } + + func goForward() { + _ = webView?.goForward() + } + + func goToBackForwardListItem(_ item: WKBackForwardListItem) { + _ = webView?.go(to: item) + } + + @discardableResult func loadRequest(_ request: URLRequest) -> WKNavigation? { + if let webView = webView { + lastRequest = request + return webView.load(request) + } + return nil + } + + func stop() { + webView?.stopLoading() + } + + func reload() { + let userAgent: String? = desktopSite ? UserAgent.desktopUserAgent() : nil + if (userAgent ?? "") != webView?.customUserAgent, + let currentItem = webView?.backForwardList.currentItem { + webView?.customUserAgent = userAgent + + // Reload the initial URL to avoid UA specific redirection + loadRequest(PrivilegedRequest(url: currentItem.initialURL, cachePolicy: .reloadIgnoringLocalCacheData, timeoutInterval: 60) as URLRequest) + return + } + + if let _ = webView?.reloadFromOrigin() { + print("reloaded zombified tab from origin") + return + } + + if let webView = self.webView { + print("restoring webView from scratch") + restore(webView) + } + } + + func addContentScript(_ helper: TabContentScript, name: String) { + contentScriptManager.addContentScript(helper, name: name, forTab: self) + } + + func getContentScript(name: String) -> TabContentScript? { + return contentScriptManager.getContentScript(name) + } + + func hideContent(_ animated: Bool = false) { + webView?.isUserInteractionEnabled = false + if animated { + UIView.animate(withDuration: 0.25, animations: { () -> Void in + self.webView?.alpha = 0.0 + }) + } else { + webView?.alpha = 0.0 + } + } + + func showContent(_ animated: Bool = false) { + webView?.isUserInteractionEnabled = true + if animated { + UIView.animate(withDuration: 0.25, animations: { () -> Void in + self.webView?.alpha = 1.0 + }) + } else { + webView?.alpha = 1.0 + } + } + + func addSnackbar(_ bar: SnackBar) { + bars.append(bar) + tabDelegate?.tab(self, didAddSnackbar: bar) + } + + func removeSnackbar(_ bar: SnackBar) { + if let index = bars.index(of: bar) { + bars.remove(at: index) + tabDelegate?.tab(self, didRemoveSnackbar: bar) + } + } + + func removeAllSnackbars() { + // Enumerate backwards here because we'll remove items from the list as we go. + for i in (0.. JSAlertInfo? { + guard !alertQueue.isEmpty else { + return nil + } + return alertQueue.removeFirst() + } + + func cancelQueuedAlerts() { + alertQueue.forEach { alert in + alert.cancel() + } + } + + override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey: Any]?, context: UnsafeMutableRawPointer?) { + guard let webView = object as? WKWebView, webView == self.webView, + let path = keyPath, path == KVOConstants.URL.rawValue else { + return assertionFailure("Unhandled KVO key: \(keyPath ?? "nil")") + } + guard let url = self.webView?.url else { + return + } + + self.urlDidChangeDelegate?.tab(self, urlDidChangeTo: url) + TabEvent.post(.didChangeURL(url), for: self) + } + + func isDescendentOf(_ ancestor: Tab) -> Bool { + var tab = parent + while tab != nil { + if tab! == ancestor { + return true + } + tab = tab?.parent + } + return false + } + + func setNightMode(_ enabled: Bool) { + webView?.evaluateJavaScript("window.__firefox__.NightMode.setEnabled(\(enabled))", completionHandler: nil) + // For WKWebView background color to take effect, isOpaque must be false, which is counter-intuitive. Default is true. + // The color is previously set to black in the webview init + webView?.isOpaque = !enabled + } + + func injectUserScriptWith(fileName: String, type: String = "js", injectionTime: WKUserScriptInjectionTime = .atDocumentEnd, mainFrameOnly: Bool = true) { + guard let webView = self.webView else { + return + } + if let path = Bundle.main.path(forResource: fileName, ofType: type), + let source = try? String(contentsOfFile: path) { + let userScript = WKUserScript(source: source, injectionTime: injectionTime, forMainFrameOnly: mainFrameOnly) + webView.configuration.userContentController.addUserScript(userScript) + } + } + + func observeURLChanges(delegate: URLChangeDelegate) { + self.urlDidChangeDelegate = delegate + } + + func removeURLChangeObserver(delegate: URLChangeDelegate) { + if let existing = self.urlDidChangeDelegate, existing === delegate { + self.urlDidChangeDelegate = nil + } + } +} + +extension Tab: TabWebViewDelegate { + fileprivate func tabWebView(_ tabWebView: TabWebView, didSelectFindInPageForSelection selection: String) { + tabDelegate?.tab(self, didSelectFindInPageForSelection: selection) + } +} + +private class TabContentScriptManager: NSObject, WKScriptMessageHandler { + fileprivate var helpers = [String: TabContentScript]() + + @objc func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) { + for helper in helpers.values { + if let scriptMessageHandlerName = helper.scriptMessageHandlerName() { + if scriptMessageHandlerName == message.name { + helper.userContentController(userContentController, didReceiveScriptMessage: message) + return + } + } + } + } + + func addContentScript(_ helper: TabContentScript, name: String, forTab tab: Tab) { + if let _ = helpers[name] { + assertionFailure("Duplicate helper added: \(name)") + } + + helpers[name] = helper + + // If this helper handles script messages, then get the handler name and register it. The Browser + // receives all messages and then dispatches them to the right TabHelper. + if let scriptMessageHandlerName = helper.scriptMessageHandlerName() { + tab.webView?.configuration.userContentController.add(self, name: scriptMessageHandlerName) + } + } + + func getContentScript(_ name: String) -> TabContentScript? { + return helpers[name] + } +} + +private protocol TabWebViewDelegate: class { + func tabWebView(_ tabWebView: TabWebView, didSelectFindInPageForSelection selection: String) +} + +private class TabWebView: WKWebView, MenuHelperInterface { + fileprivate weak var delegate: TabWebViewDelegate? + + override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool { + return super.canPerformAction(action, withSender: sender) || action == MenuHelper.SelectorFindInPage + } + + @objc func menuHelperFindInPage() { + evaluateJavaScript("getSelection().toString()") { result, _ in + let selection = result as? String ?? "" + self.delegate?.tabWebView(self, didSelectFindInPageForSelection: selection) + } + } + + fileprivate override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + // The find-in-page selection menu only appears if the webview is the first responder. + becomeFirstResponder() + + return super.hitTest(point, with: event) + } +} + +/// +// Temporary fix for Bug 1390871 - NSInvalidArgumentException: -[WKContentView menuHelperFindInPage]: unrecognized selector +// +// This class only exists to contain the swizzledMenuHelperFindInPage. This class is actually never +// instantiated. It only serves as a placeholder for the method. When the method is called, self is +// actually pointing to a WKContentView. Which is not public, but that is fine, we only need to know +// that it is a UIView subclass to access its superview. +// + +class TabWebViewMenuHelper: UIView { + @objc func swizzledMenuHelperFindInPage() { + if let tabWebView = superview?.superview as? TabWebView { + tabWebView.evaluateJavaScript("getSelection().toString()") { result, _ in + let selection = result as? String ?? "" + tabWebView.delegate?.tabWebView(tabWebView, didSelectFindInPageForSelection: selection) + } + } + } +} diff --git a/mobile/ios/Client/Frontend/Browser/TabLocationView.swift b/mobile/ios/Client/Frontend/Browser/TabLocationView.swift new file mode 100644 index 0000000000..c426a7a1c5 --- /dev/null +++ b/mobile/ios/Client/Frontend/Browser/TabLocationView.swift @@ -0,0 +1,399 @@ +/* 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 UIKit +import Shared +import SnapKit +import XCGLogger + +private let log = Logger.browserLogger + +protocol TabLocationViewDelegate { + func tabLocationViewDidTapLocation(_ tabLocationView: TabLocationView) + func tabLocationViewDidLongPressLocation(_ tabLocationView: TabLocationView) + func tabLocationViewDidTapReaderMode(_ tabLocationView: TabLocationView) + func tabLocationViewDidTapPageOptions(_ tabLocationView: TabLocationView, from button: UIButton) + func tabLocationViewDidLongPressPageOptions(_ tabLocationVIew: TabLocationView) + + /// - returns: whether the long-press was handled by the delegate; i.e. return `false` when the conditions for even starting handling long-press were not satisfied + @discardableResult func tabLocationViewDidLongPressReaderMode(_ tabLocationView: TabLocationView) -> Bool + func tabLocationViewLocationAccessibilityActions(_ tabLocationView: TabLocationView) -> [UIAccessibilityCustomAction]? +} + +struct TabLocationViewUX { + static let HostFontColor = UIColor.black + static let BaseURLFontColor = UIColor.gray + static let LocationContentInset = 8 + static let URLBarPadding = 4 + + static let Themes: [String: Theme] = { + var themes = [String: Theme]() + var theme = Theme() + theme.URLFontColor = UIColor.lightGray + theme.textColor = UIColor(rgb: 0xf9f9fa) + theme.highlightButtonColor = UIConstants.PrivateModePurple + theme.buttonTintColor = UIColor(rgb: 0xADADb0) + theme.backgroundColor = UIColor(rgb: 0x636369) + themes[Theme.PrivateMode] = theme + + theme = Theme() + theme.textColor = UIColor(rgb: 0x27) + theme.highlightButtonColor = UIColor(rgb: 0x00A2FE) + theme.buttonTintColor = UIColor(rgb: 0x737373) + theme.backgroundColor = .white + themes[Theme.NormalMode] = theme + + return themes + }() +} + +class TabLocationView: UIView { + var delegate: TabLocationViewDelegate? + var longPressRecognizer: UILongPressGestureRecognizer! + var tapRecognizer: UITapGestureRecognizer! + + dynamic var baseURLFontColor: UIColor = TabLocationViewUX.BaseURLFontColor { + didSet { updateTextWithURL() } + } + + var url: URL? { + didSet { + let wasHidden = lockImageView.isHidden + lockImageView.isHidden = url?.scheme != "https" + if wasHidden != lockImageView.isHidden { + UIAccessibilityPostNotification(UIAccessibilityLayoutChangedNotification, nil) + } + updateTextWithURL() + pageOptionsButton.isHidden = (url == nil) + setNeedsUpdateConstraints() + } + } + + var readerModeState: ReaderModeState { + get { + return readerModeButton.readerModeState + } + set (newReaderModeState) { + if newReaderModeState != self.readerModeButton.readerModeState { + let wasHidden = readerModeButton.isHidden + self.readerModeButton.readerModeState = newReaderModeState + readerModeButton.isHidden = (newReaderModeState == ReaderModeState.unavailable) + separatorLine.isHidden = readerModeButton.isHidden + if wasHidden != readerModeButton.isHidden { + UIAccessibilityPostNotification(UIAccessibilityLayoutChangedNotification, nil) + if !readerModeButton.isHidden { + // Delay the Reader Mode accessibility announcement briefly to prevent interruptions. + DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(2)) { + UIAccessibilityPostNotification(UIAccessibilityAnnouncementNotification, Strings.ReaderModeAvailableVoiceOverAnnouncement) + } + } + } + UIView.animate(withDuration: 0.1, animations: { () -> Void in + if newReaderModeState == ReaderModeState.unavailable { + self.readerModeButton.alpha = 0.0 + } else { + self.readerModeButton.alpha = 1.0 + } + self.setNeedsUpdateConstraints() + self.layoutIfNeeded() + }) + } + } + } + + lazy var placeholder: NSAttributedString = { + let placeholderText = NSLocalizedString("Search or enter address", comment: "The text shown in the URL bar on about:home") + return NSAttributedString(string: placeholderText, attributes: [NSForegroundColorAttributeName: UIColor.gray]) + }() + + lazy var urlTextField: UITextField = { + let urlTextField = DisplayTextField() + + self.longPressRecognizer.delegate = self + urlTextField.addGestureRecognizer(self.longPressRecognizer) + self.tapRecognizer.delegate = self + urlTextField.addGestureRecognizer(self.tapRecognizer) + + // Prevent the field from compressing the toolbar buttons on the 4S in landscape. + urlTextField.setContentCompressionResistancePriority(250, for: UILayoutConstraintAxis.horizontal) + urlTextField.attributedPlaceholder = self.placeholder + urlTextField.accessibilityIdentifier = "url" + urlTextField.accessibilityActionsSource = self + urlTextField.font = UIConstants.DefaultChromeFont + urlTextField.backgroundColor = .clear + return urlTextField + }() + + fileprivate lazy var lockImageView: UIImageView = { + let lockImageView = UIImageView(image: UIImage.templateImageNamed("lock_verified")) + lockImageView.isHidden = true + lockImageView.tintColor = UIColor(rgb: 0x16DA00) + lockImageView.isAccessibilityElement = true + lockImageView.contentMode = UIViewContentMode.center + lockImageView.accessibilityLabel = NSLocalizedString("Secure connection", comment: "Accessibility label for the lock icon, which is only present if the connection is secure") + return lockImageView + }() + + fileprivate lazy var readerModeButton: ReaderModeButton = { + let readerModeButton = ReaderModeButton(frame: CGRect.zero) + readerModeButton.isHidden = true + readerModeButton.addTarget(self, action: #selector(TabLocationView.SELtapReaderModeButton), for: .touchUpInside) + readerModeButton.addGestureRecognizer(UILongPressGestureRecognizer(target: self, action: #selector(TabLocationView.SELlongPressReaderModeButton(_:)))) + readerModeButton.isAccessibilityElement = true + readerModeButton.imageView?.contentMode = UIViewContentMode.scaleAspectFit + readerModeButton.accessibilityLabel = NSLocalizedString("Reader View", comment: "Accessibility label for the Reader View button") + readerModeButton.accessibilityIdentifier = "TabLocationView.readerModeButton" + readerModeButton.accessibilityCustomActions = [UIAccessibilityCustomAction(name: NSLocalizedString("Add to Reading List", comment: "Accessibility label for action adding current page to reading list."), target: self, selector: #selector(TabLocationView.SELreaderModeCustomAction))] + return readerModeButton + }() + + lazy var pageOptionsButton: ToolbarButton = { + let pageOptionsButton = ToolbarButton(frame: CGRect.zero) + pageOptionsButton.setImage(UIImage.templateImageNamed("menu-More-Options"), for: .normal) + pageOptionsButton.isHidden = true + pageOptionsButton.addTarget(self, action: #selector(TabLocationView.SELDidPressPageOptionsButton), for: .touchUpInside) + pageOptionsButton.isAccessibilityElement = true + pageOptionsButton.imageView?.contentMode = .center + pageOptionsButton.accessibilityLabel = NSLocalizedString("Page Options Menu", comment: "Accessibility label for the Page Options menu button") + pageOptionsButton.accessibilityIdentifier = "TabLocationView.pageOptionsButton" + let longPressGesture = UILongPressGestureRecognizer(target: self, action: #selector(TabLocationView.SELDidLongPressPageOptionsButton)) + pageOptionsButton.addGestureRecognizer(longPressGesture) + return pageOptionsButton + }() + + lazy var separatorLine: UIView = { + let line = UIView() + line.layer.cornerRadius = 2 + line.backgroundColor = UIColor(rgb: 0xE5E5E5) + line.isHidden = true + return line + }() + + override init(frame: CGRect) { + super.init(frame: frame) + + longPressRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(TabLocationView.SELlongPressLocation(_:))) + tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(TabLocationView.SELtapLocation(_:))) + + addSubview(urlTextField) + addSubview(lockImageView) + addSubview(readerModeButton) + addSubview(pageOptionsButton) + addSubview(separatorLine) + + lockImageView.snp.makeConstraints { make in + make.size.equalTo(24) + make.centerY.equalTo(self) + make.leading.equalTo(self).offset(9) + } + + pageOptionsButton.snp.makeConstraints { make in + make.centerY.equalTo(self) + make.trailing.equalTo(self) + make.width.equalTo(44) + make.height.equalTo(self) + } + + separatorLine.snp.makeConstraints { make in + make.width.equalTo(1) + make.height.equalTo(26) + make.trailing.equalTo(pageOptionsButton.snp.leading) + make.centerY.equalTo(self) + } + + readerModeButton.snp.makeConstraints { make in + make.centerY.equalTo(self) + make.trailing.equalTo(separatorLine.snp.leading).offset(-9) + make.size.equalTo(24) + } + } + + override var accessibilityElements: [Any]? { + get { + return [lockImageView, urlTextField, readerModeButton, pageOptionsButton].filter { !$0.isHidden } + } + set { + super.accessibilityElements = newValue + } + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func updateConstraints() { + urlTextField.snp.remakeConstraints { make in + make.top.bottom.equalTo(self) + + if lockImageView.isHidden { + make.leading.equalTo(self).offset(TabLocationViewUX.LocationContentInset) + } else { + make.leading.equalTo(self.lockImageView.snp.trailing).offset(TabLocationViewUX.URLBarPadding) + } + + if readerModeButton.isHidden { + make.trailing.equalTo(self.pageOptionsButton.snp.leading).offset(-TabLocationViewUX.URLBarPadding) + } else { + make.trailing.equalTo(self.readerModeButton.snp.leading).offset(-TabLocationViewUX.URLBarPadding) + } + } + + super.updateConstraints() + } + + func SELtapReaderModeButton() { + delegate?.tabLocationViewDidTapReaderMode(self) + } + + func SELlongPressReaderModeButton(_ recognizer: UILongPressGestureRecognizer) { + if recognizer.state == UIGestureRecognizerState.began { + delegate?.tabLocationViewDidLongPressReaderMode(self) + } + } + + func SELDidPressPageOptionsButton(_ button: UIButton) { + delegate?.tabLocationViewDidTapPageOptions(self, from: button) + } + + func SELDidLongPressPageOptionsButton(_ recognizer: UILongPressGestureRecognizer) { + delegate?.tabLocationViewDidLongPressPageOptions(self) + } + + func SELlongPressLocation(_ recognizer: UITapGestureRecognizer) { + if recognizer.state == UIGestureRecognizerState.began { + delegate?.tabLocationViewDidLongPressLocation(self) + } + } + + func SELtapLocation(_ recognizer: UITapGestureRecognizer) { + delegate?.tabLocationViewDidTapLocation(self) + } + + func SELreaderModeCustomAction() -> Bool { + return delegate?.tabLocationViewDidLongPressReaderMode(self) ?? false + } + + fileprivate func updateTextWithURL() { + if let host = url?.host, AppConstants.MOZ_PUNYCODE { + urlTextField.text = url?.absoluteString.replacingOccurrences(of: host, with: host.asciiHostToUTF8()) + } else { + urlTextField.text = url?.absoluteString + } + // remove https:// (the scheme) from the url when displaying + if let scheme = url?.scheme, let range = url?.absoluteString.range(of: "\(scheme)://") { + urlTextField.text = url?.absoluteString.replacingCharacters(in: range, with: "") + } + } +} + +extension TabLocationView: UIGestureRecognizerDelegate { + func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { + return true + } + + func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldBeRequiredToFailBy otherGestureRecognizer: UIGestureRecognizer) -> Bool { + // If the longPressRecognizer is active, fail all other recognizers to avoid conflicts. + return gestureRecognizer == longPressRecognizer + } +} + +extension TabLocationView: AccessibilityActionsSource { + func accessibilityCustomActionsForView(_ view: UIView) -> [UIAccessibilityCustomAction]? { + if view === urlTextField { + return delegate?.tabLocationViewLocationAccessibilityActions(self) + } + return nil + } +} + +extension TabLocationView: Themeable { + func applyTheme(_ themeName: String) { + guard let theme = TabLocationViewUX.Themes[themeName] else { + log.error("Unable to apply unknown theme \(themeName)") + return + } + let isPrivate = themeName == Theme.PrivateMode + backgroundColor = theme.backgroundColor + urlTextField.textColor = theme.textColor + readerModeButton.selectedTintColor = theme.highlightButtonColor + readerModeButton.unselectedTintColor = theme.buttonTintColor + pageOptionsButton.selectedTintColor = theme.highlightButtonColor + + pageOptionsButton.unselectedTintColor = isPrivate ? UIColor(rgb: 0xD2d2d4) : UIColor(rgb: 0x272727) + pageOptionsButton.tintColor = pageOptionsButton.unselectedTintColor + separatorLine.backgroundColor = isPrivate ? UIColor(rgb: 0x3f3f43) : UIColor(rgb: 0xE5E5E5) + } +} + +class ReaderModeButton: UIButton { + var selectedTintColor: UIColor? + var unselectedTintColor: UIColor? + override init(frame: CGRect) { + super.init(frame: frame) + adjustsImageWhenHighlighted = false + setImage(UIImage.templateImageNamed("reader"), for: .normal) + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override var isSelected: Bool { + didSet { + self.tintColor = (isHighlighted || isSelected) ? selectedTintColor : unselectedTintColor + } + } + + override open var isHighlighted: Bool { + didSet { + self.tintColor = (isHighlighted || isSelected) ? selectedTintColor : unselectedTintColor + } + } + + override var tintColor: UIColor! { + didSet { + self.imageView?.tintColor = self.tintColor + } + } + + var _readerModeState: ReaderModeState = ReaderModeState.unavailable + + var readerModeState: ReaderModeState { + get { + return _readerModeState + } + set (newReaderModeState) { + _readerModeState = newReaderModeState + switch _readerModeState { + case .available: + self.isEnabled = true + self.isSelected = false + case .unavailable: + self.isEnabled = false + self.isSelected = false + case .active: + self.isEnabled = true + self.isSelected = true + } + } + } +} + +private class DisplayTextField: UITextField { + weak var accessibilityActionsSource: AccessibilityActionsSource? + + override var accessibilityCustomActions: [UIAccessibilityCustomAction]? { + get { + return accessibilityActionsSource?.accessibilityCustomActionsForView(self) + } + set { + super.accessibilityCustomActions = newValue + } + } + + fileprivate override var canBecomeFirstResponder: Bool { + return false + } +} diff --git a/mobile/ios/Client/Frontend/Browser/TabManager.swift b/mobile/ios/Client/Frontend/Browser/TabManager.swift new file mode 100644 index 0000000000..643bfbf5e1 --- /dev/null +++ b/mobile/ios/Client/Frontend/Browser/TabManager.swift @@ -0,0 +1,988 @@ +/* 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 WebKit +import Storage +import Shared + +protocol TabManagerDelegate: class { + func tabManager(_ tabManager: TabManager, didSelectedTabChange selected: Tab?, previous: Tab?) + func tabManager(_ tabManager: TabManager, willAddTab tab: Tab) + func tabManager(_ tabManager: TabManager, didAddTab tab: Tab) + func tabManager(_ tabManager: TabManager, willRemoveTab tab: Tab) + func tabManager(_ tabManager: TabManager, didRemoveTab tab: Tab) + + func tabManagerDidRestoreTabs(_ tabManager: TabManager) + func tabManagerDidAddTabs(_ tabManager: TabManager) + func tabManagerDidRemoveAllTabs(_ tabManager: TabManager, toast: ButtonToast?) +} + +protocol TabManagerStateDelegate: class { + func tabManagerWillStoreTabs(_ tabs: [Tab]) +} + +// We can't use a WeakList here because this is a protocol. +class WeakTabManagerDelegate { + weak var value: TabManagerDelegate? + + init (value: TabManagerDelegate) { + self.value = value + } + + func get() -> TabManagerDelegate? { + return value + } +} + +// TabManager must extend NSObjectProtocol in order to implement WKNavigationDelegate +class TabManager: NSObject { + fileprivate var delegates = [WeakTabManagerDelegate]() + fileprivate var tabEventHandlers = TabEventHandlers.default.handlers + weak var stateDelegate: TabManagerStateDelegate? + + func addDelegate(_ delegate: TabManagerDelegate) { + assert(Thread.isMainThread) + delegates.append(WeakTabManagerDelegate(value: delegate)) + } + + func removeDelegate(_ delegate: TabManagerDelegate) { + assert(Thread.isMainThread) + for i in 0 ..< delegates.count { + let del = delegates[i] + if delegate === del.get() || del.get() == nil { + delegates.remove(at: i) + return + } + } + } + + fileprivate(set) var tabs = [Tab]() + fileprivate var _selectedIndex = -1 + fileprivate let navDelegate: TabManagerNavDelegate + fileprivate(set) var isRestoring = false + + // A WKWebViewConfiguration used for normal tabs + lazy fileprivate var configuration: WKWebViewConfiguration = { + let configuration = WKWebViewConfiguration() + configuration.processPool = WKProcessPool() + configuration.preferences.javaScriptCanOpenWindowsAutomatically = !(self.prefs.boolForKey("blockPopups") ?? true) + return configuration + }() + + // A WKWebViewConfiguration used for private mode tabs + lazy fileprivate var privateConfiguration: WKWebViewConfiguration = { + let configuration = WKWebViewConfiguration() + configuration.processPool = WKProcessPool() + configuration.preferences.javaScriptCanOpenWindowsAutomatically = !(self.prefs.boolForKey("blockPopups") ?? true) + configuration.websiteDataStore = WKWebsiteDataStore.nonPersistent() + return configuration + }() + + fileprivate let imageStore: DiskImageStore? + + fileprivate let prefs: Prefs + var selectedIndex: Int { return _selectedIndex } + var tempTabs: [Tab]? + + var normalTabs: [Tab] { + assert(Thread.isMainThread) + + return tabs.filter { !$0.isPrivate } + } + + var privateTabs: [Tab] { + assert(Thread.isMainThread) + return tabs.filter { $0.isPrivate } + } + + init(prefs: Prefs, imageStore: DiskImageStore?) { + assert(Thread.isMainThread) + + self.prefs = prefs + self.navDelegate = TabManagerNavDelegate() + self.imageStore = imageStore + super.init() + + addNavigationDelegate(self) + + NotificationCenter.default.addObserver(self, selector: #selector(TabManager.prefsDidChange), name: UserDefaults.didChangeNotification, object: nil) + } + + func addNavigationDelegate(_ delegate: WKNavigationDelegate) { + assert(Thread.isMainThread) + + self.navDelegate.insert(delegate) + } + + var count: Int { + assert(Thread.isMainThread) + + return tabs.count + } + + var selectedTab: Tab? { + assert(Thread.isMainThread) + if !(0.. Tab? { + assert(Thread.isMainThread) + + if index >= tabs.count { + return nil + } + return tabs[index] + } + + subscript(webView: WKWebView) -> Tab? { + assert(Thread.isMainThread) + + for tab in tabs where tab.webView === webView { + return tab + } + + return nil + } + + func getTabFor(_ url: URL) -> Tab? { + assert(Thread.isMainThread) + + for tab in tabs { + if tab.webView?.url == url { + return tab + } + + // Also look for tabs that haven't been restored yet. + if let sessionData = tab.sessionData, + 0.. Bool { + return prefs.boolForKey("settings.closePrivateTabs") ?? false + } + + //Called by other classes to signal that they are entering/exiting private mode + //This is called by TabTrayVC when the private mode button is pressed and BEFORE we've switched to the new mode + func willSwitchTabMode() { + if shouldClearPrivateTabs() && (selectedTab?.isPrivate ?? false) { + removeAllPrivateTabs() + } + } + + func expireSnackbars() { + assert(Thread.isMainThread) + + for tab in tabs { + tab.expireSnackbars() + } + } + + @discardableResult func addTab(_ request: URLRequest! = nil, configuration: WKWebViewConfiguration! = nil, afterTab: Tab? = nil, isPrivate: Bool) -> Tab { + return self.addTab(request, configuration: configuration, afterTab: afterTab, flushToDisk: true, zombie: false, isPrivate: isPrivate) + } + + func addTabAndSelect(_ request: URLRequest! = nil, configuration: WKWebViewConfiguration! = nil, afterTab: Tab? = nil, isPrivate: Bool) -> Tab { + let tab = addTab(request, configuration: configuration, afterTab: afterTab, isPrivate: isPrivate) + selectTab(tab) + return tab + } + + @discardableResult func addTabAndSelect(_ request: URLRequest! = nil, configuration: WKWebViewConfiguration! = nil, afterTab: Tab? = nil) -> Tab { + let tab = addTab(request, configuration: configuration, afterTab: afterTab) + selectTab(tab) + return tab + } + + // This method is duplicated to hide the flushToDisk option from consumers. + @discardableResult func addTab(_ request: URLRequest! = nil, configuration: WKWebViewConfiguration! = nil, afterTab: Tab? = nil) -> Tab { + return self.addTab(request, configuration: configuration, afterTab: afterTab, flushToDisk: true, zombie: false) + } + + func addTabsForURLs(_ urls: [URL], zombie: Bool) { + assert(Thread.isMainThread) + + if urls.isEmpty { + return + } + // When bulk adding tabs don't notify delegates until we are done + self.isRestoring = true + var tab: Tab! + for url in urls { + tab = self.addTab(URLRequest(url: url), flushToDisk: false, zombie: zombie) + } + // Flush. + storeChanges() + // Select the most recent. + self.selectTab(tab) + self.isRestoring = false + // Okay now notify that we bulk-loaded so we can adjust counts and animate changes. + delegates.forEach { $0.get()?.tabManagerDidAddTabs(self) } + } + + fileprivate func addTab(_ request: URLRequest? = nil, configuration: WKWebViewConfiguration? = nil, afterTab: Tab? = nil, flushToDisk: Bool, zombie: Bool, isPrivate: Bool) -> Tab { + assert(Thread.isMainThread) + + // Take the given configuration. Or if it was nil, take our default configuration for the current browsing mode. + let configuration: WKWebViewConfiguration = configuration ?? (isPrivate ? privateConfiguration : self.configuration) + + let tab = Tab(configuration: configuration, isPrivate: isPrivate) + configureTab(tab, request: request, afterTab: afterTab, flushToDisk: flushToDisk, zombie: zombie) + return tab + } + + fileprivate func addTab(_ request: URLRequest? = nil, configuration: WKWebViewConfiguration? = nil, afterTab: Tab? = nil, flushToDisk: Bool, zombie: Bool) -> Tab { + assert(Thread.isMainThread) + + let tab = Tab(configuration: configuration ?? self.configuration) + configureTab(tab, request: request, afterTab: afterTab, flushToDisk: flushToDisk, zombie: zombie) + return tab + } + + func moveTab(isPrivate privateMode: Bool, fromIndex visibleFromIndex: Int, toIndex visibleToIndex: Int) { + assert(Thread.isMainThread) + + let currentTabs = privateMode ? privateTabs : normalTabs + let fromIndex = tabs.index(of: currentTabs[visibleFromIndex]) ?? tabs.count - 1 + let toIndex = tabs.index(of: currentTabs[visibleToIndex]) ?? tabs.count - 1 + + let previouslySelectedTab = selectedTab + + tabs.insert(tabs.remove(at: fromIndex), at: toIndex) + + if let previouslySelectedTab = previouslySelectedTab, let previousSelectedIndex = tabs.index(of: previouslySelectedTab) { + _selectedIndex = previousSelectedIndex + } + + storeChanges() + } + + func configureTab(_ tab: Tab, request: URLRequest?, afterTab parent: Tab? = nil, flushToDisk: Bool, zombie: Bool) { + assert(Thread.isMainThread) + + delegates.forEach { $0.get()?.tabManager(self, willAddTab: tab) } + + if parent == nil || parent?.isPrivate != tab.isPrivate { + tabs.append(tab) + } else if let parent = parent, var insertIndex = tabs.index(of: parent) { + insertIndex += 1 + while insertIndex < tabs.count && tabs[insertIndex].isDescendentOf(parent) { + insertIndex += 1 + } + tab.parent = parent + tabs.insert(tab, at: insertIndex) + } + + delegates.forEach { $0.get()?.tabManager(self, didAddTab: tab) } + + if !zombie { + tab.createWebview() + } + tab.navigationDelegate = self.navDelegate + + if let request = request { + tab.loadRequest(request) + } else { + let newTabChoice = NewTabAccessors.getNewTabPage(prefs) + switch newTabChoice { + case .homePage: + // We definitely have a homepage if we've got here + // (so we can safely dereference it). + let url = HomePageAccessors.getHomePage(prefs)! + tab.loadRequest(URLRequest(url: url)) + case .blankPage: + // Do nothing: we're already seeing a blank page. + break + default: + // The common case, where the NewTabPage enum defines + // one of the about:home pages. + if let url = newTabChoice.url { + tab.loadRequest(PrivilegedRequest(url: url) as URLRequest) + tab.url = url + } + } + } + if flushToDisk { + storeChanges() + } + } + + // This method is duplicated to hide the flushToDisk option from consumers. + func removeTab(_ tab: Tab) { + self.removeTab(tab, flushToDisk: true, notify: true) + hideNetworkActivitySpinner() + } + + /// - Parameter notify: if set to true, will call the delegate after the tab + /// is removed. + fileprivate func removeTab(_ tab: Tab, flushToDisk: Bool, notify: Bool) { + assert(Thread.isMainThread) + + guard let removalIndex = tabs.index(where: { $0 === tab }) else { + Sentry.shared.sendWithStacktrace(message: "Could not find index of tab to remove", tag: .tabManager, severity: .fatal, description: "Tab count: \(count)") + return + } + + if tab.isPrivate { + removeAllBrowsingDataForTab(tab) + } + + let oldSelectedTab = selectedTab + + if notify { + delegates.forEach { $0.get()?.tabManager(self, willRemoveTab: tab) } + } + + // The index of the tab in its respective tab grouping. Used to figure out which tab is next + var tabIndex: Int = -1 + if let oldTab = oldSelectedTab { + tabIndex = (tab.isPrivate ? privateTabs.index(of: oldTab) : normalTabs.index(of: oldTab)) ?? -1 + } + + let prevCount = count + tabs.remove(at: removalIndex) + + let viableTabs: [Tab] = tab.isPrivate ? privateTabs : normalTabs + + // Let's select the tab to be selected next. + if let oldTab = oldSelectedTab, tab !== oldTab { + // If it wasn't the selected tab we removed, then keep it like that. + // It might have changed index, so we look it up again. + _selectedIndex = tabs.index(of: oldTab) ?? -1 + } else if let newTab = viableTabs.reduce(viableTabs.first, { currentBestTab, tab2 in + if let tab1 = currentBestTab, let time1 = tab1.lastExecutedTime { + if let time2 = tab2.lastExecutedTime { + return time1 <= time2 ? tab2 : tab1 + } + return tab1 + } else { + return tab2 + } + }), tab !== newTab, newTab.lastExecutedTime != nil { + // Next we look for the most recently loaded one. It might not exist, of course. + _selectedIndex = tabs.index(of: newTab) ?? -1 + } else { + // By now, we've just removed the selected one, and no previously loaded + // tabs. So let's load the final one in the tab tray. + if tabIndex == viableTabs.count { + tabIndex -= 1 + } + if tabIndex < viableTabs.count && !viableTabs.isEmpty { + _selectedIndex = tabs.index(of: viableTabs[tabIndex]) ?? -1 + } else { + _selectedIndex = -1 + } + } + + assert(count == prevCount - 1, "Make sure the tab count was actually removed") + + // There's still some time between this and the webView being destroyed. We don't want to pick up any stray events. + tab.webView?.navigationDelegate = nil + + if notify { + delegates.forEach { $0.get()?.tabManager(self, didRemoveTab: tab) } + TabEvent.post(.didClose, for: tab) + } + + if !tab.isPrivate && viableTabs.isEmpty { + addTab() + } + + // If the removed tab was selected, find the new tab to select. + if selectedTab != nil { + selectTab(selectedTab, previous: oldSelectedTab) + } else { + selectTab(tabs.last, previous: oldSelectedTab) + } + + if flushToDisk { + storeChanges() + } + } + + /// Removes all private tabs from the manager without notifying delegates. + private func removeAllPrivateTabs() { + // reset the selectedTabIndex if we are on a private tab because we will be removing it. + if selectedTab?.isPrivate ?? false { + _selectedIndex = -1 + } + tabs.forEach { tab in + if tab.isPrivate { + removeAllBrowsingDataForTab(tab) + } + } + + tabs = tabs.filter { !$0.isPrivate } + } + + func removeAllBrowsingDataForTab(_ tab: Tab, completionHandler: @escaping () -> Void = {}) { + let dataTypes = Set([WKWebsiteDataTypeCookies, + WKWebsiteDataTypeLocalStorage, + WKWebsiteDataTypeSessionStorage, + WKWebsiteDataTypeWebSQLDatabases, + WKWebsiteDataTypeIndexedDBDatabases]) + tab.webView?.configuration.websiteDataStore.removeData(ofTypes: dataTypes, + modifiedSince: Date.distantPast, + completionHandler: completionHandler) + } + + func removeTabsWithUndoToast(_ tabs: [Tab]) { + tempTabs = tabs + var tabsCopy = tabs + + // Remove the current tab last to prevent switching tabs while removing tabs + if let selectedTab = selectedTab { + if let selectedIndex = tabsCopy.index(of: selectedTab) { + let removed = tabsCopy.remove(at: selectedIndex) + removeTabs(tabsCopy) + removeTab(removed) + } else { + removeTabs(tabsCopy) + } + } + for tab in tabs { + tab.hideContent() + } + var toast: ButtonToast? + if let numberOfTabs = tempTabs?.count, numberOfTabs > 0 { + toast = ButtonToast(labelText: String.localizedStringWithFormat(Strings.TabsDeleteAllUndoTitle, numberOfTabs), buttonText: Strings.TabsDeleteAllUndoAction, completion: { buttonPressed in + if buttonPressed { + self.undoCloseTabs() + self.storeChanges() + for delegate in self.delegates { + delegate.get()?.tabManagerDidAddTabs(self) + } + } + self.eraseUndoCache() + }) + } + + delegates.forEach { $0.get()?.tabManagerDidRemoveAllTabs(self, toast: toast) } + } + + func undoCloseTabs() { + guard let tempTabs = self.tempTabs, tempTabs.count > 0 else { + return + } + let tabsCopy = normalTabs + restoreTabs(tempTabs) + self.isRestoring = true + for tab in tempTabs { + tab.showContent(true) + } + if !tempTabs[0].isPrivate { + removeTabs(tabsCopy) + } + selectTab(tempTabs.first) + self.isRestoring = false + delegates.forEach { $0.get()?.tabManagerDidRestoreTabs(self) } + self.tempTabs?.removeAll() + tabs.first?.createWebview() + } + + func eraseUndoCache() { + tempTabs?.removeAll() + } + + func removeTabs(_ tabs: [Tab]) { + for tab in tabs { + self.removeTab(tab, flushToDisk: false, notify: true) + } + storeChanges() + } + + func removeAll() { + removeTabs(self.tabs) + } + + func getIndex(_ tab: Tab) -> Int? { + assert(Thread.isMainThread) + + for i in 0.. Tab? { + assert(Thread.isMainThread) + + return tabs.filter { $0.webView?.url == url } .first + } + + func storeChanges() { + stateDelegate?.tabManagerWillStoreTabs(normalTabs) + + // Also save (full) tab state to disk. + preserveTabs() + } + + func prefsDidChange() { + DispatchQueue.main.async { + let allowPopups = !(self.prefs.boolForKey("blockPopups") ?? true) + // Each tab may have its own configuration, so we should tell each of them in turn. + for tab in self.tabs { + tab.webView?.configuration.preferences.javaScriptCanOpenWindowsAutomatically = allowPopups + } + // The default tab configurations also need to change. + self.configuration.preferences.javaScriptCanOpenWindowsAutomatically = allowPopups + self.privateConfiguration.preferences.javaScriptCanOpenWindowsAutomatically = allowPopups + } + } + + func resetProcessPool() { + assert(Thread.isMainThread) + + configuration.processPool = WKProcessPool() + } +} + +class SavedTab: NSObject, NSCoding { + let isSelected: Bool + let title: String? + let isPrivate: Bool + var sessionData: SessionData? + var screenshotUUID: UUID? + var faviconURL: String? + + var jsonDictionary: [String: AnyObject] { + let title: String = self.title ?? "null" + let faviconURL: String = self.faviconURL ?? "null" + let uuid: String = self.screenshotUUID?.uuidString ?? "null" + + var json: [String: AnyObject] = [ + "title": title as AnyObject, + "isPrivate": String(self.isPrivate) as AnyObject, + "isSelected": String(self.isSelected) as AnyObject, + "faviconURL": faviconURL as AnyObject, + "screenshotUUID": uuid as AnyObject + ] + + if let sessionDataInfo = self.sessionData?.jsonDictionary { + json["sessionData"] = sessionDataInfo as AnyObject? + } + + return json + } + + init?(tab: Tab, isSelected: Bool) { + assert(Thread.isMainThread) + + self.screenshotUUID = tab.screenshotUUID as UUID? + self.isSelected = isSelected + self.title = tab.displayTitle + self.isPrivate = tab.isPrivate + self.faviconURL = tab.displayFavicon?.url + super.init() + + if tab.sessionData == nil { + let currentItem: WKBackForwardListItem! = tab.webView?.backForwardList.currentItem + + // Freshly created web views won't have any history entries at all. + // If we have no history, abort. + if currentItem == nil { + return nil + } + + let backList = tab.webView?.backForwardList.backList ?? [] + let forwardList = tab.webView?.backForwardList.forwardList ?? [] + let urls = (backList + [currentItem] + forwardList).map { $0.url } + let currentPage = -forwardList.count + self.sessionData = SessionData(currentPage: currentPage, urls: urls, lastUsedTime: tab.lastExecutedTime ?? Date.now()) + } else { + self.sessionData = tab.sessionData + } + } + + required init?(coder: NSCoder) { + self.sessionData = coder.decodeObject(forKey: "sessionData") as? SessionData + self.screenshotUUID = coder.decodeObject(forKey: "screenshotUUID") as? UUID + self.isSelected = coder.decodeBool(forKey: "isSelected") + self.title = coder.decodeObject(forKey: "title") as? String + self.isPrivate = coder.decodeBool(forKey: "isPrivate") + self.faviconURL = coder.decodeObject(forKey: "faviconURL") as? String + } + + func encode(with coder: NSCoder) { + coder.encode(sessionData, forKey: "sessionData") + coder.encode(screenshotUUID, forKey: "screenshotUUID") + coder.encode(isSelected, forKey: "isSelected") + coder.encode(title, forKey: "title") + coder.encode(isPrivate, forKey: "isPrivate") + coder.encode(faviconURL, forKey: "faviconURL") + } +} + +extension TabManager { + + static fileprivate func tabsStateArchivePath() -> String { + let documentsPath = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0] + return URL(fileURLWithPath: documentsPath).appendingPathComponent("tabsState.archive").path + } + + static func tabArchiveData() -> Data? { + let tabStateArchivePath = tabsStateArchivePath() + if FileManager.default.fileExists(atPath: tabStateArchivePath) { + return (try? Data(contentsOf: URL(fileURLWithPath: tabStateArchivePath))) + } else { + return nil + } + } + + static func tabsToRestore() -> [SavedTab]? { + if let tabData = tabArchiveData() { + let unarchiver = NSKeyedUnarchiver(forReadingWith: tabData) + unarchiver.decodingFailurePolicy = .setErrorAndReturn + guard let tabs = unarchiver.decodeObject(forKey: "tabs") as? [SavedTab] else { + Sentry.shared.send(message: "Failed to restore tabs", tag: SentryTag.tabManager, severity: .error, description: "\(unarchiver.error ??? "nil")") + return nil + } + return tabs + } else { + return nil + } + } + + fileprivate func preserveTabsInternal() { + assert(Thread.isMainThread) + + guard !isRestoring else { return } + + let path = TabManager.tabsStateArchivePath() + var savedTabs = [SavedTab]() + var savedUUIDs = Set() + for (tabIndex, tab) in tabs.enumerated() { + if let savedTab = SavedTab(tab: tab, isSelected: tabIndex == selectedIndex) { + savedTabs.append(savedTab) + + if let screenshot = tab.screenshot, + let screenshotUUID = tab.screenshotUUID { + savedUUIDs.insert(screenshotUUID.uuidString) + imageStore?.put(screenshotUUID.uuidString, image: screenshot) + } + } + } + + // Clean up any screenshots that are no longer associated with a tab. + _ = imageStore?.clearExcluding(savedUUIDs) + + let tabStateData = NSMutableData() + let archiver = NSKeyedArchiver(forWritingWith: tabStateData) + archiver.encode(savedTabs, forKey: "tabs") + archiver.finishEncoding() + tabStateData.write(toFile: path, atomically: true) + } + + func preserveTabs() { + // This is wrapped in an Objective-C @try/@catch handler because NSKeyedArchiver may throw exceptions which Swift cannot handle + _ = Try(withTry: { () -> Void in + self.preserveTabsInternal() + }) { (exception) -> Void in + Sentry.shared.send(message: "Failed to preserve tabs", tag: SentryTag.tabManager, severity: .error, description: "\(exception ??? "nil")") + } + } + + fileprivate func restoreTabsInternal() { + guard var savedTabs = TabManager.tabsToRestore() else { + return + } + + // Make sure to wipe the private tabs if the user has the pref turned on + if shouldClearPrivateTabs() { + savedTabs = savedTabs.filter { !$0.isPrivate } + } + + var tabToSelect: Tab? + for savedTab in savedTabs { + // Provide an empty request to prevent a new tab from loading the home screen + let tab = self.addTab(nil, configuration: nil, afterTab: nil, flushToDisk: false, zombie: true, isPrivate: savedTab.isPrivate) + + // Since this is a restored tab, reset the URL to be loaded as that will be handled by the SessionRestoreHandler + tab.url = nil + + if let faviconURL = savedTab.faviconURL { + let icon = Favicon(url: faviconURL, date: Date(), type: IconType.noneFound) + icon.width = 1 + tab.favicons.append(icon) + } + + // Set the UUID for the tab, asynchronously fetch the UIImage, then store + // the screenshot in the tab as long as long as a newer one hasn't been taken. + if let screenshotUUID = savedTab.screenshotUUID, + let imageStore = self.imageStore { + tab.screenshotUUID = screenshotUUID + imageStore.get(screenshotUUID.uuidString) >>== { screenshot in + if tab.screenshotUUID == screenshotUUID { + tab.setScreenshot(screenshot, revUUID: false) + } + } + } + + if savedTab.isSelected { + tabToSelect = tab + } + + tab.sessionData = savedTab.sessionData + tab.lastTitle = savedTab.title + } + + if tabToSelect == nil { + tabToSelect = tabs.first + } + + // Only tell our delegates that we restored tabs if we actually restored a tab(s) + if savedTabs.count > 0 { + for delegate in delegates { + delegate.get()?.tabManagerDidRestoreTabs(self) + } + } + + if let tab = tabToSelect { + selectTab(tab) + tab.createWebview() + } + } + + func restoreTabs() { + isRestoring = true + + if count == 0 && !AppConstants.IsRunningTest && !DebugSettingsBundleOptions.skipSessionRestore { + // This is wrapped in an Objective-C @try/@catch handler because NSKeyedUnarchiver may throw exceptions which Swift cannot handle + _ = Try( + withTry: { () -> Void in + self.restoreTabsInternal() + }, + catch: { exception in + Sentry.shared.send(message: "Failed to restore tabs: ", tag: SentryTag.tabManager, severity: .error, description: "\(exception ??? "nil")") + } + ) + } + isRestoring = false + + // Always make sure there is a single normal tab. + if normalTabs.isEmpty { + let tab = addTab() + if selectedTab == nil { + selectTab(tab) + } + } + } + + func restoreTabs(_ savedTabs: [Tab]) { + isRestoring = true + for tab in savedTabs { + tabs.append(tab) + tab.navigationDelegate = self.navDelegate + for delegate in delegates { + delegate.get()?.tabManager(self, didAddTab: tab) + } + } + isRestoring = false + } +} + +extension TabManager: WKNavigationDelegate { + func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) { + UIApplication.shared.isNetworkActivityIndicatorVisible = true + } + + func webView(_ webView: WKWebView, didCommit navigation: WKNavigation!) { + let tab = self[webView] + let isNightMode = NightModeAccessors.isNightMode(self.prefs) + tab?.setNightMode(isNightMode) + + if #available(iOS 11, *) { + let isNoImageMode = self.prefs.boolForKey(PrefsKeys.KeyNoImageModeStatus) ?? false + tab?.noImageMode = isNoImageMode + } + } + + func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { + hideNetworkActivitySpinner() + // only store changes if this is not an error page + // as we current handle tab restore as error page redirects then this ensures that we don't + // call storeChanges unnecessarily on startup + if let url = webView.url { + if !url.isErrorPageURL { + storeChanges() + } + } + } + + func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) { + hideNetworkActivitySpinner() + } + + func hideNetworkActivitySpinner() { + for tab in tabs { + if let tabWebView = tab.webView { + // If we find one tab loading, we don't hide the spinner + if tabWebView.isLoading { + return + } + } + } + UIApplication.shared.isNetworkActivityIndicatorVisible = false + } + + /// Called when the WKWebView's content process has gone away. If this happens for the currently selected tab + /// then we immediately reload it. + + func webViewWebContentProcessDidTerminate(_ webView: WKWebView) { + if let tab = selectedTab, tab.webView == webView { + webView.reload() + } + } +} + +extension TabManager { + class func tabRestorationDebugInfo() -> String { + assert(Thread.isMainThread) + + let tabs = TabManager.tabsToRestore()?.map { $0.jsonDictionary } ?? [] + do { + let jsonData = try JSONSerialization.data(withJSONObject: tabs, options: [.prettyPrinted]) + return String(data: jsonData, encoding: String.Encoding.utf8) ?? "" + } catch _ { + return "" + } + } +} + +// WKNavigationDelegates must implement NSObjectProtocol +class TabManagerNavDelegate: NSObject, WKNavigationDelegate { + fileprivate var delegates = WeakList() + + func insert(_ delegate: WKNavigationDelegate) { + delegates.insert(delegate) + } + + func webView(_ webView: WKWebView, didCommit navigation: WKNavigation!) { + for delegate in delegates { + delegate.webView?(webView, didCommit: navigation) + } + } + + func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) { + for delegate in delegates { + delegate.webView?(webView, didFail: navigation, withError: error) + } + } + + func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error) { + for delegate in delegates { + delegate.webView?(webView, didFailProvisionalNavigation: navigation, withError: error) + } + } + + func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { + for delegate in delegates { + delegate.webView?(webView, didFinish: navigation) + } + } + + func webView(_ webView: WKWebView, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) { + let authenticatingDelegates = delegates.filter { wv in + return wv.responds(to: #selector(WKNavigationDelegate.webView(_:didReceive:completionHandler:))) + } + + guard let firstAuthenticatingDelegate = authenticatingDelegates.first else { + return completionHandler(URLSession.AuthChallengeDisposition.performDefaultHandling, nil) + } + + firstAuthenticatingDelegate.webView?(webView, didReceive: challenge) { (disposition, credential) in + completionHandler(disposition, credential) + } + } + + func webView(_ webView: WKWebView, didReceiveServerRedirectForProvisionalNavigation navigation: WKNavigation!) { + for delegate in delegates { + delegate.webView?(webView, didReceiveServerRedirectForProvisionalNavigation: navigation) + } + } + + func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) { + for delegate in delegates { + delegate.webView?(webView, didStartProvisionalNavigation: navigation) + } + } + + func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) { + var res = WKNavigationActionPolicy.allow + for delegate in delegates { + delegate.webView?(webView, decidePolicyFor: navigationAction, decisionHandler: { policy in + if policy == .cancel { + res = policy + } + }) + } + + decisionHandler(res) + } + + func webView(_ webView: WKWebView, + decidePolicyFor navigationResponse: WKNavigationResponse, + decisionHandler: @escaping (WKNavigationResponsePolicy) -> Void) { + var res = WKNavigationResponsePolicy.allow + for delegate in delegates { + delegate.webView?(webView, decidePolicyFor: navigationResponse, decisionHandler: { policy in + if policy == .cancel { + res = policy + } + }) + } + + if res == .allow, let appDelegate = UIApplication.shared.delegate as? AppDelegate { + let tab = appDelegate.browserViewController.tabManager[webView] + tab?.mimeType = navigationResponse.response.mimeType + } + + decisionHandler(res) + } +} diff --git a/mobile/ios/Client/Frontend/Browser/TabPeekViewController.swift b/mobile/ios/Client/Frontend/Browser/TabPeekViewController.swift new file mode 100644 index 0000000000..fcde0e0ad8 --- /dev/null +++ b/mobile/ios/Client/Frontend/Browser/TabPeekViewController.swift @@ -0,0 +1,174 @@ +/* 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 UIKit +import Shared +import Storage +import ReadingList +import WebKit + +protocol TabPeekDelegate: class { + func tabPeekDidAddBookmark(_ tab: Tab) + @discardableResult func tabPeekDidAddToReadingList(_ tab: Tab) -> ReadingListClientRecord? + func tabPeekRequestsPresentationOf(_ viewController: UIViewController) + func tabPeekDidCloseTab(_ tab: Tab) +} + +class TabPeekViewController: UIViewController, WKNavigationDelegate { + + fileprivate static let PreviewActionAddToBookmarks = NSLocalizedString("Add to Bookmarks", tableName: "3DTouchActions", comment: "Label for preview action on Tab Tray Tab to add current tab to Bookmarks") + fileprivate static let PreviewActionAddToReadingList = NSLocalizedString("Add to Reading List", tableName: "3DTouchActions", comment: "Label for preview action on Tab Tray Tab to add current tab to Reading List") + fileprivate static let PreviewActionCopyURL = NSLocalizedString("Copy URL", tableName: "3DTouchActions", comment: "Label for preview action on Tab Tray Tab to copy the URL of the current tab to clipboard") + fileprivate static let PreviewActionCloseTab = NSLocalizedString("Close Tab", tableName: "3DTouchActions", comment: "Label for preview action on Tab Tray Tab to close the current tab") + + weak var tab: Tab? + + fileprivate weak var delegate: TabPeekDelegate? + fileprivate var clientPicker: UINavigationController? + fileprivate var isBookmarked: Bool = false + fileprivate var isInReadingList: Bool = false + fileprivate var hasRemoteClients: Bool = false + fileprivate var ignoreURL: Bool = false + + fileprivate var screenShot: UIImageView? + fileprivate var previewAccessibilityLabel: String! + + // Preview action items. + lazy var previewActions: [UIPreviewActionItem] = { + var actions = [UIPreviewActionItem]() + + let urlIsTooLongToSave = self.tab?.urlIsTooLong ?? false + if !self.ignoreURL && !urlIsTooLongToSave { + if !self.isInReadingList { + actions.append(UIPreviewAction(title: TabPeekViewController.PreviewActionAddToReadingList, style: .default) { previewAction, viewController in + guard let tab = self.tab else { return } + _ = self.delegate?.tabPeekDidAddToReadingList(tab) + }) + } + + if !self.isBookmarked { + actions.append(UIPreviewAction(title: TabPeekViewController.PreviewActionAddToBookmarks, style: .default) { previewAction, viewController in + guard let tab = self.tab else { return } + self.delegate?.tabPeekDidAddBookmark(tab) + }) + } + if self.hasRemoteClients { + actions.append(UIPreviewAction(title: Strings.SendToDeviceTitle, style: .default) { previewAction, viewController in + guard let clientPicker = self.clientPicker else { return } + self.delegate?.tabPeekRequestsPresentationOf(clientPicker) + }) + } + // only add the copy URL action if we don't already have 3 items in our list + // as we are only allowed 4 in total and we always want to display close tab + if actions.count < 3 { + actions.append(UIPreviewAction(title: TabPeekViewController.PreviewActionCopyURL, style: .default) { previewAction, viewController in + guard let url = self.tab?.canonicalURL else { return } + UIPasteboard.general.url = url + SimpleToast().showAlertWithText(Strings.AppMenuCopyURLConfirmMessage, bottomContainer: self.view) + }) + } + } + actions.append(UIPreviewAction(title: TabPeekViewController.PreviewActionCloseTab, style: .destructive) { previewAction, viewController in + guard let tab = self.tab else { return } + self.delegate?.tabPeekDidCloseTab(tab) + }) + + return actions + }() + + init(tab: Tab, delegate: TabPeekDelegate?) { + self.tab = tab + self.delegate = delegate + super.init(nibName: nil, bundle: nil) + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + if let webViewAccessibilityLabel = tab?.webView?.accessibilityLabel { + previewAccessibilityLabel = String(format: NSLocalizedString("Preview of %@", tableName: "3DTouchActions", comment: "Accessibility label, associated to the 3D Touch action on the current tab in the tab tray, used to display a larger preview of the tab."), webViewAccessibilityLabel) + } + // if there is no screenshot, load the URL in a web page + // otherwise just show the screenshot + setupWebView(tab?.webView) + guard let screenshot = tab?.screenshot else { return } + setupWithScreenshot(screenshot) + } + + fileprivate func setupWithScreenshot(_ screenshot: UIImage) { + let imageView = UIImageView(image: screenshot) + self.view.addSubview(imageView) + + imageView.snp.makeConstraints { make in + make.edges.equalTo(self.view) + } + + screenShot = imageView + screenShot?.accessibilityLabel = previewAccessibilityLabel + } + + fileprivate func setupWebView(_ webView: WKWebView?) { + guard let webView = webView, let url = webView.url, !isIgnoredURL(url) else { return } + let clonedWebView = WKWebView(frame: webView.frame, configuration: webView.configuration) + clonedWebView.allowsLinkPreview = false + webView.accessibilityLabel = previewAccessibilityLabel + self.view.addSubview(clonedWebView) + + clonedWebView.snp.makeConstraints { make in + make.edges.equalTo(self.view) + } + + clonedWebView.navigationDelegate = self + + clonedWebView.load(URLRequest(url: url)) + } + + func setState(withProfile browserProfile: BrowserProfile, clientPickerDelegate: ClientPickerViewControllerDelegate) { + assert(Thread.current.isMainThread) + + guard let tab = self.tab else { + return + } + + guard let displayURL = tab.url?.absoluteString, displayURL.characters.count > 0 else { + return + } + + let mainQueue = DispatchQueue.main + browserProfile.bookmarks.modelFactory >>== { + $0.isBookmarked(displayURL).uponQueue(mainQueue) { + self.isBookmarked = $0.successValue ?? false + } + } + + browserProfile.remoteClientsAndTabs.getClientGUIDs().uponQueue(mainQueue) { + guard let clientGUIDs = $0.successValue else { + return + } + + self.hasRemoteClients = !clientGUIDs.isEmpty + let clientPickerController = ClientPickerViewController() + clientPickerController.clientPickerDelegate = clientPickerDelegate + clientPickerController.profile = browserProfile + if let url = tab.url?.absoluteString { + clientPickerController.shareItem = ShareItem(url: url, title: tab.title, favicon: nil) + } + + self.clientPicker = UINavigationController(rootViewController: clientPickerController) + } + + let result = browserProfile.readingList?.getRecordWithURL(displayURL).successValue! + + self.isInReadingList = (result?.url.characters.count ?? 0) > 0 + self.ignoreURL = isIgnoredURL(displayURL) + } + + func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { + screenShot?.removeFromSuperview() + screenShot = nil + } +} diff --git a/mobile/ios/Client/Frontend/Browser/TabPrintPageRenderer.swift b/mobile/ios/Client/Frontend/Browser/TabPrintPageRenderer.swift new file mode 100644 index 0000000000..bd30759b9d --- /dev/null +++ b/mobile/ios/Client/Frontend/Browser/TabPrintPageRenderer.swift @@ -0,0 +1,73 @@ +/* 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 + +private struct PrintedPageUX { + static let PageInsets = CGFloat(36.0) + static let PageTextFont = DynamicFontHelper.defaultHelper.DefaultSmallFont + static let PageMarginScale = CGFloat(0.5) +} + +class TabPrintPageRenderer: UIPrintPageRenderer { + fileprivate weak var tab: Tab? + let textAttributes = [NSFontAttributeName: PrintedPageUX.PageTextFont] + let dateString: String + + required init(tab: Tab) { + self.tab = tab + let dateFormatter = DateFormatter() + dateFormatter.dateStyle = .short + dateFormatter.timeStyle = .short + self.dateString = dateFormatter.string(from: Date()) + + super.init() + + self.footerHeight = PrintedPageUX.PageMarginScale * PrintedPageUX.PageInsets + self.headerHeight = PrintedPageUX.PageMarginScale * PrintedPageUX.PageInsets + + if let tab = self.tab { + let formatter = tab.webView!.viewPrintFormatter() + formatter.perPageContentInsets = UIEdgeInsets(top: PrintedPageUX.PageInsets, left: PrintedPageUX.PageInsets, bottom: PrintedPageUX.PageInsets, right: PrintedPageUX.PageInsets) + addPrintFormatter(formatter, startingAtPageAt: 0) + } + } + + override func drawFooterForPage(at pageIndex: Int, in headerRect: CGRect) { + let headerInsets = UIEdgeInsets(top: headerRect.minY, left: PrintedPageUX.PageInsets, bottom: paperRect.maxY - headerRect.maxY, right: PrintedPageUX.PageInsets) + let headerRect = UIEdgeInsetsInsetRect(paperRect, headerInsets) + + // url on left + self.drawTextAtPoint(tab!.url?.displayURL?.absoluteString ?? "", rect: headerRect, onLeft: true) + + // page number on right + let pageNumberString = "\(pageIndex + 1)" + self.drawTextAtPoint(pageNumberString, rect: headerRect, onLeft: false) + } + + override func drawHeaderForPage(at pageIndex: Int, in headerRect: CGRect) { + let headerInsets = UIEdgeInsets(top: headerRect.minY, left: PrintedPageUX.PageInsets, bottom: paperRect.maxY - headerRect.maxY, right: PrintedPageUX.PageInsets) + let headerRect = UIEdgeInsetsInsetRect(paperRect, headerInsets) + + // page title on left + self.drawTextAtPoint(tab!.displayTitle, rect: headerRect, onLeft: true) + + // date on right + self.drawTextAtPoint(dateString, rect: headerRect, onLeft: false) + } + + func drawTextAtPoint(_ text: String, rect: CGRect, onLeft: Bool) { + let size = text.size(attributes: textAttributes) + let x, y: CGFloat + if onLeft { + x = rect.minX + y = rect.midY - size.height / 2 + } else { + x = rect.maxX - size.width + y = rect.midY - size.height / 2 + } + text.draw(at: CGPoint(x: x, y: y), withAttributes: textAttributes) + } + +} diff --git a/mobile/ios/Client/Frontend/Browser/TabScrollController.swift b/mobile/ios/Client/Frontend/Browser/TabScrollController.swift new file mode 100644 index 0000000000..9f7f7f8bc9 --- /dev/null +++ b/mobile/ios/Client/Frontend/Browser/TabScrollController.swift @@ -0,0 +1,344 @@ +/* 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 UIKit +import SnapKit + +private let ToolbarBaseAnimationDuration: CGFloat = 0.2 + +class TabScrollingController: NSObject { + enum ScrollDirection { + case up + case down + } + + enum ToolbarState { + case collapsed + case visible + case animating + } + + weak var tab: Tab? { + willSet { + self.scrollView?.delegate = nil + self.scrollView?.removeGestureRecognizer(panGesture) + } + + didSet { + self.scrollView?.addGestureRecognizer(panGesture) + scrollView?.delegate = self + } + } + + // Constraint-based animation is causing PDF docs to flicker. This is used to bypass this animation. + var isTabShowingPDF: Bool { + return (tab?.mimeType ?? "") == MimeType.PDF.rawValue + } + + weak var header: UIView? + weak var footer: UIView? + weak var urlBar: URLBarView? + weak var snackBars: UIView? + weak var webViewContainerToolbar: UIView? + + var footerBottomConstraint: Constraint? + var headerTopConstraint: Constraint? + var toolbarsShowing: Bool { return headerTopOffset == 0 } + + fileprivate var isZoomedOut: Bool = false + fileprivate var lastZoomedScale: CGFloat = 0 + fileprivate var isUserZoom: Bool = false + + fileprivate var headerTopOffset: CGFloat = 0 { + didSet { + headerTopConstraint?.update(offset: headerTopOffset) + header?.superview?.setNeedsLayout() + } + } + + fileprivate var footerBottomOffset: CGFloat = 0 { + didSet { + footerBottomConstraint?.update(offset: footerBottomOffset) + footer?.superview?.setNeedsLayout() + } + } + + fileprivate lazy var panGesture: UIPanGestureRecognizer = { + let panGesture = UIPanGestureRecognizer(target: self, action: #selector(TabScrollingController.handlePan(_:))) + panGesture.maximumNumberOfTouches = 1 + panGesture.delegate = self + return panGesture + }() + + fileprivate var scrollView: UIScrollView? { return tab?.webView?.scrollView } + fileprivate var contentOffset: CGPoint { return scrollView?.contentOffset ?? CGPoint.zero } + fileprivate var contentSize: CGSize { return scrollView?.contentSize ?? CGSize.zero } + fileprivate var scrollViewHeight: CGFloat { return scrollView?.frame.height ?? 0 } + fileprivate var topScrollHeight: CGFloat { return header?.frame.height ?? 0 } + fileprivate var bottomScrollHeight: CGFloat { return footer?.frame.height ?? 0 } + fileprivate var snackBarsFrame: CGRect { return snackBars?.frame ?? CGRect.zero } + + fileprivate var lastContentOffset: CGFloat = 0 + fileprivate var scrollDirection: ScrollDirection = .down + fileprivate var toolbarState: ToolbarState = .visible + + override init() { + super.init() + } + + func showToolbars(animated: Bool, completion: ((_ finished: Bool) -> Void)? = nil) { + if toolbarState == .visible { + completion?(true) + return + } + toolbarState = .visible + let durationRatio = abs(headerTopOffset / topScrollHeight) + let actualDuration = TimeInterval(ToolbarBaseAnimationDuration * durationRatio) + self.animateToolbarsWithOffsets( + animated, + duration: actualDuration, + headerOffset: 0, + footerOffset: 0, + alpha: 1, + completion: completion) + } + + func hideToolbars(animated: Bool, completion: ((_ finished: Bool) -> Void)? = nil) { + if toolbarState == .collapsed { + completion?(true) + return + } + toolbarState = .collapsed + let durationRatio = abs((topScrollHeight + headerTopOffset) / topScrollHeight) + let actualDuration = TimeInterval(ToolbarBaseAnimationDuration * durationRatio) + self.animateToolbarsWithOffsets( + animated, + duration: actualDuration, + headerOffset: -topScrollHeight, + footerOffset: bottomScrollHeight, + alpha: 0, + completion: completion) + } + + override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey: Any]?, context: UnsafeMutableRawPointer?) { + if keyPath == "contentSize" { + if !checkScrollHeightIsLargeEnoughForScrolling() && !toolbarsShowing { + showToolbars(animated: true, completion: nil) + } + } + } + + func updateMinimumZoom() { + guard let scrollView = scrollView else { + return + } + self.isZoomedOut = roundNum(scrollView.zoomScale) == roundNum(scrollView.minimumZoomScale) + self.lastZoomedScale = self.isZoomedOut ? 0 : scrollView.zoomScale + } + + func setMinimumZoom() { + guard let scrollView = scrollView else { + return + } + if self.isZoomedOut && roundNum(scrollView.zoomScale) != roundNum(scrollView.minimumZoomScale) { + scrollView.zoomScale = scrollView.minimumZoomScale + } + } + + func resetZoomState() { + self.isZoomedOut = false + self.lastZoomedScale = 0 + } + + fileprivate func roundNum(_ num: CGFloat) -> CGFloat { + return round(100 * num) / 100 + } + +} + +private extension TabScrollingController { + func tabIsLoading() -> Bool { + return tab?.loading ?? true + } + + func isBouncingAtBottom() -> Bool { + guard let scrollView = scrollView else { return false } + return scrollView.contentOffset.y > (scrollView.contentSize.height - scrollView.frame.size.height) && scrollView.contentSize.height > scrollView.frame.size.height + } + + @objc func handlePan(_ gesture: UIPanGestureRecognizer) { + if tabIsLoading() { + return + } + + if let containerView = scrollView?.superview { + let translation = gesture.translation(in: containerView) + let delta = lastContentOffset - translation.y + + if delta > 0 { + scrollDirection = .down + } else if delta < 0 { + scrollDirection = .up + } + + lastContentOffset = translation.y + if checkRubberbandingForDelta(delta) && checkScrollHeightIsLargeEnoughForScrolling() { + let bottomIsNotRubberbanding = contentOffset.y + scrollViewHeight < contentSize.height + let topIsRubberbanding = contentOffset.y <= 0 + if isTabShowingPDF || ((toolbarState != .collapsed || topIsRubberbanding) && bottomIsNotRubberbanding) { + scrollWithDelta(delta) + } + + if headerTopOffset == -topScrollHeight && footerBottomOffset == bottomScrollHeight { + toolbarState = .collapsed + } else if headerTopOffset == 0 { + toolbarState = .visible + } else { + toolbarState = .animating + } + } + + if gesture.state == .ended || gesture.state == .cancelled { + lastContentOffset = 0 + } + + showOrHideWebViewContainerToolbar() + } + } + + func checkRubberbandingForDelta(_ delta: CGFloat) -> Bool { + return !((delta < 0 && contentOffset.y + scrollViewHeight > contentSize.height && + scrollViewHeight < contentSize.height) || + contentOffset.y < delta) + } + + func scrollWithDelta(_ delta: CGFloat) { + if scrollViewHeight >= contentSize.height { + return + } + + var updatedOffset = headerTopOffset - delta + headerTopOffset = clamp(updatedOffset, min: -topScrollHeight, max: 0) + if isHeaderDisplayedForGivenOffset(updatedOffset) { + scrollView?.contentOffset = CGPoint(x: contentOffset.x, y: contentOffset.y - delta) + } + + updatedOffset = footerBottomOffset + delta + footerBottomOffset = clamp(updatedOffset, min: 0, max: bottomScrollHeight) + + let alpha = 1 - abs(headerTopOffset / topScrollHeight) + urlBar?.updateAlphaForSubviews(alpha) + } + + func isHeaderDisplayedForGivenOffset(_ offset: CGFloat) -> Bool { + return offset > -topScrollHeight && offset < 0 + } + + func clamp(_ y: CGFloat, min: CGFloat, max: CGFloat) -> CGFloat { + if y >= max { + return max + } else if y <= min { + return min + } + return y + } + + func animateToolbarsWithOffsets(_ animated: Bool, duration: TimeInterval, headerOffset: CGFloat, footerOffset: CGFloat, alpha: CGFloat, completion: ((_ finished: Bool) -> Void)?) { + guard let scrollView = scrollView else { return } + let initialContentOffset = scrollView.contentOffset + + // If this function is used to fully animate the toolbar from hidden to shown, keep the page from scrolling by adjusting contentOffset, + // Otherwise when the toolbar is hidden and a link navigated, showing the toolbar will scroll the page and + // produce a ~50px page jumping effect in response to tap navigations. + let isShownFromHidden = headerTopOffset == -topScrollHeight && headerOffset == 0 + + let animation: () -> Void = { + if isShownFromHidden { + scrollView.contentOffset = CGPoint(x: initialContentOffset.x, y: initialContentOffset.y + self.topScrollHeight) + } + self.headerTopOffset = headerOffset + self.footerBottomOffset = footerOffset + self.urlBar?.updateAlphaForSubviews(alpha) + self.header?.superview?.layoutIfNeeded() + } + + if animated { + UIView.animate(withDuration: duration, delay: 0, options: .allowUserInteraction, animations: animation, completion: completion) + } else { + animation() + completion?(true) + } + } + + func checkScrollHeightIsLargeEnoughForScrolling() -> Bool { + return (UIScreen.main.bounds.size.height + 2 * UIConstants.ToolbarHeight) < scrollView?.contentSize.height ?? 0 + } + + func showOrHideWebViewContainerToolbar() { + if contentOffset.y >= webViewContainerToolbar?.frame.height ?? 0 { + webViewContainerToolbar?.isHidden = true + } else { + webViewContainerToolbar?.isHidden = false + } + } +} + +extension TabScrollingController: UIGestureRecognizerDelegate { + func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, + shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { + return true + } +} + +extension TabScrollingController: UIScrollViewDelegate { + func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) { + if tabIsLoading() || isBouncingAtBottom() { + return + } + + if (decelerate || (toolbarState == .animating && !decelerate)) && checkScrollHeightIsLargeEnoughForScrolling() { + if scrollDirection == .up { + showToolbars(animated: !isTabShowingPDF) + } else if scrollDirection == .down { + hideToolbars(animated: !isTabShowingPDF) + } + } + } + + func scrollViewDidZoom(_ scrollView: UIScrollView) { + // Only mess with the zoom level if the user did not initate the zoom via a zoom gesture + if self.isUserZoom { + return + } + + //scrollViewDidZoom will be called multiple times when a rotation happens. + // In that case ALWAYS reset to the minimum zoom level if the previous state was zoomed out (isZoomedOut=true) + if isZoomedOut { + scrollView.zoomScale = scrollView.minimumZoomScale + } else if roundNum(scrollView.zoomScale) > roundNum(self.lastZoomedScale) && self.lastZoomedScale != 0 { + //When we have manually zoomed in we want to preserve that scale. + //But sometimes when we rotate a larger zoomScale is appled. In that case apply the lastZoomedScale + scrollView.zoomScale = self.lastZoomedScale + } + } + + func scrollViewWillBeginZooming(_ scrollView: UIScrollView, with view: UIView?) { + self.isUserZoom = true + } + + func scrollViewDidEndZooming(_ scrollView: UIScrollView, with view: UIView?, atScale scale: CGFloat) { + self.isUserZoom = false + showOrHideWebViewContainerToolbar() + } + + func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) { + showOrHideWebViewContainerToolbar() + } + + func scrollViewShouldScrollToTop(_ scrollView: UIScrollView) -> Bool { + showToolbars(animated: true) + webViewContainerToolbar?.isHidden = false + return true + } +} diff --git a/mobile/ios/Client/Frontend/Browser/TabToolbar.swift b/mobile/ios/Client/Frontend/Browser/TabToolbar.swift new file mode 100644 index 0000000000..0372245411 --- /dev/null +++ b/mobile/ios/Client/Frontend/Browser/TabToolbar.swift @@ -0,0 +1,315 @@ +/* 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 UIKit +import SnapKit +import Shared +import XCGLogger + +private let log = Logger.browserLogger + +protocol TabToolbarProtocol: class { + weak var tabToolbarDelegate: TabToolbarDelegate? { get set } + var tabsButton: TabsButton { get } + var menuButton: ToolbarButton { get } + var forwardButton: ToolbarButton { get } + var backButton: ToolbarButton { get } + var stopReloadButton: ToolbarButton { get } + var actionButtons: [Themeable & UIButton] { get } + + func updateBackStatus(_ canGoBack: Bool) + func updateForwardStatus(_ canGoForward: Bool) + func updateReloadStatus(_ isLoading: Bool) + func updatePageStatus(_ isWebPage: Bool) + func updateTabCount(_ count: Int, animated: Bool) +} + +protocol TabToolbarDelegate: class { + func tabToolbarDidPressBack(_ tabToolbar: TabToolbarProtocol, button: UIButton) + func tabToolbarDidPressForward(_ tabToolbar: TabToolbarProtocol, button: UIButton) + func tabToolbarDidLongPressBack(_ tabToolbar: TabToolbarProtocol, button: UIButton) + func tabToolbarDidLongPressForward(_ tabToolbar: TabToolbarProtocol, button: UIButton) + func tabToolbarDidPressReload(_ tabToolbar: TabToolbarProtocol, button: UIButton) + func tabToolbarDidLongPressReload(_ tabToolbar: TabToolbarProtocol, button: UIButton) + func tabToolbarDidPressStop(_ tabToolbar: TabToolbarProtocol, button: UIButton) + func tabToolbarDidPressMenu(_ tabToolbar: TabToolbarProtocol, button: UIButton) + func tabToolbarDidPressTabs(_ tabToolbar: TabToolbarProtocol, button: UIButton) + func tabToolbarDidLongPressTabs(_ tabToolbar: TabToolbarProtocol, button: UIButton) +} + +@objc +open class TabToolbarHelper: NSObject { + let toolbar: TabToolbarProtocol + + let ImageReload = UIImage.templateImageNamed("nav-refresh") + let ImageStop = UIImage.templateImageNamed("nav-stop") + + var loading: Bool = false { + didSet { + if loading { + toolbar.stopReloadButton.setImage(ImageStop, for: .normal) + toolbar.stopReloadButton.accessibilityLabel = NSLocalizedString("Stop", comment: "Accessibility Label for the tab toolbar Stop button") + } else { + toolbar.stopReloadButton.setImage(ImageReload, for: .normal) + toolbar.stopReloadButton.accessibilityLabel = NSLocalizedString("Reload", comment: "Accessibility Label for the tab toolbar Reload button") + } + } + } + + fileprivate func setTheme(theme: String, forButtons buttons: [Themeable]) { + buttons.forEach { $0.applyTheme(theme) } + } + + init(toolbar: TabToolbarProtocol) { + self.toolbar = toolbar + super.init() + + toolbar.backButton.setImage(UIImage.templateImageNamed("nav-back"), for: .normal) + toolbar.backButton.accessibilityLabel = NSLocalizedString("Back", comment: "Accessibility label for the Back button in the tab toolbar.") + let longPressGestureBackButton = UILongPressGestureRecognizer(target: self, action: #selector(TabToolbarHelper.SELdidLongPressBack(_:))) + toolbar.backButton.addGestureRecognizer(longPressGestureBackButton) + toolbar.backButton.addTarget(self, action: #selector(TabToolbarHelper.SELdidClickBack), for: UIControlEvents.touchUpInside) + + toolbar.forwardButton.setImage(UIImage.templateImageNamed("nav-forward"), for: .normal) + toolbar.forwardButton.accessibilityLabel = NSLocalizedString("Forward", comment: "Accessibility Label for the tab toolbar Forward button") + let longPressGestureForwardButton = UILongPressGestureRecognizer(target: self, action: #selector(TabToolbarHelper.SELdidLongPressForward(_:))) + toolbar.forwardButton.addGestureRecognizer(longPressGestureForwardButton) + toolbar.forwardButton.addTarget(self, action: #selector(TabToolbarHelper.SELdidClickForward), for: UIControlEvents.touchUpInside) + + toolbar.stopReloadButton.setImage(UIImage.templateImageNamed("nav-refresh"), for: .normal) + toolbar.stopReloadButton.accessibilityLabel = NSLocalizedString("Reload", comment: "Accessibility Label for the tab toolbar Reload button") + let longPressGestureStopReloadButton = UILongPressGestureRecognizer(target: self, action: #selector(TabToolbarHelper.SELdidLongPressStopReload(_:))) + toolbar.stopReloadButton.addGestureRecognizer(longPressGestureStopReloadButton) + toolbar.stopReloadButton.addTarget(self, action: #selector(TabToolbarHelper.SELdidClickStopReload), for: UIControlEvents.touchUpInside) + + toolbar.tabsButton.addTarget(self, action: #selector(TabToolbarHelper.SELdidClickTabs), for: .touchUpInside) + let longPressGestureTabsButton = UILongPressGestureRecognizer(target: self, action: #selector(TabToolbarHelper.SELdidLongPressTabs(_:))) + toolbar.tabsButton.addGestureRecognizer(longPressGestureTabsButton) + + toolbar.menuButton.contentMode = UIViewContentMode.center + toolbar.menuButton.setImage(UIImage.templateImageNamed("nav-menu"), for: .normal) + toolbar.menuButton.accessibilityLabel = Strings.AppMenuButtonAccessibilityLabel + toolbar.menuButton.addTarget(self, action: #selector(TabToolbarHelper.SELdidClickMenu), for: UIControlEvents.touchUpInside) + toolbar.menuButton.accessibilityIdentifier = "TabToolbar.menuButton" + setTheme(theme: Theme.NormalMode, forButtons: toolbar.actionButtons) + } + + func SELdidClickBack() { + toolbar.tabToolbarDelegate?.tabToolbarDidPressBack(toolbar, button: toolbar.backButton) + } + + func SELdidLongPressBack(_ recognizer: UILongPressGestureRecognizer) { + if recognizer.state == UIGestureRecognizerState.began { + toolbar.tabToolbarDelegate?.tabToolbarDidLongPressBack(toolbar, button: toolbar.backButton) + } + } + + func SELdidClickTabs() { + toolbar.tabToolbarDelegate?.tabToolbarDidPressTabs(toolbar, button: toolbar.tabsButton) + } + + func SELdidLongPressTabs(_ recognizer: UILongPressGestureRecognizer) { + toolbar.tabToolbarDelegate?.tabToolbarDidLongPressTabs(toolbar, button: toolbar.tabsButton) + } + + func SELdidClickForward() { + toolbar.tabToolbarDelegate?.tabToolbarDidPressForward(toolbar, button: toolbar.forwardButton) + } + + func SELdidLongPressForward(_ recognizer: UILongPressGestureRecognizer) { + if recognizer.state == UIGestureRecognizerState.began { + toolbar.tabToolbarDelegate?.tabToolbarDidLongPressForward(toolbar, button: toolbar.forwardButton) + } + } + + func SELdidClickMenu() { + toolbar.tabToolbarDelegate?.tabToolbarDidPressMenu(toolbar, button: toolbar.menuButton) + } + + func SELdidClickStopReload() { + if loading { + toolbar.tabToolbarDelegate?.tabToolbarDidPressStop(toolbar, button: toolbar.stopReloadButton) + } else { + toolbar.tabToolbarDelegate?.tabToolbarDidPressReload(toolbar, button: toolbar.stopReloadButton) + } + } + + func SELdidLongPressStopReload(_ recognizer: UILongPressGestureRecognizer) { + if recognizer.state == UIGestureRecognizerState.began && !loading { + toolbar.tabToolbarDelegate?.tabToolbarDidLongPressReload(toolbar, button: toolbar.stopReloadButton) + } + } + + func updateReloadStatus(_ isLoading: Bool) { + loading = isLoading + } +} + +class ToolbarButton: UIButton { + static let Themes: [String: Theme] = { + var themes = [String: Theme]() + var theme = Theme() + theme.buttonTintColor = UIColor(rgb: 0xd2d2d4) + theme.highlightButtonColor = UIColor(rgb: 0xAC39FF) + theme.disabledButtonColor = UIColor.gray + themes[Theme.PrivateMode] = theme + + theme = Theme() + theme.buttonTintColor = UIColor(rgb: 0x272727) + theme.highlightButtonColor = UIColor(rgb: 0x00A2FE) + theme.disabledButtonColor = UIColor.lightGray + themes[Theme.NormalMode] = theme + + return themes + }() + + var selectedTintColor: UIColor! + var unselectedTintColor: UIColor! + var disabledTintColor: UIColor! + + override init(frame: CGRect) { + super.init(frame: frame) + adjustsImageWhenHighlighted = false + selectedTintColor = tintColor + unselectedTintColor = tintColor + disabledTintColor = UIColor.gray + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override open var isHighlighted: Bool { + didSet { + self.tintColor = isHighlighted ? selectedTintColor : unselectedTintColor + } + } + + override open var isEnabled: Bool { + didSet { + self.tintColor = isEnabled ? unselectedTintColor : disabledTintColor + } + } + + override var tintColor: UIColor! { + didSet { + self.imageView?.tintColor = self.tintColor + } + } + +} + +extension ToolbarButton: Themeable { + func applyTheme(_ themeName: String) { + guard let theme = ToolbarButton.Themes[themeName] else { + log.error("Unable to apply unknown theme \(themeName)") + return + } + selectedTintColor = theme.highlightButtonColor + disabledTintColor = theme.disabledButtonColor + unselectedTintColor = theme.buttonTintColor + tintColor = isEnabled ? unselectedTintColor : disabledTintColor + imageView?.tintColor = tintColor + } +} + +class TabToolbar: Toolbar, TabToolbarProtocol { + weak var tabToolbarDelegate: TabToolbarDelegate? + + let tabsButton: TabsButton + let menuButton: ToolbarButton + let forwardButton: ToolbarButton + let backButton: ToolbarButton + let stopReloadButton: ToolbarButton + let actionButtons: [Themeable & UIButton] + + var helper: TabToolbarHelper? + + static let Themes: [String: Theme] = { + var themes = [String: Theme]() + var theme = Theme() + theme.backgroundColor = UIColor(rgb: 0x38383D) + themes[Theme.PrivateMode] = theme + + theme = Theme() + theme.backgroundColor = UIConstants.AppBackgroundColor + themes[Theme.NormalMode] = theme + + return themes + }() + + // This has to be here since init() calls it + fileprivate override init(frame: CGRect) { + // And these have to be initialized in here or the compiler will get angry + backButton = ToolbarButton() + backButton.accessibilityIdentifier = "TabToolbar.backButton" + forwardButton = ToolbarButton() + forwardButton.accessibilityIdentifier = "TabToolbar.forwardButton" + stopReloadButton = ToolbarButton() + stopReloadButton.accessibilityIdentifier = "TabToolbar.stopReloadButton" + tabsButton = TabsButton() + tabsButton.accessibilityIdentifier = "TabToolbar.tabsButton" + menuButton = ToolbarButton() + menuButton.accessibilityIdentifier = "TabToolbar.menuButton" + actionButtons = [backButton, forwardButton, stopReloadButton, tabsButton, menuButton] + + super.init(frame: frame) + + helper = TabToolbarHelper(toolbar: self) + addButtons(actionButtons) + + accessibilityNavigationStyle = .combined + accessibilityLabel = NSLocalizedString("Navigation Toolbar", comment: "Accessibility label for the navigation toolbar displayed at the bottom of the screen.") + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func updateBackStatus(_ canGoBack: Bool) { + backButton.isEnabled = canGoBack + } + + func updateForwardStatus(_ canGoForward: Bool) { + forwardButton.isEnabled = canGoForward + } + + func updateReloadStatus(_ isLoading: Bool) { + helper?.updateReloadStatus(isLoading) + } + + func updatePageStatus(_ isWebPage: Bool) { + stopReloadButton.isEnabled = isWebPage + } + + func updateTabCount(_ count: Int, animated: Bool) { + tabsButton.updateTabCount(count, animated: animated) + } + + override func draw(_ rect: CGRect) { + if let context = UIGraphicsGetCurrentContext() { + drawLine(context, start: CGPoint(x: 0, y: 0), end: CGPoint(x: frame.width, y: 0)) + } + } + + fileprivate func drawLine(_ context: CGContext, start: CGPoint, end: CGPoint) { + context.setStrokeColor(UIColor.black.withAlphaComponent(0.05).cgColor) + context.setLineWidth(2) + context.move(to: CGPoint(x: start.x, y: start.y)) + context.addLine(to: CGPoint(x: end.x, y: end.y)) + context.strokePath() + } +} + +extension TabToolbar: Themeable { + func applyTheme(_ themeName: String) { + guard let theme = TabToolbar.Themes[themeName] else { + log.error("Unable to apply unknown theme \(themeName)") + return + } + backgroundColor = theme.backgroundColor! + helper?.setTheme(theme: themeName, forButtons: actionButtons) + } +} diff --git a/mobile/ios/Client/Frontend/Browser/TabTrayButtonExtensions.swift b/mobile/ios/Client/Frontend/Browser/TabTrayButtonExtensions.swift new file mode 100644 index 0000000000..6960eb210c --- /dev/null +++ b/mobile/ios/Client/Frontend/Browser/TabTrayButtonExtensions.swift @@ -0,0 +1,46 @@ +/* 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 UIKit + +class PrivateModeButton: ToggleButton { + var light: Bool = false + + override init(frame: CGRect) { + super.init(frame: frame) + self.accessibilityLabel = PrivateModeStrings.toggleAccessibilityLabel + self.accessibilityHint = PrivateModeStrings.toggleAccessibilityHint + let maskImage = UIImage(named: "smallPrivateMask")?.withRenderingMode(.alwaysTemplate) + self.setImage(maskImage, for: UIControlState()) + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func styleForMode(privateMode isPrivate: Bool) { + self.tintColor = isPrivate ? UIColor(rgb: 0xf9f9fa) : UIColor(rgb: 0x272727) + self.imageView?.tintColor = self.tintColor + self.isSelected = isPrivate + self.accessibilityValue = isPrivate ? PrivateModeStrings.toggleAccessibilityValueOn : PrivateModeStrings.toggleAccessibilityValueOff + } +} + +extension UIButton { + static func newTabButton() -> UIButton { + let newTab = UIButton() + newTab.setImage(UIImage.templateImageNamed("quick_action_new_tab"), for: .normal) + newTab.accessibilityLabel = NSLocalizedString("New Tab", comment: "Accessibility label for the New Tab button in the tab toolbar.") + return newTab + } +} + +extension TabsButton { + static func tabTrayButton() -> TabsButton { + let tabsButton = TabsButton() + tabsButton.countLabel.text = "0" + tabsButton.accessibilityLabel = NSLocalizedString("Show Tabs", comment: "Accessibility Label for the tabs button in the tab toolbar") + return tabsButton + } +} diff --git a/mobile/ios/Client/Frontend/Browser/TabTrayController.swift b/mobile/ios/Client/Frontend/Browser/TabTrayController.swift new file mode 100644 index 0000000000..10d32be686 --- /dev/null +++ b/mobile/ios/Client/Frontend/Browser/TabTrayController.swift @@ -0,0 +1,1061 @@ +/* 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 UIKit +import SnapKit +import Storage +import ReadingList +import Shared + +struct TabTrayControllerUX { + static let CornerRadius = CGFloat(6.0) + static let BackgroundColor = UIConstants.TabTrayBG + static let CellBackgroundColor = UIConstants.TabTrayBG + static let TextBoxHeight = CGFloat(32.0) + static let FaviconSize = CGFloat(20) + static let Margin = CGFloat(15) + static let ToolbarBarTintColor = UIColor.black + static let ToolbarButtonOffset = CGFloat(10.0) + static let CloseButtonSize = CGFloat(18.0) + static let CloseButtonMargin = CGFloat(6.0) + static let CloseButtonEdgeInset = CGFloat(10) + + static let NumberOfColumnsThin = 1 + static let NumberOfColumnsWide = 3 + static let CompactNumberOfColumnsThin = 2 + + static let MenuFixedWidth: CGFloat = 320 +} + +struct LightTabCellUX { + static let TabTitleTextColor = UIColor.black +} + +struct DarkTabCellUX { + static let TabTitleTextColor = UIColor.white +} + +protocol TabCellDelegate: class { + func tabCellDidClose(_ cell: TabCell) +} + +class TabCell: UICollectionViewCell { + enum Style { + case light + case dark + } + + static let Identifier = "TabCellIdentifier" + static let BorderWidth: CGFloat = 3 + + var style: Style = .light { + didSet { + applyStyle(style) + } + } + + let backgroundHolder = UIView() + let screenshotView = UIImageViewAligned() + let titleText: UILabel + let favicon: UIImageView = UIImageView() + let closeButton: UIButton + + var title: UIVisualEffectView! + var animator: SwipeAnimator! + + weak var delegate: TabCellDelegate? + + // Changes depending on whether we're full-screen or not. + var margin = CGFloat(0) + + override init(frame: CGRect) { + self.backgroundHolder.backgroundColor = UIColor.white + self.backgroundHolder.layer.cornerRadius = TabTrayControllerUX.CornerRadius + self.backgroundHolder.clipsToBounds = true + self.backgroundHolder.backgroundColor = TabTrayControllerUX.CellBackgroundColor + + self.screenshotView.contentMode = UIViewContentMode.scaleAspectFill + self.screenshotView.clipsToBounds = true + self.screenshotView.isUserInteractionEnabled = false + self.screenshotView.alignLeft = true + self.screenshotView.alignTop = true + screenshotView.backgroundColor = UIConstants.AppBackgroundColor + + self.favicon.backgroundColor = UIColor.clear + self.favicon.layer.cornerRadius = 2.0 + self.favicon.layer.masksToBounds = true + + self.titleText = UILabel() + self.titleText.textAlignment = NSTextAlignment.left + self.titleText.isUserInteractionEnabled = false + self.titleText.numberOfLines = 1 + self.titleText.font = DynamicFontHelper.defaultHelper.DefaultSmallFontBold + + self.closeButton = UIButton() + self.closeButton.setImage(UIImage.templateImageNamed("nav-stop"), for: UIControlState()) + self.closeButton.tintColor = UIColor.lightGray + self.closeButton.imageEdgeInsets = UIEdgeInsets(equalInset: TabTrayControllerUX.CloseButtonEdgeInset) + + super.init(frame: frame) + + self.animator = SwipeAnimator(animatingView: self) + self.closeButton.addTarget(self, action: #selector(TabCell.close), for: UIControlEvents.touchUpInside) + + contentView.addSubview(backgroundHolder) + backgroundHolder.addSubview(self.screenshotView) + + // Default style is light + applyStyle(style) + + self.accessibilityCustomActions = [ + UIAccessibilityCustomAction(name: NSLocalizedString("Close", comment: "Accessibility label for action denoting closing a tab in tab list (tray)"), target: self.animator, selector: #selector(SwipeAnimator.closeWithoutGesture)) + ] + } + + fileprivate func applyStyle(_ style: Style) { + self.title?.removeFromSuperview() + + let title: UIVisualEffectView + switch style { + case .light: + title = UIVisualEffectView(effect: UIBlurEffect(style: .extraLight)) + self.titleText.textColor = LightTabCellUX.TabTitleTextColor + case .dark: + title = UIVisualEffectView(effect: UIBlurEffect(style: .dark)) + self.titleText.textColor = DarkTabCellUX.TabTitleTextColor + } + + titleText.backgroundColor = UIColor.clear + + title.contentView.addSubview(self.closeButton) + title.contentView.addSubview(self.titleText) + title.contentView.addSubview(self.favicon) + + backgroundHolder.addSubview(title) + self.title = title + } + + func setTabSelected(_ isPrivate: Bool) { + // This creates a border around a tabcell. Using the shadow craetes a border _outside_ of the tab frame. + layer.shadowColor = (isPrivate ? UIConstants.PrivateModePurple : UIConstants.SystemBlueColor).cgColor + layer.shadowOpacity = 1 + layer.shadowRadius = 0 // A 0 radius creates a solid border instead of a gradient blur + layer.masksToBounds = false + // create a frame that is "BorderWidth" size bigger than the cell + layer.shadowOffset = CGSize(width: -TabCell.BorderWidth, height: -TabCell.BorderWidth) + let shadowPath = CGRect(x: 0, y: 0, width: layer.frame.width + (TabCell.BorderWidth * 2), height: layer.frame.height + (TabCell.BorderWidth * 2)) + layer.shadowPath = UIBezierPath(roundedRect: shadowPath, cornerRadius: TabTrayControllerUX.CornerRadius+TabCell.BorderWidth).cgPath + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func layoutSubviews() { + super.layoutSubviews() + + let w = frame.width + let h = frame.height + backgroundHolder.frame = CGRect(x: margin, + y: margin, + width: w, + height: h) + screenshotView.frame = CGRect(origin: CGPoint(x: 0, y: 0), size: backgroundHolder.frame.size) + + title.frame = CGRect(x: 0, + y: 0, + width: backgroundHolder.frame.width, + height: TabTrayControllerUX.TextBoxHeight) + + favicon.frame = CGRect(x: 6, + y: (TabTrayControllerUX.TextBoxHeight - TabTrayControllerUX.FaviconSize)/2, + width: TabTrayControllerUX.FaviconSize, + height: TabTrayControllerUX.FaviconSize) + + let titleTextLeft = favicon.frame.origin.x + favicon.frame.width + 6 + titleText.frame = CGRect(x: titleTextLeft, + y: 0, + width: title.frame.width - titleTextLeft - margin - TabTrayControllerUX.CloseButtonSize - TabTrayControllerUX.CloseButtonMargin * 2, + height: title.frame.height) + + closeButton.snp.makeConstraints { make in + make.size.equalTo(title.snp.height) + make.trailing.centerY.equalTo(title) + } + + let top = (TabTrayControllerUX.TextBoxHeight - titleText.bounds.height) / 2.0 + titleText.frame.origin = CGPoint(x: titleText.frame.origin.x, y: max(0, top)) + let shadowPath = CGRect(x: 0, y: 0, width: layer.frame.width + (TabCell.BorderWidth * 2), height: layer.frame.height + (TabCell.BorderWidth * 2)) + layer.shadowPath = UIBezierPath(roundedRect: shadowPath, cornerRadius: TabTrayControllerUX.CornerRadius+TabCell.BorderWidth).cgPath + } + + override func prepareForReuse() { + // Reset any close animations. + backgroundHolder.transform = CGAffineTransform.identity + backgroundHolder.alpha = 1 + self.titleText.font = DynamicFontHelper.defaultHelper.DefaultSmallFontBold + layer.shadowOffset = CGSize.zero + layer.shadowPath = nil + layer.shadowOpacity = 0 + } + + override func accessibilityScroll(_ direction: UIAccessibilityScrollDirection) -> Bool { + var right: Bool + switch direction { + case .left: + right = false + case .right: + right = true + default: + return false + } + animator.close(right: right) + return true + } + + @objc + func close() { + self.animator.closeWithoutGesture() + } +} + +struct PrivateModeStrings { + static let toggleAccessibilityLabel = NSLocalizedString("Private Mode", tableName: "PrivateBrowsing", comment: "Accessibility label for toggling on/off private mode") + static let toggleAccessibilityHint = NSLocalizedString("Turns private mode on or off", tableName: "PrivateBrowsing", comment: "Accessiblity hint for toggling on/off private mode") + static let toggleAccessibilityValueOn = NSLocalizedString("On", tableName: "PrivateBrowsing", comment: "Toggled ON accessibility value") + static let toggleAccessibilityValueOff = NSLocalizedString("Off", tableName: "PrivateBrowsing", comment: "Toggled OFF accessibility value") +} + +protocol TabTrayDelegate: class { + func tabTrayDidDismiss(_ tabTray: TabTrayController) + func tabTrayDidAddBookmark(_ tab: Tab) + func tabTrayDidAddToReadingList(_ tab: Tab) -> ReadingListClientRecord? + func tabTrayRequestsPresentationOf(_ viewController: UIViewController) +} + +class TabTrayController: UIViewController { + let tabManager: TabManager + let profile: Profile + weak var delegate: TabTrayDelegate? + + var collectionView: UICollectionView! + var draggedCell: TabCell? + var dragOffset: CGPoint = CGPoint.zero + lazy var toolbar: TrayToolbar = { + let toolbar = TrayToolbar() + toolbar.addTabButton.addTarget(self, action: #selector(TabTrayController.didClickAddTab), for: .touchUpInside) + toolbar.maskButton.addTarget(self, action: #selector(TabTrayController.didTogglePrivateMode), for: .touchUpInside) + toolbar.deleteButton.addTarget(self, action: #selector(TabTrayController.didTapDelete(_:)), for: .touchUpInside) + return toolbar + }() + + fileprivate(set) internal var privateMode: Bool = false { + didSet { + tabDataSource.tabs = tabsToDisplay + toolbar.styleToolbar(privateMode) + collectionView?.reloadData() + } + } + + fileprivate var tabsToDisplay: [Tab] { + return self.privateMode ? tabManager.privateTabs : tabManager.normalTabs + } + + fileprivate lazy var emptyPrivateTabsView: EmptyPrivateTabsView = { + let emptyView = EmptyPrivateTabsView() + emptyView.learnMoreButton.addTarget(self, action: #selector(TabTrayController.didTapLearnMore), for: UIControlEvents.touchUpInside) + return emptyView + }() + + fileprivate lazy var tabDataSource: TabManagerDataSource = { + return TabManagerDataSource(tabs: self.tabsToDisplay, cellDelegate: self, tabManager: self.tabManager) + }() + + fileprivate lazy var tabLayoutDelegate: TabLayoutDelegate = { + let delegate = TabLayoutDelegate(profile: self.profile, traitCollection: self.traitCollection) + delegate.tabSelectionDelegate = self + return delegate + }() + + init(tabManager: TabManager, profile: Profile) { + self.tabManager = tabManager + self.profile = profile + super.init(nibName: nil, bundle: nil) + + tabManager.addDelegate(self) + } + + convenience init(tabManager: TabManager, profile: Profile, tabTrayDelegate: TabTrayDelegate) { + self.init(tabManager: tabManager, profile: profile) + self.delegate = tabTrayDelegate + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + self.tabManager.removeDelegate(self) + } + + func dynamicFontChanged(_ notification: Notification) { + guard notification.name == NotificationDynamicFontChanged else { return } + + self.collectionView.reloadData() + } + +// MARK: View Controller Callbacks + override func viewDidLoad() { + super.viewDidLoad() + + view.accessibilityLabel = NSLocalizedString("Tabs Tray", comment: "Accessibility label for the Tabs Tray view.") + + collectionView = UICollectionView(frame: view.frame, collectionViewLayout: UICollectionViewFlowLayout()) + + collectionView.dataSource = tabDataSource + collectionView.delegate = tabLayoutDelegate + collectionView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: UIConstants.BottomToolbarHeight, right: 0) + collectionView.register(TabCell.self, forCellWithReuseIdentifier: TabCell.Identifier) + collectionView.backgroundColor = TabTrayControllerUX.BackgroundColor + + view.addSubview(collectionView) + view.addSubview(toolbar) + + makeConstraints() + + view.insertSubview(emptyPrivateTabsView, aboveSubview: collectionView) + emptyPrivateTabsView.snp.makeConstraints { make in + make.top.left.right.equalTo(self.collectionView) + make.bottom.equalTo(self.toolbar.snp.top) + } + + if let tab = tabManager.selectedTab, tab.isPrivate { + privateMode = true + } + + // register for previewing delegate to enable peek and pop if force touch feature available + if traitCollection.forceTouchCapability == .available { + registerForPreviewing(with: self, sourceView: view) + } + + emptyPrivateTabsView.isHidden = !privateTabsAreEmpty() + + NotificationCenter.default.addObserver(self, selector: #selector(TabTrayController.appWillResignActiveNotification), name: NSNotification.Name.UIApplicationWillResignActive, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(TabTrayController.appDidBecomeActiveNotification), name: NSNotification.Name.UIApplicationDidBecomeActive, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(TabTrayController.dynamicFontChanged(_:)), name: NotificationDynamicFontChanged, object: nil) + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + } + + override func viewDidDisappear(_ animated: Bool) { + super.viewDidDisappear(animated) + } + + override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { + super.traitCollectionDidChange(previousTraitCollection) + + // Update the trait collection we reference in our layout delegate + tabLayoutDelegate.traitCollection = traitCollection + self.collectionView.collectionViewLayout.invalidateLayout() + } + + fileprivate func cancelExistingGestures() { + if let visibleCells = self.collectionView.visibleCells as? [TabCell] { + for cell in visibleCells { + cell.animator.cancelExistingGestures() + } + } + } + + override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { + super.viewWillTransition(to: size, with: coordinator) + coordinator.animate(alongsideTransition: { _ in + self.collectionView.collectionViewLayout.invalidateLayout() + }, completion: nil) + } + + override var preferredStatusBarStyle: UIStatusBarStyle { + return UIStatusBarStyle.lightContent //this will need to be fixed + } + + fileprivate func makeConstraints() { + collectionView.snp.makeConstraints { make in + make.left.bottom.right.equalTo(view) + make.top.equalTo(self.topLayoutGuide.snp.bottom) + } + + toolbar.snp.makeConstraints { make in + make.left.right.bottom.equalTo(view) + make.height.equalTo(UIConstants.BottomToolbarHeight) + } + } + +// MARK: Selectors + func didClickDone() { + presentingViewController!.dismiss(animated: true, completion: nil) + } + + func didClickSettingsItem() { + assert(Thread.isMainThread, "Opening settings requires being invoked on the main thread") + + let settingsTableViewController = AppSettingsTableViewController() + settingsTableViewController.profile = profile + settingsTableViewController.tabManager = tabManager + settingsTableViewController.settingsDelegate = self + + let controller = SettingsNavigationController(rootViewController: settingsTableViewController) + controller.popoverDelegate = self + controller.modalPresentationStyle = UIModalPresentationStyle.formSheet + present(controller, animated: true, completion: nil) + } + + func didClickAddTab() { + openNewTab() + LeanPlumClient.shared.track(event: .openedNewTab, withParameters: ["Source": "Tab Tray" as AnyObject]) + } + + func didTapLearnMore() { + let appVersion = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as! String + if let langID = Locale.preferredLanguages.first { + let learnMoreRequest = URLRequest(url: "https://support.mozilla.org/1/mobile/\(appVersion)/iOS/\(langID)/private-browsing-ios".asURL!) + openNewTab(learnMoreRequest) + } + } + + func didTogglePrivateMode() { + let scaleDownTransform = CGAffineTransform(scaleX: 0.9, y: 0.9) + + let fromView: UIView + if !privateTabsAreEmpty(), let snapshot = collectionView.snapshotView(afterScreenUpdates: false) { + snapshot.frame = collectionView.frame + view.insertSubview(snapshot, aboveSubview: collectionView) + fromView = snapshot + } else { + fromView = emptyPrivateTabsView + } + + tabManager.willSwitchTabMode() + privateMode = !privateMode + // If we are exiting private mode and we have the close private tabs option selected, make sure + // we clear out all of the private tabs + let exitingPrivateMode = !privateMode && tabManager.shouldClearPrivateTabs() + + toolbar.maskButton.setSelected(privateMode, animated: true) + collectionView.layoutSubviews() + + let toView: UIView + if !privateTabsAreEmpty(), let newSnapshot = collectionView.snapshotView(afterScreenUpdates: !exitingPrivateMode) { + emptyPrivateTabsView.isHidden = true + //when exiting private mode don't screenshot the collectionview (causes the UI to hang) + newSnapshot.frame = collectionView.frame + view.insertSubview(newSnapshot, aboveSubview: fromView) + collectionView.alpha = 0 + toView = newSnapshot + } else { + emptyPrivateTabsView.isHidden = false + toView = emptyPrivateTabsView + } + toView.alpha = 0 + toView.transform = scaleDownTransform + + UIView.animate(withDuration: 0.2, delay: 0, options: [], animations: { () -> Void in + fromView.transform = scaleDownTransform + fromView.alpha = 0 + toView.transform = CGAffineTransform.identity + toView.alpha = 1 + }) { finished in + if fromView != self.emptyPrivateTabsView { + fromView.removeFromSuperview() + } + if toView != self.emptyPrivateTabsView { + toView.removeFromSuperview() + } + self.collectionView.alpha = 1 + } + } + + fileprivate func privateTabsAreEmpty() -> Bool { + return privateMode && tabManager.privateTabs.count == 0 + } + + func changePrivacyMode(_ isPrivate: Bool) { + if isPrivate != privateMode { + guard let _ = collectionView else { + privateMode = isPrivate + return + } + didTogglePrivateMode() + } + } + + fileprivate func openNewTab(_ request: URLRequest? = nil) { + toolbar.isUserInteractionEnabled = false + + // We're only doing one update here, but using a batch update lets us delay selecting the tab + // until after its insert animation finishes. + self.collectionView.performBatchUpdates({ _ in + _ = self.tabManager.addTab(request, isPrivate: self.privateMode) + }, completion: { finished in + // The addTab delegate method will pop to the BVC no need to do anything here. + self.toolbar.isUserInteractionEnabled = true + if finished { + if request == nil && NewTabAccessors.getNewTabPage(self.profile.prefs) == .blankPage { + if let bvc = self.navigationController?.topViewController as? BrowserViewController { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.6) { + bvc.urlBar.tabLocationViewDidTapLocation(bvc.urlBar.locationView) + } + } + } + } + }) + } + + fileprivate func closeTabsForCurrentTray() { + tabManager.removeTabsWithUndoToast(tabsToDisplay) + self.collectionView.reloadData() + } +} + +// MARK: - App Notifications +extension TabTrayController { + func appWillResignActiveNotification() { + if privateMode { + collectionView.alpha = 0 + } + } + + func appDidBecomeActiveNotification() { + // Re-show any components that might have been hidden because they were being displayed + // as part of a private mode tab + UIView.animate(withDuration: 0.2, delay: 0, options: UIViewAnimationOptions(), animations: { + self.collectionView.alpha = 1 + }, + completion: nil) + } +} + +extension TabTrayController: TabSelectionDelegate { + func didSelectTabAtIndex(_ index: Int) { + let tab = tabsToDisplay[index] + tabManager.selectTab(tab) + _ = self.navigationController?.popViewController(animated: true) + } +} + +extension TabTrayController: PresentingModalViewControllerDelegate { + func dismissPresentedModalViewController(_ modalViewController: UIViewController, animated: Bool) { + dismiss(animated: animated, completion: { self.collectionView.reloadData() }) + } +} + +extension TabTrayController: TabManagerDelegate { + func tabManager(_ tabManager: TabManager, didSelectedTabChange selected: Tab?, previous: Tab?) { + } + + func tabManager(_ tabManager: TabManager, willAddTab tab: Tab) { + } + + func tabManager(_ tabManager: TabManager, willRemoveTab tab: Tab) { + } + + func tabManager(_ tabManager: TabManager, didAddTab tab: Tab) { + // Get the index of the added tab from it's set (private or normal) + guard let index = tabsToDisplay.index(of: tab) else { return } + if !privateTabsAreEmpty() { + emptyPrivateTabsView.isHidden = true + } + + tabDataSource.addTab(tab) + self.collectionView?.performBatchUpdates({ _ in + self.collectionView.insertItems(at: [IndexPath(item: index, section: 0)]) + }, completion: { finished in + if finished { + tabManager.selectTab(tab) + // don't pop the tab tray view controller if it is not in the foreground + if self.presentedViewController == nil { + _ = self.navigationController?.popViewController(animated: true) + } + } + }) + } + + func tabManager(_ tabManager: TabManager, didRemoveTab tab: Tab) { + // it is possible that we are removing a tab that we are not currently displaying + // through the Close All Tabs feature (which will close tabs that are not in our current privacy mode) + // check this before removing the item from the collection + let removedIndex = tabDataSource.removeTab(tab) + if removedIndex > -1 { + self.collectionView.performBatchUpdates({ + self.collectionView.deleteItems(at: [IndexPath(item: removedIndex, section: 0)]) + }, completion: { finished in + guard finished && self.privateTabsAreEmpty() else { return } + self.emptyPrivateTabsView.isHidden = false + }) + } + } + + func tabManagerDidAddTabs(_ tabManager: TabManager) { + } + + func tabManagerDidRestoreTabs(_ tabManager: TabManager) { + } + + func tabManagerDidRemoveAllTabs(_ tabManager: TabManager, toast: ButtonToast?) { + guard privateMode else { + return + } + + if let toast = toast { + view.addSubview(toast) + toast.snp.makeConstraints { make in + make.left.right.equalTo(view) + make.bottom.equalTo(toolbar.snp.top) + } + toast.showToast() + } + } +} + +extension TabTrayController: UIScrollViewAccessibilityDelegate { + func accessibilityScrollStatus(for scrollView: UIScrollView) -> String? { + var visibleCells = collectionView.visibleCells as! [TabCell] + var bounds = collectionView.bounds + bounds = bounds.offsetBy(dx: collectionView.contentInset.left, dy: collectionView.contentInset.top) + bounds.size.width -= collectionView.contentInset.left + collectionView.contentInset.right + bounds.size.height -= collectionView.contentInset.top + collectionView.contentInset.bottom + // visible cells do sometimes return also not visible cells when attempting to go past the last cell with VoiceOver right-flick gesture; so make sure we have only visible cells (yeah...) + visibleCells = visibleCells.filter { !$0.frame.intersection(bounds).isEmpty } + + let cells = visibleCells.map { self.collectionView.indexPath(for: $0)! } + let indexPaths = cells.sorted { (a: IndexPath, b: IndexPath) -> Bool in + return a.section < b.section || (a.section == b.section && a.row < b.row) + } + + if indexPaths.count == 0 { + return NSLocalizedString("No tabs", comment: "Message spoken by VoiceOver to indicate that there are no tabs in the Tabs Tray") + } + + let firstTab = indexPaths.first!.row + 1 + let lastTab = indexPaths.last!.row + 1 + let tabCount = collectionView.numberOfItems(inSection: 0) + + if firstTab == lastTab { + let format = NSLocalizedString("Tab %@ of %@", comment: "Message spoken by VoiceOver saying the position of the single currently visible tab in Tabs Tray, along with the total number of tabs. E.g. \"Tab 2 of 5\" says that tab 2 is visible (and is the only visible tab), out of 5 tabs total.") + return String(format: format, NSNumber(value: firstTab as Int), NSNumber(value: tabCount as Int)) + } else { + let format = NSLocalizedString("Tabs %@ to %@ of %@", comment: "Message spoken by VoiceOver saying the range of tabs that are currently visible in Tabs Tray, along with the total number of tabs. E.g. \"Tabs 8 to 10 of 15\" says tabs 8, 9 and 10 are visible, out of 15 tabs total.") + return String(format: format, NSNumber(value: firstTab as Int), NSNumber(value: lastTab as Int), NSNumber(value: tabCount as Int)) + } + } +} + +extension TabTrayController: SwipeAnimatorDelegate { + func swipeAnimator(_ animator: SwipeAnimator, viewWillExitContainerBounds: UIView) { + let tabCell = animator.animatingView as! TabCell + if let indexPath = collectionView.indexPath(for: tabCell) { + let tab = tabsToDisplay[indexPath.item] + tabManager.removeTab(tab) + UIAccessibilityPostNotification(UIAccessibilityAnnouncementNotification, NSLocalizedString("Closing tab", comment: "Accessibility label (used by assistive technology) notifying the user that the tab is being closed.")) + } + } +} + +extension TabTrayController: TabCellDelegate { + func tabCellDidClose(_ cell: TabCell) { + let indexPath = collectionView.indexPath(for: cell)! + let tab = tabsToDisplay[indexPath.item] + tabManager.removeTab(tab) + } +} + +extension TabTrayController: SettingsDelegate { + func settingsOpenURLInNewTab(_ url: URL) { + let request = URLRequest(url: url) + openNewTab(request) + } +} + +extension TabTrayController: PhotonActionSheetProtocol { + func didTapDelete(_ sender: UIButton) { + let controller = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet) + controller.addAction(UIAlertAction(title: Strings.AppMenuCloseAllTabsTitleString, style: .default, handler: { _ in self.closeTabsForCurrentTray() })) + controller.addAction(UIAlertAction(title: NSLocalizedString("Cancel", comment: "Label for Cancel button"), style: .cancel, handler: nil)) + controller.popoverPresentationController?.sourceView = sender + controller.popoverPresentationController?.sourceRect = sender.bounds + present(controller, animated: true, completion: nil) + } +} + +fileprivate class TabManagerDataSource: NSObject, UICollectionViewDataSource { + unowned var cellDelegate: TabCellDelegate & SwipeAnimatorDelegate + fileprivate var tabs: [Tab] + fileprivate var tabManager: TabManager + + init(tabs: [Tab], cellDelegate: TabCellDelegate & SwipeAnimatorDelegate, tabManager: TabManager) { + self.cellDelegate = cellDelegate + self.tabs = tabs + self.tabManager = tabManager + super.init() + } + + /** + Removes the given tab from the data source + + - parameter tab: Tab to remove + + - returns: The index of the removed tab, -1 if tab did not exist + */ + func removeTab(_ tabToRemove: Tab) -> Int { + var index: Int = -1 + for (i, tab) in tabs.enumerated() where tabToRemove === tab { + index = i + tabs.remove(at: index) + break + } + return index + } + + /** + Adds the given tab to the data source + + - parameter tab: Tab to add + */ + func addTab(_ tab: Tab) { + tabs.append(tab) + } + + @objc func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { + let tabCell = collectionView.dequeueReusableCell(withReuseIdentifier: TabCell.Identifier, for: indexPath) as! TabCell + tabCell.animator.delegate = cellDelegate + tabCell.delegate = cellDelegate + + let tab = tabs[indexPath.item] + tabCell.style = tab.isPrivate ? .dark : .light + tabCell.titleText.text = tab.displayTitle + tabCell.closeButton.tintColor = tab.isPrivate ? UIColor.white : UIColor.gray + + if !tab.displayTitle.isEmpty { + tabCell.accessibilityLabel = tab.displayTitle + } else { + tabCell.accessibilityLabel = tab.url?.aboutComponent ?? "" // If there is no title we are most likely on a home panel. + } + tabCell.isAccessibilityElement = true + tabCell.accessibilityHint = NSLocalizedString("Swipe right or left with three fingers to close the tab.", comment: "Accessibility hint for tab tray's displayed tab.") + + if let favIcon = tab.displayFavicon { + tabCell.favicon.sd_setImage(with: URL(string: favIcon.url)!) + } else { + let defaultFavicon = UIImage(named: "defaultFavicon") + if tab.isPrivate { + tabCell.favicon.image = defaultFavicon + tabCell.favicon.tintColor = UIColor.white + } else { + tabCell.favicon.image = defaultFavicon + } + } + if tab == tabManager.selectedTab { + tabCell.setTabSelected(tab.isPrivate) + } + tabCell.screenshotView.image = tab.screenshot + return tabCell + } + + @objc func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { + return tabs.count + } + + @objc fileprivate func collectionView(_ collectionView: UICollectionView, moveItemAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) { + let fromIndex = sourceIndexPath.item + let toIndex = destinationIndexPath.item + tabs.insert(tabs.remove(at: fromIndex), at: toIndex < fromIndex ? toIndex : toIndex - 1) + tabManager.moveTab(isPrivate: tabs[fromIndex].isPrivate, fromIndex: fromIndex, toIndex: toIndex) + } +} + +@objc protocol TabSelectionDelegate: class { + func didSelectTabAtIndex(_ index: Int) +} + +fileprivate class TabLayoutDelegate: NSObject, UICollectionViewDelegateFlowLayout { + weak var tabSelectionDelegate: TabSelectionDelegate? + + fileprivate var traitCollection: UITraitCollection + fileprivate var profile: Profile + fileprivate var numberOfColumns: Int { + // iPhone 4-6+ portrait + if traitCollection.horizontalSizeClass == .compact && traitCollection.verticalSizeClass == .regular { + return TabTrayControllerUX.CompactNumberOfColumnsThin + } else { + return TabTrayControllerUX.NumberOfColumnsWide + } + } + + init(profile: Profile, traitCollection: UITraitCollection) { + self.profile = profile + self.traitCollection = traitCollection + super.init() + } + + fileprivate func cellHeightForCurrentDevice() -> CGFloat { + let shortHeight = TabTrayControllerUX.TextBoxHeight * 6 + + if self.traitCollection.verticalSizeClass == UIUserInterfaceSizeClass.compact { + return shortHeight + } else if self.traitCollection.horizontalSizeClass == UIUserInterfaceSizeClass.compact { + return shortHeight + } else { + return TabTrayControllerUX.TextBoxHeight * 8 + } + } + + @objc func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat { + return TabTrayControllerUX.Margin + } + + @objc func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { + let cellWidth = floor((collectionView.bounds.width - TabTrayControllerUX.Margin * CGFloat(numberOfColumns + 1)) / CGFloat(numberOfColumns)) + return CGSize(width: cellWidth, height: self.cellHeightForCurrentDevice()) + } + + @objc func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets { + return UIEdgeInsets(equalInset: TabTrayControllerUX.Margin) + } + + @objc func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat { + return TabTrayControllerUX.Margin + } + + @objc func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { + tabSelectionDelegate?.didSelectTabAtIndex(indexPath.row) + } +} + +struct EmptyPrivateTabsViewUX { + static let TitleColor = UIColor.white + static let TitleFont = UIFont.systemFont(ofSize: 22, weight: UIFontWeightMedium) + static let DescriptionColor = UIColor.white + static let DescriptionFont = UIFont.systemFont(ofSize: 17) + static let LearnMoreFont = UIFont.systemFont(ofSize: 15, weight: UIFontWeightMedium) + static let TextMargin: CGFloat = 18 + static let LearnMoreMargin: CGFloat = 30 + static let MaxDescriptionWidth: CGFloat = 250 + static let MinBottomMargin: CGFloat = 10 +} + +// View we display when there are no private tabs created +fileprivate class EmptyPrivateTabsView: UIView { + fileprivate lazy var titleLabel: UILabel = { + let label = UILabel() + label.textColor = EmptyPrivateTabsViewUX.TitleColor + label.font = EmptyPrivateTabsViewUX.TitleFont + label.textAlignment = NSTextAlignment.center + return label + }() + + fileprivate var descriptionLabel: UILabel = { + let label = UILabel() + label.textColor = EmptyPrivateTabsViewUX.DescriptionColor + label.font = EmptyPrivateTabsViewUX.DescriptionFont + label.textAlignment = NSTextAlignment.center + label.numberOfLines = 0 + label.preferredMaxLayoutWidth = EmptyPrivateTabsViewUX.MaxDescriptionWidth + return label + }() + + fileprivate var learnMoreButton: UIButton = { + let button = UIButton(type: .system) + button.setTitle( + NSLocalizedString("Learn More", tableName: "PrivateBrowsing", comment: "Text button displayed when there are no tabs open while in private mode"), + for: UIControlState()) + button.setTitleColor(UIConstants.PrivateModeTextHighlightColor, for: UIControlState()) + button.titleLabel?.font = EmptyPrivateTabsViewUX.LearnMoreFont + return button + }() + + fileprivate var iconImageView: UIImageView = { + let imageView = UIImageView(image: UIImage(named: "largePrivateMask")) + return imageView + }() + + override init(frame: CGRect) { + super.init(frame: frame) + + titleLabel.text = NSLocalizedString("Private Browsing", + tableName: "PrivateBrowsing", comment: "Title displayed for when there are no open tabs while in private mode") + descriptionLabel.text = NSLocalizedString("Firefox won’t remember any of your history or cookies, but new bookmarks will be saved.", + tableName: "PrivateBrowsing", comment: "Description text displayed when there are no open tabs while in private mode") + + addSubview(titleLabel) + addSubview(descriptionLabel) + addSubview(iconImageView) + addSubview(learnMoreButton) + + titleLabel.snp.makeConstraints { make in + make.center.equalTo(self) + } + + iconImageView.snp.makeConstraints { make in + make.bottom.equalTo(titleLabel.snp.top).offset(-EmptyPrivateTabsViewUX.TextMargin) + make.centerX.equalTo(self) + } + + descriptionLabel.snp.makeConstraints { make in + make.top.equalTo(titleLabel.snp.bottom).offset(EmptyPrivateTabsViewUX.TextMargin) + make.centerX.equalTo(self) + } + + learnMoreButton.snp.makeConstraints { (make) -> Void in + make.top.equalTo(descriptionLabel.snp.bottom).offset(EmptyPrivateTabsViewUX.LearnMoreMargin).priority(10) + make.bottom.lessThanOrEqualTo(self).offset(-EmptyPrivateTabsViewUX.MinBottomMargin).priority(1000) + make.centerX.equalTo(self) + } + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +extension TabTrayController: TabPeekDelegate { + + func tabPeekDidAddBookmark(_ tab: Tab) { + delegate?.tabTrayDidAddBookmark(tab) + } + + func tabPeekDidAddToReadingList(_ tab: Tab) -> ReadingListClientRecord? { + return delegate?.tabTrayDidAddToReadingList(tab) + } + + func tabPeekDidCloseTab(_ tab: Tab) { + if let index = self.tabDataSource.tabs.index(of: tab), + let cell = self.collectionView?.cellForItem(at: IndexPath(item: index, section: 0)) as? TabCell { + cell.close() + } + } + + func tabPeekRequestsPresentationOf(_ viewController: UIViewController) { + delegate?.tabTrayRequestsPresentationOf(viewController) + } +} + +extension TabTrayController: UIViewControllerPreviewingDelegate { + + func previewingContext(_ previewingContext: UIViewControllerPreviewing, viewControllerForLocation location: CGPoint) -> UIViewController? { + + guard let collectionView = collectionView else { return nil } + let convertedLocation = self.view.convert(location, to: collectionView) + + guard let indexPath = collectionView.indexPathForItem(at: convertedLocation), + let cell = collectionView.cellForItem(at: indexPath) else { return nil } + + let tab = tabDataSource.tabs[indexPath.row] + let tabVC = TabPeekViewController(tab: tab, delegate: self) + if let browserProfile = profile as? BrowserProfile { + tabVC.setState(withProfile: browserProfile, clientPickerDelegate: self) + } + previewingContext.sourceRect = self.view.convert(cell.frame, from: collectionView) + + return tabVC + } + + func previewingContext(_ previewingContext: UIViewControllerPreviewing, commit viewControllerToCommit: UIViewController) { + guard let tpvc = viewControllerToCommit as? TabPeekViewController else { return } + tabManager.selectTab(tpvc.tab) + navigationController?.popViewController(animated: true) + delegate?.tabTrayDidDismiss(self) + } +} + +extension TabTrayController: ClientPickerViewControllerDelegate { + + func clientPickerViewController(_ clientPickerViewController: ClientPickerViewController, didPickClients clients: [RemoteClient]) { + if let item = clientPickerViewController.shareItem { + _ = self.profile.sendItems([item], toClients: clients) + } + clientPickerViewController.dismiss(animated: true, completion: nil) + } + + func clientPickerViewControllerDidCancel(_ clientPickerViewController: ClientPickerViewController) { + clientPickerViewController.dismiss(animated: true, completion: nil) + } +} + +extension TabTrayController: UIAdaptivePresentationControllerDelegate, UIPopoverPresentationControllerDelegate { + // Returning None here makes sure that the Popover is actually presented as a Popover and + // not as a full-screen modal, which is the default on compact device classes. + func adaptivePresentationStyle(for controller: UIPresentationController, traitCollection: UITraitCollection) -> UIModalPresentationStyle { + return UIModalPresentationStyle.none + } +} + +// MARK: - Toolbar +class TrayToolbar: UIView { + fileprivate let toolbarButtonSize = CGSize(width: 44, height: 44) + + lazy var addTabButton: UIButton = { + let button = UIButton() + button.setImage(UIImage.templateImageNamed("nav-add"), for: .normal) + button.accessibilityLabel = NSLocalizedString("Add Tab", comment: "Accessibility label for the Add Tab button in the Tab Tray.") + button.accessibilityIdentifier = "TabTrayController.addTabButton" + return button + }() + + lazy var deleteButton: UIButton = { + let button = UIButton() + button.setImage(UIImage.templateImageNamed("action_delete"), for: .normal) + button.accessibilityLabel = Strings.TabTrayDeleteMenuButtonAccessibilityLabel + button.accessibilityIdentifier = "TabTrayController.removeTabsButton" + return button + }() + + lazy var maskButton: PrivateModeButton = PrivateModeButton() + fileprivate let sideOffset: CGFloat = 32 + + fileprivate override init(frame: CGRect) { + super.init(frame: frame) + backgroundColor = .white + addSubview(addTabButton) + + var buttonToCenter: UIButton? + addSubview(deleteButton) + buttonToCenter = deleteButton + + maskButton.accessibilityIdentifier = "TabTrayController.maskButton" + + buttonToCenter?.snp.makeConstraints { make in + make.centerX.equalTo(self) + make.top.equalTo(self) + make.size.equalTo(toolbarButtonSize) + } + + addTabButton.snp.makeConstraints { make in + make.top.equalTo(self) + make.right.equalTo(self).offset(-sideOffset) + make.size.equalTo(toolbarButtonSize) + } + + addSubview(maskButton) + maskButton.snp.makeConstraints { make in + make.top.equalTo(self) + make.left.equalTo(self).offset(sideOffset) + make.size.equalTo(toolbarButtonSize) + } + + styleToolbar(false) + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + fileprivate func styleToolbar(_ isPrivate: Bool) { + addTabButton.tintColor = isPrivate ? UIColor(rgb: 0xf9f9fa) : UIColor(rgb: 0x272727) + deleteButton.tintColor = isPrivate ? UIColor(rgb: 0xf9f9fa) : UIColor(rgb: 0x272727) + backgroundColor = isPrivate ? UIConstants.PrivateModeToolbarTintColor : UIColor(rgb: 0xf9f9fa) + maskButton.styleForMode(privateMode: isPrivate) + } +} diff --git a/mobile/ios/Client/Frontend/Browser/ThirdPartySearchAlerts.swift b/mobile/ios/Client/Frontend/Browser/ThirdPartySearchAlerts.swift new file mode 100644 index 0000000000..1ebcce473d --- /dev/null +++ b/mobile/ios/Client/Frontend/Browser/ThirdPartySearchAlerts.swift @@ -0,0 +1,88 @@ +/* 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 + +class ThirdPartySearchAlerts: UIAlertController { + + /** + Allows the keyboard to pop back up after an alertview. + **/ + override var canBecomeFirstResponder: Bool { + return false + } + + /** + Builds the Alert view that asks if the users wants to add a third party search engine. + + - parameter okayCallback: Okay option handler. + + - returns: UIAlertController for asking the user to add a search engine + **/ + + static func addThirdPartySearchEngine(_ okayCallback: @escaping (UIAlertAction) -> Void) -> UIAlertController { + let alert = ThirdPartySearchAlerts( + title: Strings.ThirdPartySearchAddTitle, + message: Strings.ThirdPartySearchAddMessage, + preferredStyle: UIAlertControllerStyle.alert + ) + + let noOption = UIAlertAction( + title: Strings.ThirdPartySearchCancelButton, + style: UIAlertActionStyle.cancel, + handler: nil + ) + + let okayOption = UIAlertAction( + title: Strings.ThirdPartySearchOkayButton, + style: UIAlertActionStyle.default, + handler: okayCallback + ) + + alert.addAction(okayOption) + alert.addAction(noOption) + + return alert + } + + /** + Builds the Alert view that shows the user an error in case a search engine could not be added. + + - returns: UIAlertController with an error dialog + **/ + + static func failedToAddThirdPartySearch() -> UIAlertController { + return searchAlertWithOK(title: Strings.ThirdPartySearchFailedTitle, + message: Strings.ThirdPartySearchFailedMessage) + } + + static func incorrectCustomEngineForm() -> UIAlertController { + return searchAlertWithOK(title: Strings.CustomEngineFormErrorTitle, + message: Strings.CustomEngineFormErrorMessage) + } + + static func duplicateCustomEngine() -> UIAlertController { + return searchAlertWithOK(title: Strings.CustomEngineDuplicateErrorTitle, + message: Strings.CustomEngineDuplicateErrorMessage) + } + + private static func searchAlertWithOK(title: String, message: String) -> UIAlertController { + let alert = ThirdPartySearchAlerts( + title: title, + message: message, + preferredStyle: UIAlertControllerStyle.alert + ) + + let okayOption = UIAlertAction( + title: Strings.ThirdPartySearchOkayButton, + style: UIAlertActionStyle.default, + handler: nil + ) + + alert.addAction(okayOption) + return alert + } + +} diff --git a/mobile/ios/Client/Frontend/Browser/TopTabsLayout.swift b/mobile/ios/Client/Frontend/Browser/TopTabsLayout.swift new file mode 100644 index 0000000000..595022ea9e --- /dev/null +++ b/mobile/ios/Client/Frontend/Browser/TopTabsLayout.swift @@ -0,0 +1,113 @@ +/* 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 + +class TopTabsLayoutDelegate: NSObject, UICollectionViewDelegateFlowLayout { + weak var tabSelectionDelegate: TabSelectionDelegate? + let HeaderFooterWidth = TopTabsUX.SeparatorWidth + TopTabsUX.FaderPading + + @objc func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat { + return TopTabsUX.SeparatorWidth + } + + @objc func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { + return CGSize(width: TopTabsUX.TabWidth, height: collectionView.frame.height) + } + + @objc func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets { + return UIEdgeInsets.zero + } + + @objc func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat { + return TopTabsUX.SeparatorWidth + } + + @objc func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { + tabSelectionDelegate?.didSelectTabAtIndex(indexPath.row) + } + + @objc func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize { + return CGSize(width: HeaderFooterWidth, height: 0) + } + + @objc func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForFooterInSection section: Int) -> CGSize { + return CGSize(width: HeaderFooterWidth, height: 0) + } + +} + +class TopTabsViewLayout: UICollectionViewFlowLayout { + var decorationAttributeArr: [Int: UICollectionViewLayoutAttributes?] = [:] + let separatorYOffset = TopTabsUX.SeparatorYOffset + let separatorSize = TopTabsUX.SeparatorHeight + let SeparatorZIndex = -2 ///Prevent the header/footer from appearing above the Tabs + + override var collectionViewContentSize: CGSize { + let tabsWidth = ((CGFloat(collectionView!.numberOfItems(inSection: 0))) * (TopTabsUX.TabWidth + TopTabsUX.SeparatorWidth)) - TopTabsUX.SeparatorWidth + return CGSize(width: tabsWidth + (TopTabsUX.TopTabsBackgroundShadowWidth * 2), height: collectionView!.bounds.height) + } + + override func prepare() { + super.prepare() + self.minimumLineSpacing = TopTabsUX.SeparatorWidth + scrollDirection = UICollectionViewScrollDirection.horizontal + register(TopTabsSeparator.self, forDecorationViewOfKind: TopTabsSeparatorUX.Identifier) + } + + override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool { + decorationAttributeArr = [:] + return true + } + + // MARK: layoutAttributesForElementsInRect + override func layoutAttributesForDecorationView(ofKind elementKind: String, at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? { + guard indexPath.row < self.collectionView!.numberOfItems(inSection: 0) else { + let separatorAttr = UICollectionViewLayoutAttributes(forDecorationViewOfKind: TopTabsSeparatorUX.Identifier, with: indexPath) + separatorAttr.frame = CGRect.zero + separatorAttr.zIndex = SeparatorZIndex + return separatorAttr + } + + if let attr = self.decorationAttributeArr[indexPath.item] { + return attr + } else { + // Compute the separator if it does not exist in the cache + let separatorAttr = UICollectionViewLayoutAttributes(forDecorationViewOfKind: TopTabsSeparatorUX.Identifier, with: indexPath) + let x = TopTabsUX.TopTabsBackgroundShadowWidth + ((CGFloat(indexPath.row) * (TopTabsUX.TabWidth + TopTabsUX.SeparatorWidth)) - TopTabsUX.SeparatorWidth) + separatorAttr.frame = CGRect(x: x, y: separatorYOffset, width: TopTabsUX.SeparatorWidth, height: separatorSize) + separatorAttr.zIndex = SeparatorZIndex + return separatorAttr + } + } + + override func layoutAttributesForSupplementaryView(ofKind elementKind: String, at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? { + let attributes = super.layoutAttributesForSupplementaryView(ofKind: elementKind, at: indexPath) + attributes?.zIndex = SeparatorZIndex + return attributes + } + + override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? { + var attributes = super.layoutAttributesForElements(in: rect)! + + // Create attributes for the Tab Separator. + for i in attributes { + guard i.representedElementKind != UICollectionElementKindSectionHeader && i.representedElementKind != UICollectionElementKindSectionFooter else { + i.zIndex = SeparatorZIndex + continue + } + let sep = UICollectionViewLayoutAttributes(forDecorationViewOfKind: TopTabsSeparatorUX.Identifier, with: i.indexPath) + sep.frame = CGRect(x: i.frame.origin.x - TopTabsUX.SeparatorWidth, y: separatorYOffset, width: TopTabsUX.SeparatorWidth, height: separatorSize) + sep.zIndex = SeparatorZIndex + i.zIndex = 10 + + // Only add the seperator if it will be shown. + if i.indexPath.row != 0 && i.indexPath.row < self.collectionView!.numberOfItems(inSection: 0) { + attributes.append(sep) + decorationAttributeArr[i.indexPath.item] = sep + } + } + + return attributes + } +} diff --git a/mobile/ios/Client/Frontend/Browser/TopTabsViewController.swift b/mobile/ios/Client/Frontend/Browser/TopTabsViewController.swift new file mode 100644 index 0000000000..82e172aa3e --- /dev/null +++ b/mobile/ios/Client/Frontend/Browser/TopTabsViewController.swift @@ -0,0 +1,560 @@ +/* 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 WebKit + +struct TopTabsUX { + static let TopTabsViewHeight: CGFloat = 44 + static let TopTabsBackgroundColor = UIColor(rgb: 0x2a2a2e) + static let TopTabsBackgroundPadding: CGFloat = 35 + static let TopTabsBackgroundShadowWidth: CGFloat = 12 + static let TabWidth: CGFloat = 190 + static let CollectionViewPadding: CGFloat = 15 + static let FaderPading: CGFloat = 8 + static let SeparatorWidth: CGFloat = 1 + static let HighlightLineWidth: CGFloat = 3 + static let TabNudge: CGFloat = 1 // Nudge the favicon and close button by 1px + static let TabTitleWidth: CGFloat = 110 + static let TabTitlePadding: CGFloat = 10 + static let AnimationSpeed: TimeInterval = 0.1 + static let SeparatorYOffset: CGFloat = 7 + static let SeparatorHeight: CGFloat = 32 +} + +protocol TopTabsDelegate: class { + func topTabsDidPressTabs() + func topTabsDidPressNewTab(_ isPrivate: Bool) + + func topTabsDidTogglePrivateMode() + func topTabsDidChangeTab() +} + +protocol TopTabCellDelegate: class { + func tabCellDidClose(_ cell: TopTabCell) +} + +class TopTabsViewController: UIViewController { + let tabManager: TabManager + weak var delegate: TopTabsDelegate? + fileprivate var isPrivate = false + let faviconNotification = NSNotification.Name(rawValue: FaviconManager.FaviconDidLoad) + + lazy var collectionView: UICollectionView = { + let collectionView = UICollectionView(frame: CGRect.zero, collectionViewLayout: TopTabsViewLayout()) + collectionView.register(TopTabCell.self, forCellWithReuseIdentifier: TopTabCell.Identifier) + collectionView.showsVerticalScrollIndicator = false + collectionView.showsHorizontalScrollIndicator = false + collectionView.bounces = false + collectionView.clipsToBounds = false + collectionView.accessibilityIdentifier = "Top Tabs View" + return collectionView + }() + + fileprivate lazy var tabsButton: TabsButton = { + let tabsButton = TabsButton.tabTrayButton() + tabsButton.addTarget(self, action: #selector(TopTabsViewController.tabsTrayTapped), for: UIControlEvents.touchUpInside) + tabsButton.accessibilityIdentifier = "TopTabsViewController.tabsButton" + return tabsButton + }() + + fileprivate lazy var newTab: UIButton = { + let newTab = UIButton.newTabButton() + newTab.addTarget(self, action: #selector(TopTabsViewController.newTabTapped), for: UIControlEvents.touchUpInside) + return newTab + }() + + lazy var privateModeButton: PrivateModeButton = { + let privateModeButton = PrivateModeButton() + privateModeButton.light = true + privateModeButton.addTarget(self, action: #selector(TopTabsViewController.togglePrivateModeTapped), for: UIControlEvents.touchUpInside) + return privateModeButton + }() + + fileprivate lazy var tabLayoutDelegate: TopTabsLayoutDelegate = { + let delegate = TopTabsLayoutDelegate() + delegate.tabSelectionDelegate = self + return delegate + }() + + fileprivate var tabsToDisplay: [Tab] { + return self.isPrivate ? tabManager.privateTabs : tabManager.normalTabs + } + + // Handle animations. + fileprivate var tabStore: [Tab] = [] //the actual datastore + fileprivate var pendingUpdatesToTabs: [Tab] = [] //the datastore we are transitioning to + fileprivate var needReloads: [Tab?] = [] // Tabs that need to be reloaded + fileprivate var isUpdating = false + fileprivate var pendingReloadData = false + fileprivate var oldTabs: [Tab]? // The last state of the tabs before an animation + fileprivate weak var oldSelectedTab: Tab? // Used to select the right tab when transitioning between private/normal tabs + + init(tabManager: TabManager) { + self.tabManager = tabManager + super.init(nibName: nil, bundle: nil) + collectionView.dataSource = self + collectionView.delegate = tabLayoutDelegate + [UICollectionElementKindSectionHeader, UICollectionElementKindSectionFooter].forEach { + collectionView.register(TopTabsHeaderFooter.self, forSupplementaryViewOfKind: $0, withReuseIdentifier: "HeaderFooter") + } + NotificationCenter.default.addObserver(self, selector: #selector(TopTabsViewController.reloadFavicons(_:)), name: faviconNotification, object: nil) + } + + deinit { + self.tabManager.removeDelegate(self) + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + if self.tabsToDisplay != self.tabStore { + self.reloadData() + } + } + + override func viewDidLoad() { + super.viewDidLoad() + + tabManager.addDelegate(self) + self.tabStore = self.tabsToDisplay + + let topTabFader = TopTabFader() + + view.addSubview(topTabFader) + topTabFader.addSubview(collectionView) + view.addSubview(tabsButton) + view.addSubview(newTab) + view.addSubview(privateModeButton) + + newTab.snp.makeConstraints { make in + make.centerY.equalTo(view) + make.trailing.equalTo(tabsButton.snp.leading).offset(-10) + make.size.equalTo(view.snp.height) + } + tabsButton.snp.makeConstraints { make in + make.centerY.equalTo(view) + make.trailing.equalTo(view).offset(-10) + make.size.equalTo(view.snp.height) + } + privateModeButton.snp.makeConstraints { make in + make.centerY.equalTo(view) + make.leading.equalTo(view).offset(10) + make.size.equalTo(view.snp.height) + } + topTabFader.snp.makeConstraints { make in + make.top.bottom.equalTo(view) + make.leading.equalTo(privateModeButton.snp.trailing) + make.trailing.equalTo(newTab.snp.leading) + } + collectionView.snp.makeConstraints { make in + make.edges.equalTo(topTabFader) + } + + view.backgroundColor = UIColor(rgb: 0x272727) + tabsButton.applyTheme(Theme.NormalMode) + if let currentTab = tabManager.selectedTab { + applyTheme(currentTab.isPrivate ? Theme.PrivateMode : Theme.NormalMode) + } + updateTabCount(tabStore.count, animated: false) + } + + func switchForegroundStatus(isInForeground reveal: Bool) { + // Called when the app leaves the foreground to make sure no information is inadvertently revealed + if let cells = self.collectionView.visibleCells as? [TopTabCell] { + let alpha: CGFloat = reveal ? 1 : 0 + for cell in cells { + cell.titleText.alpha = alpha + cell.favicon.alpha = alpha + } + } + } + + func updateTabCount(_ count: Int, animated: Bool = true) { + self.tabsButton.updateTabCount(count, animated: animated) + } + + func tabsTrayTapped() { + delegate?.topTabsDidPressTabs() + } + + func newTabTapped() { + if pendingReloadData { + return + } + self.delegate?.topTabsDidPressNewTab(self.isPrivate) + LeanPlumClient.shared.track(event: .openedNewTab, withParameters: ["Source": "Add tab button in the URL Bar on iPad" as AnyObject]) + } + + func togglePrivateModeTapped() { + if isUpdating || pendingReloadData { + return + } + let isPrivate = self.isPrivate + delegate?.topTabsDidTogglePrivateMode() + self.pendingReloadData = true // Stops animations from happening + let oldSelectedTab = self.oldSelectedTab + self.oldSelectedTab = tabManager.selectedTab + self.privateModeButton.setSelected(!isPrivate, animated: true) + + //if private tabs is empty and we are transitioning to it add a tab + if tabManager.privateTabs.isEmpty && !isPrivate { + tabManager.addTab(isPrivate: true) + } + + //get the tabs from which we will select which one to nominate for tribute (selection) + //the isPrivate boolean still hasnt been flipped. (It'll be flipped in the BVC didSelectedTabChange method) + let tabs = !isPrivate ? tabManager.privateTabs : tabManager.normalTabs + if let tab = oldSelectedTab, tabs.index(of: tab) != nil { + tabManager.selectTab(tab) + } else { + tabManager.selectTab(tabs.last) + } + } + + func reloadFavicons(_ notification: Notification) { + // Notifications might be called from a different thread. Make sure animations only happen on the main thread. + DispatchQueue.main.async { + if let tab = notification.object as? Tab, self.tabStore.index(of: tab) != nil { + self.needReloads.append(tab) + self.performTabUpdates() + } + } + } + + func scrollToCurrentTab(_ animated: Bool = true, centerCell: Bool = false) { + assertIsMainThread("Only animate on the main thread") + + guard let currentTab = tabManager.selectedTab, let index = tabStore.index(of: currentTab), !collectionView.frame.isEmpty else { + return + } + if let frame = collectionView.layoutAttributesForItem(at: IndexPath(row: index, section: 0))?.frame { + if centerCell { + collectionView.scrollToItem(at: IndexPath(item: index, section: 0), at: .centeredHorizontally, animated: false) + } else { + // Padding is added to ensure the tab is completely visible (none of the tab is under the fader) + let padFrame = frame.insetBy(dx: -(TopTabsUX.TopTabsBackgroundShadowWidth+TopTabsUX.FaderPading), dy: 0) + if animated { + UIView.animate(withDuration: TopTabsUX.AnimationSpeed, animations: { + self.collectionView.scrollRectToVisible(padFrame, animated: true) + }) + } else { + collectionView.scrollRectToVisible(padFrame, animated: false) + } + } + } + } +} + +extension TopTabsViewController: Themeable { + func applyTheme(_ themeName: String) { + tabsButton.applyTheme(themeName) + tabsButton.titleBackgroundColor = view.backgroundColor ?? UIColor(rgb: 0x272727) + tabsButton.textColor = UIColor(rgb: 0xb1b1b3) + isPrivate = (themeName == Theme.PrivateMode) + privateModeButton.styleForMode(privateMode: isPrivate) + privateModeButton.tintColor = isPrivate ? UIColor(rgb: 0xf9f9fa) : UIColor(rgb: 0xb1b1b3) + privateModeButton.imageView?.tintColor = privateModeButton.tintColor + newTab.tintColor = UIColor(rgb: 0xb1b1b3) + collectionView.backgroundColor = view.backgroundColor + } +} + +extension TopTabsViewController: TopTabCellDelegate { + func tabCellDidClose(_ cell: TopTabCell) { + // Trying to remove tabs while animating can lead to crashes as indexes change. If updates are happening don't allow tabs to be removed. + guard let index = collectionView.indexPath(for: cell)?.item else { + return + } + let tab = tabStore[index] + if tabsToDisplay.index(of: tab) != nil { + tabManager.removeTab(tab) + } + } +} + +extension TopTabsViewController: UICollectionViewDataSource { + @objc func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { + let index = indexPath.item + let tabCell = collectionView.dequeueReusableCell(withReuseIdentifier: TopTabCell.Identifier, for: indexPath) as! TopTabCell + tabCell.delegate = self + + let tab = tabStore[index] + tabCell.style = tab.isPrivate ? .dark : .light + tabCell.titleText.text = tab.displayTitle + + if tab.displayTitle.isEmpty { + if tab.webView?.url?.baseDomain?.contains("localhost") ?? true { + tabCell.titleText.text = Strings.AppMenuNewTabTitleString + } else { + tabCell.titleText.text = tab.webView?.url?.absoluteDisplayString + } + tabCell.accessibilityLabel = tab.url?.aboutComponent ?? "" + tabCell.closeButton.accessibilityLabel = String(format: Strings.TopSitesRemoveButtonAccessibilityLabel, tabCell.titleText.text ?? "") + } else { + tabCell.accessibilityLabel = tab.displayTitle + tabCell.closeButton.accessibilityLabel = String(format: Strings.TopSitesRemoveButtonAccessibilityLabel, tab.displayTitle) + } + + tabCell.selectedTab = (tab == tabManager.selectedTab) + if let siteURL = tab.url?.displayURL { + tabCell.favicon.setIcon(tab.displayFavicon, forURL: siteURL, completed: { (color, url) in + if siteURL == url { + tabCell.favicon.image = tabCell.favicon.image?.createScaled(CGSize(width: 15, height: 15)) + tabCell.favicon.backgroundColor = color == .clear ? .white : color + tabCell.favicon.contentMode = .center + } + }) + } else { + tabCell.favicon.image = UIImage(named: "defaultFavicon") + tabCell.favicon.contentMode = .scaleAspectFit + tabCell.favicon.backgroundColor = .clear + } + + return tabCell + } + + @objc func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { + return tabStore.count + } + + @objc func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView { + let view = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: "HeaderFooter", for: indexPath) as! TopTabsHeaderFooter + view.arrangeLine(kind) + return view + } + +} + +extension TopTabsViewController: TabSelectionDelegate { + func didSelectTabAtIndex(_ index: Int) { + let tab = tabStore[index] + if tabsToDisplay.index(of: tab) != nil { + tabManager.selectTab(tab) + } + } +} + +// Collection Diff (animations) +extension TopTabsViewController { + + struct TopTabChangeSet { + let reloads: Set + let inserts: Set + let deletes: Set + + init(reloadArr: [IndexPath], insertArr: [IndexPath], deleteArr: [IndexPath]) { + reloads = Set(reloadArr) + inserts = Set(insertArr) + deletes = Set(deleteArr) + } + + var all: [Set] { + return [inserts, reloads, deletes] + } + + } + + // create a TopTabChangeSet which is a snapshot of updates to perfrom on a collectionView + func calculateDiffWith(_ oldTabs: [Tab], to newTabs: [Tab], and reloadTabs: [Tab?]) -> TopTabChangeSet { + let inserts: [IndexPath] = newTabs.enumerated().flatMap { index, tab in + if oldTabs.index(of: tab) == nil { + return IndexPath(row: index, section: 0) + } + return nil + } + + let deletes: [IndexPath] = oldTabs.enumerated().flatMap { index, tab in + if newTabs.index(of: tab) == nil { + return IndexPath(row: index, section: 0) + } + return nil + } + + // Create based on what is visibile but filter out tabs we are about to insert/delete. + let reloads: [IndexPath] = reloadTabs.flatMap { tab in + guard let tab = tab, newTabs.index(of: tab) != nil else { + return nil + } + return IndexPath(row: newTabs.index(of: tab)!, section: 0) + }.filter { return inserts.index(of: $0) == nil && deletes.index(of: $0) == nil } + + return TopTabChangeSet(reloadArr: reloads, insertArr: inserts, deleteArr: deletes) + } + + func updateTabsFrom(_ oldTabs: [Tab]?, to newTabs: [Tab], on completion: (() -> Void)? = nil) { + assertIsMainThread("Updates can only be performed from the main thread") + guard let oldTabs = oldTabs, !self.isUpdating, !self.pendingReloadData else { + return + } + + // Lets create our change set + let update = self.calculateDiffWith(oldTabs, to: newTabs, and: needReloads) + flushPendingChanges() + + // If there are no changes. We have nothing to do + if update.all.every({ $0.isEmpty }) { + completion?() + return + } + + // The actual update block. We update the dataStore right before we do the UI updates. + let updateBlock = { + self.tabStore = newTabs + self.collectionView.deleteItems(at: Array(update.deletes)) + self.collectionView.insertItems(at: Array(update.inserts)) + self.collectionView.reloadItems(at: Array(update.reloads)) + } + + //Lets lock any other updates from happening. + self.isUpdating = true + self.pendingUpdatesToTabs = newTabs // This var helps other mutations that might happen while updating. + + // The actual update + UIView.animate(withDuration: TopTabsUX.AnimationSpeed, animations: { + self.collectionView.performBatchUpdates(updateBlock) + }) { (_) in + self.isUpdating = false + self.pendingUpdatesToTabs = [] + // Sometimes there might be a pending reload. Lets do that. + if self.pendingReloadData { + return self.reloadData() + } + + // There can be pending animations. Run update again to clear them. + let tabs = self.oldTabs ?? self.tabStore + self.updateTabsFrom(tabs, to: self.tabsToDisplay, on: { + if !update.inserts.isEmpty || !update.reloads.isEmpty { + self.scrollToCurrentTab() + } + }) + } + } + + fileprivate func flushPendingChanges() { + oldTabs = nil + needReloads.removeAll() + } + + fileprivate func reloadData() { + assertIsMainThread("reloadData must only be called from main thread") + + if self.isUpdating || self.collectionView.frame == CGRect.zero { + self.pendingReloadData = true + return + } + + isUpdating = true + self.tabStore = self.tabsToDisplay + self.newTab.isUserInteractionEnabled = false + self.flushPendingChanges() + UIView.animate(withDuration: TopTabsUX.AnimationSpeed, animations: { + self.collectionView.reloadData() + self.collectionView.collectionViewLayout.invalidateLayout() + self.collectionView.layoutIfNeeded() + self.scrollToCurrentTab(true, centerCell: true) + }, completion: { (_) in + self.isUpdating = false + self.pendingReloadData = false + self.performTabUpdates() + self.newTab.isUserInteractionEnabled = true + }) + } +} + +extension TopTabsViewController: TabManagerDelegate { + + // Because we don't know when we are about to transition to private mode + // check to make sure that the tab we are trying to add is being added to the right tab group + fileprivate func tabsMatchDisplayGroup(_ a: Tab?, b: Tab?) -> Bool { + if let a = a, let b = b, a.isPrivate == b.isPrivate { + return true + } + return false + } + + func performTabUpdates() { + guard !isUpdating else { + return + } + + let fromTabs = !self.pendingUpdatesToTabs.isEmpty ? self.pendingUpdatesToTabs : self.oldTabs + self.oldTabs = fromTabs ?? self.tabStore + if self.pendingReloadData && !isUpdating { + self.reloadData() + } else { + self.updateTabsFrom(self.oldTabs, to: self.tabsToDisplay) + } + } + + // This helps make sure animations don't happen before the view is loaded. + fileprivate var isRestoring: Bool { + return self.tabManager.isRestoring || self.collectionView.frame == CGRect.zero + } + + func tabManager(_ tabManager: TabManager, didSelectedTabChange selected: Tab?, previous: Tab?) { + if isRestoring { + return + } + if !tabsMatchDisplayGroup(selected, b: previous) { + self.reloadData() + } else { + self.needReloads.append(selected) + self.needReloads.append(previous) + performTabUpdates() + delegate?.topTabsDidChangeTab() + } + } + + func tabManager(_ tabManager: TabManager, willAddTab tab: Tab) { + // We need to store the earliest oldTabs. So if one already exists use that. + self.oldTabs = self.oldTabs ?? tabStore + } + + func tabManager(_ tabManager: TabManager, didAddTab tab: Tab) { + if isRestoring || (tabManager.selectedTab != nil && !tabsMatchDisplayGroup(tab, b: tabManager.selectedTab)) { + return + } + performTabUpdates() + } + + func tabManager(_ tabManager: TabManager, willRemoveTab tab: Tab) { + // We need to store the earliest oldTabs. So if one already exists use that. + self.oldTabs = self.oldTabs ?? tabStore + } + + func tabManager(_ tabManager: TabManager, didRemoveTab tab: Tab) { + if isRestoring { + return + } + // If we deleted the last private tab. We'll be switching back to normal browsing. Pause updates till then + if self.tabsToDisplay.isEmpty { + self.pendingReloadData = true + return + } + + // dont want to hold a ref to a deleted tab + if tab === oldSelectedTab { + oldSelectedTab = nil + } + + performTabUpdates() + } + + func tabManagerDidRestoreTabs(_ tabManager: TabManager) { + self.reloadData() + } + + func tabManagerDidAddTabs(_ tabManager: TabManager) { + self.reloadData() + } + + func tabManagerDidRemoveAllTabs(_ tabManager: TabManager, toast: ButtonToast?) { + self.reloadData() + } +} diff --git a/mobile/ios/Client/Frontend/Browser/TopTabsViews.swift b/mobile/ios/Client/Frontend/Browser/TopTabsViews.swift new file mode 100644 index 0000000000..898ed32b60 --- /dev/null +++ b/mobile/ios/Client/Frontend/Browser/TopTabsViews.swift @@ -0,0 +1,258 @@ +/* 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 + +struct TopTabsSeparatorUX { + static let Identifier = "Separator" + static let Color = UIColor(rgb: 0x3c3c3d) + static let Width: CGFloat = 1 +} + +class TopTabsSeparator: UICollectionReusableView { + override init(frame: CGRect) { + super.init(frame: frame) + self.backgroundColor = TopTabsSeparatorUX.Color + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +class TopTabsHeaderFooter: UICollectionReusableView { + let line = UIView() + override init(frame: CGRect) { + super.init(frame: frame) + addSubview(line) + line.backgroundColor = TopTabsSeparatorUX.Color + } + + func arrangeLine(_ kind: String) { + line.snp.removeConstraints() + switch kind { + case UICollectionElementKindSectionHeader: + line.snp.makeConstraints { make in + make.trailing.equalTo(self) + } + case UICollectionElementKindSectionFooter: + line.snp.makeConstraints { make in + make.leading.equalTo(self) + } + default: + break + } + line.snp.makeConstraints { make in + make.height.equalTo(TopTabsUX.SeparatorHeight) + make.width.equalTo(TopTabsUX.SeparatorWidth) + make.top.equalTo(self).offset(TopTabsUX.SeparatorYOffset) + } + } + + override func apply(_ layoutAttributes: UICollectionViewLayoutAttributes) { + layer.zPosition = CGFloat(layoutAttributes.zIndex) + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +class TopTabCell: UICollectionViewCell { + enum Style { + case light + case dark + } + + static let Identifier = "TopTabCellIdentifier" + static let ShadowOffsetSize: CGFloat = 2 //The shadow is used to hide the tab separator + + var style: Style = .light { + didSet { + if style != oldValue { + applyStyle(style) + } + } + } + + var selectedTab = false { + didSet { + backgroundColor = selectedTab ? UIColor(rgb: 0xf9f9fa) : UIColor(rgb: 0x272727) + titleText.textColor = selectedTab ? UIColor(rgb: 0x0c0c0d) : UIColor(rgb: 0xb1b1b3) + highlightLine.isHidden = !selectedTab + closeButton.tintColor = selectedTab ? UIColor(rgb: 0x272727) : UIColor(rgb: 0xb1b1b3) + // restyle if we are in PBM + if style == .dark && selectedTab { + backgroundColor = UIColor(rgb: 0x38383D) + titleText.textColor = UIColor(rgb: 0xf9f9fa) + closeButton.tintColor = UIColor(rgb: 0xf9f9fa) + } + closeButton.backgroundColor = backgroundColor + closeButton.layer.shadowColor = backgroundColor?.cgColor + if selectedTab { + drawShadow() + } + } + } + + let titleText: UILabel = { + let titleText = UILabel() + titleText.textAlignment = NSTextAlignment.left + titleText.isUserInteractionEnabled = false + titleText.numberOfLines = 1 + titleText.lineBreakMode = .byCharWrapping + titleText.font = DynamicFontHelper.defaultHelper.DefaultSmallFont + return titleText + }() + + let favicon: UIImageView = { + let favicon = UIImageView() + favicon.layer.cornerRadius = 2.0 + favicon.layer.masksToBounds = true + return favicon + }() + + let closeButton: UIButton = { + let closeButton = UIButton() + closeButton.setImage(UIImage.templateImageNamed("menu-CloseTabs"), for: UIControlState()) + closeButton.tintColor = UIColor(rgb: 0xb1b1b3) + closeButton.imageEdgeInsets = UIEdgeInsets(top: 15, left: TopTabsUX.TabTitlePadding, bottom: 15, right: TopTabsUX.TabTitlePadding) + closeButton.layer.shadowOpacity = 0.8 + closeButton.layer.masksToBounds = false + closeButton.layer.shadowOffset = CGSize(width: -TopTabsUX.TabTitlePadding, height: 0) + return closeButton + }() + + let highlightLine: UIView = { + let line = UIView() + line.backgroundColor = UIColor(rgb: 0x0066DC) + line.isHidden = true + return line + }() + + weak var delegate: TopTabCellDelegate? + + override init(frame: CGRect) { + super.init(frame: frame) + + closeButton.addTarget(self, action: #selector(TopTabCell.closeTab), for: UIControlEvents.touchUpInside) + + contentView.addSubview(titleText) + contentView.addSubview(closeButton) + contentView.addSubview(favicon) + contentView.addSubview(highlightLine) + + favicon.snp.makeConstraints { make in + make.centerY.equalTo(self).offset(TopTabsUX.TabNudge) + make.size.equalTo(TabTrayControllerUX.FaviconSize) + make.leading.equalTo(self).offset(TopTabsUX.TabTitlePadding) + } + titleText.snp.makeConstraints { make in + make.centerY.equalTo(self) + make.height.equalTo(self) + make.trailing.equalTo(closeButton.snp.leading).offset(TopTabsUX.TabTitlePadding) + make.leading.equalTo(favicon.snp.trailing).offset(TopTabsUX.TabTitlePadding) + } + closeButton.snp.makeConstraints { make in + make.centerY.equalTo(self).offset(TopTabsUX.TabNudge) + make.height.equalTo(self) + make.width.equalTo(self.snp.height).offset(-TopTabsUX.TabTitlePadding) + make.trailing.equalTo(self.snp.trailing) + } + highlightLine.snp.makeConstraints { make in + make.top.equalTo(self) + make.leading.equalTo(self).offset(-TopTabCell.ShadowOffsetSize) + make.trailing.equalTo(self).offset(TopTabCell.ShadowOffsetSize) + make.height.equalTo(TopTabsUX.HighlightLineWidth) + } + + self.clipsToBounds = false + + applyStyle(style) + } + + fileprivate func applyStyle(_ style: Style) { + switch style { + case Style.light: + titleText.textColor = UIColor.darkText + backgroundColor = UIConstants.AppBackgroundColor + highlightLine.backgroundColor = UIColor(rgb: 0x0066DC) + case Style.dark: + titleText.textColor = UIColor.lightText + backgroundColor = UIColor(rgb: 0x38383D) + highlightLine.backgroundColor = UIColor(rgb: 0x9400ff) + } + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func prepareForReuse() { + super.prepareForReuse() + self.layer.shadowOpacity = 0 + } + + func closeTab() { + delegate?.tabCellDidClose(self) + } + + // When a tab is selected the shadow prevents the tab separators from showing. + func drawShadow() { + self.layer.masksToBounds = false + self.layer.shadowColor = backgroundColor?.cgColor + self.layer.shadowOpacity = 1 + self.layer.shadowRadius = 0 + + self.layer.shadowPath = UIBezierPath(roundedRect: CGRect(x: 0, y: 0, width: self.frame.size.width + (TopTabCell.ShadowOffsetSize * 2), height: self.frame.size.height), cornerRadius: 0).cgPath + self.layer.shadowOffset = CGSize(width: -TopTabCell.ShadowOffsetSize, height: 0) + } + + override func apply(_ layoutAttributes: UICollectionViewLayoutAttributes) { + layer.zPosition = CGFloat(layoutAttributes.zIndex) + } +} + +class TopTabFader: UIView { + lazy var hMaskLayer: CAGradientLayer = { + let innerColor: CGColor = UIColor(white: 1, alpha: 1.0).cgColor + let outerColor: CGColor = UIColor(white: 1, alpha: 0.0).cgColor + let hMaskLayer = CAGradientLayer() + hMaskLayer.colors = [outerColor, innerColor, innerColor, outerColor] + hMaskLayer.locations = [0.00, 0.005, 0.995, 1.0] + hMaskLayer.startPoint = CGPoint(x: 0, y: 0.5) + hMaskLayer.endPoint = CGPoint(x: 1.0, y: 0.5) + hMaskLayer.anchorPoint = CGPoint.zero + return hMaskLayer + }() + + init() { + super.init(frame: CGRect.zero) + layer.mask = hMaskLayer + } + + internal override func layoutSubviews() { + super.layoutSubviews() + + let widthA = NSNumber(value: Float(CGFloat(8) / frame.width)) + let widthB = NSNumber(value: Float(1 - CGFloat(8) / frame.width)) + + hMaskLayer.locations = [0.00, widthA, widthB, 1.0] + hMaskLayer.frame = CGRect(x: 0, y: 0, width: frame.width, height: frame.height) + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +class TopTabsViewLayoutAttributes: UICollectionViewLayoutAttributes { + + override func isEqual(_ object: Any?) -> Bool { + guard let object = object as? TopTabsViewLayoutAttributes else { + return false + } + return super.isEqual(object) + } +} diff --git a/mobile/ios/Client/Frontend/Browser/URIFixup.swift b/mobile/ios/Client/Frontend/Browser/URIFixup.swift new file mode 100644 index 0000000000..df7e027aa4 --- /dev/null +++ b/mobile/ios/Client/Frontend/Browser/URIFixup.swift @@ -0,0 +1,53 @@ +/* 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 + +class URIFixup { + static func getURL(_ entry: String) -> URL? { + let trimmed = entry.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines) + guard let escaped = trimmed.addingPercentEncoding(withAllowedCharacters: CharacterSet.URLAllowedCharacterSet()) else { + return nil + } + + // Then check if the URL includes a scheme. This will handle + // all valid requests starting with "http://", "about:", etc. + // However, we ensure that the scheme is one that is listed in + // the official URI scheme list, so that other such search phrases + // like "filetype:" are recognised as searches rather than URLs. + if let url = punycodedURL(escaped), url.schemeIsValid { + return url + } + + // If there's no scheme, we're going to prepend "http://". First, + // make sure there's at least one "." in the host. This means + // we'll allow single-word searches (e.g., "foo") at the expense + // of breaking single-word hosts without a scheme (e.g., "localhost"). + if trimmed.range(of: ".") == nil { + return nil + } + + if trimmed.range(of: " ") != nil { + return nil + } + + // If there is a ".", prepend "http://" and try again. Since this + // is strictly an "http://" URL, we also require a host. + if let url = punycodedURL("http://\(escaped)"), url.host != nil { + return url + } + + return nil + } + + static func punycodedURL(_ string: String) -> URL? { + var components = URLComponents(string: string) + if AppConstants.MOZ_PUNYCODE { + let host = components?.host?.utf8HostToAscii() + components?.host = host + } + return components?.url + } +} diff --git a/mobile/ios/Client/Frontend/Browser/URLBarView.swift b/mobile/ios/Client/Frontend/Browser/URLBarView.swift new file mode 100644 index 0000000000..86e9f8cf17 --- /dev/null +++ b/mobile/ios/Client/Frontend/Browser/URLBarView.swift @@ -0,0 +1,825 @@ +/* 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 UIKit +import Shared +import SnapKit +import XCGLogger + +private let log = Logger.browserLogger + +struct URLBarViewUX { + static let TextFieldBorderColor = UIColor(rgb: 0xBBBBBB) + static let TextFieldActiveBorderColor = UIColor(rgb: 0xB0D5FB) + static let LocationLeftPadding: CGFloat = 8 + static let Padding: CGFloat = 10 + static let LocationHeight: CGFloat = 40 + static let ButtonHeight: CGFloat = 44 + static let LocationContentOffset: CGFloat = 8 + static let TextFieldCornerRadius: CGFloat = 8 + static let TextFieldBorderWidth: CGFloat = 1 + static let TextFieldBorderWidthSelected: CGFloat = 4 + // offset from edge of tabs button + static let ProgressTintColor = UIColor(rgb: 0x00dcfc) + static let ProgressBarHeight: CGFloat = 3 + + static let TabsButtonRotationOffset: CGFloat = 1.5 + static let TabsButtonHeight: CGFloat = 18.0 + static let ToolbarButtonInsets = UIEdgeInsets(top: Padding, left: Padding, bottom: Padding, right: Padding) + + static let Themes: [String: Theme] = { + var themes = [String: Theme]() + var theme = Theme() + theme.borderColor = UIColor(rgb: 0x2D2D31) + theme.backgroundColor = UIColor(rgb: 0x38383D) + theme.activeBorderColor = UIColor(rgb: 0x4a4a4f) + theme.tintColor = UIColor(rgb: 0xf9f9fa) + theme.textColor = UIColor(rgb: 0xf9f9fa) + theme.buttonTintColor = UIColor(rgb: 0xD2d2d4) + theme.disabledButtonColor = UIColor.gray + theme.highlightButtonColor = UIColor(rgb: 0xAC39FF) + themes[Theme.PrivateMode] = theme + + theme = Theme() + theme.borderColor = UIColor(rgb: 0x737373).withAlphaComponent(0.3) + theme.activeBorderColor = TextFieldActiveBorderColor + theme.disabledButtonColor = UIColor.lightGray + theme.highlightButtonColor = UIColor(rgb: 0x00A2FE) + theme.tintColor = ProgressTintColor + theme.textColor = UIColor(rgb: 0x272727) + theme.backgroundColor = UIConstants.AppBackgroundColor + theme.buttonTintColor = UIColor(rgb: 0x272727) + themes[Theme.NormalMode] = theme + + return themes + }() +} + +protocol URLBarDelegate: class { + func urlBarDidPressTabs(_ urlBar: URLBarView) + func urlBarDidPressReaderMode(_ urlBar: URLBarView) + /// - returns: whether the long-press was handled by the delegate; i.e. return `false` when the conditions for even starting handling long-press were not satisfied + func urlBarDidLongPressReaderMode(_ urlBar: URLBarView) -> Bool + func urlBarDidPressStop(_ urlBar: URLBarView) + func urlBarDidPressReload(_ urlBar: URLBarView) + func urlBarDidEnterOverlayMode(_ urlBar: URLBarView) + func urlBarDidLeaveOverlayMode(_ urlBar: URLBarView) + func urlBarDidLongPressLocation(_ urlBar: URLBarView) + func urlBarDidPressQRButton(_ urlBar: URLBarView) + func urlBarDidPressPageOptions(_ urlBar: URLBarView, from button: UIButton) + func urlBarLocationAccessibilityActions(_ urlBar: URLBarView) -> [UIAccessibilityCustomAction]? + func urlBarDidPressScrollToTop(_ urlBar: URLBarView) + func urlBar(_ urlBar: URLBarView, didEnterText text: String) + func urlBar(_ urlBar: URLBarView, didSubmitText text: String) + // Returns either (search query, true) or (url, false). + func urlBarDisplayTextForURL(_ url: URL?) -> (String?, Bool) + func urlBarDidLongPressPageOptions(_ urlBar: URLBarView, from button: UIButton) +} + +class URLBarView: UIView { + // Additional UIAppearance-configurable properties + dynamic var locationBorderColor: UIColor = URLBarViewUX.TextFieldBorderColor { + didSet { + if !inOverlayMode { + locationContainer.layer.borderColor = locationBorderColor.cgColor + } + } + } + dynamic var locationActiveBorderColor: UIColor = URLBarViewUX.TextFieldActiveBorderColor { + didSet { + if inOverlayMode { + locationContainer.layer.borderColor = locationActiveBorderColor.cgColor + } + } + } + + weak var delegate: URLBarDelegate? + weak var tabToolbarDelegate: TabToolbarDelegate? + var helper: TabToolbarHelper? + var isTransitioning: Bool = false { + didSet { + if isTransitioning { + // Cancel any pending/in-progress animations related to the progress bar + self.progressBar.setProgress(1, animated: false) + self.progressBar.alpha = 0.0 + } + } + } + + fileprivate var currentTheme: String = Theme.NormalMode + + var toolbarIsShowing = false + var topTabsIsShowing = false + + fileprivate var locationTextField: ToolbarTextField? + + /// Overlay mode is the state where the lock/reader icons are hidden, the home panels are shown, + /// and the Cancel button is visible (allowing the user to leave overlay mode). Overlay mode + /// is *not* tied to the location text field's editing state; for instance, when selecting + /// a panel, the first responder will be resigned, yet the overlay mode UI is still active. + var inOverlayMode = false + + lazy var locationView: TabLocationView = { + let locationView = TabLocationView() + locationView.translatesAutoresizingMaskIntoConstraints = false + locationView.readerModeState = ReaderModeState.unavailable + locationView.delegate = self + return locationView + }() + + lazy var locationContainer: UIView = { + let locationContainer = TabLocationContainerView() + locationContainer.translatesAutoresizingMaskIntoConstraints = false + locationContainer.layer.shadowColor = self.locationBorderColor.cgColor + locationContainer.layer.borderWidth = URLBarViewUX.TextFieldBorderWidth + locationContainer.layer.borderColor = self.locationBorderColor.cgColor + locationContainer.backgroundColor = .clear + return locationContainer + }() + + let line = UIView() + + lazy var tabsButton: TabsButton = { + let tabsButton = TabsButton.tabTrayButton() + tabsButton.accessibilityIdentifier = "URLBarView.tabsButton" + return tabsButton + }() + + fileprivate lazy var progressBar: GradientProgressBar = { + let progressBar = GradientProgressBar() + progressBar.clipsToBounds = false + return progressBar + }() + + fileprivate lazy var cancelButton: UIButton = { + let cancelButton = InsetButton() + cancelButton.setImage(UIImage.templateImageNamed("goBack"), for: .normal) + cancelButton.addTarget(self, action: #selector(URLBarView.SELdidClickCancel), for: .touchUpInside) + cancelButton.alpha = 0 + return cancelButton + }() + + fileprivate lazy var showQRScannerButton: InsetButton = { + let button = InsetButton() + button.setImage(UIImage.templateImageNamed("menu-ScanQRCode"), for: .normal) + button.clipsToBounds = false + button.addTarget(self, action: #selector(URLBarView.showQRScanner), for: .touchUpInside) + button.setContentHuggingPriority(1000, for: UILayoutConstraintAxis.horizontal) + button.setContentCompressionResistancePriority(1000, for: UILayoutConstraintAxis.horizontal) + return button + }() + + fileprivate lazy var scrollToTopButton: UIButton = { + let button = UIButton() + button.addTarget(self, action: #selector(URLBarView.SELtappedScrollToTopArea), for: .touchUpInside) + return button + }() + + var menuButton = ToolbarButton() + var bookmarkButton = ToolbarButton() + var forwardButton = ToolbarButton() + var stopReloadButton = ToolbarButton() + + var backButton: ToolbarButton = { + let backButton = ToolbarButton() + backButton.accessibilityIdentifier = "URLBarView.backButton" + return backButton + }() + + lazy var actionButtons: [Themeable & UIButton] = [self.tabsButton, self.menuButton, self.forwardButton, self.backButton, self.stopReloadButton] + + var currentURL: URL? { + get { + return locationView.url as URL? + } + + set(newURL) { + locationView.url = newURL + line.isHidden = newURL?.isAboutHomeURL ?? true + } + } + + override init(frame: CGRect) { + super.init(frame: frame) + commonInit() + } + + required init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + commonInit() + } + + fileprivate func commonInit() { + locationContainer.addSubview(locationView) + + [scrollToTopButton, line, tabsButton, progressBar, cancelButton, showQRScannerButton].forEach { addSubview($0) } + [menuButton, forwardButton, backButton, stopReloadButton, locationContainer].forEach { addSubview($0) } + + helper = TabToolbarHelper(toolbar: self) + setupConstraints() + + // Make sure we hide any views that shouldn't be showing in non-overlay mode. + updateViewsForOverlayModeAndToolbarChanges() + } + + fileprivate func setupConstraints() { + + line.snp.makeConstraints { make in + make.bottom.leading.trailing.equalTo(self) + make.height.equalTo(1) + } + + scrollToTopButton.snp.makeConstraints { make in + make.top.equalTo(self) + make.left.right.equalTo(self.locationContainer) + } + + progressBar.snp.makeConstraints { make in + make.top.equalTo(self.snp.bottom).inset(URLBarViewUX.ProgressBarHeight / 2) + make.height.equalTo(URLBarViewUX.ProgressBarHeight) + make.left.right.equalTo(self) + } + + locationView.snp.makeConstraints { make in + make.edges.equalTo(self.locationContainer) + } + + cancelButton.snp.makeConstraints { make in + make.centerY.equalTo(self.locationContainer) + make.size.equalTo(URLBarViewUX.ButtonHeight) + make.leading.equalTo(self) + } + + backButton.snp.makeConstraints { make in + make.centerY.equalTo(self) + make.leading.equalTo(self).offset(URLBarViewUX.Padding) + make.size.equalTo(URLBarViewUX.ButtonHeight) + } + + forwardButton.snp.makeConstraints { make in + make.left.equalTo(self.backButton.snp.right) + make.centerY.equalTo(self) + make.size.equalTo(URLBarViewUX.ButtonHeight) + } + + stopReloadButton.snp.makeConstraints { make in + make.left.equalTo(self.forwardButton.snp.right) + make.centerY.equalTo(self) + make.size.equalTo(URLBarViewUX.ButtonHeight) + } + + menuButton.snp.makeConstraints { make in + make.trailing.equalTo(self.snp.trailing).offset(-URLBarViewUX.Padding) + make.centerY.equalTo(self) + make.size.equalTo(URLBarViewUX.ButtonHeight) + } + + tabsButton.snp.makeConstraints { make in + make.trailing.equalTo(self.menuButton.snp.leading) + make.centerY.equalTo(self) + make.size.equalTo(URLBarViewUX.ButtonHeight) + } + + showQRScannerButton.snp.makeConstraints { make in + make.centerY.equalTo(self.locationContainer) + make.trailing.equalTo(self) + make.size.equalTo(URLBarViewUX.ButtonHeight) + } + } + + override func updateConstraints() { + super.updateConstraints() + if inOverlayMode { + // In overlay mode, we always show the location view full width + self.locationContainer.layer.borderWidth = URLBarViewUX.TextFieldBorderWidthSelected + self.locationContainer.snp.remakeConstraints { make in + let height = URLBarViewUX.LocationHeight + (URLBarViewUX.TextFieldBorderWidthSelected * 2) + make.height.equalTo(height) + make.trailing.equalTo(self.showQRScannerButton.snp.leading) + make.leading.equalTo(self.cancelButton.snp.trailing) + make.centerY.equalTo(self) + } + self.locationView.snp.remakeConstraints { make in + make.edges.equalTo(self.locationContainer).inset(UIEdgeInsets(equalInset: URLBarViewUX.TextFieldBorderWidthSelected)) + } + self.locationTextField?.snp.remakeConstraints { make in + make.edges.equalTo(self.locationView).inset(UIEdgeInsets(top: 0, left: URLBarViewUX.LocationLeftPadding, bottom: 0, right: URLBarViewUX.LocationLeftPadding)) + } + } else { + self.locationContainer.snp.remakeConstraints { make in + if self.toolbarIsShowing { + // If we are showing a toolbar, show the text field next to the forward button + make.leading.equalTo(self.stopReloadButton.snp.trailing).offset(URLBarViewUX.Padding) + if self.topTabsIsShowing { + make.trailing.equalTo(self.menuButton.snp.leading).offset(-URLBarViewUX.Padding) + } else { + make.trailing.equalTo(self.tabsButton.snp.leading).offset(-URLBarViewUX.Padding) + } + + } else { + // Otherwise, left align the location view + make.leading.trailing.equalTo(self).inset(UIEdgeInsets(top: 0, left: URLBarViewUX.LocationLeftPadding-1, bottom: 0, right: URLBarViewUX.LocationLeftPadding-1)) + } + + make.height.equalTo(URLBarViewUX.LocationHeight+2) + make.centerY.equalTo(self) + } + self.locationContainer.layer.borderWidth = URLBarViewUX.TextFieldBorderWidth + self.locationView.snp.remakeConstraints { make in + make.edges.equalTo(self.locationContainer).inset(UIEdgeInsets(equalInset: URLBarViewUX.TextFieldBorderWidth)) + } + } + + } + + func showQRScanner() { + self.delegate?.urlBarDidPressQRButton(self) + } + + func createLocationTextField() { + guard locationTextField == nil else { return } + + locationTextField = ToolbarTextField() + + guard let locationTextField = locationTextField else { return } + + locationTextField.translatesAutoresizingMaskIntoConstraints = false + locationTextField.autocompleteDelegate = self + locationTextField.keyboardType = UIKeyboardType.webSearch + locationTextField.autocorrectionType = UITextAutocorrectionType.no + locationTextField.autocapitalizationType = UITextAutocapitalizationType.none + locationTextField.returnKeyType = UIReturnKeyType.go + locationTextField.clearButtonMode = UITextFieldViewMode.whileEditing + locationTextField.font = UIConstants.DefaultChromeFont + locationTextField.accessibilityIdentifier = "address" + locationTextField.accessibilityLabel = NSLocalizedString("Address and Search", comment: "Accessibility label for address and search field, both words (Address, Search) are therefore nouns.") + locationTextField.attributedPlaceholder = self.locationView.placeholder + locationContainer.addSubview(locationTextField) + locationTextField.snp.remakeConstraints { make in + make.edges.equalTo(self.locationView) + } + + locationTextField.applyTheme(currentTheme) + } + + func removeLocationTextField() { + locationTextField?.removeFromSuperview() + locationTextField = nil + } + + // Ideally we'd split this implementation in two, one URLBarView with a toolbar and one without + // However, switching views dynamically at runtime is a difficult. For now, we just use one view + // that can show in either mode. + func setShowToolbar(_ shouldShow: Bool) { + toolbarIsShowing = shouldShow + setNeedsUpdateConstraints() + // when we transition from portrait to landscape, calling this here causes + // the constraints to be calculated too early and there are constraint errors + if !toolbarIsShowing { + updateConstraintsIfNeeded() + } + updateViewsForOverlayModeAndToolbarChanges() + } + + func updateAlphaForSubviews(_ alpha: CGFloat) { + self.locationContainer.alpha = alpha + self.alpha = alpha + } + + func updateProgressBar(_ progress: Float) { + progressBar.alpha = 1 + progressBar.isHidden = false + progressBar.setProgress(progress, animated: !isTransitioning) + } + + func hideProgressBar() { + progressBar.isHidden = true + progressBar.setProgress(0, animated: false) + } + + func updateReaderModeState(_ state: ReaderModeState) { + locationView.readerModeState = state + } + + func setAutocompleteSuggestion(_ suggestion: String?) { + locationTextField?.setAutocompleteSuggestion(suggestion) + } + + func setLocation(_ location: String?, search: Bool) { + locationTextField?.text = location + if search, let location = location, !location.isEmpty { + // Not notifying when empty agrees with AutocompleteTextField.textDidChange. + delegate?.urlBar(self, didEnterText: location) + } + } + + func enterOverlayMode(_ locationText: String?, pasted: Bool, search: Bool) { + createLocationTextField() + + // Show the overlay mode UI, which includes hiding the locationView and replacing it + // with the editable locationTextField. + animateToOverlayState(overlayMode: true) + + delegate?.urlBarDidEnterOverlayMode(self) + + // Bug 1193755 Workaround - Calling becomeFirstResponder before the animation happens + // won't take the initial frame of the label into consideration, which makes the label + // look squished at the start of the animation and expand to be correct. As a workaround, + // we becomeFirstResponder as the next event on UI thread, so the animation starts before we + // set a first responder. + if pasted { + // Clear any existing text, focus the field, then set the actual pasted text. + // This avoids highlighting all of the text. + self.locationTextField?.text = "" + DispatchQueue.main.async { + self.locationTextField?.becomeFirstResponder() + self.setLocation(locationText, search: search) + } + } else { + // Copy the current URL to the editable text field, then activate it. + self.setLocation(locationText, search: search) + DispatchQueue.main.async { + self.locationTextField?.becomeFirstResponder() + } + } + } + + func leaveOverlayMode(didCancel cancel: Bool = false) { + locationTextField?.resignFirstResponder() + animateToOverlayState(overlayMode: false, didCancel: cancel) + delegate?.urlBarDidLeaveOverlayMode(self) + } + + func prepareOverlayAnimation() { + // Make sure everything is showing during the transition (we'll hide it afterwards). + self.bringSubview(toFront: self.locationContainer) + self.cancelButton.isHidden = false + self.showQRScannerButton.isHidden = false + self.progressBar.isHidden = false + self.menuButton.isHidden = !self.toolbarIsShowing + self.forwardButton.isHidden = !self.toolbarIsShowing + self.backButton.isHidden = !self.toolbarIsShowing + self.tabsButton.isHidden = !self.toolbarIsShowing || topTabsIsShowing + self.stopReloadButton.isHidden = !self.toolbarIsShowing + } + + func transitionToOverlay(_ didCancel: Bool = false) { + self.cancelButton.alpha = inOverlayMode ? 1 : 0 + self.showQRScannerButton.alpha = inOverlayMode ? 1 : 0 + self.progressBar.alpha = inOverlayMode || didCancel ? 0 : 1 + self.tabsButton.alpha = inOverlayMode ? 0 : 1 + self.menuButton.alpha = inOverlayMode ? 0 : 1 + self.forwardButton.alpha = inOverlayMode ? 0 : 1 + self.backButton.alpha = inOverlayMode ? 0 : 1 + self.stopReloadButton.alpha = inOverlayMode ? 0 : 1 + + let borderColor = inOverlayMode ? locationActiveBorderColor : locationBorderColor + locationContainer.layer.borderColor = borderColor.cgColor + + if inOverlayMode { + self.line.isHidden = inOverlayMode + // Make the editable text field span the entire URL bar, covering the lock and reader icons. + self.locationTextField?.snp.remakeConstraints { make in + make.edges.equalTo(self.locationView) + } + } else { + // Shrink the editable text field back to the size of the location view before hiding it. + self.locationTextField?.snp.remakeConstraints { make in + make.edges.equalTo(self.locationView.urlTextField) + } + } + } + + func updateViewsForOverlayModeAndToolbarChanges() { + self.cancelButton.isHidden = !inOverlayMode + self.showQRScannerButton.isHidden = !inOverlayMode + self.progressBar.isHidden = inOverlayMode + self.menuButton.isHidden = !self.toolbarIsShowing || inOverlayMode + self.forwardButton.isHidden = !self.toolbarIsShowing || inOverlayMode + self.backButton.isHidden = !self.toolbarIsShowing || inOverlayMode + self.tabsButton.isHidden = !self.toolbarIsShowing || inOverlayMode || topTabsIsShowing + self.stopReloadButton.isHidden = !self.toolbarIsShowing || inOverlayMode + } + + func animateToOverlayState(overlayMode overlay: Bool, didCancel cancel: Bool = false) { + prepareOverlayAnimation() + layoutIfNeeded() + + inOverlayMode = overlay + + if !overlay { + removeLocationTextField() + } + + UIView.animate(withDuration: 0.3, delay: 0.0, usingSpringWithDamping: 0.85, initialSpringVelocity: 0.0, options: [], animations: { _ in + self.transitionToOverlay(cancel) + self.setNeedsUpdateConstraints() + self.layoutIfNeeded() + }, completion: { _ in + self.updateViewsForOverlayModeAndToolbarChanges() + }) + } + + func SELdidClickAddTab() { + delegate?.urlBarDidPressTabs(self) + } + + func SELdidClickCancel() { + leaveOverlayMode(didCancel: true) + } + + func SELtappedScrollToTopArea() { + delegate?.urlBarDidPressScrollToTop(self) + } +} + +extension URLBarView: TabToolbarProtocol { + + func updateBackStatus(_ canGoBack: Bool) { + backButton.isEnabled = canGoBack + } + + func updateForwardStatus(_ canGoForward: Bool) { + forwardButton.isEnabled = canGoForward + } + + func updateTabCount(_ count: Int, animated: Bool = true) { + self.tabsButton.updateTabCount(count, animated: animated) + } + + func updateReloadStatus(_ isLoading: Bool) { + helper?.updateReloadStatus(isLoading) + if isLoading { + stopReloadButton.setImage(helper?.ImageStop, for: .normal) + } else { + stopReloadButton.setImage(helper?.ImageReload, for: .normal) + } + } + + func updatePageStatus(_ isWebPage: Bool) { + stopReloadButton.isEnabled = isWebPage + } + + var access: [Any]? { + get { + if inOverlayMode { + guard let locationTextField = locationTextField else { return nil } + return [locationTextField, cancelButton] + } else { + if toolbarIsShowing { + return [backButton, forwardButton, stopReloadButton, locationView, tabsButton, menuButton, progressBar] + } else { + return [locationView, progressBar] + } + } + } + set { + super.accessibilityElements = newValue + } + } +} + +extension URLBarView: TabLocationViewDelegate { + func tabLocationViewDidLongPressReaderMode(_ tabLocationView: TabLocationView) -> Bool { + return delegate?.urlBarDidLongPressReaderMode(self) ?? false + } + + func tabLocationViewDidTapLocation(_ tabLocationView: TabLocationView) { + guard let (locationText, isSearchQuery) = delegate?.urlBarDisplayTextForURL(locationView.url as URL?) else { return } + + var overlayText = locationText + // Make sure to use the result from urlBarDisplayTextForURL as it is responsible for extracting out search terms when on a search page + if let text = locationText, let url = URL(string: text), let host = url.host, AppConstants.MOZ_PUNYCODE { + overlayText = url.absoluteString.replacingOccurrences(of: host, with: host.asciiHostToUTF8()) + } + enterOverlayMode(overlayText, pasted: false, search: isSearchQuery) + } + + func tabLocationViewDidLongPressLocation(_ tabLocationView: TabLocationView) { + delegate?.urlBarDidLongPressLocation(self) + } + + func tabLocationViewDidTapReload(_ tabLocationView: TabLocationView) { + delegate?.urlBarDidPressReload(self) + } + + func tabLocationViewDidTapStop(_ tabLocationView: TabLocationView) { + delegate?.urlBarDidPressStop(self) + } + + func tabLocationViewDidTapReaderMode(_ tabLocationView: TabLocationView) { + delegate?.urlBarDidPressReaderMode(self) + } + + func tabLocationViewDidTapPageOptions(_ tabLocationView: TabLocationView, from button: UIButton) { + delegate?.urlBarDidPressPageOptions(self, from: tabLocationView.pageOptionsButton) + } + + func tabLocationViewDidLongPressPageOptions(_ tabLocationView: TabLocationView) { + delegate?.urlBarDidLongPressPageOptions(self, from: tabLocationView.pageOptionsButton) + } + + func tabLocationViewLocationAccessibilityActions(_ tabLocationView: TabLocationView) -> [UIAccessibilityCustomAction]? { + return delegate?.urlBarLocationAccessibilityActions(self) + } +} + +extension URLBarView: AutocompleteTextFieldDelegate { + func autocompleteTextFieldShouldReturn(_ autocompleteTextField: AutocompleteTextField) -> Bool { + guard let text = locationTextField?.text else { return true } + if !text.trimmingCharacters(in: .whitespaces).isEmpty { + delegate?.urlBar(self, didSubmitText: text) + return true + } else { + return false + } + } + + func autocompleteTextField(_ autocompleteTextField: AutocompleteTextField, didEnterText text: String) { + delegate?.urlBar(self, didEnterText: text) + } + + func autocompleteTextFieldDidBeginEditing(_ autocompleteTextField: AutocompleteTextField) { + autocompleteTextField.highlightAll() + } + + func autocompleteTextFieldShouldClear(_ autocompleteTextField: AutocompleteTextField) -> Bool { + delegate?.urlBar(self, didEnterText: "") + return true + } +} + +// MARK: UIAppearance +extension URLBarView { + + dynamic var cancelTintColor: UIColor? { + get { return cancelButton.tintColor } + set { return cancelButton.tintColor = newValue } + } + + dynamic var showQRButtonTintColor: UIColor? { + get { return showQRScannerButton.tintColor } + set { return showQRScannerButton.tintColor = newValue } + } + +} + +extension URLBarView: Themeable { + + func applyTheme(_ themeName: String) { + locationView.applyTheme(themeName) + locationTextField?.applyTheme(themeName) + + guard let theme = URLBarViewUX.Themes[themeName] else { + fatalError("Theme not found") + } + + let isPrivate = themeName == Theme.PrivateMode + + progressBar.setGradientColors(startColor: UIConstants.LoadingStartColor.color(isPBM: isPrivate), endColor: UIConstants.LoadingEndColor.color(isPBM: isPrivate)) + currentTheme = themeName + locationBorderColor = theme.borderColor! + locationActiveBorderColor = theme.activeBorderColor! + cancelTintColor = theme.buttonTintColor + showQRButtonTintColor = theme.buttonTintColor + backgroundColor = theme.backgroundColor + self.actionButtons.forEach { $0.applyTheme(themeName) } + tabsButton.applyTheme(themeName) + line.backgroundColor = UIConstants.URLBarDivider.color(isPBM: isPrivate) + locationContainer.layer.shadowColor = self.locationBorderColor.cgColor + } +} + +// We need a subclass so we can setup the shadows correctly +// This subclass creates a strong shadow on the URLBar +class TabLocationContainerView: UIView { + + struct LocationContainerUX { + static let CornerRadius: CGFloat = 4 + static let ShadowRadius: CGFloat = 2 + static let ShadowOpacity: Float = 1 + } + + override init(frame: CGRect) { + super.init(frame: frame) + let layer = self.layer + layer.cornerRadius = LocationContainerUX.CornerRadius + layer.shadowRadius = LocationContainerUX.ShadowRadius + layer.shadowOpacity = LocationContainerUX.ShadowOpacity + layer.masksToBounds = false + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func layoutSubviews() { + let layer = self.layer + + layer.shadowOffset = CGSize(width: 0, height: 1) + // the shadow appears 2px off from the view rect + let shadowLength: CGFloat = 2 + let shadowPath = CGRect(x: shadowLength, y: shadowLength, width: layer.frame.width - (shadowLength * 2), height: layer.frame.height - (shadowLength * 2)) + layer.shadowPath = UIBezierPath(roundedRect: shadowPath, cornerRadius: layer.cornerRadius).cgPath + super.layoutSubviews() + } +} + +class ToolbarTextField: AutocompleteTextField { + static let Themes: [String: Theme] = { + var themes = [String: Theme]() + var theme = Theme() + theme.backgroundColor = UIColor(rgb: 0x636369) + theme.textColor = UIColor.white + theme.buttonTintColor = UIColor.white + theme.highlightColor = UIConstants.PrivateModeInputHighlightColor + themes[Theme.PrivateMode] = theme + + theme = Theme() + theme.backgroundColor = .white + theme.textColor = UIColor(rgb: 0x272727) + theme.highlightColor = AutocompleteTextFieldUX.HighlightColor + themes[Theme.NormalMode] = theme + + return themes + }() + + dynamic var clearButtonTintColor: UIColor? { + didSet { + // Clear previous tinted image that's cache and ask for a relayout + tintedClearImage = nil + setNeedsLayout() + } + } + + fileprivate var tintedClearImage: UIImage? + + override init(frame: CGRect) { + super.init(frame: frame) + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func layoutSubviews() { + super.layoutSubviews() + + // Since we're unable to change the tint color of the clear image, we need to iterate through the + // subviews, find the clear button, and tint it ourselves. Thanks to Mikael Hellman for the tip: + // http://stackoverflow.com/questions/27944781/how-to-change-the-tint-color-of-the-clear-button-on-a-uitextfield + for view in subviews as [UIView] { + if let button = view as? UIButton { + if let image = button.image(for: UIControlState()) { + if tintedClearImage == nil { + tintedClearImage = tintImage(image, color: clearButtonTintColor) + } + + if button.imageView?.image != tintedClearImage { + button.setImage(tintedClearImage, for: UIControlState()) + } + } + } + } + } + + fileprivate func tintImage(_ image: UIImage, color: UIColor?) -> UIImage { + guard let color = color else { return image } + + let size = image.size + + UIGraphicsBeginImageContextWithOptions(size, false, 2) + let context = UIGraphicsGetCurrentContext()! + image.draw(at: CGPoint.zero, blendMode: CGBlendMode.normal, alpha: 1.0) + + context.setFillColor(color.cgColor) + context.setBlendMode(CGBlendMode.sourceIn) + context.setAlpha(1.0) + + let rect = CGRect( + x: CGPoint.zero.x, + y: CGPoint.zero.y, + width: image.size.width, + height: image.size.height) + context.fill(rect) + let tintedImage = UIGraphicsGetImageFromCurrentImageContext()! + UIGraphicsEndImageContext() + + return tintedImage + } +} + +extension ToolbarTextField: Themeable { + func applyTheme(_ themeName: String) { + guard let theme = ToolbarTextField.Themes[themeName] else { + fatalError("Theme not found") + } + + backgroundColor = theme.backgroundColor + textColor = theme.textColor + clearButtonTintColor = theme.buttonTintColor + highlightColor = theme.highlightColor! + } +} diff --git a/mobile/ios/Client/Frontend/ContentBlocker/ContentBlockerHelper.swift b/mobile/ios/Client/Frontend/ContentBlocker/ContentBlockerHelper.swift new file mode 100644 index 0000000000..46c50e96f9 --- /dev/null +++ b/mobile/ios/Client/Frontend/ContentBlocker/ContentBlockerHelper.swift @@ -0,0 +1,310 @@ +/* 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 WebKit +import Shared +import Deferred + +fileprivate let NotificationContentBlockerReloadNeeded = "NotificationContentBlockerReloadNeeded" + +@available(iOS 11.0, *) +class ContentBlockerHelper { + static let PrefKeyEnabledState = "prefkey.trackingprotection.enabled" + static let PrefKeyStrength = "prefkey.trackingprotection.strength" + fileprivate let blocklistBasic = ["disconnect-advertising", "disconnect-analytics", "disconnect-social"] + fileprivate let blocklistStrict = ["disconnect-content"] + fileprivate let ruleStore: WKContentRuleListStore + fileprivate weak var tab: Tab? + fileprivate weak var profile: Profile? + + static var blockImagesRule: WKContentRuleList? + + // Raw values are stored to prefs, be careful changing them. + enum EnabledState: String { + case on + case onInPrivateBrowsing + case off + + var settingTitle: String { + switch self { + case .on: + return Strings.TrackingProtectionOptionAlwaysOn + case .onInPrivateBrowsing: + return Strings.TrackingProtectionOptionOnInPrivateBrowsing + case .off: + return Strings.TrackingProtectionOptionAlwaysOff + } + } + + static let allOptions: [EnabledState] = [.on, .onInPrivateBrowsing, .off] + } + + // Raw values are stored to prefs, be careful changing them. + enum BlockingStrength: String { + case basic + case strict + + var settingTitle: String { + switch self { + case .basic: + return Strings.TrackingProtectionOptionBlockListTypeBasic + case .strict: + return Strings.TrackingProtectionOptionBlockListTypeStrict + } + } + + var subtitle: String { + switch self { + case .basic: + return Strings.TrackingProtectionOptionBlockListTypeBasicDescription + case .strict: + return Strings.TrackingProtectionOptionBlockListTypeStrictDescription + } + } + + static let allOptions: [BlockingStrength] = [.basic, .strict] + } + + static func prefsChanged() { + NotificationCenter.default.post(name: Notification.Name(rawValue: NotificationContentBlockerReloadNeeded), object: nil) + } + + class func name() -> String { + return "ContentBlockerHelper" + } + + private static var heavyInitHasRunOnce = false + + init(tab: Tab, profile: Profile) { + self.ruleStore = WKContentRuleListStore.default() + self.tab = tab + self.profile = profile + + if ContentBlockerHelper.heavyInitHasRunOnce { + return + } + ContentBlockerHelper.heavyInitHasRunOnce = true + + removeOldListsByDateFromStore() { + self.removeOldListsByNameFromStore() { + self.compileListsNotInStore(completion: {}) + } + } + + let blockImages = "[{'trigger':{'url-filter':'.*','resource-type':['image']},'action':{'type':'block'}}]".replacingOccurrences(of: "'", with: "\"") + ruleStore.compileContentRuleList(forIdentifier: "images", encodedContentRuleList: blockImages) { + rule, error in + assert(rule != nil && error == nil) + ContentBlockerHelper.blockImagesRule = rule + } + } + + func setupForWebView() { + NotificationCenter.default.addObserver(self, selector: #selector(ContentBlockerHelper.reloadTab), name: NSNotification.Name(rawValue: NotificationContentBlockerReloadNeeded), object: nil) + addActiveRulesToTab() + } + + deinit { + NotificationCenter.default.removeObserver(self) + } + + @objc func reloadTab() { + addActiveRulesToTab() + } + + fileprivate var blockingStrengthPref: BlockingStrength { + let pref = profile?.prefs.stringForKey(ContentBlockerHelper.PrefKeyStrength) ?? "" + return BlockingStrength(rawValue: pref) ?? .basic + } + + fileprivate var enabledStatePref: EnabledState { + let pref = profile?.prefs.stringForKey(ContentBlockerHelper.PrefKeyEnabledState) ?? "" + return EnabledState(rawValue: pref) ?? .onInPrivateBrowsing + } + + fileprivate func addActiveRulesToTab() { + guard let tab = tab else { return } + removeTrackingProtectionFromTab() + + switch enabledStatePref { + case .off: + return + case .on: + break + case .onInPrivateBrowsing: + if !tab.isPrivate { + return + } + } + + let rules = blocklistBasic + (blockingStrengthPref == .strict ? blocklistStrict : []) + for name in rules { + ruleStore.lookUpContentRuleList(forIdentifier: name) { rule, error in + guard let rule = rule else { + let msg = "lookUpContentRuleList for \(name): \(error?.localizedDescription ?? "empty rules")" + Sentry.shared.send(message: "Content blocker error", tag: .general, description: msg) + return + } + self.addToTab(contentRuleList: rule) + } + } + } + + func removeTrackingProtectionFromTab() { + guard let tab = tab else { return } + tab.webView?.configuration.userContentController.removeAllContentRuleLists() + + if let rule = ContentBlockerHelper.blockImagesRule, tab.noImageMode { + addToTab(contentRuleList: rule) + } + } + + fileprivate func addToTab(contentRuleList: WKContentRuleList) { + tab?.webView?.configuration.userContentController.add(contentRuleList) + } + + func noImageMode(enabled: Bool) { + guard let rule = ContentBlockerHelper.blockImagesRule else { return } + + if enabled { + addToTab(contentRuleList: rule) + } else { + tab?.webView?.configuration.userContentController.remove(rule) + } + + // Async required here to ensure remove() call is processed. + DispatchQueue.main.async() { + self.tab?.webView?.evaluateJavaScript("window.__firefox__.NoImageMode.setEnabled(\(enabled))", completionHandler: nil) + } + } +} + +// MARK: Private initialization code +// The rule store can compile JSON rule files into a private format which is cached on disk. +// On app boot, we need to check if the ruleStore's data is out-of-date, or if the names of the rule files +// no longer match. Finally, any JSON rule files that aren't in the ruleStore need to be compiled and stored in the +// ruleStore. +@available(iOS 11, *) +extension ContentBlockerHelper { + fileprivate func loadJsonFromBundle(forResource file: String, completion: @escaping (_ jsonString: String) -> Void) { + DispatchQueue.global().async { + guard let path = Bundle.main.path(forResource: file, ofType: "json"), + let source = try? NSString(contentsOfFile: path, encoding: String.Encoding.utf8.rawValue) as String else { + return + } + DispatchQueue.main.async { + completion(source) + } + } + } + + fileprivate func lastModifiedSince1970(forFileAtPath path: String) -> Timestamp? { + do { + let url = URL(fileURLWithPath: path) + let attr = try FileManager.default.attributesOfItem(atPath: url.path) + guard let date = attr[FileAttributeKey.modificationDate] as? Date else { return nil } + return UInt64(1000.0 * date.timeIntervalSince1970) + } catch { + return nil + } + } + + fileprivate func dateOfMostRecentBlockerFile() -> Timestamp { + let blocklists = blocklistBasic + blocklistStrict + return blocklists.reduce(Timestamp(0)) { result, filename in + guard let path = Bundle.main.path(forResource: filename, ofType: "json") else { return result } + let date = lastModifiedSince1970(forFileAtPath: path) ?? 0 + return date > result ? date : result + } + } + + fileprivate func removeAllRulesInStore(completion: @escaping () -> Void) { + ruleStore.getAvailableContentRuleListIdentifiers { available in + guard let available = available else { + completion() + return + } + let deferreds: [Deferred] = available.map { filename in + let result = Deferred() + self.ruleStore.removeContentRuleList(forIdentifier: filename) { _ in + result.fill() + } + return result + } + all(deferreds).uponQueue(.main) { _ in + completion() + } + } + } + + // If any blocker files are newer than the date saved in prefs, + // remove all the content blockers and reload them. + fileprivate func removeOldListsByDateFromStore(completion: @escaping () -> Void) { + let fileDate = self.dateOfMostRecentBlockerFile() + let prefsNewestDate = profile?.prefs.longForKey("blocker-file-date") ?? 0 + if prefsNewestDate < 1 || fileDate <= prefsNewestDate { + completion() + return + } + + profile?.prefs.setTimestamp(fileDate, forKey: "blocker-file-date") + self.removeAllRulesInStore() { + completion() + } + } + + fileprivate func removeOldListsByNameFromStore(completion: @escaping () -> Void) { + var noMatchingIdentifierFoundForRule = false + + ruleStore.getAvailableContentRuleListIdentifiers { available in + guard let available = available else { + completion() + return + } + + let blocklists = self.blocklistBasic + self.blocklistStrict + for contentRuleIdentifier in available { + if !blocklists.contains(where: { $0 == contentRuleIdentifier }) { + noMatchingIdentifierFoundForRule = true + break + } + } + + let fileDate = self.dateOfMostRecentBlockerFile() + let prefsNewestDate = self.profile?.prefs.timestampForKey("blocker-file-date") ?? 0 + if prefsNewestDate > 0 && fileDate <= prefsNewestDate && !noMatchingIdentifierFoundForRule { + completion() + return + } + self.profile?.prefs.setTimestamp(fileDate, forKey: "blocker-file-date") + + self.removeAllRulesInStore { + completion() + } + } + } + + fileprivate func compileListsNotInStore(completion: @escaping () -> Void) { + let blocklists = blocklistBasic + blocklistStrict + let deferreds: [Deferred] = blocklists.map { filename in + let result = Deferred() + ruleStore.lookUpContentRuleList(forIdentifier: filename) { contentRuleList, error in + if contentRuleList != nil { + result.fill() + return + } + self.loadJsonFromBundle(forResource: filename) { jsonString in + self.ruleStore.compileContentRuleList(forIdentifier: filename, encodedContentRuleList: jsonString) { _, _ in + result.fill() + } + } + } + return result + } + + all(deferreds).uponQueue(.main) { _ in + completion() + } + } +} diff --git a/mobile/ios/Client/Frontend/ContentBlocker/ContentBlockerSettingViewController.swift b/mobile/ios/Client/Frontend/ContentBlocker/ContentBlockerSettingViewController.swift new file mode 100644 index 0000000000..f497a36f0a --- /dev/null +++ b/mobile/ios/Client/Frontend/ContentBlocker/ContentBlockerSettingViewController.swift @@ -0,0 +1,99 @@ +/* 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 + +@available(iOS 11.0, *) +class ContentBlockerSettingsTableView: SettingsTableViewController { + // The first section header gets a More Info link + override func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? { + if section == 0 { + return super.tableView(tableView, viewForFooterInSection: section) + } + + // TODO: Get a dedicated string for this. + let title = NSLocalizedString("More Info…", tableName: "SendAnonymousUsageData", comment: "Re-using more info label from 'anonymous usage data' item for showing a 'More Info' link on the Tracking Protection settings screen.") + + var attributes = [String: AnyObject]() + attributes[NSFontAttributeName] = UIFont.systemFont(ofSize: 12, weight: UIFontWeightRegular) + attributes[NSForegroundColorAttributeName] = UIConstants.HighlightBlue + + let button = UIButton() + button.setAttributedTitle(NSAttributedString(string: title, attributes: attributes), for: .normal) + button.contentHorizontalAlignment = .left + // Top and left insets are needed to match the table row style. + button.contentEdgeInsets = UIEdgeInsets(top: 8, left: 16, bottom: 0, right: 0) + button.addTarget(self, action: #selector(ContentBlockerSettingsTableView.moreInfoTapped), for: .touchUpInside) + return button + } + + func moreInfoTapped() { + let viewController = SettingsContentViewController() + viewController.url = SupportUtils.URLForTopic("tracking-protection-ios") + navigationController?.pushViewController(viewController, animated: true) + } +} + +@available(iOS 11.0, *) +class ContentBlockerSettingViewController: ContentBlockerSettingsTableView { + let prefs: Prefs + let EnabledStates = ContentBlockerHelper.EnabledState.allOptions + let BlockingStrengths = ContentBlockerHelper.BlockingStrength.allOptions + var currentEnabledState: ContentBlockerHelper.EnabledState + var currentBlockingStrength: ContentBlockerHelper.BlockingStrength + + init(prefs: Prefs) { + self.prefs = prefs + currentEnabledState = ContentBlockerHelper.EnabledState(rawValue: prefs.stringForKey(ContentBlockerHelper.PrefKeyEnabledState) ?? "") ?? .onInPrivateBrowsing + currentBlockingStrength = ContentBlockerHelper.BlockingStrength(rawValue: prefs.stringForKey(ContentBlockerHelper.PrefKeyStrength) ?? "") ?? .basic + + super.init(style: .grouped) + + self.title = Strings.SettingsTrackingProtectionSectionName + hasSectionSeparatorLine = false + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func generateSettings() -> [SettingSection] { + let enabledSetting: [CheckmarkSetting] = EnabledStates.map { option in + return CheckmarkSetting(title: NSAttributedString(string: option.settingTitle), subtitle: nil, isEnabled: { + return option == self.currentEnabledState + }, onChanged: { + self.currentEnabledState = option + self.prefs.setString(self.currentEnabledState.rawValue, forKey: ContentBlockerHelper.PrefKeyEnabledState) + self.tableView.reloadData() + ContentBlockerHelper.prefsChanged() + + LeanPlumClient.shared.track(event: .trackingProtectionSettings, withParameters: ["Enabled option": option.rawValue as AnyObject]) + UnifiedTelemetry.recordEvent(category: .action, method: .change, object: .setting, value: ContentBlockerHelper.PrefKeyEnabledState, extras: ["to": option.rawValue]) + }) + } + + let strengthSetting: [CheckmarkSetting] = BlockingStrengths.map { option in + return CheckmarkSetting(title: NSAttributedString(string: option.settingTitle), subtitle: NSAttributedString(string: option.subtitle), isEnabled: { + return option == self.currentBlockingStrength + }, onChanged: { + self.currentBlockingStrength = option + self.prefs.setString(self.currentBlockingStrength.rawValue, forKey: ContentBlockerHelper.PrefKeyStrength) + self.tableView.reloadData() + ContentBlockerHelper.prefsChanged() + + LeanPlumClient.shared.track(event: .trackingProtectionSettings, withParameters: ["Strength option": option.rawValue as AnyObject]) + UnifiedTelemetry.recordEvent(category: .action, method: .change, object: .setting, value: ContentBlockerHelper.PrefKeyStrength, extras: ["to": option.rawValue]) + }) + } + + let firstSection = SettingSection(title: NSAttributedString(string: Strings.TrackingProtectionOptionOnOffHeader), footerTitle: NSAttributedString(string: Strings.TrackingProtectionOptionOnOffFooter), children: enabledSetting) + + // The bottom of the block lists section has a More Info button, implemented as a custom footer view, + // SettingSection needs footerTitle set to create a footer, which we then override the view for. + let blockListsTitle = Strings.TrackingProtectionOptionBlockListsTitle + let secondSection = SettingSection(title: NSAttributedString(string: blockListsTitle), footerTitle: NSAttributedString(string: "placeholder replaced with UIButton"), children: strengthSetting) + return [firstSection, secondSection] + } +} diff --git a/mobile/ios/Client/Frontend/ContentBlocker/disconnect-advertising.json b/mobile/ios/Client/Frontend/ContentBlocker/disconnect-advertising.json new file mode 100644 index 0000000000..13520936c9 --- /dev/null +++ b/mobile/ios/Client/Frontend/ContentBlocker/disconnect-advertising.json @@ -0,0 +1 @@ +[{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?2leep\\.com","load-type":["third-party"],"unless-domain":["*2leep.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?33across\\.com","load-type":["third-party"],"unless-domain":["*33across.com","*tynt.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?4info\\.com","load-type":["third-party"],"unless-domain":["*4info.com","*adhaven.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?adhaven\\.com","load-type":["third-party"],"unless-domain":["*4info.com","*adhaven.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?4mads\\.com","load-type":["third-party"],"unless-domain":["*4mads.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?adeurope\\.com","load-type":["third-party"],"unless-domain":["*adeurope.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?ad2onegroup\\.com","load-type":["third-party"],"unless-domain":["*ad2onegroup.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?adition\\.com","load-type":["third-party"],"unless-domain":["*adition.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?admission\\.net","load-type":["third-party"],"unless-domain":["*adpdealerservices.com","*admission.net","*cobalt.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?adpdealerservices\\.com","load-type":["third-party"],"unless-domain":["*adpdealerservices.com","*admission.net","*cobalt.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?cobalt\\.com","load-type":["third-party"],"unless-domain":["*adpdealerservices.com","*admission.net","*cobalt.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?adtech\\.com","load-type":["third-party"],"unless-domain":["*adtech.com","*adtech.de","*adtechus.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?adtech\\.de","load-type":["third-party"],"unless-domain":["*adtech.com","*adtech.de","*adtechus.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?adtechus\\.com","load-type":["third-party"],"unless-domain":["*adtech.com","*adtech.de","*adtechus.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?adtelligence\\.de","load-type":["third-party"],"unless-domain":["*adtelligence.de"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?adzcentral\\.com","load-type":["third-party"],"unless-domain":["*adzcentral.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?aerifymedia\\.com","load-type":["third-party"],"unless-domain":["*aerifymedia.com","*anonymous-media.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?anonymous-media\\.com","load-type":["third-party"],"unless-domain":["*aerifymedia.com","*anonymous-media.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?aggregateknowledge\\.com","load-type":["third-party"],"unless-domain":["*aggregateknowledge.com","*agkn.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?agkn\\.com","load-type":["third-party"],"unless-domain":["*aggregateknowledge.com","*agkn.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?adsonar\\.com","load-type":["third-party"],"unless-domain":["*aol.com","*adsonar.com","*advertising.com","*atwola.com","*leadback.com","*tacoda.net","*5min.com","*aim.com","*aolcdn.com","*aoltechguru.com","*autoblog.com","*cambio.com","*dailyfinance.com","*editions.com","*engadget.com","*games.com","*homesessive.com","*huffingtonpost.com","*joystiq.com","*kitchendaily.com","*makers.com","*mandatory.com","*mapquest.com","*moviefone.com","*noisecreep.com","*patch.com","*pawnation.com","*shortcuts.com","*shoutcast.com","*spinner.com","*stylelist.com","*stylemepretty.com","*surphace.com","*techcrunch.com","*theboombox.com","*theboot.com","*tuaw.com","*userplane.com","*winamp.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?advertising\\.com","load-type":["third-party"],"unless-domain":["*aol.com","*adsonar.com","*advertising.com","*atwola.com","*leadback.com","*tacoda.net","*5min.com","*aim.com","*aolcdn.com","*aoltechguru.com","*autoblog.com","*cambio.com","*dailyfinance.com","*editions.com","*engadget.com","*games.com","*homesessive.com","*huffingtonpost.com","*joystiq.com","*kitchendaily.com","*makers.com","*mandatory.com","*mapquest.com","*moviefone.com","*noisecreep.com","*patch.com","*pawnation.com","*shortcuts.com","*shoutcast.com","*spinner.com","*stylelist.com","*stylemepretty.com","*surphace.com","*techcrunch.com","*theboombox.com","*theboot.com","*tuaw.com","*userplane.com","*winamp.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?atwola\\.com","load-type":["third-party"],"unless-domain":["*aol.com","*adsonar.com","*advertising.com","*atwola.com","*leadback.com","*tacoda.net","*5min.com","*aim.com","*aolcdn.com","*aoltechguru.com","*autoblog.com","*cambio.com","*dailyfinance.com","*editions.com","*engadget.com","*games.com","*homesessive.com","*huffingtonpost.com","*joystiq.com","*kitchendaily.com","*makers.com","*mandatory.com","*mapquest.com","*moviefone.com","*noisecreep.com","*patch.com","*pawnation.com","*shortcuts.com","*shoutcast.com","*spinner.com","*stylelist.com","*stylemepretty.com","*surphace.com","*techcrunch.com","*theboombox.com","*theboot.com","*tuaw.com","*userplane.com","*winamp.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?leadback\\.com","load-type":["third-party"],"unless-domain":["*aol.com","*adsonar.com","*advertising.com","*atwola.com","*leadback.com","*tacoda.net","*5min.com","*aim.com","*aolcdn.com","*aoltechguru.com","*autoblog.com","*cambio.com","*dailyfinance.com","*editions.com","*engadget.com","*games.com","*homesessive.com","*huffingtonpost.com","*joystiq.com","*kitchendaily.com","*makers.com","*mandatory.com","*mapquest.com","*moviefone.com","*noisecreep.com","*patch.com","*pawnation.com","*shortcuts.com","*shoutcast.com","*spinner.com","*stylelist.com","*stylemepretty.com","*surphace.com","*techcrunch.com","*theboombox.com","*theboot.com","*tuaw.com","*userplane.com","*winamp.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?tacoda\\.net","load-type":["third-party"],"unless-domain":["*aol.com","*adsonar.com","*advertising.com","*atwola.com","*leadback.com","*tacoda.net","*5min.com","*aim.com","*aolcdn.com","*aoltechguru.com","*autoblog.com","*cambio.com","*dailyfinance.com","*editions.com","*engadget.com","*games.com","*homesessive.com","*huffingtonpost.com","*joystiq.com","*kitchendaily.com","*makers.com","*mandatory.com","*mapquest.com","*moviefone.com","*noisecreep.com","*patch.com","*pawnation.com","*shortcuts.com","*shoutcast.com","*spinner.com","*stylelist.com","*stylemepretty.com","*surphace.com","*techcrunch.com","*theboombox.com","*theboot.com","*tuaw.com","*userplane.com","*winamp.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?hit-parade\\.com","load-type":["third-party"],"unless-domain":["*atinternet.com","*hit-parade.com","*xiti.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?att\\.com","load-type":["third-party"],"unless-domain":["*att.com","*yp.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?yp\\.com","load-type":["third-party"],"unless-domain":["*att.com","*yp.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?affiliatetracking\\.com","load-type":["third-party"],"unless-domain":["*affiliatetracking.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?am\\.ua","load-type":["third-party"],"unless-domain":["*autocentre.ua","*am.ua"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?autocentre\\.ua","load-type":["third-party"],"unless-domain":["*autocentre.ua","*am.ua"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?aweber\\.com","load-type":["third-party"],"unless-domain":["*aweber.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?abaxinteractive\\.com","load-type":["third-party"],"unless-domain":["*abaxinteractive.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?accelia\\.net","load-type":["third-party"],"unless-domain":["*accelia.net","*durasite.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?durasite\\.net","load-type":["third-party"],"unless-domain":["*accelia.net","*durasite.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?accordantmedia\\.com","load-type":["third-party"],"unless-domain":["*accordantmedia.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?acquisio\\.com","load-type":["third-party"],"unless-domain":["*acquisio.com","*clickequations.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?clickequations\\.net","load-type":["third-party"],"unless-domain":["*acquisio.com","*clickequations.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?act-on\\.com","load-type":["third-party"],"unless-domain":["*act-on.com","*actonsoftware.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?actonsoftware\\.com","load-type":["third-party"],"unless-domain":["*act-on.com","*actonsoftware.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?actisens\\.com","load-type":["third-party"],"unless-domain":["*actisens.com","*gestionpub.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?gestionpub\\.com","load-type":["third-party"],"unless-domain":["*actisens.com","*gestionpub.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?activeconversion\\.com","load-type":["third-party"],"unless-domain":["*activeconversion.com","*activemeter.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?activemeter\\.com","load-type":["third-party"],"unless-domain":["*activeconversion.com","*activemeter.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?acuity\\.com","load-type":["third-party"],"unless-domain":["*acuity.com","*acuityads.com","*acuityplatform.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?acuityads\\.com","load-type":["third-party"],"unless-domain":["*acuity.com","*acuityads.com","*acuityplatform.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?acuityplatform\\.com","load-type":["third-party"],"unless-domain":["*acuity.com","*acuityads.com","*acuityplatform.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?a2dfp\\.net","load-type":["third-party"],"unless-domain":["*addecisive.com","*a2dfp.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?addecisive\\.com","load-type":["third-party"],"unless-domain":["*addecisive.com","*a2dfp.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?addynamo\\.com","load-type":["third-party"],"unless-domain":["*addynamo.com","*addynamo.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?addynamo\\.net","load-type":["third-party"],"unless-domain":["*addynamo.com","*addynamo.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?adknife\\.com","load-type":["third-party"],"unless-domain":["*adknife.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?admagnet\\.com","load-type":["third-party"],"unless-domain":["*admagnet.com","*admagnet.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?admagnet\\.net","load-type":["third-party"],"unless-domain":["*admagnet.com","*admagnet.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?ad4game\\.com","load-type":["third-party"],"unless-domain":["*ad4game.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?adbrain\\.com","load-type":["third-party"],"unless-domain":["*adbrain.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?adbrn\\.com","load-type":["third-party"],"unless-domain":["*adbrain.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?adcirrus\\.com","load-type":["third-party"],"unless-domain":["*adcirrus.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?adengage\\.com","load-type":["third-party"],"unless-domain":["*adengage.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?adextent\\.com","load-type":["third-party"],"unless-domain":["*adextent.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?adf\\.ly","load-type":["third-party"],"unless-domain":["*adf.ly"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?adfox\\.ru","load-type":["third-party"],"unless-domain":["*adfox.ru"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?adfrontiers\\.com","load-type":["third-party"],"unless-domain":["*adfrontiers.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?adgentdigital\\.com","load-type":["third-party"],"unless-domain":["*adgentdigital.com","*shorttailmedia.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?shorttailmedia\\.com","load-type":["third-party"],"unless-domain":["*adgentdigital.com","*shorttailmedia.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?adgibbon\\.com","load-type":["third-party"],"unless-domain":["*adgibbon.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?adiquity\\.com","load-type":["third-party"],"unless-domain":["*adiquity.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?adinsight\\.com","load-type":["third-party"],"unless-domain":["*adinsight.com","*adinsight.eu"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?adinsight\\.eu","load-type":["third-party"],"unless-domain":["*adinsight.com","*adinsight.eu"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?adjug\\.com","load-type":["third-party"],"unless-domain":["*adjug.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?adjuggler\\.com","load-type":["third-party"],"unless-domain":["*adjuggler.com","*adjuggler.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?adjuggler\\.net","load-type":["third-party"],"unless-domain":["*adjuggler.com","*adjuggler.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?adkeeper\\.com","load-type":["third-party"],"unless-domain":["*adkeeper.com","*akncdn.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?akncdn\\.com","load-type":["third-party"],"unless-domain":["*adkeeper.com","*akncdn.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?adimg\\.net","load-type":["third-party"],"unless-domain":["*www.adlantis.jp","*adimg.net","*adlantis.jp"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?adlantis\\.jp","load-type":["third-party"],"unless-domain":["*www.adlantis.jp","*adimg.net","*adlantis.jp"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?adleave\\.com","load-type":["third-party"],"unless-domain":["*adleave.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?admarvel\\.com","load-type":["third-party"],"unless-domain":["*admarvel.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?admaximizer\\.com","load-type":["third-party"],"unless-domain":["*admaximizer.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?admedia\\.com","load-type":["third-party"],"unless-domain":["*admedia.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?adnetwork\\.net","load-type":["third-party"],"unless-domain":["*adnetwork.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?adocean-global\\.com","load-type":["third-party"],"unless-domain":["*adocean-global.com","*adocean.pl"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?adocean\\.pl","load-type":["third-party"],"unless-domain":["*adocean-global.com","*adocean.pl"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?adonnetwork\\.com","load-type":["third-party"],"unless-domain":["*adonnetwork.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?dashboardad\\.net","load-type":["third-party"],"unless-domain":["*adonnetwork.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?adonion\\.com","load-type":["third-party"],"unless-domain":["*adonion.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?adperfect\\.com","load-type":["third-party"],"unless-domain":["*adperfect.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?adpredictive\\.com","load-type":["third-party"],"unless-domain":["*adpredictive.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?adreactor\\.com","load-type":["third-party"],"unless-domain":["*adreactor.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?adready\\.com","load-type":["third-party"],"unless-domain":["*adready.com","*adreadytractions.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?adreadytractions\\.com","load-type":["third-party"],"unless-domain":["*adready.com","*adreadytractions.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?adrevolution\\.com","load-type":["third-party"],"unless-domain":["*adrevolution.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?adriver\\.ru","load-type":["third-party"],"unless-domain":["*adriver.ru"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?adroll\\.com","load-type":["third-party"],"unless-domain":["*adroll.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?adsafemedia\\.com","load-type":["third-party"],"unless-domain":["*adsafemedia.com","*adsafeprotected.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?adsafeprotected\\.com","load-type":["third-party"],"unless-domain":["*adsafemedia.com","*adsafeprotected.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?adserverpub\\.com","load-type":["third-party"],"unless-domain":["*adserverpub.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?adshuffle\\.com","load-type":["third-party"],"unless-domain":["*adshuffle.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?adside\\.com","load-type":["third-party"],"unless-domain":["*adside.com","*doclix.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?doclix\\.com","load-type":["third-party"],"unless-domain":["*adside.com","*doclix.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?adsmart\\.com","load-type":["third-party"],"unless-domain":["*adsmart.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?adspeed\\.com","load-type":["third-party"],"unless-domain":["*adspeed.com","*adspeed.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?adspeed\\.net","load-type":["third-party"],"unless-domain":["*adspeed.com","*adspeed.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?adspirit\\.com","load-type":["third-party"],"unless-domain":["*adspirit.de","*adspirit.com","*adspirit.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?adspirit\\.de","load-type":["third-party"],"unless-domain":["*adspirit.de","*adspirit.com","*adspirit.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?adspirit\\.net","load-type":["third-party"],"unless-domain":["*adspirit.de","*adspirit.com","*adspirit.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?adtiger\\.de","load-type":["third-party"],"unless-domain":["*adtiger.de"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?adtruth\\.com","load-type":["third-party"],"unless-domain":["*adtruth.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?adxpansion\\.com","load-type":["third-party"],"unless-domain":["*adxpansion.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?adaptiveads\\.com","load-type":["third-party"],"unless-domain":["*adaptiveads.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?adaptly\\.com","load-type":["third-party"],"unless-domain":["*adaptly.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?adaramedia\\.com","load-type":["third-party"],"unless-domain":["*adaramedia.com","*opinmind.com","*yieldoptimizer.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?opinmind\\.com","load-type":["third-party"],"unless-domain":["*adaramedia.com","*opinmind.com","*yieldoptimizer.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?yieldoptimizer\\.com","load-type":["third-party"],"unless-domain":["*adaramedia.com","*opinmind.com","*yieldoptimizer.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?adatus\\.com","load-type":["third-party"],"unless-domain":["*adatus.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?adbroker\\.de","load-type":["third-party"],"unless-domain":["*adbroker.de"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?adchemy\\.com","load-type":["third-party"],"unless-domain":["*adchemy.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?adconion\\.com","load-type":["third-party"],"unless-domain":["*adconion.com","*amgdgt.com","*euroclick.com","*smartclip.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?amgdgt\\.com","load-type":["third-party"],"unless-domain":["*adconion.com","*amgdgt.com","*euroclick.com","*smartclip.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?euroclick\\.com","load-type":["third-party"],"unless-domain":["*adconion.com","*amgdgt.com","*euroclick.com","*smartclip.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?smartclip\\.com","load-type":["third-party"],"unless-domain":["*adconion.com","*amgdgt.com","*euroclick.com","*smartclip.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?addvantagemedia\\.com","load-type":["third-party"],"unless-domain":["*addvantagemedia.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?adfonic\\.com","load-type":["third-party"],"unless-domain":["*adfonic.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?adform\\.com","load-type":["third-party"],"unless-domain":["*adform.com","*adform.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?adform\\.net","load-type":["third-party"],"unless-domain":["*adform.com","*adform.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?adfunky\\.com","load-type":["third-party"],"unless-domain":["*adfunky.com","*adfunkyserver.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?adfunkyserver\\.com","load-type":["third-party"],"unless-domain":["*adfunky.com","*adfunkyserver.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?adfusion\\.com","load-type":["third-party"],"unless-domain":["*adfusion.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?adblade\\.com","load-type":["third-party"],"unless-domain":["*adiant.com","*adblade.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?adiant\\.com","load-type":["third-party"],"unless-domain":["*adiant.com","*adblade.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?adknowledge\\.com","load-type":["third-party"],"unless-domain":["*adknowledge.com","*adparlor.com","*bidsystem.com","*cubics.com","*lookery.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?adparlor\\.com","load-type":["third-party"],"unless-domain":["*adknowledge.com","*adparlor.com","*bidsystem.com","*cubics.com","*lookery.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?bidsystem\\.com","load-type":["third-party"],"unless-domain":["*adknowledge.com","*adparlor.com","*bidsystem.com","*cubics.com","*lookery.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?cubics\\.com","load-type":["third-party"],"unless-domain":["*adknowledge.com","*adparlor.com","*bidsystem.com","*cubics.com","*lookery.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?lookery\\.com","load-type":["third-party"],"unless-domain":["*adknowledge.com","*adparlor.com","*bidsystem.com","*cubics.com","*lookery.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?adlibrium\\.com","load-type":["third-party"],"unless-domain":["*adlibrium.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?admeta\\.com","load-type":["third-party"],"unless-domain":["*admeta.com","*atemda.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?atemda\\.com","load-type":["third-party"],"unless-domain":["*admeta.com","*atemda.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?admicro\\.vn","load-type":["third-party"],"unless-domain":["*admicro.vn","*vcmedia.vn"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?vcmedia\\.vn","load-type":["third-party"],"unless-domain":["*admicro.vn","*vcmedia.vn"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?admobile\\.com","load-type":["third-party"],"unless-domain":["*admobile.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?admotion\\.com","load-type":["third-party"],"unless-domain":["*admotion.com","*nspmotion.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?nspmotion\\.com","load-type":["third-party"],"unless-domain":["*admotion.com","*nspmotion.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?adnetik\\.com","load-type":["third-party"],"unless-domain":["*adnetik.com","*wtp101.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?wtp101\\.com","load-type":["third-party"],"unless-domain":["*adnetik.com","*wtp101.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?2o7\\.net","load-type":["third-party"],"unless-domain":["*adobe.com","*2o7.net","*auditude.com","*demdex.com","*demdex.net","*dmtracker.com","*efrontier.com","*everestads.net","*everestjs.net","*everesttech.net","*hitbox.com","*omniture.com","*omtrdc.net","*touchclarity.com","*typekit.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?auditude\\.com","load-type":["third-party"],"unless-domain":["*adobe.com","*2o7.net","*auditude.com","*demdex.com","*demdex.net","*dmtracker.com","*efrontier.com","*everestads.net","*everestjs.net","*everesttech.net","*hitbox.com","*omniture.com","*omtrdc.net","*touchclarity.com","*typekit.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?demdex\\.com","load-type":["third-party"],"unless-domain":["*adobe.com","*2o7.net","*auditude.com","*demdex.com","*demdex.net","*dmtracker.com","*efrontier.com","*everestads.net","*everestjs.net","*everesttech.net","*hitbox.com","*omniture.com","*omtrdc.net","*touchclarity.com","*typekit.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?demdex\\.net","load-type":["third-party"],"unless-domain":["*adobe.com","*2o7.net","*auditude.com","*demdex.com","*demdex.net","*dmtracker.com","*efrontier.com","*everestads.net","*everestjs.net","*everesttech.net","*hitbox.com","*omniture.com","*omtrdc.net","*touchclarity.com","*typekit.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?dmtracker\\.com","load-type":["third-party"],"unless-domain":["*adobe.com","*2o7.net","*auditude.com","*demdex.com","*demdex.net","*dmtracker.com","*efrontier.com","*everestads.net","*everestjs.net","*everesttech.net","*hitbox.com","*omniture.com","*omtrdc.net","*touchclarity.com","*typekit.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?efrontier\\.com","load-type":["third-party"],"unless-domain":["*adobe.com","*2o7.net","*auditude.com","*demdex.com","*demdex.net","*dmtracker.com","*efrontier.com","*everestads.net","*everestjs.net","*everesttech.net","*hitbox.com","*omniture.com","*omtrdc.net","*touchclarity.com","*typekit.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?everestads\\.net","load-type":["third-party"],"unless-domain":["*adobe.com","*2o7.net","*auditude.com","*demdex.com","*demdex.net","*dmtracker.com","*efrontier.com","*everestads.net","*everestjs.net","*everesttech.net","*hitbox.com","*omniture.com","*omtrdc.net","*touchclarity.com","*typekit.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?everestjs\\.net","load-type":["third-party"],"unless-domain":["*adobe.com","*2o7.net","*auditude.com","*demdex.com","*demdex.net","*dmtracker.com","*efrontier.com","*everestads.net","*everestjs.net","*everesttech.net","*hitbox.com","*omniture.com","*omtrdc.net","*touchclarity.com","*typekit.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?everesttech\\.net","load-type":["third-party"],"unless-domain":["*adobe.com","*2o7.net","*auditude.com","*demdex.com","*demdex.net","*dmtracker.com","*efrontier.com","*everestads.net","*everestjs.net","*everesttech.net","*hitbox.com","*omniture.com","*omtrdc.net","*touchclarity.com","*typekit.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?hitbox\\.com","load-type":["third-party"],"unless-domain":["*adobe.com","*2o7.net","*auditude.com","*demdex.com","*demdex.net","*dmtracker.com","*efrontier.com","*everestads.net","*everestjs.net","*everesttech.net","*hitbox.com","*omniture.com","*omtrdc.net","*touchclarity.com","*typekit.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?omniture\\.com","load-type":["third-party"],"unless-domain":["*adobe.com","*2o7.net","*auditude.com","*demdex.com","*demdex.net","*dmtracker.com","*efrontier.com","*everestads.net","*everestjs.net","*everesttech.net","*hitbox.com","*omniture.com","*omtrdc.net","*touchclarity.com","*typekit.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?omtrdc\\.net","load-type":["third-party"],"unless-domain":["*adobe.com","*2o7.net","*auditude.com","*demdex.com","*demdex.net","*dmtracker.com","*efrontier.com","*everestads.net","*everestjs.net","*everesttech.net","*hitbox.com","*omniture.com","*omtrdc.net","*touchclarity.com","*typekit.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?touchclarity\\.com","load-type":["third-party"],"unless-domain":["*adobe.com","*2o7.net","*auditude.com","*demdex.com","*demdex.net","*dmtracker.com","*efrontier.com","*everestads.net","*everestjs.net","*everesttech.net","*hitbox.com","*omniture.com","*omtrdc.net","*touchclarity.com","*typekit.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?adometry\\.com","load-type":["third-party"],"unless-domain":["*adometry.com","*dmtry.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?dmtry\\.com","load-type":["third-party"],"unless-domain":["*adometry.com","*dmtry.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?clickotmedia\\.com","load-type":["third-party"],"unless-domain":["*clickotmedia.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?adperium\\.com","load-type":["third-party"],"unless-domain":["*adperium.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?adpersia\\.com","load-type":["third-party"],"unless-domain":["*adpersia.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?adstours\\.com","load-type":["third-party"],"unless-domain":["*adstours.com","*clickintext.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?clickintext\\.net","load-type":["third-party"],"unless-domain":["*adstours.com","*clickintext.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?adsnative\\.com","load-type":["third-party"],"unless-domain":["*adsnative.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?adsrevenue\\.net","load-type":["third-party"],"unless-domain":["*adsrevenue.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?adtegrity\\.com","load-type":["third-party"],"unless-domain":["*adtegrity.com","*adtegrity.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?adtegrity\\.net","load-type":["third-party"],"unless-domain":["*adtegrity.com","*adtegrity.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?adultadworld\\.com","load-type":["third-party"],"unless-domain":["*adultadworld.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?adultmoda\\.com","load-type":["third-party"],"unless-domain":["*adultmoda.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?adnext\\.fr","load-type":["third-party"],"unless-domain":["*adverline.com","*adnext.fr"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?adverline\\.com","load-type":["third-party"],"unless-domain":["*adverline.com","*adnext.fr"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?adversal\\.com","load-type":["third-party"],"unless-domain":["*adversal.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?adv-adserver\\.com","load-type":["third-party"],"unless-domain":["*adversal.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?advertstream\\.com","load-type":["third-party"],"unless-domain":["*advertstream.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?adverticum\\.com","load-type":["third-party"],"unless-domain":["*adverticum.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?adverticum\\.net","load-type":["third-party"],"unless-domain":["*adverticum.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?advertise\\.com","load-type":["third-party"],"unless-domain":["*advertise.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?advertisespace\\.com","load-type":["third-party"],"unless-domain":["*advertisespace.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?advisormedia\\.cz","load-type":["third-party"],"unless-domain":["*advisormedia.cz"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?adworx\\.at","load-type":["third-party"],"unless-domain":["*adworx.at"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?adworx\\.be","load-type":["third-party"],"unless-domain":["*adworx.at"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?adworx\\.nl","load-type":["third-party"],"unless-domain":["*adworx.at"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?adxvalue\\.com","load-type":["third-party"],"unless-domain":["*adxvalue.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?adxvalue\\.de","load-type":["third-party"],"unless-domain":["*adxvalue.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?adzerk\\.com","load-type":["third-party"],"unless-domain":["*adzerk.com","*adzerk.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?adzerk\\.net","load-type":["third-party"],"unless-domain":["*adzerk.com","*adzerk.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?aemedia\\.com","load-type":["third-party"],"unless-domain":["*aemedia.com","*bluestreak.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?bluestreak\\.com","load-type":["third-party"],"unless-domain":["*aemedia.com","*bluestreak.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?affectv\\.co\\.uk","load-type":["third-party"],"unless-domain":["*affectv.co.uk"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?affine\\.tv","load-type":["third-party"],"unless-domain":["*affine.tv","*affinesystems.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?affinesystems\\.com","load-type":["third-party"],"unless-domain":["*affine.tv","*affinesystems.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?afdads\\.com","load-type":["third-party"],"unless-domain":["*afterdownload.com","*afdads.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?afterdownload\\.com","load-type":["third-party"],"unless-domain":["*afterdownload.com","*afdads.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?aim4media\\.com","load-type":["third-party"],"unless-domain":["*aim4media.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?airpush\\.com","load-type":["third-party"],"unless-domain":["*airpush.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?imiclk\\.com","load-type":["third-party"],"unless-domain":["*akamai.com","*imiclk.com","*abmr.net","*edgesuite.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?allstarmediagroup\\.com","load-type":["third-party"],"unless-domain":["*allstarmediagroup.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?ad\\.aloodo\\.com","load-type":["third-party"],"unless-domain":["*ad.aloodo.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?amazon-adsystem\\.com","load-type":["third-party"],"unless-domain":["*amazon.com","*amazon-adsystem.com","*amazon.ca","*amazon.co.jp","*amazon.co.uk","*amazon.de","*amazon.es","*amazon.fr","*amazon.it","*assoc-amazon.com","*alexa.com","*alexametrics.com","*cloudfront.net","*amazonaws.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?amazon\\.ca","load-type":["third-party"],"unless-domain":["*amazon.com","*amazon-adsystem.com","*amazon.ca","*amazon.co.jp","*amazon.co.uk","*amazon.de","*amazon.es","*amazon.fr","*amazon.it","*assoc-amazon.com","*alexa.com","*alexametrics.com","*cloudfront.net","*amazonaws.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?amazon\\.co\\.jp","load-type":["third-party"],"unless-domain":["*amazon.com","*amazon-adsystem.com","*amazon.ca","*amazon.co.jp","*amazon.co.uk","*amazon.de","*amazon.es","*amazon.fr","*amazon.it","*assoc-amazon.com","*alexa.com","*alexametrics.com","*cloudfront.net","*amazonaws.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?amazon\\.co\\.uk","load-type":["third-party"],"unless-domain":["*amazon.com","*amazon-adsystem.com","*amazon.ca","*amazon.co.jp","*amazon.co.uk","*amazon.de","*amazon.es","*amazon.fr","*amazon.it","*assoc-amazon.com","*alexa.com","*alexametrics.com","*cloudfront.net","*amazonaws.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?amazon\\.de","load-type":["third-party"],"unless-domain":["*amazon.com","*amazon-adsystem.com","*amazon.ca","*amazon.co.jp","*amazon.co.uk","*amazon.de","*amazon.es","*amazon.fr","*amazon.it","*assoc-amazon.com","*alexa.com","*alexametrics.com","*cloudfront.net","*amazonaws.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?amazon\\.es","load-type":["third-party"],"unless-domain":["*amazon.com","*amazon-adsystem.com","*amazon.ca","*amazon.co.jp","*amazon.co.uk","*amazon.de","*amazon.es","*amazon.fr","*amazon.it","*assoc-amazon.com","*alexa.com","*alexametrics.com","*cloudfront.net","*amazonaws.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?amazon\\.fr","load-type":["third-party"],"unless-domain":["*amazon.com","*amazon-adsystem.com","*amazon.ca","*amazon.co.jp","*amazon.co.uk","*amazon.de","*amazon.es","*amazon.fr","*amazon.it","*assoc-amazon.com","*alexa.com","*alexametrics.com","*cloudfront.net","*amazonaws.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?amazon\\.it","load-type":["third-party"],"unless-domain":["*amazon.com","*amazon-adsystem.com","*amazon.ca","*amazon.co.jp","*amazon.co.uk","*amazon.de","*amazon.es","*amazon.fr","*amazon.it","*assoc-amazon.com","*alexa.com","*alexametrics.com","*cloudfront.net","*amazonaws.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?assoc-amazon\\.com","load-type":["third-party"],"unless-domain":["*amazon.com","*amazon-adsystem.com","*amazon.ca","*amazon.co.jp","*amazon.co.uk","*amazon.de","*amazon.es","*amazon.fr","*amazon.it","*assoc-amazon.com","*alexa.com","*alexametrics.com","*cloudfront.net","*amazonaws.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?adnetwork\\.vn","load-type":["third-party"],"unless-domain":["*ambientdigital.com.vn","*adnetwork.vn"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?ambientdigital\\.com\\.vn","load-type":["third-party"],"unless-domain":["*ambientdigital.com.vn","*adnetwork.vn"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?amobee\\.com","load-type":["third-party"],"unless-domain":["*amobee.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?appflood\\.com","load-type":["third-party"],"unless-domain":["*appflood.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?adlantic\\.nl","load-type":["third-party"],"unless-domain":["*appnexus.com","*adlantic.nl","*adnxs.com","*adrdgt.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?adnxs\\.com","load-type":["third-party"],"unless-domain":["*appnexus.com","*adlantic.nl","*adnxs.com","*adrdgt.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?adrdgt\\.com","load-type":["third-party"],"unless-domain":["*appnexus.com","*adlantic.nl","*adnxs.com","*adrdgt.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?appnexus\\.com","load-type":["third-party"],"unless-domain":["*appnexus.com","*adlantic.nl","*adnxs.com","*adrdgt.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?alenty\\.com","load-type":["third-party"],"unless-domain":["*appnexus.com","*adlantic.nl","*adnxs.com","*adrdgt.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?appenda\\.com","load-type":["third-party"],"unless-domain":["*appenda.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?appier\\.com","load-type":["third-party"],"unless-domain":["*appier.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?applifier\\.com","load-type":["third-party"],"unless-domain":["*applifier.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?applovin\\.com","load-type":["third-party"],"unless-domain":["*applovin.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?arkwrightshomebrew\\.com","load-type":["third-party"],"unless-domain":["*arkwrightshomebrew.com","*ctasnet.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?ctasnet\\.com","load-type":["third-party"],"unless-domain":["*arkwrightshomebrew.com","*ctasnet.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?atoomic\\.com","load-type":["third-party"],"unless-domain":["*atoomic.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?atrinsic\\.com","load-type":["third-party"],"unless-domain":["*atrinsic.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?audienceadnetwork\\.com","load-type":["third-party"],"unless-domain":["*audienceadnetwork.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?audience2media\\.com","load-type":["third-party"],"unless-domain":["*audience2media.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?audiencescience\\.com","load-type":["third-party"],"unless-domain":["*audiencescience.com","*revsci.net","*targetingmarketplace.com","*wunderloop.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?revsci\\.net","load-type":["third-party"],"unless-domain":["*audiencescience.com","*revsci.net","*targetingmarketplace.com","*wunderloop.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?targetingmarketplace\\.com","load-type":["third-party"],"unless-domain":["*audiencescience.com","*revsci.net","*targetingmarketplace.com","*wunderloop.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?wunderloop\\.net","load-type":["third-party"],"unless-domain":["*audiencescience.com","*revsci.net","*targetingmarketplace.com","*wunderloop.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?augme\\.com","load-type":["third-party"],"unless-domain":["*augme.com","*hipcricket.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?hipcricket\\.com","load-type":["third-party"],"unless-domain":["*augme.com","*hipcricket.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?augur\\.io","load-type":["third-party"],"unless-domain":["*augur.io"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?avalanchers\\.com","load-type":["third-party"],"unless-domain":["*avalanchers.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?avantlink\\.com","load-type":["third-party"],"unless-domain":["*avantlink.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?avsads\\.com","load-type":["third-party"],"unless-domain":["*avsads.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?adgear\\.com","load-type":["third-party"],"unless-domain":["*bloom-hq.com","*adgear.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?bloom-hq\\.com","load-type":["third-party"],"unless-domain":["*bloom-hq.com","*adgear.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?adgrx\\.com","load-type":["third-party"],"unless-domain":["*bloom-hq.com","*adgear.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?buzzcity\\.com","load-type":["third-party"],"unless-domain":["*buzzcity.com","*bvmedia.ca","*networldmedia.com","*networldmedia.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?bvmedia\\.ca","load-type":["third-party"],"unless-domain":["*buzzcity.com","*bvmedia.ca","*networldmedia.com","*networldmedia.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?networldmedia\\.com","load-type":["third-party"],"unless-domain":["*buzzcity.com","*bvmedia.ca","*networldmedia.com","*networldmedia.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?networldmedia\\.net","load-type":["third-party"],"unless-domain":["*buzzcity.com","*bvmedia.ca","*networldmedia.com","*networldmedia.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?backbeatmedia\\.com","load-type":["third-party"],"unless-domain":["*backbeatmedia.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?bannerconnect\\.net","load-type":["third-party"],"unless-domain":["*bannerconnect.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?barilliance\\.com","load-type":["third-party"],"unless-domain":["*barilliance.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?baronsoffers\\.com","load-type":["third-party"],"unless-domain":["*baronsoffers.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?batanga\\.com","load-type":["third-party"],"unless-domain":["*batanganetwork.com","*batanga.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?batanganetwork\\.com","load-type":["third-party"],"unless-domain":["*batanganetwork.com","*batanga.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?beanstockmedia\\.com","load-type":["third-party"],"unless-domain":["*beanstockmedia.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?begun\\.ru","load-type":["third-party"],"unless-domain":["*begun.ru"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?betgenius\\.com","load-type":["third-party"],"unless-domain":["*betgenius.com","*connextra.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?connextra\\.com","load-type":["third-party"],"unless-domain":["*betgenius.com","*connextra.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?bidvertiser\\.com","load-type":["third-party"],"unless-domain":["*bidvertiser.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?bidswitch\\.net","load-type":["third-party"],"unless-domain":["*bidswitch.net","*bidswitch.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?bidswitch\\.com","load-type":["third-party"],"unless-domain":["*bidswitch.net","*bidswitch.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?binlayer\\.com","load-type":["third-party"],"unless-domain":["*binlayer.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?bitcoinplus\\.com","load-type":["third-party"],"unless-domain":["*bitcoinplus.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?bittads\\.com","load-type":["third-party"],"unless-domain":["*bittads.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?bizo\\.com","load-type":["third-party"],"unless-domain":["*bizo.com","*bizographics.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?bizographics\\.com","load-type":["third-party"],"unless-domain":["*bizo.com","*bizographics.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?blacklabelads\\.com","load-type":["third-party"],"unless-domain":["*blacklabelads.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?blogcatalog\\.com","load-type":["third-party"],"unless-domain":["*blogcatalog.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?theblogfrog\\.com","load-type":["third-party"],"unless-domain":["*theblogfrog.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?blogher\\.com","load-type":["third-party"],"unless-domain":["*blogher.com","*blogherads.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?blogherads\\.com","load-type":["third-party"],"unless-domain":["*blogher.com","*blogherads.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?blogrollr\\.com","load-type":["third-party"],"unless-domain":["*blogrollr.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?bloomreach\\.com","load-type":["third-party"],"unless-domain":["*bloomreach.com","*brcdn.com","*brsrvr.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?brcdn\\.com","load-type":["third-party"],"unless-domain":["*bloomreach.com","*brcdn.com","*brsrvr.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?brsrvr\\.com","load-type":["third-party"],"unless-domain":["*bloomreach.com","*brcdn.com","*brsrvr.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?blutrumpet\\.com","load-type":["third-party"],"unless-domain":["*blutrumpet.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?bluecava\\.com","load-type":["third-party"],"unless-domain":["*bluecava.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?bkrtx\\.com","load-type":["third-party"],"unless-domain":["*bluekai.com","*bkrtx.com","*tracksimple.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?bluekai\\.com","load-type":["third-party"],"unless-domain":["*bluekai.com","*bkrtx.com","*tracksimple.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?tracksimple\\.com","load-type":["third-party"],"unless-domain":["*bluekai.com","*bkrtx.com","*tracksimple.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?brainient\\.com","load-type":["third-party"],"unless-domain":["*brainient.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?brandaffinity\\.net","load-type":["third-party"],"unless-domain":["*brandaffinity.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?brand\\.net","load-type":["third-party"],"unless-domain":["*brand.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?brandscreen\\.com","load-type":["third-party"],"unless-domain":["*brandscreen.com","*rtbidder.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?rtbidder\\.net","load-type":["third-party"],"unless-domain":["*brandscreen.com","*rtbidder.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?brightroll\\.com","load-type":["third-party"],"unless-domain":["*brightroll.com","*btrll.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?btrll\\.com","load-type":["third-party"],"unless-domain":["*brightroll.com","*btrll.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?brighttag\\.com","load-type":["third-party"],"unless-domain":["*brighttag.com","*btstatic.com","*thebrighttag.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?btstatic\\.com","load-type":["third-party"],"unless-domain":["*brighttag.com","*btstatic.com","*thebrighttag.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?thebrighttag\\.com","load-type":["third-party"],"unless-domain":["*brighttag.com","*btstatic.com","*thebrighttag.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?brilig\\.com","load-type":["third-party"],"unless-domain":["*brilig.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?burstbeacon\\.com","load-type":["third-party"],"unless-domain":["*burstmedia.com","*burstbeacon.com","*burstdirectads.com","*burstnet.com","*giantrealm.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?burstdirectads\\.com","load-type":["third-party"],"unless-domain":["*burstmedia.com","*burstbeacon.com","*burstdirectads.com","*burstnet.com","*giantrealm.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?burstmedia\\.com","load-type":["third-party"],"unless-domain":["*burstmedia.com","*burstbeacon.com","*burstdirectads.com","*burstnet.com","*giantrealm.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?burstnet\\.com","load-type":["third-party"],"unless-domain":["*burstmedia.com","*burstbeacon.com","*burstdirectads.com","*burstnet.com","*giantrealm.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?giantrealm\\.com","load-type":["third-party"],"unless-domain":["*burstmedia.com","*burstbeacon.com","*burstdirectads.com","*burstnet.com","*giantrealm.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?burstly\\.com","load-type":["third-party"],"unless-domain":["*burstly.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?businessol\\.com","load-type":["third-party"],"unless-domain":["*businessol.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?beaconads\\.com","load-type":["third-party"],"unless-domain":["*buysellads.com","*beaconads.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?buysellads\\.com","load-type":["third-party"],"unless-domain":["*buysellads.com","*beaconads.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?buysight\\.com","load-type":["third-party"],"unless-domain":["*buysight.com","*permuto.com","*pulsemgr.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?permuto\\.com","load-type":["third-party"],"unless-domain":["*buysight.com","*permuto.com","*pulsemgr.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?pulsemgr\\.com","load-type":["third-party"],"unless-domain":["*buysight.com","*permuto.com","*pulsemgr.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?buzzcity\\.com","load-type":["third-party"],"unless-domain":["*buzzcity.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?buzzparadise\\.com","load-type":["third-party"],"unless-domain":["*buzzparadise.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?capitaldata\\.fr","load-type":["third-party"],"unless-domain":["*capitaldata.fr"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?cbproads\\.com","load-type":["third-party"],"unless-domain":["*cbproads.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?contaxe\\.com","load-type":["third-party"],"unless-domain":["*contaxe.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?contextweb\\.com","load-type":["third-party"],"unless-domain":["*contextweb.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?admailtiser\\.com","load-type":["third-party"],"unless-domain":["*contextin.com","*admailtiser.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?contextin\\.com","load-type":["third-party"],"unless-domain":["*contextin.com","*admailtiser.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?cpmstar\\.com","load-type":["third-party"],"unless-domain":["*cpmstar.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?cpxadroit\\.com","load-type":["third-party"],"unless-domain":["*cpxinteractive.com","*cpxadroit.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?cpxinteractive\\.com","load-type":["third-party"],"unless-domain":["*cpxinteractive.com","*cpxadroit.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?cadreon\\.com","load-type":["third-party"],"unless-domain":["*cadreon.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?campaigngrid\\.com","load-type":["third-party"],"unless-domain":["*campaigngrid.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?caraytech\\.com\\.ar","load-type":["third-party"],"unless-domain":["*www.caraytech.com.ar","*caraytech.com.ar","*e-planning.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?e-planning\\.net","load-type":["third-party"],"unless-domain":["*www.caraytech.com.ar","*caraytech.com.ar","*e-planning.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?cart\\.ro","load-type":["third-party"],"unless-domain":["*cart.ro","*statistics.ro"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?statistics\\.ro","load-type":["third-party"],"unless-domain":["*cart.ro","*statistics.ro"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?casalemedia\\.com","load-type":["third-party"],"unless-domain":["*casalemedia.com","*medianet.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?medianet\\.com","load-type":["third-party"],"unless-domain":["*casalemedia.com","*medianet.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?chango\\.ca","load-type":["third-party"],"unless-domain":["*chango.com","*chango.ca"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?chango\\.com","load-type":["third-party"],"unless-domain":["*chango.com","*chango.ca"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?channelintelligence\\.com","load-type":["third-party"],"unless-domain":["*channelintelligence.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?channeladvisor\\.com","load-type":["third-party"],"unless-domain":["*channeladvisor.com","*searchmarketing.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?searchmarketing\\.com","load-type":["third-party"],"unless-domain":["*channeladvisor.com","*searchmarketing.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?chartboost\\.com","load-type":["third-party"],"unless-domain":["*chartboost.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?checkm8\\.com","load-type":["third-party"],"unless-domain":["*checkm8.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?chitika\\.com","load-type":["third-party"],"unless-domain":["*chitika.com","*chitika.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?chitika\\.net","load-type":["third-party"],"unless-domain":["*chitika.com","*chitika.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?choicestream\\.com","load-type":["third-party"],"unless-domain":["*choicestream.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?clearsaleing\\.com","load-type":["third-party"],"unless-domain":["*clearsaleing.com","*csdata1.com","*csdata2.com","*csdata3.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?csdata1\\.com","load-type":["third-party"],"unless-domain":["*clearsaleing.com","*csdata1.com","*csdata2.com","*csdata3.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?csdata2\\.com","load-type":["third-party"],"unless-domain":["*clearsaleing.com","*csdata1.com","*csdata2.com","*csdata3.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?csdata3\\.com","load-type":["third-party"],"unless-domain":["*clearsaleing.com","*csdata1.com","*csdata2.com","*csdata3.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?clearsightinteractive\\.com","load-type":["third-party"],"unless-domain":["*clearsightinteractive.com","*csi-tracking.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?csi-tracking\\.com","load-type":["third-party"],"unless-domain":["*clearsightinteractive.com","*csi-tracking.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?clearsearchmedia\\.com","load-type":["third-party"],"unless-domain":["*clearsearchmedia.com","*csm-secure.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?csm-secure\\.com","load-type":["third-party"],"unless-domain":["*clearsearchmedia.com","*csm-secure.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?clicmanager\\.fr","load-type":["third-party"],"unless-domain":["*clicmanager.fr"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?clickaider\\.com","load-type":["third-party"],"unless-domain":["*clickaider.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?clickdimensions\\.com","load-type":["third-party"],"unless-domain":["*clickdimensions.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?clickdistrict\\.com","load-type":["third-party"],"unless-domain":["*clickdistrict.com","*creative-serving.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?creative-serving\\.com","load-type":["third-party"],"unless-domain":["*clickdistrict.com","*creative-serving.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?conversiondashboard\\.com","load-type":["third-party"],"unless-domain":["*clickfuel.com","*conversiondashboard.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?clickinc\\.com","load-type":["third-party"],"unless-domain":["*clickinc.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?clickbooth\\.com","load-type":["third-party"],"unless-domain":["*clickbooth.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?adtoll\\.com","load-type":["third-party"],"unless-domain":["*clickbooth.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?clicksor\\.com","load-type":["third-party"],"unless-domain":["*clicksor.com","*clicksor.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?clicksor\\.net","load-type":["third-party"],"unless-domain":["*clicksor.com","*clicksor.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?clickwinks\\.com","load-type":["third-party"],"unless-domain":["*clickwinks.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?clovenetwork\\.com","load-type":["third-party"],"unless-domain":["*clovenetwork.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?cmads\\.com\\.tw","load-type":["third-party"],"unless-domain":["*cognitivematch.com","*cmads.com.tw","*cmadsasia.com","*cmadseu.com","*cmmeglobal.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?cmadsasia\\.com","load-type":["third-party"],"unless-domain":["*cognitivematch.com","*cmads.com.tw","*cmadsasia.com","*cmadseu.com","*cmmeglobal.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?cmadseu\\.com","load-type":["third-party"],"unless-domain":["*cognitivematch.com","*cmads.com.tw","*cmadsasia.com","*cmadseu.com","*cmmeglobal.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?cmmeglobal\\.com","load-type":["third-party"],"unless-domain":["*cognitivematch.com","*cmads.com.tw","*cmadsasia.com","*cmadseu.com","*cmmeglobal.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?cognitivematch\\.com","load-type":["third-party"],"unless-domain":["*cognitivematch.com","*cmads.com.tw","*cmadsasia.com","*cmadseu.com","*cmmeglobal.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?collective-media\\.net","load-type":["third-party"],"unless-domain":["*collective.com","*collective-media.net","*oggifinogi.com","*tumri.com","*tumri.net","*yt1187.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?collective\\.com","load-type":["third-party"],"unless-domain":["*collective.com","*collective-media.net","*oggifinogi.com","*tumri.com","*tumri.net","*yt1187.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?oggifinogi\\.com","load-type":["third-party"],"unless-domain":["*collective.com","*collective-media.net","*oggifinogi.com","*tumri.com","*tumri.net","*yt1187.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?tumri\\.com","load-type":["third-party"],"unless-domain":["*collective.com","*collective-media.net","*oggifinogi.com","*tumri.com","*tumri.net","*yt1187.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?tumri\\.net","load-type":["third-party"],"unless-domain":["*collective.com","*collective-media.net","*oggifinogi.com","*tumri.com","*tumri.net","*yt1187.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?yt1187\\.net","load-type":["third-party"],"unless-domain":["*collective.com","*collective-media.net","*oggifinogi.com","*tumri.com","*tumri.net","*yt1187.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?apmebf\\.com","load-type":["third-party"],"unless-domain":["*cj.com","*apmebf.com","*awltovhc.com","*ftjcfx.com","*kcdwa.com","*qksz.com","*qksz.net","*tqlkg.com","*yceml.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?awltovhc\\.com","load-type":["third-party"],"unless-domain":["*cj.com","*apmebf.com","*awltovhc.com","*ftjcfx.com","*kcdwa.com","*qksz.com","*qksz.net","*tqlkg.com","*yceml.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?cj\\.com","load-type":["third-party"],"unless-domain":["*cj.com","*apmebf.com","*awltovhc.com","*ftjcfx.com","*kcdwa.com","*qksz.com","*qksz.net","*tqlkg.com","*yceml.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?ftjcfx\\.com","load-type":["third-party"],"unless-domain":["*cj.com","*apmebf.com","*awltovhc.com","*ftjcfx.com","*kcdwa.com","*qksz.com","*qksz.net","*tqlkg.com","*yceml.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?kcdwa\\.com","load-type":["third-party"],"unless-domain":["*cj.com","*apmebf.com","*awltovhc.com","*ftjcfx.com","*kcdwa.com","*qksz.com","*qksz.net","*tqlkg.com","*yceml.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?qksz\\.com","load-type":["third-party"],"unless-domain":["*cj.com","*apmebf.com","*awltovhc.com","*ftjcfx.com","*kcdwa.com","*qksz.com","*qksz.net","*tqlkg.com","*yceml.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?qksz\\.net","load-type":["third-party"],"unless-domain":["*cj.com","*apmebf.com","*awltovhc.com","*ftjcfx.com","*kcdwa.com","*qksz.com","*qksz.net","*tqlkg.com","*yceml.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?tqlkg\\.com","load-type":["third-party"],"unless-domain":["*cj.com","*apmebf.com","*awltovhc.com","*ftjcfx.com","*kcdwa.com","*qksz.com","*qksz.net","*tqlkg.com","*yceml.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?yceml\\.net","load-type":["third-party"],"unless-domain":["*cj.com","*apmebf.com","*awltovhc.com","*ftjcfx.com","*kcdwa.com","*qksz.com","*qksz.net","*tqlkg.com","*yceml.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?communicatorcorp\\.com","load-type":["third-party"],"unless-domain":["*communicatorcorp.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?compasslabs\\.com","load-type":["third-party"],"unless-domain":["*compasslabs.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?complex\\.com","load-type":["third-party"],"unless-domain":["*complexmedianetwork.com","*complex.com","*collider.com","*solecollector.com","*pigeonsandplanes.com","*theridechannel.com","*firstwefeast.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?complexmedianetwork\\.com","load-type":["third-party"],"unless-domain":["*complexmedianetwork.com","*complex.com","*collider.com","*solecollector.com","*pigeonsandplanes.com","*theridechannel.com","*firstwefeast.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?consiliummedia\\.com","load-type":["third-party"],"unless-domain":["*consiliummedia.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?agencytradingdesk\\.net","load-type":["third-party"],"unless-domain":["*contextuads.com","*agencytradingdesk.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?contextuads\\.com","load-type":["third-party"],"unless-domain":["*contextuads.com","*agencytradingdesk.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?convergedirect\\.com","load-type":["third-party"],"unless-domain":["*convergedirect.com","*convergetrack.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?convergetrack\\.com","load-type":["third-party"],"unless-domain":["*convergedirect.com","*convergetrack.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?conversionruler\\.com","load-type":["third-party"],"unless-domain":["*conversionruler.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?conversive\\.nl","load-type":["third-party"],"unless-domain":["*conversive.nl"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?coremotives\\.com","load-type":["third-party"],"unless-domain":["*coremotives.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?adify\\.com","load-type":["third-party"],"unless-domain":["*coxdigitalsolutions.com","*adify.com","*afy11.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?afy11\\.net","load-type":["third-party"],"unless-domain":["*coxdigitalsolutions.com","*adify.com","*afy11.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?coxdigitalsolutions\\.com","load-type":["third-party"],"unless-domain":["*coxdigitalsolutions.com","*adify.com","*afy11.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?creafi\\.com","load-type":["third-party"],"unless-domain":["*creafi.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?crimtan\\.com","load-type":["third-party"],"unless-domain":["*crimtan.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?crispmedia\\.com","load-type":["third-party"],"unless-domain":["*crispmedia.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?criteo\\.com","load-type":["third-party"],"unless-domain":["*criteo.com","*criteo.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?criteo\\.net","load-type":["third-party"],"unless-domain":["*criteo.com","*criteo.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?crosspixel\\.net","load-type":["third-party"],"unless-domain":["*crosspixel.net","*crosspixelmedia.com","*crsspxl.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?crosspixelmedia\\.com","load-type":["third-party"],"unless-domain":["*crosspixel.net","*crosspixelmedia.com","*crsspxl.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?crsspxl\\.com","load-type":["third-party"],"unless-domain":["*crosspixel.net","*crosspixelmedia.com","*crsspxl.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?cyberplex\\.com","load-type":["third-party"],"unless-domain":["*cyberplex.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?dc-storm\\.com","load-type":["third-party"],"unless-domain":["*dc-storm.com","*stormiq.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?stormiq\\.com","load-type":["third-party"],"unless-domain":["*dc-storm.com","*stormiq.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?dgit\\.com","load-type":["third-party"],"unless-domain":["*dgit.com","*eyeblaster.com","*eyewonder.com","*mdadx.com","*serving-sys.com","*unicast.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?eyeblaster\\.com","load-type":["third-party"],"unless-domain":["*dgit.com","*eyeblaster.com","*eyewonder.com","*mdadx.com","*serving-sys.com","*unicast.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?eyewonder\\.com","load-type":["third-party"],"unless-domain":["*dgit.com","*eyeblaster.com","*eyewonder.com","*mdadx.com","*serving-sys.com","*unicast.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?mdadx\\.com","load-type":["third-party"],"unless-domain":["*dgit.com","*eyeblaster.com","*eyewonder.com","*mdadx.com","*serving-sys.com","*unicast.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?serving-sys\\.com","load-type":["third-party"],"unless-domain":["*dgit.com","*eyeblaster.com","*eyewonder.com","*mdadx.com","*serving-sys.com","*unicast.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?unicast\\.com","load-type":["third-party"],"unless-domain":["*dgit.com","*eyeblaster.com","*eyewonder.com","*mdadx.com","*serving-sys.com","*unicast.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?ds-iq\\.com","load-type":["third-party"],"unless-domain":["*ds-iq.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?dsnrgroup\\.com","load-type":["third-party"],"unless-domain":["*dsnrmg.com","*dsnrgroup.com","*traffiliate.com","*z5x.com","*z5x.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?dsnrmg\\.com","load-type":["third-party"],"unless-domain":["*dsnrmg.com","*dsnrgroup.com","*traffiliate.com","*z5x.com","*z5x.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?traffiliate\\.com","load-type":["third-party"],"unless-domain":["*dsnrmg.com","*dsnrgroup.com","*traffiliate.com","*z5x.com","*z5x.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?z5x\\.com","load-type":["third-party"],"unless-domain":["*dsnrmg.com","*dsnrgroup.com","*traffiliate.com","*z5x.com","*z5x.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?z5x\\.net","load-type":["third-party"],"unless-domain":["*dsnrmg.com","*dsnrgroup.com","*traffiliate.com","*z5x.com","*z5x.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?dada\\.pro","load-type":["third-party"],"unless-domain":["*dada.pro","*simply.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?simply\\.com","load-type":["third-party"],"unless-domain":["*dada.pro","*simply.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?dataxu\\.com","load-type":["third-party"],"unless-domain":["*dataxu.com","*dataxu.net","*mexad.com","*w55c.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?dataxu\\.net","load-type":["third-party"],"unless-domain":["*dataxu.com","*dataxu.net","*mexad.com","*w55c.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?mexad\\.com","load-type":["third-party"],"unless-domain":["*dataxu.com","*dataxu.net","*mexad.com","*w55c.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?w55c\\.net","load-type":["third-party"],"unless-domain":["*dataxu.com","*dataxu.net","*mexad.com","*w55c.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?nexac\\.com","load-type":["third-party"],"unless-domain":["*datalogix.com","*nexac.com","*nextaction.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?nextaction\\.net","load-type":["third-party"],"unless-domain":["*datalogix.com","*nexac.com","*nextaction.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?datonics\\.com","load-type":["third-party"],"unless-domain":["*datonics.com","*pro-market.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?pro-market\\.net","load-type":["third-party"],"unless-domain":["*datonics.com","*pro-market.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?datranmedia\\.com","load-type":["third-party"],"unless-domain":["*datranmedia.com","*displaymarketplace.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?displaymarketplace\\.com","load-type":["third-party"],"unless-domain":["*datranmedia.com","*displaymarketplace.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?datvantage\\.com","load-type":["third-party"],"unless-domain":["*datvantage.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?dedicatedmedia\\.com","load-type":["third-party"],"unless-domain":["*dedicatedmedia.com","*dedicatednetworks.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?dedicatednetworks\\.com","load-type":["third-party"],"unless-domain":["*dedicatedmedia.com","*dedicatednetworks.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?delivr\\.com","load-type":["third-party"],"unless-domain":["*delivr.com","*percentmobile.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?percentmobile\\.com","load-type":["third-party"],"unless-domain":["*delivr.com","*percentmobile.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?adaction\\.se","load-type":["third-party"],"unless-domain":["*deltaprojects.se","*adaction.se","*de17a.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?de17a\\.com","load-type":["third-party"],"unless-domain":["*deltaprojects.se","*adaction.se","*de17a.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?deltaprojects\\.se","load-type":["third-party"],"unless-domain":["*deltaprojects.se","*adaction.se","*de17a.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?demandmedia\\.com","load-type":["third-party"],"unless-domain":["*demandmedia.com","*indieclick.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?indieclick\\.com","load-type":["third-party"],"unless-domain":["*demandmedia.com","*indieclick.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?adcloud\\.com","load-type":["third-party"],"unless-domain":["*dp-dhl.com","*adcloud.com","*adcloud.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?adcloud\\.net","load-type":["third-party"],"unless-domain":["*dp-dhl.com","*adcloud.com","*adcloud.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?dp-dhl\\.com","load-type":["third-party"],"unless-domain":["*dp-dhl.com","*adcloud.com","*adcloud.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?developermedia\\.com","load-type":["third-party"],"unless-domain":["*developermedia.com","*lqcdn.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?lqcdn\\.com","load-type":["third-party"],"unless-domain":["*developermedia.com","*lqcdn.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?did-it\\.com","load-type":["third-party"],"unless-domain":["*didit.com","*did-it.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?didit\\.com","load-type":["third-party"],"unless-domain":["*didit.com","*did-it.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?digitalriver\\.com","load-type":["third-party"],"unless-domain":["*digitalriver.com","*keywordmax.com","*netflame.cc"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?keywordmax\\.com","load-type":["third-party"],"unless-domain":["*digitalriver.com","*keywordmax.com","*netflame.cc"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?netflame\\.cc","load-type":["third-party"],"unless-domain":["*digitalriver.com","*keywordmax.com","*netflame.cc"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?digitalwindow\\.com","load-type":["third-party"],"unless-domain":["*digitalwindow.com","*perfiliate.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?perfiliate\\.com","load-type":["third-party"],"unless-domain":["*digitalwindow.com","*perfiliate.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?digitize\\.ie","load-type":["third-party"],"unless-domain":["*digitize.ie"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?directresponsegroup\\.com","load-type":["third-party"],"unless-domain":["*directresponsegroup.com","*ppctracking.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?ppctracking\\.net","load-type":["third-party"],"unless-domain":["*directresponsegroup.com","*ppctracking.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?directtrack\\.com","load-type":["third-party"],"unless-domain":["*directtrack.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?doublepimp\\.com","load-type":["third-party"],"unless-domain":["*doublepimp.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?bid-tag\\.com","load-type":["third-party"],"unless-domain":["*doublepositive.com","*bid-tag.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?doublepositive\\.com","load-type":["third-party"],"unless-domain":["*doublepositive.com","*bid-tag.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?doubleverify\\.com","load-type":["third-party"],"unless-domain":["*doubleverify.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?adsymptotic\\.com","load-type":["third-party"],"unless-domain":["*drawbrid.ge","*adsymptotic.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?drawbrid\\.ge","load-type":["third-party"],"unless-domain":["*drawbrid.ge","*adsymptotic.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?dynamicoxygen\\.com","load-type":["third-party"],"unless-domain":["*dynamicoxygen.com","*exitjunction.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?exitjunction\\.com","load-type":["third-party"],"unless-domain":["*dynamicoxygen.com","*exitjunction.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?eqads\\.com","load-type":["third-party"],"unless-domain":["*eqads.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?extensions\\.ru","load-type":["third-party"],"unless-domain":["*extensions.ru"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?earnify\\.com","load-type":["third-party"],"unless-domain":["*earnify.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?effectivemeasure\\.com","load-type":["third-party"],"unless-domain":["*effectivemeasure.com","*effectivemeasure.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?effectivemeasure\\.net","load-type":["third-party"],"unless-domain":["*effectivemeasure.com","*effectivemeasure.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?eleavers\\.com","load-type":["third-party"],"unless-domain":["*eleavers.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?emediate\\.biz","load-type":["third-party"],"unless-domain":["*emediate.com","*emediate.biz","*emediate.dk","*emediate.eu"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?emediate\\.com","load-type":["third-party"],"unless-domain":["*emediate.com","*emediate.biz","*emediate.dk","*emediate.eu"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?emediate\\.dk","load-type":["third-party"],"unless-domain":["*emediate.com","*emediate.biz","*emediate.dk","*emediate.eu"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?emediate\\.eu","load-type":["third-party"],"unless-domain":["*emediate.com","*emediate.biz","*emediate.dk","*emediate.eu"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?usemax\\.de","load-type":["third-party"],"unless-domain":["*usemax.de"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?enecto\\.com","load-type":["third-party"],"unless-domain":["*enecto.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?appmetrx\\.com","load-type":["third-party"],"unless-domain":["*engago.com","*appmetrx.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?engago\\.com","load-type":["third-party"],"unless-domain":["*engago.com","*appmetrx.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?enginenetwork\\.com","load-type":["third-party"],"unless-domain":["*enginenetwork.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?ensighten\\.com","load-type":["third-party"],"unless-domain":["*ensighten.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?entireweb\\.com","load-type":["third-party"],"unless-domain":["*entireweb.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?epicadvertising\\.com","load-type":["third-party"],"unless-domain":["*theepicmediagroup.com","*epicadvertising.com","*epicmarketplace.com","*epicmobileads.com","*trafficmp.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?epicmarketplace\\.com","load-type":["third-party"],"unless-domain":["*theepicmediagroup.com","*epicadvertising.com","*epicmarketplace.com","*epicmobileads.com","*trafficmp.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?epicmobileads\\.com","load-type":["third-party"],"unless-domain":["*theepicmediagroup.com","*epicadvertising.com","*epicmarketplace.com","*epicmobileads.com","*trafficmp.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?theepicmediagroup\\.com","load-type":["third-party"],"unless-domain":["*theepicmediagroup.com","*epicadvertising.com","*epicmarketplace.com","*epicmobileads.com","*trafficmp.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?trafficmp\\.com","load-type":["third-party"],"unless-domain":["*theepicmediagroup.com","*epicadvertising.com","*epicmarketplace.com","*epicmobileads.com","*trafficmp.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?epsilon\\.com","load-type":["third-party"],"unless-domain":["*epsilon.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?ero-advertising\\.com","load-type":["third-party"],"unless-domain":["*ero-advertising.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?etargetnet\\.com","load-type":["third-party"],"unless-domain":["*etargetnet.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?etarget\\.eu","load-type":["third-party"],"unless-domain":["*etargetnet.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?adwitserver\\.com","load-type":["third-party"],"unless-domain":["*etineria.com","*adwitserver.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?etineria\\.com","load-type":["third-party"],"unless-domain":["*etineria.com","*adwitserver.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?everydayhealth\\.com","load-type":["third-party"],"unless-domain":["*everydayhealth.com","*waterfrontmedia.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?waterfrontmedia\\.com","load-type":["third-party"],"unless-domain":["*everydayhealth.com","*waterfrontmedia.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?betrad\\.com","load-type":["third-party"],"unless-domain":["*evidon.com","*betrad.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?evidon\\.com","load-type":["third-party"],"unless-domain":["*evidon.com","*betrad.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?engineseeker\\.com","load-type":["third-party"],"unless-domain":["*evisionsmarketing.com","*engineseeker.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?evisionsmarketing\\.com","load-type":["third-party"],"unless-domain":["*evisionsmarketing.com","*engineseeker.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?evolvemediacorp\\.com","load-type":["third-party"],"unless-domain":["*evolvemediacorp.com","*evolvemediametrics.com","*gorillanation.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?evolvemediametrics\\.com","load-type":["third-party"],"unless-domain":["*evolvemediacorp.com","*evolvemediametrics.com","*gorillanation.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?gorillanation\\.com","load-type":["third-party"],"unless-domain":["*evolvemediacorp.com","*evolvemediametrics.com","*gorillanation.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?exoclick\\.com","load-type":["third-party"],"unless-domain":["*exoclick.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?audienceiq\\.com","load-type":["third-party"],"unless-domain":["*experian.com","*audienceiq.com","*pricegrabber.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?experian\\.com","load-type":["third-party"],"unless-domain":["*experian.com","*audienceiq.com","*pricegrabber.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?pricegrabber\\.com","load-type":["third-party"],"unless-domain":["*experian.com","*audienceiq.com","*pricegrabber.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?adotube\\.com","load-type":["third-party"],"unless-domain":["*exponential.com","*adotube.com","*fulltango.com","*tribalfusion.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?exponential\\.com","load-type":["third-party"],"unless-domain":["*exponential.com","*adotube.com","*fulltango.com","*tribalfusion.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?fulltango\\.com","load-type":["third-party"],"unless-domain":["*exponential.com","*adotube.com","*fulltango.com","*tribalfusion.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?tribalfusion\\.com","load-type":["third-party"],"unless-domain":["*exponential.com","*adotube.com","*fulltango.com","*tribalfusion.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?extensionfactory\\.com","load-type":["third-party"],"unless-domain":["*extensionfactory.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?eyeconomy\\.co\\.uk","load-type":["third-party"],"unless-domain":["*www.eyeconomy.co.uk","*eyeconomy.co.uk","*eyeconomy.com","*sublimemedia.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?eyeconomy\\.com","load-type":["third-party"],"unless-domain":["*www.eyeconomy.co.uk","*eyeconomy.co.uk","*eyeconomy.com","*sublimemedia.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?sublimemedia\\.net","load-type":["third-party"],"unless-domain":["*www.eyeconomy.co.uk","*eyeconomy.co.uk","*eyeconomy.com","*sublimemedia.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?eyeviewdigital\\.com","load-type":["third-party"],"unless-domain":["*eyeviewdigital.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?eyeviewads\\.com","load-type":["third-party"],"unless-domain":["*eyeviewdigital.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?adsfac\\.eu","load-type":["third-party"],"unless-domain":["*facilitatedigital.com","*adsfac.eu","*adsfac.info","*adsfac.net","*adsfac.sg","*adsfac.us"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?adsfac\\.info","load-type":["third-party"],"unless-domain":["*facilitatedigital.com","*adsfac.eu","*adsfac.info","*adsfac.net","*adsfac.sg","*adsfac.us"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?adsfac\\.net","load-type":["third-party"],"unless-domain":["*facilitatedigital.com","*adsfac.eu","*adsfac.info","*adsfac.net","*adsfac.sg","*adsfac.us"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?adsfac\\.sg","load-type":["third-party"],"unless-domain":["*facilitatedigital.com","*adsfac.eu","*adsfac.info","*adsfac.net","*adsfac.sg","*adsfac.us"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?adsfac\\.us","load-type":["third-party"],"unless-domain":["*facilitatedigital.com","*adsfac.eu","*adsfac.info","*adsfac.net","*adsfac.sg","*adsfac.us"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?facilitatedigital\\.com","load-type":["third-party"],"unless-domain":["*facilitatedigital.com","*adsfac.eu","*adsfac.info","*adsfac.net","*adsfac.sg","*adsfac.us"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?fairfax\\.com\\.au","load-type":["third-party"],"unless-domain":["*www.fxj.com.au","*fairfax.com.au","*fxj.com.au"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?fxj\\.com\\.au","load-type":["third-party"],"unless-domain":["*www.fxj.com.au","*fairfax.com.au","*fxj.com.au"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?fathomdelivers\\.com","load-type":["third-party"],"unless-domain":["*fathomdelivers.com","*fathomseo.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?fathomseo\\.com","load-type":["third-party"],"unless-domain":["*fathomdelivers.com","*fathomseo.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?federatedmedia\\.net","load-type":["third-party"],"unless-domain":["*federatedmedia.net","*fmpub.net","*lijit.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?fmpub\\.net","load-type":["third-party"],"unless-domain":["*federatedmedia.net","*fmpub.net","*lijit.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?lijit\\.com","load-type":["third-party"],"unless-domain":["*federatedmedia.net","*fmpub.net","*lijit.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?fetchback\\.com","load-type":["third-party"],"unless-domain":["*fetchback.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?fiksu\\.com","load-type":["third-party"],"unless-domain":["*fiksu.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?financialcontent\\.com","load-type":["third-party"],"unless-domain":["*financialcontent.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?fizzbuzzmedia\\.com","load-type":["third-party"],"unless-domain":["*fizzbuzzmedia.com","*fizzbuzzmedia.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?fizzbuzzmedia\\.net","load-type":["third-party"],"unless-domain":["*fizzbuzzmedia.com","*fizzbuzzmedia.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?flashtalking\\.com","load-type":["third-party"],"unless-domain":["*flashtalking.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?flite\\.com","load-type":["third-party"],"unless-domain":["*flite.com","*widgetserver.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?widgetserver\\.com","load-type":["third-party"],"unless-domain":["*flite.com","*widgetserver.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?flurry\\.com","load-type":["third-party"],"unless-domain":["*flurry.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?flytxt\\.com","load-type":["third-party"],"unless-domain":["*flytxt.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?brandsideplatform\\.com","load-type":["third-party"],"unless-domain":["*forbes.com","*brandsideplatform.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?forbes\\.com","load-type":["third-party"],"unless-domain":["*forbes.com","*brandsideplatform.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?fimserve\\.com","load-type":["third-party"],"unless-domain":["*foxonestop.com","*fimserve.com","*foxnetworks.com","*mobsmith.com","*myads.com","*othersonline.com","*rubiconproject.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?foxnetworks\\.com","load-type":["third-party"],"unless-domain":["*foxonestop.com","*fimserve.com","*foxnetworks.com","*mobsmith.com","*myads.com","*othersonline.com","*rubiconproject.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?foxonestop\\.com","load-type":["third-party"],"unless-domain":["*foxonestop.com","*fimserve.com","*foxnetworks.com","*mobsmith.com","*myads.com","*othersonline.com","*rubiconproject.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?mobsmith\\.com","load-type":["third-party"],"unless-domain":["*foxonestop.com","*fimserve.com","*foxnetworks.com","*mobsmith.com","*myads.com","*othersonline.com","*rubiconproject.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?myads\\.com","load-type":["third-party"],"unless-domain":["*foxonestop.com","*fimserve.com","*foxnetworks.com","*mobsmith.com","*myads.com","*othersonline.com","*rubiconproject.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?othersonline\\.com","load-type":["third-party"],"unless-domain":["*foxonestop.com","*fimserve.com","*foxnetworks.com","*mobsmith.com","*myads.com","*othersonline.com","*rubiconproject.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?rubiconproject\\.com","load-type":["third-party"],"unless-domain":["*foxonestop.com","*fimserve.com","*foxnetworks.com","*mobsmith.com","*myads.com","*othersonline.com","*rubiconproject.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?fout\\.jp","load-type":["third-party"],"unless-domain":["*fout.jp"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?freedom\\.com","load-type":["third-party"],"unless-domain":["*freedom.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?adultfriendfinder\\.com","load-type":["third-party"],"unless-domain":["*ffn.com","*adultfriendfinder.com","*pop6.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?ffn\\.com","load-type":["third-party"],"unless-domain":["*ffn.com","*adultfriendfinder.com","*pop6.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?pop6\\.com","load-type":["third-party"],"unless-domain":["*ffn.com","*adultfriendfinder.com","*pop6.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?double-check\\.com","load-type":["third-party"],"unless-domain":["*frogsex.com","*double-check.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?frogsex\\.com","load-type":["third-party"],"unless-domain":["*frogsex.com","*double-check.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?futureads\\.com","load-type":["third-party"],"unless-domain":["*futureads.com","*resultlinks.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?resultlinks\\.com","load-type":["third-party"],"unless-domain":["*futureads.com","*resultlinks.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?gb-world\\.net","load-type":["third-party"],"unless-domain":["*gb-world.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?geniegroupltd\\.co\\.uk","load-type":["third-party"],"unless-domain":["*www.geniegroupltd.co.uk","*geniegroupltd.co.uk"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?gismads\\.jp","load-type":["third-party"],"unless-domain":["*www.gismads.jp","*gismads.jp"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?gsicommerce\\.com","load-type":["third-party"],"unless-domain":["*gsicommerce.com","*gsimedia.net","*pepperjam.com","*pjatr.com","*pjtra.com","*pntra.com","*pntrac.com","*pntrs.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?gsimedia\\.net","load-type":["third-party"],"unless-domain":["*gsicommerce.com","*gsimedia.net","*pepperjam.com","*pjatr.com","*pjtra.com","*pntra.com","*pntrac.com","*pntrs.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?pepperjam\\.com","load-type":["third-party"],"unless-domain":["*gsicommerce.com","*gsimedia.net","*pepperjam.com","*pjatr.com","*pjtra.com","*pntra.com","*pntrac.com","*pntrs.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?pjatr\\.com","load-type":["third-party"],"unless-domain":["*gsicommerce.com","*gsimedia.net","*pepperjam.com","*pjatr.com","*pjtra.com","*pntra.com","*pntrac.com","*pntrs.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?pjtra\\.com","load-type":["third-party"],"unless-domain":["*gsicommerce.com","*gsimedia.net","*pepperjam.com","*pjatr.com","*pjtra.com","*pntra.com","*pntrac.com","*pntrs.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?pntra\\.com","load-type":["third-party"],"unless-domain":["*gsicommerce.com","*gsimedia.net","*pepperjam.com","*pjatr.com","*pjtra.com","*pntra.com","*pntrac.com","*pntrs.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?pntrac\\.com","load-type":["third-party"],"unless-domain":["*gsicommerce.com","*gsimedia.net","*pepperjam.com","*pjatr.com","*pjtra.com","*pntra.com","*pntrac.com","*pntrs.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?pntrs\\.com","load-type":["third-party"],"unless-domain":["*gsicommerce.com","*gsimedia.net","*pepperjam.com","*pjatr.com","*pjtra.com","*pntra.com","*pntrac.com","*pntrs.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?game-advertising-online\\.com","load-type":["third-party"],"unless-domain":["*game-advertising-online.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?games2win\\.com","load-type":["third-party"],"unless-domain":["*games2win.com","*inviziads.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?inviziads\\.com","load-type":["third-party"],"unless-domain":["*games2win.com","*inviziads.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?gamned\\.com","load-type":["third-party"],"unless-domain":["*gamned.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?gannett\\.com","load-type":["third-party"],"unless-domain":["*gannett.com","*pointroll.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?pointroll\\.com","load-type":["third-party"],"unless-domain":["*gannett.com","*pointroll.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?gemius\\.com","load-type":["third-party"],"unless-domain":["*gemius.com","*gemius.pl"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?gemius\\.pl","load-type":["third-party"],"unless-domain":["*gemius.com","*gemius.pl"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?geoads\\.com","load-type":["third-party"],"unless-domain":["*geoads.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?getglue\\.com","load-type":["third-party"],"unless-domain":["*getglue.com","*smrtlnks.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?smrtlnks\\.com","load-type":["third-party"],"unless-domain":["*getglue.com","*smrtlnks.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?glam\\.com","load-type":["third-party"],"unless-domain":["*glammedia.com","*glam.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?glammedia\\.com","load-type":["third-party"],"unless-domain":["*glammedia.com","*glam.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?globe7\\.com","load-type":["third-party"],"unless-domain":["*globe7.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?godatafeed\\.com","load-type":["third-party"],"unless-domain":["*godatafeed.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?goldspotmedia\\.com","load-type":["third-party"],"unless-domain":["*goldspotmedia.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?2mdn\\.net","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?admeld\\.com","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?admob\\.com","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?cc-dt\\.com","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?destinationurl\\.com","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?doubleclick\\.net","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?adwords\\.google\\.com","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?googleadservices\\.com","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?googlesyndication\\.com","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?googletagservices\\.com","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?invitemedia\\.com","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?smtad\\.net","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?teracent\\.com","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?teracent\\.net","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?ytsa\\.net","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?grapeshot\\.co\\.uk","load-type":["third-party"],"unless-domain":["*www.grapeshot.co.uk","*grapeshot.co.uk"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?groceryshopping\\.net","load-type":["third-party"],"unless-domain":["*groceryshopping.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?groovinads\\.com","load-type":["third-party"],"unless-domain":["*groovinads.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?guj\\.de","load-type":["third-party"],"unless-domain":["*guj.de","*ligatus.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?ligatus\\.com","load-type":["third-party"],"unless-domain":["*guj.de","*ligatus.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?gumgum\\.com","load-type":["third-party"],"unless-domain":["*gumgum.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?gunggo\\.com","load-type":["third-party"],"unless-domain":["*gunggo.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?hotwords\\.com","load-type":["third-party"],"unless-domain":["*hotwords.com","*hotwords.es"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?hotwords\\.es","load-type":["third-party"],"unless-domain":["*hotwords.com","*hotwords.es"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?hp\\.com","load-type":["third-party"],"unless-domain":["*hp.com","*optimost.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?optimost\\.com","load-type":["third-party"],"unless-domain":["*hp.com","*optimost.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?huntmads\\.com","load-type":["third-party"],"unless-domain":["*huntmads.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?hands\\.com\\.br","load-type":["third-party"],"unless-domain":["*www.hands.com.br","*hands.com.br"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?harrenmedia\\.com","load-type":["third-party"],"unless-domain":["*harrenmedia.com","*harrenmedianetwork.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?harrenmedianetwork\\.com","load-type":["third-party"],"unless-domain":["*harrenmedia.com","*harrenmedianetwork.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?adacado\\.com","load-type":["third-party"],"unless-domain":["*healthpricer.com","*adacado.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?healthpricer\\.com","load-type":["third-party"],"unless-domain":["*healthpricer.com","*adacado.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?hearst\\.com","load-type":["third-party"],"unless-domain":["*hearst.com","*ic-live.com","*iclive.com","*icrossing.com","*sptag.com","*sptag1.com","*sptag2.com","*sptag3.com","*raasnet.com","*redaril.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?ic-live\\.com","load-type":["third-party"],"unless-domain":["*hearst.com","*ic-live.com","*iclive.com","*icrossing.com","*sptag.com","*sptag1.com","*sptag2.com","*sptag3.com","*raasnet.com","*redaril.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?iclive\\.com","load-type":["third-party"],"unless-domain":["*hearst.com","*ic-live.com","*iclive.com","*icrossing.com","*sptag.com","*sptag1.com","*sptag2.com","*sptag3.com","*raasnet.com","*redaril.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?icrossing\\.com","load-type":["third-party"],"unless-domain":["*hearst.com","*ic-live.com","*iclive.com","*icrossing.com","*sptag.com","*sptag1.com","*sptag2.com","*sptag3.com","*raasnet.com","*redaril.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?sptag\\.com","load-type":["third-party"],"unless-domain":["*hearst.com","*ic-live.com","*iclive.com","*icrossing.com","*sptag.com","*sptag1.com","*sptag2.com","*sptag3.com","*raasnet.com","*redaril.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?sptag1\\.com","load-type":["third-party"],"unless-domain":["*hearst.com","*ic-live.com","*iclive.com","*icrossing.com","*sptag.com","*sptag1.com","*sptag2.com","*sptag3.com","*raasnet.com","*redaril.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?sptag2\\.com","load-type":["third-party"],"unless-domain":["*hearst.com","*ic-live.com","*iclive.com","*icrossing.com","*sptag.com","*sptag1.com","*sptag2.com","*sptag3.com","*raasnet.com","*redaril.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?sptag3\\.com","load-type":["third-party"],"unless-domain":["*hearst.com","*ic-live.com","*iclive.com","*icrossing.com","*sptag.com","*sptag1.com","*sptag2.com","*sptag3.com","*raasnet.com","*redaril.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?comclick\\.com","load-type":["third-party"],"unless-domain":["*hi-media.com","*comclick.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?hi-media\\.com","load-type":["third-party"],"unless-domain":["*hi-media.com","*comclick.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?hlserve\\.com","load-type":["third-party"],"unless-domain":["*hooklogic.com","*hlserve.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?hooklogic\\.com","load-type":["third-party"],"unless-domain":["*hooklogic.com","*hlserve.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?horyzon-media\\.com","load-type":["third-party"],"unless-domain":["*horyzon-media.com","*meetic-partners.com","*smartadserver.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?meetic-partners\\.com","load-type":["third-party"],"unless-domain":["*horyzon-media.com","*meetic-partners.com","*smartadserver.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?smartadserver\\.com","load-type":["third-party"],"unless-domain":["*horyzon-media.com","*meetic-partners.com","*smartadserver.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?httpool\\.com","load-type":["third-party"],"unless-domain":["*httpool.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?hurra\\.com","load-type":["third-party"],"unless-domain":["*hurra.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?i-behavior\\.com","load-type":["third-party"],"unless-domain":["*i-behavior.com","*ib-ibi.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?ib-ibi\\.com","load-type":["third-party"],"unless-domain":["*i-behavior.com","*ib-ibi.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?i\\.ua","load-type":["third-party"],"unless-domain":["*i.ua"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?iac\\.com","load-type":["third-party"],"unless-domain":["*iac.com","*iacadvertising.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?iacadvertising\\.com","load-type":["third-party"],"unless-domain":["*iac.com","*iacadvertising.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?unica\\.com","load-type":["third-party"],"unless-domain":["*ibm.com","*unica.com","*cmcore.com","*coremetrics.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?idg\\.com","load-type":["third-party"],"unless-domain":["*idg.com","*idgtechnetwork.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?idgtechnetwork\\.com","load-type":["third-party"],"unless-domain":["*idg.com","*idgtechnetwork.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?adversalservers\\.com","load-type":["third-party"],"unless-domain":["*digbro.com","*adversalservers.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?digbro\\.com","load-type":["third-party"],"unless-domain":["*digbro.com","*adversalservers.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?ignitad\\.com","load-type":["third-party"],"unless-domain":["*ignitad.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?ignitionone\\.com","load-type":["third-party"],"unless-domain":["*ignitionone.com","*ignitionone.net","*searchignite.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?ignitionone\\.net","load-type":["third-party"],"unless-domain":["*ignitionone.com","*ignitionone.net","*searchignite.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?searchignite\\.com","load-type":["third-party"],"unless-domain":["*ignitionone.com","*ignitionone.net","*searchignite.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?360yield\\.com","load-type":["third-party"],"unless-domain":["*improvedigital.com","*360yield.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?improvedigital\\.com","load-type":["third-party"],"unless-domain":["*improvedigital.com","*360yield.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?inmobi\\.com","load-type":["third-party"],"unless-domain":["*inmobi.com","*sproutinc.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?sproutinc\\.com","load-type":["third-party"],"unless-domain":["*inmobi.com","*sproutinc.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?inskinmedia\\.com","load-type":["third-party"],"unless-domain":["*inskinmedia.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?anadcoads\\.com","load-type":["third-party"],"unless-domain":["*inadco.com","*anadcoads.com","*inadcoads.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?inadco\\.com","load-type":["third-party"],"unless-domain":["*inadco.com","*anadcoads.com","*inadcoads.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?inadcoads\\.com","load-type":["third-party"],"unless-domain":["*inadco.com","*anadcoads.com","*inadcoads.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?impressiondesk\\.com","load-type":["third-party"],"unless-domain":["*infectiousmedia.com","*impressiondesk.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?infectiousmedia\\.com","load-type":["third-party"],"unless-domain":["*infectiousmedia.com","*impressiondesk.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?inflectionpointmedia\\.com","load-type":["third-party"],"unless-domain":["*inflectionpointmedia.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?infogroup\\.com","load-type":["third-party"],"unless-domain":["*infogroup.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?infolinks\\.com","load-type":["third-party"],"unless-domain":["*infolinks.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?infra-ad\\.com","load-type":["third-party"],"unless-domain":["*infra-ad.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?innity\\.com","load-type":["third-party"],"unless-domain":["*innity.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?insightexpress\\.com","load-type":["third-party"],"unless-domain":["*insightexpress.com","*insightexpressai.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?insightexpressai\\.com","load-type":["third-party"],"unless-domain":["*insightexpress.com","*insightexpressai.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?intentmedia\\.com","load-type":["third-party"],"unless-domain":["*intentmedia.com","*intentmedia.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?intentmedia\\.net","load-type":["third-party"],"unless-domain":["*intentmedia.com","*intentmedia.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?intergi\\.com","load-type":["third-party"],"unless-domain":["*intergi.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?intermarkets\\.net","load-type":["third-party"],"unless-domain":["*intermarkets.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?intermundomedia\\.com","load-type":["third-party"],"unless-domain":["*intermundomedia.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?ibpxl\\.com","load-type":["third-party"],"unless-domain":["*internetbrands.com","*ibpxl.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?internetbrands\\.com","load-type":["third-party"],"unless-domain":["*internetbrands.com","*ibpxl.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?interpolls\\.com","load-type":["third-party"],"unless-domain":["*interpolls.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?inuvo\\.com","load-type":["third-party"],"unless-domain":["*inuvo.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?investingchannel\\.com","load-type":["third-party"],"unless-domain":["*investingchannel.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?jaroop\\.com","load-type":["third-party"],"unless-domain":["*jaroop.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?jasperlabs\\.com","load-type":["third-party"],"unless-domain":["*jasperlabs.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?jemmgroup\\.com","load-type":["third-party"],"unless-domain":["*jemmgroup.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?jink\\.de","load-type":["third-party"],"unless-domain":["*jink.de","*jinkads.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?jinkads\\.com","load-type":["third-party"],"unless-domain":["*jink.de","*jinkads.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?adcolony\\.com","load-type":["third-party"],"unless-domain":["*jirbo.com","*adcolony.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?jirbo\\.com","load-type":["third-party"],"unless-domain":["*jirbo.com","*adcolony.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?jivox\\.com","load-type":["third-party"],"unless-domain":["*jivox.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?jobthread\\.com","load-type":["third-party"],"unless-domain":["*jobthread.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?juicyads\\.com","load-type":["third-party"],"unless-domain":["*juicyads.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?jumptap\\.com","load-type":["third-party"],"unless-domain":["*jumptap.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?keewurd\\.com","load-type":["third-party"],"unless-domain":["*kitd.com","*keewurd.com","*peerset.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?kitd\\.com","load-type":["third-party"],"unless-domain":["*kitd.com","*keewurd.com","*peerset.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?peerset\\.com","load-type":["third-party"],"unless-domain":["*kitd.com","*keewurd.com","*peerset.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?kenshoo\\.com","load-type":["third-party"],"unless-domain":["*kenshoo.com","*xg4ken.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?xg4ken\\.com","load-type":["third-party"],"unless-domain":["*kenshoo.com","*xg4ken.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?keyade\\.com","load-type":["third-party"],"unless-domain":["*keyade.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?kissmyads\\.com","load-type":["third-party"],"unless-domain":["*kissmyads.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?103092804\\.com","load-type":["third-party"],"unless-domain":["*kitaramedia.com","*103092804.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?kitaramedia\\.com","load-type":["third-party"],"unless-domain":["*kitaramedia.com","*103092804.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?admost\\.com","load-type":["third-party"],"unless-domain":["*kokteyl.com","*admost.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?kokteyl\\.com","load-type":["third-party"],"unless-domain":["*kokteyl.com","*admost.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?komli\\.com","load-type":["third-party"],"unless-domain":["*komli.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?kontera\\.com","load-type":["third-party"],"unless-domain":["*kontera.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?adsummos\\.com","load-type":["third-party"],"unless-domain":["*korrelate.com","*adsummos.com","*adsummos.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?adsummos\\.net","load-type":["third-party"],"unless-domain":["*korrelate.com","*adsummos.com","*adsummos.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?korrelate\\.com","load-type":["third-party"],"unless-domain":["*korrelate.com","*adsummos.com","*adsummos.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?krux\\.com","load-type":["third-party"],"unless-domain":["*krux.com","*kruxdigital.com","*krxd.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?kruxdigital\\.com","load-type":["third-party"],"unless-domain":["*krux.com","*kruxdigital.com","*krxd.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?krxd\\.net","load-type":["third-party"],"unless-domain":["*krux.com","*kruxdigital.com","*krxd.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?lakana\\.com","load-type":["third-party"],"unless-domain":["*lakana.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?ibsys\\.com","load-type":["third-party"],"unless-domain":["*lakana.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?layer-ads\\.net","load-type":["third-party"],"unless-domain":["*layer-ads.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?layer-ad\\.org","load-type":["third-party"],"unless-domain":["*layer-ad.org"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?leadbolt\\.com","load-type":["third-party"],"unless-domain":["*leadbolt.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?leadformix\\.com","load-type":["third-party"],"unless-domain":["*leadformix.com","*leadforce1.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?leadforce1\\.com","load-type":["third-party"],"unless-domain":["*leadformix.com","*leadforce1.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?leadlander\\.com","load-type":["third-party"],"unless-domain":["*leadlander.com","*trackalyzer.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?trackalyzer\\.com","load-type":["third-party"],"unless-domain":["*leadlander.com","*trackalyzer.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?legolas-media\\.com","load-type":["third-party"],"unless-domain":["*legolas-media.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?levexis\\.com","load-type":["third-party"],"unless-domain":["*levexis.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?adbull\\.com","load-type":["third-party"],"unless-domain":["*lexosmedia.com","*adbull.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?lexosmedia\\.com","load-type":["third-party"],"unless-domain":["*lexosmedia.com","*adbull.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?lfstmedia\\.com","load-type":["third-party"],"unless-domain":["*lifestreetmedia.com","*lfstmedia.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?lifestreetmedia\\.com","load-type":["third-party"],"unless-domain":["*lifestreetmedia.com","*lfstmedia.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?linkconnector\\.com","load-type":["third-party"],"unless-domain":["*linkconnector.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?linkshare\\.com","load-type":["third-party"],"unless-domain":["*linkshare.com","*linksynergy.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?linksynergy\\.com","load-type":["third-party"],"unless-domain":["*linkshare.com","*linksynergy.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?linkz\\.net","load-type":["third-party"],"unless-domain":["*linkz.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?listrak\\.com","load-type":["third-party"],"unless-domain":["*listrak.com","*listrakbi.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?listrakbi\\.com","load-type":["third-party"],"unless-domain":["*listrak.com","*listrakbi.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?liadm\\.com","load-type":["third-party"],"unless-domain":["*liveintent.com","*liadm.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?liveintent\\.com","load-type":["third-party"],"unless-domain":["*liveintent.com","*liadm.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?liveinternet\\.ru","load-type":["third-party"],"unless-domain":["*liveinternet.ru","*yadro.ru"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?yadro\\.ru","load-type":["third-party"],"unless-domain":["*liveinternet.ru","*yadro.ru"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?localyokelmedia\\.com","load-type":["third-party"],"unless-domain":["*localyokelmedia.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?longboardmedia\\.com","load-type":["third-party"],"unless-domain":["*longboardmedia.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?loomia\\.com","load-type":["third-party"],"unless-domain":["*loomia.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?lfov\\.net","load-type":["third-party"],"unless-domain":["*loopfuse.net","*lfov.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?loopfuse\\.net","load-type":["third-party"],"unless-domain":["*loopfuse.net","*lfov.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?lucidmedia\\.com","load-type":["third-party"],"unless-domain":["*lucidmedia.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?cpalead\\.com","load-type":["third-party"],"unless-domain":["*cpalead.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?mundomedia\\.com","load-type":["third-party"],"unless-domain":["*mundomedia.com","*silver-path.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?silver-path\\.com","load-type":["third-party"],"unless-domain":["*mundomedia.com","*silver-path.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?madhouse\\.cn","load-type":["third-party"],"unless-domain":["*madhouse.cn"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?dinclinx\\.com","load-type":["third-party"],"unless-domain":["*madisonlogic.com","*dinclinx.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?madisonlogic\\.com","load-type":["third-party"],"unless-domain":["*madisonlogic.com","*dinclinx.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?domdex\\.com","load-type":["third-party"],"unless-domain":["*magnetic.com","*domdex.com","*domdex.net","*qjex.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?domdex\\.net","load-type":["third-party"],"unless-domain":["*magnetic.com","*domdex.com","*domdex.net","*qjex.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?magnetic\\.com","load-type":["third-party"],"unless-domain":["*magnetic.com","*domdex.com","*domdex.net","*qjex.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?qjex\\.net","load-type":["third-party"],"unless-domain":["*magnetic.com","*domdex.com","*domdex.net","*qjex.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?dialogmgr\\.com","load-type":["third-party"],"unless-domain":["*magnify360.com","*dialogmgr.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?magnify360\\.com","load-type":["third-party"],"unless-domain":["*magnify360.com","*dialogmgr.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?campaign-archive1\\.com","load-type":["third-party"],"unless-domain":["*mailchimp.com","*campaign-archive1.com","*list-manage.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?list-manage\\.com","load-type":["third-party"],"unless-domain":["*mailchimp.com","*campaign-archive1.com","*list-manage.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?mailchimp\\.com","load-type":["third-party"],"unless-domain":["*mailchimp.com","*campaign-archive1.com","*list-manage.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?bannerbank\\.ru","load-type":["third-party"],"unless-domain":["*manifest.ru","*bannerbank.ru"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?manifest\\.ru","load-type":["third-party"],"unless-domain":["*manifest.ru","*bannerbank.ru"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?industrybrains\\.com","load-type":["third-party"],"unless-domain":["*marchex.com","*industrybrains.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?marchex\\.com","load-type":["third-party"],"unless-domain":["*marchex.com","*industrybrains.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?marimedia\\.net","load-type":["third-party"],"unless-domain":["*marimedia.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?dt00\\.net","load-type":["third-party"],"unless-domain":["*marketgid.com","*dt00.net","*dt07.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?dt07\\.net","load-type":["third-party"],"unless-domain":["*marketgid.com","*dt00.net","*dt07.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?marketgid\\.com","load-type":["third-party"],"unless-domain":["*marketgid.com","*dt00.net","*dt07.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?marketo\\.com","load-type":["third-party"],"unless-domain":["*marketo.com","*marketo.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?marketo\\.net","load-type":["third-party"],"unless-domain":["*marketo.com","*marketo.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?martiniadnetwork\\.com","load-type":["third-party"],"unless-domain":["*martinimedianetwork.com","*martiniadnetwork.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?martinimedianetwork\\.com","load-type":["third-party"],"unless-domain":["*martinimedianetwork.com","*martiniadnetwork.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?chemistry\\.com","load-type":["third-party"],"unless-domain":["*match.com","*chemistry.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?match\\.com","load-type":["third-party"],"unless-domain":["*match.com","*chemistry.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?matomy\\.com","load-type":["third-party"],"unless-domain":["*matomy.com","*matomymarket.com","*matomymedia.com","*xtendmedia.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?matomymarket\\.com","load-type":["third-party"],"unless-domain":["*matomy.com","*matomymarket.com","*matomymedia.com","*xtendmedia.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?matomymedia\\.com","load-type":["third-party"],"unless-domain":["*matomy.com","*matomymarket.com","*matomymedia.com","*xtendmedia.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?xtendmedia\\.com","load-type":["third-party"],"unless-domain":["*matomy.com","*matomymarket.com","*matomymedia.com","*xtendmedia.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?adsmarket\\.com","load-type":["third-party"],"unless-domain":["*matomy.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?matomy\\.com","load-type":["third-party"],"unless-domain":["*matomy.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?maxbounty\\.com","load-type":["third-party"],"unless-domain":["*maxbounty.com","*mb01.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?mb01\\.com","load-type":["third-party"],"unless-domain":["*maxbounty.com","*mb01.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?maxpointinteractive\\.com","load-type":["third-party"],"unless-domain":["*maxpointinteractive.com","*maxusglobal.com","*mxptint.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?maxusglobal\\.com","load-type":["third-party"],"unless-domain":["*maxpointinteractive.com","*maxusglobal.com","*mxptint.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?mxptint\\.net","load-type":["third-party"],"unless-domain":["*maxpointinteractive.com","*maxusglobal.com","*mxptint.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?mdotm\\.com","load-type":["third-party"],"unless-domain":["*mdotm.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?mediabrix\\.com","load-type":["third-party"],"unless-domain":["*mediabrix.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?mediacom\\.com","load-type":["third-party"],"unless-domain":["*mediacom.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?adroitinteractive\\.com","load-type":["third-party"],"unless-domain":["*mediamath.com","*adroitinteractive.com","*designbloxlive.com","*mathtag.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?designbloxlive\\.com","load-type":["third-party"],"unless-domain":["*mediamath.com","*adroitinteractive.com","*designbloxlive.com","*mathtag.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?mathtag\\.com","load-type":["third-party"],"unless-domain":["*mediamath.com","*adroitinteractive.com","*designbloxlive.com","*mathtag.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?mediamath\\.com","load-type":["third-party"],"unless-domain":["*mediamath.com","*adroitinteractive.com","*designbloxlive.com","*mathtag.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?media-servers\\.net","load-type":["third-party"],"unless-domain":["*mediashakers.com","*media-servers.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?mediashakers\\.com","load-type":["third-party"],"unless-domain":["*mediashakers.com","*media-servers.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?mediatrust\\.com","load-type":["third-party"],"unless-domain":["*mediatrust.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?adnetinteractive\\.com","load-type":["third-party"],"unless-domain":["*mediawhiz.com","*adnetinteractive.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?mediawhiz\\.com","load-type":["third-party"],"unless-domain":["*mediawhiz.com","*adnetinteractive.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?medialets\\.com","load-type":["third-party"],"unless-domain":["*medialets.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?adbuyer\\.com","load-type":["third-party"],"unless-domain":["*mediaocean.com","*adbuyer.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?mediaocean\\.com","load-type":["third-party"],"unless-domain":["*mediaocean.com","*adbuyer.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?medicxmedia\\.com","load-type":["third-party"],"unless-domain":["*medicxmedia.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?megaindex\\.ru","load-type":["third-party"],"unless-domain":["*megaindex.ru"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?mercent\\.com","load-type":["third-party"],"unless-domain":["*mercent.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?merchantadvantage\\.com","load-type":["third-party"],"unless-domain":["*merchantadvantage.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?merchenta\\.com","load-type":["third-party"],"unless-domain":["*merchenta.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?metanetwork\\.com","load-type":["third-party"],"unless-domain":["*metanetwork.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?meteorsolutions\\.com","load-type":["third-party"],"unless-domain":["*meteorsolutions.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?microad\\.jp","load-type":["third-party"],"unless-domain":["*www.microad.jp","*microad.jp"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?adbureau\\.net","load-type":["third-party"],"unless-domain":["*microsoft.com","*adbureau.net","*adecn.com","*aquantive.com","*atdmt.com","*msads.net","*netconversions.com","*roiservice.com","*bing.com","*gamesforwindows.com","*getgamesmart.com","*healthvault.com","*ieaddons.com","*iegallery.com","*live.com","*microsoftalumni.com","*microsoftalumni.org","*microsoftstore.com","*msn.com","*msnbc.com","*nbcnews.com","*office.com","*officelive.com","*outlook.com","*s-msn.com","*skype.com","*windowsphone.com","*worldwidetelescope.org","*xbox.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?adecn\\.com","load-type":["third-party"],"unless-domain":["*microsoft.com","*adbureau.net","*adecn.com","*aquantive.com","*atdmt.com","*msads.net","*netconversions.com","*roiservice.com","*bing.com","*gamesforwindows.com","*getgamesmart.com","*healthvault.com","*ieaddons.com","*iegallery.com","*live.com","*microsoftalumni.com","*microsoftalumni.org","*microsoftstore.com","*msn.com","*msnbc.com","*nbcnews.com","*office.com","*officelive.com","*outlook.com","*s-msn.com","*skype.com","*windowsphone.com","*worldwidetelescope.org","*xbox.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?aquantive\\.com","load-type":["third-party"],"unless-domain":["*microsoft.com","*adbureau.net","*adecn.com","*aquantive.com","*atdmt.com","*msads.net","*netconversions.com","*roiservice.com","*bing.com","*gamesforwindows.com","*getgamesmart.com","*healthvault.com","*ieaddons.com","*iegallery.com","*live.com","*microsoftalumni.com","*microsoftalumni.org","*microsoftstore.com","*msn.com","*msnbc.com","*nbcnews.com","*office.com","*officelive.com","*outlook.com","*s-msn.com","*skype.com","*windowsphone.com","*worldwidetelescope.org","*xbox.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?atdmt\\.com","load-type":["third-party"],"unless-domain":["*microsoft.com","*adbureau.net","*adecn.com","*aquantive.com","*atdmt.com","*msads.net","*netconversions.com","*roiservice.com","*bing.com","*gamesforwindows.com","*getgamesmart.com","*healthvault.com","*ieaddons.com","*iegallery.com","*live.com","*microsoftalumni.com","*microsoftalumni.org","*microsoftstore.com","*msn.com","*msnbc.com","*nbcnews.com","*office.com","*officelive.com","*outlook.com","*s-msn.com","*skype.com","*windowsphone.com","*worldwidetelescope.org","*xbox.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?msads\\.net","load-type":["third-party"],"unless-domain":["*microsoft.com","*adbureau.net","*adecn.com","*aquantive.com","*atdmt.com","*msads.net","*netconversions.com","*roiservice.com","*bing.com","*gamesforwindows.com","*getgamesmart.com","*healthvault.com","*ieaddons.com","*iegallery.com","*live.com","*microsoftalumni.com","*microsoftalumni.org","*microsoftstore.com","*msn.com","*msnbc.com","*nbcnews.com","*office.com","*officelive.com","*outlook.com","*s-msn.com","*skype.com","*windowsphone.com","*worldwidetelescope.org","*xbox.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?netconversions\\.com","load-type":["third-party"],"unless-domain":["*microsoft.com","*adbureau.net","*adecn.com","*aquantive.com","*atdmt.com","*msads.net","*netconversions.com","*roiservice.com","*bing.com","*gamesforwindows.com","*getgamesmart.com","*healthvault.com","*ieaddons.com","*iegallery.com","*live.com","*microsoftalumni.com","*microsoftalumni.org","*microsoftstore.com","*msn.com","*msnbc.com","*nbcnews.com","*office.com","*officelive.com","*outlook.com","*s-msn.com","*skype.com","*windowsphone.com","*worldwidetelescope.org","*xbox.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?roiservice\\.com","load-type":["third-party"],"unless-domain":["*microsoft.com","*adbureau.net","*adecn.com","*aquantive.com","*atdmt.com","*msads.net","*netconversions.com","*roiservice.com","*bing.com","*gamesforwindows.com","*getgamesmart.com","*healthvault.com","*ieaddons.com","*iegallery.com","*live.com","*microsoftalumni.com","*microsoftalumni.org","*microsoftstore.com","*msn.com","*msnbc.com","*nbcnews.com","*office.com","*officelive.com","*outlook.com","*s-msn.com","*skype.com","*windowsphone.com","*worldwidetelescope.org","*xbox.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?decktrade\\.com","load-type":["third-party"],"unless-domain":["*millennialmedia.com","*decktrade.com","*mydas.mobi"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?millennialmedia\\.com","load-type":["third-party"],"unless-domain":["*millennialmedia.com","*decktrade.com","*mydas.mobi"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?mydas\\.mobi","load-type":["third-party"],"unless-domain":["*millennialmedia.com","*decktrade.com","*mydas.mobi"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?mindset-media\\.com","load-type":["third-party"],"unless-domain":["*mindset-media.com","*mmismm.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?mmismm\\.com","load-type":["third-party"],"unless-domain":["*mindset-media.com","*mmismm.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?mirando\\.de","load-type":["third-party"],"unless-domain":["*mirando.de"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?mixpo\\.com","load-type":["third-party"],"unless-domain":["*mixpo.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?mopub\\.com","load-type":["third-party"],"unless-domain":["*mopub.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?moat\\.com","load-type":["third-party"],"unless-domain":["*moat.com","*moatads.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?moatads\\.com","load-type":["third-party"],"unless-domain":["*moat.com","*moatads.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?mobfox\\.com","load-type":["third-party"],"unless-domain":["*mobfox.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?admoda\\.com","load-type":["third-party"],"unless-domain":["*mobvision.com","*admoda.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?mobvision\\.com","load-type":["third-party"],"unless-domain":["*mobvision.com","*admoda.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?mobilemeteor\\.com","load-type":["third-party"],"unless-domain":["*mobilemeteor.com","*showmeinn.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?showmeinn\\.com","load-type":["third-party"],"unless-domain":["*mobilemeteor.com","*showmeinn.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?mobilestorm\\.com","load-type":["third-party"],"unless-domain":["*mobilestorm.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?moceanmobile\\.com","load-type":["third-party"],"unless-domain":["*moceanmobile.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?mochila\\.com","load-type":["third-party"],"unless-domain":["*mochila.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?mojiva\\.com","load-type":["third-party"],"unless-domain":["*mojiva.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?monetate\\.com","load-type":["third-party"],"unless-domain":["*monetate.com","*monetate.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?monetate\\.net","load-type":["third-party"],"unless-domain":["*monetate.com","*monetate.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?monetizemore\\.com","load-type":["third-party"],"unless-domain":["*monetizemore.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?monoloop\\.com","load-type":["third-party"],"unless-domain":["*monoloop.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?monster\\.com","load-type":["third-party"],"unless-domain":["*monster.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?moolah-media\\.com","load-type":["third-party"],"unless-domain":["*moolahmedia.com","*moolah-media.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?moolahmedia\\.com","load-type":["third-party"],"unless-domain":["*moolahmedia.com","*moolah-media.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?affbuzzads\\.com","load-type":["third-party"],"unless-domain":["*movielush.com","*affbuzzads.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?movielush\\.com","load-type":["third-party"],"unless-domain":["*movielush.com","*affbuzzads.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?adclickmedia\\.com","load-type":["third-party"],"unless-domain":["*multiplestreammktg.com","*adclickmedia.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?multiplestreammktg\\.com","load-type":["third-party"],"unless-domain":["*multiplestreammktg.com","*adclickmedia.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?mybuys\\.com","load-type":["third-party"],"unless-domain":["*mybuys.com","*veruta.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?veruta\\.com","load-type":["third-party"],"unless-domain":["*mybuys.com","*veruta.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?mycounter\\.com\\.ua","load-type":["third-party"],"unless-domain":["*mycounter.com.ua"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?mywebgrocer\\.com","load-type":["third-party"],"unless-domain":["*mywebgrocer.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?nanigans\\.com","load-type":["third-party"],"unless-domain":["*nanigans.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?navdmp\\.com","load-type":["third-party"],"unless-domain":["*navegg.com","*navdmp.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?navegg\\.com","load-type":["third-party"],"unless-domain":["*navegg.com","*navdmp.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?cdnma\\.com","load-type":["third-party"],"unless-domain":["*net-results.com","*cdnma.com","*nr7.us"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?net-results\\.com","load-type":["third-party"],"unless-domain":["*net-results.com","*cdnma.com","*nr7.us"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?nr7\\.us","load-type":["third-party"],"unless-domain":["*net-results.com","*cdnma.com","*nr7.us"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?netaffiliation\\.com","load-type":["third-party"],"unless-domain":["*netaffiliation.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?netbina\\.com","load-type":["third-party"],"unless-domain":["*netbina.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?adelixir\\.com","load-type":["third-party"],"unless-domain":["*netelixir.com","*adelixir.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?netelixir\\.com","load-type":["third-party"],"unless-domain":["*netelixir.com","*adelixir.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?netseer\\.com","load-type":["third-party"],"unless-domain":["*netseer.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?netshelter\\.com","load-type":["third-party"],"unless-domain":["*netshelter.com","*netshelter.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?netshelter\\.net","load-type":["third-party"],"unless-domain":["*netshelter.com","*netshelter.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?netmining\\.com","load-type":["third-party"],"unless-domain":["*netmining.com","*netmng.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?netmng\\.com","load-type":["third-party"],"unless-domain":["*netmining.com","*netmng.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?adadvisor\\.net","load-type":["third-party"],"unless-domain":["*neustar.biz","*adadvisor.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?neustar\\.biz","load-type":["third-party"],"unless-domain":["*neustar.biz","*adadvisor.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?nexage\\.com","load-type":["third-party"],"unless-domain":["*nexage.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?nextperformance\\.com","load-type":["third-party"],"unless-domain":["*nextperformance.com","*nxtck.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?nxtck\\.com","load-type":["third-party"],"unless-domain":["*nextperformance.com","*nxtck.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?nextag\\.com","load-type":["third-party"],"unless-domain":["*nextag.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?imrworldwide\\.com","load-type":["third-party"],"unless-domain":["*nielsen.com","*imrworldwide.com","*imrworldwide.net","*glanceguide.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?imrworldwide\\.net","load-type":["third-party"],"unless-domain":["*nielsen.com","*imrworldwide.com","*imrworldwide.net","*glanceguide.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?networkedblogs\\.com","load-type":["third-party"],"unless-domain":["*ninua.com","*networkedblogs.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?ninua\\.com","load-type":["third-party"],"unless-domain":["*ninua.com","*networkedblogs.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?noktamedya\\.com","load-type":["third-party"],"unless-domain":["*noktamedya.com","*virgul.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?virgul\\.com","load-type":["third-party"],"unless-domain":["*noktamedya.com","*virgul.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?nowspots\\.com","load-type":["third-party"],"unless-domain":["*nowspots.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?nuffnang\\.com","load-type":["third-party"],"unless-domain":["*www.nuffnang.com.my","*nuffnang.com","*nuffnang.com.my"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?nuffnang\\.com\\.my","load-type":["third-party"],"unless-domain":["*www.nuffnang.com.my","*nuffnang.com","*nuffnang.com.my"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?advg\\.jp","load-type":["third-party"],"unless-domain":["*www.opt.ne.jp","*advg.jp","*opt.ne.jp","*p-advg.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?opt\\.ne\\.jp","load-type":["third-party"],"unless-domain":["*www.opt.ne.jp","*advg.jp","*opt.ne.jp","*p-advg.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?p-advg\\.com","load-type":["third-party"],"unless-domain":["*www.opt.ne.jp","*advg.jp","*opt.ne.jp","*p-advg.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?adohana\\.com","load-type":["third-party"],"unless-domain":["*ohana-media.com","*adohana.com","*ohanaqb.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?ohana-media\\.com","load-type":["third-party"],"unless-domain":["*ohana-media.com","*adohana.com","*ohanaqb.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?ohanaqb\\.com","load-type":["third-party"],"unless-domain":["*ohana-media.com","*adohana.com","*ohanaqb.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?accuenmedia\\.com","load-type":["third-party"],"unless-domain":["*omnicomgroup.com","*accuenmedia.com","*p-td.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?omnicomgroup\\.com","load-type":["third-party"],"unless-domain":["*omnicomgroup.com","*accuenmedia.com","*p-td.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?p-td\\.com","load-type":["third-party"],"unless-domain":["*omnicomgroup.com","*accuenmedia.com","*p-td.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?itsoneiota\\.com","load-type":["third-party"],"unless-domain":["*itsoneiota.com","*oneiota.co.uk"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?oneiota\\.co\\.uk","load-type":["third-party"],"unless-domain":["*itsoneiota.com","*oneiota.co.uk"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?oneupweb\\.com","load-type":["third-party"],"unless-domain":["*oneupweb.com","*sodoit.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?sodoit\\.com","load-type":["third-party"],"unless-domain":["*oneupweb.com","*sodoit.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?onm\\.de","load-type":["third-party"],"unless-domain":["*onm.de"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?liftdna\\.com","load-type":["third-party"],"unless-domain":["*openx.com","*openx.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?openx\\.com","load-type":["third-party"],"unless-domain":["*openx.com","*openx.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?openx\\.net","load-type":["third-party"],"unless-domain":["*openx.com","*openx.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?openx\\.org","load-type":["third-party"],"unless-domain":["*openx.com","*openx.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?openxenterprise\\.com","load-type":["third-party"],"unless-domain":["*openx.com","*openx.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?servedbyopenx\\.com","load-type":["third-party"],"unless-domain":["*openx.com","*openx.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?mobiletheory\\.com","load-type":["third-party"],"unless-domain":["*opera.com","*mobiletheory.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?operamediaworks\\.com","load-type":["third-party"],"unless-domain":["*opera.com","*mobiletheory.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?operasoftware\\.com","load-type":["third-party"],"unless-domain":["*opera.com","*mobiletheory.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?opera\\.com","load-type":["third-party"],"unless-domain":["*opera.com","*mobiletheory.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?optmd\\.com","load-type":["third-party"],"unless-domain":["*optmd.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?optify\\.net","load-type":["third-party"],"unless-domain":["*optify.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?cpmadvisors\\.com","load-type":["third-party"],"unless-domain":["*bn.co"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?cpmatic\\.com","load-type":["third-party"],"unless-domain":["*bn.co"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?nprove\\.com","load-type":["third-party"],"unless-domain":["*bn.co"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?optim\\.al","load-type":["third-party"],"unless-domain":["*bn.co"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?orbengine\\.com","load-type":["third-party"],"unless-domain":["*bn.co"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?xa\\.net","load-type":["third-party"],"unless-domain":["*bn.co"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?optimumresponse\\.com","load-type":["third-party"],"unless-domain":["*optimumresponse.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?estara\\.com","load-type":["third-party"],"unless-domain":["*oracle.com","*estara.com","*atgsvcs.com","*instantservice.com","*istrack.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?orangesoda\\.com","load-type":["third-party"],"unless-domain":["*orangesoda.com","*otracking.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?otracking\\.com","load-type":["third-party"],"unless-domain":["*orangesoda.com","*otracking.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?out-there-media\\.com","load-type":["third-party"],"unless-domain":["*out-there-media.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?outbrain\\.com","load-type":["third-party"],"unless-domain":["*outbrain.com","*sphere.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?sphere\\.com","load-type":["third-party"],"unless-domain":["*outbrain.com","*sphere.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?dsnextgen\\.com","load-type":["third-party"],"unless-domain":["*oversee.net","*dsnextgen.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?oversee\\.net","load-type":["third-party"],"unless-domain":["*oversee.net","*dsnextgen.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?owneriq\\.com","load-type":["third-party"],"unless-domain":["*owneriq.com","*owneriq.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?owneriq\\.net","load-type":["third-party"],"unless-domain":["*owneriq.com","*owneriq.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?adconnexa\\.com","load-type":["third-party"],"unless-domain":["*oxamedia.com","*adconnexa.com","*adsbwm.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?adsbwm\\.com","load-type":["third-party"],"unless-domain":["*oxamedia.com","*adconnexa.com","*adsbwm.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?oxamedia\\.com","load-type":["third-party"],"unless-domain":["*oxamedia.com","*adconnexa.com","*adsbwm.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?platform-one\\.co\\.jp","load-type":["third-party"],"unless-domain":["*www.platform-one.co.jp","*platform-one.co.jp"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?paid-to-promote\\.net","load-type":["third-party"],"unless-domain":["*paid-to-promote.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?pardot\\.com","load-type":["third-party"],"unless-domain":["*pardot.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?payhit\\.com","load-type":["third-party"],"unless-domain":["*payhit.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?lzjl\\.com","load-type":["third-party"],"unless-domain":["*paypopup.com","*lzjl.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?paypopup\\.com","load-type":["third-party"],"unless-domain":["*paypopup.com","*lzjl.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?peer39\\.com","load-type":["third-party"],"unless-domain":["*peer39.com","*peer39.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?peer39\\.net","load-type":["third-party"],"unless-domain":["*peer39.com","*peer39.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?peerfly\\.com","load-type":["third-party"],"unless-domain":["*peerfly.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?performancing\\.com","load-type":["third-party"],"unless-domain":["*performancing.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?pheedo\\.com","load-type":["third-party"],"unless-domain":["*pheedo.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?pictela\\.com","load-type":["third-party"],"unless-domain":["*pictela.com","*pictela.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?pictela\\.net","load-type":["third-party"],"unless-domain":["*pictela.com","*pictela.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?pixel\\.sg","load-type":["third-party"],"unless-domain":["*pixel.sg"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?piximedia\\.com","load-type":["third-party"],"unless-domain":["*piximedia.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?po\\.st","load-type":["third-party"],"unless-domain":["*po.st"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?pocketcents\\.com","load-type":["third-party"],"unless-domain":["*pocketcents.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?politads\\.com","load-type":["third-party"],"unless-domain":["*politads.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?pontiflex\\.com","load-type":["third-party"],"unless-domain":["*pontiflex.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?popads\\.net","load-type":["third-party"],"unless-domain":["*popads.net","*popadscdn.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?popadscdn\\.net","load-type":["third-party"],"unless-domain":["*popads.net","*popadscdn.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?gocampaignlive\\.com","load-type":["third-party"],"unless-domain":["*poprule.com","*gocampaignlive.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?poprule\\.com","load-type":["third-party"],"unless-domain":["*poprule.com","*gocampaignlive.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?popunder\\.ru","load-type":["third-party"],"unless-domain":["*popunder.ru"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?precisionclick\\.com","load-type":["third-party"],"unless-domain":["*precisionclick.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?predictad\\.com","load-type":["third-party"],"unless-domain":["*predictad.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?blogads\\.com","load-type":["third-party"],"unless-domain":["*pressflex.com","*blogads.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?pressflex\\.com","load-type":["third-party"],"unless-domain":["*pressflex.com","*blogads.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?adcde\\.com","load-type":["third-party"],"unless-domain":["*primevisibility.com","*adonnetwork.com","*adonnetwork.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?addlvr\\.com","load-type":["third-party"],"unless-domain":["*primevisibility.com","*adonnetwork.com","*adonnetwork.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?adonnetwork\\.com","load-type":["third-party"],"unless-domain":["*primevisibility.com","*adonnetwork.com","*adonnetwork.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?adonnetwork\\.net","load-type":["third-party"],"unless-domain":["*primevisibility.com","*adonnetwork.com","*adonnetwork.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?adtrgt\\.com","load-type":["third-party"],"unless-domain":["*primevisibility.com","*adonnetwork.com","*adonnetwork.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?bannertgt\\.com","load-type":["third-party"],"unless-domain":["*primevisibility.com","*adonnetwork.com","*adonnetwork.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?cptgt\\.com","load-type":["third-party"],"unless-domain":["*primevisibility.com","*adonnetwork.com","*adonnetwork.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?cpvfeed\\.com","load-type":["third-party"],"unless-domain":["*primevisibility.com","*adonnetwork.com","*adonnetwork.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?cpvtgt\\.com","load-type":["third-party"],"unless-domain":["*primevisibility.com","*adonnetwork.com","*adonnetwork.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?popcde\\.com","load-type":["third-party"],"unless-domain":["*primevisibility.com","*adonnetwork.com","*adonnetwork.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?primevisibility\\.com","load-type":["third-party"],"unless-domain":["*primevisibility.com","*adonnetwork.com","*adonnetwork.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?sdfje\\.com","load-type":["third-party"],"unless-domain":["*primevisibility.com","*adonnetwork.com","*adonnetwork.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?urtbk\\.com","load-type":["third-party"],"unless-domain":["*primevisibility.com","*adonnetwork.com","*adonnetwork.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?proclivitymedia\\.com","load-type":["third-party"],"unless-domain":["*proclivitymedia.com","*proclivitysystems.com","*pswec.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?proclivitysystems\\.com","load-type":["third-party"],"unless-domain":["*proclivitymedia.com","*proclivitysystems.com","*pswec.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?pswec\\.com","load-type":["third-party"],"unless-domain":["*proclivitymedia.com","*proclivitysystems.com","*pswec.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?projectwonderful\\.com","load-type":["third-party"],"unless-domain":["*projectwonderful.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?propellerads\\.com","load-type":["third-party"],"unless-domain":["*propellerads.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?prosperent\\.com","load-type":["third-party"],"unless-domain":["*prosperent.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?proxilinks\\.com","load-type":["third-party"],"unless-domain":["*proximic.com","*proxilinks.com","*proximic.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?proximic\\.com","load-type":["third-party"],"unless-domain":["*proximic.com","*proxilinks.com","*proximic.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?proximic\\.net","load-type":["third-party"],"unless-domain":["*proximic.com","*proxilinks.com","*proximic.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?pubmatic\\.com","load-type":["third-party"],"unless-domain":["*pubmatic.com","*revinet.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?revinet\\.com","load-type":["third-party"],"unless-domain":["*pubmatic.com","*revinet.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?publicidees\\.com","load-type":["third-party"],"unless-domain":["*publicidees.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?pch\\.com","load-type":["third-party"],"unless-domain":["*pch.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?i-behavior\\.com","load-type":["third-party"],"unless-domain":["*iaded.com","*quisma.com","*quismatch.com","*xmladed.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?iaded\\.com","load-type":["third-party"],"unless-domain":["*iaded.com","*quisma.com","*quismatch.com","*xmladed.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?quisma\\.com","load-type":["third-party"],"unless-domain":["*iaded.com","*quisma.com","*quismatch.com","*xmladed.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?quismatch\\.com","load-type":["third-party"],"unless-domain":["*iaded.com","*quisma.com","*quismatch.com","*xmladed.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?xaded\\.com","load-type":["third-party"],"unless-domain":["*iaded.com","*quisma.com","*quismatch.com","*xmladed.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?xmladed\\.com","load-type":["third-party"],"unless-domain":["*iaded.com","*quisma.com","*quismatch.com","*xmladed.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?quakemarketing\\.com","load-type":["third-party"],"unless-domain":["*quakemarketing.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?quantcast\\.com","load-type":["third-party"],"unless-domain":["*quantcast.com","*quantserve.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?quantserve\\.com","load-type":["third-party"],"unless-domain":["*quantcast.com","*quantserve.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?qnsr\\.com","load-type":["third-party"],"unless-domain":["*quinstreet.com","*thecounter.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?qsstats\\.com","load-type":["third-party"],"unless-domain":["*quinstreet.com","*thecounter.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?quinstreet\\.com","load-type":["third-party"],"unless-domain":["*quinstreet.com","*thecounter.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?rmbn\\.net","load-type":["third-party"],"unless-domain":["*rmbn.net","*rmbn.ru"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?rmbn\\.ru","load-type":["third-party"],"unless-domain":["*rmbn.net","*rmbn.ru"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?rmmonline\\.com","load-type":["third-party"],"unless-domain":["*rmmonline.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?matchbin\\.com","load-type":["third-party"],"unless-domain":["*radiatemedia.com","*matchbin.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?radiatemedia\\.com","load-type":["third-party"],"unless-domain":["*radiatemedia.com","*matchbin.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?gwallet\\.com","load-type":["third-party"],"unless-domain":["*radiumone.com","*gwallet.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?radiumone\\.com","load-type":["third-party"],"unless-domain":["*radiumone.com","*gwallet.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?radiusmarketing\\.com","load-type":["third-party"],"unless-domain":["*radiusmarketing.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?rambler\\.ru","load-type":["third-party"],"unless-domain":["*rambler.ru"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?liveramp\\.com","load-type":["third-party"],"unless-domain":["*rapleaf.com","*liveramp.com","*rlcdn.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?rapleaf\\.com","load-type":["third-party"],"unless-domain":["*rapleaf.com","*liveramp.com","*rlcdn.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?rlcdn\\.com","load-type":["third-party"],"unless-domain":["*rapleaf.com","*liveramp.com","*rlcdn.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?retargeter\\.com","load-type":["third-party"],"unless-domain":["*retargeter.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?reachlocal\\.com","load-type":["third-party"],"unless-domain":["*reachlocal.com","*rlcdn.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?rlcdn\\.net","load-type":["third-party"],"unless-domain":["*reachlocal.com","*rlcdn.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?react2media\\.com","load-type":["third-party"],"unless-domain":["*react2media.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?reduxmedia\\.com","load-type":["third-party"],"unless-domain":["*reduxmedia.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?convertglobal\\.com","load-type":["third-party"],"unless-domain":["*rekko.com","*convertglobal.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?rekko\\.com","load-type":["third-party"],"unless-domain":["*rekko.com","*convertglobal.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?reklamstore\\.com","load-type":["third-party"],"unless-domain":["*reklamstore.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?reklamport\\.com","load-type":["third-party"],"unless-domain":["*reklamport.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?reklamz\\.com","load-type":["third-party"],"unless-domain":["*reklamz.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?relestar\\.com","load-type":["third-party"],"unless-domain":["*relevad.com","*relestar.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?relevad\\.com","load-type":["third-party"],"unless-domain":["*relevad.com","*relestar.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?advertserve\\.com","load-type":["third-party"],"unless-domain":["*renegadeinternet.com","*advertserve.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?renegadeinternet\\.com","load-type":["third-party"],"unless-domain":["*renegadeinternet.com","*advertserve.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?resolutionmedia\\.com","load-type":["third-party"],"unless-domain":["*resolutionmedia.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?resonateinsights\\.com","load-type":["third-party"],"unless-domain":["*resonateinsights.com","*resonatenetworks.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?resonatenetworks\\.com","load-type":["third-party"],"unless-domain":["*resonateinsights.com","*resonatenetworks.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?responsys\\.com","load-type":["third-party"],"unless-domain":["*responsys.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?blvdstatus\\.com","load-type":["third-party"],"unless-domain":["*retirement-living.com","*blvdstatus.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?retirement-living\\.com","load-type":["third-party"],"unless-domain":["*retirement-living.com","*blvdstatus.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?revcontent\\.com","load-type":["third-party"],"unless-domain":["*revcontent.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?revenuemax\\.de","load-type":["third-party"],"unless-domain":["*revenuemax.de"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?rhythmnewmedia\\.com","load-type":["third-party"],"unless-domain":["*rhythmnewmedia.com","*rnmd.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?rnmd\\.net","load-type":["third-party"],"unless-domain":["*rhythmnewmedia.com","*rnmd.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?rhythmxchange\\.com","load-type":["third-party"],"unless-domain":["*rhythmnewmedia.com","*rnmd.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?1rx\\.io","load-type":["third-party"],"unless-domain":["*rhythmnewmedia.com","*rnmd.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?richrelevance\\.com","load-type":["third-party"],"unless-domain":["*richrelevance.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?rightaction\\.com","load-type":["third-party"],"unless-domain":["*rightaction.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?rfihub\\.com","load-type":["third-party"],"unless-domain":["*rocketfuel.com","*rfihub.com","*rfihub.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?rfihub\\.net","load-type":["third-party"],"unless-domain":["*rocketfuel.com","*rfihub.com","*rfihub.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?rocketfuel\\.com","load-type":["third-party"],"unless-domain":["*rocketfuel.com","*rfihub.com","*rfihub.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?rovion\\.com","load-type":["third-party"],"unless-domain":["*rovion.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?rutarget\\.ru","load-type":["third-party"],"unless-domain":["*rutarget.ru"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?aimatch\\.com","load-type":["third-party"],"unless-domain":["*sas.com","*aimatch.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?sas\\.com","load-type":["third-party"],"unless-domain":["*sas.com","*aimatch.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?reztrack\\.com","load-type":["third-party"],"unless-domain":["*sabre.com","*reztrack.com","*sabrehospitality.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?sabre\\.com","load-type":["third-party"],"unless-domain":["*sabre.com","*reztrack.com","*sabrehospitality.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?sabrehospitality\\.com","load-type":["third-party"],"unless-domain":["*sabre.com","*reztrack.com","*sabrehospitality.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?salesforce\\.com","load-type":["third-party"],"unless-domain":["*salesforce.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?samurai-factory\\.jp","load-type":["third-party"],"unless-domain":["*samurai-factory.jp","*shinobi.jp"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?shinobi\\.jp","load-type":["third-party"],"unless-domain":["*samurai-factory.jp","*shinobi.jp"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?bridgetrack\\.com","load-type":["third-party"],"unless-domain":["*sapient.com","*bridgetrack.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?sapient\\.com","load-type":["third-party"],"unless-domain":["*sapient.com","*bridgetrack.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?scandinavianadnetworks\\.com","load-type":["third-party"],"unless-domain":["*scandinavianadnetworks.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?scribol\\.com","load-type":["third-party"],"unless-domain":["*scribol.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?searchforce\\.com","load-type":["third-party"],"unless-domain":["*searchforce.com","*searchforce.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?searchforce\\.net","load-type":["third-party"],"unless-domain":["*searchforce.com","*searchforce.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?kanoodle\\.com","load-type":["third-party"],"unless-domain":["*seevast.com","*kanoodle.com","*pulse360.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?pulse360\\.com","load-type":["third-party"],"unless-domain":["*seevast.com","*kanoodle.com","*pulse360.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?seevast\\.com","load-type":["third-party"],"unless-domain":["*seevast.com","*kanoodle.com","*pulse360.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?syndigonetworks\\.com","load-type":["third-party"],"unless-domain":["*seevast.com","*kanoodle.com","*pulse360.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?nabbr\\.com","load-type":["third-party"],"unless-domain":["*selectablemedia.com","*nabbr.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?selectablemedia\\.com","load-type":["third-party"],"unless-domain":["*selectablemedia.com","*nabbr.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?sevenads\\.net","load-type":["third-party"],"unless-domain":["*sevenads.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?sexinyourcity\\.com","load-type":["third-party"],"unless-domain":["*sexinyourcity.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?shareasale\\.com","load-type":["third-party"],"unless-domain":["*shareasale.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?shopzilla\\.com","load-type":["third-party"],"unless-domain":["*shopzilla.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?mkt51\\.net","load-type":["third-party"],"unless-domain":["*silverpop.com","*mkt51.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?pages05\\.net","load-type":["third-party"],"unless-domain":["*silverpop.com","*mkt51.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?silverpop\\.com","load-type":["third-party"],"unless-domain":["*silverpop.com","*mkt51.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?vtrenz\\.net","load-type":["third-party"],"unless-domain":["*silverpop.com","*mkt51.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?simpli\\.fi","load-type":["third-party"],"unless-domain":["*simpli.fi"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?sitescout\\.com","load-type":["third-party"],"unless-domain":["*sitescout.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?skimlinks\\.com","load-type":["third-party"],"unless-domain":["*skimlinks.com","*skimresources.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?skimresources\\.com","load-type":["third-party"],"unless-domain":["*skimlinks.com","*skimresources.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?adcentriconline\\.com","load-type":["third-party"],"unless-domain":["*skupenet.com","*adcentriconline.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?skupenet\\.com","load-type":["third-party"],"unless-domain":["*skupenet.com","*adcentriconline.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?smaato\\.com","load-type":["third-party"],"unless-domain":["*smaato.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?smileymedia\\.com","load-type":["third-party"],"unless-domain":["*smileymedia.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?smowtion\\.com","load-type":["third-party"],"unless-domain":["*smowtion.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?snap\\.com","load-type":["third-party"],"unless-domain":["*snap.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?halogenmediagroup\\.com","load-type":["third-party"],"unless-domain":["*socialchorus.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?halogennetwork\\.com","load-type":["third-party"],"unless-domain":["*socialchorus.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?socialchorus\\.com","load-type":["third-party"],"unless-domain":["*socialchorus.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?ratevoice\\.com","load-type":["third-party"],"unless-domain":["*socialinterface.com","*ratevoice.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?socialinterface\\.com","load-type":["third-party"],"unless-domain":["*socialinterface.com","*ratevoice.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?socialtwist\\.com","load-type":["third-party"],"unless-domain":["*socialtwist.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?spacechimpmedia\\.com","load-type":["third-party"],"unless-domain":["*spacechimpmedia.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?sparkstudios\\.com","load-type":["third-party"],"unless-domain":["*sparkstudios.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?adbutler\\.com","load-type":["third-party"],"unless-domain":["*sparklit.com","*adbutler.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?sparklit\\.com","load-type":["third-party"],"unless-domain":["*sparklit.com","*adbutler.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?adviva\\.co\\.uk","load-type":["third-party"],"unless-domain":["*specificmedia.com","*sitemeter.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?adviva\\.net","load-type":["third-party"],"unless-domain":["*specificmedia.com","*sitemeter.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?sitemeter\\.com","load-type":["third-party"],"unless-domain":["*specificmedia.com","*sitemeter.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?specificclick\\.net","load-type":["third-party"],"unless-domain":["*specificmedia.com","*sitemeter.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?specificmedia\\.com","load-type":["third-party"],"unless-domain":["*specificmedia.com","*sitemeter.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?specificmedia\\.co\\.uk","load-type":["third-party"],"unless-domain":["*specificmedia.com","*sitemeter.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?spectate\\.com","load-type":["third-party"],"unless-domain":["*spectate.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?spongegroup\\.com","load-type":["third-party"],"unless-domain":["*spongegroup.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?spongecell\\.com","load-type":["third-party"],"unless-domain":["*spongecell.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?sponsorads\\.de","load-type":["third-party"],"unless-domain":["*sponsorads.de"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?spot200\\.com","load-type":["third-party"],"unless-domain":["*spot200.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?spotxchange\\.com","load-type":["third-party"],"unless-domain":["*spotxchange.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?spotxcdn\\.com","load-type":["third-party"],"unless-domain":["*spotxchange.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?stargamesaffiliate\\.com","load-type":["third-party"],"unless-domain":["*stargames.net","*stargamesaffiliate.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?steelhouse\\.com","load-type":["third-party"],"unless-domain":["*steelhouse.com","*steelhousemedia.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?steelhousemedia\\.com","load-type":["third-party"],"unless-domain":["*steelhouse.com","*steelhousemedia.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?cams\\.com","load-type":["third-party"],"unless-domain":["*streamray.com","*cams.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?streamray\\.com","load-type":["third-party"],"unless-domain":["*streamray.com","*cams.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?strikead\\.com","load-type":["third-party"],"unless-domain":["*strikead.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?popularmedia\\.com","load-type":["third-party"],"unless-domain":["*strongmail.com","*popularmedia.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?struq\\.com","load-type":["third-party"],"unless-domain":["*struq.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?suite66\\.com","load-type":["third-party"],"unless-domain":["*suite66.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?summitmedia\\.co\\.uk","load-type":["third-party"],"unless-domain":["*www.summit.co.uk","*summitmedia.co.uk"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?supersonicads\\.com","load-type":["third-party"],"unless-domain":["*supersonicads.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?switchadhub\\.com","load-type":["third-party"],"unless-domain":["*switchconcepts.com","*switchadhub.com","*switchconcepts.co.uk"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?switchconcepts\\.co\\.uk","load-type":["third-party"],"unless-domain":["*switchconcepts.com","*switchadhub.com","*switchconcepts.co.uk"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?switchconcepts\\.com","load-type":["third-party"],"unless-domain":["*switchconcepts.com","*switchadhub.com","*switchconcepts.co.uk"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?ethicalads\\.net","load-type":["third-party"],"unless-domain":["*switchconcepts.com","*switchadhub.com","*switchconcepts.co.uk"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?switchads\\.com","load-type":["third-party"],"unless-domain":["*switchconcepts.com","*switchadhub.com","*switchconcepts.co.uk"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?swoop\\.com","load-type":["third-party"],"unless-domain":["*swoop.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?factortg\\.com","load-type":["third-party"],"unless-domain":["*factortg.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?clickable\\.net","load-type":["third-party"],"unless-domain":["*syncapse.com","*clickable.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?syncapse\\.com","load-type":["third-party"],"unless-domain":["*syncapse.com","*clickable.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?tlvmedia\\.com","load-type":["third-party"],"unless-domain":["*tlvmedia.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?taboola\\.com","load-type":["third-party"],"unless-domain":["*taboola.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?tailsweep\\.com","load-type":["third-party"],"unless-domain":["*tailsweep.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?tap\\.me","load-type":["third-party"],"unless-domain":["*tap.me"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?tapit\\.com","load-type":["third-party"],"unless-domain":["*tapit.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?tapad\\.com","load-type":["third-party"],"unless-domain":["*tapad.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?bizmey\\.com","load-type":["third-party"],"unless-domain":["*tapgage.com","*bizmey.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?tapgage\\.com","load-type":["third-party"],"unless-domain":["*tapgage.com","*bizmey.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?targetix\\.net","load-type":["third-party"],"unless-domain":["*targetix.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?quicknoodles\\.com","load-type":["third-party"],"unless-domain":["*tattomedia.com","*quicknoodles.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?tattomedia\\.com","load-type":["third-party"],"unless-domain":["*tattomedia.com","*quicknoodles.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?teadma\\.com","load-type":["third-party"],"unless-domain":["*teadma.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?teads\\.tv","load-type":["third-party"],"unless-domain":["*teads.tv"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?ebuzzing\\.com","load-type":["third-party"],"unless-domain":["*teads.tv"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?technorati\\.com","load-type":["third-party"],"unless-domain":["*technorati.com","*technoratimedia.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?technoratimedia\\.com","load-type":["third-party"],"unless-domain":["*technorati.com","*technoratimedia.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?tellapart\\.com","load-type":["third-party"],"unless-domain":["*tellapart.com","*tellapt.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?tellapt\\.com","load-type":["third-party"],"unless-domain":["*tellapart.com","*tellapt.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?sensis\\.com\\.au","load-type":["third-party"],"unless-domain":["*telstra.com.au","*sensis.com.au","*sensisdata.com.au","*sensisdigitalmedia.com.au"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?sensisdata\\.com\\.au","load-type":["third-party"],"unless-domain":["*telstra.com.au","*sensis.com.au","*sensisdata.com.au","*sensisdigitalmedia.com.au"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?sensisdigitalmedia\\.com\\.au","load-type":["third-party"],"unless-domain":["*telstra.com.au","*sensis.com.au","*sensisdata.com.au","*sensisdigitalmedia.com.au"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?telstra\\.com\\.au","load-type":["third-party"],"unless-domain":["*telstra.com.au","*sensis.com.au","*sensisdata.com.au","*sensisdigitalmedia.com.au"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?eztargetmedia\\.com","load-type":["third-party"],"unless-domain":["*www.terra.com.br","*eztargetmedia.com","*terra.com.br"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?terra\\.com\\.br","load-type":["third-party"],"unless-domain":["*www.terra.com.br","*eztargetmedia.com","*terra.com.br"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?hittail\\.com","load-type":["third-party"],"unless-domain":["*thenumagroup.com","*hittail.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?thenumagroup\\.com","load-type":["third-party"],"unless-domain":["*thenumagroup.com","*hittail.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?rimmkaufman\\.com","load-type":["third-party"],"unless-domain":["*rimmkaufman.com","*rkdms.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?rkdms\\.com","load-type":["third-party"],"unless-domain":["*rimmkaufman.com","*rkdms.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?thesearchagency\\.com","load-type":["third-party"],"unless-domain":["*thesearchagency.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?thesearchagency\\.net","load-type":["third-party"],"unless-domain":["*thesearchagency.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?adsrvr\\.org","load-type":["third-party"],"unless-domain":["*thetradedesk.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?thetradedesk\\.com","load-type":["third-party"],"unless-domain":["*thetradedesk.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?echosearch\\.com","load-type":["third-party"],"unless-domain":["*thinkrealtime.com","*echosearch.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?esm1\\.net","load-type":["third-party"],"unless-domain":["*thinkrealtime.com","*echosearch.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?thinkrealtime\\.com","load-type":["third-party"],"unless-domain":["*thinkrealtime.com","*echosearch.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?carbonads\\.com","load-type":["third-party"],"unless-domain":["*tinder.com","*carbonads.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?tinder\\.com","load-type":["third-party"],"unless-domain":["*tinder.com","*carbonads.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?tiqiq\\.com","load-type":["third-party"],"unless-domain":["*tiqiq.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?todacell\\.com","load-type":["third-party"],"unless-domain":["*todacell.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?tonefuse\\.com","load-type":["third-party"],"unless-domain":["*tonefuse.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?clickfuse\\.com","load-type":["third-party"],"unless-domain":["*tonemedia.com","*clickfuse.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?tonemedia\\.com","load-type":["third-party"],"unless-domain":["*tonemedia.com","*clickfuse.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?inq\\.com","load-type":["third-party"],"unless-domain":["*touchcommerce.com","*inq.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?touchcommerce\\.com","load-type":["third-party"],"unless-domain":["*touchcommerce.com","*inq.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?trackingsoft\\.com","load-type":["third-party"],"unless-domain":["*trackingsoft.com","*roia.biz"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?tradetracker\\.com","load-type":["third-party"],"unless-domain":["*tradetracker.com","*tradetracker.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?tradetracker\\.net","load-type":["third-party"],"unless-domain":["*tradetracker.com","*tradetracker.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?tradedoubler\\.com","load-type":["third-party"],"unless-domain":["*tradedoubler.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?traffichaus\\.com","load-type":["third-party"],"unless-domain":["*traffichaus.com","*traffichouse.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?traffichouse\\.com","load-type":["third-party"],"unless-domain":["*traffichaus.com","*traffichouse.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?trafficrevenue\\.net","load-type":["third-party"],"unless-domain":["*trafficrevenue.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?traffiq\\.com","load-type":["third-party"],"unless-domain":["*traffiq.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?traveladnetwork\\.com","load-type":["third-party"],"unless-domain":["*travoramedia.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?traveladvertising\\.com","load-type":["third-party"],"unless-domain":["*travoramedia.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?travoramedia\\.com","load-type":["third-party"],"unless-domain":["*travoramedia.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?scanscout\\.com","load-type":["third-party"],"unless-domain":["*tremorvideo.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?tmnetads\\.com","load-type":["third-party"],"unless-domain":["*tremorvideo.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?tremormedia\\.com","load-type":["third-party"],"unless-domain":["*tremorvideo.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?tremorvideo\\.com","load-type":["third-party"],"unless-domain":["*tremorvideo.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?triggit\\.com","load-type":["third-party"],"unless-domain":["*triggit.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?adlegend\\.com","load-type":["third-party"],"unless-domain":["*trueffect.com","*adlegend.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?trueffect\\.com","load-type":["third-party"],"unless-domain":["*trueffect.com","*adlegend.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?tmogul\\.com","load-type":["third-party"],"unless-domain":["*tubemogul.com","*tmogul.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?tubemogul\\.com","load-type":["third-party"],"unless-domain":["*tubemogul.com","*tmogul.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?buzzlogic\\.com","load-type":["third-party"],"unless-domain":["*twelvefold.com","*buzzlogic.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?twelvefold\\.com","load-type":["third-party"],"unless-domain":["*twelvefold.com","*buzzlogic.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?tyroo\\.com","load-type":["third-party"],"unless-domain":["*tyroo.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?upsellit\\.com","load-type":["third-party"],"unless-domain":["*usitechnologies.com","*upsellit.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?usitechnologies\\.com","load-type":["third-party"],"unless-domain":["*usitechnologies.com","*upsellit.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?unanimis\\.co\\.uk","load-type":["third-party"],"unless-domain":["*www.unanimis.co.uk","*unanimis.co.uk"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?udmserve\\.net","load-type":["third-party"],"unless-domain":["*underdogmedia.com","*udmserve.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?underdogmedia\\.com","load-type":["third-party"],"unless-domain":["*underdogmedia.com","*udmserve.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?undertone\\.com","load-type":["third-party"],"unless-domain":["*undertone.com","*undertonenetworks.com","*undertonevideo.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?undertonenetworks\\.com","load-type":["third-party"],"unless-domain":["*undertone.com","*undertonenetworks.com","*undertonevideo.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?undertonevideo\\.com","load-type":["third-party"],"unless-domain":["*undertone.com","*undertonenetworks.com","*undertonevideo.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?51network\\.com","load-type":["third-party"],"unless-domain":["*uniqlick.com","*51network.com","*wanmo.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?uniqlick\\.com","load-type":["third-party"],"unless-domain":["*uniqlick.com","*51network.com","*wanmo.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?wanmo\\.com","load-type":["third-party"],"unless-domain":["*uniqlick.com","*51network.com","*wanmo.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?unrulymedia\\.com","load-type":["third-party"],"unless-domain":["*unrulymedia.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?valuead\\.com","load-type":["third-party"],"unless-domain":["*valuead.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?adserver\\.com","load-type":["third-party"],"unless-domain":["*conversantmedia.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?dotomi\\.com","load-type":["third-party"],"unless-domain":["*conversantmedia.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?dtmpub\\.com","load-type":["third-party"],"unless-domain":["*conversantmedia.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?emjcd\\.com","load-type":["third-party"],"unless-domain":["*conversantmedia.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?fastclick\\.com","load-type":["third-party"],"unless-domain":["*conversantmedia.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?fastclick\\.net","load-type":["third-party"],"unless-domain":["*conversantmedia.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?greystripe\\.com","load-type":["third-party"],"unless-domain":["*conversantmedia.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?lduhtrp\\.net","load-type":["third-party"],"unless-domain":["*conversantmedia.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?mediaplex\\.com","load-type":["third-party"],"unless-domain":["*conversantmedia.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?valueclick\\.com","load-type":["third-party"],"unless-domain":["*conversantmedia.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?valueclick\\.net","load-type":["third-party"],"unless-domain":["*conversantmedia.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?valueclickmedia\\.com","load-type":["third-party"],"unless-domain":["*conversantmedia.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?amigos\\.com","load-type":["third-party"],"unless-domain":["*various.com","*amigos.com","*getiton.com","*medley.com","*nostringsattached.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?getiton\\.com","load-type":["third-party"],"unless-domain":["*various.com","*amigos.com","*getiton.com","*medley.com","*nostringsattached.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?medley\\.com","load-type":["third-party"],"unless-domain":["*various.com","*amigos.com","*getiton.com","*medley.com","*nostringsattached.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?nostringsattached\\.com","load-type":["third-party"],"unless-domain":["*various.com","*amigos.com","*getiton.com","*medley.com","*nostringsattached.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?various\\.com","load-type":["third-party"],"unless-domain":["*various.com","*amigos.com","*getiton.com","*medley.com","*nostringsattached.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?vcmedia\\.vn","load-type":["third-party"],"unless-domain":["*vcmedia.vn"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?ivdopia\\.com","load-type":["third-party"],"unless-domain":["*vdopia.com","*ivdopia.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?vdopia\\.com","load-type":["third-party"],"unless-domain":["*vdopia.com","*ivdopia.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?adsvelocity\\.com","load-type":["third-party"],"unless-domain":["*adsvelocity.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?mobclix\\.com","load-type":["third-party"],"unless-domain":["*velti.com","*mobclix.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?velti\\.com","load-type":["third-party"],"unless-domain":["*velti.com","*mobclix.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?vemba\\.com","load-type":["third-party"],"unless-domain":["*vemba.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?singlefeed\\.com","load-type":["third-party"],"unless-domain":["*vendio.com","*singlefeed.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?vendio\\.com","load-type":["third-party"],"unless-domain":["*vendio.com","*singlefeed.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?veoxa\\.com","load-type":["third-party"],"unless-domain":["*veoxa.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?veremedia\\.com","load-type":["third-party"],"unless-domain":["*veremedia.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?verticalresponse\\.com","load-type":["third-party"],"unless-domain":["*verticalresponse.com","*vresp.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?vresp\\.com","load-type":["third-party"],"unless-domain":["*verticalresponse.com","*vresp.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?intellitxt\\.com","load-type":["third-party"],"unless-domain":["*vibrantmedia.com","*intellitxt.com","*picadmedia.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?picadmedia\\.com","load-type":["third-party"],"unless-domain":["*vibrantmedia.com","*intellitxt.com","*picadmedia.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?vibrantmedia\\.com","load-type":["third-party"],"unless-domain":["*vibrantmedia.com","*intellitxt.com","*picadmedia.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?viglink\\.com","load-type":["third-party"],"unless-domain":["*viglink.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?viewablemedia\\.net","load-type":["third-party"],"unless-domain":["*visiblemeasures.com","*viewablemedia.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?visiblemeasures\\.com","load-type":["third-party"],"unless-domain":["*visiblemeasures.com","*viewablemedia.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?visbrands\\.com","load-type":["third-party"],"unless-domain":["*visbrands.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?vdna-assets\\.com","load-type":["third-party"],"unless-domain":["*visualdna.com","*vdna-assets.com","*visualdna-stats.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?visualdna-stats\\.com","load-type":["third-party"],"unless-domain":["*visualdna.com","*vdna-assets.com","*visualdna-stats.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?visualdna\\.com","load-type":["third-party"],"unless-domain":["*visualdna.com","*vdna-assets.com","*visualdna-stats.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?vizu\\.com","load-type":["third-party"],"unless-domain":["*vizu.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?vizury\\.com","load-type":["third-party"],"unless-domain":["*vizury.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?vserv\\.com","load-type":["third-party"],"unless-domain":["*vserv.com","*vserv.mobi"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?vserv\\.mobi","load-type":["third-party"],"unless-domain":["*vserv.com","*vserv.mobi"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?247realmedia\\.com","load-type":["third-party"],"unless-domain":["*wpp.com","*247realmedia.com","*accelerator-media.com","*acceleratorusa.com","*decdna.net","*decideinteractive.com","*gmads.net","*groupm.com","*kantarmedia.com","*mecglobal.com","*mindshare.nl","*mookie1.com","*pm14.com","*realmedia.com","*targ.ad","*themig.com","*xaxis.com","*compete.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?accelerator-media\\.com","load-type":["third-party"],"unless-domain":["*wpp.com","*247realmedia.com","*accelerator-media.com","*acceleratorusa.com","*decdna.net","*decideinteractive.com","*gmads.net","*groupm.com","*kantarmedia.com","*mecglobal.com","*mindshare.nl","*mookie1.com","*pm14.com","*realmedia.com","*targ.ad","*themig.com","*xaxis.com","*compete.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?acceleratorusa\\.com","load-type":["third-party"],"unless-domain":["*wpp.com","*247realmedia.com","*accelerator-media.com","*acceleratorusa.com","*decdna.net","*decideinteractive.com","*gmads.net","*groupm.com","*kantarmedia.com","*mecglobal.com","*mindshare.nl","*mookie1.com","*pm14.com","*realmedia.com","*targ.ad","*themig.com","*xaxis.com","*compete.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?decdna\\.net","load-type":["third-party"],"unless-domain":["*wpp.com","*247realmedia.com","*accelerator-media.com","*acceleratorusa.com","*decdna.net","*decideinteractive.com","*gmads.net","*groupm.com","*kantarmedia.com","*mecglobal.com","*mindshare.nl","*mookie1.com","*pm14.com","*realmedia.com","*targ.ad","*themig.com","*xaxis.com","*compete.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?decideinteractive\\.com","load-type":["third-party"],"unless-domain":["*wpp.com","*247realmedia.com","*accelerator-media.com","*acceleratorusa.com","*decdna.net","*decideinteractive.com","*gmads.net","*groupm.com","*kantarmedia.com","*mecglobal.com","*mindshare.nl","*mookie1.com","*pm14.com","*realmedia.com","*targ.ad","*themig.com","*xaxis.com","*compete.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?gmads\\.net","load-type":["third-party"],"unless-domain":["*wpp.com","*247realmedia.com","*accelerator-media.com","*acceleratorusa.com","*decdna.net","*decideinteractive.com","*gmads.net","*groupm.com","*kantarmedia.com","*mecglobal.com","*mindshare.nl","*mookie1.com","*pm14.com","*realmedia.com","*targ.ad","*themig.com","*xaxis.com","*compete.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?groupm\\.com","load-type":["third-party"],"unless-domain":["*wpp.com","*247realmedia.com","*accelerator-media.com","*acceleratorusa.com","*decdna.net","*decideinteractive.com","*gmads.net","*groupm.com","*kantarmedia.com","*mecglobal.com","*mindshare.nl","*mookie1.com","*pm14.com","*realmedia.com","*targ.ad","*themig.com","*xaxis.com","*compete.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?kantarmedia\\.com","load-type":["third-party"],"unless-domain":["*wpp.com","*247realmedia.com","*accelerator-media.com","*acceleratorusa.com","*decdna.net","*decideinteractive.com","*gmads.net","*groupm.com","*kantarmedia.com","*mecglobal.com","*mindshare.nl","*mookie1.com","*pm14.com","*realmedia.com","*targ.ad","*themig.com","*xaxis.com","*compete.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?mecglobal\\.com","load-type":["third-party"],"unless-domain":["*wpp.com","*247realmedia.com","*accelerator-media.com","*acceleratorusa.com","*decdna.net","*decideinteractive.com","*gmads.net","*groupm.com","*kantarmedia.com","*mecglobal.com","*mindshare.nl","*mookie1.com","*pm14.com","*realmedia.com","*targ.ad","*themig.com","*xaxis.com","*compete.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?mindshare\\.nl","load-type":["third-party"],"unless-domain":["*wpp.com","*247realmedia.com","*accelerator-media.com","*acceleratorusa.com","*decdna.net","*decideinteractive.com","*gmads.net","*groupm.com","*kantarmedia.com","*mecglobal.com","*mindshare.nl","*mookie1.com","*pm14.com","*realmedia.com","*targ.ad","*themig.com","*xaxis.com","*compete.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?mookie1\\.com","load-type":["third-party"],"unless-domain":["*wpp.com","*247realmedia.com","*accelerator-media.com","*acceleratorusa.com","*decdna.net","*decideinteractive.com","*gmads.net","*groupm.com","*kantarmedia.com","*mecglobal.com","*mindshare.nl","*mookie1.com","*pm14.com","*realmedia.com","*targ.ad","*themig.com","*xaxis.com","*compete.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?pm14\\.com","load-type":["third-party"],"unless-domain":["*wpp.com","*247realmedia.com","*accelerator-media.com","*acceleratorusa.com","*decdna.net","*decideinteractive.com","*gmads.net","*groupm.com","*kantarmedia.com","*mecglobal.com","*mindshare.nl","*mookie1.com","*pm14.com","*realmedia.com","*targ.ad","*themig.com","*xaxis.com","*compete.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?realmedia\\.com","load-type":["third-party"],"unless-domain":["*wpp.com","*247realmedia.com","*accelerator-media.com","*acceleratorusa.com","*decdna.net","*decideinteractive.com","*gmads.net","*groupm.com","*kantarmedia.com","*mecglobal.com","*mindshare.nl","*mookie1.com","*pm14.com","*realmedia.com","*targ.ad","*themig.com","*xaxis.com","*compete.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?targ\\.ad","load-type":["third-party"],"unless-domain":["*wpp.com","*247realmedia.com","*accelerator-media.com","*acceleratorusa.com","*decdna.net","*decideinteractive.com","*gmads.net","*groupm.com","*kantarmedia.com","*mecglobal.com","*mindshare.nl","*mookie1.com","*pm14.com","*realmedia.com","*targ.ad","*themig.com","*xaxis.com","*compete.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?themig\\.com","load-type":["third-party"],"unless-domain":["*wpp.com","*247realmedia.com","*accelerator-media.com","*acceleratorusa.com","*decdna.net","*decideinteractive.com","*gmads.net","*groupm.com","*kantarmedia.com","*mecglobal.com","*mindshare.nl","*mookie1.com","*pm14.com","*realmedia.com","*targ.ad","*themig.com","*xaxis.com","*compete.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?wpp\\.com","load-type":["third-party"],"unless-domain":["*wpp.com","*247realmedia.com","*accelerator-media.com","*acceleratorusa.com","*decdna.net","*decideinteractive.com","*gmads.net","*groupm.com","*kantarmedia.com","*mecglobal.com","*mindshare.nl","*mookie1.com","*pm14.com","*realmedia.com","*targ.ad","*themig.com","*xaxis.com","*compete.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?xaxis\\.com","load-type":["third-party"],"unless-domain":["*wpp.com","*247realmedia.com","*accelerator-media.com","*acceleratorusa.com","*decdna.net","*decideinteractive.com","*gmads.net","*groupm.com","*kantarmedia.com","*mecglobal.com","*mindshare.nl","*mookie1.com","*pm14.com","*realmedia.com","*targ.ad","*themig.com","*xaxis.com","*compete.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?contentwidgets\\.net","load-type":["third-party"],"unless-domain":["*wahoha.com","*contentwidgets.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?wahoha\\.com","load-type":["third-party"],"unless-domain":["*wahoha.com","*contentwidgets.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?feedperfect\\.com","load-type":["third-party"],"unless-domain":["*web.com","*feedperfect.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?web\\.com","load-type":["third-party"],"unless-domain":["*web.com","*feedperfect.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?webads\\.co\\.uk","load-type":["third-party"],"unless-domain":["*www.webads.co.uk","*webads.co.uk"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?webgozar\\.com","load-type":["third-party"],"unless-domain":["*webgozar.com","*webgozar.ir"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?webgozar\\.ir","load-type":["third-party"],"unless-domain":["*webgozar.com","*webgozar.ir"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?dsmmadvantage\\.com","load-type":["third-party"],"unless-domain":["*webmetro.com","*dsmmadvantage.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?webmetro\\.com","load-type":["third-party"],"unless-domain":["*webmetro.com","*dsmmadvantage.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?weborama\\.com","load-type":["third-party"],"unless-domain":["*weborama.com","*weborama.fr"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?weborama\\.fr","load-type":["third-party"],"unless-domain":["*weborama.com","*weborama.fr"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?webtraffic\\.no","load-type":["third-party"],"unless-domain":["*webtraffic.se","*webtraffic.no"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?webtraffic\\.se","load-type":["third-party"],"unless-domain":["*webtraffic.se","*webtraffic.no"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?wiredminds\\.com","load-type":["third-party"],"unless-domain":["*wiredminds.com","*wiredminds.de"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?wiredminds\\.de","load-type":["third-party"],"unless-domain":["*wiredminds.com","*wiredminds.de"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?adtotal\\.pl","load-type":["third-party"],"unless-domain":["*wp.pl","*adtotal.pl"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?wp\\.pl","load-type":["third-party"],"unless-domain":["*wp.pl","*adtotal.pl"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?wordstream\\.com","load-type":["third-party"],"unless-domain":["*wordstream.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?admanager-xertive\\.com","load-type":["third-party"],"unless-domain":["*xertivemedia.com","*admanager-xertive.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?xertivemedia\\.com","load-type":["third-party"],"unless-domain":["*xertivemedia.com","*admanager-xertive.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?adplan-ds\\.com","load-type":["third-party"],"unless-domain":["*adplan-ds.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?ydworld\\.com","load-type":["third-party"],"unless-domain":["*ydworld.com","*yieldivision.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?yieldivision\\.com","load-type":["third-party"],"unless-domain":["*ydworld.com","*yieldivision.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?yoc\\.com","load-type":["third-party"],"unless-domain":["*yoc.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?yoc-performance\\.com","load-type":["third-party"],"unless-domain":["*yoc.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?yabuka\\.com","load-type":["third-party"],"unless-domain":["*yabuka.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?adinterax\\.com","load-type":["third-party"],"unless-domain":["*flickr.com","*yuilibrary.com","*tumblr.com","*yahoo.com","*yahoostudios.com","*yahoo.co.jp"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?adrevolver\\.com","load-type":["third-party"],"unless-domain":["*flickr.com","*yuilibrary.com","*tumblr.com","*yahoo.com","*yahoostudios.com","*yahoo.co.jp"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?bluelithium\\.com","load-type":["third-party"],"unless-domain":["*flickr.com","*yuilibrary.com","*tumblr.com","*yahoo.com","*yahoostudios.com","*yahoo.co.jp"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?dapper\\.net","load-type":["third-party"],"unless-domain":["*flickr.com","*yuilibrary.com","*tumblr.com","*yahoo.com","*yahoostudios.com","*yahoo.co.jp"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?interclick\\.com","load-type":["third-party"],"unless-domain":["*flickr.com","*yuilibrary.com","*tumblr.com","*yahoo.com","*yahoostudios.com","*yahoo.co.jp"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?overture\\.com","load-type":["third-party"],"unless-domain":["*flickr.com","*yuilibrary.com","*tumblr.com","*yahoo.com","*yahoostudios.com","*yahoo.co.jp"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?rightmedia\\.com","load-type":["third-party"],"unless-domain":["*flickr.com","*yuilibrary.com","*tumblr.com","*yahoo.com","*yahoostudios.com","*yahoo.co.jp"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?rmxads\\.com","load-type":["third-party"],"unless-domain":["*flickr.com","*yuilibrary.com","*tumblr.com","*yahoo.com","*yahoostudios.com","*yahoo.co.jp"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?secure-adserver\\.com","load-type":["third-party"],"unless-domain":["*flickr.com","*yuilibrary.com","*tumblr.com","*yahoo.com","*yahoostudios.com","*yahoo.co.jp"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?adserver\\.yahoo\\.com","load-type":["third-party"],"unless-domain":["*flickr.com","*yuilibrary.com","*tumblr.com","*yahoo.com","*yahoostudios.com","*yahoo.co.jp"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?advertising\\.yahoo\\.com","load-type":["third-party"],"unless-domain":["*flickr.com","*yuilibrary.com","*tumblr.com","*yahoo.com","*yahoostudios.com","*yahoo.co.jp"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?marketingsolutions\\.yahoo\\.com","load-type":["third-party"],"unless-domain":["*flickr.com","*yuilibrary.com","*tumblr.com","*yahoo.com","*yahoostudios.com","*yahoo.co.jp"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?thewheelof\\.com","load-type":["third-party"],"unless-domain":["*flickr.com","*yuilibrary.com","*tumblr.com","*yahoo.com","*yahoostudios.com","*yahoo.co.jp"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?yieldmanager\\.com","load-type":["third-party"],"unless-domain":["*flickr.com","*yuilibrary.com","*tumblr.com","*yahoo.com","*yahoostudios.com","*yahoo.co.jp"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?yieldmanager\\.net","load-type":["third-party"],"unless-domain":["*flickr.com","*yuilibrary.com","*tumblr.com","*yahoo.com","*yahoostudios.com","*yahoo.co.jp"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?yldmgrimg\\.net","load-type":["third-party"],"unless-domain":["*flickr.com","*yuilibrary.com","*tumblr.com","*yahoo.com","*yahoostudios.com","*yahoo.co.jp"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?metrica\\.yandex\\.com\\.tr","load-type":["third-party"],"unless-domain":["*yandex.com","*web-visor.com","*moikrug.ru","*yandex.ru","*yandex.st","*yandex.ua","*yandex.com.tr","*yandex.by"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?metrika\\.yandex\\.kz","load-type":["third-party"],"unless-domain":["*yandex.com","*web-visor.com","*moikrug.ru","*yandex.ru","*yandex.st","*yandex.ua","*yandex.com.tr","*yandex.by"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?metrika\\.yandex\\.by","load-type":["third-party"],"unless-domain":["*yandex.com","*web-visor.com","*moikrug.ru","*yandex.ru","*yandex.st","*yandex.ua","*yandex.com.tr","*yandex.by"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?metrika\\.yandex\\.ua","load-type":["third-party"],"unless-domain":["*yandex.com","*web-visor.com","*moikrug.ru","*yandex.ru","*yandex.st","*yandex.ua","*yandex.com.tr","*yandex.by"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?metrika\\.yandex\\.ru","load-type":["third-party"],"unless-domain":["*yandex.com","*web-visor.com","*moikrug.ru","*yandex.ru","*yandex.st","*yandex.ua","*yandex.com.tr","*yandex.by"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?metrica\\.yandex\\.com","load-type":["third-party"],"unless-domain":["*yandex.com","*web-visor.com","*moikrug.ru","*yandex.ru","*yandex.st","*yandex.ua","*yandex.com.tr","*yandex.by"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?advertising\\.yandex\\.ru","load-type":["third-party"],"unless-domain":["*yandex.com","*web-visor.com","*moikrug.ru","*yandex.ru","*yandex.st","*yandex.ua","*yandex.com.tr","*yandex.by"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?advertising\\.yandex\\.com","load-type":["third-party"],"unless-domain":["*yandex.com","*web-visor.com","*moikrug.ru","*yandex.ru","*yandex.st","*yandex.ua","*yandex.com.tr","*yandex.by"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?direct\\.yandex\\.ru","load-type":["third-party"],"unless-domain":["*yandex.com","*web-visor.com","*moikrug.ru","*yandex.ru","*yandex.st","*yandex.ua","*yandex.com.tr","*yandex.by"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?direct\\.yandex\\.com","load-type":["third-party"],"unless-domain":["*yandex.com","*web-visor.com","*moikrug.ru","*yandex.ru","*yandex.st","*yandex.ua","*yandex.com.tr","*yandex.by"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?appmetrika\\.yandex\\.ru","load-type":["third-party"],"unless-domain":["*yandex.com","*web-visor.com","*moikrug.ru","*yandex.ru","*yandex.st","*yandex.ua","*yandex.com.tr","*yandex.by"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?appmetrica\\.yandex\\.com","load-type":["third-party"],"unless-domain":["*yandex.com","*web-visor.com","*moikrug.ru","*yandex.ru","*yandex.st","*yandex.ua","*yandex.com.tr","*yandex.by"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?appmetrica\\.yandex\\.com\\.tr","load-type":["third-party"],"unless-domain":["*yandex.com","*web-visor.com","*moikrug.ru","*yandex.ru","*yandex.st","*yandex.ua","*yandex.com.tr","*yandex.by"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?api-metrika\\.yandex\\.com","load-type":["third-party"],"unless-domain":["*yandex.com","*web-visor.com","*moikrug.ru","*yandex.ru","*yandex.st","*yandex.ua","*yandex.com.tr","*yandex.by"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?api-metrika\\.yandex\\.ru","load-type":["third-party"],"unless-domain":["*yandex.com","*web-visor.com","*moikrug.ru","*yandex.ru","*yandex.st","*yandex.ua","*yandex.com.tr","*yandex.by"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?addynamix\\.com","load-type":["third-party"],"unless-domain":["*ybrantdigital.com","*addynamix.com","*adserverplus.com","*oridian.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?adserverplus\\.com","load-type":["third-party"],"unless-domain":["*ybrantdigital.com","*addynamix.com","*adserverplus.com","*oridian.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?oridian\\.com","load-type":["third-party"],"unless-domain":["*ybrantdigital.com","*addynamix.com","*adserverplus.com","*oridian.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?ybrantdigital\\.com","load-type":["third-party"],"unless-domain":["*ybrantdigital.com","*addynamix.com","*adserverplus.com","*oridian.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?attracto\\.com","load-type":["third-party"],"unless-domain":["*yhmg.com","*attracto.com","*clickhype.com","*yellowhammermg.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?clickhype\\.com","load-type":["third-party"],"unless-domain":["*yhmg.com","*attracto.com","*clickhype.com","*yellowhammermg.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?yellowhammermg\\.com","load-type":["third-party"],"unless-domain":["*yhmg.com","*attracto.com","*clickhype.com","*yellowhammermg.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?yhmg\\.com","load-type":["third-party"],"unless-domain":["*yhmg.com","*attracto.com","*clickhype.com","*yellowhammermg.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?yesads\\.com","load-type":["third-party"],"unless-domain":["*yesads.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?yieldads\\.com","load-type":["third-party"],"unless-domain":["*yieldads.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?yieldbuild\\.com","load-type":["third-party"],"unless-domain":["*yieldbuild.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?yieldlab\\.de","load-type":["third-party"],"unless-domain":["*yieldlab.de","*yieldlab.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?yieldlab\\.net","load-type":["third-party"],"unless-domain":["*yieldlab.de","*yieldlab.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?yoggrt\\.com","load-type":["third-party"],"unless-domain":["*yoggrt.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?yume\\.com","load-type":["third-party"],"unless-domain":["*yume.com","*yumenetworks.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?yumenetworks\\.com","load-type":["third-party"],"unless-domain":["*yume.com","*yumenetworks.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?zedo\\.com","load-type":["third-party"],"unless-domain":["*zedo.com","*zincx.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?zincx\\.com","load-type":["third-party"],"unless-domain":["*zedo.com","*zincx.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?metricsdirect\\.com","load-type":["third-party"],"unless-domain":["*zango.com","*metricsdirect.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?zango\\.com","load-type":["third-party"],"unless-domain":["*zango.com","*metricsdirect.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?zemanta\\.com","load-type":["third-party"],"unless-domain":["*zemanta.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?zestad\\.com","load-type":["third-party"],"unless-domain":["*zestad.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?insightgrit\\.com","load-type":["third-party"],"unless-domain":["*zetaemailsolutions.com","*insightgrit.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?zetaemailsolutions\\.com","load-type":["third-party"],"unless-domain":["*zetaemailsolutions.com","*insightgrit.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?zumobi\\.com","load-type":["third-party"],"unless-domain":["*zumobi.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?ru4\\.com","load-type":["third-party"],"unless-domain":["*xplusone.com","*ru4.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?xplusone\\.com","load-type":["third-party"],"unless-domain":["*xplusone.com","*ru4.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?adpepper\\.com","load-type":["third-party"],"unless-domain":["*adpepper.us","*adpepper.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?adpepper\\.us","load-type":["third-party"],"unless-domain":["*adpepper.us","*adpepper.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?ad6media\\.fr","load-type":["third-party"],"unless-domain":["*ad6media.fr"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?adbrite\\.com","load-type":["third-party"],"unless-domain":["*adbrite.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?admarketplace\\.com","load-type":["third-party"],"unless-domain":["*admarketplace.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?adprs\\.net","load-type":["third-party"],"unless-domain":["*adprecision.net","*adprs.net","*aprecision.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?aprecision\\.net","load-type":["third-party"],"unless-domain":["*adprecision.net","*adprs.net","*aprecision.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?addgloo\\.com","load-type":["third-party"],"unless-domain":["*addgloo.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?adhood\\.com","load-type":["third-party"],"unless-domain":["*adhood.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?adnologies\\.com","load-type":["third-party"],"unless-domain":["*adnologies.com","*heias.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?heias\\.com","load-type":["third-party"],"unless-domain":["*adnologies.com","*heias.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?adrolays\\.com","load-type":["third-party"],"unless-domain":["*adrolays.com","*adrolays.de"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?adrolays\\.de","load-type":["third-party"],"unless-domain":["*adrolays.com","*adrolays.de"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?adscale\\.de","load-type":["third-party"],"unless-domain":["*adscale.de"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?adyard\\.de","load-type":["third-party"],"unless-domain":["*adyard.de"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?adzly\\.com","load-type":["third-party"],"unless-domain":["*adzly.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?affili\\.net","load-type":["third-party"],"unless-domain":["*affili.net","*affilinet-inside.de","*banner-rotation.com","*successfultogether.co.uk"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?affilinet-inside\\.de","load-type":["third-party"],"unless-domain":["*affili.net","*affilinet-inside.de","*banner-rotation.com","*successfultogether.co.uk"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?banner-rotation\\.com","load-type":["third-party"],"unless-domain":["*affili.net","*affilinet-inside.de","*banner-rotation.com","*successfultogether.co.uk"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?successfultogether\\.co\\.uk","load-type":["third-party"],"unless-domain":["*affili.net","*affilinet-inside.de","*banner-rotation.com","*successfultogether.co.uk"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?appssavvy\\.com","load-type":["third-party"],"unless-domain":["*appssavvy.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?beencounter\\.com","load-type":["third-party"],"unless-domain":["*beencounter.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?adbutler\\.de","load-type":["third-party"],"unless-domain":["*belboon.com","*adbutler.de"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?belboon\\.com","load-type":["third-party"],"unless-domain":["*belboon.com","*adbutler.de"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?bigmir\\.net","load-type":["third-party"],"unless-domain":["*bigmir.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?cxense\\.com","load-type":["third-party"],"unless-domain":["*cxense.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?adxpose\\.com","load-type":["third-party"],"unless-domain":["*comscore.com","*adxpose.com","*certifica.com","*scorecardresearch.com","*sitestat.com","*voicefive.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?dianomi\\.com","load-type":["third-party"],"unless-domain":["*dianomi.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?ebay\\.com","load-type":["third-party"],"unless-domain":["*ebay.com","*ebay.at","*ebay.ba","*ebay.be","*ebay.com.au","*ebay.ca","*ebay.ch","*ebay.cn","*ebay.de","*ebay.es","*ebay.fr","*ebay.com.hk","*ebay.ie","*ebay.in","*ebay.it","*ebay.co.jp","*ebay.co.kr","*ebay.com.my","*ebay.nl","*ebay.com.ph","*ebay.pl","*ebay.com.sg","*ebay.com.tw","*ebay.co.uk","*gopjn.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?gopjn\\.com","load-type":["third-party"],"unless-domain":["*ebay.com","*ebay.at","*ebay.ba","*ebay.be","*ebay.com.au","*ebay.ca","*ebay.ch","*ebay.cn","*ebay.de","*ebay.es","*ebay.fr","*ebay.com.hk","*ebay.ie","*ebay.in","*ebay.it","*ebay.co.jp","*ebay.co.kr","*ebay.com.my","*ebay.nl","*ebay.com.ph","*ebay.pl","*ebay.com.sg","*ebay.com.tw","*ebay.co.uk","*gopjn.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?etrigue\\.com","load-type":["third-party"],"unless-domain":["*etrigue.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?ewaydirect\\.com","load-type":["third-party"],"unless-domain":["*ewaydirect.com","*ixs1.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?ixs1\\.net","load-type":["third-party"],"unless-domain":["*ewaydirect.com","*ixs1.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?exelate\\.com","load-type":["third-party"],"unless-domain":["*exelate.com","*exelator.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?exelator\\.com","load-type":["third-party"],"unless-domain":["*exelate.com","*exelator.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?e-kolay\\.net","load-type":["third-party"],"unless-domain":["*ekolay.net","*e-kolay.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?ekolay\\.net","load-type":["third-party"],"unless-domain":["*ekolay.net","*e-kolay.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?bnmla\\.com","load-type":["third-party"],"unless-domain":["*engagebdr.com","*bnmla.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?engagebdr\\.com","load-type":["third-party"],"unless-domain":["*engagebdr.com","*bnmla.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?777seo\\.com","load-type":["third-party"],"unless-domain":["*ewebse.com","*777seo.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?ewebse\\.com","load-type":["third-party"],"unless-domain":["*ewebse.com","*777seo.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?excitad\\.com","load-type":["third-party"],"unless-domain":["*excitad.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?expo-max\\.com","load-type":["third-party"],"unless-domain":["*expo-max.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?eyereturn\\.com","load-type":["third-party"],"unless-domain":["*eyereturnmarketing.com","*eyereturn.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?eyereturnmarketing\\.com","load-type":["third-party"],"unless-domain":["*eyereturnmarketing.com","*eyereturn.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?faithadnet\\.com","load-type":["third-party"],"unless-domain":["*faithadnet.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?600z\\.com","load-type":["third-party"],"unless-domain":["*ientry.com","*600z.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?ientry\\.com","load-type":["third-party"],"unless-domain":["*ientry.com","*600z.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?centraliprom\\.com","load-type":["third-party"],"unless-domain":["*iprom.si","*centraliprom.com","*iprom.net","*mediaiprom.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?iprom\\.net","load-type":["third-party"],"unless-domain":["*iprom.si","*centraliprom.com","*iprom.net","*mediaiprom.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?iprom\\.si","load-type":["third-party"],"unless-domain":["*iprom.si","*centraliprom.com","*iprom.net","*mediaiprom.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?mediaiprom\\.com","load-type":["third-party"],"unless-domain":["*iprom.si","*centraliprom.com","*iprom.net","*mediaiprom.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?ipromote\\.com","load-type":["third-party"],"unless-domain":["*ipromote.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?iprospect\\.com","load-type":["third-party"],"unless-domain":["*iprospect.com","*clickmanage.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?clickmanage\\.com","load-type":["third-party"],"unless-domain":["*iprospect.com","*clickmanage.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?inner-active\\.com","load-type":["third-party"],"unless-domain":["*inner-active.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?adsbyisocket\\.com","load-type":["third-party"],"unless-domain":["*isocket.com","*adsbyisocket.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?isocket\\.com","load-type":["third-party"],"unless-domain":["*isocket.com","*adsbyisocket.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?m6d\\.com","load-type":["third-party"],"unless-domain":["*m6d.com","*media6degrees.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?media6degrees\\.com","load-type":["third-party"],"unless-domain":["*m6d.com","*media6degrees.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?madvertise\\.com","load-type":["third-party"],"unless-domain":["*madvertise.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?mashero\\.com","load-type":["third-party"],"unless-domain":["*mashero.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?media\\.net","load-type":["third-party"],"unless-domain":["*media.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?mediaforge\\.com","load-type":["third-party"],"unless-domain":["*mediaforge.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?mythings\\.com","load-type":["third-party"],"unless-domain":["*mythings.com","*mythingsmedia.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?mythingsmedia\\.com","load-type":["third-party"],"unless-domain":["*mythings.com","*mythingsmedia.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?newtention\\.de","load-type":["third-party"],"unless-domain":["*newtention.de","*newtention.net","*newtentionassets.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?newtention\\.net","load-type":["third-party"],"unless-domain":["*newtention.de","*newtention.net","*newtentionassets.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?newtentionassets\\.net","load-type":["third-party"],"unless-domain":["*newtention.de","*newtention.net","*newtentionassets.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?nrelate\\.com","load-type":["third-party"],"unless-domain":["*nrelate.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?nugg\\.ad","load-type":["third-party"],"unless-domain":["*nugg.ad","*nuggad.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?nuggad\\.net","load-type":["third-party"],"unless-domain":["*nugg.ad","*nuggad.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?onad\\.eu","load-type":["third-party"],"unless-domain":["*onad.eu"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?plista\\.com","load-type":["third-party"],"unless-domain":["*plista.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?quadrantone\\.com","load-type":["third-party"],"unless-domain":["*quadrantone.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?sociomantic\\.com","load-type":["third-party"],"unless-domain":["*sociomantic.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?sophus3\\.co\\.uk","load-type":["third-party"],"unless-domain":["*sophus3.com","*sophus3.co.uk"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?sophus3\\.com","load-type":["third-party"],"unless-domain":["*sophus3.com","*sophus3.co.uk"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?twyn\\.com","load-type":["third-party"],"unless-domain":["*twyn.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?ucoz\\.ae","load-type":["third-party"],"unless-domain":["*ucoz.com","*ucoz.ae","*ucoz.fr","*ucoz.net","*ucoz.ru"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?ucoz\\.br","load-type":["third-party"],"unless-domain":["*ucoz.com","*ucoz.ae","*ucoz.fr","*ucoz.net","*ucoz.ru"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?ucoz\\.com","load-type":["third-party"],"unless-domain":["*ucoz.com","*ucoz.ae","*ucoz.fr","*ucoz.net","*ucoz.ru"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?ucoz\\.du","load-type":["third-party"],"unless-domain":["*ucoz.com","*ucoz.ae","*ucoz.fr","*ucoz.net","*ucoz.ru"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?ucoz\\.fr","load-type":["third-party"],"unless-domain":["*ucoz.com","*ucoz.ae","*ucoz.fr","*ucoz.net","*ucoz.ru"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?ucoz\\.net","load-type":["third-party"],"unless-domain":["*ucoz.com","*ucoz.ae","*ucoz.fr","*ucoz.net","*ucoz.ru"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?ucoz\\.ru","load-type":["third-party"],"unless-domain":["*ucoz.com","*ucoz.ae","*ucoz.fr","*ucoz.net","*ucoz.ru"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?up-value\\.de","load-type":["third-party"],"unless-domain":["*up-value.de"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?xad\\.com","load-type":["third-party"],"unless-domain":["*xad.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?xplosion\\.de","load-type":["third-party"],"unless-domain":["*xplosion.de"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?youknowbest\\.com","load-type":["third-party"],"unless-domain":["*youknowbest.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?buy\\.at","load-type":["third-party"],"unless-domain":["*zanox.com","*buy.at","*zanox-affiliate.de"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?zanox-affiliate\\.de","load-type":["third-party"],"unless-domain":["*zanox.com","*buy.at","*zanox-affiliate.de"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?zanox\\.com","load-type":["third-party"],"unless-domain":["*zanox.com","*buy.at","*zanox-affiliate.de"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?zaparena\\.com","load-type":["third-party"],"unless-domain":["*zapunited.com","*zaparena.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?zapunited\\.com","load-type":["third-party"],"unless-domain":["*zapunited.com","*zaparena.com"]}}] \ No newline at end of file diff --git a/mobile/ios/Client/Frontend/ContentBlocker/disconnect-analytics.json b/mobile/ios/Client/Frontend/ContentBlocker/disconnect-analytics.json new file mode 100644 index 0000000000..c8b365397a --- /dev/null +++ b/mobile/ios/Client/Frontend/ContentBlocker/disconnect-analytics.json @@ -0,0 +1 @@ +[{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?63squares\\.com","load-type":["third-party"],"unless-domain":["*63squares.com","*i-stats.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?i-stats\\.com","load-type":["third-party"],"unless-domain":["*63squares.com","*i-stats.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?atinternet\\.com","load-type":["third-party"],"unless-domain":["*atinternet.com","*hit-parade.com","*xiti.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?xiti\\.com","load-type":["third-party"],"unless-domain":["*atinternet.com","*hit-parade.com","*xiti.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?acxiom\\.com","load-type":["third-party"],"unless-domain":["*acxiom.com","*mm7.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?mm7\\.net","load-type":["third-party"],"unless-domain":["*acxiom.com","*mm7.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?3dstats\\.com","load-type":["third-party"],"unless-domain":["*addfreestats.com","*3dstats.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?addfreestats\\.com","load-type":["third-party"],"unless-domain":["*addfreestats.com","*3dstats.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?amadesa\\.com","load-type":["third-party"],"unless-domain":["*amadesa.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?amazingcounters\\.com","load-type":["third-party"],"unless-domain":["*amazingcounters.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?alexa\\.com","load-type":["third-party"],"unless-domain":["*amazon.com","*amazon-adsystem.com","*amazon.ca","*amazon.co.jp","*amazon.co.uk","*amazon.de","*amazon.es","*amazon.fr","*amazon.it","*assoc-amazon.com","*alexa.com","*alexametrics.com","*cloudfront.net","*amazonaws.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?alexametrics\\.com","load-type":["third-party"],"unless-domain":["*amazon.com","*amazon-adsystem.com","*amazon.ca","*amazon.co.jp","*amazon.co.uk","*amazon.de","*amazon.es","*amazon.fr","*amazon.it","*assoc-amazon.com","*alexa.com","*alexametrics.com","*cloudfront.net","*amazonaws.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?attracta\\.com","load-type":["third-party"],"unless-domain":["*attracta.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?polldaddy\\.com","load-type":["third-party"],"unless-domain":["*automattic.com","*polldaddy.com","*gravatar.com","*intensedebate.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?awio\\.com","load-type":["third-party"],"unless-domain":["*awio.com","*w3counter.com","*w3roi.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?w3counter\\.com","load-type":["third-party"],"unless-domain":["*awio.com","*w3counter.com","*w3roi.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?w3roi\\.com","load-type":["third-party"],"unless-domain":["*awio.com","*w3counter.com","*w3roi.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?belstat\\.be","load-type":["third-party"],"unless-domain":["*belstat.com","*belstat.be","*belstat.de","*belstat.fr","*belstat.nl"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?belstat\\.com","load-type":["third-party"],"unless-domain":["*belstat.com","*belstat.be","*belstat.de","*belstat.fr","*belstat.nl"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?belstat\\.de","load-type":["third-party"],"unless-domain":["*belstat.com","*belstat.be","*belstat.de","*belstat.fr","*belstat.nl"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?belstat\\.fr","load-type":["third-party"],"unless-domain":["*belstat.com","*belstat.be","*belstat.de","*belstat.fr","*belstat.nl"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?belstat\\.nl","load-type":["third-party"],"unless-domain":["*belstat.com","*belstat.be","*belstat.de","*belstat.fr","*belstat.nl"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?blogcounter\\.de","load-type":["third-party"],"unless-domain":["*blogcounter.de"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?bluemetrix\\.com","load-type":["third-party"],"unless-domain":["*bluemetrix.com","*bmmetrix.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?bmmetrix\\.com","load-type":["third-party"],"unless-domain":["*bluemetrix.com","*bmmetrix.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?branica\\.com","load-type":["third-party"],"unless-domain":["*branica.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?brightedge\\.com","load-type":["third-party"],"unless-domain":["*brightedge.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?bubblestat\\.com","load-type":["third-party"],"unless-domain":["*bubblestat.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?attributionmodel\\.com","load-type":["third-party"],"unless-domain":["*c3metrics.com","*attributionmodel.com","*c3tag.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?c3metrics\\.com","load-type":["third-party"],"unless-domain":["*c3metrics.com","*attributionmodel.com","*c3tag.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?c3tag\\.com","load-type":["third-party"],"unless-domain":["*c3metrics.com","*attributionmodel.com","*c3tag.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?cnzz\\.com","load-type":["third-party"],"unless-domain":["*cnzz.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?chartbeat\\.com","load-type":["third-party"],"unless-domain":["*chartbeat.com","*chartbeat.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?chartbeat\\.net","load-type":["third-party"],"unless-domain":["*chartbeat.com","*chartbeat.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?clicktale\\.com","load-type":["third-party"],"unless-domain":["*clicktale.com","*clicktale.net","*pantherssl.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?clicktale\\.net","load-type":["third-party"],"unless-domain":["*clicktale.com","*clicktale.net","*pantherssl.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?pantherssl\\.com","load-type":["third-party"],"unless-domain":["*clicktale.com","*clicktale.net","*pantherssl.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?clickdensity\\.com","load-type":["third-party"],"unless-domain":["*clickdensity.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?clixmetrix\\.com","load-type":["third-party"],"unless-domain":["*clixmetrix.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?clixpy\\.com","load-type":["third-party"],"unless-domain":["*clixpy.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?clustrmaps\\.com","load-type":["third-party"],"unless-domain":["*clustrmaps.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?axf8\\.net","load-type":["third-party"],"unless-domain":["*compuware.com","*axf8.net","*gomez.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?compuware\\.com","load-type":["third-party"],"unless-domain":["*compuware.com","*axf8.net","*gomez.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?gomez\\.com","load-type":["third-party"],"unless-domain":["*compuware.com","*axf8.net","*gomez.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?connexity\\.com","load-type":["third-party"],"unless-domain":["*connexity.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?zmedia\\.com","load-type":["third-party"],"unless-domain":["*conversantmedia.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?conversantmedia\\.com","load-type":["third-party"],"unless-domain":["*conversantmedia.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?convert\\.com","load-type":["third-party"],"unless-domain":["*convert.com","*reedge.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?reedge\\.com","load-type":["third-party"],"unless-domain":["*convert.com","*reedge.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?convertro\\.com","load-type":["third-party"],"unless-domain":["*convertro.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?cetrk\\.com","load-type":["third-party"],"unless-domain":["*crazyegg.com","*cetrk.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?crazyegg\\.com","load-type":["third-party"],"unless-domain":["*crazyegg.com","*cetrk.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?crowdscience\\.com","load-type":["third-party"],"unless-domain":["*crowdscience.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?cya2\\.net","load-type":["third-party"],"unless-domain":["*cya2.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?collserve\\.com","load-type":["third-party"],"unless-domain":["*dataium.com","*collserve.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?dataium\\.com","load-type":["third-party"],"unless-domain":["*dataium.com","*collserve.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?demandbase\\.com","load-type":["third-party"],"unless-domain":["*demandbase.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?ipcounter\\.de","load-type":["third-party"],"unless-domain":["*directcorp.de","*ipcounter.de"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?trackersimulator\\.org","load-type":["third-party"],"unless-domain":["*eff.org"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?eviltracker\\.net","load-type":["third-party"],"unless-domain":["*eff.org"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?do-not-tracker\\.org","load-type":["third-party"],"unless-domain":["*eff.org"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?eloqua\\.com","load-type":["third-party"],"unless-domain":["*eloqua.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?encoremetrics\\.com","load-type":["third-party"],"unless-domain":["*encoremetrics.com","*sitecompass.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?sitecompass\\.com","load-type":["third-party"],"unless-domain":["*encoremetrics.com","*sitecompass.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?eulerian\\.com","load-type":["third-party"],"unless-domain":["*eulerian.com","*eulerian.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?eulerian\\.net","load-type":["third-party"],"unless-domain":["*eulerian.com","*eulerian.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?feedjit\\.com","load-type":["third-party"],"unless-domain":["*feedjit.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?footprintlive\\.com","load-type":["third-party"],"unless-domain":["*footprintlive.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?freeonlineusers\\.com","load-type":["third-party"],"unless-domain":["*freeonlineusers.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?free-pagerank\\.com","load-type":["third-party"],"unless-domain":["*free-pagerank.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?gtop\\.ro","load-type":["third-party"],"unless-domain":["*gtop.ro","*gtopstats.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?gtopstats\\.com","load-type":["third-party"],"unless-domain":["*gtop.ro","*gtopstats.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?daphnecm\\.com","load-type":["third-party"],"unless-domain":["*gfk.com","*daphnecm.com","*gfkdaphne.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?gfk\\.com","load-type":["third-party"],"unless-domain":["*gfk.com","*daphnecm.com","*gfkdaphne.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?gfkdaphne\\.com","load-type":["third-party"],"unless-domain":["*gfk.com","*daphnecm.com","*gfkdaphne.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?gaug\\.es","load-type":["third-party"],"unless-domain":["*github.com","*gaug.es"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?godaddy\\.com","load-type":["third-party"],"unless-domain":["*godaddy.com","*trafficfacts.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?trafficfacts\\.com","load-type":["third-party"],"unless-domain":["*godaddy.com","*trafficfacts.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?gosquared\\.com","load-type":["third-party"],"unless-domain":["*gosquared.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?gostats\\.com","load-type":["third-party"],"unless-domain":["*gostats.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?google-analytics\\.com","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?postrank\\.com","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?raasnet\\.com","load-type":["third-party"],"unless-domain":["*hearst.com","*ic-live.com","*iclive.com","*icrossing.com","*sptag.com","*sptag1.com","*sptag2.com","*sptag3.com","*raasnet.com","*redaril.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?redaril\\.com","load-type":["third-party"],"unless-domain":["*hearst.com","*ic-live.com","*iclive.com","*icrossing.com","*sptag.com","*sptag1.com","*sptag2.com","*sptag3.com","*raasnet.com","*redaril.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?histats\\.com","load-type":["third-party"],"unless-domain":["*histats.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?hitsniffer\\.com","load-type":["third-party"],"unless-domain":["*hitsniffer.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?hitslink\\.com","load-type":["third-party"],"unless-domain":["*hitslink.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?cmcore\\.com","load-type":["third-party"],"unless-domain":["*ibm.com","*unica.com","*cmcore.com","*coremetrics.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?coremetrics\\.com","load-type":["third-party"],"unless-domain":["*ibm.com","*unica.com","*cmcore.com","*coremetrics.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?ibm\\.com","load-type":["third-party"],"unless-domain":["*ibm.com","*unica.com","*cmcore.com","*coremetrics.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?infonline\\.de","load-type":["third-party"],"unless-domain":["*infonline.de","*ioam.de","*ivwbox.de"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?ioam\\.de","load-type":["third-party"],"unless-domain":["*infonline.de","*ioam.de","*ivwbox.de"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?ivwbox\\.de","load-type":["third-party"],"unless-domain":["*infonline.de","*ioam.de","*ivwbox.de"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?enquisite\\.com","load-type":["third-party"],"unless-domain":["*inboundwriter.com","*enquisite.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?inboundwriter\\.com","load-type":["third-party"],"unless-domain":["*inboundwriter.com","*enquisite.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?hotlog\\.ru","load-type":["third-party"],"unless-domain":["*infostars.ru","*hotlog.ru"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?infostars\\.ru","load-type":["third-party"],"unless-domain":["*infostars.ru","*hotlog.ru"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?inspectlet\\.com","load-type":["third-party"],"unless-domain":["*inspectlet.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?domodomain\\.com","load-type":["third-party"],"unless-domain":["*intelligencefocus.com","*domodomain.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?intelligencefocus\\.com","load-type":["third-party"],"unless-domain":["*intelligencefocus.com","*domodomain.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?intercom\\.io","load-type":["third-party"],"unless-domain":["*intercom.io"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?kissmetrics\\.com","load-type":["third-party"],"unless-domain":["*kissmetrics.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?keymetric\\.net","load-type":["third-party"],"unless-domain":["*keymetric.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?src\\.kitcode\\.net","load-type":["third-party"],"unless-domain":["*src.kitcode.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?linezing\\.com","load-type":["third-party"],"unless-domain":["*linezing.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?liveperson\\.com","load-type":["third-party"],"unless-domain":["*liveperson.com","*nuconomy.com","*liveperson.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?nuconomy\\.com","load-type":["third-party"],"unless-domain":["*liveperson.com","*nuconomy.com","*liveperson.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?logdy\\.com","load-type":["third-party"],"unless-domain":["*logdy.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?crwdcntrl\\.net","load-type":["third-party"],"unless-domain":["*lotame.com","*crwdcntrl.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?lotame\\.com","load-type":["third-party"],"unless-domain":["*lotame.com","*crwdcntrl.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?lynchpin\\.com","load-type":["third-party"],"unless-domain":["*lynchpin.com","*lypn.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?lypn\\.com","load-type":["third-party"],"unless-domain":["*lynchpin.com","*lypn.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?clicktracks\\.com","load-type":["third-party"],"unless-domain":["*lyris.com","*clicktracks.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?lyris\\.com","load-type":["third-party"],"unless-domain":["*lyris.com","*clicktracks.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?lytiks\\.com","load-type":["third-party"],"unless-domain":["*lytiks.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?marktest\\.com","load-type":["third-party"],"unless-domain":["*marktest.com","*marktest.pt"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?marktest\\.pt","load-type":["third-party"],"unless-domain":["*marktest.com","*marktest.pt"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?maxymiser\\.com","load-type":["third-party"],"unless-domain":["*maxymiser.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?meetrics\\.de","load-type":["third-party"],"unless-domain":["*meetrics.de","*meetrics.net","*de.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?meetrics\\.net","load-type":["third-party"],"unless-domain":["*meetrics.de","*meetrics.net","*de.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?research\\.de\\.com","load-type":["third-party"],"unless-domain":["*meetrics.de","*meetrics.net","*de.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?crm-metrix\\.com","load-type":["third-party"],"unless-domain":["*metrixlab.com","*crm-metrix.com","*customerconversio.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?customerconversio\\.com","load-type":["third-party"],"unless-domain":["*metrixlab.com","*crm-metrix.com","*customerconversio.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?metrixlab\\.com","load-type":["third-party"],"unless-domain":["*metrixlab.com","*crm-metrix.com","*customerconversio.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?mixpanel\\.com","load-type":["third-party"],"unless-domain":["*mixpanel.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?mongoosemetrics\\.com","load-type":["third-party"],"unless-domain":["*mongoosemetrics.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?monitus\\.net","load-type":["third-party"],"unless-domain":["*monitus.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?mouseflow\\.com","load-type":["third-party"],"unless-domain":["*mouseflow.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?mypagerank\\.net","load-type":["third-party"],"unless-domain":["*mypagerank.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?estat\\.com","load-type":["third-party"],"unless-domain":["*mediametrie-estat.com","*estat.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?mediametrie-estat\\.com","load-type":["third-party"],"unless-domain":["*mediametrie-estat.com","*estat.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?hitsprocessor\\.com","load-type":["third-party"],"unless-domain":["*netapplications.com","*hitsprocessor.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?netapplications\\.com","load-type":["third-party"],"unless-domain":["*netapplications.com","*hitsprocessor.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?newrelic\\.com","load-type":["third-party"],"unless-domain":["*newrelic.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?nr-data\\.net","load-type":["third-party"],"unless-domain":["*newrelic.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?apnewsregistry\\.com","load-type":["third-party"],"unless-domain":["*newsright.com","*apnewsregistry.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?nextstat\\.com","load-type":["third-party"],"unless-domain":["*nextstat.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?glanceguide\\.com","load-type":["third-party"],"unless-domain":["*nielsen.com","*imrworldwide.com","*imrworldwide.net","*glanceguide.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?nielsen\\.com","load-type":["third-party"],"unless-domain":["*nielsen.com","*imrworldwide.com","*imrworldwide.net","*glanceguide.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?observerapp\\.com","load-type":["third-party"],"unless-domain":["*observerapp.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?onestat\\.com","load-type":["third-party"],"unless-domain":["*onestat.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?openstat\\.ru","load-type":["third-party"],"unless-domain":["*openstat.ru","*spylog.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?spylog\\.com","load-type":["third-party"],"unless-domain":["*openstat.ru","*spylog.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?opentracker\\.net","load-type":["third-party"],"unless-domain":["*opentracker.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?persianstat\\.com","load-type":["third-party"],"unless-domain":["*persianstat.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?phonalytics\\.com","load-type":["third-party"],"unless-domain":["*phonalytics.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?piwik\\.org","load-type":["third-party"],"unless-domain":["*piwik.org"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?pronunciator\\.com","load-type":["third-party"],"unless-domain":["*pronunciator.com","*visitorville.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?visitorville\\.com","load-type":["third-party"],"unless-domain":["*pronunciator.com","*visitorville.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?kissinsights\\.com","load-type":["third-party"],"unless-domain":["*qualaroo.com","*kissinsights.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?qualaroo\\.com","load-type":["third-party"],"unless-domain":["*qualaroo.com","*kissinsights.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?thecounter\\.com","load-type":["third-party"],"unless-domain":["*quinstreet.com","*thecounter.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?quintelligence\\.com","load-type":["third-party"],"unless-domain":["*quintelligence.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?radarurl\\.com","load-type":["third-party"],"unless-domain":["*radarurl.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?researchnow\\.com","load-type":["third-party"],"unless-domain":["*researchnow.com","*valuedopinions.co.uk"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?valuedopinions\\.co\\.uk","load-type":["third-party"],"unless-domain":["*researchnow.com","*valuedopinions.co.uk"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?revtrax\\.com","load-type":["third-party"],"unless-domain":["*revtrax.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?ringier\\.cz","load-type":["third-party"],"unless-domain":["*ringier.cz"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?getclicky\\.com","load-type":["third-party"],"unless-domain":["*roxr.net","*getclicky.com","*staticstuff.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?roxr\\.net","load-type":["third-party"],"unless-domain":["*roxr.net","*getclicky.com","*staticstuff.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?staticstuff\\.net","load-type":["third-party"],"unless-domain":["*roxr.net","*getclicky.com","*staticstuff.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?statsit\\.com","load-type":["third-party"],"unless-domain":["*statsit.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?dl-rms\\.com","load-type":["third-party"],"unless-domain":["*safecount.net","*dl-rms.com","*dlqm.net","*questionmarket.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?dlqm\\.net","load-type":["third-party"],"unless-domain":["*safecount.net","*dl-rms.com","*dlqm.net","*questionmarket.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?questionmarket\\.com","load-type":["third-party"],"unless-domain":["*safecount.net","*dl-rms.com","*dlqm.net","*questionmarket.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?safecount\\.net","load-type":["third-party"],"unless-domain":["*safecount.net","*dl-rms.com","*dlqm.net","*questionmarket.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?sageanalyst\\.net","load-type":["third-party"],"unless-domain":["*sagemetrics.com","*sageanalyst.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?sagemetrics\\.com","load-type":["third-party"],"unless-domain":["*sagemetrics.com","*sageanalyst.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?seevolution\\.com","load-type":["third-party"],"unless-domain":["*seevolution.com","*svlu.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?svlu\\.net","load-type":["third-party"],"unless-domain":["*seevolution.com","*svlu.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?segment\\.io","load-type":["third-party"],"unless-domain":["*segment.io"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?shinystat\\.com","load-type":["third-party"],"unless-domain":["*shinystat.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?shorte\\.st","load-type":["third-party"],"unless-domain":["*shorte.st"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?snoobi\\.com","load-type":["third-party"],"unless-domain":["*snoobi.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?statcounter\\.com","load-type":["third-party"],"unless-domain":["*statcounter.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?statisfy\\.net","load-type":["third-party"],"unless-domain":["*statisfy.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?stratigent\\.com","load-type":["third-party"],"unless-domain":["*stratigent.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?tensquare\\.com","load-type":["third-party"],"unless-domain":["*tensquare.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?sesamestats\\.com","load-type":["third-party"],"unless-domain":["*tnsglobal.com","*sesamestats.com","*statistik-gallup.net","*tns-counter.ru","*tns-cs.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?statistik-gallup\\.net","load-type":["third-party"],"unless-domain":["*tnsglobal.com","*sesamestats.com","*statistik-gallup.net","*tns-counter.ru","*tns-cs.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?tns-counter\\.ru","load-type":["third-party"],"unless-domain":["*tnsglobal.com","*sesamestats.com","*statistik-gallup.net","*tns-counter.ru","*tns-cs.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?tns-cs\\.net","load-type":["third-party"],"unless-domain":["*tnsglobal.com","*sesamestats.com","*statistik-gallup.net","*tns-counter.ru","*tns-cs.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?tnsglobal\\.com","load-type":["third-party"],"unless-domain":["*tnsglobal.com","*sesamestats.com","*statistik-gallup.net","*tns-counter.ru","*tns-cs.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?tealium\\.com","load-type":["third-party"],"unless-domain":["*tealium.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?tiqcdn\\.com","load-type":["third-party"],"unless-domain":["*tealium.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?heronpartners\\.com\\.au","load-type":["third-party"],"unless-domain":["*www.heronpartners.com.au","*heronpartners.com.au","*marinsm.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?marinsm\\.com","load-type":["third-party"],"unless-domain":["*www.heronpartners.com.au","*heronpartners.com.au","*marinsm.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?roia\\.biz","load-type":["third-party"],"unless-domain":["*trackingsoft.com","*roia.biz"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?trackingsoft\\.com","load-type":["third-party"],"unless-domain":["*trackingsoft.com","*roia.biz"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?umbel\\.com","load-type":["third-party"],"unless-domain":["*umbel.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?nakanohito\\.jp","load-type":["third-party"],"unless-domain":["*nakanohito.jp"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?vertster\\.com","load-type":["third-party"],"unless-domain":["*vertster.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?sa-as\\.com","load-type":["third-party"],"unless-domain":["*visistat.com","*sa-as.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?visistat\\.com","load-type":["third-party"],"unless-domain":["*visistat.com","*sa-as.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?visitstreamer\\.com","load-type":["third-party"],"unless-domain":["*visitstreamer.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?vizisense\\.com","load-type":["third-party"],"unless-domain":["*vizisense.com","*vizisense.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?vizisense\\.net","load-type":["third-party"],"unless-domain":["*vizisense.com","*vizisense.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?wowanalytics\\.co\\.uk","load-type":["third-party"],"unless-domain":["*wowanalytics.co.uk"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?compete\\.com","load-type":["third-party"],"unless-domain":["*wpp.com","*247realmedia.com","*accelerator-media.com","*acceleratorusa.com","*decdna.net","*decideinteractive.com","*gmads.net","*groupm.com","*kantarmedia.com","*mecglobal.com","*mindshare.nl","*mookie1.com","*pm14.com","*realmedia.com","*targ.ad","*themig.com","*xaxis.com","*compete.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?onlinewebstats\\.com","load-type":["third-party"],"unless-domain":["*onlinewebstats.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?web-stat\\.com","load-type":["third-party"],"unless-domain":["*webtrackingservices.com","*web-stat.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?webtrackingservices\\.com","load-type":["third-party"],"unless-domain":["*webtrackingservices.com","*web-stat.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?webtraxs\\.com","load-type":["third-party"],"unless-domain":["*webtraxs.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?webclicktracker\\.com","load-type":["third-party"],"unless-domain":["*webclicktracker.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?webtrekk\\.com","load-type":["third-party"],"unless-domain":["*webtrekk.com","*webtrekk.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?webtrekk\\.net","load-type":["third-party"],"unless-domain":["*webtrekk.com","*webtrekk.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?reinvigorate\\.net","load-type":["third-party"],"unless-domain":["*webtrends.com","*reinvigorate.net","*webtrendslive.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?webtrends\\.com","load-type":["third-party"],"unless-domain":["*webtrends.com","*reinvigorate.net","*webtrendslive.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?webtrendslive\\.com","load-type":["third-party"],"unless-domain":["*webtrends.com","*reinvigorate.net","*webtrendslive.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?woopra-ns\\.com","load-type":["third-party"],"unless-domain":["*woopra.com","*woopra-ns.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?woopra\\.com","load-type":["third-party"],"unless-domain":["*woopra.com","*woopra-ns.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?wysistat\\.com","load-type":["third-party"],"unless-domain":["*wysistat.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?analytics\\.yahoo\\.com","load-type":["third-party"],"unless-domain":["*flickr.com","*yuilibrary.com","*tumblr.com","*yahoo.com","*yahoostudios.com","*yahoo.co.jp"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?yellowtracker\\.com","load-type":["third-party"],"unless-domain":["*yellowtracker.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?anormal-media\\.de","load-type":["third-party"],"unless-domain":["*anormal-media.de","*anormal-tracker.de"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?anormal-tracker\\.de","load-type":["third-party"],"unless-domain":["*anormal-media.de","*anormal-tracker.de"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?certifica\\.com","load-type":["third-party"],"unless-domain":["*comscore.com","*adxpose.com","*certifica.com","*scorecardresearch.com","*sitestat.com","*voicefive.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?comscore\\.com","load-type":["third-party"],"unless-domain":["*comscore.com","*adxpose.com","*certifica.com","*scorecardresearch.com","*sitestat.com","*voicefive.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?scorecardresearch\\.com","load-type":["third-party"],"unless-domain":["*comscore.com","*adxpose.com","*certifica.com","*scorecardresearch.com","*sitestat.com","*voicefive.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?sitestat\\.com","load-type":["third-party"],"unless-domain":["*comscore.com","*adxpose.com","*certifica.com","*scorecardresearch.com","*sitestat.com","*voicefive.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?voicefive\\.com","load-type":["third-party"],"unless-domain":["*comscore.com","*adxpose.com","*certifica.com","*scorecardresearch.com","*sitestat.com","*voicefive.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?dwstat\\.cn","load-type":["third-party"],"unless-domain":["*dwstat.cn"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?eproof\\.com","load-type":["third-party"],"unless-domain":["*eproof.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?extreme-dm\\.com","load-type":["third-party"],"unless-domain":["*extremetracking.com","*extreme-dm.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?extremetracking\\.com","load-type":["third-party"],"unless-domain":["*extremetracking.com","*extreme-dm.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?etracker\\.com","load-type":["third-party"],"unless-domain":["*etracker.com","*etracker.de","*sedotracker.com","*sedotracker.de"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?etracker\\.de","load-type":["third-party"],"unless-domain":["*etracker.com","*etracker.de","*sedotracker.com","*sedotracker.de"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?sedotracker\\.com","load-type":["third-party"],"unless-domain":["*etracker.com","*etracker.de","*sedotracker.com","*sedotracker.de"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?sedotracker\\.de","load-type":["third-party"],"unless-domain":["*etracker.com","*etracker.de","*sedotracker.com","*sedotracker.de"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?iperceptions\\.com","load-type":["third-party"],"unless-domain":["*iperceptions.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?motigo\\.com","load-type":["third-party"],"unless-domain":["*motigo.com","*nedstatbasic.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?nedstatbasic\\.net","load-type":["third-party"],"unless-domain":["*motigo.com","*nedstatbasic.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?nurago\\.com","load-type":["third-party"],"unless-domain":["*nurago.com","*nurago.de","*sensic.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?nurago\\.de","load-type":["third-party"],"unless-domain":["*nurago.com","*nurago.de","*sensic.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?sensic\\.net","load-type":["third-party"],"unless-domain":["*nurago.com","*nurago.de","*sensic.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?phpmyvisites\\.us","load-type":["third-party"],"unless-domain":["*phpmyvisites.us"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?4u\\.pl","load-type":["third-party"],"unless-domain":["*4u.pl"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?vistrac\\.com","load-type":["third-party"],"unless-domain":["*vistrac.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?amung\\.us","load-type":["third-party"],"unless-domain":["*amung.us"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?oewa\\.at","load-type":["third-party"],"unless-domain":["*oewa.at","*oewabox.at"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?oewabox\\.at","load-type":["third-party"],"unless-domain":["*oewa.at","*oewabox.at"]}}] \ No newline at end of file diff --git a/mobile/ios/Client/Frontend/ContentBlocker/disconnect-content.json b/mobile/ios/Client/Frontend/ContentBlocker/disconnect-content.json new file mode 100644 index 0000000000..927a0c7a5d --- /dev/null +++ b/mobile/ios/Client/Frontend/ContentBlocker/disconnect-content.json @@ -0,0 +1 @@ +[{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?tynt\\.com","load-type":["third-party"],"unless-domain":["*33across.com","*tynt.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?activengage\\.com","load-type":["third-party"],"unless-domain":["*activengage.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?adap\\.tv","load-type":["third-party"],"unless-domain":["*adap.tv"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?adobe\\.com","load-type":["third-party"],"unless-domain":["*adobe.com","*2o7.net","*auditude.com","*demdex.com","*demdex.net","*dmtracker.com","*efrontier.com","*everestads.net","*everestjs.net","*everesttech.net","*hitbox.com","*omniture.com","*omtrdc.net","*touchclarity.com","*typekit.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?typekit\\.com","load-type":["third-party"],"unless-domain":["*adobe.com","*2o7.net","*auditude.com","*demdex.com","*demdex.net","*dmtracker.com","*efrontier.com","*everestads.net","*everestjs.net","*everesttech.net","*hitbox.com","*omniture.com","*omtrdc.net","*touchclarity.com","*typekit.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?abmr\\.net","load-type":["third-party"],"unless-domain":["*akamai.com","*imiclk.com","*abmr.net","*edgesuite.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?akamai\\.com","load-type":["third-party"],"unless-domain":["*akamai.com","*imiclk.com","*abmr.net","*edgesuite.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?edgesuite\\.net","load-type":["third-party"],"unless-domain":["*akamai.com","*imiclk.com","*abmr.net","*edgesuite.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?akqa\\.com","load-type":["third-party"],"unless-domain":["*akqa.com","*srtk.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?srtk\\.net","load-type":["third-party"],"unless-domain":["*akqa.com","*srtk.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?amazon\\.com","load-type":["third-party"],"unless-domain":["*amazon.com","*amazon-adsystem.com","*amazon.ca","*amazon.co.jp","*amazon.co.uk","*amazon.de","*amazon.es","*amazon.fr","*amazon.it","*assoc-amazon.com","*alexa.com","*alexametrics.com","*cloudfront.net","*amazonaws.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?cloudfront\\.net","load-type":["third-party"],"unless-domain":["*amazon.com","*amazon-adsystem.com","*amazon.ca","*amazon.co.jp","*amazon.co.uk","*amazon.de","*amazon.es","*amazon.fr","*amazon.it","*assoc-amazon.com","*alexa.com","*alexametrics.com","*cloudfront.net","*amazonaws.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?amazonaws\\.com","load-type":["third-party"],"unless-domain":["*amazon.com","*amazon-adsystem.com","*amazon.ca","*amazon.co.jp","*amazon.co.uk","*amazon.de","*amazon.es","*amazon.fr","*amazon.it","*assoc-amazon.com","*alexa.com","*alexametrics.com","*cloudfront.net","*amazonaws.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?5min\\.com","load-type":["third-party"],"unless-domain":["*aol.com","*adsonar.com","*advertising.com","*atwola.com","*leadback.com","*tacoda.net","*5min.com","*aim.com","*aolcdn.com","*aoltechguru.com","*autoblog.com","*cambio.com","*dailyfinance.com","*editions.com","*engadget.com","*games.com","*homesessive.com","*huffingtonpost.com","*joystiq.com","*kitchendaily.com","*makers.com","*mandatory.com","*mapquest.com","*moviefone.com","*noisecreep.com","*patch.com","*pawnation.com","*shortcuts.com","*shoutcast.com","*spinner.com","*stylelist.com","*stylemepretty.com","*surphace.com","*techcrunch.com","*theboombox.com","*theboot.com","*tuaw.com","*userplane.com","*winamp.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?aim\\.com","load-type":["third-party"],"unless-domain":["*aol.com","*adsonar.com","*advertising.com","*atwola.com","*leadback.com","*tacoda.net","*5min.com","*aim.com","*aolcdn.com","*aoltechguru.com","*autoblog.com","*cambio.com","*dailyfinance.com","*editions.com","*engadget.com","*games.com","*homesessive.com","*huffingtonpost.com","*joystiq.com","*kitchendaily.com","*makers.com","*mandatory.com","*mapquest.com","*moviefone.com","*noisecreep.com","*patch.com","*pawnation.com","*shortcuts.com","*shoutcast.com","*spinner.com","*stylelist.com","*stylemepretty.com","*surphace.com","*techcrunch.com","*theboombox.com","*theboot.com","*tuaw.com","*userplane.com","*winamp.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?aol\\.com","load-type":["third-party"],"unless-domain":["*aol.com","*adsonar.com","*advertising.com","*atwola.com","*leadback.com","*tacoda.net","*5min.com","*aim.com","*aolcdn.com","*aoltechguru.com","*autoblog.com","*cambio.com","*dailyfinance.com","*editions.com","*engadget.com","*games.com","*homesessive.com","*huffingtonpost.com","*joystiq.com","*kitchendaily.com","*makers.com","*mandatory.com","*mapquest.com","*moviefone.com","*noisecreep.com","*patch.com","*pawnation.com","*shortcuts.com","*shoutcast.com","*spinner.com","*stylelist.com","*stylemepretty.com","*surphace.com","*techcrunch.com","*theboombox.com","*theboot.com","*tuaw.com","*userplane.com","*winamp.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?aolanswers\\.com","load-type":["third-party"],"unless-domain":["*aol.com","*adsonar.com","*advertising.com","*atwola.com","*leadback.com","*tacoda.net","*5min.com","*aim.com","*aolcdn.com","*aoltechguru.com","*autoblog.com","*cambio.com","*dailyfinance.com","*editions.com","*engadget.com","*games.com","*homesessive.com","*huffingtonpost.com","*joystiq.com","*kitchendaily.com","*makers.com","*mandatory.com","*mapquest.com","*moviefone.com","*noisecreep.com","*patch.com","*pawnation.com","*shortcuts.com","*shoutcast.com","*spinner.com","*stylelist.com","*stylemepretty.com","*surphace.com","*techcrunch.com","*theboombox.com","*theboot.com","*tuaw.com","*userplane.com","*winamp.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?aolcdn\\.com","load-type":["third-party"],"unless-domain":["*aol.com","*adsonar.com","*advertising.com","*atwola.com","*leadback.com","*tacoda.net","*5min.com","*aim.com","*aolcdn.com","*aoltechguru.com","*autoblog.com","*cambio.com","*dailyfinance.com","*editions.com","*engadget.com","*games.com","*homesessive.com","*huffingtonpost.com","*joystiq.com","*kitchendaily.com","*makers.com","*mandatory.com","*mapquest.com","*moviefone.com","*noisecreep.com","*patch.com","*pawnation.com","*shortcuts.com","*shoutcast.com","*spinner.com","*stylelist.com","*stylemepretty.com","*surphace.com","*techcrunch.com","*theboombox.com","*theboot.com","*tuaw.com","*userplane.com","*winamp.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?aoltechguru\\.com","load-type":["third-party"],"unless-domain":["*aol.com","*adsonar.com","*advertising.com","*atwola.com","*leadback.com","*tacoda.net","*5min.com","*aim.com","*aolcdn.com","*aoltechguru.com","*autoblog.com","*cambio.com","*dailyfinance.com","*editions.com","*engadget.com","*games.com","*homesessive.com","*huffingtonpost.com","*joystiq.com","*kitchendaily.com","*makers.com","*mandatory.com","*mapquest.com","*moviefone.com","*noisecreep.com","*patch.com","*pawnation.com","*shortcuts.com","*shoutcast.com","*spinner.com","*stylelist.com","*stylemepretty.com","*surphace.com","*techcrunch.com","*theboombox.com","*theboot.com","*tuaw.com","*userplane.com","*winamp.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?autoblog\\.com","load-type":["third-party"],"unless-domain":["*aol.com","*adsonar.com","*advertising.com","*atwola.com","*leadback.com","*tacoda.net","*5min.com","*aim.com","*aolcdn.com","*aoltechguru.com","*autoblog.com","*cambio.com","*dailyfinance.com","*editions.com","*engadget.com","*games.com","*homesessive.com","*huffingtonpost.com","*joystiq.com","*kitchendaily.com","*makers.com","*mandatory.com","*mapquest.com","*moviefone.com","*noisecreep.com","*patch.com","*pawnation.com","*shortcuts.com","*shoutcast.com","*spinner.com","*stylelist.com","*stylemepretty.com","*surphace.com","*techcrunch.com","*theboombox.com","*theboot.com","*tuaw.com","*userplane.com","*winamp.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?cambio\\.com","load-type":["third-party"],"unless-domain":["*aol.com","*adsonar.com","*advertising.com","*atwola.com","*leadback.com","*tacoda.net","*5min.com","*aim.com","*aolcdn.com","*aoltechguru.com","*autoblog.com","*cambio.com","*dailyfinance.com","*editions.com","*engadget.com","*games.com","*homesessive.com","*huffingtonpost.com","*joystiq.com","*kitchendaily.com","*makers.com","*mandatory.com","*mapquest.com","*moviefone.com","*noisecreep.com","*patch.com","*pawnation.com","*shortcuts.com","*shoutcast.com","*spinner.com","*stylelist.com","*stylemepretty.com","*surphace.com","*techcrunch.com","*theboombox.com","*theboot.com","*tuaw.com","*userplane.com","*winamp.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?dailyfinance\\.com","load-type":["third-party"],"unless-domain":["*aol.com","*adsonar.com","*advertising.com","*atwola.com","*leadback.com","*tacoda.net","*5min.com","*aim.com","*aolcdn.com","*aoltechguru.com","*autoblog.com","*cambio.com","*dailyfinance.com","*editions.com","*engadget.com","*games.com","*homesessive.com","*huffingtonpost.com","*joystiq.com","*kitchendaily.com","*makers.com","*mandatory.com","*mapquest.com","*moviefone.com","*noisecreep.com","*patch.com","*pawnation.com","*shortcuts.com","*shoutcast.com","*spinner.com","*stylelist.com","*stylemepretty.com","*surphace.com","*techcrunch.com","*theboombox.com","*theboot.com","*tuaw.com","*userplane.com","*winamp.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?editions\\.com","load-type":["third-party"],"unless-domain":["*aol.com","*adsonar.com","*advertising.com","*atwola.com","*leadback.com","*tacoda.net","*5min.com","*aim.com","*aolcdn.com","*aoltechguru.com","*autoblog.com","*cambio.com","*dailyfinance.com","*editions.com","*engadget.com","*games.com","*homesessive.com","*huffingtonpost.com","*joystiq.com","*kitchendaily.com","*makers.com","*mandatory.com","*mapquest.com","*moviefone.com","*noisecreep.com","*patch.com","*pawnation.com","*shortcuts.com","*shoutcast.com","*spinner.com","*stylelist.com","*stylemepretty.com","*surphace.com","*techcrunch.com","*theboombox.com","*theboot.com","*tuaw.com","*userplane.com","*winamp.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?engadget\\.com","load-type":["third-party"],"unless-domain":["*aol.com","*adsonar.com","*advertising.com","*atwola.com","*leadback.com","*tacoda.net","*5min.com","*aim.com","*aolcdn.com","*aoltechguru.com","*autoblog.com","*cambio.com","*dailyfinance.com","*editions.com","*engadget.com","*games.com","*homesessive.com","*huffingtonpost.com","*joystiq.com","*kitchendaily.com","*makers.com","*mandatory.com","*mapquest.com","*moviefone.com","*noisecreep.com","*patch.com","*pawnation.com","*shortcuts.com","*shoutcast.com","*spinner.com","*stylelist.com","*stylemepretty.com","*surphace.com","*techcrunch.com","*theboombox.com","*theboot.com","*tuaw.com","*userplane.com","*winamp.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?games\\.com","load-type":["third-party"],"unless-domain":["*aol.com","*adsonar.com","*advertising.com","*atwola.com","*leadback.com","*tacoda.net","*5min.com","*aim.com","*aolcdn.com","*aoltechguru.com","*autoblog.com","*cambio.com","*dailyfinance.com","*editions.com","*engadget.com","*games.com","*homesessive.com","*huffingtonpost.com","*joystiq.com","*kitchendaily.com","*makers.com","*mandatory.com","*mapquest.com","*moviefone.com","*noisecreep.com","*patch.com","*pawnation.com","*shortcuts.com","*shoutcast.com","*spinner.com","*stylelist.com","*stylemepretty.com","*surphace.com","*techcrunch.com","*theboombox.com","*theboot.com","*tuaw.com","*userplane.com","*winamp.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?homesessive\\.com","load-type":["third-party"],"unless-domain":["*aol.com","*adsonar.com","*advertising.com","*atwola.com","*leadback.com","*tacoda.net","*5min.com","*aim.com","*aolcdn.com","*aoltechguru.com","*autoblog.com","*cambio.com","*dailyfinance.com","*editions.com","*engadget.com","*games.com","*homesessive.com","*huffingtonpost.com","*joystiq.com","*kitchendaily.com","*makers.com","*mandatory.com","*mapquest.com","*moviefone.com","*noisecreep.com","*patch.com","*pawnation.com","*shortcuts.com","*shoutcast.com","*spinner.com","*stylelist.com","*stylemepretty.com","*surphace.com","*techcrunch.com","*theboombox.com","*theboot.com","*tuaw.com","*userplane.com","*winamp.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?huffingtonpost\\.com","load-type":["third-party"],"unless-domain":["*aol.com","*adsonar.com","*advertising.com","*atwola.com","*leadback.com","*tacoda.net","*5min.com","*aim.com","*aolcdn.com","*aoltechguru.com","*autoblog.com","*cambio.com","*dailyfinance.com","*editions.com","*engadget.com","*games.com","*homesessive.com","*huffingtonpost.com","*joystiq.com","*kitchendaily.com","*makers.com","*mandatory.com","*mapquest.com","*moviefone.com","*noisecreep.com","*patch.com","*pawnation.com","*shortcuts.com","*shoutcast.com","*spinner.com","*stylelist.com","*stylemepretty.com","*surphace.com","*techcrunch.com","*theboombox.com","*theboot.com","*tuaw.com","*userplane.com","*winamp.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?joystiq\\.com","load-type":["third-party"],"unless-domain":["*aol.com","*adsonar.com","*advertising.com","*atwola.com","*leadback.com","*tacoda.net","*5min.com","*aim.com","*aolcdn.com","*aoltechguru.com","*autoblog.com","*cambio.com","*dailyfinance.com","*editions.com","*engadget.com","*games.com","*homesessive.com","*huffingtonpost.com","*joystiq.com","*kitchendaily.com","*makers.com","*mandatory.com","*mapquest.com","*moviefone.com","*noisecreep.com","*patch.com","*pawnation.com","*shortcuts.com","*shoutcast.com","*spinner.com","*stylelist.com","*stylemepretty.com","*surphace.com","*techcrunch.com","*theboombox.com","*theboot.com","*tuaw.com","*userplane.com","*winamp.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?kitchendaily\\.com","load-type":["third-party"],"unless-domain":["*aol.com","*adsonar.com","*advertising.com","*atwola.com","*leadback.com","*tacoda.net","*5min.com","*aim.com","*aolcdn.com","*aoltechguru.com","*autoblog.com","*cambio.com","*dailyfinance.com","*editions.com","*engadget.com","*games.com","*homesessive.com","*huffingtonpost.com","*joystiq.com","*kitchendaily.com","*makers.com","*mandatory.com","*mapquest.com","*moviefone.com","*noisecreep.com","*patch.com","*pawnation.com","*shortcuts.com","*shoutcast.com","*spinner.com","*stylelist.com","*stylemepretty.com","*surphace.com","*techcrunch.com","*theboombox.com","*theboot.com","*tuaw.com","*userplane.com","*winamp.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?makers\\.com","load-type":["third-party"],"unless-domain":["*aol.com","*adsonar.com","*advertising.com","*atwola.com","*leadback.com","*tacoda.net","*5min.com","*aim.com","*aolcdn.com","*aoltechguru.com","*autoblog.com","*cambio.com","*dailyfinance.com","*editions.com","*engadget.com","*games.com","*homesessive.com","*huffingtonpost.com","*joystiq.com","*kitchendaily.com","*makers.com","*mandatory.com","*mapquest.com","*moviefone.com","*noisecreep.com","*patch.com","*pawnation.com","*shortcuts.com","*shoutcast.com","*spinner.com","*stylelist.com","*stylemepretty.com","*surphace.com","*techcrunch.com","*theboombox.com","*theboot.com","*tuaw.com","*userplane.com","*winamp.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?mandatory\\.com","load-type":["third-party"],"unless-domain":["*aol.com","*adsonar.com","*advertising.com","*atwola.com","*leadback.com","*tacoda.net","*5min.com","*aim.com","*aolcdn.com","*aoltechguru.com","*autoblog.com","*cambio.com","*dailyfinance.com","*editions.com","*engadget.com","*games.com","*homesessive.com","*huffingtonpost.com","*joystiq.com","*kitchendaily.com","*makers.com","*mandatory.com","*mapquest.com","*moviefone.com","*noisecreep.com","*patch.com","*pawnation.com","*shortcuts.com","*shoutcast.com","*spinner.com","*stylelist.com","*stylemepretty.com","*surphace.com","*techcrunch.com","*theboombox.com","*theboot.com","*tuaw.com","*userplane.com","*winamp.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?mapquest\\.com","load-type":["third-party"],"unless-domain":["*aol.com","*adsonar.com","*advertising.com","*atwola.com","*leadback.com","*tacoda.net","*5min.com","*aim.com","*aolcdn.com","*aoltechguru.com","*autoblog.com","*cambio.com","*dailyfinance.com","*editions.com","*engadget.com","*games.com","*homesessive.com","*huffingtonpost.com","*joystiq.com","*kitchendaily.com","*makers.com","*mandatory.com","*mapquest.com","*moviefone.com","*noisecreep.com","*patch.com","*pawnation.com","*shortcuts.com","*shoutcast.com","*spinner.com","*stylelist.com","*stylemepretty.com","*surphace.com","*techcrunch.com","*theboombox.com","*theboot.com","*tuaw.com","*userplane.com","*winamp.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?moviefone\\.com","load-type":["third-party"],"unless-domain":["*aol.com","*adsonar.com","*advertising.com","*atwola.com","*leadback.com","*tacoda.net","*5min.com","*aim.com","*aolcdn.com","*aoltechguru.com","*autoblog.com","*cambio.com","*dailyfinance.com","*editions.com","*engadget.com","*games.com","*homesessive.com","*huffingtonpost.com","*joystiq.com","*kitchendaily.com","*makers.com","*mandatory.com","*mapquest.com","*moviefone.com","*noisecreep.com","*patch.com","*pawnation.com","*shortcuts.com","*shoutcast.com","*spinner.com","*stylelist.com","*stylemepretty.com","*surphace.com","*techcrunch.com","*theboombox.com","*theboot.com","*tuaw.com","*userplane.com","*winamp.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?noisecreep\\.com","load-type":["third-party"],"unless-domain":["*aol.com","*adsonar.com","*advertising.com","*atwola.com","*leadback.com","*tacoda.net","*5min.com","*aim.com","*aolcdn.com","*aoltechguru.com","*autoblog.com","*cambio.com","*dailyfinance.com","*editions.com","*engadget.com","*games.com","*homesessive.com","*huffingtonpost.com","*joystiq.com","*kitchendaily.com","*makers.com","*mandatory.com","*mapquest.com","*moviefone.com","*noisecreep.com","*patch.com","*pawnation.com","*shortcuts.com","*shoutcast.com","*spinner.com","*stylelist.com","*stylemepretty.com","*surphace.com","*techcrunch.com","*theboombox.com","*theboot.com","*tuaw.com","*userplane.com","*winamp.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?patch\\.com","load-type":["third-party"],"unless-domain":["*aol.com","*adsonar.com","*advertising.com","*atwola.com","*leadback.com","*tacoda.net","*5min.com","*aim.com","*aolcdn.com","*aoltechguru.com","*autoblog.com","*cambio.com","*dailyfinance.com","*editions.com","*engadget.com","*games.com","*homesessive.com","*huffingtonpost.com","*joystiq.com","*kitchendaily.com","*makers.com","*mandatory.com","*mapquest.com","*moviefone.com","*noisecreep.com","*patch.com","*pawnation.com","*shortcuts.com","*shoutcast.com","*spinner.com","*stylelist.com","*stylemepretty.com","*surphace.com","*techcrunch.com","*theboombox.com","*theboot.com","*tuaw.com","*userplane.com","*winamp.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?pawnation\\.com","load-type":["third-party"],"unless-domain":["*aol.com","*adsonar.com","*advertising.com","*atwola.com","*leadback.com","*tacoda.net","*5min.com","*aim.com","*aolcdn.com","*aoltechguru.com","*autoblog.com","*cambio.com","*dailyfinance.com","*editions.com","*engadget.com","*games.com","*homesessive.com","*huffingtonpost.com","*joystiq.com","*kitchendaily.com","*makers.com","*mandatory.com","*mapquest.com","*moviefone.com","*noisecreep.com","*patch.com","*pawnation.com","*shortcuts.com","*shoutcast.com","*spinner.com","*stylelist.com","*stylemepretty.com","*surphace.com","*techcrunch.com","*theboombox.com","*theboot.com","*tuaw.com","*userplane.com","*winamp.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?shortcuts\\.com","load-type":["third-party"],"unless-domain":["*aol.com","*adsonar.com","*advertising.com","*atwola.com","*leadback.com","*tacoda.net","*5min.com","*aim.com","*aolcdn.com","*aoltechguru.com","*autoblog.com","*cambio.com","*dailyfinance.com","*editions.com","*engadget.com","*games.com","*homesessive.com","*huffingtonpost.com","*joystiq.com","*kitchendaily.com","*makers.com","*mandatory.com","*mapquest.com","*moviefone.com","*noisecreep.com","*patch.com","*pawnation.com","*shortcuts.com","*shoutcast.com","*spinner.com","*stylelist.com","*stylemepretty.com","*surphace.com","*techcrunch.com","*theboombox.com","*theboot.com","*tuaw.com","*userplane.com","*winamp.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?shoutcast\\.com","load-type":["third-party"],"unless-domain":["*aol.com","*adsonar.com","*advertising.com","*atwola.com","*leadback.com","*tacoda.net","*5min.com","*aim.com","*aolcdn.com","*aoltechguru.com","*autoblog.com","*cambio.com","*dailyfinance.com","*editions.com","*engadget.com","*games.com","*homesessive.com","*huffingtonpost.com","*joystiq.com","*kitchendaily.com","*makers.com","*mandatory.com","*mapquest.com","*moviefone.com","*noisecreep.com","*patch.com","*pawnation.com","*shortcuts.com","*shoutcast.com","*spinner.com","*stylelist.com","*stylemepretty.com","*surphace.com","*techcrunch.com","*theboombox.com","*theboot.com","*tuaw.com","*userplane.com","*winamp.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?spinner\\.com","load-type":["third-party"],"unless-domain":["*aol.com","*adsonar.com","*advertising.com","*atwola.com","*leadback.com","*tacoda.net","*5min.com","*aim.com","*aolcdn.com","*aoltechguru.com","*autoblog.com","*cambio.com","*dailyfinance.com","*editions.com","*engadget.com","*games.com","*homesessive.com","*huffingtonpost.com","*joystiq.com","*kitchendaily.com","*makers.com","*mandatory.com","*mapquest.com","*moviefone.com","*noisecreep.com","*patch.com","*pawnation.com","*shortcuts.com","*shoutcast.com","*spinner.com","*stylelist.com","*stylemepretty.com","*surphace.com","*techcrunch.com","*theboombox.com","*theboot.com","*tuaw.com","*userplane.com","*winamp.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?stylelist\\.com","load-type":["third-party"],"unless-domain":["*aol.com","*adsonar.com","*advertising.com","*atwola.com","*leadback.com","*tacoda.net","*5min.com","*aim.com","*aolcdn.com","*aoltechguru.com","*autoblog.com","*cambio.com","*dailyfinance.com","*editions.com","*engadget.com","*games.com","*homesessive.com","*huffingtonpost.com","*joystiq.com","*kitchendaily.com","*makers.com","*mandatory.com","*mapquest.com","*moviefone.com","*noisecreep.com","*patch.com","*pawnation.com","*shortcuts.com","*shoutcast.com","*spinner.com","*stylelist.com","*stylemepretty.com","*surphace.com","*techcrunch.com","*theboombox.com","*theboot.com","*tuaw.com","*userplane.com","*winamp.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?stylemepretty\\.com","load-type":["third-party"],"unless-domain":["*aol.com","*adsonar.com","*advertising.com","*atwola.com","*leadback.com","*tacoda.net","*5min.com","*aim.com","*aolcdn.com","*aoltechguru.com","*autoblog.com","*cambio.com","*dailyfinance.com","*editions.com","*engadget.com","*games.com","*homesessive.com","*huffingtonpost.com","*joystiq.com","*kitchendaily.com","*makers.com","*mandatory.com","*mapquest.com","*moviefone.com","*noisecreep.com","*patch.com","*pawnation.com","*shortcuts.com","*shoutcast.com","*spinner.com","*stylelist.com","*stylemepretty.com","*surphace.com","*techcrunch.com","*theboombox.com","*theboot.com","*tuaw.com","*userplane.com","*winamp.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?surphace\\.com","load-type":["third-party"],"unless-domain":["*aol.com","*adsonar.com","*advertising.com","*atwola.com","*leadback.com","*tacoda.net","*5min.com","*aim.com","*aolcdn.com","*aoltechguru.com","*autoblog.com","*cambio.com","*dailyfinance.com","*editions.com","*engadget.com","*games.com","*homesessive.com","*huffingtonpost.com","*joystiq.com","*kitchendaily.com","*makers.com","*mandatory.com","*mapquest.com","*moviefone.com","*noisecreep.com","*patch.com","*pawnation.com","*shortcuts.com","*shoutcast.com","*spinner.com","*stylelist.com","*stylemepretty.com","*surphace.com","*techcrunch.com","*theboombox.com","*theboot.com","*tuaw.com","*userplane.com","*winamp.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?techcrunch\\.com","load-type":["third-party"],"unless-domain":["*aol.com","*adsonar.com","*advertising.com","*atwola.com","*leadback.com","*tacoda.net","*5min.com","*aim.com","*aolcdn.com","*aoltechguru.com","*autoblog.com","*cambio.com","*dailyfinance.com","*editions.com","*engadget.com","*games.com","*homesessive.com","*huffingtonpost.com","*joystiq.com","*kitchendaily.com","*makers.com","*mandatory.com","*mapquest.com","*moviefone.com","*noisecreep.com","*patch.com","*pawnation.com","*shortcuts.com","*shoutcast.com","*spinner.com","*stylelist.com","*stylemepretty.com","*surphace.com","*techcrunch.com","*theboombox.com","*theboot.com","*tuaw.com","*userplane.com","*winamp.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?theboombox\\.com","load-type":["third-party"],"unless-domain":["*aol.com","*adsonar.com","*advertising.com","*atwola.com","*leadback.com","*tacoda.net","*5min.com","*aim.com","*aolcdn.com","*aoltechguru.com","*autoblog.com","*cambio.com","*dailyfinance.com","*editions.com","*engadget.com","*games.com","*homesessive.com","*huffingtonpost.com","*joystiq.com","*kitchendaily.com","*makers.com","*mandatory.com","*mapquest.com","*moviefone.com","*noisecreep.com","*patch.com","*pawnation.com","*shortcuts.com","*shoutcast.com","*spinner.com","*stylelist.com","*stylemepretty.com","*surphace.com","*techcrunch.com","*theboombox.com","*theboot.com","*tuaw.com","*userplane.com","*winamp.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?theboot\\.com","load-type":["third-party"],"unless-domain":["*aol.com","*adsonar.com","*advertising.com","*atwola.com","*leadback.com","*tacoda.net","*5min.com","*aim.com","*aolcdn.com","*aoltechguru.com","*autoblog.com","*cambio.com","*dailyfinance.com","*editions.com","*engadget.com","*games.com","*homesessive.com","*huffingtonpost.com","*joystiq.com","*kitchendaily.com","*makers.com","*mandatory.com","*mapquest.com","*moviefone.com","*noisecreep.com","*patch.com","*pawnation.com","*shortcuts.com","*shoutcast.com","*spinner.com","*stylelist.com","*stylemepretty.com","*surphace.com","*techcrunch.com","*theboombox.com","*theboot.com","*tuaw.com","*userplane.com","*winamp.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?tuaw\\.com","load-type":["third-party"],"unless-domain":["*aol.com","*adsonar.com","*advertising.com","*atwola.com","*leadback.com","*tacoda.net","*5min.com","*aim.com","*aolcdn.com","*aoltechguru.com","*autoblog.com","*cambio.com","*dailyfinance.com","*editions.com","*engadget.com","*games.com","*homesessive.com","*huffingtonpost.com","*joystiq.com","*kitchendaily.com","*makers.com","*mandatory.com","*mapquest.com","*moviefone.com","*noisecreep.com","*patch.com","*pawnation.com","*shortcuts.com","*shoutcast.com","*spinner.com","*stylelist.com","*stylemepretty.com","*surphace.com","*techcrunch.com","*theboombox.com","*theboot.com","*tuaw.com","*userplane.com","*winamp.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?userplane\\.com","load-type":["third-party"],"unless-domain":["*aol.com","*adsonar.com","*advertising.com","*atwola.com","*leadback.com","*tacoda.net","*5min.com","*aim.com","*aolcdn.com","*aoltechguru.com","*autoblog.com","*cambio.com","*dailyfinance.com","*editions.com","*engadget.com","*games.com","*homesessive.com","*huffingtonpost.com","*joystiq.com","*kitchendaily.com","*makers.com","*mandatory.com","*mapquest.com","*moviefone.com","*noisecreep.com","*patch.com","*pawnation.com","*shortcuts.com","*shoutcast.com","*spinner.com","*stylelist.com","*stylemepretty.com","*surphace.com","*techcrunch.com","*theboombox.com","*theboot.com","*tuaw.com","*userplane.com","*winamp.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?winamp\\.com","load-type":["third-party"],"unless-domain":["*aol.com","*adsonar.com","*advertising.com","*atwola.com","*leadback.com","*tacoda.net","*5min.com","*aim.com","*aolcdn.com","*aoltechguru.com","*autoblog.com","*cambio.com","*dailyfinance.com","*editions.com","*engadget.com","*games.com","*homesessive.com","*huffingtonpost.com","*joystiq.com","*kitchendaily.com","*makers.com","*mandatory.com","*mapquest.com","*moviefone.com","*noisecreep.com","*patch.com","*pawnation.com","*shortcuts.com","*shoutcast.com","*spinner.com","*stylelist.com","*stylemepretty.com","*surphace.com","*techcrunch.com","*theboombox.com","*theboot.com","*tuaw.com","*userplane.com","*winamp.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?automattic\\.com","load-type":["third-party"],"unless-domain":["*automattic.com","*polldaddy.com","*gravatar.com","*intensedebate.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?gravatar\\.com","load-type":["third-party"],"unless-domain":["*automattic.com","*polldaddy.com","*gravatar.com","*intensedebate.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?intensedebate\\.com","load-type":["third-party"],"unless-domain":["*automattic.com","*polldaddy.com","*gravatar.com","*intensedebate.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?baynote\\.com","load-type":["third-party"],"unless-domain":["*baynote.com","*baynote.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?baynote\\.net","load-type":["third-party"],"unless-domain":["*baynote.com","*baynote.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?bazaarvoice\\.com","load-type":["third-party"],"unless-domain":["*bazaarvoice.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?bigdoor\\.com","load-type":["third-party"],"unless-domain":["*bigdoor.com","*onetruefan.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?onetruefan\\.com","load-type":["third-party"],"unless-domain":["*bigdoor.com","*onetruefan.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?brightcove\\.com","load-type":["third-party"],"unless-domain":["*brightcove.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?browser-update\\.org","load-type":["third-party"],"unless-domain":["*browser-update.org"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?btbuckets\\.com","load-type":["third-party"],"unless-domain":["*btbuckets.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?bufferapp\\.com","load-type":["third-party"],"unless-domain":["*bufferapp.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?bunchball\\.com","load-type":["third-party"],"unless-domain":["*bunchball.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?buysafe\\.com","load-type":["third-party"],"unless-domain":["*buysafe.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?buzzfed\\.com","load-type":["third-party"],"unless-domain":["*buzzfeed.com","*buzzfed.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?buzzfeed\\.com","load-type":["third-party"],"unless-domain":["*buzzfeed.com","*buzzfed.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?cbox\\.ws","load-type":["third-party"],"unless-domain":["*cbox.ws"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?cbsinteractive\\.com","load-type":["third-party"],"unless-domain":["*cbsinteractive.com","*com.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?com\\.com","load-type":["third-party"],"unless-domain":["*cbsinteractive.com","*com.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?cedexis\\.com","load-type":["third-party"],"unless-domain":["*cedexis.com","*cedexis.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?cedexis\\.net","load-type":["third-party"],"unless-domain":["*cedexis.com","*cedexis.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?certona\\.com","load-type":["third-party"],"unless-domain":["*certona.com","*res-x.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?res-x\\.com","load-type":["third-party"],"unless-domain":["*certona.com","*res-x.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?clipsyndicate\\.com","load-type":["third-party"],"unless-domain":["*clipsyndicate.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?collarity\\.com","load-type":["third-party"],"unless-domain":["*collarity.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?conduit-banners\\.com","load-type":["third-party"],"unless-domain":["*conduit.com","*conduit-banners.com","*conduit-services.com","*wibiya.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?conduit-services\\.com","load-type":["third-party"],"unless-domain":["*conduit.com","*conduit-banners.com","*conduit-services.com","*wibiya.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?conduit\\.com","load-type":["third-party"],"unless-domain":["*conduit.com","*conduit-banners.com","*conduit-services.com","*wibiya.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?wibiya\\.com","load-type":["third-party"],"unless-domain":["*conduit.com","*conduit-banners.com","*conduit-services.com","*wibiya.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?congoo\\.com","load-type":["third-party"],"unless-domain":["*congoo.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?contactatonce\\.com","load-type":["third-party"],"unless-domain":["*contactatonce.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?conviva\\.com","load-type":["third-party"],"unless-domain":["*conviva.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?dailyme\\.com","load-type":["third-party"],"unless-domain":["*dailyme.com","*newstogram.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?newstogram\\.com","load-type":["third-party"],"unless-domain":["*dailyme.com","*newstogram.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?datasift\\.com","load-type":["third-party"],"unless-domain":["*datasift.com","*tweetmeme.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?tweetmeme\\.com","load-type":["third-party"],"unless-domain":["*datasift.com","*tweetmeme.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?disqus\\.com","load-type":["third-party"],"unless-domain":["*disqus.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?aboutecho\\.com","load-type":["third-party"],"unless-domain":["*aboutecho.com","*haloscan.com","*js-kit.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?haloscan\\.com","load-type":["third-party"],"unless-domain":["*aboutecho.com","*haloscan.com","*js-kit.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?js-kit\\.com","load-type":["third-party"],"unless-domain":["*aboutecho.com","*haloscan.com","*js-kit.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?akamaihd\\.net","load-type":["third-party"],"unless-domain":["*facebook.com","*facebook.de","*facebook.fr","*facebook.net","*fb.com","*fb.me","*friendfeed.com","*instagram.com","*fbcdn.net","*messenger.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?instagram\\.com","load-type":["third-party"],"unless-domain":["*facebook.com","*facebook.de","*facebook.fr","*facebook.net","*fb.com","*fb.me","*friendfeed.com","*instagram.com","*fbcdn.net","*messenger.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?fbcdn\\.net","load-type":["third-party"],"unless-domain":["*facebook.com","*facebook.de","*facebook.fr","*facebook.net","*fb.com","*fb.me","*friendfeed.com","*instagram.com","*fbcdn.net","*messenger.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?flattr\\.com","load-type":["third-party"],"unless-domain":["*flattr.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?freewheel\\.tv","load-type":["third-party"],"unless-domain":["*freewheel.tv","*fwmrm.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?fwmrm\\.net","load-type":["third-party"],"unless-domain":["*freewheel.tv","*fwmrm.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?genius\\.com","load-type":["third-party"],"unless-domain":["*genius.com","*rsvpgenius.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?getsatisfaction\\.com","load-type":["third-party"],"unless-domain":["*getsatisfaction.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?gigcount\\.com","load-type":["third-party"],"unless-domain":["*gigya.com","*gigcount.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?gigya\\.com","load-type":["third-party"],"unless-domain":["*gigya.com","*gigcount.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?globaltakeoff\\.com","load-type":["third-party"],"unless-domain":["*globaltakeoff.com","*globaltakeoff.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?globaltakeoff\\.net","load-type":["third-party"],"unless-domain":["*globaltakeoff.com","*globaltakeoff.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?formalyzer\\.com","load-type":["third-party"],"unless-domain":["*gogrid.com","*formalyzer.com","*komli.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?gogrid\\.com","load-type":["third-party"],"unless-domain":["*gogrid.com","*formalyzer.com","*komli.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?komli\\.net","load-type":["third-party"],"unless-domain":["*gogrid.com","*formalyzer.com","*komli.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?apture\\.com","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?blogger\\.com","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?feedburner\\.com","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?ggpht\\.com","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?gmodules\\.com","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?google-melange\\.com","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?google\\.ad","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?google\\.ae","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?google\\.com\\.af","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?google\\.com\\.ag","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?google\\.com\\.ai","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?google\\.al","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?google\\.am","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?google\\.co\\.ao","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?google\\.com\\.ar","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?google\\.as","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?google\\.at","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?google\\.com\\.au","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?google\\.az","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?google\\.ba","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?google\\.com\\.bd","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?google\\.be","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?google\\.bf","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?google\\.bg","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?google\\.com\\.bh","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?google\\.bi","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?google\\.bj","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?google\\.com\\.bn","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?google\\.com\\.bo","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?google\\.com\\.br","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?google\\.bs","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?google\\.bt","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?google\\.co\\.bw","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?google\\.by","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?google\\.com\\.bz","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?google\\.ca","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?google\\.cd","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?google\\.cf","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?google\\.cg","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?google\\.ch","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?google\\.ci","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?google\\.co\\.ck","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?google\\.cl","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?google\\.cm","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?google\\.cn","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?google\\.com\\.co","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?accounts\\.google\\.com","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?apis\\.google\\.com","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?appengine\\.google\\.com","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?books\\.google\\.com","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?checkout\\.google\\.com","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?chrome\\.google\\.com","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?code\\.google\\.com","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?codesearch\\.google\\.com","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?docs\\.google\\.com","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?drive\\.google\\.com","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?earth\\.google\\.com","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?encrypted\\.google\\.com","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?feedburner\\.google\\.com","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?feedproxy\\.google\\.com","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?finance\\.google\\.com","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?groups\\.google\\.com","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?health\\.google\\.com","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?images\\.google\\.com","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?investor\\.google\\.com","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?knol\\.google\\.com","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?maps\\.google\\.com","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?music\\.google\\.com","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?news\\.google\\.com","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?picasa\\.google\\.com","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?picasaweb\\.google\\.com","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?play\\.google\\.com","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?script\\.google\\.com","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?shopping\\.google\\.com","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?sites\\.google\\.com","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?sketchup\\.google\\.com","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?support\\.google\\.com","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?talk\\.google\\.com","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?talkgadget\\.google\\.com","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?toolbar\\.google\\.com","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?translate\\.google\\.com","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?trends\\.google\\.com","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?video\\.google\\.com","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?videos\\.google\\.com","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?wallet\\.google\\.com","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?www\\.google\\.com","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?google\\.co\\.cr","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?google\\.com\\.cu","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?google\\.cv","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?google\\.com\\.cy","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?google\\.cz","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?google\\.de","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?google\\.dj","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?google\\.dk","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?google\\.dm","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?google\\.com\\.do","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?google\\.dz","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?google\\.com\\.ec","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?google\\.ee","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?google\\.com\\.eg","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?google\\.es","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?google\\.com\\.et","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?google\\.fi","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?google\\.com\\.fj","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?google\\.fm","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?google\\.fr","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?google\\.ga","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?google\\.ge","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?google\\.gg","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?google\\.com\\.gh","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?google\\.com\\.gi","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?google\\.gl","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?google\\.gm","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?google\\.gp","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?google\\.gr","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?google\\.com\\.gt","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?google\\.gy","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?google\\.com\\.hk","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?google\\.hn","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?google\\.hr","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?google\\.ht","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?google\\.hu","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?google\\.co\\.id","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?google\\.ie","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?google\\.co\\.il","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?google\\.im","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?google\\.co\\.in","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?google\\.iq","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?google\\.is","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?google\\.it","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?google\\.je","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?google\\.com\\.jm","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?google\\.jo","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?google\\.co\\.jp","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?google\\.co\\.ke","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?google\\.com\\.kh","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?google\\.ki","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?google\\.kg","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?google\\.co\\.kr","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?google\\.com\\.kw","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?google\\.kz","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?google\\.la","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?google\\.com\\.lb","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?google\\.li","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?google\\.lk","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?google\\.co\\.ls","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?google\\.lt","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?google\\.lu","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?google\\.lv","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?google\\.com\\.ly","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?google\\.co\\.ma","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?google\\.md","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?google\\.me","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?google\\.mg","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?google\\.mk","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?google\\.ml","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?google\\.com\\.mm","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?google\\.mn","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?google\\.ms","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?google\\.com\\.mt","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?google\\.mu","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?google\\.mv","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?google\\.mw","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?google\\.com\\.mx","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?google\\.com\\.my","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?google\\.co\\.mz","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?google\\.com\\.na","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?google\\.com\\.nf","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?google\\.com\\.ng","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?google\\.com\\.ni","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?google\\.ne","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?google\\.nl","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?google\\.no","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?google\\.com\\.np","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?google\\.nr","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?google\\.nu","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?google\\.co\\.nz","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?google\\.com\\.om","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?google\\.com\\.pa","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?google\\.com\\.pe","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?google\\.com\\.pg","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?google\\.com\\.ph","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?google\\.com\\.pk","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?google\\.pl","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?google\\.pn","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?google\\.com\\.pr","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?google\\.ps","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?google\\.pt","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?google\\.com\\.py","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?google\\.com\\.qa","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?google\\.ro","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?google\\.ru","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?google\\.rw","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?google\\.com\\.sa","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?google\\.com\\.sb","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?google\\.sc","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?google\\.se","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?google\\.com\\.sg","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?google\\.sh","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?google\\.si","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?google\\.sk","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?google\\.com\\.sl","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?google\\.sn","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?google\\.so","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?google\\.sm","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?google\\.st","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?google\\.com\\.sv","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?google\\.td","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?google\\.tg","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?google\\.co\\.th","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?google\\.com\\.tj","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?google\\.tk","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?google\\.tl","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?google\\.tm","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?google\\.tn","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?google\\.to","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?google\\.com\\.tr","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?google\\.tt","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?google\\.com\\.tw","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?google\\.co\\.tz","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?google\\.com\\.ua","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?google\\.co\\.ug","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?google\\.co\\.uk","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?google\\.com\\.uy","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?google\\.co\\.uz","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?google\\.com\\.vc","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?google\\.co\\.ve","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?google\\.vg","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?google\\.co\\.vi","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?google\\.com\\.vn","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?google\\.vu","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?google\\.ws","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?google\\.rs","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?google\\.co\\.za","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?google\\.co\\.zm","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?google\\.co\\.zw","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?google\\.cat","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?googleapis\\.com","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?googleartproject\\.com","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?googleusercontent\\.com","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?gstatic\\.com","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?panoramio\\.com","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?postini\\.com","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?recaptcha\\.net","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?youtube\\.com","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?gravity\\.com","load-type":["third-party"],"unless-domain":["*gravity.com","*grvcdn.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?grvcdn\\.com","load-type":["third-party"],"unless-domain":["*gravity.com","*grvcdn.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?heyzap\\.com","load-type":["third-party"],"unless-domain":["*heyzap.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?hubspot\\.com","load-type":["third-party"],"unless-domain":["*hubspot.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?iesnare\\.com","load-type":["third-party"],"unless-domain":["*iovation.com","*iesnare.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?iovation\\.com","load-type":["third-party"],"unless-domain":["*iovation.com","*iesnare.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?kaltura\\.com","load-type":["third-party"],"unless-domain":["*kaltura.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?kikin\\.com","load-type":["third-party"],"unless-domain":["*kikin.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?clickability\\.com","load-type":["third-party"],"unless-domain":["*limelight.com","*clickability.com","*llnwd.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?limelight\\.com","load-type":["third-party"],"unless-domain":["*limelight.com","*clickability.com","*llnwd.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?llnwd\\.net","load-type":["third-party"],"unless-domain":["*limelight.com","*clickability.com","*llnwd.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?fyre\\.co","load-type":["third-party"],"unless-domain":["*livefyre.com","*fyre.co"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?livefyre\\.com","load-type":["third-party"],"unless-domain":["*livefyre.com","*fyre.co"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?liveperson\\.net","load-type":["third-party"],"unless-domain":["*liveperson.com","*nuconomy.com","*liveperson.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?liverail\\.com","load-type":["third-party"],"unless-domain":["*liverail.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?longtailvideo\\.com","load-type":["third-party"],"unless-domain":["*longtailvideo.com","*ltassrv.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?ltassrv\\.com","load-type":["third-party"],"unless-domain":["*longtailvideo.com","*ltassrv.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?luminate\\.com","load-type":["third-party"],"unless-domain":["*luminate.com","*pixazza.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?pixazza\\.com","load-type":["third-party"],"unless-domain":["*luminate.com","*pixazza.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?markit\\.com","load-type":["third-party"],"unless-domain":["*markit.com","*wsod.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?wsod\\.com","load-type":["third-party"],"unless-domain":["*markit.com","*wsod.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?mashlogic\\.com","load-type":["third-party"],"unless-domain":["*mashlogic.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?mcafee\\.com","load-type":["third-party"],"unless-domain":["*mcafee.com","*scanalert.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?scanalert\\.com","load-type":["third-party"],"unless-domain":["*mcafee.com","*scanalert.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?bing\\.com","load-type":["third-party"],"unless-domain":["*microsoft.com","*adbureau.net","*adecn.com","*aquantive.com","*atdmt.com","*msads.net","*netconversions.com","*roiservice.com","*bing.com","*gamesforwindows.com","*getgamesmart.com","*healthvault.com","*ieaddons.com","*iegallery.com","*live.com","*microsoftalumni.com","*microsoftalumni.org","*microsoftstore.com","*msn.com","*msnbc.com","*nbcnews.com","*office.com","*officelive.com","*outlook.com","*s-msn.com","*skype.com","*windowsphone.com","*worldwidetelescope.org","*xbox.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?gamesforwindows\\.com","load-type":["third-party"],"unless-domain":["*microsoft.com","*adbureau.net","*adecn.com","*aquantive.com","*atdmt.com","*msads.net","*netconversions.com","*roiservice.com","*bing.com","*gamesforwindows.com","*getgamesmart.com","*healthvault.com","*ieaddons.com","*iegallery.com","*live.com","*microsoftalumni.com","*microsoftalumni.org","*microsoftstore.com","*msn.com","*msnbc.com","*nbcnews.com","*office.com","*officelive.com","*outlook.com","*s-msn.com","*skype.com","*windowsphone.com","*worldwidetelescope.org","*xbox.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?getgamesmart\\.com","load-type":["third-party"],"unless-domain":["*microsoft.com","*adbureau.net","*adecn.com","*aquantive.com","*atdmt.com","*msads.net","*netconversions.com","*roiservice.com","*bing.com","*gamesforwindows.com","*getgamesmart.com","*healthvault.com","*ieaddons.com","*iegallery.com","*live.com","*microsoftalumni.com","*microsoftalumni.org","*microsoftstore.com","*msn.com","*msnbc.com","*nbcnews.com","*office.com","*officelive.com","*outlook.com","*s-msn.com","*skype.com","*windowsphone.com","*worldwidetelescope.org","*xbox.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?healthvault\\.com","load-type":["third-party"],"unless-domain":["*microsoft.com","*adbureau.net","*adecn.com","*aquantive.com","*atdmt.com","*msads.net","*netconversions.com","*roiservice.com","*bing.com","*gamesforwindows.com","*getgamesmart.com","*healthvault.com","*ieaddons.com","*iegallery.com","*live.com","*microsoftalumni.com","*microsoftalumni.org","*microsoftstore.com","*msn.com","*msnbc.com","*nbcnews.com","*office.com","*officelive.com","*outlook.com","*s-msn.com","*skype.com","*windowsphone.com","*worldwidetelescope.org","*xbox.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?ieaddons\\.com","load-type":["third-party"],"unless-domain":["*microsoft.com","*adbureau.net","*adecn.com","*aquantive.com","*atdmt.com","*msads.net","*netconversions.com","*roiservice.com","*bing.com","*gamesforwindows.com","*getgamesmart.com","*healthvault.com","*ieaddons.com","*iegallery.com","*live.com","*microsoftalumni.com","*microsoftalumni.org","*microsoftstore.com","*msn.com","*msnbc.com","*nbcnews.com","*office.com","*officelive.com","*outlook.com","*s-msn.com","*skype.com","*windowsphone.com","*worldwidetelescope.org","*xbox.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?iegallery\\.com","load-type":["third-party"],"unless-domain":["*microsoft.com","*adbureau.net","*adecn.com","*aquantive.com","*atdmt.com","*msads.net","*netconversions.com","*roiservice.com","*bing.com","*gamesforwindows.com","*getgamesmart.com","*healthvault.com","*ieaddons.com","*iegallery.com","*live.com","*microsoftalumni.com","*microsoftalumni.org","*microsoftstore.com","*msn.com","*msnbc.com","*nbcnews.com","*office.com","*officelive.com","*outlook.com","*s-msn.com","*skype.com","*windowsphone.com","*worldwidetelescope.org","*xbox.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?live\\.com","load-type":["third-party"],"unless-domain":["*microsoft.com","*adbureau.net","*adecn.com","*aquantive.com","*atdmt.com","*msads.net","*netconversions.com","*roiservice.com","*bing.com","*gamesforwindows.com","*getgamesmart.com","*healthvault.com","*ieaddons.com","*iegallery.com","*live.com","*microsoftalumni.com","*microsoftalumni.org","*microsoftstore.com","*msn.com","*msnbc.com","*nbcnews.com","*office.com","*officelive.com","*outlook.com","*s-msn.com","*skype.com","*windowsphone.com","*worldwidetelescope.org","*xbox.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?microsoft\\.com","load-type":["third-party"],"unless-domain":["*microsoft.com","*adbureau.net","*adecn.com","*aquantive.com","*atdmt.com","*msads.net","*netconversions.com","*roiservice.com","*bing.com","*gamesforwindows.com","*getgamesmart.com","*healthvault.com","*ieaddons.com","*iegallery.com","*live.com","*microsoftalumni.com","*microsoftalumni.org","*microsoftstore.com","*msn.com","*msnbc.com","*nbcnews.com","*office.com","*officelive.com","*outlook.com","*s-msn.com","*skype.com","*windowsphone.com","*worldwidetelescope.org","*xbox.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?microsoftalumni\\.com","load-type":["third-party"],"unless-domain":["*microsoft.com","*adbureau.net","*adecn.com","*aquantive.com","*atdmt.com","*msads.net","*netconversions.com","*roiservice.com","*bing.com","*gamesforwindows.com","*getgamesmart.com","*healthvault.com","*ieaddons.com","*iegallery.com","*live.com","*microsoftalumni.com","*microsoftalumni.org","*microsoftstore.com","*msn.com","*msnbc.com","*nbcnews.com","*office.com","*officelive.com","*outlook.com","*s-msn.com","*skype.com","*windowsphone.com","*worldwidetelescope.org","*xbox.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?microsoftalumni\\.org","load-type":["third-party"],"unless-domain":["*microsoft.com","*adbureau.net","*adecn.com","*aquantive.com","*atdmt.com","*msads.net","*netconversions.com","*roiservice.com","*bing.com","*gamesforwindows.com","*getgamesmart.com","*healthvault.com","*ieaddons.com","*iegallery.com","*live.com","*microsoftalumni.com","*microsoftalumni.org","*microsoftstore.com","*msn.com","*msnbc.com","*nbcnews.com","*office.com","*officelive.com","*outlook.com","*s-msn.com","*skype.com","*windowsphone.com","*worldwidetelescope.org","*xbox.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?microsoftstore\\.com","load-type":["third-party"],"unless-domain":["*microsoft.com","*adbureau.net","*adecn.com","*aquantive.com","*atdmt.com","*msads.net","*netconversions.com","*roiservice.com","*bing.com","*gamesforwindows.com","*getgamesmart.com","*healthvault.com","*ieaddons.com","*iegallery.com","*live.com","*microsoftalumni.com","*microsoftalumni.org","*microsoftstore.com","*msn.com","*msnbc.com","*nbcnews.com","*office.com","*officelive.com","*outlook.com","*s-msn.com","*skype.com","*windowsphone.com","*worldwidetelescope.org","*xbox.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?msn\\.com","load-type":["third-party"],"unless-domain":["*microsoft.com","*adbureau.net","*adecn.com","*aquantive.com","*atdmt.com","*msads.net","*netconversions.com","*roiservice.com","*bing.com","*gamesforwindows.com","*getgamesmart.com","*healthvault.com","*ieaddons.com","*iegallery.com","*live.com","*microsoftalumni.com","*microsoftalumni.org","*microsoftstore.com","*msn.com","*msnbc.com","*nbcnews.com","*office.com","*officelive.com","*outlook.com","*s-msn.com","*skype.com","*windowsphone.com","*worldwidetelescope.org","*xbox.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?msnbc\\.com","load-type":["third-party"],"unless-domain":["*microsoft.com","*adbureau.net","*adecn.com","*aquantive.com","*atdmt.com","*msads.net","*netconversions.com","*roiservice.com","*bing.com","*gamesforwindows.com","*getgamesmart.com","*healthvault.com","*ieaddons.com","*iegallery.com","*live.com","*microsoftalumni.com","*microsoftalumni.org","*microsoftstore.com","*msn.com","*msnbc.com","*nbcnews.com","*office.com","*officelive.com","*outlook.com","*s-msn.com","*skype.com","*windowsphone.com","*worldwidetelescope.org","*xbox.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?msndirect\\.com","load-type":["third-party"],"unless-domain":["*microsoft.com","*adbureau.net","*adecn.com","*aquantive.com","*atdmt.com","*msads.net","*netconversions.com","*roiservice.com","*bing.com","*gamesforwindows.com","*getgamesmart.com","*healthvault.com","*ieaddons.com","*iegallery.com","*live.com","*microsoftalumni.com","*microsoftalumni.org","*microsoftstore.com","*msn.com","*msnbc.com","*nbcnews.com","*office.com","*officelive.com","*outlook.com","*s-msn.com","*skype.com","*windowsphone.com","*worldwidetelescope.org","*xbox.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?nbcnews\\.com","load-type":["third-party"],"unless-domain":["*microsoft.com","*adbureau.net","*adecn.com","*aquantive.com","*atdmt.com","*msads.net","*netconversions.com","*roiservice.com","*bing.com","*gamesforwindows.com","*getgamesmart.com","*healthvault.com","*ieaddons.com","*iegallery.com","*live.com","*microsoftalumni.com","*microsoftalumni.org","*microsoftstore.com","*msn.com","*msnbc.com","*nbcnews.com","*office.com","*officelive.com","*outlook.com","*s-msn.com","*skype.com","*windowsphone.com","*worldwidetelescope.org","*xbox.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?office\\.com","load-type":["third-party"],"unless-domain":["*microsoft.com","*adbureau.net","*adecn.com","*aquantive.com","*atdmt.com","*msads.net","*netconversions.com","*roiservice.com","*bing.com","*gamesforwindows.com","*getgamesmart.com","*healthvault.com","*ieaddons.com","*iegallery.com","*live.com","*microsoftalumni.com","*microsoftalumni.org","*microsoftstore.com","*msn.com","*msnbc.com","*nbcnews.com","*office.com","*officelive.com","*outlook.com","*s-msn.com","*skype.com","*windowsphone.com","*worldwidetelescope.org","*xbox.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?officelive\\.com","load-type":["third-party"],"unless-domain":["*microsoft.com","*adbureau.net","*adecn.com","*aquantive.com","*atdmt.com","*msads.net","*netconversions.com","*roiservice.com","*bing.com","*gamesforwindows.com","*getgamesmart.com","*healthvault.com","*ieaddons.com","*iegallery.com","*live.com","*microsoftalumni.com","*microsoftalumni.org","*microsoftstore.com","*msn.com","*msnbc.com","*nbcnews.com","*office.com","*officelive.com","*outlook.com","*s-msn.com","*skype.com","*windowsphone.com","*worldwidetelescope.org","*xbox.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?outlook\\.com","load-type":["third-party"],"unless-domain":["*microsoft.com","*adbureau.net","*adecn.com","*aquantive.com","*atdmt.com","*msads.net","*netconversions.com","*roiservice.com","*bing.com","*gamesforwindows.com","*getgamesmart.com","*healthvault.com","*ieaddons.com","*iegallery.com","*live.com","*microsoftalumni.com","*microsoftalumni.org","*microsoftstore.com","*msn.com","*msnbc.com","*nbcnews.com","*office.com","*officelive.com","*outlook.com","*s-msn.com","*skype.com","*windowsphone.com","*worldwidetelescope.org","*xbox.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?s-msn\\.com","load-type":["third-party"],"unless-domain":["*microsoft.com","*adbureau.net","*adecn.com","*aquantive.com","*atdmt.com","*msads.net","*netconversions.com","*roiservice.com","*bing.com","*gamesforwindows.com","*getgamesmart.com","*healthvault.com","*ieaddons.com","*iegallery.com","*live.com","*microsoftalumni.com","*microsoftalumni.org","*microsoftstore.com","*msn.com","*msnbc.com","*nbcnews.com","*office.com","*officelive.com","*outlook.com","*s-msn.com","*skype.com","*windowsphone.com","*worldwidetelescope.org","*xbox.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?skype\\.com","load-type":["third-party"],"unless-domain":["*microsoft.com","*adbureau.net","*adecn.com","*aquantive.com","*atdmt.com","*msads.net","*netconversions.com","*roiservice.com","*bing.com","*gamesforwindows.com","*getgamesmart.com","*healthvault.com","*ieaddons.com","*iegallery.com","*live.com","*microsoftalumni.com","*microsoftalumni.org","*microsoftstore.com","*msn.com","*msnbc.com","*nbcnews.com","*office.com","*officelive.com","*outlook.com","*s-msn.com","*skype.com","*windowsphone.com","*worldwidetelescope.org","*xbox.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?windowsphone\\.com","load-type":["third-party"],"unless-domain":["*microsoft.com","*adbureau.net","*adecn.com","*aquantive.com","*atdmt.com","*msads.net","*netconversions.com","*roiservice.com","*bing.com","*gamesforwindows.com","*getgamesmart.com","*healthvault.com","*ieaddons.com","*iegallery.com","*live.com","*microsoftalumni.com","*microsoftalumni.org","*microsoftstore.com","*msn.com","*msnbc.com","*nbcnews.com","*office.com","*officelive.com","*outlook.com","*s-msn.com","*skype.com","*windowsphone.com","*worldwidetelescope.org","*xbox.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?worldwidetelescope\\.org","load-type":["third-party"],"unless-domain":["*microsoft.com","*adbureau.net","*adecn.com","*aquantive.com","*atdmt.com","*msads.net","*netconversions.com","*roiservice.com","*bing.com","*gamesforwindows.com","*getgamesmart.com","*healthvault.com","*ieaddons.com","*iegallery.com","*live.com","*microsoftalumni.com","*microsoftalumni.org","*microsoftstore.com","*msn.com","*msnbc.com","*nbcnews.com","*office.com","*officelive.com","*outlook.com","*s-msn.com","*skype.com","*windowsphone.com","*worldwidetelescope.org","*xbox.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?xbox\\.com","load-type":["third-party"],"unless-domain":["*microsoft.com","*adbureau.net","*adecn.com","*aquantive.com","*atdmt.com","*msads.net","*netconversions.com","*roiservice.com","*bing.com","*gamesforwindows.com","*getgamesmart.com","*healthvault.com","*ieaddons.com","*iegallery.com","*live.com","*microsoftalumni.com","*microsoftalumni.org","*microsoftstore.com","*msn.com","*msnbc.com","*nbcnews.com","*office.com","*officelive.com","*outlook.com","*s-msn.com","*skype.com","*windowsphone.com","*worldwidetelescope.org","*xbox.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?zune\\.com","load-type":["third-party"],"unless-domain":["*microsoft.com","*adbureau.net","*adecn.com","*aquantive.com","*atdmt.com","*msads.net","*netconversions.com","*roiservice.com","*bing.com","*gamesforwindows.com","*getgamesmart.com","*healthvault.com","*ieaddons.com","*iegallery.com","*live.com","*microsoftalumni.com","*microsoftalumni.org","*microsoftstore.com","*msn.com","*msnbc.com","*nbcnews.com","*office.com","*officelive.com","*outlook.com","*s-msn.com","*skype.com","*windowsphone.com","*worldwidetelescope.org","*xbox.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?zune\\.net","load-type":["third-party"],"unless-domain":["*microsoft.com","*adbureau.net","*adecn.com","*aquantive.com","*atdmt.com","*msads.net","*netconversions.com","*roiservice.com","*bing.com","*gamesforwindows.com","*getgamesmart.com","*healthvault.com","*ieaddons.com","*iegallery.com","*live.com","*microsoftalumni.com","*microsoftalumni.org","*microsoftstore.com","*msn.com","*msnbc.com","*nbcnews.com","*office.com","*officelive.com","*outlook.com","*s-msn.com","*skype.com","*windowsphone.com","*worldwidetelescope.org","*xbox.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?newsinc\\.com","load-type":["third-party"],"unless-domain":["*newsinc.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?blaze\\.com","load-type":["third-party"],"unless-domain":["*oberon-media.com","*blaze.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?oberon-media\\.com","load-type":["third-party"],"unless-domain":["*oberon-media.com","*blaze.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?olark\\.com","load-type":["third-party"],"unless-domain":["*olark.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?oo4\\.com","load-type":["third-party"],"unless-domain":["*ooyala.com","*oo4.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?ooyala\\.com","load-type":["third-party"],"unless-domain":["*ooyala.com","*oo4.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?optimizely\\.com","load-type":["third-party"],"unless-domain":["*optimizely.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?atgsvcs\\.com","load-type":["third-party"],"unless-domain":["*oracle.com","*estara.com","*atgsvcs.com","*instantservice.com","*istrack.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?instantservice\\.com","load-type":["third-party"],"unless-domain":["*oracle.com","*estara.com","*atgsvcs.com","*instantservice.com","*istrack.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?istrack\\.com","load-type":["third-party"],"unless-domain":["*oracle.com","*estara.com","*atgsvcs.com","*instantservice.com","*istrack.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?oracle\\.com","load-type":["third-party"],"unless-domain":["*oracle.com","*estara.com","*atgsvcs.com","*instantservice.com","*istrack.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?parsely\\.com","load-type":["third-party"],"unless-domain":["*parsely.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?peerius\\.com","load-type":["third-party"],"unless-domain":["*peerius.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?pinterest\\.com","load-type":["third-party"],"unless-domain":["*pinterest.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?punchtab\\.com","load-type":["third-party"],"unless-domain":["*punchtab.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?rim\\.com","load-type":["third-party"],"unless-domain":["*rim.com","*scoreloop.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?scoreloop\\.com","load-type":["third-party"],"unless-domain":["*rim.com","*scoreloop.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?saymedia\\.com","load-type":["third-party"],"unless-domain":["*saymedia.com","*typepad.com","*videoegg.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?typepad\\.com","load-type":["third-party"],"unless-domain":["*saymedia.com","*typepad.com","*videoegg.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?videoegg\\.com","load-type":["third-party"],"unless-domain":["*saymedia.com","*typepad.com","*videoegg.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?scribefire\\.com","load-type":["third-party"],"unless-domain":["*scribefire.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?sixapart\\.com","load-type":["third-party"],"unless-domain":["*sixapart.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?skribit\\.com","load-type":["third-party"],"unless-domain":["*skribit.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?snapengage\\.com","load-type":["third-party"],"unless-domain":["*snapengage.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?springmetrics\\.com","load-type":["third-party"],"unless-domain":["*springmetrics.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?superfish\\.com","load-type":["third-party"],"unless-domain":["*superfish.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?synacor\\.com","load-type":["third-party"],"unless-domain":["*synacor.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?thinglink\\.com","load-type":["third-party"],"unless-domain":["*thinglink.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?thismoment\\.com","load-type":["third-party"],"unless-domain":["*thismoment.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?thummit\\.com","load-type":["third-party"],"unless-domain":["*thummit.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?topsy\\.com","load-type":["third-party"],"unless-domain":["*topsy.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?tracemyip\\.org","load-type":["third-party"],"unless-domain":["*tracemyip.org"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?trackset\\.com","load-type":["third-party"],"unless-domain":["*trackset.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?trovus\\.co\\.uk","load-type":["third-party"],"unless-domain":["*www.trovus.co.uk","*trovus.co.uk"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?trumba\\.com","load-type":["third-party"],"unless-domain":["*trumba.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?tumblr\\.com","load-type":["third-party"],"unless-domain":["*tumblr.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?turn\\.com","load-type":["third-party"],"unless-domain":["*turn.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?turnto\\.com","load-type":["third-party"],"unless-domain":["*turntonetworks.com","*turnto.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?turntonetworks\\.com","load-type":["third-party"],"unless-domain":["*turntonetworks.com","*turnto.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?tweetboard\\.com","load-type":["third-party"],"unless-domain":["*tweetboard.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?truste\\.com","load-type":["third-party"],"unless-domain":["*truste.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?twittercounter\\.com","load-type":["third-party"],"unless-domain":["*twittercounter.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?ubermedia\\.com","load-type":["third-party"],"unless-domain":["*ubermedia.com","*tweetup.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?tweetup\\.com","load-type":["third-party"],"unless-domain":["*ubermedia.com","*tweetup.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?ubertags\\.com","load-type":["third-party"],"unless-domain":["*ubertags.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?unbounce\\.com","load-type":["third-party"],"unless-domain":["*unbounce.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?uptrends\\.com","load-type":["third-party"],"unless-domain":["*uptrends.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?usabilitysciences\\.com","load-type":["third-party"],"unless-domain":["*usabilitysciences.com","*webiqonline.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?webiqonline\\.com","load-type":["third-party"],"unless-domain":["*usabilitysciences.com","*webiqonline.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?uservoice\\.com","load-type":["third-party"],"unless-domain":["*uservoice.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?verticalacuity\\.com","load-type":["third-party"],"unless-domain":["*verticalacuity.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?vgwort\\.de","load-type":["third-party"],"unless-domain":["*vgwort.de"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?tidaltv\\.com","load-type":["third-party"],"unless-domain":["*videologygroup.com","*tidaltv.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?videologygroup\\.com","load-type":["third-party"],"unless-domain":["*videologygroup.com","*tidaltv.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?qoof\\.com","load-type":["third-party"],"unless-domain":["*viewbix.com","*qoof.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?viewbix\\.com","load-type":["third-party"],"unless-domain":["*viewbix.com","*qoof.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?vimeo\\.com","load-type":["third-party"],"unless-domain":["*vimeo.com","*vimeocdn.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?vimeocdn\\.com","load-type":["third-party"],"unless-domain":["*vimeo.com","*vimeocdn.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?vindicogroup\\.com","load-type":["third-party"],"unless-domain":["*vindicogroup.com","*vindicosuite.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?vindicosuite\\.com","load-type":["third-party"],"unless-domain":["*vindicogroup.com","*vindicosuite.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?visualrevenue\\.com","load-type":["third-party"],"unless-domain":["*visualrevenue.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?voice2page\\.com","load-type":["third-party"],"unless-domain":["*voice2page.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?websitealive\\.com","load-type":["third-party"],"unless-domain":["*websitealive.com","*websitealive0.com","*websitealive1.com","*websitealive2.com","*websitealive3.com","*websitealive4.com","*websitealive5.com","*websitealive6.com","*websitealive7.com","*websitealive8.com","*websitealive9.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?websitealive0\\.com","load-type":["third-party"],"unless-domain":["*websitealive.com","*websitealive0.com","*websitealive1.com","*websitealive2.com","*websitealive3.com","*websitealive4.com","*websitealive5.com","*websitealive6.com","*websitealive7.com","*websitealive8.com","*websitealive9.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?websitealive1\\.com","load-type":["third-party"],"unless-domain":["*websitealive.com","*websitealive0.com","*websitealive1.com","*websitealive2.com","*websitealive3.com","*websitealive4.com","*websitealive5.com","*websitealive6.com","*websitealive7.com","*websitealive8.com","*websitealive9.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?websitealive2\\.com","load-type":["third-party"],"unless-domain":["*websitealive.com","*websitealive0.com","*websitealive1.com","*websitealive2.com","*websitealive3.com","*websitealive4.com","*websitealive5.com","*websitealive6.com","*websitealive7.com","*websitealive8.com","*websitealive9.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?websitealive3\\.com","load-type":["third-party"],"unless-domain":["*websitealive.com","*websitealive0.com","*websitealive1.com","*websitealive2.com","*websitealive3.com","*websitealive4.com","*websitealive5.com","*websitealive6.com","*websitealive7.com","*websitealive8.com","*websitealive9.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?websitealive4\\.com","load-type":["third-party"],"unless-domain":["*websitealive.com","*websitealive0.com","*websitealive1.com","*websitealive2.com","*websitealive3.com","*websitealive4.com","*websitealive5.com","*websitealive6.com","*websitealive7.com","*websitealive8.com","*websitealive9.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?websitealive5\\.com","load-type":["third-party"],"unless-domain":["*websitealive.com","*websitealive0.com","*websitealive1.com","*websitealive2.com","*websitealive3.com","*websitealive4.com","*websitealive5.com","*websitealive6.com","*websitealive7.com","*websitealive8.com","*websitealive9.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?websitealive6\\.com","load-type":["third-party"],"unless-domain":["*websitealive.com","*websitealive0.com","*websitealive1.com","*websitealive2.com","*websitealive3.com","*websitealive4.com","*websitealive5.com","*websitealive6.com","*websitealive7.com","*websitealive8.com","*websitealive9.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?websitealive7\\.com","load-type":["third-party"],"unless-domain":["*websitealive.com","*websitealive0.com","*websitealive1.com","*websitealive2.com","*websitealive3.com","*websitealive4.com","*websitealive5.com","*websitealive6.com","*websitealive7.com","*websitealive8.com","*websitealive9.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?websitealive8\\.com","load-type":["third-party"],"unless-domain":["*websitealive.com","*websitealive0.com","*websitealive1.com","*websitealive2.com","*websitealive3.com","*websitealive4.com","*websitealive5.com","*websitealive6.com","*websitealive7.com","*websitealive8.com","*websitealive9.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?websitealive9\\.com","load-type":["third-party"],"unless-domain":["*websitealive.com","*websitealive0.com","*websitealive1.com","*websitealive2.com","*websitealive3.com","*websitealive4.com","*websitealive5.com","*websitealive6.com","*websitealive7.com","*websitealive8.com","*websitealive9.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?visualwebsiteoptimizer\\.com","load-type":["third-party"],"unless-domain":["*wingify.com","*visualwebsiteoptimizer.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?wingify\\.com","load-type":["third-party"],"unless-domain":["*wingify.com","*visualwebsiteoptimizer.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?xtify\\.com","load-type":["third-party"],"unless-domain":["*xtify.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?flickr\\.com","load-type":["third-party"],"unless-domain":["*flickr.com","*yuilibrary.com","*tumblr.com","*yahoo.com","*yahoostudios.com","*yahoo.co.jp"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?staticflickr\\.com","load-type":["third-party"],"unless-domain":["*flickr.com","*yuilibrary.com","*tumblr.com","*yahoo.com","*yahoostudios.com","*yahoo.co.jp"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?answers\\.yahoo\\.com","load-type":["third-party"],"unless-domain":["*flickr.com","*yuilibrary.com","*tumblr.com","*yahoo.com","*yahoostudios.com","*yahoo.co.jp"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?apps\\.yahoo\\.com","load-type":["third-party"],"unless-domain":["*flickr.com","*yuilibrary.com","*tumblr.com","*yahoo.com","*yahoostudios.com","*yahoo.co.jp"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?autos\\.yahoo\\.com","load-type":["third-party"],"unless-domain":["*flickr.com","*yuilibrary.com","*tumblr.com","*yahoo.com","*yahoostudios.com","*yahoo.co.jp"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?biz\\.yahoo\\.com","load-type":["third-party"],"unless-domain":["*flickr.com","*yuilibrary.com","*tumblr.com","*yahoo.com","*yahoostudios.com","*yahoo.co.jp"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?developer\\.yahoo\\.com","load-type":["third-party"],"unless-domain":["*flickr.com","*yuilibrary.com","*tumblr.com","*yahoo.com","*yahoostudios.com","*yahoo.co.jp"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?everything\\.yahoo\\.com","load-type":["third-party"],"unless-domain":["*flickr.com","*yuilibrary.com","*tumblr.com","*yahoo.com","*yahoostudios.com","*yahoo.co.jp"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?finance\\.yahoo\\.com","load-type":["third-party"],"unless-domain":["*flickr.com","*yuilibrary.com","*tumblr.com","*yahoo.com","*yahoostudios.com","*yahoo.co.jp"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?games\\.yahoo\\.com","load-type":["third-party"],"unless-domain":["*flickr.com","*yuilibrary.com","*tumblr.com","*yahoo.com","*yahoostudios.com","*yahoo.co.jp"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?groups\\.yahoo\\.com","load-type":["third-party"],"unless-domain":["*flickr.com","*yuilibrary.com","*tumblr.com","*yahoo.com","*yahoostudios.com","*yahoo.co.jp"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?help\\.yahoo\\.com","load-type":["third-party"],"unless-domain":["*flickr.com","*yuilibrary.com","*tumblr.com","*yahoo.com","*yahoostudios.com","*yahoo.co.jp"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?hotjobs\\.yahoo\\.com","load-type":["third-party"],"unless-domain":["*flickr.com","*yuilibrary.com","*tumblr.com","*yahoo.com","*yahoostudios.com","*yahoo.co.jp"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?info\\.yahoo\\.com","load-type":["third-party"],"unless-domain":["*flickr.com","*yuilibrary.com","*tumblr.com","*yahoo.com","*yahoostudios.com","*yahoo.co.jp"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?local\\.yahoo\\.com","load-type":["third-party"],"unless-domain":["*flickr.com","*yuilibrary.com","*tumblr.com","*yahoo.com","*yahoostudios.com","*yahoo.co.jp"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?messages\\.yahoo\\.com","load-type":["third-party"],"unless-domain":["*flickr.com","*yuilibrary.com","*tumblr.com","*yahoo.com","*yahoostudios.com","*yahoo.co.jp"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?movies\\.yahoo\\.com","load-type":["third-party"],"unless-domain":["*flickr.com","*yuilibrary.com","*tumblr.com","*yahoo.com","*yahoostudios.com","*yahoo.co.jp"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?msg\\.yahoo\\.com","load-type":["third-party"],"unless-domain":["*flickr.com","*yuilibrary.com","*tumblr.com","*yahoo.com","*yahoostudios.com","*yahoo.co.jp"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?news\\.yahoo\\.com","load-type":["third-party"],"unless-domain":["*flickr.com","*yuilibrary.com","*tumblr.com","*yahoo.com","*yahoostudios.com","*yahoo.co.jp"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?omg\\.yahoo\\.com","load-type":["third-party"],"unless-domain":["*flickr.com","*yuilibrary.com","*tumblr.com","*yahoo.com","*yahoostudios.com","*yahoo.co.jp"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?pipes\\.yahoo\\.com","load-type":["third-party"],"unless-domain":["*flickr.com","*yuilibrary.com","*tumblr.com","*yahoo.com","*yahoostudios.com","*yahoo.co.jp"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?realestate\\.yahoo\\.com","load-type":["third-party"],"unless-domain":["*flickr.com","*yuilibrary.com","*tumblr.com","*yahoo.com","*yahoostudios.com","*yahoo.co.jp"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?search\\.yahoo\\.com","load-type":["third-party"],"unless-domain":["*flickr.com","*yuilibrary.com","*tumblr.com","*yahoo.com","*yahoostudios.com","*yahoo.co.jp"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?shine\\.yahoo\\.com","load-type":["third-party"],"unless-domain":["*flickr.com","*yuilibrary.com","*tumblr.com","*yahoo.com","*yahoostudios.com","*yahoo.co.jp"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?smallbusiness\\.yahoo\\.com","load-type":["third-party"],"unless-domain":["*flickr.com","*yuilibrary.com","*tumblr.com","*yahoo.com","*yahoostudios.com","*yahoo.co.jp"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?sports\\.yahoo\\.com","load-type":["third-party"],"unless-domain":["*flickr.com","*yuilibrary.com","*tumblr.com","*yahoo.com","*yahoostudios.com","*yahoo.co.jp"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?suggestions\\.yahoo\\.com","load-type":["third-party"],"unless-domain":["*flickr.com","*yuilibrary.com","*tumblr.com","*yahoo.com","*yahoostudios.com","*yahoo.co.jp"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?travel\\.yahoo\\.com","load-type":["third-party"],"unless-domain":["*flickr.com","*yuilibrary.com","*tumblr.com","*yahoo.com","*yahoostudios.com","*yahoo.co.jp"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?upcoming\\.yahoo\\.com","load-type":["third-party"],"unless-domain":["*flickr.com","*yuilibrary.com","*tumblr.com","*yahoo.com","*yahoostudios.com","*yahoo.co.jp"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?webhosting\\.yahoo\\.com","load-type":["third-party"],"unless-domain":["*flickr.com","*yuilibrary.com","*tumblr.com","*yahoo.com","*yahoostudios.com","*yahoo.co.jp"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?widgets\\.yahoo\\.com","load-type":["third-party"],"unless-domain":["*flickr.com","*yuilibrary.com","*tumblr.com","*yahoo.com","*yahoostudios.com","*yahoo.co.jp"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?www\\.yahoo\\.com","load-type":["third-party"],"unless-domain":["*flickr.com","*yuilibrary.com","*tumblr.com","*yahoo.com","*yahoostudios.com","*yahoo.co.jp"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?yahooapis\\.com","load-type":["third-party"],"unless-domain":["*flickr.com","*yuilibrary.com","*tumblr.com","*yahoo.com","*yahoostudios.com","*yahoo.co.jp"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?yahoofs\\.com","load-type":["third-party"],"unless-domain":["*flickr.com","*yuilibrary.com","*tumblr.com","*yahoo.com","*yahoostudios.com","*yahoo.co.jp"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?yimg\\.com","load-type":["third-party"],"unless-domain":["*flickr.com","*yuilibrary.com","*tumblr.com","*yahoo.com","*yahoostudios.com","*yahoo.co.jp"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?ypolicyblog\\.com","load-type":["third-party"],"unless-domain":["*flickr.com","*yuilibrary.com","*tumblr.com","*yahoo.com","*yahoostudios.com","*yahoo.co.jp"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?yuilibrary\\.com","load-type":["third-party"],"unless-domain":["*flickr.com","*yuilibrary.com","*tumblr.com","*yahoo.com","*yahoostudios.com","*yahoo.co.jp"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?zenfs\\.com","load-type":["third-party"],"unless-domain":["*flickr.com","*yuilibrary.com","*tumblr.com","*yahoo.com","*yahoostudios.com","*yahoo.co.jp"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?yandex\\.com","load-type":["third-party"],"unless-domain":["*yandex.com","*web-visor.com","*moikrug.ru","*yandex.ru","*yandex.st","*yandex.ua","*yandex.com.tr","*yandex.by"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?web-visor\\.com","load-type":["third-party"],"unless-domain":["*yandex.com","*web-visor.com","*moikrug.ru","*yandex.ru","*yandex.st","*yandex.ua","*yandex.com.tr","*yandex.by"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?moikrug\\.ru","load-type":["third-party"],"unless-domain":["*yandex.com","*web-visor.com","*moikrug.ru","*yandex.ru","*yandex.st","*yandex.ua","*yandex.com.tr","*yandex.by"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?yandex\\.ru","load-type":["third-party"],"unless-domain":["*yandex.com","*web-visor.com","*moikrug.ru","*yandex.ru","*yandex.st","*yandex.ua","*yandex.com.tr","*yandex.by"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?yandex\\.st","load-type":["third-party"],"unless-domain":["*yandex.com","*web-visor.com","*moikrug.ru","*yandex.ru","*yandex.st","*yandex.ua","*yandex.com.tr","*yandex.by"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?yandex\\.ua","load-type":["third-party"],"unless-domain":["*yandex.com","*web-visor.com","*moikrug.ru","*yandex.ru","*yandex.st","*yandex.ua","*yandex.com.tr","*yandex.by"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?yandex\\.com\\.tr","load-type":["third-party"],"unless-domain":["*yandex.com","*web-visor.com","*moikrug.ru","*yandex.ru","*yandex.st","*yandex.ua","*yandex.com.tr","*yandex.by"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?yandex\\.by","load-type":["third-party"],"unless-domain":["*yandex.com","*web-visor.com","*moikrug.ru","*yandex.ru","*yandex.st","*yandex.ua","*yandex.com.tr","*yandex.by"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?zendesk\\.com","load-type":["third-party"],"unless-domain":["*zendesk.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?zopim\\.com","load-type":["third-party"],"unless-domain":["*zopim.com"]}}] \ No newline at end of file diff --git a/mobile/ios/Client/Frontend/ContentBlocker/disconnect-social.json b/mobile/ios/Client/Frontend/ContentBlocker/disconnect-social.json new file mode 100644 index 0000000000..ce332270bd --- /dev/null +++ b/mobile/ios/Client/Frontend/ContentBlocker/disconnect-social.json @@ -0,0 +1 @@ +[{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?addthis\\.com","load-type":["third-party"],"unless-domain":["*addthis.com","*addthiscdn.com","*addthisedge.com","*clearspring.com","*connectedads.net","*xgraph.com","*xgraph.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?addthiscdn\\.com","load-type":["third-party"],"unless-domain":["*addthis.com","*addthiscdn.com","*addthisedge.com","*clearspring.com","*connectedads.net","*xgraph.com","*xgraph.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?addthisedge\\.com","load-type":["third-party"],"unless-domain":["*addthis.com","*addthiscdn.com","*addthisedge.com","*clearspring.com","*connectedads.net","*xgraph.com","*xgraph.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?clearspring\\.com","load-type":["third-party"],"unless-domain":["*addthis.com","*addthiscdn.com","*addthisedge.com","*clearspring.com","*connectedads.net","*xgraph.com","*xgraph.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?connectedads\\.net","load-type":["third-party"],"unless-domain":["*addthis.com","*addthiscdn.com","*addthisedge.com","*clearspring.com","*connectedads.net","*xgraph.com","*xgraph.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?xgraph\\.com","load-type":["third-party"],"unless-domain":["*addthis.com","*addthiscdn.com","*addthisedge.com","*clearspring.com","*connectedads.net","*xgraph.com","*xgraph.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?xgraph\\.net","load-type":["third-party"],"unless-domain":["*addthis.com","*addthiscdn.com","*addthisedge.com","*clearspring.com","*connectedads.net","*xgraph.com","*xgraph.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?causes\\.com","load-type":["third-party"],"unless-domain":["*causes.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?digg\\.com","load-type":["third-party"],"unless-domain":["*digg.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?facebook\\.com","load-type":["third-party"],"unless-domain":["*facebook.com","*facebook.de","*facebook.fr","*facebook.net","*fb.com","*fb.me","*friendfeed.com","*instagram.com","*fbcdn.net","*messenger.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?facebook\\.de","load-type":["third-party"],"unless-domain":["*facebook.com","*facebook.de","*facebook.fr","*facebook.net","*fb.com","*fb.me","*friendfeed.com","*instagram.com","*fbcdn.net","*messenger.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?facebook\\.fr","load-type":["third-party"],"unless-domain":["*facebook.com","*facebook.de","*facebook.fr","*facebook.net","*fb.com","*fb.me","*friendfeed.com","*instagram.com","*fbcdn.net","*messenger.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?facebook\\.net","load-type":["third-party"],"unless-domain":["*facebook.com","*facebook.de","*facebook.fr","*facebook.net","*fb.com","*fb.me","*friendfeed.com","*instagram.com","*fbcdn.net","*messenger.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?fb\\.com","load-type":["third-party"],"unless-domain":["*facebook.com","*facebook.de","*facebook.fr","*facebook.net","*fb.com","*fb.me","*friendfeed.com","*instagram.com","*fbcdn.net","*messenger.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?atlassolutions\\.com","load-type":["third-party"],"unless-domain":["*facebook.com","*facebook.de","*facebook.fr","*facebook.net","*fb.com","*fb.me","*friendfeed.com","*instagram.com","*fbcdn.net","*messenger.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?friendfeed\\.com","load-type":["third-party"],"unless-domain":["*facebook.com","*facebook.de","*facebook.fr","*facebook.net","*fb.com","*fb.me","*friendfeed.com","*instagram.com","*fbcdn.net","*messenger.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?developers\\.google\\.com","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?gmail\\.com","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?mail\\.google\\.com","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?inbox\\.google\\.com","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?orkut\\.com","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?plus\\.google\\.com","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?plusone\\.google\\.com","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?voice\\.google\\.com","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?wave\\.google\\.com","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?googlemail\\.com","load-type":["third-party"],"unless-domain":["*abc.xyz","*google.com","*ingress.com","*admeld.com","*blogger.com","*google-melange.com","*google.ad","*google.ae","*google.com.af","*google.com.ag","*google.com.ai","*google.al","*google.am","*google.co.ao","*google.com.ar","*google.as","*google.at","*google.com.au","*google.az","*google.ba","*google.com.bd","*google.be","*google.bf","*google.bg","*google.com.bh","*google.bi","*google.bj","*google.com.bn","*google.com.bo","*google.com.br","*google.bs","*google.bt","*google.co.bw","*google.by","*google.com.bz","*google.ca","*google.cd","*google.cf","*google.cg","*google.ch","*google.ci","*google.co.ck","*google.cl","*google.cm","*google.cn","*google.com.co","*google.co.cr","*google.com.cu","*google.cv","*google.com.cy","*google.cz","*google.de","*google.dj","*google.dk","*google.dm","*google.com.do","*google.dz","*google.com.ec","*google.ee","*google.com.eg","*google.es","*google.com.et","*google.fi","*google.com.fj","*google.fm","*google.fr","*google.ga","*google.ge","*google.gg","*google.com.gh","*google.com.gi","*google.gl","*google.gm","*google.gp","*google.gr","*google.com.gt","*google.gy","*google.com.hk","*google.hn","*google.hr","*google.ht","*google.hu","*google.co.id","*google.ie","*google.co.il","*google.im","*google.co.in","*google.iq","*google.is","*google.it","*google.je","*google.com.jm","*google.jo","*google.co.jp","*google.co.ke","*google.com.kh","*google.ki","*google.kg","*google.co.kr","*google.com.kw","*google.kz","*google.la","*google.com.lb","*google.li","*google.lk","*google.co.ls","*google.lt","*google.lu","*google.lv","*google.com.ly","*google.co.ma","*google.md","*google.me","*google.mg","*google.mk","*google.ml","*google.com.mm","*google.mn","*google.ms","*google.com.mt","*google.mu","*google.mv","*google.mw","*google.com.mx","*google.com.my","*google.co.mz","*google.com.na","*google.com.nf","*google.com.ng","*google.com.ni","*google.ne","*google.nl","*google.no","*google.com.np","*google.nr","*google.nu","*google.co.nz","*google.com.om","*google.com.pa","*google.com.pe","*google.com.pg","*google.com.ph","*google.com.pk","*google.pl","*google.pn","*google.com.pr","*google.ps","*google.pt","*google.com.py","*google.com.qa","*google.ro","*google.ru","*google.rw","*google.com.sa","*google.com.sb","*google.sc","*google.se","*google.com.sg","*google.sh","*google.si","*google.sk","*google.com.sl","*google.sn","*google.so","*google.sm","*google.st","*google.com.sv","*google.td","*google.tg","*google.co.th","*google.com.tj","*google.tk","*google.tl","*google.tm","*google.tn","*google.to","*google.com.tr","*google.tt","*google.com.tw","*google.co.tz","*google.com.ua","*google.co.ug","*google.co.uk","*google.com.uy","*google.co.uz","*google.com.vc","*google.co.ve","*google.vg","*google.co.vi","*google.com.vn","*google.vu","*google.ws","*google.rs","*google.co.za","*google.co.zm","*google.co.zw","*google.cat","*panoramio.com","*youtube.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?licdn\\.com","load-type":["third-party"],"unless-domain":["*linkedin.com","*licdn.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?linkedin\\.com","load-type":["third-party"],"unless-domain":["*linkedin.com","*licdn.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?addtoany\\.com","load-type":["third-party"],"unless-domain":["*lockerz.com","*addtoany.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?lockerz\\.com","load-type":["third-party"],"unless-domain":["*lockerz.com","*addtoany.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?list\\.ru","load-type":["third-party"],"unless-domain":["*mail.ru","*list.ru"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?mail\\.ru","load-type":["third-party"],"unless-domain":["*mail.ru","*list.ru"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?meebo\\.com","load-type":["third-party"],"unless-domain":["*meebo.com","*meebocdn.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?meebocdn\\.net","load-type":["third-party"],"unless-domain":["*meebo.com","*meebocdn.net"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?papayamobile\\.com","load-type":["third-party"],"unless-domain":["*papayamobile.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?sharethis\\.com","load-type":["third-party"],"unless-domain":["*sharethis.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?buzzster\\.com","load-type":["third-party"],"unless-domain":["*shareaholic.com","*buzzster.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?shareaholic\\.com","load-type":["third-party"],"unless-domain":["*shareaholic.com","*buzzster.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?stumble-upon\\.com","load-type":["third-party"],"unless-domain":["*stumbleupon.com","*stumble-upon.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?stumbleupon\\.com","load-type":["third-party"],"unless-domain":["*stumbleupon.com","*stumble-upon.com"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?backtype\\.com","load-type":["third-party"],"unless-domain":["*twitter.com","*backtype.com","*crashlytics.com","*tweetdeck.com","*twimg.com","*twitter.jp","*digits.com","*fabric.io"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?crashlytics\\.com","load-type":["third-party"],"unless-domain":["*twitter.com","*backtype.com","*crashlytics.com","*tweetdeck.com","*twimg.com","*twitter.jp","*digits.com","*fabric.io"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?tweetdeck\\.com","load-type":["third-party"],"unless-domain":["*twitter.com","*backtype.com","*crashlytics.com","*tweetdeck.com","*twimg.com","*twitter.jp","*digits.com","*fabric.io"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?twimg\\.com","load-type":["third-party"],"unless-domain":["*twitter.com","*backtype.com","*crashlytics.com","*tweetdeck.com","*twimg.com","*twitter.jp","*digits.com","*fabric.io"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?twitter\\.com","load-type":["third-party"],"unless-domain":["*twitter.com","*backtype.com","*crashlytics.com","*tweetdeck.com","*twimg.com","*twitter.jp","*digits.com","*fabric.io"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?twitter\\.jp","load-type":["third-party"],"unless-domain":["*twitter.com","*backtype.com","*crashlytics.com","*tweetdeck.com","*twimg.com","*twitter.jp","*digits.com","*fabric.io"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?userapi\\.com","load-type":["third-party"],"unless-domain":["*vk.com","*userapi.com","*vkontakte.ru"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?vk\\.com","load-type":["third-party"],"unless-domain":["*vk.com","*userapi.com","*vkontakte.ru"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?vkontakte\\.ru","load-type":["third-party"],"unless-domain":["*vk.com","*userapi.com","*vkontakte.ru"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?mybloglog\\.com","load-type":["third-party"],"unless-domain":["*flickr.com","*yuilibrary.com","*tumblr.com","*yahoo.com","*yahoostudios.com","*yahoo.co.jp"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?rocketmail\\.com","load-type":["third-party"],"unless-domain":["*flickr.com","*yuilibrary.com","*tumblr.com","*yahoo.com","*yahoostudios.com","*yahoo.co.jp"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?address\\.yahoo\\.com","load-type":["third-party"],"unless-domain":["*flickr.com","*yuilibrary.com","*tumblr.com","*yahoo.com","*yahoostudios.com","*yahoo.co.jp"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?alerts\\.yahoo\\.com","load-type":["third-party"],"unless-domain":["*flickr.com","*yuilibrary.com","*tumblr.com","*yahoo.com","*yahoostudios.com","*yahoo.co.jp"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?avatars\\.yahoo\\.com","load-type":["third-party"],"unless-domain":["*flickr.com","*yuilibrary.com","*tumblr.com","*yahoo.com","*yahoostudios.com","*yahoo.co.jp"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?buzz\\.yahoo\\.com","load-type":["third-party"],"unless-domain":["*flickr.com","*yuilibrary.com","*tumblr.com","*yahoo.com","*yahoostudios.com","*yahoo.co.jp"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?calendar\\.yahoo\\.com","load-type":["third-party"],"unless-domain":["*flickr.com","*yuilibrary.com","*tumblr.com","*yahoo.com","*yahoostudios.com","*yahoo.co.jp"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?edit\\.yahoo\\.com","load-type":["third-party"],"unless-domain":["*flickr.com","*yuilibrary.com","*tumblr.com","*yahoo.com","*yahoostudios.com","*yahoo.co.jp"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?legalredirect\\.yahoo\\.com","load-type":["third-party"],"unless-domain":["*flickr.com","*yuilibrary.com","*tumblr.com","*yahoo.com","*yahoostudios.com","*yahoo.co.jp"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?login\\.yahoo\\.com","load-type":["third-party"],"unless-domain":["*flickr.com","*yuilibrary.com","*tumblr.com","*yahoo.com","*yahoostudios.com","*yahoo.co.jp"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?mail\\.yahoo\\.com","load-type":["third-party"],"unless-domain":["*flickr.com","*yuilibrary.com","*tumblr.com","*yahoo.com","*yahoostudios.com","*yahoo.co.jp"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?my\\.yahoo\\.com","load-type":["third-party"],"unless-domain":["*flickr.com","*yuilibrary.com","*tumblr.com","*yahoo.com","*yahoostudios.com","*yahoo.co.jp"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?notepad\\.yahoo\\.com","load-type":["third-party"],"unless-domain":["*flickr.com","*yuilibrary.com","*tumblr.com","*yahoo.com","*yahoostudios.com","*yahoo.co.jp"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?pulse\\.yahoo\\.com","load-type":["third-party"],"unless-domain":["*flickr.com","*yuilibrary.com","*tumblr.com","*yahoo.com","*yahoostudios.com","*yahoo.co.jp"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?webmessenger\\.yahoo\\.com","load-type":["third-party"],"unless-domain":["*flickr.com","*yuilibrary.com","*tumblr.com","*yahoo.com","*yahoostudios.com","*yahoo.co.jp"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?ymail\\.com","load-type":["third-party"],"unless-domain":["*flickr.com","*yuilibrary.com","*tumblr.com","*yahoo.com","*yahoostudios.com","*yahoo.co.jp"]}},{"action":{"type":"block"},"trigger":{"url-filter":"^https?://([^/]+\\.)?reddit\\.com","load-type":["third-party"],"unless-domain":["*reddit.com"]}}] \ No newline at end of file diff --git a/mobile/ios/Client/Frontend/Home/ActivityStreamPanel.swift b/mobile/ios/Client/Frontend/Home/ActivityStreamPanel.swift new file mode 100644 index 0000000000..974fe1545f --- /dev/null +++ b/mobile/ios/Client/Frontend/Home/ActivityStreamPanel.swift @@ -0,0 +1,1073 @@ +/* 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 Shared +import UIKit +import Deferred +import Storage +import SDWebImage +import XCGLogger +import SyncTelemetry +import SnapKit + +private let log = Logger.browserLogger +private let DefaultSuggestedSitesKey = "topSites.deletedSuggestedSites" + +// MARK: - Lifecycle +struct ASPanelUX { + static let backgroundColor = UIConstants.AppBackgroundColor + static let rowSpacing: CGFloat = UIDevice.current.userInterfaceIdiom == .pad ? 30 : 20 + static let highlightCellHeight: CGFloat = UIDevice.current.userInterfaceIdiom == .pad ? 250 : 200 + static let sectionInsetsForSizeClass = UXSizeClasses(compact: 0, regular: 101, other: 14) + static let numberOfItemsPerRowForSizeClassIpad = UXSizeClasses(compact: 3, regular: 4, other: 2) + static let SectionInsetsForIpad: CGFloat = 101 + static let SectionInsetsForIphone: CGFloat = 14 + static let MinimumInsets: CGFloat = 14 + static let BookmarkHighlights = 2 + +} +/* + Size classes are the way Apple requires us to specify our UI. + Split view on iPad can make a landscape app appear with the demensions of an iPhone app + Use UXSizeClasses to specify things like offsets/itemsizes with respect to size classes + For a primer on size classes https://useyourloaf.com/blog/size-classes/ + */ +struct UXSizeClasses { + var compact: CGFloat + var regular: CGFloat + var unspecified: CGFloat + + init(compact: CGFloat, regular: CGFloat, other: CGFloat) { + self.compact = compact + self.regular = regular + self.unspecified = other + } + + subscript(sizeClass: UIUserInterfaceSizeClass) -> CGFloat { + switch sizeClass { + case .compact: + return self.compact + case .regular: + return self.regular + case .unspecified: + return self.unspecified + } + + } +} + +class ActivityStreamPanel: UICollectionViewController, HomePanel { + weak var homePanelDelegate: HomePanelDelegate? + fileprivate let profile: Profile + fileprivate let telemetry: ActivityStreamTracker + fileprivate let pocketAPI = Pocket() + fileprivate let flowLayout: UICollectionViewFlowLayout = UICollectionViewFlowLayout() + + fileprivate let topSitesManager = ASHorizontalScrollCellManager() + fileprivate var showHighlightIntro = false + fileprivate var sessionStart: Timestamp? + + fileprivate lazy var longPressRecognizer: UILongPressGestureRecognizer = { + return UILongPressGestureRecognizer(target: self, action: #selector(ActivityStreamPanel.longPress(_:))) + }() + + // Not used for displaying. Only used for calculating layout. + lazy var topSiteCell: ASHorizontalScrollCell = { + let customCell = ASHorizontalScrollCell(frame: CGRect(x: 0, y: 0, width: self.view.frame.size.width, height: 0)) + customCell.delegate = self.topSitesManager + return customCell + }() + + var highlights: [Site] = [] + var pocketStories: [PocketStory] = [] + + init(profile: Profile, telemetry: ActivityStreamTracker? = nil) { + self.profile = profile + self.telemetry = telemetry ?? ActivityStreamTracker(eventsTracker: PingCentre.clientForTopic(.ActivityStreamEvents, clientID: profile.clientID), sessionsTracker: PingCentre.clientForTopic(.ActivityStreamSessions, clientID: profile.clientID)) + + super.init(collectionViewLayout: flowLayout) + self.collectionView?.delegate = self + self.collectionView?.dataSource = self + + collectionView?.addGestureRecognizer(longPressRecognizer) + + NotificationCenter.default.addObserver(self, + selector: #selector(didChangeFontSize(notification:)), + name: NotificationDynamicFontChanged, + object: nil) + + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + + Section.allValues.forEach { self.collectionView?.register(Section($0.rawValue).cellType, forCellWithReuseIdentifier: Section($0.rawValue).cellIdentifier) } + self.collectionView?.register(ASHeaderView.self, forSupplementaryViewOfKind: UICollectionElementKindSectionHeader, withReuseIdentifier: "Header") + self.collectionView?.register(ASFooterView.self, forSupplementaryViewOfKind: UICollectionElementKindSectionFooter, withReuseIdentifier: "Footer") + collectionView?.backgroundColor = ASPanelUX.backgroundColor + collectionView?.keyboardDismissMode = .onDrag + + self.profile.panelDataObservers.activityStream.delegate = self + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + sessionStart = Date.now() + reloadAll() + } + + override func viewDidDisappear(_ animated: Bool) { + super.viewDidDisappear(animated) + telemetry.reportSessionStop(Date.now() - (sessionStart ?? 0)) + sessionStart = nil + } + + override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { + super.viewWillTransition(to: size, with: coordinator) + coordinator.animate(alongsideTransition: {context in + //The AS context menu does not behave correctly. Dismiss it when rotating. + if let _ = self.presentedViewController as? PhotonActionSheet { + self.presentedViewController?.dismiss(animated: true, completion: nil) + } + self.collectionViewLayout.invalidateLayout() + self.collectionView?.reloadData() + }, completion: nil) + } + + override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { + super.traitCollectionDidChange(previousTraitCollection) + self.topSitesManager.currentTraits = self.traitCollection + } + + func didChangeFontSize(notification: Notification) { + // Don't need to invalidate the data for a font change. Just reload the UI. + reloadAll() + } +} + +// MARK: - Section management +extension ActivityStreamPanel { + enum Section: Int { + case topSites + case pocket + case highlights + case highlightIntro + + static let count = 4 + static let allValues = [topSites, pocket, highlights, highlightIntro] + + var title: String? { + switch self { + case .highlights: return Strings.ASHighlightsTitle + case .pocket: return Strings.ASPocketTitle + case .topSites: return nil + case .highlightIntro: return nil + } + } + + var headerHeight: CGSize { + switch self { + case .highlights, .pocket: return CGSize(width: 50, height: 40) + case .topSites: return CGSize(width: 0, height: 0) + case .highlightIntro: return CGSize(width: 50, height: 2) + } + } + + var footerHeight: CGSize { + switch self { + case .highlights, .highlightIntro, .pocket: return CGSize.zero + case .topSites: return CGSize(width: 50, height: 5) + } + } + + func cellHeight(_ traits: UITraitCollection, width: CGFloat) -> CGFloat { + switch self { + case .highlights, .pocket: return ASPanelUX.highlightCellHeight + case .topSites: return 0 //calculated dynamically + case .highlightIntro: return 200 + } + } + + /* + There are edge cases to handle when calculating section insets + - An iPhone 7+ is considered regular width when in landscape + - An iPad in 66% split view is still considered regular width + */ + func sectionInsets(_ traits: UITraitCollection, frameWidth: CGFloat) -> CGFloat { + var currentTraits = traits + if (traits.horizontalSizeClass == .regular && UIScreen.main.bounds.size.width != frameWidth) || UIDevice.current.userInterfaceIdiom == .phone { + currentTraits = UITraitCollection(horizontalSizeClass: .compact) + } + switch self { + case .highlights, .pocket: + var insets = ASPanelUX.sectionInsetsForSizeClass[currentTraits.horizontalSizeClass] + insets = insets + ASPanelUX.MinimumInsets + return insets + case .topSites: + return ASPanelUX.sectionInsetsForSizeClass[currentTraits.horizontalSizeClass] + case .highlightIntro: + return ASPanelUX.sectionInsetsForSizeClass[currentTraits.horizontalSizeClass] + } + } + + func numberOfItemsForRow(_ traits: UITraitCollection) -> CGFloat { + switch self { + case .highlights, .pocket: + var numItems: CGFloat = ASPanelUX.numberOfItemsPerRowForSizeClassIpad[traits.horizontalSizeClass] + if UIInterfaceOrientationIsPortrait(UIApplication.shared.statusBarOrientation) { + numItems = numItems - 1 + } + if traits.horizontalSizeClass == .compact && UIInterfaceOrientationIsLandscape(UIApplication.shared.statusBarOrientation) { + numItems = numItems - 1 + } + return numItems + case .topSites, .highlightIntro: + return 1 + } + } + + func cellSize(for traits: UITraitCollection, frameWidth: CGFloat) -> CGSize { + let height = cellHeight(traits, width: frameWidth) + let inset = sectionInsets(traits, frameWidth: frameWidth) * 2 + + switch self { + case .highlights, .pocket: + let numItems = numberOfItemsForRow(traits) + return CGSize(width: floor(((frameWidth - inset) - (ASPanelUX.MinimumInsets * (numItems - 1))) / numItems), height: height) + case .topSites: + return CGSize(width: frameWidth - inset, height: height) + case .highlightIntro: + return CGSize(width: frameWidth - inset - (ASPanelUX.MinimumInsets * 2), height: height) + } + } + + var headerView: UIView? { + switch self { + case .highlights, .highlightIntro, .pocket: + let view = ASHeaderView() + view.title = title + return view + case .topSites: + return nil + } + } + + var cellIdentifier: String { + switch self { + case .topSites: return "TopSiteCell" + case .highlights: return "HistoryCell" + case .pocket: return "PocketCell" + case .highlightIntro: return "HighlightIntroCell" + } + } + + var cellType: UICollectionViewCell.Type { + switch self { + case .topSites: return ASHorizontalScrollCell.self + case .highlights, .pocket: return ActivityStreamHighlightCell.self + case .highlightIntro: return HighlightIntroCell.self + } + } + + init(at indexPath: IndexPath) { + self.init(rawValue: indexPath.section)! + } + + init(_ section: Int) { + self.init(rawValue: section)! + } + } +} + +// MARK: - Tableview Delegate +extension ActivityStreamPanel: UICollectionViewDelegateFlowLayout { + + override func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView { + switch kind { + case UICollectionElementKindSectionHeader: + let view = collectionView.dequeueReusableSupplementaryView(ofKind: UICollectionElementKindSectionHeader, withReuseIdentifier: "Header", for: indexPath) as! ASHeaderView + let title = Section(indexPath.section).title + switch Section(indexPath.section) { + case .highlights, .highlightIntro: + view.title = title + return view + case .pocket: + view.title = title + view.moreButton.isHidden = false + view.moreButton.addTarget(self, action: #selector(ActivityStreamPanel.showMorePocketStories), for: .touchUpInside) + return view + case .topSites: + return UICollectionReusableView() + } + case UICollectionElementKindSectionFooter: + let view = collectionView.dequeueReusableSupplementaryView(ofKind: UICollectionElementKindSectionFooter, withReuseIdentifier: "Footer", for: indexPath) as! ASFooterView + switch Section(indexPath.section) { + case .highlights, .highlightIntro: + return UICollectionReusableView() + case .topSites, .pocket: + return view + } + default: + return UICollectionReusableView() + } + } + + override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { + self.longPressRecognizer.isEnabled = false + selectItemAtIndex(indexPath.item, inSection: Section(indexPath.section)) + } + + func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { + let cellSize = Section(indexPath.section).cellSize(for: self.traitCollection, frameWidth: self.view.frame.width) + + switch Section(indexPath.section) { + case .highlights: + if highlights.isEmpty { + return CGSize.zero + } + return cellSize + case .topSites: + // Create a temporary cell so we can calculate the height. + let layout = topSiteCell.collectionView.collectionViewLayout as! HorizontalFlowLayout + let estimatedLayout = layout.calculateLayout(for: CGSize(width: cellSize.width, height: 0)) + return CGSize(width: cellSize.width, height: estimatedLayout.size.height) + case .highlightIntro, .pocket: + return cellSize + } + } + + func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize { + switch Section(section) { + case .highlights: + return highlights.isEmpty ? CGSize.zero : CGSize(width: self.view.frame.size.width, height: Section(section).headerHeight.height) + case .highlightIntro: + return !highlights.isEmpty ? CGSize.zero : CGSize(width: self.view.frame.size.width, height: Section(section).headerHeight.height) + case .pocket: + return pocketStories.isEmpty ? CGSize.zero : Section(section).headerHeight + case .topSites: + return Section(section).headerHeight + } + } + + func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForFooterInSection section: Int) -> CGSize { + switch Section(section) { + case .highlights, .highlightIntro, .pocket: + return CGSize.zero + case .topSites: + return Section(section).footerHeight + } + } + + func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat { + return 0 + } + + func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat { + return ASPanelUX.rowSpacing + } + + func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets { + let insets = Section(section).sectionInsets(self.traitCollection, frameWidth: self.view.frame.width) + return UIEdgeInsets(top: 0, left: insets, bottom: 0, right: insets) + } + + fileprivate func showSiteWithURLHandler(_ url: URL) { + let visitType = VisitType.bookmark + homePanelDelegate?.homePanel(self, didSelectURL: url, visitType: visitType) + } +} + +// MARK: - Tableview Data Source +extension ActivityStreamPanel { + + override func numberOfSections(in collectionView: UICollectionView) -> Int { + return 4 + } + + override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { + var numItems: CGFloat = ASPanelUX.numberOfItemsPerRowForSizeClassIpad[self.traitCollection.horizontalSizeClass] + if UIInterfaceOrientationIsPortrait(UIApplication.shared.statusBarOrientation) { + numItems = numItems - 1 + } + if self.traitCollection.horizontalSizeClass == .compact && UIInterfaceOrientationIsLandscape(UIApplication.shared.statusBarOrientation) { + numItems = numItems - 1 + } + switch Section(section) { + case .topSites: + return topSitesManager.content.isEmpty ? 0 : 1 + case .highlights: + return self.highlights.count + case .pocket: + return pocketStories.isEmpty ? 0 : Int(numItems) + case .highlightIntro: + return self.highlights.isEmpty && showHighlightIntro && isHighlightsEnabled() ? 1 : 0 + } + } + + override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { + let identifier = Section(indexPath.section).cellIdentifier + let cell = collectionView.dequeueReusableCell(withReuseIdentifier: identifier, for: indexPath) + + switch Section(indexPath.section) { + case .topSites: + return configureTopSitesCell(cell, forIndexPath: indexPath) + case .highlights: + return configureHistoryItemCell(cell, forIndexPath: indexPath) + case .pocket: + return configurePocketItemCell(cell, forIndexPath: indexPath) + case .highlightIntro: + return configureHighlightIntroCell(cell, forIndexPath: indexPath) + } + } + + //should all be collectionview + func configureTopSitesCell(_ cell: UICollectionViewCell, forIndexPath indexPath: IndexPath) -> UICollectionViewCell { + let topSiteCell = cell as! ASHorizontalScrollCell + topSiteCell.delegate = self.topSitesManager + topSiteCell.setNeedsLayout() + topSiteCell.collectionView.reloadData() + return cell + } + + func configureHistoryItemCell(_ cell: UICollectionViewCell, forIndexPath indexPath: IndexPath) -> UICollectionViewCell { + let site = highlights[indexPath.row] + let simpleHighlightCell = cell as! ActivityStreamHighlightCell + simpleHighlightCell.configureWithSite(site) + return simpleHighlightCell + } + + func configurePocketItemCell(_ cell: UICollectionViewCell, forIndexPath indexPath: IndexPath) -> UICollectionViewCell { + let pocketStory = pocketStories[indexPath.row] + let pocketItemCell = cell as! ActivityStreamHighlightCell + pocketItemCell.configureWithPocketStory(pocketStory) + return pocketItemCell + } + + func configureHighlightIntroCell(_ cell: UICollectionViewCell, forIndexPath indexPath: IndexPath) -> UICollectionViewCell { + let introCell = cell as! HighlightIntroCell + //The cell is configured on creation. No need to configure. But leave this here in case we need it. + return introCell + } +} + +// MARK: - Data Management +extension ActivityStreamPanel: DataObserverDelegate { + fileprivate func reportMissingData(sites: [Site], source: ASPingSource) { + let missingImagePings: [[String: Any]] = sites.flatMap { site in + if site.metadata?.mediaURL == nil { + return self.telemetry.pingFor(badState: .MissingMetadataImage, source: source) + } + return nil + } + + let missingFaviconPings: [[String: Any]] = sites.flatMap { site in + if site.icon == nil { + return self.telemetry.pingFor(badState: .MissingFavicon, source: source) + } + return nil + } + + let badPings = missingImagePings + missingFaviconPings + self.telemetry.eventsTracker.sendBatch(badPings, validate: true) + } + + // Reloads both highlights and top sites data from their respective caches. Does not invalidate the cache. + // See ActivityStreamDataObserver for invalidation logic. + func reloadAll() { + // If the pocket stories are not availible for the Locale the PocketAPI will return nil + // So it is okay if the default here is true + self.getPocketSites().uponQueue(.main) { _ in + if !self.pocketStories.isEmpty { + self.collectionView?.reloadData() + } + } + + accumulate([self.getHighlights, self.getTopSites]).uponQueue(.main) { _ in + // If there is no pending cache update and highlights are empty. Show the onboarding screen + self.showHighlightIntro = self.highlights.isEmpty + self.collectionView?.reloadData() + + // Refresh the AS data in the background so we'll have fresh data next time we show. + self.profile.panelDataObservers.activityStream.refreshIfNeeded(forceHighlights: false, forceTopSites: false) + } + } + + func getBookmarksForHighlights() -> Deferred>> { + let count = ASPanelUX.BookmarkHighlights // Fetch 2 bookmarks + return self.profile.recommendations.getRecentBookmarks(count) + } + + // Used to check if the entire section is turned off + // when it is we shouldnt show the emtpy state + func isHighlightsEnabled() -> Bool { + let bookmarks = profile.prefs.boolForKey(PrefsKeys.ASBookmarkHighlightsVisible) ?? false + let history = profile.prefs.boolForKey(PrefsKeys.ASRecentHighlightsVisible) ?? false + return history && bookmarks + } + + func getHighlights() -> Success { + var queries: [() -> Deferred>>] = [] + if profile.prefs.boolForKey(PrefsKeys.ASBookmarkHighlightsVisible) ?? true { + queries.append(getBookmarksForHighlights) + } + + if profile.prefs.boolForKey(PrefsKeys.ASRecentHighlightsVisible) ?? true { + queries.append(self.profile.recommendations.getHighlights) + } + + guard !queries.isEmpty else { + self.highlights = [] + return succeed() + } + + return accumulate(queries).bindQueue(.main) { result in + guard let resultArr = result.successValue else { + return succeed() + } + let sites = resultArr.reduce([]) { $0 + $1.asArray() } + + // Scan through the fetched highlights and report on anything that might be missing. + self.reportMissingData(sites: sites, source: .Highlights) + self.highlights = sites + return succeed() + } + } + + func getPocketSites() -> Success { + let showPocket = (profile.prefs.boolForKey(PrefsKeys.ASPocketStoriesVisible) ?? Pocket.IslocaleSupported(Locale.current.identifier)) && AppConstants.MOZ_POCKET_STORIES + guard showPocket else { + self.pocketStories = [] + return succeed() + } + + return pocketAPI.globalFeed(items: 4).bindQueue(.main) { pStory in + self.pocketStories = pStory + return succeed() + } + } + + @objc func showMorePocketStories() { + showSiteWithURLHandler(Pocket.MoreStoriesURL) + } + + func getTopSites() -> Success { + return self.profile.history.getTopSitesWithLimit(16).both(self.profile.history.getPinnedTopSites()).bindQueue(.main) { (topsites, pinnedSites) in + guard let mySites = topsites.successValue?.asArray(), let pinned = pinnedSites.successValue?.asArray() else { + return succeed() + } + + // How sites are merged together. We compare against the urls second level domain. example m.youtube.com is compared against `youtube` + let unionOnURL = { (site: Site) -> String in + return URL(string: site.url)?.hostSLD ?? "" + } + + // Fetch the default sites + let defaultSites = self.defaultTopSites() + // create PinnedSite objects. used by the view layer to tell topsites apart + let pinnedSites: [Site] = pinned.map({ PinnedSite(site: $0) }) + + // Merge default topsites with a user's topsites. + let mergedSites = mySites.union(defaultSites, f: unionOnURL) + // Merge pinnedSites with sites from the previous step + let allSites = pinnedSites.union(mergedSites, f: unionOnURL) + + // Favour topsites from defaultSites as they have better favicons. But keep PinnedSites + let newSites = allSites.map { site -> Site in + if let _ = site as? PinnedSite { + return site + } + let domain = URL(string: site.url)?.hostSLD + return defaultSites.find { $0.title.lowercased() == domain } ?? site + } + + // Don't report bad states for default sites we provide + self.reportMissingData(sites: mySites, source: .TopSites) + + self.topSitesManager.currentTraits = self.view.traitCollection + + if newSites.count > Int(ActivityStreamTopSiteCacheSize) { + self.topSitesManager.content = Array(newSites[0.. [Site] { + let suggested = SuggestedSites.asArray() + let deleted = profile.prefs.arrayForKey(DefaultSuggestedSitesKey) as? [String] ?? [] + return suggested.filter({deleted.index(of: $0.url) == .none}) + } + + @objc fileprivate func longPress(_ longPressGestureRecognizer: UILongPressGestureRecognizer) { + guard longPressGestureRecognizer.state == UIGestureRecognizerState.began else { return } + + let point = longPressGestureRecognizer.location(in: self.collectionView) + guard let indexPath = self.collectionView?.indexPathForItem(at: point) else { return } + + switch Section(indexPath.section) { + case .highlights, .pocket: + presentContextMenu(for: indexPath) + case .topSites: + let topSiteCell = self.collectionView?.cellForItem(at: indexPath) as! ASHorizontalScrollCell + let pointInTopSite = longPressGestureRecognizer.location(in: topSiteCell.collectionView) + guard let topSiteIndexPath = topSiteCell.collectionView.indexPathForItem(at: pointInTopSite) else { return } + presentContextMenu(for: topSiteIndexPath) + case .highlightIntro: + break + } + } + + fileprivate func fetchBookmarkStatus(for site: Site, with indexPath: IndexPath, forSection section: Section, completionHandler: @escaping () -> Void) { + profile.bookmarks.modelFactory >>== { + $0.isBookmarked(site.url).uponQueue(.main) { result in + guard let isBookmarked = result.successValue else { + log.error("Error getting bookmark status: \(result.failureValue ??? "nil").") + return + } + site.setBookmarked(isBookmarked) + completionHandler() + } + } + } + + func selectItemAtIndex(_ index: Int, inSection section: Section) { + let site: Site? + switch section { + case .highlights: + site = self.highlights[index] + telemetry.reportEvent(.Click, source: .Highlights, position: index) + case .pocket: + site = Site(url: pocketStories[index].url.absoluteString, title: pocketStories[index].title) + telemetry.reportEvent(.Click, source: .Pocket, position: index) + LeanPlumClient.shared.track(event: .openedPocketStory, withParameters: ["Source": "Activity Stream" as AnyObject]) + case .topSites, .highlightIntro: + return + } + if let site = site { + showSiteWithURLHandler(URL(string: site.url)!) + } + } +} + +extension ActivityStreamPanel: HomePanelContextMenu { + func presentContextMenu(for site: Site, with indexPath: IndexPath, completionHandler: @escaping () -> PhotonActionSheet?) { + + fetchBookmarkStatus(for: site, with: indexPath, forSection: Section(indexPath.section)) { + guard let contextMenu = completionHandler() else { return } + self.present(contextMenu, animated: true, completion: nil) + } + } + + func getSiteDetails(for indexPath: IndexPath) -> Site? { + let site: Site + + switch Section(indexPath.section) { + case .highlights: + site = highlights[indexPath.row] + case .pocket: + site = Site(url: pocketStories[indexPath.row].dedupeURL.absoluteString, title: pocketStories[indexPath.row].title) + case .topSites: + site = topSitesManager.content[indexPath.item] + case .highlightIntro: + return nil + } + + return site + } + + func getContextMenuActions(for site: Site, with indexPath: IndexPath) -> [PhotonActionSheetItem]? { + guard let siteURL = URL(string: site.url) else { return nil } + + let pingSource: ASPingSource + let index: Int + var sourceView: UIView? + + switch Section(indexPath.section) { + case .topSites: + pingSource = .TopSites + index = indexPath.item + if let topSiteCell = self.collectionView?.cellForItem(at: IndexPath(row: 0, section: 0)) as? ASHorizontalScrollCell { + sourceView = topSiteCell.collectionView.cellForItem(at: indexPath) + } + case .highlights: + pingSource = .Highlights + index = indexPath.row + sourceView = self.collectionView?.cellForItem(at: indexPath) + case .pocket: + pingSource = .Pocket + index = indexPath.item + sourceView = self.collectionView?.cellForItem(at: indexPath) + case .highlightIntro: + return nil + } + + let openInNewTabAction = PhotonActionSheetItem(title: Strings.OpenInNewTabContextMenuTitle, iconString: "quick_action_new_tab") { action in + self.homePanelDelegate?.homePanelDidRequestToOpenInNewTab(siteURL, isPrivate: false) + self.telemetry.reportEvent(.NewTab, source: pingSource, position: index) + let source = ["Source": "Activity Stream Long Press Context Menu" as AnyObject] + LeanPlumClient.shared.track(event: .openedNewTab, withParameters: source) + if Section(indexPath.section) == .pocket { + LeanPlumClient.shared.track(event: .openedPocketStory, withParameters: source) + } + } + + let openInNewPrivateTabAction = PhotonActionSheetItem(title: Strings.OpenInNewPrivateTabContextMenuTitle, iconString: "quick_action_new_private_tab") { action in + self.homePanelDelegate?.homePanelDidRequestToOpenInNewTab(siteURL, isPrivate: true) + } + + let bookmarkAction: PhotonActionSheetItem + if site.bookmarked ?? false { + bookmarkAction = PhotonActionSheetItem(title: Strings.RemoveBookmarkContextMenuTitle, iconString: "action_bookmark_remove", handler: { action in + self.profile.bookmarks.modelFactory >>== { + $0.removeByURL(siteURL.absoluteString).uponQueue(.main) {_ in + self.profile.panelDataObservers.activityStream.refreshIfNeeded(forceHighlights: true, forceTopSites: false) + } + site.setBookmarked(false) + } + self.telemetry.reportEvent(.RemoveBookmark, source: pingSource, position: index) + UnifiedTelemetry.recordEvent(category: .action, method: .delete, object: .bookmark, value: .activityStream) + }) + } else { + bookmarkAction = PhotonActionSheetItem(title: Strings.BookmarkContextMenuTitle, iconString: "action_bookmark", handler: { action in + let shareItem = ShareItem(url: site.url, title: site.title, favicon: site.icon) + _ = self.profile.bookmarks.shareItem(shareItem) + var userData = [QuickActions.TabURLKey: shareItem.url] + if let title = shareItem.title { + userData[QuickActions.TabTitleKey] = title + } + QuickActions.sharedInstance.addDynamicApplicationShortcutItemOfType(.openLastBookmark, + withUserData: userData, + toApplication: UIApplication.shared) + site.setBookmarked(true) + self.profile.panelDataObservers.activityStream.refreshIfNeeded(forceHighlights: true, forceTopSites: true) + self.telemetry.reportEvent(.AddBookmark, source: pingSource, position: index) + LeanPlumClient.shared.track(event: .savedBookmark) + UnifiedTelemetry.recordEvent(category: .action, method: .add, object: .bookmark, value: .activityStream) + }) + } + + let deleteFromHistoryAction = PhotonActionSheetItem(title: Strings.DeleteFromHistoryContextMenuTitle, iconString: "action_delete", handler: { action in + self.telemetry.reportEvent(.Delete, source: pingSource, position: index) + self.profile.history.removeHistoryForURL(site.url).uponQueue(.main) { result in + guard result.isSuccess else { return } + self.profile.panelDataObservers.activityStream.refreshIfNeeded(forceHighlights: true, forceTopSites: true) + } + }) + + let shareAction = PhotonActionSheetItem(title: Strings.ShareContextMenuTitle, iconString: "action_share", handler: { action in + let helper = ShareExtensionHelper(url: siteURL, tab: nil) + let controller = helper.createActivityViewController { completed, activityType in + self.telemetry.reportEvent(.Share, source: pingSource, position: index, shareProvider: activityType) + } + if UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiom.pad, let popoverController = controller.popoverPresentationController { + let cellRect = sourceView?.frame ?? CGRect.zero + let cellFrameInSuperview = self.collectionView?.convert(cellRect, to: self.collectionView) ?? CGRect.zero + + popoverController.sourceView = sourceView + popoverController.sourceRect = CGRect(origin: CGPoint(x: cellFrameInSuperview.size.width/2, y: cellFrameInSuperview.height/2), size: .zero) + popoverController.permittedArrowDirections = [.up, .down, .left] + popoverController.delegate = self + } + self.present(controller, animated: true, completion: nil) + }) + + let removeTopSiteAction = PhotonActionSheetItem(title: Strings.RemoveContextMenuTitle, iconString: "action_remove", handler: { action in + self.telemetry.reportEvent(.Remove, source: pingSource, position: index) + self.hideURLFromTopSites(site) + }) + + let dismissHighlightAction = PhotonActionSheetItem(title: Strings.RemoveContextMenuTitle, iconString: "action_remove", handler: { action in + self.telemetry.reportEvent(.Dismiss, source: pingSource, position: index) + self.hideFromHighlights(site) + }) + + let pinTopSite = PhotonActionSheetItem(title: Strings.PinTopsiteActionTitle, iconString: "action_pin", handler: { action in + self.pinTopSite(site) + }) + + let removePinTopSite = PhotonActionSheetItem(title: Strings.RemovePinTopsiteActionTitle, iconString: "action_unpin", handler: { action in + self.removePinTopSite(site) + }) + + let topSiteActions: [PhotonActionSheetItem] + if let _ = site as? PinnedSite { + topSiteActions = [removePinTopSite] + } else { + topSiteActions = [pinTopSite, removeTopSiteAction] + } + + var actions = [openInNewTabAction, openInNewPrivateTabAction, bookmarkAction, shareAction] + + switch Section(indexPath.section) { + case .highlights: actions.append(contentsOf: [dismissHighlightAction, deleteFromHistoryAction]) + case .pocket: break + case .topSites: actions.append(contentsOf: topSiteActions) + case .highlightIntro: break + } + return actions + } +} + +extension ActivityStreamPanel: UIPopoverPresentationControllerDelegate { + + // Dismiss the popover if the device is being rotated. + // This is used by the Share UIActivityViewController action sheet on iPad + func popoverPresentationController(_ popoverPresentationController: UIPopoverPresentationController, willRepositionPopoverTo rect: UnsafeMutablePointer, in view: AutoreleasingUnsafeMutablePointer) { + popoverPresentationController.presentedViewController.dismiss(animated: false, completion: nil) + } +} + +// MARK: Telemetry + +enum ASPingEvent: String { + case Click = "CLICK" + case Delete = "DELETE" + case Dismiss = "DISMISS" + case Share = "SHARE" + case NewTab = "NEW_TAB" + case AddBookmark = "ADD_BOOKMARK" + case RemoveBookmark = "REMOVE_BOOKMARK" + case Remove = "REMOVE" +} + +enum ASPingBadStateEvent: String { + case MissingMetadataImage = "MISSING_METADATA_IMAGE" + case MissingFavicon = "MISSING_FAVICON" +} + +enum ASPingSource: String { + case Highlights = "HIGHLIGHTS" + case TopSites = "TOP_SITES" + case HighlightsIntro = "HIGHLIGHTS_INTRO" + case Pocket = "POCKET" +} + +struct ActivityStreamTracker { + let eventsTracker: PingCentreClient + let sessionsTracker: PingCentreClient + + private var baseASPing: [String: Any] { + return [ + "app_version": AppInfo.appVersion, + "build": AppInfo.buildNumber, + "locale": Locale.current.identifier, + "release_channel": AppConstants.BuildChannel.rawValue + ] + } + + func pingFor(badState: ASPingBadStateEvent, source: ASPingSource) -> [String: Any] { + var eventPing: [String: Any] = [ + "event": badState.rawValue, + "page": "NEW_TAB", + "source": source.rawValue, + ] + eventPing.merge(with: baseASPing) + return eventPing + } + + func reportEvent(_ event: ASPingEvent, source: ASPingSource, position: Int, shareProvider: String? = nil) { + var eventPing: [String: Any] = [ + "event": event.rawValue, + "page": "NEW_TAB", + "source": source.rawValue, + "action_position": position, + ] + + if let provider = shareProvider { + eventPing["share_provider"] = provider + } + + eventPing.merge(with: baseASPing) + eventsTracker.sendPing(eventPing as [String : AnyObject], validate: true) + } + + func reportSessionStop(_ duration: UInt64) { + sessionsTracker.sendPing([ + "session_duration": NSNumber(value: duration), + "app_version": AppInfo.appVersion, + "build": AppInfo.buildNumber, + "locale": Locale.current.identifier, + "release_channel": AppConstants.BuildChannel.rawValue + ] as [String: Any], validate: true) + } +} + +// MARK: - Section Header View +struct ASHeaderViewUX { + static let SeperatorColor = UIColor(rgb: 0xedecea) + static let TextFont = DynamicFontHelper.defaultHelper.MediumSizeBoldFontAS + static let SeperatorHeight = 1 + static let Insets: CGFloat = UIDevice.current.userInterfaceIdiom == .pad ? ASPanelUX.SectionInsetsForIpad + ASPanelUX.MinimumInsets : ASPanelUX.MinimumInsets + static let TitleTopInset: CGFloat = 5 +} + +class ASFooterView: UICollectionReusableView { + + override init(frame: CGRect) { + super.init(frame: frame) + + let seperatorLine = UIView() + seperatorLine.backgroundColor = ASHeaderViewUX.SeperatorColor + self.backgroundColor = UIColor.clear + addSubview(seperatorLine) + seperatorLine.snp.makeConstraints { make in + make.height.equalTo(ASHeaderViewUX.SeperatorHeight) + make.leading.equalTo(self.snp.leading) + make.trailing.equalTo(self.snp.trailing) + make.top.equalTo(self.snp.top) + } + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +class ASHeaderView: UICollectionReusableView { + lazy fileprivate var titleLabel: UILabel = { + let titleLabel = UILabel() + titleLabel.text = self.title + titleLabel.textColor = UIColor.gray + titleLabel.font = ASHeaderViewUX.TextFont + titleLabel.minimumScaleFactor = 0.6 + titleLabel.numberOfLines = 1 + titleLabel.adjustsFontSizeToFitWidth = true + return titleLabel + }() + + lazy var moreButton: UIButton = { + let button = UIButton() + button.setTitle("More", for: .normal) + button.isHidden = true + button.titleLabel?.font = ASHeaderViewUX.TextFont + button.contentHorizontalAlignment = .right + button.setTitleColor(UIConstants.SystemBlueColor, for: .normal) + button.setTitleColor(.gray, for: UIControlState.highlighted) + return button + }() + + var title: String? { + willSet(newTitle) { + titleLabel.text = newTitle + } + } + + var leftConstraint: Constraint? + var rightConstraint: Constraint? + + var titleInsets: CGFloat { + get { + return UIScreen.main.bounds.size.width == self.frame.size.width && UIDevice.current.userInterfaceIdiom == .pad ? ASHeaderViewUX.Insets : ASPanelUX.MinimumInsets + } + } + + override func prepareForReuse() { + super.prepareForReuse() + moreButton.isHidden = true + moreButton.removeTarget(nil, action: nil, for: .allEvents) + } + override init(frame: CGRect) { + super.init(frame: frame) + addSubview(titleLabel) + addSubview(moreButton) + moreButton.snp.makeConstraints { make in + make.top.equalTo(self).inset(ASHeaderViewUX.TitleTopInset) + make.bottom.equalTo(self) + self.rightConstraint = make.trailing.equalTo(self).inset(-titleInsets).constraint + } + moreButton.setContentCompressionResistancePriority(UILayoutPriorityRequired, for: UILayoutConstraintAxis.horizontal) + titleLabel.snp.makeConstraints { make in + self.leftConstraint = make.leading.equalTo(self).inset(titleInsets).constraint + make.trailing.equalTo(moreButton.snp.leading).inset(-ASHeaderViewUX.TitleTopInset) + make.top.equalTo(self).inset(ASHeaderViewUX.TitleTopInset) + make.bottom.equalTo(self) + } + } + + override func layoutSubviews() { + super.layoutSubviews() + leftConstraint?.update(offset: titleInsets) + rightConstraint?.update(offset: -titleInsets) + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +open class PinnedSite: Site { + let isPinnedSite = true + + init(site: Site) { + super.init(url: site.url, title: site.title, bookmarked: site.bookmarked) + self.icon = site.icon + self.metadata = site.metadata + } + +} diff --git a/mobile/ios/Client/Frontend/Home/ActivityStreamTopSitesCell.swift b/mobile/ios/Client/Frontend/Home/ActivityStreamTopSitesCell.swift new file mode 100644 index 0000000000..91cabbca6d --- /dev/null +++ b/mobile/ios/Client/Frontend/Home/ActivityStreamTopSitesCell.swift @@ -0,0 +1,558 @@ +/* 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 SDWebImage +import Storage + +struct TopSiteCellUX { + static let TitleHeight: CGFloat = 20 + static let TitleBackgroundColor = UIColor(colorLiteralRed: 1, green: 1, blue: 1, alpha: 0.7) + static let TitleTextColor = UIColor.black + static let TitleFont = DynamicFontHelper.defaultHelper.SmallSizeRegularWeightAS + static let SelectedOverlayColor = UIColor(white: 0.0, alpha: 0.25) + static let CellCornerRadius: CGFloat = 4 + static let TitleOffset: CGFloat = 5 + static let OverlayColor = UIColor(white: 0.0, alpha: 0.25) + static let IconSizePercent: CGFloat = 0.8 + static let BorderColor = UIColor(white: 0, alpha: 0.1) + static let BorderWidth: CGFloat = 0.5 + static let PinIconSize: CGFloat = 12 + static let PinColor = UIColor(rgb: 0x272727) +} + +/* + * The TopSite cell that appears in the ASHorizontalScrollView. + */ +class TopSiteItemCell: UICollectionViewCell { + + var url: URL? + + lazy var imageView: UIImageView = { + let imageView = UIImageView() + imageView.layer.masksToBounds = true + return imageView + }() + + lazy var pinImageView: UIImageView = { + let imageView = UIImageView() + imageView.image = UIImage.templateImageNamed("pin_small") + imageView.tintColor = TopSiteCellUX.PinColor + return imageView + }() + + lazy fileprivate var titleLabel: UILabel = { + let titleLabel = UILabel() + titleLabel.layer.masksToBounds = true + titleLabel.textAlignment = .center + titleLabel.font = TopSiteCellUX.TitleFont + titleLabel.textColor = TopSiteCellUX.TitleTextColor + titleLabel.backgroundColor = UIColor.clear + return titleLabel + }() + + lazy private var faviconBG: UIView = { + let view = UIView() + view.layer.cornerRadius = TopSiteCellUX.CellCornerRadius + view.layer.masksToBounds = true + view.layer.borderWidth = TopSiteCellUX.BorderWidth + view.layer.borderColor = TopSiteCellUX.BorderColor.cgColor + return view + }() + + lazy var selectedOverlay: UIView = { + let selectedOverlay = UIView() + selectedOverlay.backgroundColor = TopSiteCellUX.OverlayColor + selectedOverlay.isHidden = true + return selectedOverlay + }() + + lazy var titleBorder: CALayer = { + let border = CALayer() + border.backgroundColor = TopSiteCellUX.BorderColor.cgColor + return border + }() + + override var isSelected: Bool { + didSet { + self.selectedOverlay.isHidden = !isSelected + } + } + + override init(frame: CGRect) { + super.init(frame: frame) + isAccessibilityElement = true + accessibilityIdentifier = "TopSite" + + contentView.addSubview(titleLabel) + contentView.addSubview(faviconBG) + contentView.addSubview(imageView) + contentView.addSubview(selectedOverlay) + + titleLabel.snp.makeConstraints { make in + make.left.equalTo(self).offset(TopSiteCellUX.TitleOffset) + make.right.equalTo(self).offset(-TopSiteCellUX.TitleOffset) + make.height.equalTo(TopSiteCellUX.TitleHeight) + make.bottom.equalTo(self) + } + + imageView.snp.makeConstraints { make in + make.size.equalTo(floor(frame.width * TopSiteCellUX.IconSizePercent)) + make.centerX.equalTo(self) + make.centerY.equalTo(self).inset(-TopSiteCellUX.TitleHeight/2) + } + + selectedOverlay.snp.makeConstraints { make in + make.edges.equalTo(contentView) + } + + faviconBG.snp.makeConstraints { make in + make.top.left.right.equalTo(self) + make.bottom.equalTo(self).inset(TopSiteCellUX.TitleHeight) + } + } + + override func layoutSubviews() { + super.layoutSubviews() + titleBorder.frame = CGRect(x: 0, y: frame.height - TopSiteCellUX.TitleHeight - TopSiteCellUX.BorderWidth, width: frame.width, height: TopSiteCellUX.BorderWidth) + + imageView.snp.remakeConstraints { make in + make.size.equalTo(floor(self.frame.width * TopSiteCellUX.IconSizePercent)) + make.centerX.equalTo(self) + make.centerY.equalTo(self).inset(-TopSiteCellUX.TitleHeight/2) + } + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func prepareForReuse() { + super.prepareForReuse() + contentView.backgroundColor = UIColor.clear + imageView.image = nil + imageView.backgroundColor = UIColor.clear + faviconBG.backgroundColor = UIColor.clear + pinImageView.removeFromSuperview() + imageView.sd_cancelCurrentImageLoad() + titleLabel.text = "" + titleLabel.snp.updateConstraints { make in + make.left.equalTo(self).offset(TopSiteCellUX.TitleOffset) + } + } + + func configureWithTopSiteItem(_ site: Site) { + url = site.tileURL + + if let provider = site.metadata?.providerName { + titleLabel.text = provider.lowercased() + } else { + titleLabel.text = site.tileURL.hostSLD + } + + // If its a pinned site add a bullet point to the front + if let _ = site as? PinnedSite { + contentView.addSubview(pinImageView) + pinImageView.snp.makeConstraints { make in + make.right.equalTo(self.titleLabel.snp.left) + make.size.equalTo(TopSiteCellUX.PinIconSize) + make.centerY.equalTo(self.titleLabel.snp.centerY) + } + titleLabel.snp.updateConstraints { make in + make.left.equalTo(self).offset(TopSiteCellUX.PinIconSize) + } + } + + accessibilityLabel = titleLabel.text + imageView.setFavicon(forSite: site, onCompletion: { [weak self] (color, url) in + if let url = url, url == self?.url { + self?.faviconBG.backgroundColor = color + self?.imageView.backgroundColor = color + } + }) + } + +} + +// An empty cell to show when a row is incomplete +class EmptyTopsiteDecorationCell: UICollectionReusableView { + + override init(frame: CGRect) { + super.init(frame: frame) + self.layer.cornerRadius = TopSiteCellUX.CellCornerRadius + self.layer.borderWidth = TopSiteCellUX.BorderWidth + self.layer.borderColor = TopSiteCellUX.BorderColor.cgColor + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +struct ASHorizontalScrollCellUX { + static let TopSiteCellIdentifier = "TopSiteItemCell" + static let TopSiteEmptyCellIdentifier = "TopSiteItemEmptyCell" + + static let TopSiteItemSize = CGSize(width: 75, height: 75) + static let BackgroundColor = UIColor.white + static let PageControlRadius: CGFloat = 3 + static let PageControlSize = CGSize(width: 30, height: 15) + static let PageControlOffset: CGFloat = 12 + static let MinimumInsets: CGFloat = 14 +} + +/* + The View that describes the topSite cell that appears in the tableView. + */ +class ASHorizontalScrollCell: UICollectionViewCell { + + lazy var collectionView: UICollectionView = { + let layout = HorizontalFlowLayout() + layout.itemSize = ASHorizontalScrollCellUX.TopSiteItemSize + let collectionView = UICollectionView(frame: CGRect.zero, collectionViewLayout: layout) + collectionView.register(TopSiteItemCell.self, forCellWithReuseIdentifier: ASHorizontalScrollCellUX.TopSiteCellIdentifier) + collectionView.backgroundColor = UIColor.clear + collectionView.showsHorizontalScrollIndicator = false + collectionView.isPagingEnabled = true + return collectionView + }() + + lazy fileprivate var pageControl: FilledPageControl = { + let pageControl = FilledPageControl() + pageControl.tintColor = UIColor.gray + pageControl.indicatorRadius = ASHorizontalScrollCellUX.PageControlRadius + pageControl.isUserInteractionEnabled = true + pageControl.isAccessibilityElement = true + pageControl.accessibilityIdentifier = "pageControl" + pageControl.accessibilityLabel = Strings.ASPageControlButton + pageControl.accessibilityTraits = UIAccessibilityTraitButton + return pageControl + }() + + lazy fileprivate var pageControlPress: UITapGestureRecognizer = { + let press = UITapGestureRecognizer(target: self, action: #selector(ASHorizontalScrollCell.handlePageTap(_:))) + // press.delegate = self + return press + }() + + weak var delegate: ASHorizontalScrollCellManager? { + didSet { + collectionView.delegate = delegate + collectionView.dataSource = delegate + delegate?.pageChangedHandler = { [weak self] progress in + self?.currentPageChanged(progress) + } + } + } + + override init(frame: CGRect) { + super.init(frame: frame) + isAccessibilityElement = false + accessibilityIdentifier = "TopSitesCell" + backgroundColor = UIColor.clear + contentView.addSubview(collectionView) + contentView.addSubview(pageControl) + + pageControl.addGestureRecognizer(self.pageControlPress) + + collectionView.snp.makeConstraints { make in + make.edges.equalTo(contentView) + } + + pageControl.snp.makeConstraints { make in + make.size.equalTo(ASHorizontalScrollCellUX.PageControlSize) + make.top.equalTo(collectionView.snp.bottom).inset(ASHorizontalScrollCellUX.PageControlOffset) + make.centerX.equalTo(self.snp.centerX) + } + } + + override func layoutSubviews() { + super.layoutSubviews() + let layout = collectionView.collectionViewLayout as! HorizontalFlowLayout + + pageControl.pageCount = layout.numberOfPages(with: self.frame.size) + pageControl.isHidden = pageControl.pageCount <= 1 + } + + func currentPageChanged(_ currentPage: CGFloat) { + pageControl.progress = currentPage + if currentPage == floor(currentPage) { + UIAccessibilityPostNotification(UIAccessibilityLayoutChangedNotification, nil) + self.setNeedsLayout() + } + } + + func handlePageTap(_ gesture: UITapGestureRecognizer) { + guard pageControl.pageCount > 1 else { + return + } + + if pageControl.pageCount > pageControl.currentPage + 1 { + pageControl.progress = CGFloat(pageControl.currentPage + 1) + } else { + pageControl.progress = CGFloat(pageControl.currentPage - 1) + } + let swipeCoordinate = CGFloat(pageControl.currentPage) * self.collectionView.frame.size.width + self.collectionView.setContentOffset(CGPoint(x: swipeCoordinate, y: 0), animated: true) + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} +/* + A custom layout used to show a horizontal scrolling list with paging. Similar to iOS springboard. + A modified version of http://stackoverflow.com/a/34167915 + */ + +class HorizontalFlowLayout: UICollectionViewLayout { + fileprivate var cellCount: Int { + if let collectionView = collectionView, let dataSource = collectionView.dataSource { + return dataSource.collectionView(collectionView, numberOfItemsInSection: 0) + } + return 0 + } + var boundsSize = CGSize.zero + private var insets = UIEdgeInsets(equalInset: ASHorizontalScrollCellUX.MinimumInsets) + private var sectionInsets: CGFloat = 0 + var itemSize = CGSize.zero + var cachedAttributes: [UICollectionViewLayoutAttributes]? + + override func prepare() { + super.prepare() + if boundsSize != self.collectionView?.frame.size { + self.collectionView?.setContentOffset(CGPoint.zero, animated: false) + } + boundsSize = self.collectionView?.frame.size ?? CGSize.zero + cachedAttributes = nil + register(EmptyTopsiteDecorationCell.self, forDecorationViewOfKind: ASHorizontalScrollCellUX.TopSiteEmptyCellIdentifier) + } + + func numberOfPages(with bounds: CGSize) -> Int { + let itemsPerPage = maxVerticalItemsCount(height: bounds.height) * maxHorizontalItemsCount(width: bounds.width) + // Sometimes itemsPerPage is 0. In this case just return 0. We dont want to try dividing by 0. + return itemsPerPage == 0 ? 0 : Int(ceil(Double(cellCount) / Double(itemsPerPage))) + } + + func calculateLayout(for size: CGSize) -> (size: CGSize, cellSize: CGSize, cellInsets: UIEdgeInsets) { + let width = size.width + let height = size.height + guard width != 0 else { + return (size: CGSize.zero, cellSize: self.itemSize, cellInsets: self.insets) + } + + let horizontalItemsCount = maxHorizontalItemsCount(width: width) + var verticalItemsCount = maxVerticalItemsCount(height: height) + if cellCount <= horizontalItemsCount { + // If we have only a few items don't provide space for multiple rows. + verticalItemsCount = 1 + } + + // Take the number of cells and subtract its space in the view from the height. The left over space is the white space. + // The left over space is then devided evenly into (n + 1) parts to figure out how much space should be inbetween a cell + var verticalInsets = floor((height - (CGFloat(verticalItemsCount) * itemSize.height)) / CGFloat(verticalItemsCount + 1)) + var horizontalInsets = floor((width - (CGFloat(horizontalItemsCount) * itemSize.width)) / CGFloat(horizontalItemsCount + 1)) + + // We want a minimum inset to make things not look crowded. We also don't want uneven spacing. + // If we dont have this. Set a minimum inset and recalculate the size of a cell + var estimatedItemSize = itemSize + if horizontalInsets != ASHorizontalScrollCellUX.MinimumInsets { + verticalInsets = ASHorizontalScrollCellUX.MinimumInsets + horizontalInsets = ASHorizontalScrollCellUX.MinimumInsets + estimatedItemSize.width = floor((width - (CGFloat(horizontalItemsCount + 1) * horizontalInsets)) / CGFloat(horizontalItemsCount)) + estimatedItemSize.height = estimatedItemSize.width + TopSiteCellUX.TitleHeight + } + + //calculate our estimates. + let estimatedHeight = floor(estimatedItemSize.height * CGFloat(verticalItemsCount)) + (verticalInsets * (CGFloat(verticalItemsCount) + 1)) + let estimatedSize = CGSize(width: CGFloat(numberOfPages(with: boundsSize)) * width, height: estimatedHeight) + + let estimatedInsets = UIEdgeInsets(top: verticalInsets, left: horizontalInsets, bottom: verticalInsets, right: horizontalInsets) + return (size: estimatedSize, cellSize: estimatedItemSize, cellInsets: estimatedInsets) + } + + override var collectionViewContentSize: CGSize { + let estimatedLayout = calculateLayout(for: boundsSize) + insets = estimatedLayout.cellInsets + itemSize = estimatedLayout.cellSize + boundsSize.height = estimatedLayout.size.height + return estimatedLayout.size + } + + func maxVerticalItemsCount(height: CGFloat) -> Int { + let verticalItemsCount = Int(floor(height / (ASHorizontalScrollCellUX.TopSiteItemSize.height + insets.top))) + if let delegate = self.collectionView?.delegate as? ASHorizontalLayoutDelegate { + return delegate.numberOfVerticalItems() + } else { + return verticalItemsCount + } + } + + func maxHorizontalItemsCount(width: CGFloat) -> Int { + let horizontalItemsCount = Int(floor(width / (ASHorizontalScrollCellUX.TopSiteItemSize.width + insets.left))) + if let delegate = self.collectionView?.delegate as? ASHorizontalLayoutDelegate { + return delegate.numberOfHorizontalItems() + } else { + return horizontalItemsCount + } + } + + override func layoutAttributesForDecorationView(ofKind elementKind: String, at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? { + let decorationAttr = UICollectionViewLayoutAttributes(forDecorationViewOfKind: elementKind, with: indexPath) + let cellAttr = self.computeLayoutAttributesForCellAtIndexPath(indexPath) + decorationAttr.frame = cellAttr.frame + + decorationAttr.frame.size.height -= TopSiteCellUX.TitleHeight + decorationAttr.zIndex = -1 + return decorationAttr + } + + override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? { + if cachedAttributes != nil { + return cachedAttributes + } + var allAttributes = [UICollectionViewLayoutAttributes]() + for i in 0 ..< cellCount { + let indexPath = IndexPath(row: i, section: 0) + let attr = self.computeLayoutAttributesForCellAtIndexPath(indexPath) + allAttributes.append(attr) + } + + //create decoration attributes + let horizontalItemsCount = maxHorizontalItemsCount(width: boundsSize.width) + var numberOfCells = cellCount + while numberOfCells % horizontalItemsCount != 0 { + //we need some empty cells dawg. + + let attr = self.layoutAttributesForDecorationView(ofKind: ASHorizontalScrollCellUX.TopSiteEmptyCellIdentifier, at: IndexPath(item: numberOfCells, section: 0)) + allAttributes.append(attr!) + numberOfCells += 1 + } + cachedAttributes = allAttributes + return allAttributes + } + + override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? { + return self.computeLayoutAttributesForCellAtIndexPath(indexPath) + } + + override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool { + cachedAttributes = nil + // Sometimes when the topsiteCell isnt on the screen the newbounds that it tries to layout in is 0 + // Resulting in incorrect layouts. Only layout when a valid width is given + return newBounds.width > 0 + } + + func computeLayoutAttributesForCellAtIndexPath(_ indexPath: IndexPath) -> UICollectionViewLayoutAttributes { + let row = indexPath.row + let bounds = self.collectionView!.bounds + + let verticalItemsCount = maxVerticalItemsCount(height: bounds.size.height) + let horizontalItemsCount = maxHorizontalItemsCount(width: bounds.size.width) + + let itemsPerPage = verticalItemsCount * horizontalItemsCount + + let columnPosition = row % horizontalItemsCount + let rowPosition = (row / horizontalItemsCount) % verticalItemsCount + let itemPage = Int(floor(Double(row)/Double(itemsPerPage))) + + let attr = UICollectionViewLayoutAttributes(forCellWith: indexPath) + var frame = CGRect.zero + frame.origin.x = CGFloat(itemPage) * bounds.size.width + CGFloat(columnPosition) * (itemSize.width + insets.left) + insets.left + frame.origin.y = CGFloat(rowPosition) * (itemSize.height + insets.top) + insets.top + frame.size = itemSize + attr.frame = frame + + return attr + } +} + +/* + Defines the number of items to show in topsites for different size classes. +*/ +struct ASTopSiteSourceUX { + static let verticalItemsForTraitSizes = [UIUserInterfaceSizeClass.compact: 1, UIUserInterfaceSizeClass.regular: 2, UIUserInterfaceSizeClass.unspecified: 0] + static let maxNumberOfPages = 2 + static let CellIdentifier = "TopSiteItemCell" +} + +protocol ASHorizontalLayoutDelegate { + func numberOfVerticalItems() -> Int + func numberOfHorizontalItems() -> Int +} + +/* + This Delegate/DataSource is used to manage the ASHorizontalScrollCell's UICollectionView. + This is left generic enough for it to be re used for other parts of Activity Stream. + */ + +class ASHorizontalScrollCellManager: NSObject, UICollectionViewDelegate, UICollectionViewDataSource, ASHorizontalLayoutDelegate { + + var content: [Site] = [] + + var urlPressedHandler: ((URL, IndexPath) -> Void)? + var pageChangedHandler: ((CGFloat) -> Void)? + + // The current traits that define the parent ViewController. Used to determine how many rows/columns should be created. + var currentTraits: UITraitCollection? + + // Size classes define how many items to show per row/column. + func numberOfVerticalItems() -> Int { + guard let traits = currentTraits else { + return 0 + } + return ASTopSiteSourceUX.verticalItemsForTraitSizes[traits.verticalSizeClass]! + } + + func numberOfHorizontalItems() -> Int { + guard let traits = currentTraits else { + return 0 + } + let isLandscape = UIInterfaceOrientationIsLandscape(UIApplication.shared.statusBarOrientation) + if UIDevice.current.userInterfaceIdiom == .phone { + if isLandscape { + return 8 + } else { + return 4 + } + } + // On iPad + // The number of items in a row is equal to the number of highlights in a row * 2 + var numItems: Int = Int(ASPanelUX.numberOfItemsPerRowForSizeClassIpad[traits.horizontalSizeClass]) + if UIInterfaceOrientationIsPortrait(UIApplication.shared.statusBarOrientation) || (traits.horizontalSizeClass == .compact && isLandscape) { + numItems = numItems - 1 + } + return numItems * 2 + } + + func numberOfSections(in collectionView: UICollectionView) -> Int { + return 1 + } + + func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { + return self.content.count + } + + func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { + let cell = collectionView.dequeueReusableCell(withReuseIdentifier: ASTopSiteSourceUX.CellIdentifier, for: indexPath) as! TopSiteItemCell + let contentItem = content[indexPath.row] + cell.configureWithTopSiteItem(contentItem) + return cell + } + + func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { + let contentItem = content[indexPath.row] + guard let url = contentItem.url.asURL else { + return + } + urlPressedHandler?(url, indexPath) + } + + func scrollViewDidScroll(_ scrollView: UIScrollView) { + let pageWidth = scrollView.frame.width + pageChangedHandler?(scrollView.contentOffset.x / pageWidth) + } + +} diff --git a/mobile/ios/Client/Frontend/Home/BookmarksPanel.swift b/mobile/ios/Client/Frontend/Home/BookmarksPanel.swift new file mode 100644 index 0000000000..12e2d059d4 --- /dev/null +++ b/mobile/ios/Client/Frontend/Home/BookmarksPanel.swift @@ -0,0 +1,593 @@ +/* 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 UIKit +import Storage +import Shared +import XCGLogger + +private let log = Logger.browserLogger + +let BookmarkStatusChangedNotification = "BookmarkStatusChangedNotification" + +// MARK: - Placeholder strings for Bug 1232810. + +let deleteWarningTitle = NSLocalizedString("This folder isn’t empty.", tableName: "BookmarkPanelDeleteConfirm", comment: "Title of the confirmation alert when the user tries to delete a folder that still contains bookmarks and/or folders.") +let deleteWarningDescription = NSLocalizedString("Are you sure you want to delete it and its contents?", tableName: "BookmarkPanelDeleteConfirm", comment: "Main body of the confirmation alert when the user tries to delete a folder that still contains bookmarks and/or folders.") +let deleteCancelButtonLabel = NSLocalizedString("Cancel", tableName: "BookmarkPanelDeleteConfirm", comment: "Button label to cancel deletion when the user tried to delete a non-empty folder.") +let deleteDeleteButtonLabel = NSLocalizedString("Delete", tableName: "BookmarkPanelDeleteConfirm", comment: "Button label for the button that deletes a folder and all of its children.") + +// Placeholder strings for Bug 1248034 +let emptyBookmarksText = NSLocalizedString("Bookmarks you save will show up here.", comment: "Status label for the empty Bookmarks state.") + +// MARK: - UX constants. + +struct BookmarksPanelUX { + static let BookmarkFolderHeaderViewChevronInset: CGFloat = 10 + static let BookmarkFolderChevronSize: CGFloat = 20 + static let BookmarkFolderChevronLineWidth: CGFloat = 2.0 + static let BookmarkFolderTextColor = UIColor(red: 92/255, green: 92/255, blue: 92/255, alpha: 1.0) + static let BookmarkFolderBGColor = UIColor(rgb: 0xf7f8f7).withAlphaComponent(0.3) + static let WelcomeScreenPadding: CGFloat = 15 + static let WelcomeScreenItemTextColor = UIColor.gray + static let WelcomeScreenItemWidth = 170 + static let SeparatorRowHeight: CGFloat = 0.5 + static let IconSize: CGFloat = 23 + static let IconBorderColor = UIColor(white: 0, alpha: 0.1) + static let IconBorderWidth: CGFloat = 0.5 +} + +class BookmarksPanel: SiteTableViewController, HomePanel { + weak var homePanelDelegate: HomePanelDelegate? + var source: BookmarksModel? + var parentFolders = [BookmarkFolder]() + var bookmarkFolder: BookmarkFolder? + var refreshControl: UIRefreshControl? + + fileprivate lazy var longPressRecognizer: UILongPressGestureRecognizer = { + return UILongPressGestureRecognizer(target: self, action: #selector(BookmarksPanel.longPress(_:))) + }() + fileprivate lazy var emptyStateOverlayView: UIView = self.createEmptyStateOverlayView() + + fileprivate let BookmarkFolderCellIdentifier = "BookmarkFolderIdentifier" + fileprivate let BookmarkSeparatorCellIdentifier = "BookmarkSeparatorIdentifier" + fileprivate let BookmarkFolderHeaderViewIdentifier = "BookmarkFolderHeaderIdentifier" + + init() { + super.init(nibName: nil, bundle: nil) + NotificationCenter.default.addObserver(self, selector: #selector(BookmarksPanel.notificationReceived(_:)), name: NotificationFirefoxAccountChanged, object: nil) + + self.tableView.register(SeparatorTableCell.self, forCellReuseIdentifier: BookmarkSeparatorCellIdentifier) + self.tableView.register(BookmarkFolderTableViewCell.self, forCellReuseIdentifier: BookmarkFolderCellIdentifier) + self.tableView.register(BookmarkFolderTableViewHeader.self, forHeaderFooterViewReuseIdentifier: BookmarkFolderHeaderViewIdentifier) + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + tableView.addGestureRecognizer(longPressRecognizer) + + self.tableView.accessibilityIdentifier = "Bookmarks List" + + self.refreshControl = UIRefreshControl() + self.tableView.addSubview(refreshControl!) + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + + refreshControl?.addTarget(self, action: #selector(BookmarksPanel.refreshBookmarks), for: .valueChanged) + + loadData() + } + + override func viewDidDisappear(_ animated: Bool) { + super.viewDidDisappear(animated) + refreshControl?.removeTarget(self, action: #selector(BookmarksPanel.refreshBookmarks), for: .valueChanged) + } + + func loadData() { + // If we've not already set a source for this panel, fetch a new model from + // the root; otherwise, just use the existing source to select a folder. + guard let source = self.source else { + // Get all the bookmarks split by folders + if let bookmarkFolder = bookmarkFolder { + profile.bookmarks.modelFactory >>== { $0.modelForFolder(bookmarkFolder).upon(self.onModelFetched) } + } else { + profile.bookmarks.modelFactory >>== { $0.modelForRoot().upon(self.onModelFetched) } + } + return + } + + if let bookmarkFolder = bookmarkFolder { + source.selectFolder(bookmarkFolder).upon(onModelFetched) + } else { + source.selectFolder(BookmarkRoots.MobileFolderGUID).upon(onModelFetched) + } + } + + func notificationReceived(_ notification: Notification) { + switch notification.name { + case NotificationFirefoxAccountChanged: + self.reloadData() + break + default: + // no need to do anything at all + log.warning("Received unexpected notification \(notification.name)") + break + } + } + + @objc fileprivate func refreshBookmarks() { + profile.syncManager.mirrorBookmarks().upon { (_) in + DispatchQueue.main.async { + self.loadData() + self.refreshControl?.endRefreshing() + } + } + } + + fileprivate func createEmptyStateOverlayView() -> UIView { + let overlayView = UIView() + overlayView.backgroundColor = UIColor.white + + let logoImageView = UIImageView(image: UIImage(named: "emptyBookmarks")) + overlayView.addSubview(logoImageView) + logoImageView.snp.makeConstraints { make in + make.centerX.equalTo(overlayView) + + // Sets proper top constraint for iPhone 6 in portait and for iPad. + make.centerY.equalTo(overlayView).offset(HomePanelUX.EmptyTabContentOffset).priority(100) + + // Sets proper top constraint for iPhone 4, 5 in portrait. + make.top.greaterThanOrEqualTo(overlayView).offset(50) + } + + let welcomeLabel = UILabel() + overlayView.addSubview(welcomeLabel) + welcomeLabel.text = emptyBookmarksText + welcomeLabel.textAlignment = NSTextAlignment.center + welcomeLabel.font = DynamicFontHelper.defaultHelper.DeviceFontLight + welcomeLabel.textColor = BookmarksPanelUX.WelcomeScreenItemTextColor + welcomeLabel.numberOfLines = 0 + welcomeLabel.adjustsFontSizeToFitWidth = true + + welcomeLabel.snp.makeConstraints { make in + make.centerX.equalTo(overlayView) + make.top.equalTo(logoImageView.snp.bottom).offset(BookmarksPanelUX.WelcomeScreenPadding) + make.width.equalTo(BookmarksPanelUX.WelcomeScreenItemWidth) + } + + return overlayView + } + + fileprivate func updateEmptyPanelState() { + if source?.current.count == 0 && source?.current.guid == BookmarkRoots.MobileFolderGUID { + if self.emptyStateOverlayView.superview == nil { + self.view.addSubview(self.emptyStateOverlayView) + self.view.bringSubview(toFront: self.emptyStateOverlayView) + self.emptyStateOverlayView.snp.makeConstraints { make -> Void in + make.edges.equalTo(self.tableView) + } + } + } else { + self.emptyStateOverlayView.removeFromSuperview() + } + } + + fileprivate func onModelFetched(_ result: Maybe) { + guard let model = result.successValue else { + self.onModelFailure(result.failureValue as Any) + return + } + self.onNewModel(model) + } + + fileprivate func onNewModel(_ model: BookmarksModel) { + if Thread.current.isMainThread { + self.source = model + self.tableView.reloadData() + return + } + + DispatchQueue.main.async { + self.source = model + self.tableView.reloadData() + self.updateEmptyPanelState() + } + } + + fileprivate func onModelFailure(_ e: Any) { + log.error("Error: failed to get data: \(e)") + } + + override func reloadData() { + self.source?.reloadData().upon(onModelFetched) + } + + @objc fileprivate func longPress(_ longPressGestureRecognizer: UILongPressGestureRecognizer) { + guard longPressGestureRecognizer.state == UIGestureRecognizerState.began else { return } + let touchPoint = longPressGestureRecognizer.location(in: tableView) + guard let indexPath = tableView.indexPathForRow(at: touchPoint) else { return } + presentContextMenu(for: indexPath) + } + + override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return source?.current.count ?? 0 + } + + override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + guard let source = source, let bookmark = source.current[indexPath.row] else { return super.tableView(tableView, cellForRowAt: indexPath) } + switch bookmark { + case let item as BookmarkItem: + let cell = super.tableView(tableView, cellForRowAt: indexPath) + if item.title.isEmpty { + cell.textLabel?.text = item.url + } else { + cell.textLabel?.text = item.title + } + if let url = bookmark.favicon?.url.asURL, url.scheme == "asset" { + cell.imageView?.image = UIImage(named: url.host!) + } else { + cell.imageView?.layer.borderColor = BookmarksPanelUX.IconBorderColor.cgColor + cell.imageView?.layer.borderWidth = BookmarksPanelUX.IconBorderWidth + let bookmarkURL = URL(string: item.url) + cell.imageView?.setIcon(bookmark.favicon, forURL: bookmarkURL, completed: { (color, url) in + if bookmarkURL == url { + cell.imageView?.image = cell.imageView?.image?.createScaled(CGSize(width: BookmarksPanelUX.IconSize, height: BookmarksPanelUX.IconSize)) + cell.imageView?.backgroundColor = color + cell.imageView?.contentMode = .center + } + }) + } + return cell + case is BookmarkSeparator: + return tableView.dequeueReusableCell(withIdentifier: BookmarkSeparatorCellIdentifier, for: indexPath) + case let bookmark as BookmarkFolder: + let cell = tableView.dequeueReusableCell(withIdentifier: BookmarkFolderCellIdentifier, for: indexPath) + cell.textLabel?.text = bookmark.title + return cell + default: + // This should never happen. + return super.tableView(tableView, cellForRowAt: indexPath) + } + } + + func tableView(_ tableView: UITableView, willDisplayCell cell: UITableViewCell, forRowAtIndexPath indexPath: IndexPath) { + if let cell = cell as? BookmarkFolderTableViewCell { + cell.textLabel?.font = DynamicFontHelper.defaultHelper.DeviceFontHistoryPanel + } + } + + override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { + // Don't show a header for the root + if source == nil || parentFolders.isEmpty { + return nil + } + guard let header = tableView.dequeueReusableHeaderFooterView(withIdentifier: BookmarkFolderHeaderViewIdentifier) as? BookmarkFolderTableViewHeader else { return nil } + + // register as delegate to ensure we get notified when the user interacts with this header + if header.delegate == nil { + header.delegate = self + } + + if parentFolders.count == 1 { + header.textLabel?.text = NSLocalizedString("Bookmarks", comment: "Panel accessibility label") + } else if let parentFolder = parentFolders.last { + header.textLabel?.text = parentFolder.title + } + + return header + } + + override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { + if let it = self.source?.current[indexPath.row], it is BookmarkSeparator { + return BookmarksPanelUX.SeparatorRowHeight + } + + return super.tableView(tableView, heightForRowAt: indexPath) + } + + override func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { + // Don't show a header for the root. If there's no root (i.e. source == nil), we'll also show no header. + if source == nil || parentFolders.isEmpty { + return 0 + } + + return SiteTableViewControllerUX.RowHeight + } + + func tableView(_ tableView: UITableView, willDisplayHeaderView view: UIView, forSection section: Int) { + if let header = view as? BookmarkFolderTableViewHeader { + // for some reason specifying the font in header view init is being ignored, so setting it here + header.textLabel?.font = DynamicFontHelper.defaultHelper.DeviceFontHistoryPanel + } + } + + override func tableView(_ tableView: UITableView, hasFullWidthSeparatorForRowAtIndexPath indexPath: IndexPath) -> Bool { + // Show a full-width border for cells above separators, so they don't have a weird step. + // Separators themselves already have a full-width border, but let's force the issue + // just in case. + let this = self.source?.current[indexPath.row] + if (indexPath.row + 1) < (self.source?.current.count)! { + let below = self.source?.current[indexPath.row + 1] + if this is BookmarkSeparator || below is BookmarkSeparator { + return true + } + } + return super.tableView(tableView, hasFullWidthSeparatorForRowAtIndexPath: indexPath) + } + + func tableView(_ tableView: UITableView, didSelectRowAtIndexPath indexPath: IndexPath) { + tableView.deselectRow(at: indexPath, animated: false) + guard let source = source else { + return + } + + let bookmark = source.current[indexPath.row] + + switch bookmark { + case let item as BookmarkItem: + homePanelDelegate?.homePanel(self, didSelectURLString: item.url, visitType: VisitType.bookmark) + LeanPlumClient.shared.track(event: .openedBookmark) + UnifiedTelemetry.recordEvent(category: .action, method: .open, object: .bookmark, value: .bookmarksPanel) + break + + case let folder as BookmarkFolder: + log.debug("Selected \(folder.guid)") + let nextController = BookmarksPanel() + nextController.parentFolders = parentFolders + [source.current] + nextController.bookmarkFolder = folder + nextController.homePanelDelegate = self.homePanelDelegate + nextController.profile = self.profile + source.modelFactory.uponQueue(DispatchQueue.main) { maybe in + guard let factory = maybe.successValue else { + // Nothing we can do. + return + } + let specificFactory = factory.factoryForIndex(indexPath.row, inFolder: source.current) + nextController.source = BookmarksModel(modelFactory: specificFactory, root: folder) + self.navigationController?.pushViewController(nextController, animated: true) + } + break + + default: + // You can't do anything with separators. + break + } + } + + func tableView(_ tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: IndexPath) { + // Intentionally blank. Required to use UITableViewRowActions + } + + private func editingStyleforRow(atIndexPath indexPath: IndexPath) -> UITableViewCellEditingStyle { + guard let source = source else { + return .none + } + + if source.current[indexPath.row] is BookmarkSeparator { + // Because the deletion block is too big. + return .none + } + + if source.current.itemIsEditableAtIndex(indexPath.row) { + return .delete + } + + return .none + } + + func tableView(_ tableView: UITableView, editingStyleForRowAtIndexPath indexPath: IndexPath) -> UITableViewCellEditingStyle { + return editingStyleforRow(atIndexPath: indexPath) + } + + func tableView(_ tableView: UITableView, editActionsForRowAtIndexPath indexPath: IndexPath) -> [AnyObject]? { + let editingStyle = editingStyleforRow(atIndexPath: indexPath) + guard let source = self.source, editingStyle == .delete else { + return nil + } + + let title = NSLocalizedString("Delete", tableName: "BookmarkPanel", comment: "Action button for deleting bookmarks in the bookmarks panel.") + + let delete = UITableViewRowAction(style: UITableViewRowActionStyle.default, title: title, handler: { (action, indexPath) in + self.deleteBookmark(indexPath: indexPath, source: source) + UnifiedTelemetry.recordEvent(category: .action, method: .delete, object: .bookmark, value: .bookmarksPanel, extras: ["gesture": "swipe"]) + }) + + return [delete] + } + + func pinTopSite(_ site: Site) { + _ = profile.history.addPinnedTopSite(site).value + } + + func deleteBookmark(indexPath: IndexPath, source: BookmarksModel) { + guard let bookmark = source.current[indexPath.row] else { + return + } + + assert(!(bookmark is BookmarkFolder)) + if bookmark is BookmarkFolder { + // TODO: check whether the folder is empty (excluding separators). If it isn't + // then we must ask the user to confirm. Bug 1232810. + log.debug("Not deleting folder.") + return + } + + log.debug("Removing rows \(indexPath).") + + // Block to do this -- this is UI code. + guard let factory = source.modelFactory.value.successValue else { + log.error("Couldn't get model factory. This is unexpected.") + self.onModelFailure(DatabaseError(description: "Unable to get factory.")) + return + } + + let specificFactory = factory.factoryForIndex(indexPath.row, inFolder: source.current) + if let err = specificFactory.removeByGUID(bookmark.guid).value.failureValue { + log.debug("Failed to remove \(bookmark.guid).") + self.onModelFailure(err) + return + } + + self.tableView.beginUpdates() + self.source = source.removeGUIDFromCurrent(bookmark.guid) + self.tableView.deleteRows(at: [indexPath], with: UITableViewRowAnimation.left) + self.tableView.endUpdates() + self.updateEmptyPanelState() + + NotificationCenter.default.post(name: NSNotification.Name(rawValue: BookmarkStatusChangedNotification), object: bookmark, userInfo: ["added": false] + ) + } +} + +extension BookmarksPanel: HomePanelContextMenu { + func presentContextMenu(for site: Site, with indexPath: IndexPath, completionHandler: @escaping () -> PhotonActionSheet?) { + guard let contextMenu = completionHandler() else { return } + self.present(contextMenu, animated: true, completion: nil) + } + + func getSiteDetails(for indexPath: IndexPath) -> Site? { + guard let bookmarkItem = source?.current[indexPath.row] as? BookmarkItem else { return nil } + let site = Site(url: bookmarkItem.url, title: bookmarkItem.title, bookmarked: true, guid: bookmarkItem.guid) + site.icon = bookmarkItem.favicon + return site + } + + func getContextMenuActions(for site: Site, with indexPath: IndexPath) -> [PhotonActionSheetItem]? { + guard var actions = getDefaultContextMenuActions(for: site, homePanelDelegate: homePanelDelegate) else { return nil } + + let pinTopSite = PhotonActionSheetItem(title: Strings.PinTopsiteActionTitle, iconString: "action_pin", handler: { action in + self.pinTopSite(site) + }) + + actions.append(pinTopSite) + + // Only local bookmarks can be removed + guard let source = source else { return nil } + if source.current.itemIsEditableAtIndex(indexPath.row) { + let removeAction = PhotonActionSheetItem(title: Strings.RemoveBookmarkContextMenuTitle, iconString: "action_bookmark_remove", handler: { action in + self.deleteBookmark(indexPath: indexPath, source: source) + UnifiedTelemetry.recordEvent(category: .action, method: .delete, object: .bookmark, value: .bookmarksPanel, extras: ["gesture": "long-press"]) + }) + actions.append(removeAction) + } + return actions + } +} + +private protocol BookmarkFolderTableViewHeaderDelegate { + func didSelectHeader() +} + +extension BookmarksPanel: BookmarkFolderTableViewHeaderDelegate { + fileprivate func didSelectHeader() { + _ = self.navigationController?.popViewController(animated: true) + } +} + +class BookmarkFolderTableViewCell: TwoLineTableViewCell { + + override init(style: UITableViewCellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + self.backgroundColor = BookmarksPanelUX.BookmarkFolderBGColor + textLabel?.backgroundColor = UIColor.clear + textLabel?.tintColor = BookmarksPanelUX.BookmarkFolderTextColor + + imageView?.image = UIImage(named: "bookmarkFolder") + accessoryType = UITableViewCellAccessoryType.disclosureIndicator + separatorInset = UIEdgeInsets.zero + } + + override func layoutSubviews() { + super.layoutSubviews() + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +fileprivate class BookmarkFolderTableViewHeader: UITableViewHeaderFooterView { + var delegate: BookmarkFolderTableViewHeaderDelegate? + + lazy var titleLabel: UILabel = { + let label = UILabel() + label.textColor = UIConstants.HighlightBlue + return label + }() + + lazy var chevron: ChevronView = { + let chevron = ChevronView(direction: .left) + chevron.tintColor = UIConstants.HighlightBlue + chevron.lineWidth = BookmarksPanelUX.BookmarkFolderChevronLineWidth + return chevron + }() + + lazy var topBorder: UIView = { + let view = UIView() + view.backgroundColor = SiteTableViewControllerUX.HeaderBorderColor + return view + }() + + lazy var bottomBorder: UIView = { + let view = UIView() + view.backgroundColor = SiteTableViewControllerUX.HeaderBorderColor + return view + }() + + override var textLabel: UILabel? { + return titleLabel + } + + override init(reuseIdentifier: String?) { + super.init(reuseIdentifier: reuseIdentifier) + + isUserInteractionEnabled = true + + let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(BookmarkFolderTableViewHeader.viewWasTapped(_:))) + tapGestureRecognizer.numberOfTapsRequired = 1 + addGestureRecognizer(tapGestureRecognizer) + + addSubview(topBorder) + addSubview(bottomBorder) + contentView.addSubview(chevron) + contentView.addSubview(titleLabel) + + chevron.snp.makeConstraints { make in + make.left.equalTo(contentView).offset(BookmarksPanelUX.BookmarkFolderHeaderViewChevronInset) + make.centerY.equalTo(contentView) + make.size.equalTo(BookmarksPanelUX.BookmarkFolderChevronSize) + } + + titleLabel.snp.makeConstraints { make in + make.left.equalTo(chevron.snp.right).offset(BookmarksPanelUX.BookmarkFolderHeaderViewChevronInset) + make.right.greaterThanOrEqualTo(contentView).offset(-BookmarksPanelUX.BookmarkFolderHeaderViewChevronInset) + make.centerY.equalTo(contentView) + } + + topBorder.snp.makeConstraints { make in + make.left.right.equalTo(self) + make.top.equalTo(self).offset(-0.5) + make.height.equalTo(0.5) + } + + bottomBorder.snp.makeConstraints { make in + make.left.right.bottom.equalTo(self) + make.height.equalTo(0.5) + } + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + @objc fileprivate func viewWasTapped(_ gestureRecognizer: UITapGestureRecognizer) { + delegate?.didSelectHeader() + } +} diff --git a/mobile/ios/Client/Frontend/Home/HistoryPanel.swift b/mobile/ios/Client/Frontend/Home/HistoryPanel.swift new file mode 100644 index 0000000000..1ba5499263 --- /dev/null +++ b/mobile/ios/Client/Frontend/Home/HistoryPanel.swift @@ -0,0 +1,574 @@ +/* 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 UIKit + +import Shared +import Storage +import XCGLogger +import Deferred + +private typealias SectionNumber = Int +private typealias CategoryNumber = Int +private typealias CategorySpec = (section: SectionNumber?, rows: Int, offset: Int) + +private struct HistoryPanelUX { + static let WelcomeScreenItemTextColor = UIColor.gray + static let WelcomeScreenItemWidth = 170 + static let IconSize = 23 + static let IconBorderColor = UIColor(white: 0, alpha: 0.1) + static let IconBorderWidth: CGFloat = 0.5 +} + +private func getDate(_ dayOffset: Int) -> Date { + let calendar = Calendar(identifier: Calendar.Identifier.gregorian) + let nowComponents = (calendar as NSCalendar).components([.year, .month, .day], from: Date()) + let today = calendar.date(from: nowComponents)! + return (calendar as NSCalendar).date(byAdding: NSCalendar.Unit.day, value: dayOffset, to: today, options: [])! +} + +class HistoryPanel: SiteTableViewController, HomePanel { + weak var homePanelDelegate: HomePanelDelegate? + private var currentSyncedDevicesCount: Int? + + var events = [NotificationFirefoxAccountChanged, NotificationPrivateDataClearedHistory, NotificationDynamicFontChanged] + var refreshControl: UIRefreshControl? + + fileprivate lazy var longPressRecognizer: UILongPressGestureRecognizer = { + return UILongPressGestureRecognizer(target: self, action: #selector(HistoryPanel.longPress(_:))) + }() + + private lazy var emptyStateOverlayView: UIView = self.createEmptyStateOverlayView() + private let QueryLimit = 100 + private let NumSections = 5 + private let Today = getDate(0) + private let Yesterday = getDate(-1) + private let ThisWeek = getDate(-7) + private var categories: [CategorySpec] = [CategorySpec]() // Category number (index) -> (UI section, row count, cursor offset). + private var sectionLookup = [SectionNumber: CategoryNumber]() // Reverse lookup from UI section to data category. + + var syncDetailText = "" + var hasRecentlyClosed: Bool { + return self.profile.recentlyClosedTabs.tabs.count > 0 + } + + // MARK: - Lifecycle + init() { + super.init(nibName: nil, bundle: nil) + events.forEach { NotificationCenter.default.addObserver(self, selector: #selector(HistoryPanel.notificationReceived(_:)), name: $0, object: nil) } + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + tableView.addGestureRecognizer(longPressRecognizer) + tableView.accessibilityIdentifier = "History List" + updateSyncedDevicesCount().uponQueue(DispatchQueue.main) { result in + self.updateNumberOfSyncedDevices(self.currentSyncedDevicesCount) + } + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + + // Add a refresh control if the user is logged in and the control was not added before. If the user is not + // logged in, remove any existing control but only when it is not currently refreshing. Otherwise, wait for + // the refresh to finish before removing the control. + if profile.hasSyncableAccount() && refreshControl == nil { + addRefreshControl() + } else if refreshControl?.isRefreshing == false { + removeRefreshControl() + } + + if profile.hasSyncableAccount() { + syncDetailText = " " + updateSyncedDevicesCount().uponQueue(DispatchQueue.main) { result in + self.updateNumberOfSyncedDevices(self.currentSyncedDevicesCount) + } + } else { + syncDetailText = "" + } + } + + @objc fileprivate func longPress(_ longPressGestureRecognizer: UILongPressGestureRecognizer) { + guard longPressGestureRecognizer.state == UIGestureRecognizerState.began else { return } + let touchPoint = longPressGestureRecognizer.location(in: tableView) + guard let indexPath = tableView.indexPathForRow(at: touchPoint) else { return } + + if indexPath.section != 0 { + presentContextMenu(for: indexPath) + } + } + + // MARK: - History Data Store + func updateNumberOfSyncedDevices(_ count: Int?) { + if let count = count, count > 0 { + syncDetailText = String.localizedStringWithFormat(Strings.SyncedTabsTableViewCellDescription, count) + } else { + syncDetailText = "" + } + self.tableView.reloadRows(at: [IndexPath(row: 1, section: 0)], with: .automatic) + } + + func updateSyncedDevicesCount() -> Success { + return chainDeferred(self.profile.getCachedClientsAndTabs()) { tabsAndClients in + self.currentSyncedDevicesCount = tabsAndClients.count + return succeed() + } + } + + func notificationReceived(_ notification: Notification) { + reloadData() + + switch notification.name { + case NotificationFirefoxAccountChanged, NotificationPrivateDataClearedHistory: + if self.profile.hasSyncableAccount() { + resyncHistory() + } + break + case NotificationDynamicFontChanged: + if emptyStateOverlayView.superview != nil { + emptyStateOverlayView.removeFromSuperview() + } + emptyStateOverlayView = createEmptyStateOverlayView() + resyncHistory() + break + default: + // no need to do anything at all + print("Error: Received unexpected notification \(notification.name)") + break + } + } + + private func fetchData() -> Deferred>> { + return profile.history.getSitesByLastVisit(QueryLimit) + } + + private func setData(_ data: Cursor) { + self.data = data + self.computeSectionOffsets() + } + + func resyncHistory() { + profile.syncManager.syncHistory().uponQueue(DispatchQueue.main) { result in + if result.isSuccess { + self.reloadData() + } else { + self.endRefreshing() + } + + self.updateSyncedDevicesCount().uponQueue(DispatchQueue.main) { result in + self.updateNumberOfSyncedDevices(self.currentSyncedDevicesCount) + } + } + } + + // MARK: - Refreshing TableView + func addRefreshControl() { + let refresh = UIRefreshControl() + refresh.addTarget(self, action: #selector(HistoryPanel.refresh), for: UIControlEvents.valueChanged) + self.refreshControl = refresh + self.tableView.refreshControl = refresh + } + + func removeRefreshControl() { + self.tableView.refreshControl = nil + self.refreshControl = nil + } + + func endRefreshing() { + // Always end refreshing, even if we failed! + self.refreshControl?.endRefreshing() + + // Remove the refresh control if the user has logged out in the meantime + if !self.profile.hasSyncableAccount() { + self.removeRefreshControl() + } + } + + @objc func refresh() { + self.refreshControl?.beginRefreshing() + resyncHistory() + } + + override func reloadData() { + self.fetchData().uponQueue(DispatchQueue.main) { result in + if let data = result.successValue { + self.setData(data) + self.tableView.reloadData() + self.updateEmptyPanelState() + } + self.endRefreshing() + } + } + + // MARK: - Empty State + private func updateEmptyPanelState() { + if data.count == 0 { + if self.emptyStateOverlayView.superview == nil { + self.tableView.addSubview(self.emptyStateOverlayView) + self.emptyStateOverlayView.snp.makeConstraints { make -> Void in + make.left.right.bottom.equalTo(self.view) + make.top.equalTo(self.view).offset(100) + } + } + } else { + self.tableView.alwaysBounceVertical = true + self.emptyStateOverlayView.removeFromSuperview() + } + } + + private func createEmptyStateOverlayView() -> UIView { + let overlayView = UIView() + overlayView.backgroundColor = UIColor.white + + let welcomeLabel = UILabel() + overlayView.addSubview(welcomeLabel) + welcomeLabel.text = Strings.HistoryPanelEmptyStateTitle + welcomeLabel.textAlignment = NSTextAlignment.center + welcomeLabel.font = DynamicFontHelper.defaultHelper.DeviceFontLight + welcomeLabel.textColor = HistoryPanelUX.WelcomeScreenItemTextColor + welcomeLabel.numberOfLines = 0 + welcomeLabel.adjustsFontSizeToFitWidth = true + + welcomeLabel.snp.makeConstraints { make in + make.centerX.equalTo(overlayView) + // Sets proper top constraint for iPhone 6 in portait and for iPad. + make.centerY.equalTo(overlayView).offset(HomePanelUX.EmptyTabContentOffset).priority(100) + // Sets proper top constraint for iPhone 4, 5 in portrait. + make.top.greaterThanOrEqualTo(overlayView).offset(50) + make.width.equalTo(HistoryPanelUX.WelcomeScreenItemWidth) + } + return overlayView + } + + // MARK: - TableView Row Helpers + func computeSectionOffsets() { + var counts = [Int](repeating: 0, count: NumSections) + + // Loop over all the data. Record the start of each "section" of our list. + for i in 0.. 0 { + self.categories.append((section: section, rows: count, offset: offset)) + sectionLookup[section] = i + offset += count + section += 1 + } else { + self.categories.append((section: nil, rows: 0, offset: offset)) + } + } + } + + fileprivate func siteForIndexPath(_ indexPath: IndexPath) -> Site? { + let offset = self.categories[sectionLookup[indexPath.section]!].offset + return data[indexPath.row + offset] + } + + private func categoryForDate(_ date: MicrosecondTimestamp) -> Int { + let date = Double(date) + if date > (1000000 * Today.timeIntervalSince1970) { + return 0 + } + if date > (1000000 * Yesterday.timeIntervalSince1970) { + return 1 + } + if date > (1000000 * ThisWeek.timeIntervalSince1970) { + return 2 + } + return 3 + } + + private func isInCategory(_ date: MicrosecondTimestamp, category: Int) -> Bool { + return self.categoryForDate(date) == category + } + + // UI sections disappear as categories empty. We need to translate back and forth. + private func uiSectionToCategory(_ section: SectionNumber) -> CategoryNumber { + for i in 0.. SectionNumber? { + return self.categories[category].section + } + + // MARK: - TableView Delegate / DataSource + override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let cell = super.tableView(tableView, cellForRowAt: indexPath) + cell.accessoryType = UITableViewCellAccessoryType.none + + if indexPath.section == 0 { + cell.imageView!.layer.borderWidth = 0 + return indexPath.row == 0 ? configureRecentlyClosed(cell, for: indexPath) : configureSyncedTabs(cell, for: indexPath) + } else { + return configureSite(cell, for: indexPath) + } + } + + func configureRecentlyClosed(_ cell: UITableViewCell, for indexPath: IndexPath) -> UITableViewCell { + cell.accessoryType = UITableViewCellAccessoryType.disclosureIndicator + cell.textLabel!.text = Strings.RecentlyClosedTabsButtonTitle + cell.detailTextLabel!.text = "" + cell.imageView!.image = UIImage(named: "recently_closed") + cell.imageView?.backgroundColor = UIColor.white + if !hasRecentlyClosed { + cell.textLabel?.alpha = 0.5 + cell.imageView!.alpha = 0.5 + cell.selectionStyle = .none + } + cell.accessibilityIdentifier = "HistoryPanel.recentlyClosedCell" + return cell + } + + func configureSyncedTabs(_ cell: UITableViewCell, for indexPath: IndexPath) -> UITableViewCell { + cell.accessoryType = UITableViewCellAccessoryType.disclosureIndicator + cell.textLabel!.text = Strings.SyncedTabsTableViewCellTitle + cell.detailTextLabel!.text = self.syncDetailText + cell.imageView!.image = UIImage(named: "synced_devices") + cell.imageView?.backgroundColor = UIColor.white + cell.accessibilityIdentifier = "HistoryPanel.syncedDevicesCell" + return cell + } + + func configureSite(_ cell: UITableViewCell, for indexPath: IndexPath) -> UITableViewCell { + if let site = siteForIndexPath(indexPath), let cell = cell as? TwoLineTableViewCell { + cell.setLines(site.title, detailText: site.url) + + cell.imageView!.layer.borderColor = HistoryPanelUX.IconBorderColor.cgColor + cell.imageView!.layer.borderWidth = HistoryPanelUX.IconBorderWidth + cell.imageView?.setIcon(site.icon, forURL: site.tileURL, completed: { (color, url) in + if site.tileURL == url { + cell.imageView?.image = cell.imageView?.image?.createScaled(CGSize(width: HistoryPanelUX.IconSize, height: HistoryPanelUX.IconSize)) + cell.imageView?.backgroundColor = color + cell.imageView?.contentMode = .center + } + }) + } + return cell + } + + func numberOfSectionsInTableView(_ tableView: UITableView) -> Int { + var count = 1 + for category in self.categories where category.rows > 0 { + count += 1 + } + return count + } + + func tableView(_ tableView: UITableView, didSelectRowAtIndexPath indexPath: IndexPath) { + if indexPath.section == 0 { + self.tableView.deselectRow(at: indexPath, animated: true) + return indexPath.row == 0 ? self.showRecentlyClosed() : self.showSyncedTabs() + } + if let site = self.siteForIndexPath(indexPath), let url = URL(string: site.url) { + let visitType = VisitType.typed // Means History, too. + if let homePanelDelegate = homePanelDelegate { + homePanelDelegate.homePanel(self, didSelectURL: url, visitType: visitType) + } + return + } + print("Error: No site or no URL when selecting row.") + } + + func pinTopSite(_ site: Site) { + _ = profile.history.addPinnedTopSite(site).value + } + + func showSyncedTabs() { + let nextController = RemoteTabsPanel() + nextController.homePanelDelegate = self.homePanelDelegate + nextController.profile = self.profile + self.refreshControl?.endRefreshing() + self.navigationController?.pushViewController(nextController, animated: true) + } + + func showRecentlyClosed() { + guard hasRecentlyClosed else { + return + } + let nextController = RecentlyClosedTabsPanel() + nextController.homePanelDelegate = self.homePanelDelegate + nextController.profile = self.profile + self.refreshControl?.endRefreshing() + self.navigationController?.pushViewController(nextController, animated: true) + } + + func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { + var title = String() + switch sectionLookup[section]! { + case 0: return nil + case 1: title = NSLocalizedString("Today", comment: "History tableview section header") + case 2: title = NSLocalizedString("Yesterday", comment: "History tableview section header") + case 3: title = NSLocalizedString("Last week", comment: "History tableview section header") + case 4: title = NSLocalizedString("Last month", comment: "History tableview section header") + default: + assertionFailure("Invalid history section \(section)") + } + return title + } + + override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { + if section == 0 { + return nil + } + return super.tableView(tableView, viewForHeaderInSection: section) + } + + override func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { + if section == 0 { + return 0 + } + return super.tableView(tableView, heightForHeaderInSection: section) + } + + override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + if section == 0 { + return 2 + } + return self.categories[uiSectionToCategory(section)].rows + } + + func tableView(_ tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: IndexPath) { + // Intentionally blank. Required to use UITableViewRowActions + } + + fileprivate func removeHistoryForURLAtIndexPath(indexPath: IndexPath) { + if let site = self.siteForIndexPath(indexPath) { + // Why the dispatches? Because we call success and failure on the DB + // queue, and so calling anything else that calls through to the DB will + // deadlock. This problem will go away when the history API switches to + // Deferred instead of using callbacks. + self.profile.history.removeHistoryForURL(site.url) + .upon { res in + self.fetchData().uponQueue(DispatchQueue.main) { result in + // If a section will be empty after removal, we must remove the section itself. + if let data = result.successValue { + + let oldCategories = self.categories + self.data = data + self.computeSectionOffsets() + + let sectionsToDelete = NSMutableIndexSet() + var rowsToDelete = [IndexPath]() + let sectionsToAdd = NSMutableIndexSet() + var rowsToAdd = [IndexPath]() + + for (index, category) in self.categories.enumerated() { + let oldCategory = oldCategories[index] + + // don't bother if we're not displaying this category + if oldCategory.section == nil && category.section == nil { + continue + } + + // 1. add a new section if the section didn't previously exist + if oldCategory.section == nil && category.section != oldCategory.section { + sectionsToAdd.add(category.section!) + } + + // 2. add a new row if there are more rows now than there were before + if oldCategory.rows < category.rows { + rowsToAdd.append(IndexPath(row: category.rows-1, section: category.section!)) + } + + // if we're dealing with the section where the row was deleted: + // 1. if the category no longer has a section, then we need to delete the entire section + // 2. delete a row if the number of rows has been reduced + // 3. delete the selected row and add a new one on the bottom of the section if the number of rows has stayed the same + if oldCategory.section == indexPath.section { + if category.section == nil { + sectionsToDelete.add(indexPath.section) + } else if oldCategory.section == category.section { + if oldCategory.rows > category.rows { + rowsToDelete.append(indexPath) + } else if category.rows == oldCategory.rows { + rowsToDelete.append(indexPath) + rowsToAdd.append(IndexPath(row: category.rows-1, section: indexPath.section)) + } + } + } + } + + self.tableView.beginUpdates() + if sectionsToAdd.count > 0 { + self.tableView.insertSections(sectionsToAdd as IndexSet, with: UITableViewRowAnimation.left) + } + if sectionsToDelete.count > 0 { + self.tableView.deleteSections(sectionsToDelete as IndexSet, with: UITableViewRowAnimation.right) + } + if !rowsToDelete.isEmpty { + self.tableView.deleteRows(at: rowsToDelete, with: UITableViewRowAnimation.right) + } + + if !rowsToAdd.isEmpty { + self.tableView.insertRows(at: rowsToAdd, with: UITableViewRowAnimation.right) + } + + self.tableView.endUpdates() + self.updateEmptyPanelState() + } + } + } + } + } + + func tableView(_ tableView: UITableView, editActionsForRowAtIndexPath indexPath: IndexPath) -> [AnyObject]? { + if indexPath.section == 0 { + return [] + } + let title = NSLocalizedString("Delete", tableName: "HistoryPanel", comment: "Action button for deleting history entries in the history panel.") + + let delete = UITableViewRowAction(style: UITableViewRowActionStyle.default, title: title, handler: { (action, indexPath) in + self.removeHistoryForURLAtIndexPath(indexPath: indexPath) + }) + return [delete] + } +} + +extension HistoryPanel: HomePanelContextMenu { + func presentContextMenu(for site: Site, with indexPath: IndexPath, completionHandler: @escaping () -> PhotonActionSheet?) { + guard let contextMenu = completionHandler() else { return } + self.present(contextMenu, animated: true, completion: nil) + } + + func getSiteDetails(for indexPath: IndexPath) -> Site? { + return siteForIndexPath(indexPath) + } + + func getContextMenuActions(for site: Site, with indexPath: IndexPath) -> [PhotonActionSheetItem]? { + guard var actions = getDefaultContextMenuActions(for: site, homePanelDelegate: homePanelDelegate) else { return nil } + + let removeAction = PhotonActionSheetItem(title: Strings.DeleteFromHistoryContextMenuTitle, iconString: "action_delete", handler: { action in + self.removeHistoryForURLAtIndexPath(indexPath: indexPath) + }) + + let pinTopSite = PhotonActionSheetItem(title: Strings.PinTopsiteActionTitle, iconString: "action_pin", handler: { action in + self.pinTopSite(site) + }) + actions.append(pinTopSite) + actions.append(removeAction) + return actions + } +} diff --git a/mobile/ios/Client/Frontend/Home/Home.xcassets/AddToReadingListCircle.imageset/Contents.json b/mobile/ios/Client/Frontend/Home/Home.xcassets/AddToReadingListCircle.imageset/Contents.json new file mode 100644 index 0000000000..d7def16a3e --- /dev/null +++ b/mobile/ios/Client/Frontend/Home/Home.xcassets/AddToReadingListCircle.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "reading list add circle.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "reading list add circle@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "reading list add circle@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/mobile/ios/Client/Frontend/Home/Home.xcassets/AddToReadingListCircle.imageset/reading list add circle.png b/mobile/ios/Client/Frontend/Home/Home.xcassets/AddToReadingListCircle.imageset/reading list add circle.png new file mode 100644 index 0000000000..76724fc007 Binary files /dev/null and b/mobile/ios/Client/Frontend/Home/Home.xcassets/AddToReadingListCircle.imageset/reading list add circle.png differ diff --git a/mobile/ios/Client/Frontend/Home/Home.xcassets/AddToReadingListCircle.imageset/reading list add circle@2x.png b/mobile/ios/Client/Frontend/Home/Home.xcassets/AddToReadingListCircle.imageset/reading list add circle@2x.png new file mode 100644 index 0000000000..73a6405add Binary files /dev/null and b/mobile/ios/Client/Frontend/Home/Home.xcassets/AddToReadingListCircle.imageset/reading list add circle@2x.png differ diff --git a/mobile/ios/Client/Frontend/Home/Home.xcassets/AddToReadingListCircle.imageset/reading list add circle@3x.png b/mobile/ios/Client/Frontend/Home/Home.xcassets/AddToReadingListCircle.imageset/reading list add circle@3x.png new file mode 100644 index 0000000000..41ff52928f Binary files /dev/null and b/mobile/ios/Client/Frontend/Home/Home.xcassets/AddToReadingListCircle.imageset/reading list add circle@3x.png differ diff --git a/mobile/ios/Client/Frontend/Home/Home.xcassets/Contents.json b/mobile/ios/Client/Frontend/Home/Home.xcassets/Contents.json new file mode 100644 index 0000000000..da4a164c91 --- /dev/null +++ b/mobile/ios/Client/Frontend/Home/Home.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/mobile/ios/Client/Frontend/Home/Home.xcassets/ReaderModeCircle.imageset/Contents.json b/mobile/ios/Client/Frontend/Home/Home.xcassets/ReaderModeCircle.imageset/Contents.json new file mode 100644 index 0000000000..a37eb57cfe --- /dev/null +++ b/mobile/ios/Client/Frontend/Home/Home.xcassets/ReaderModeCircle.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "reader view circle.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "reader view circle@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "reader view circle@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/mobile/ios/Client/Frontend/Home/Home.xcassets/ReaderModeCircle.imageset/reader view circle.png b/mobile/ios/Client/Frontend/Home/Home.xcassets/ReaderModeCircle.imageset/reader view circle.png new file mode 100644 index 0000000000..97e8d25092 Binary files /dev/null and b/mobile/ios/Client/Frontend/Home/Home.xcassets/ReaderModeCircle.imageset/reader view circle.png differ diff --git a/mobile/ios/Client/Frontend/Home/Home.xcassets/ReaderModeCircle.imageset/reader view circle@2x.png b/mobile/ios/Client/Frontend/Home/Home.xcassets/ReaderModeCircle.imageset/reader view circle@2x.png new file mode 100644 index 0000000000..725797ea1e Binary files /dev/null and b/mobile/ios/Client/Frontend/Home/Home.xcassets/ReaderModeCircle.imageset/reader view circle@2x.png differ diff --git a/mobile/ios/Client/Frontend/Home/Home.xcassets/ReaderModeCircle.imageset/reader view circle@3x.png b/mobile/ios/Client/Frontend/Home/Home.xcassets/ReaderModeCircle.imageset/reader view circle@3x.png new file mode 100644 index 0000000000..ff2654b020 Binary files /dev/null and b/mobile/ios/Client/Frontend/Home/Home.xcassets/ReaderModeCircle.imageset/reader view circle@3x.png differ diff --git a/mobile/ios/Client/Frontend/Home/Home.xcassets/clear.imageset/Contents.json b/mobile/ios/Client/Frontend/Home/Home.xcassets/clear.imageset/Contents.json new file mode 100644 index 0000000000..5ede011af1 --- /dev/null +++ b/mobile/ios/Client/Frontend/Home/Home.xcassets/clear.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "clear.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "clear@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "clear@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/mobile/ios/Client/Frontend/Home/Home.xcassets/clear.imageset/clear.png b/mobile/ios/Client/Frontend/Home/Home.xcassets/clear.imageset/clear.png new file mode 100644 index 0000000000..b0ebd2762d Binary files /dev/null and b/mobile/ios/Client/Frontend/Home/Home.xcassets/clear.imageset/clear.png differ diff --git a/mobile/ios/Client/Frontend/Home/Home.xcassets/clear.imageset/clear@2x.png b/mobile/ios/Client/Frontend/Home/Home.xcassets/clear.imageset/clear@2x.png new file mode 100644 index 0000000000..ecaf5fa679 Binary files /dev/null and b/mobile/ios/Client/Frontend/Home/Home.xcassets/clear.imageset/clear@2x.png differ diff --git a/mobile/ios/Client/Frontend/Home/Home.xcassets/clear.imageset/clear@3x.png b/mobile/ios/Client/Frontend/Home/Home.xcassets/clear.imageset/clear@3x.png new file mode 100644 index 0000000000..965e526caf Binary files /dev/null and b/mobile/ios/Client/Frontend/Home/Home.xcassets/clear.imageset/clear@3x.png differ diff --git a/mobile/ios/Client/Frontend/Home/Home.xcassets/emptyBookmarks.imageset/BookmarksEmptyPanel.png b/mobile/ios/Client/Frontend/Home/Home.xcassets/emptyBookmarks.imageset/BookmarksEmptyPanel.png new file mode 100644 index 0000000000..65d0e9ca90 Binary files /dev/null and b/mobile/ios/Client/Frontend/Home/Home.xcassets/emptyBookmarks.imageset/BookmarksEmptyPanel.png differ diff --git a/mobile/ios/Client/Frontend/Home/Home.xcassets/emptyBookmarks.imageset/BookmarksEmptyPanel@2x.png b/mobile/ios/Client/Frontend/Home/Home.xcassets/emptyBookmarks.imageset/BookmarksEmptyPanel@2x.png new file mode 100644 index 0000000000..7b2cc29342 Binary files /dev/null and b/mobile/ios/Client/Frontend/Home/Home.xcassets/emptyBookmarks.imageset/BookmarksEmptyPanel@2x.png differ diff --git a/mobile/ios/Client/Frontend/Home/Home.xcassets/emptyBookmarks.imageset/BookmarksEmptyPanel@3x.png b/mobile/ios/Client/Frontend/Home/Home.xcassets/emptyBookmarks.imageset/BookmarksEmptyPanel@3x.png new file mode 100644 index 0000000000..119433323f Binary files /dev/null and b/mobile/ios/Client/Frontend/Home/Home.xcassets/emptyBookmarks.imageset/BookmarksEmptyPanel@3x.png differ diff --git a/mobile/ios/Client/Frontend/Home/Home.xcassets/emptyBookmarks.imageset/Contents.json b/mobile/ios/Client/Frontend/Home/Home.xcassets/emptyBookmarks.imageset/Contents.json new file mode 100644 index 0000000000..69ddbc0672 --- /dev/null +++ b/mobile/ios/Client/Frontend/Home/Home.xcassets/emptyBookmarks.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "BookmarksEmptyPanel.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "BookmarksEmptyPanel@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "BookmarksEmptyPanel@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/mobile/ios/Client/Frontend/Home/Home.xcassets/emptyHistory.imageset/Contents.json b/mobile/ios/Client/Frontend/Home/Home.xcassets/emptyHistory.imageset/Contents.json new file mode 100644 index 0000000000..86f95699c8 --- /dev/null +++ b/mobile/ios/Client/Frontend/Home/Home.xcassets/emptyHistory.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x", + "filename" : "historyEmptyPanel.png" + }, + { + "idiom" : "universal", + "scale" : "2x", + "filename" : "historyEmptyPanel@2x.png" + }, + { + "idiom" : "universal", + "scale" : "3x", + "filename" : "historyEmptyPanel@3x.png" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/mobile/ios/Client/Frontend/Home/Home.xcassets/emptyHistory.imageset/historyEmptyPanel.png b/mobile/ios/Client/Frontend/Home/Home.xcassets/emptyHistory.imageset/historyEmptyPanel.png new file mode 100644 index 0000000000..857a25f237 Binary files /dev/null and b/mobile/ios/Client/Frontend/Home/Home.xcassets/emptyHistory.imageset/historyEmptyPanel.png differ diff --git a/mobile/ios/Client/Frontend/Home/Home.xcassets/emptyHistory.imageset/historyEmptyPanel@2x.png b/mobile/ios/Client/Frontend/Home/Home.xcassets/emptyHistory.imageset/historyEmptyPanel@2x.png new file mode 100644 index 0000000000..c56355d858 Binary files /dev/null and b/mobile/ios/Client/Frontend/Home/Home.xcassets/emptyHistory.imageset/historyEmptyPanel@2x.png differ diff --git a/mobile/ios/Client/Frontend/Home/Home.xcassets/emptyHistory.imageset/historyEmptyPanel@3x.png b/mobile/ios/Client/Frontend/Home/Home.xcassets/emptyHistory.imageset/historyEmptyPanel@3x.png new file mode 100644 index 0000000000..b9c6bb719f Binary files /dev/null and b/mobile/ios/Client/Frontend/Home/Home.xcassets/emptyHistory.imageset/historyEmptyPanel@3x.png differ diff --git a/mobile/ios/Client/Frontend/Home/Home.xcassets/emptySync.imageset/Contents.json b/mobile/ios/Client/Frontend/Home/Home.xcassets/emptySync.imageset/Contents.json new file mode 100644 index 0000000000..ccd23408e7 --- /dev/null +++ b/mobile/ios/Client/Frontend/Home/Home.xcassets/emptySync.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "sync-devices.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "sync-devices@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "sync-devices@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/mobile/ios/Client/Frontend/Home/Home.xcassets/emptySync.imageset/sync-devices.png b/mobile/ios/Client/Frontend/Home/Home.xcassets/emptySync.imageset/sync-devices.png new file mode 100644 index 0000000000..d6d3a57745 Binary files /dev/null and b/mobile/ios/Client/Frontend/Home/Home.xcassets/emptySync.imageset/sync-devices.png differ diff --git a/mobile/ios/Client/Frontend/Home/Home.xcassets/emptySync.imageset/sync-devices@2x.png b/mobile/ios/Client/Frontend/Home/Home.xcassets/emptySync.imageset/sync-devices@2x.png new file mode 100644 index 0000000000..4c3aab5fb0 Binary files /dev/null and b/mobile/ios/Client/Frontend/Home/Home.xcassets/emptySync.imageset/sync-devices@2x.png differ diff --git a/mobile/ios/Client/Frontend/Home/Home.xcassets/emptySync.imageset/sync-devices@3x.png b/mobile/ios/Client/Frontend/Home/Home.xcassets/emptySync.imageset/sync-devices@3x.png new file mode 100644 index 0000000000..38dc8cf449 Binary files /dev/null and b/mobile/ios/Client/Frontend/Home/Home.xcassets/emptySync.imageset/sync-devices@3x.png differ diff --git a/mobile/ios/Client/Frontend/Home/Home.xcassets/emptyTopSites.imageset/Contents.json b/mobile/ios/Client/Frontend/Home/Home.xcassets/emptyTopSites.imageset/Contents.json new file mode 100644 index 0000000000..d1f7e65ce4 --- /dev/null +++ b/mobile/ios/Client/Frontend/Home/Home.xcassets/emptyTopSites.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "topSitesEmptyPanel.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "topSitesEmptyPanel@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "topSitesEmptyPanel@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/mobile/ios/Client/Frontend/Home/Home.xcassets/emptyTopSites.imageset/topSitesEmptyPanel.png b/mobile/ios/Client/Frontend/Home/Home.xcassets/emptyTopSites.imageset/topSitesEmptyPanel.png new file mode 100644 index 0000000000..43b5513c7f Binary files /dev/null and b/mobile/ios/Client/Frontend/Home/Home.xcassets/emptyTopSites.imageset/topSitesEmptyPanel.png differ diff --git a/mobile/ios/Client/Frontend/Home/Home.xcassets/emptyTopSites.imageset/topSitesEmptyPanel@2x.png b/mobile/ios/Client/Frontend/Home/Home.xcassets/emptyTopSites.imageset/topSitesEmptyPanel@2x.png new file mode 100644 index 0000000000..7cdd5340ba Binary files /dev/null and b/mobile/ios/Client/Frontend/Home/Home.xcassets/emptyTopSites.imageset/topSitesEmptyPanel@2x.png differ diff --git a/mobile/ios/Client/Frontend/Home/Home.xcassets/emptyTopSites.imageset/topSitesEmptyPanel@3x.png b/mobile/ios/Client/Frontend/Home/Home.xcassets/emptyTopSites.imageset/topSitesEmptyPanel@3x.png new file mode 100644 index 0000000000..4b6ff1ccc2 Binary files /dev/null and b/mobile/ios/Client/Frontend/Home/Home.xcassets/emptyTopSites.imageset/topSitesEmptyPanel@3x.png differ diff --git a/mobile/ios/Client/Frontend/Home/Home.xcassets/panelIconBookmarks.imageset/Contents.json b/mobile/ios/Client/Frontend/Home/Home.xcassets/panelIconBookmarks.imageset/Contents.json new file mode 100644 index 0000000000..554eb04b28 --- /dev/null +++ b/mobile/ios/Client/Frontend/Home/Home.xcassets/panelIconBookmarks.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "bookmark-Outline.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "bookmark-Outline@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "bookmark-Outline@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/mobile/ios/Client/Frontend/Home/Home.xcassets/panelIconBookmarks.imageset/bookmark-Outline.png b/mobile/ios/Client/Frontend/Home/Home.xcassets/panelIconBookmarks.imageset/bookmark-Outline.png new file mode 100644 index 0000000000..03fe478950 Binary files /dev/null and b/mobile/ios/Client/Frontend/Home/Home.xcassets/panelIconBookmarks.imageset/bookmark-Outline.png differ diff --git a/mobile/ios/Client/Frontend/Home/Home.xcassets/panelIconBookmarks.imageset/bookmark-Outline@2x.png b/mobile/ios/Client/Frontend/Home/Home.xcassets/panelIconBookmarks.imageset/bookmark-Outline@2x.png new file mode 100644 index 0000000000..fca7ac6cc5 Binary files /dev/null and b/mobile/ios/Client/Frontend/Home/Home.xcassets/panelIconBookmarks.imageset/bookmark-Outline@2x.png differ diff --git a/mobile/ios/Client/Frontend/Home/Home.xcassets/panelIconBookmarks.imageset/bookmark-Outline@3x.png b/mobile/ios/Client/Frontend/Home/Home.xcassets/panelIconBookmarks.imageset/bookmark-Outline@3x.png new file mode 100644 index 0000000000..c29ed85a61 Binary files /dev/null and b/mobile/ios/Client/Frontend/Home/Home.xcassets/panelIconBookmarks.imageset/bookmark-Outline@3x.png differ diff --git a/mobile/ios/Client/Frontend/Home/Home.xcassets/panelIconHistory.imageset/Contents.json b/mobile/ios/Client/Frontend/Home/Home.xcassets/panelIconHistory.imageset/Contents.json new file mode 100644 index 0000000000..e972e4e90c --- /dev/null +++ b/mobile/ios/Client/Frontend/Home/Home.xcassets/panelIconHistory.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "history.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "history@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "history@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/mobile/ios/Client/Frontend/Home/Home.xcassets/panelIconHistory.imageset/history.png b/mobile/ios/Client/Frontend/Home/Home.xcassets/panelIconHistory.imageset/history.png new file mode 100644 index 0000000000..55a38daf73 Binary files /dev/null and b/mobile/ios/Client/Frontend/Home/Home.xcassets/panelIconHistory.imageset/history.png differ diff --git a/mobile/ios/Client/Frontend/Home/Home.xcassets/panelIconHistory.imageset/history@2x.png b/mobile/ios/Client/Frontend/Home/Home.xcassets/panelIconHistory.imageset/history@2x.png new file mode 100644 index 0000000000..f4b829241e Binary files /dev/null and b/mobile/ios/Client/Frontend/Home/Home.xcassets/panelIconHistory.imageset/history@2x.png differ diff --git a/mobile/ios/Client/Frontend/Home/Home.xcassets/panelIconHistory.imageset/history@3x.png b/mobile/ios/Client/Frontend/Home/Home.xcassets/panelIconHistory.imageset/history@3x.png new file mode 100644 index 0000000000..700bb3084a Binary files /dev/null and b/mobile/ios/Client/Frontend/Home/Home.xcassets/panelIconHistory.imageset/history@3x.png differ diff --git a/mobile/ios/Client/Frontend/Home/Home.xcassets/panelIconReadingList.imageset/Contents.json b/mobile/ios/Client/Frontend/Home/Home.xcassets/panelIconReadingList.imageset/Contents.json new file mode 100644 index 0000000000..c2dfe5e38a --- /dev/null +++ b/mobile/ios/Client/Frontend/Home/Home.xcassets/panelIconReadingList.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "reading-list.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "reading-list@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "reading-list@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/mobile/ios/Client/Frontend/Home/Home.xcassets/panelIconReadingList.imageset/reading-list.png b/mobile/ios/Client/Frontend/Home/Home.xcassets/panelIconReadingList.imageset/reading-list.png new file mode 100644 index 0000000000..05f93e98cc Binary files /dev/null and b/mobile/ios/Client/Frontend/Home/Home.xcassets/panelIconReadingList.imageset/reading-list.png differ diff --git a/mobile/ios/Client/Frontend/Home/Home.xcassets/panelIconReadingList.imageset/reading-list@2x.png b/mobile/ios/Client/Frontend/Home/Home.xcassets/panelIconReadingList.imageset/reading-list@2x.png new file mode 100644 index 0000000000..05e1576f58 Binary files /dev/null and b/mobile/ios/Client/Frontend/Home/Home.xcassets/panelIconReadingList.imageset/reading-list@2x.png differ diff --git a/mobile/ios/Client/Frontend/Home/Home.xcassets/panelIconReadingList.imageset/reading-list@3x.png b/mobile/ios/Client/Frontend/Home/Home.xcassets/panelIconReadingList.imageset/reading-list@3x.png new file mode 100644 index 0000000000..0038a91124 Binary files /dev/null and b/mobile/ios/Client/Frontend/Home/Home.xcassets/panelIconReadingList.imageset/reading-list@3x.png differ diff --git a/mobile/ios/Client/Frontend/Home/Home.xcassets/panelIconSyncedTabs.imageset/Contents.json b/mobile/ios/Client/Frontend/Home/Home.xcassets/panelIconSyncedTabs.imageset/Contents.json new file mode 100644 index 0000000000..977698d614 --- /dev/null +++ b/mobile/ios/Client/Frontend/Home/Home.xcassets/panelIconSyncedTabs.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "Synced Panel.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "Synced Panel@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "Synced Panel@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/mobile/ios/Client/Frontend/Home/Home.xcassets/panelIconSyncedTabs.imageset/Synced Panel.png b/mobile/ios/Client/Frontend/Home/Home.xcassets/panelIconSyncedTabs.imageset/Synced Panel.png new file mode 100644 index 0000000000..c889e158cb Binary files /dev/null and b/mobile/ios/Client/Frontend/Home/Home.xcassets/panelIconSyncedTabs.imageset/Synced Panel.png differ diff --git a/mobile/ios/Client/Frontend/Home/Home.xcassets/panelIconSyncedTabs.imageset/Synced Panel@2x.png b/mobile/ios/Client/Frontend/Home/Home.xcassets/panelIconSyncedTabs.imageset/Synced Panel@2x.png new file mode 100644 index 0000000000..7b47639594 Binary files /dev/null and b/mobile/ios/Client/Frontend/Home/Home.xcassets/panelIconSyncedTabs.imageset/Synced Panel@2x.png differ diff --git a/mobile/ios/Client/Frontend/Home/Home.xcassets/panelIconSyncedTabs.imageset/Synced Panel@3x.png b/mobile/ios/Client/Frontend/Home/Home.xcassets/panelIconSyncedTabs.imageset/Synced Panel@3x.png new file mode 100644 index 0000000000..25e4001a66 Binary files /dev/null and b/mobile/ios/Client/Frontend/Home/Home.xcassets/panelIconSyncedTabs.imageset/Synced Panel@3x.png differ diff --git a/mobile/ios/Client/Frontend/Home/Home.xcassets/panelIconTopSites.imageset/Contents.json b/mobile/ios/Client/Frontend/Home/Home.xcassets/panelIconTopSites.imageset/Contents.json new file mode 100644 index 0000000000..d0b4f6a6ab --- /dev/null +++ b/mobile/ios/Client/Frontend/Home/Home.xcassets/panelIconTopSites.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "topsites.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "topsites@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "topsites@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/mobile/ios/Client/Frontend/Home/Home.xcassets/panelIconTopSites.imageset/topsites.png b/mobile/ios/Client/Frontend/Home/Home.xcassets/panelIconTopSites.imageset/topsites.png new file mode 100644 index 0000000000..67fc5a0c6c Binary files /dev/null and b/mobile/ios/Client/Frontend/Home/Home.xcassets/panelIconTopSites.imageset/topsites.png differ diff --git a/mobile/ios/Client/Frontend/Home/Home.xcassets/panelIconTopSites.imageset/topsites@2x.png b/mobile/ios/Client/Frontend/Home/Home.xcassets/panelIconTopSites.imageset/topsites@2x.png new file mode 100644 index 0000000000..16d6bc8a43 Binary files /dev/null and b/mobile/ios/Client/Frontend/Home/Home.xcassets/panelIconTopSites.imageset/topsites@2x.png differ diff --git a/mobile/ios/Client/Frontend/Home/Home.xcassets/panelIconTopSites.imageset/topsites@3x.png b/mobile/ios/Client/Frontend/Home/Home.xcassets/panelIconTopSites.imageset/topsites@3x.png new file mode 100644 index 0000000000..11c0270b78 Binary files /dev/null and b/mobile/ios/Client/Frontend/Home/Home.xcassets/panelIconTopSites.imageset/topsites@3x.png differ diff --git a/mobile/ios/Client/Frontend/Home/HomePanelViewController.swift b/mobile/ios/Client/Frontend/Home/HomePanelViewController.swift new file mode 100644 index 0000000000..89455f3e67 --- /dev/null +++ b/mobile/ios/Client/Frontend/Home/HomePanelViewController.swift @@ -0,0 +1,364 @@ +/* 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 SnapKit +import UIKit +import Storage + +private struct HomePanelViewControllerUX { + // Height of the top panel switcher button toolbar. + static let ButtonContainerHeight: CGFloat = 40 + static let ButtonContainerBorderColor = UIColor.black.withAlphaComponent(0.1) + static let BackgroundColorPrivateMode = UIConstants.PrivateModeAssistantToolbarBackgroundColor + static let ToolbarButtonDeselectedColorNormalMode = UIColor(white: 0.2, alpha: 0.5) + static let ToolbarButtonDeselectedColorPrivateMode = UIColor(white: 0.9, alpha: 1) + static let ButtonHighlightLineHeight: CGFloat = 2 + static let ButtonSelectionAnimationDuration = 0.2 +} + +protocol HomePanelViewControllerDelegate: class { + func homePanelViewController(_ homePanelViewController: HomePanelViewController, didSelectURL url: URL, visitType: VisitType) + func homePanelViewController(_ HomePanelViewController: HomePanelViewController, didSelectPanel panel: Int) + func homePanelViewControllerDidRequestToSignIn(_ homePanelViewController: HomePanelViewController) + func homePanelViewControllerDidRequestToCreateAccount(_ homePanelViewController: HomePanelViewController) + func homePanelViewControllerDidRequestToOpenInNewTab(_ url: URL, isPrivate: Bool) +} + +protocol HomePanel: class { + weak var homePanelDelegate: HomePanelDelegate? { get set } +} + +struct HomePanelUX { + static let EmptyTabContentOffset = -180 +} + +protocol HomePanelDelegate: class { + func homePanelDidRequestToSignIn(_ homePanel: HomePanel) + func homePanelDidRequestToCreateAccount(_ homePanel: HomePanel) + func homePanelDidRequestToOpenInNewTab(_ url: URL, isPrivate: Bool) + func homePanel(_ homePanel: HomePanel, didSelectURL url: URL, visitType: VisitType) + func homePanel(_ homePanel: HomePanel, didSelectURLString url: String, visitType: VisitType) +} + +struct HomePanelState { + var selectedIndex: Int = 0 +} + +enum HomePanelType: Int { + case topSites = 0 + case bookmarks = 1 + case history = 2 + case readingList = 3 + + var localhostURL: URL { + return URL(string: "#panel=\(self.rawValue)", relativeTo: UIConstants.AboutHomePage as URL)! + } +} + +class HomePanelViewController: UIViewController, UITextFieldDelegate, HomePanelDelegate { + static let Themes: [String: Theme] = { + var themes = [String: Theme]() + var theme = Theme() + theme.backgroundColor = UIConstants.AppBackgroundColor + theme.buttonTintColor = UIColor(rgb: 0x7e7e7f) + theme.highlightButtonColor = UIConstants.HighlightBlue + themes[Theme.PrivateMode] = theme + + theme = Theme() + theme.backgroundColor = UIConstants.AppBackgroundColor + theme.buttonTintColor = UIColor(rgb: 0x7e7e7f) + theme.highlightButtonColor = UIConstants.HighlightBlue + themes[Theme.NormalMode] = theme + + return themes + }() + + var profile: Profile! + var notificationToken: NSObjectProtocol! + var panels: [HomePanelDescriptor]! + var url: URL? + weak var delegate: HomePanelViewControllerDelegate? + + fileprivate var buttonContainerView = UIStackView() + fileprivate var buttonContainerBottomBorderView: UIView! + fileprivate var controllerContainerView: UIView! + fileprivate var buttons: [UIButton] = [] + fileprivate var highlightLine = UIView() //The line underneath a panel button that shows which one is selected + + fileprivate var buttonTintColor: UIColor? + fileprivate var buttonSelectedTintColor: UIColor? + + var homePanelState: HomePanelState { + return HomePanelState(selectedIndex: selectedPanel?.rawValue ?? 0) + } + + override func viewDidLoad() { + view.backgroundColor = UIConstants.AppBackgroundColor + + buttonContainerView.axis = .horizontal + buttonContainerView.alignment = .fill + buttonContainerView.distribution = .fillEqually + buttonContainerView.spacing = 14 + buttonContainerView.clipsToBounds = true + buttonContainerView.accessibilityNavigationStyle = .combined + buttonContainerView.accessibilityLabel = NSLocalizedString("Panel Chooser", comment: "Accessibility label for the Home panel's top toolbar containing list of the home panels (top sites, bookmarsk, history, remote tabs, reading list).") + view.addSubview(buttonContainerView) + buttonContainerView.addSubview(highlightLine) + + self.buttonContainerBottomBorderView = UIView() + self.view.addSubview(buttonContainerBottomBorderView) + buttonContainerBottomBorderView.backgroundColor = HomePanelViewControllerUX.ButtonContainerBorderColor + + controllerContainerView = UIView() + view.addSubview(controllerContainerView) + + buttonContainerView.snp.makeConstraints { make in + make.top.equalTo(self.view) + make.leading.trailing.equalTo(self.view).inset(14) + make.height.equalTo(HomePanelViewControllerUX.ButtonContainerHeight) + } + + buttonContainerBottomBorderView.snp.makeConstraints { make in + make.top.equalTo(self.buttonContainerView.snp.bottom).offset(-1) + make.bottom.equalTo(self.buttonContainerView) + make.leading.trailing.equalToSuperview() + } + + controllerContainerView.snp.makeConstraints { make in + make.top.equalTo(self.buttonContainerView.snp.bottom) + make.left.right.bottom.equalTo(self.view) + } + + self.panels = HomePanels().enabledPanels + updateButtons() + + // Gesture recognizer to dismiss the keyboard in the URLBarView when the buttonContainerView is tapped + let dismissKeyboardGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(HomePanelViewController.dismissKeyboard(_:))) + dismissKeyboardGestureRecognizer.cancelsTouchesInView = false + buttonContainerView.addGestureRecognizer(dismissKeyboardGestureRecognizer) + } + + func dismissKeyboard(_ gestureRecognizer: UITapGestureRecognizer) { + view.window?.rootViewController?.view.endEditing(true) + } + + var selectedPanel: HomePanelType? = nil { + didSet { + if oldValue == selectedPanel { + // Prevent flicker, allocations, and disk access: avoid duplicate view controllers. + return + } + + if let index = oldValue?.rawValue { + if index < buttons.count { + let currentButton = buttons[index] + currentButton.isSelected = false + currentButton.isUserInteractionEnabled = true + } + } + + hideCurrentPanel() + + if let index = selectedPanel?.rawValue { + if index < buttons.count { + let newButton = buttons[index] + newButton.isSelected = true + newButton.isUserInteractionEnabled = false + } + + if index < panels.count { + let panel = self.panels[index].makeViewController(profile) + let accessibilityLabel = self.panels[index].accessibilityLabel + if let panelController = panel as? UINavigationController, + let rootPanel = panelController.viewControllers.first { + setupHomePanel(rootPanel, accessibilityLabel: accessibilityLabel) + self.showPanel(panelController) + } else { + setupHomePanel(panel, accessibilityLabel: accessibilityLabel) + self.showPanel(panel) + } + } + } + self.updateButtonTints() + } + } + + func setupHomePanel(_ panel: UIViewController, accessibilityLabel: String) { + (panel as? HomePanel)?.homePanelDelegate = self + panel.view.accessibilityNavigationStyle = .combined + panel.view.accessibilityLabel = accessibilityLabel + } + + override var preferredStatusBarStyle: UIStatusBarStyle { + return UIStatusBarStyle.lightContent + } + + fileprivate func hideCurrentPanel() { + if let panel = childViewControllers.first { + panel.willMove(toParentViewController: nil) + panel.beginAppearanceTransition(false, animated: false) + panel.view.removeFromSuperview() + panel.endAppearanceTransition() + panel.removeFromParentViewController() + } + } + + fileprivate func showPanel(_ panel: UIViewController) { + addChildViewController(panel) + panel.beginAppearanceTransition(true, animated: false) + controllerContainerView.addSubview(panel.view) + panel.endAppearanceTransition() + panel.view.snp.makeConstraints { make in + make.top.equalTo(self.buttonContainerView.snp.bottom) + make.left.right.bottom.equalTo(self.view) + } + panel.didMove(toParentViewController: self) + } + + func tappedButton(_ sender: UIButton!) { + for (index, button) in buttons.enumerated() where button == sender { + selectedPanel = HomePanelType(rawValue: index) + delegate?.homePanelViewController(self, didSelectPanel: index) + if selectedPanel == .bookmarks { + UnifiedTelemetry.recordEvent(category: .action, method: .view, object: .bookmarksPanel, value: .homePanelTabButton) + } + break + } + } + + fileprivate func updateButtons() { + for panel in panels { + let button = UIButton() + button.addTarget(self, action: #selector(HomePanelViewController.tappedButton(_:)), for: .touchUpInside) + if let image = UIImage.templateImageNamed("panelIcon\(panel.imageName)") { + button.setImage(image, for: UIControlState.normal) + } + button.imageEdgeInsets = UIEdgeInsets(top: 0, left: 0, bottom: 4, right: 0) + button.accessibilityLabel = panel.accessibilityLabel + button.accessibilityIdentifier = panel.accessibilityIdentifier + buttons.append(button) + self.buttonContainerView.addArrangedSubview(button) + } + } + + func updateButtonTints() { + var selectedbutton: UIView? + for (index, button) in self.buttons.enumerated() { + if index == self.selectedPanel?.rawValue { + button.tintColor = self.buttonSelectedTintColor + selectedbutton = button + } else { + button.tintColor = self.buttonTintColor + } + } + guard let button = selectedbutton else { + return + } + + // Calling this before makes sure that only the highlightline animates and not the homepanels + self.view.setNeedsUpdateConstraints() + self.view.layoutIfNeeded() + UIView.animate(withDuration: HomePanelViewControllerUX.ButtonSelectionAnimationDuration, delay: 0.0, usingSpringWithDamping: 0.85, initialSpringVelocity: 0.0, options: [], animations: { _ in + self.highlightLine.snp.remakeConstraints { make in + make.leading.equalTo(button.snp.leading) + make.trailing.equalTo(button.snp.trailing) + make.bottom.equalToSuperview() + make.height.equalTo(HomePanelViewControllerUX.ButtonHighlightLineHeight) + } + self.view.setNeedsUpdateConstraints() + self.view.layoutIfNeeded() + }, completion: nil) + } + + func homePanel(_ homePanel: HomePanel, didSelectURLString url: String, visitType: VisitType) { + // If we can't get a real URL out of what should be a URL, we let the user's + // default search engine give it a shot. + // Typically we'll be in this state if the user has tapped a bookmarked search template + // (e.g., "http://foo.com/bar/?query=%s"), and this will get them the same behavior as if + // they'd copied and pasted into the URL bar. + // See BrowserViewController.urlBar:didSubmitText:. + guard let url = URIFixup.getURL(url) ?? profile.searchEngines.defaultEngine.searchURLForQuery(url) else { + Logger.browserLogger.warning("Invalid URL, and couldn't generate a search URL for it.") + return + } + + return self.homePanel(homePanel, didSelectURL: url, visitType: visitType) + } + + func homePanel(_ homePanel: HomePanel, didSelectURL url: URL, visitType: VisitType) { + delegate?.homePanelViewController(self, didSelectURL: url, visitType: visitType) + dismiss(animated: true, completion: nil) + } + + func homePanelDidRequestToCreateAccount(_ homePanel: HomePanel) { + delegate?.homePanelViewControllerDidRequestToCreateAccount(self) + } + + func homePanelDidRequestToSignIn(_ homePanel: HomePanel) { + delegate?.homePanelViewControllerDidRequestToSignIn(self) + } + + func homePanelDidRequestToOpenInNewTab(_ url: URL, isPrivate: Bool) { + delegate?.homePanelViewControllerDidRequestToOpenInNewTab(url, isPrivate: isPrivate) + } +} + +// MARK: UIAppearance +extension HomePanelViewController: Themeable { + func applyTheme(_ themeName: String) { + guard let theme = HomePanelViewController.Themes[themeName] else { + fatalError("Theme not found") + } + + highlightLine.backgroundColor = theme.highlightButtonColor + buttonContainerView.backgroundColor = theme.backgroundColor + self.view.backgroundColor = theme.backgroundColor + buttonTintColor = theme.buttonTintColor + buttonSelectedTintColor = theme.highlightButtonColor + updateButtonTints() + } +} + +protocol HomePanelContextMenu { + func getSiteDetails(for indexPath: IndexPath) -> Site? + func getContextMenuActions(for site: Site, with indexPath: IndexPath) -> [PhotonActionSheetItem]? + func presentContextMenu(for indexPath: IndexPath) + func presentContextMenu(for site: Site, with indexPath: IndexPath, completionHandler: @escaping () -> PhotonActionSheet?) +} + +extension HomePanelContextMenu { + func presentContextMenu(for indexPath: IndexPath) { + guard let site = getSiteDetails(for: indexPath) else { return } + + presentContextMenu(for: site, with: indexPath, completionHandler: { + return self.contextMenu(for: site, with: indexPath) + }) + } + + func contextMenu(for site: Site, with indexPath: IndexPath) -> PhotonActionSheet? { + guard let actions = self.getContextMenuActions(for: site, with: indexPath) else { return nil } + + let contextMenu = PhotonActionSheet(site: site, actions: actions) + contextMenu.modalPresentationStyle = .overFullScreen + contextMenu.modalTransitionStyle = .crossDissolve + + return contextMenu + } + + func getDefaultContextMenuActions(for site: Site, homePanelDelegate: HomePanelDelegate?) -> [PhotonActionSheetItem]? { + guard let siteURL = URL(string: site.url) else { return nil } + + let openInNewTabAction = PhotonActionSheetItem(title: Strings.OpenInNewTabContextMenuTitle, iconString: "quick_action_new_tab") { action in + homePanelDelegate?.homePanelDidRequestToOpenInNewTab(siteURL, isPrivate: false) + } + + let openInNewPrivateTabAction = PhotonActionSheetItem(title: Strings.OpenInNewPrivateTabContextMenuTitle, iconString: "quick_action_new_private_tab") { action in + homePanelDelegate?.homePanelDidRequestToOpenInNewTab(siteURL, isPrivate: true) + } + + return [openInNewTabAction, openInNewPrivateTabAction] + } +} diff --git a/mobile/ios/Client/Frontend/Home/HomePanels.swift b/mobile/ios/Client/Frontend/Home/HomePanels.swift new file mode 100644 index 0000000000..bb131c392f --- /dev/null +++ b/mobile/ios/Client/Frontend/Home/HomePanels.swift @@ -0,0 +1,69 @@ +/* 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 UIKit +import Shared + +/** + * Data for identifying and constructing a HomePanel. + */ +struct HomePanelDescriptor { + let makeViewController: (_ profile: Profile) -> UIViewController + let imageName: String + let accessibilityLabel: String + let accessibilityIdentifier: String +} + +class HomePanels { + let enabledPanels = [ + HomePanelDescriptor( + makeViewController: { profile in + return ActivityStreamPanel(profile: profile) + }, + imageName: "TopSites", + accessibilityLabel: NSLocalizedString("Top sites", comment: "Panel accessibility label"), + accessibilityIdentifier: "HomePanels.TopSites"), + + HomePanelDescriptor( + makeViewController: { profile in + let bookmarks = BookmarksPanel() + bookmarks.profile = profile + let controller = UINavigationController(rootViewController: bookmarks) + controller.setNavigationBarHidden(true, animated: false) + // this re-enables the native swipe to pop gesture on UINavigationController for embedded, navigation bar-less UINavigationControllers + // don't ask me why it works though, I've tried to find an answer but can't. + // found here, along with many other places: + // http://luugiathuy.com/2013/11/ios7-interactivepopgesturerecognizer-for-uinavigationcontroller-with-hidden-navigation-bar/ + controller.interactivePopGestureRecognizer?.delegate = nil + return controller + }, + imageName: "Bookmarks", + accessibilityLabel: NSLocalizedString("Bookmarks", comment: "Panel accessibility label"), + accessibilityIdentifier: "HomePanels.Bookmarks"), + + HomePanelDescriptor( + makeViewController: { profile in + let history = HistoryPanel() + history.profile = profile + let controller = UINavigationController(rootViewController: history) + controller.setNavigationBarHidden(true, animated: false) + controller.interactivePopGestureRecognizer?.delegate = nil + return controller + }, + imageName: "History", + accessibilityLabel: NSLocalizedString("History", comment: "Panel accessibility label"), + accessibilityIdentifier: "HomePanels.History"), + + HomePanelDescriptor( + makeViewController: { profile in + let controller = ReadingListPanel() + controller.profile = profile + return controller + }, + imageName: "ReadingList", + accessibilityLabel: NSLocalizedString("Reading list", comment: "Panel accessibility label"), + accessibilityIdentifier: "HomePanels.ReadingList"), + ] +} diff --git a/mobile/ios/Client/Frontend/Home/PanelDataObservers.swift b/mobile/ios/Client/Frontend/Home/PanelDataObservers.swift new file mode 100644 index 0000000000..cea62a6cb5 --- /dev/null +++ b/mobile/ios/Client/Frontend/Home/PanelDataObservers.swift @@ -0,0 +1,97 @@ +/* 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 Deferred +import Shared + +public let ActivityStreamTopSiteCacheSize: Int32 = 16 + +private let log = Logger.browserLogger + +protocol DataObserver { + var profile: Profile { get } + weak var delegate: DataObserverDelegate? { get set } + + func refreshIfNeeded(forceHighlights highlights: Bool, forceTopSites topSites: Bool) +} + +protocol DataObserverDelegate: class { + func didInvalidateDataSources(refresh forced: Bool, highlightsRefreshed: Bool, topSitesRefreshed: Bool) + func willInvalidateDataSources(forceHighlights highlights: Bool, forceTopSites topSites: Bool) +} + +// Make these delegate methods optional by providing default implementations +extension DataObserverDelegate { + func didInvalidateDataSources(refresh forced: Bool, highlightsRefreshed: Bool, topSitesRefreshed: Bool) {} + func willInvalidateDataSources(forceHighlights highlights: Bool, forceTopSites topSites: Bool) {} +} + +open class PanelDataObservers { + var activityStream: DataObserver + + init(profile: Profile) { + self.activityStream = ActivityStreamDataObserver(profile: profile) + } +} + +class ActivityStreamDataObserver: DataObserver { + let profile: Profile + weak var delegate: DataObserverDelegate? + private var invalidationTime = OneMinuteInMilliseconds * 15 + + fileprivate let events = [NotificationFirefoxAccountChanged, NotificationProfileDidFinishSyncing, NotificationPrivateDataClearedHistory] + + init(profile: Profile) { + self.profile = profile + self.profile.history.setTopSitesCacheSize(ActivityStreamTopSiteCacheSize) + events.forEach { NotificationCenter.default.addObserver(self, selector: #selector(self.notificationReceived(_:)), name: $0, object: nil) } + } + + /* + refreshIfNeeded will refresh the underlying caches for both TopSites and Highlights. + By default this will only refresh the highlights if the last fetch is older than 15 mins + By default this will only refresh topSites if KeyTopSitesCacheIsValid is false + */ + func refreshIfNeeded(forceHighlights highlights: Bool, forceTopSites topSites: Bool) { + guard !profile.isShutdown else { + return + } + + // Highlights are cached for 15 mins + let userEnabledHighlights = profile.prefs.boolForKey(PrefsKeys.ASRecentHighlightsVisible) ?? true + let lastInvalidationTime = profile.prefs.unsignedLongForKey(PrefsKeys.ASLastInvalidation) ?? 0 + let shouldInvalidateHighlights = (highlights || (Date.now() - lastInvalidationTime > invalidationTime)) && userEnabledHighlights + + // KeyTopSitesCacheIsValid is false when we want to invalidate. Thats why this logic is so backwards + let shouldInvalidateTopSites = topSites || !(profile.prefs.boolForKey(PrefsKeys.KeyTopSitesCacheIsValid) ?? false) + if !shouldInvalidateTopSites && !shouldInvalidateHighlights { + // There is nothing to refresh. Bye + return + } + + self.delegate?.willInvalidateDataSources(forceHighlights: highlights, forceTopSites: topSites) + self.profile.recommendations.repopulate(invalidateTopSites: shouldInvalidateTopSites, invalidateHighlights: shouldInvalidateHighlights).uponQueue(DispatchQueue.main) { _ in + if shouldInvalidateTopSites { + self.profile.prefs.setBool(true, forKey: PrefsKeys.KeyTopSitesCacheIsValid) + } + + if shouldInvalidateHighlights { + let newInvalidationTime = shouldInvalidateHighlights ? Date.now() : lastInvalidationTime + self.profile.prefs.setLong(newInvalidationTime, forKey: PrefsKeys.ASLastInvalidation) + } + + self.delegate?.didInvalidateDataSources(refresh: highlights || topSites, highlightsRefreshed: shouldInvalidateHighlights, topSitesRefreshed: shouldInvalidateTopSites) + } + } + + @objc func notificationReceived(_ notification: Notification) { + switch notification.name { + case NotificationProfileDidFinishSyncing, NotificationFirefoxAccountChanged, NotificationPrivateDataClearedHistory: + refreshIfNeeded(forceHighlights: true, forceTopSites: true) + default: + log.warning("Received unexpected notification \(notification.name)") + } + } +} diff --git a/mobile/ios/Client/Frontend/Home/ReaderPanel.swift b/mobile/ios/Client/Frontend/Home/ReaderPanel.swift new file mode 100644 index 0000000000..8b804f0804 --- /dev/null +++ b/mobile/ios/Client/Frontend/Home/ReaderPanel.swift @@ -0,0 +1,456 @@ +/* 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 UIKit +import SnapKit +import Storage +import ReadingList +import Shared +import XCGLogger + +private let log = Logger.browserLogger + +private struct ReadingListTableViewCellUX { + static let RowHeight: CGFloat = 86 + + static let ActiveTextColor = UIColor(red: 0.2, green: 0.2, blue: 0.2, alpha: 1.0) + static let DimmedTextColor = UIColor(red: 0.2, green: 0.2, blue: 0.2, alpha: 0.44) + + static let ReadIndicatorWidth: CGFloat = 12 // image width + static let ReadIndicatorHeight: CGFloat = 12 // image height + static let ReadIndicatorLeftOffset: CGFloat = 18 + static let ReadAccessibilitySpeechPitch: Float = 0.7 // 1.0 default, 0.0 lowest, 2.0 highest + + static let TitleLabelTopOffset: CGFloat = 14 - 4 + static let TitleLabelLeftOffset: CGFloat = 16 + 16 + 16 + static let TitleLabelRightOffset: CGFloat = -40 + + static let HostnameLabelBottomOffset: CGFloat = 11 + + static let DeleteButtonBackgroundColor = UIColor(rgb: 0xef4035) + static let DeleteButtonTitleColor = UIColor.white + static let DeleteButtonTitleEdgeInsets = UIEdgeInsets(top: 4, left: 4, bottom: 4, right: 4) + + static let MarkAsReadButtonBackgroundColor = UIColor(rgb: 0x2193d1) + static let MarkAsReadButtonTitleColor = UIColor.white + static let MarkAsReadButtonTitleEdgeInsets = UIEdgeInsets(top: 4, left: 4, bottom: 4, right: 4) + + // Localizable strings + static let DeleteButtonTitleText = NSLocalizedString("Remove", comment: "Title for the button that removes a reading list item") + static let MarkAsReadButtonTitleText = NSLocalizedString("Mark as Read", comment: "Title for the button that marks a reading list item as read") + static let MarkAsUnreadButtonTitleText = NSLocalizedString("Mark as Unread", comment: "Title for the button that marks a reading list item as unread") +} + +private struct ReadingListPanelUX { + // Welcome Screen + static let WelcomeScreenTopPadding: CGFloat = 16 + static let WelcomeScreenPadding: CGFloat = 15 + static let WelcomeScreenHeaderTextColor = UIColor.darkGray + + static let WelcomeScreenItemTextColor = UIColor.gray + static let WelcomeScreenItemWidth = 220 + static let WelcomeScreenItemOffset = -20 + + static let WelcomeScreenCircleWidth = 40 + static let WelcomeScreenCircleOffset = 20 + static let WelcomeScreenCircleSpacer = 10 +} + +class ReadingListTableViewCell: UITableViewCell { + var title: String = "Example" { + didSet { + titleLabel.text = title + updateAccessibilityLabel() + } + } + + var url: URL = URL(string: "http://www.example.com")! { + didSet { + hostnameLabel.text = simplifiedHostnameFromURL(url) + updateAccessibilityLabel() + } + } + + var unread: Bool = true { + didSet { + readStatusImageView.image = UIImage(named: unread ? "MarkAsRead" : "MarkAsUnread") + titleLabel.textColor = unread ? ReadingListTableViewCellUX.ActiveTextColor : ReadingListTableViewCellUX.DimmedTextColor + hostnameLabel.textColor = unread ? ReadingListTableViewCellUX.ActiveTextColor : ReadingListTableViewCellUX.DimmedTextColor + updateAccessibilityLabel() + } + } + + let readStatusImageView: UIImageView! + let titleLabel: UILabel! + let hostnameLabel: UILabel! + + override init(style: UITableViewCellStyle, reuseIdentifier: String?) { + readStatusImageView = UIImageView() + titleLabel = UILabel() + hostnameLabel = UILabel() + + super.init(style: style, reuseIdentifier: reuseIdentifier) + + backgroundColor = UIColor.clear + + separatorInset = UIEdgeInsets(top: 0, left: 48, bottom: 0, right: 0) + layoutMargins = UIEdgeInsets.zero + preservesSuperviewLayoutMargins = false + + contentView.addSubview(readStatusImageView) + readStatusImageView.contentMode = UIViewContentMode.scaleAspectFit + readStatusImageView.snp.makeConstraints { (make) -> Void in + make.width.equalTo(ReadingListTableViewCellUX.ReadIndicatorWidth) + make.height.equalTo(ReadingListTableViewCellUX.ReadIndicatorHeight) + make.centerY.equalTo(self.contentView) + make.leading.equalTo(self.contentView).offset(ReadingListTableViewCellUX.ReadIndicatorLeftOffset) + } + + contentView.addSubview(titleLabel) + contentView.addSubview(hostnameLabel) + + titleLabel.textColor = ReadingListTableViewCellUX.ActiveTextColor + titleLabel.numberOfLines = 2 + titleLabel.snp.makeConstraints { (make) -> Void in + make.top.equalTo(self.contentView).offset(ReadingListTableViewCellUX.TitleLabelTopOffset) + make.leading.equalTo(self.contentView).offset(ReadingListTableViewCellUX.TitleLabelLeftOffset) + make.trailing.equalTo(self.contentView).offset(ReadingListTableViewCellUX.TitleLabelRightOffset) // TODO Not clear from ux spec + make.bottom.lessThanOrEqualTo(hostnameLabel.snp.top).priority(1000) + } + + hostnameLabel.textColor = ReadingListTableViewCellUX.ActiveTextColor + hostnameLabel.numberOfLines = 1 + hostnameLabel.snp.makeConstraints { (make) -> Void in + make.bottom.equalTo(self.contentView).offset(-ReadingListTableViewCellUX.HostnameLabelBottomOffset) + make.leading.trailing.equalTo(self.titleLabel) + } + + setupDynamicFonts() + } + + func setupDynamicFonts() { + titleLabel.font = DynamicFontHelper.defaultHelper.DeviceFont + hostnameLabel.font = DynamicFontHelper.defaultHelper.DeviceFontSmallLight + } + + override func prepareForReuse() { + super.prepareForReuse() + setupDynamicFonts() + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + let prefixesToSimplify = ["www.", "mobile.", "m.", "blog."] + + fileprivate func simplifiedHostnameFromURL(_ url: URL) -> String { + let hostname = url.host ?? "" + for prefix in prefixesToSimplify { + if hostname.hasPrefix(prefix) { + return hostname.substring(from: hostname.characters.index(hostname.startIndex, offsetBy: prefix.characters.count)) + } + } + return hostname + } + + fileprivate func updateAccessibilityLabel() { + if let hostname = hostnameLabel.text, + let title = titleLabel.text { + let unreadStatus = unread ? NSLocalizedString("unread", comment: "Accessibility label for unread article in reading list. It's a past participle - functions as an adjective.") : NSLocalizedString("read", comment: "Accessibility label for read article in reading list. It's a past participle - functions as an adjective.") + let string = "\(title), \(unreadStatus), \(hostname)" + var label: AnyObject + if !unread { + // mimic light gray visual dimming by "dimming" the speech by reducing pitch + let lowerPitchString = NSMutableAttributedString(string: string as String) + lowerPitchString.addAttribute(UIAccessibilitySpeechAttributePitch, value: NSNumber(value: ReadingListTableViewCellUX.ReadAccessibilitySpeechPitch as Float), range: NSRange(location: 0, length: lowerPitchString.length)) + label = NSAttributedString(attributedString: lowerPitchString) + } else { + label = string as AnyObject + } + // need to use KVC as accessibilityLabel is of type String! and cannot be set to NSAttributedString other way than this + // see bottom of page 121 of the PDF slides of WWDC 2012 "Accessibility for iOS" session for indication that this is OK by Apple + // also this combined with Swift's strictness is why we cannot simply override accessibilityLabel and return the label directly... + setValue(label, forKey: "accessibilityLabel") + } + } +} + +class ReadingListPanel: UITableViewController, HomePanel { + weak var homePanelDelegate: HomePanelDelegate? + var profile: Profile! + + fileprivate lazy var longPressRecognizer: UILongPressGestureRecognizer = { + return UILongPressGestureRecognizer(target: self, action: #selector(ReadingListPanel.longPress(_:))) + }() + + fileprivate lazy var emptyStateOverlayView: UIView = self.createEmptyStateOverview() + + fileprivate var records: [ReadingListClientRecord]? + + init() { + super.init(nibName: nil, bundle: nil) + NotificationCenter.default.addObserver(self, selector: #selector(ReadingListPanel.notificationReceived(_:)), name: NotificationFirefoxAccountChanged, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(ReadingListPanel.notificationReceived(_:)), name: NotificationDynamicFontChanged, object: nil) + } + + required init!(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + + tableView.addGestureRecognizer(longPressRecognizer) + tableView.accessibilityIdentifier = "ReadingTable" + tableView.estimatedRowHeight = ReadingListTableViewCellUX.RowHeight + tableView.rowHeight = UITableViewAutomaticDimension + tableView.cellLayoutMarginsFollowReadableWidth = false + tableView.separatorInset = UIEdgeInsets.zero + tableView.layoutMargins = UIEdgeInsets.zero + tableView.separatorColor = UIConstants.SeparatorColor + tableView.register(ReadingListTableViewCell.self, forCellReuseIdentifier: "ReadingListTableViewCell") + + // Set an empty footer to prevent empty cells from appearing in the list. + tableView.tableFooterView = UIView() + + view.backgroundColor = UIConstants.PanelBackgroundColor + + if let result = profile.readingList?.getAvailableRecords(), result.isSuccess { + records = result.successValue + + // If no records have been added yet, we display the empty state + if records?.count == 0 { + tableView.isScrollEnabled = false + view.addSubview(emptyStateOverlayView) + + } + } + } + + func notificationReceived(_ notification: Notification) { + switch notification.name { + case NotificationFirefoxAccountChanged: + refreshReadingList() + break + case NotificationDynamicFontChanged: + if emptyStateOverlayView.superview != nil { + emptyStateOverlayView.removeFromSuperview() + } + emptyStateOverlayView = createEmptyStateOverview() + refreshReadingList() + break + default: + // no need to do anything at all + log.warning("Received unexpected notification \(notification.name)") + break + } + } + + func refreshReadingList() { + let prevNumberOfRecords = records?.count + if let result = profile.readingList?.getAvailableRecords(), result.isSuccess { + records = result.successValue + + if records?.count == 0 { + tableView.isScrollEnabled = false + if emptyStateOverlayView.superview == nil { + view.addSubview(emptyStateOverlayView) + } + } else { + if prevNumberOfRecords == 0 { + tableView.isScrollEnabled = true + emptyStateOverlayView.removeFromSuperview() + } + } + self.tableView.reloadData() + } + } + + fileprivate func createEmptyStateOverview() -> UIView { + let overlayView = UIScrollView(frame: tableView.bounds) + overlayView.backgroundColor = UIColor.white + // Unknown why this does not work with autolayout + overlayView.autoresizingMask = [UIViewAutoresizing.flexibleHeight, UIViewAutoresizing.flexibleWidth] + + let containerView = UIView() + overlayView.addSubview(containerView) + + let welcomeLabel = UILabel() + containerView.addSubview(welcomeLabel) + welcomeLabel.text = NSLocalizedString("Welcome to your Reading List", comment: "See http://mzl.la/1LXbDOL") + welcomeLabel.textAlignment = NSTextAlignment.center + welcomeLabel.font = DynamicFontHelper.defaultHelper.DeviceFontSmallBold + welcomeLabel.textColor = ReadingListPanelUX.WelcomeScreenHeaderTextColor + welcomeLabel.adjustsFontSizeToFitWidth = true + welcomeLabel.snp.makeConstraints { make in + make.centerX.equalTo(containerView) + make.width.equalTo(ReadingListPanelUX.WelcomeScreenItemWidth + ReadingListPanelUX.WelcomeScreenCircleSpacer + ReadingListPanelUX.WelcomeScreenCircleWidth) + make.top.equalTo(containerView) + // Sets proper center constraint for iPhones in landscape. + make.centerY.lessThanOrEqualTo(overlayView.snp.centerY).offset(-40).priority(1000) + } + + let readerModeLabel = UILabel() + containerView.addSubview(readerModeLabel) + readerModeLabel.text = NSLocalizedString("Open articles in Reader View by tapping the book icon when it appears in the title bar.", comment: "See http://mzl.la/1LXbDOL") + readerModeLabel.font = DynamicFontHelper.defaultHelper.DeviceFontSmallLight + readerModeLabel.textColor = ReadingListPanelUX.WelcomeScreenItemTextColor + readerModeLabel.numberOfLines = 0 + readerModeLabel.snp.makeConstraints { make in + make.top.equalTo(welcomeLabel.snp.bottom).offset(ReadingListPanelUX.WelcomeScreenPadding) + make.leading.equalTo(welcomeLabel.snp.leading) + make.width.equalTo(ReadingListPanelUX.WelcomeScreenItemWidth) + } + + let readerModeImageView = UIImageView(image: UIImage(named: "ReaderModeCircle")) + containerView.addSubview(readerModeImageView) + readerModeImageView.snp.makeConstraints { make in + make.centerY.equalTo(readerModeLabel) + make.trailing.equalTo(welcomeLabel.snp.trailing) + } + + let readingListLabel = UILabel() + containerView.addSubview(readingListLabel) + readingListLabel.text = NSLocalizedString("Save pages to your Reading List by tapping the book plus icon in the Reader View controls.", comment: "See http://mzl.la/1LXbDOL") + readingListLabel.font = DynamicFontHelper.defaultHelper.DeviceFontSmallLight + readingListLabel.textColor = ReadingListPanelUX.WelcomeScreenItemTextColor + readingListLabel.numberOfLines = 0 + readingListLabel.snp.makeConstraints { make in + make.top.equalTo(readerModeLabel.snp.bottom).offset(ReadingListPanelUX.WelcomeScreenPadding) + make.leading.equalTo(welcomeLabel.snp.leading) + make.width.equalTo(ReadingListPanelUX.WelcomeScreenItemWidth) + make.bottom.equalTo(overlayView).offset(-20) // making AutoLayout compute the overlayView's contentSize + } + + let readingListImageView = UIImageView(image: UIImage(named: "AddToReadingListCircle")) + containerView.addSubview(readingListImageView) + readingListImageView.snp.makeConstraints { make in + make.centerY.equalTo(readingListLabel) + make.trailing.equalTo(welcomeLabel.snp.trailing) + } + + containerView.snp.makeConstraints { make in + // Let the container wrap around the content + make.left.equalTo(welcomeLabel).offset(ReadingListPanelUX.WelcomeScreenItemOffset) + make.right.equalTo(welcomeLabel).offset(ReadingListPanelUX.WelcomeScreenCircleOffset) + + // And then center it in the overlay view that sits on top of the UITableView + make.centerX.equalTo(overlayView) + } + + return overlayView + } + + @objc fileprivate func longPress(_ longPressGestureRecognizer: UILongPressGestureRecognizer) { + guard longPressGestureRecognizer.state == UIGestureRecognizerState.began else { return } + let touchPoint = longPressGestureRecognizer.location(in: tableView) + guard let indexPath = tableView.indexPathForRow(at: touchPoint) else { return } + presentContextMenu(for: indexPath) + } + + override func numberOfSections(in tableView: UITableView) -> Int { + return 1 + } + + override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return records?.count ?? 0 + } + + override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let cell = tableView.dequeueReusableCell(withIdentifier: "ReadingListTableViewCell", for: indexPath) as! ReadingListTableViewCell + if let record = records?[indexPath.row] { + cell.title = record.title + cell.url = URL(string: record.url)! + cell.unread = record.unread + } + return cell + } + + override func tableView(_ tableView: UITableView, editActionsForRowAt indexPath: IndexPath) -> [UITableViewRowAction]? { + guard let record = records?[indexPath.row] else { + return [] + } + + let delete = UITableViewRowAction(style: .normal, title: ReadingListTableViewCellUX.DeleteButtonTitleText) { [weak self] action, index in + self?.deleteItem(atIndex: index) + } + delete.backgroundColor = ReadingListTableViewCellUX.DeleteButtonBackgroundColor + + let toggleText = record.unread ? ReadingListTableViewCellUX.MarkAsReadButtonTitleText : ReadingListTableViewCellUX.MarkAsUnreadButtonTitleText + let unreadToggle = UITableViewRowAction(style: .normal, title: toggleText.stringSplitWithNewline()) { [weak self] (action, index) in + self?.toggleItem(atIndex: index) + } + unreadToggle.backgroundColor = ReadingListTableViewCellUX.MarkAsReadButtonBackgroundColor + + return [unreadToggle, delete] + } + + override func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool { + // the cells you would like the actions to appear needs to be editable + return true + } + + override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + tableView.deselectRow(at: indexPath, animated: false) + if let record = records?[indexPath.row], let url = URL(string: record.url), let encodedURL = url.encodeReaderModeURL(WebServer.sharedInstance.baseReaderModeURL()) { + // Mark the item as read + profile.readingList?.updateRecord(record, unread: false) + // Reading list items are closest in concept to bookmarks. + let visitType = VisitType.bookmark + homePanelDelegate?.homePanel(self, didSelectURL: encodedURL, visitType: visitType) + UnifiedTelemetry.recordEvent(category: .action, method: .open, object: .readingListItem) + } + } + + fileprivate func deleteItem(atIndex indexPath: IndexPath) { + if let record = records?[indexPath.row] { + UnifiedTelemetry.recordEvent(category: .action, method: .delete, object: .readingListItem, value: .readingListPanel) + if let result = profile.readingList?.deleteRecord(record), result.isSuccess { + records?.remove(at: indexPath.row) + tableView.deleteRows(at: [indexPath], with: UITableViewRowAnimation.automatic) + // reshow empty state if no records left + if records?.count == 0 { + view.addSubview(emptyStateOverlayView) + } + } + } + } + + fileprivate func toggleItem(atIndex indexPath: IndexPath) { + if let record = records?[indexPath.row] { + UnifiedTelemetry.recordEvent(category: .action, method: .tap, object: .readingListItem, value: !record.unread ? .markAsUnread : .markAsRead, extras: [ "from": "reading-list-panel" ]) + if let result = profile.readingList?.updateRecord(record, unread: !record.unread), result.isSuccess { + // TODO This is a bit odd because the success value of the update is an optional optional Record + if let successValue = result.successValue, let updatedRecord = successValue { + records?[indexPath.row] = updatedRecord + tableView.reloadRows(at: [indexPath], with: UITableViewRowAnimation.automatic) + } + } + } + } +} + +extension ReadingListPanel: HomePanelContextMenu { + func presentContextMenu(for site: Site, with indexPath: IndexPath, completionHandler: @escaping () -> PhotonActionSheet?) { + guard let contextMenu = completionHandler() else { return } + self.present(contextMenu, animated: true, completion: nil) + } + + func getSiteDetails(for indexPath: IndexPath) -> Site? { + guard let record = records?[indexPath.row] else { return nil } + return Site(url: record.url, title: record.title) + } + + func getContextMenuActions(for site: Site, with indexPath: IndexPath) -> [PhotonActionSheetItem]? { + guard var actions = getDefaultContextMenuActions(for: site, homePanelDelegate: homePanelDelegate) else { return nil } + + let removeAction: PhotonActionSheetItem = PhotonActionSheetItem(title: Strings.RemoveContextMenuTitle, iconString: "action_remove", handler: { action in + self.deleteItem(atIndex: indexPath) + }) + + actions.append(removeAction) + return actions + } +} diff --git a/mobile/ios/Client/Frontend/Home/RecentlyClosedTabsPanel.swift b/mobile/ios/Client/Frontend/Home/RecentlyClosedTabsPanel.swift new file mode 100644 index 0000000000..9dd027d60c --- /dev/null +++ b/mobile/ios/Client/Frontend/Home/RecentlyClosedTabsPanel.swift @@ -0,0 +1,178 @@ +/* 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 UIKit + +import Shared +import Storage +import XCGLogger +import Deferred + +private let log = Logger.browserLogger + +struct RecentlyClosedPanelUX { + static let IconSize = CGSize(width: 23, height: 23) + static let IconBorderColor = UIColor(white: 0, alpha: 0.1) + static let IconBorderWidth: CGFloat = 0.5 +} + +class RecentlyClosedTabsPanel: UIViewController, HomePanel { + weak var homePanelDelegate: HomePanelDelegate? + var profile: Profile! + + fileprivate lazy var recentlyClosedHeader: UILabel = { + let headerLabel = UILabel() + headerLabel.text = Strings.RecentlyClosedTabsPanelTitle + headerLabel.font = DynamicFontHelper.defaultHelper.DeviceFontHistoryPanel + headerLabel.textAlignment = .center + headerLabel.backgroundColor = .white + return headerLabel + }() + + fileprivate var tableViewController = RecentlyClosedTabsPanelSiteTableViewController() + + fileprivate lazy var historyBackButton: HistoryBackButton = { + let button = HistoryBackButton() + button.addTarget(self, action: #selector(RecentlyClosedTabsPanel.historyBackButtonWasTapped), for: .touchUpInside) + return button + }() + + override func viewDidLoad() { + super.viewDidLoad() + + view.backgroundColor = .white + + tableViewController.profile = self.profile + tableViewController.homePanelDelegate = homePanelDelegate + tableViewController.recentlyClosedTabsPanel = self + + self.addChildViewController(tableViewController) + self.view.addSubview(tableViewController.view) + self.view.addSubview(historyBackButton) + self.view.addSubview(recentlyClosedHeader) + + historyBackButton.snp.makeConstraints { make in + make.top.left.right.equalTo(self.view) + make.height.equalTo(50) + make.bottom.equalTo(recentlyClosedHeader.snp.top) + } + + recentlyClosedHeader.snp.makeConstraints { make in + make.top.equalTo(historyBackButton.snp.bottom) + make.height.equalTo(20) + make.bottom.equalTo(tableViewController.view.snp.top).offset(-10) + make.left.right.equalTo(self.view) + } + + tableViewController.view.snp.makeConstraints { make in + make.left.right.bottom.equalTo(self.view) + } + + tableViewController.didMove(toParentViewController: self) + } + + @objc fileprivate func historyBackButtonWasTapped(_ gestureRecognizer: UITapGestureRecognizer) { + _ = self.navigationController?.popViewController(animated: true) + } + +} + +class RecentlyClosedTabsPanelSiteTableViewController: SiteTableViewController { + weak var homePanelDelegate: HomePanelDelegate? + var recentlyClosedTabs: [ClosedTab] = [] + weak var recentlyClosedTabsPanel: RecentlyClosedTabsPanel? + + fileprivate lazy var longPressRecognizer: UILongPressGestureRecognizer = { + return UILongPressGestureRecognizer(target: self, action: #selector(RecentlyClosedTabsPanelSiteTableViewController.longPress(_:))) + }() + + init() { + super.init(nibName: nil, bundle: nil) + } + + override func viewDidLoad() { + super.viewDidLoad() + tableView.addGestureRecognizer(longPressRecognizer) + tableView.accessibilityIdentifier = "Recently Closed Tabs List" + self.recentlyClosedTabs = profile.recentlyClosedTabs.tabs + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + @objc fileprivate func longPress(_ longPressGestureRecognizer: UILongPressGestureRecognizer) { + guard longPressGestureRecognizer.state == UIGestureRecognizerState.began else { return } + let touchPoint = longPressGestureRecognizer.location(in: tableView) + guard let indexPath = tableView.indexPathForRow(at: touchPoint) else { return } + presentContextMenu(for: indexPath) + } + + override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let cell = super.tableView(tableView, cellForRowAt: indexPath) + guard let twoLineCell = cell as? TwoLineTableViewCell else { + return cell + } + let tab = recentlyClosedTabs[indexPath.row] + let displayURL = tab.url.displayURL ?? tab.url + twoLineCell.setLines(tab.title, detailText: displayURL.absoluteDisplayString) + let site: Favicon? = (tab.faviconURL != nil) ? Favicon(url: tab.faviconURL!, type: .guess) : nil + cell.imageView!.layer.borderColor = RecentlyClosedPanelUX.IconBorderColor.cgColor + cell.imageView!.layer.borderWidth = RecentlyClosedPanelUX.IconBorderWidth + cell.imageView?.setIcon(site, forURL: displayURL, completed: { (color, url) in + if url == displayURL { + cell.imageView?.image = cell.imageView?.image?.createScaled(RecentlyClosedPanelUX.IconSize) + cell.imageView?.contentMode = .center + cell.imageView?.backgroundColor = color + } + }) + return cell + } + + func tableView(_ tableView: UITableView, didSelectRowAtIndexPath indexPath: IndexPath) { + guard let homePanelDelegate = homePanelDelegate, + let recentlyClosedTabsPanel = recentlyClosedTabsPanel else { + log.warning("No site or no URL when selecting row.") + return + } + let visitType = VisitType.typed // Means History, too. + homePanelDelegate.homePanel(recentlyClosedTabsPanel, didSelectURL: recentlyClosedTabs[indexPath.row].url, visitType: visitType) + } + + override func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { + return 0 + } + + // Functions that deal with showing header rows. + func numberOfSectionsInTableView(_ tableView: UITableView) -> Int { + return 1 + } + + override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return profile.recentlyClosedTabs.tabs.count + } + +} + +extension RecentlyClosedTabsPanelSiteTableViewController: HomePanelContextMenu { + func presentContextMenu(for site: Site, with indexPath: IndexPath, completionHandler: @escaping () -> PhotonActionSheet?) { + guard let contextMenu = completionHandler() else { return } + self.present(contextMenu, animated: true, completion: nil) + } + + func getSiteDetails(for indexPath: IndexPath) -> Site? { + let closedTab = recentlyClosedTabs[indexPath.row] + let site: Site + if let title = closedTab.title { + site = Site(url: String(describing: closedTab.url), title: title) + } else { + site = Site(url: String(describing: closedTab.url), title: "") + } + return site + } + + func getContextMenuActions(for site: Site, with indexPath: IndexPath) -> [PhotonActionSheetItem]? { + return getDefaultContextMenuActions(for: site, homePanelDelegate: homePanelDelegate) + } +} diff --git a/mobile/ios/Client/Frontend/Home/RemoteTabsPanel.swift b/mobile/ios/Client/Frontend/Home/RemoteTabsPanel.swift new file mode 100644 index 0000000000..da37a4fa8b --- /dev/null +++ b/mobile/ios/Client/Frontend/Home/RemoteTabsPanel.swift @@ -0,0 +1,610 @@ +/* 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 UIKit +import Account +import Shared +import SnapKit +import Storage +import Sync +import XCGLogger + +private let log = Logger.browserLogger + +private struct RemoteTabsPanelUX { + static let HeaderHeight = SiteTableViewControllerUX.RowHeight // Not HeaderHeight! + static let RowHeight = SiteTableViewControllerUX.RowHeight + static let HeaderBackgroundColor = UIColor(rgb: 0xf8f8f8) + + static let EmptyStateTitleTextColor = UIColor.darkGray + + static let EmptyStateInstructionsTextColor = UIColor.gray + static let EmptyStateInstructionsWidth = 170 + static let EmptyStateTopPaddingInBetweenItems: CGFloat = 15 // UX TODO I set this to 8 so that it all fits on landscape + static let EmptyStateSignInButtonColor = UIColor(red: 0.3, green: 0.62, blue: 1, alpha: 1) + static let EmptyStateSignInButtonTitleColor = UIColor.white + static let EmptyStateSignInButtonCornerRadius: CGFloat = 4 + static let EmptyStateSignInButtonHeight = 44 + static let EmptyStateSignInButtonWidth = 200 + + // Backup and active strings added in Bug 1205294. + static let EmptyStateInstructionsSyncTabsPasswordsBookmarksString = NSLocalizedString("Sync your tabs, bookmarks, passwords and more.", comment: "Text displayed when the Sync home panel is empty, describing the features provided by Sync to invite the user to log in.") + + static let EmptyStateInstructionsSyncTabsPasswordsString = NSLocalizedString("Sync your tabs, passwords and more.", comment: "Text displayed when the Sync home panel is empty, describing the features provided by Sync to invite the user to log in.") + + static let EmptyStateInstructionsGetTabsBookmarksPasswordsString = NSLocalizedString("Get your open tabs, bookmarks, and passwords from your other devices.", comment: "A re-worded offer about Sync, displayed when the Sync home panel is empty, that emphasizes one-way data transfer, not syncing.") + + static let HistoryTableViewHeaderChevronInset: CGFloat = 10 + static let HistoryTableViewHeaderChevronSize: CGFloat = 20 + static let HistoryTableViewHeaderChevronLineWidth: CGFloat = 3.0 +} + +private let RemoteClientIdentifier = "RemoteClient" +private let RemoteTabIdentifier = "RemoteTab" + +class RemoteTabsPanel: UIViewController, HomePanel { + weak var homePanelDelegate: HomePanelDelegate? + fileprivate lazy var tableViewController: RemoteTabsTableViewController = RemoteTabsTableViewController() + fileprivate lazy var historyBackButton: HistoryBackButton = { + let button = HistoryBackButton() + button.addTarget(self, action: #selector(RemoteTabsPanel.historyBackButtonWasTapped), for: .touchUpInside) + return button + }() + var profile: Profile! + + init() { + super.init(nibName: nil, bundle: nil) + NotificationCenter.default.addObserver(self, selector: #selector(RemoteTabsPanel.notificationReceived(_:)), name: NotificationFirefoxAccountChanged, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(RemoteTabsPanel.notificationReceived(_:)), name: NotificationProfileDidFinishSyncing, object: nil) + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + + tableViewController.profile = profile + tableViewController.remoteTabsPanel = self + + view.backgroundColor = UIConstants.PanelBackgroundColor + + addChildViewController(tableViewController) + self.view.addSubview(tableViewController.view) + self.view.addSubview(historyBackButton) + + historyBackButton.snp.makeConstraints { make in + make.top.left.right.equalTo(self.view) + make.height.equalTo(50) + make.bottom.equalTo(tableViewController.view.snp.top) + } + + tableViewController.view.snp.makeConstraints { make in + make.top.equalTo(historyBackButton.snp.bottom) + make.left.right.bottom.equalTo(self.view) + } + + tableViewController.didMove(toParentViewController: self) + } + + func notificationReceived(_ notification: Notification) { + switch notification.name { + case NotificationFirefoxAccountChanged, NotificationProfileDidFinishSyncing: + DispatchQueue.main.async { + print(notification.name) + self.tableViewController.refreshTabs() + } + break + default: + // no need to do anything at all + log.warning("Received unexpected notification \(notification.name)") + break + } + } + + @objc fileprivate func historyBackButtonWasTapped(_ gestureRecognizer: UITapGestureRecognizer) { + _ = self.navigationController?.popViewController(animated: true) + } +} + +enum RemoteTabsError { + case notLoggedIn + case noClients + case noTabs + case failedToSync + + func localizedString() -> String { + switch self { + case .notLoggedIn: + return "" // This does not have a localized string because we have a whole specific screen for it. + case .noClients: + return Strings.EmptySyncedTabsPanelNullStateDescription + case .noTabs: + return NSLocalizedString("You don’t have any tabs open in Firefox on your other devices.", comment: "Error message in the remote tabs panel") + case .failedToSync: + return NSLocalizedString("There was a problem accessing tabs from your other devices. Try again in a few moments.", comment: "Error message in the remote tabs panel") + } + } +} + +protocol RemoteTabsPanelDataSource: UITableViewDataSource, UITableViewDelegate { +} + +class RemoteTabsPanelClientAndTabsDataSource: NSObject, RemoteTabsPanelDataSource { + weak var homePanel: HomePanel? + fileprivate var clientAndTabs: [ClientAndTabs] + + init(homePanel: HomePanel, clientAndTabs: [ClientAndTabs]) { + self.homePanel = homePanel + self.clientAndTabs = clientAndTabs + } + + func numberOfSections(in tableView: UITableView) -> Int { + return self.clientAndTabs.count + } + + func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return self.clientAndTabs[section].tabs.count + } + + func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { + return RemoteTabsPanelUX.HeaderHeight + } + + func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { + let clientTabs = self.clientAndTabs[section] + let client = clientTabs.client + let view = tableView.dequeueReusableHeaderFooterView(withIdentifier: RemoteClientIdentifier) as! TwoLineHeaderFooterView + view.frame = CGRect(x: 0, y: 0, width: tableView.frame.width, height: RemoteTabsPanelUX.HeaderHeight) + view.textLabel?.text = client.name + view.contentView.backgroundColor = RemoteTabsPanelUX.HeaderBackgroundColor + + /* + * A note on timestamps. + * We have access to two timestamps here: the timestamp of the remote client record, + * and the set of timestamps of the client's tabs. + * Neither is "last synced". The client record timestamp changes whenever the remote + * client uploads its record (i.e., infrequently), but also whenever another device + * sends a command to that client -- which can be much later than when that client + * last synced. + * The client's tabs haven't necessarily changed, but it can still have synced. + * Ideally, we should save and use the modified time of the tabs record itself. + * This will be the real time that the other client uploaded tabs. + */ + + let timestamp = clientTabs.approximateLastSyncTime() + let label = NSLocalizedString("Last synced: %@", comment: "Remote tabs last synced time. Argument is the relative date string.") + view.detailTextLabel?.text = String(format: label, Date.fromTimestamp(timestamp).toRelativeTimeString()) + + let image: UIImage? + if client.type == "desktop" { + image = UIImage(named: "deviceTypeDesktop") + image?.accessibilityLabel = NSLocalizedString("computer", comment: "Accessibility label for Desktop Computer (PC) image in remote tabs list") + } else { + image = UIImage(named: "deviceTypeMobile") + image?.accessibilityLabel = NSLocalizedString("mobile device", comment: "Accessibility label for Mobile Device image in remote tabs list") + } + view.imageView.image = image + + view.mergeAccessibilityLabels() + return view + } + + fileprivate func tabAtIndexPath(_ indexPath: IndexPath) -> RemoteTab { + return clientAndTabs[indexPath.section].tabs[indexPath.item] + } + + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let cell = tableView.dequeueReusableCell(withIdentifier: RemoteTabIdentifier, for: indexPath) as! TwoLineTableViewCell + let tab = tabAtIndexPath(indexPath) + cell.setLines(tab.title, detailText: tab.URL.absoluteString) + // TODO: Bug 1144765 - Populate image with cached favicons. + return cell + } + + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + tableView.deselectRow(at: indexPath, animated: false) + let tab = tabAtIndexPath(indexPath) + if let homePanel = self.homePanel { + // It's not a bookmark, so let's call it Typed (which means History, too). + homePanel.homePanelDelegate?.homePanel(homePanel, didSelectURL: tab.URL, visitType: VisitType.typed) + } + } +} + +// MARK: - + +class RemoteTabsPanelErrorDataSource: NSObject, RemoteTabsPanelDataSource { + weak var homePanel: HomePanel? + var error: RemoteTabsError + var notLoggedCell: UITableViewCell? + + init(homePanel: HomePanel, error: RemoteTabsError) { + self.homePanel = homePanel + self.error = error + self.notLoggedCell = nil + } + + func numberOfSections(in tableView: UITableView) -> Int { + return 1 + } + + func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return 1 + } + + func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { + if let cell = self.notLoggedCell { + cell.updateConstraints() + } + return tableView.bounds.height + } + + func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat { + // Making the footer height as small as possible because it will disable button tappability if too high. + return 1 + } + + func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? { + return UIView() + } + + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + switch error { + case .notLoggedIn: + let cell = RemoteTabsNotLoggedInCell(homePanel: homePanel) + self.notLoggedCell = cell + return cell + default: + let cell = RemoteTabsErrorCell(error: self.error) + self.notLoggedCell = nil + return cell + } + } + +} + +// MARK: - + +class RemoteTabsErrorCell: UITableViewCell { + static let Identifier = "RemoteTabsErrorCell" + + init(error: RemoteTabsError) { + super.init(style: .default, reuseIdentifier: RemoteTabsErrorCell.Identifier) + + separatorInset = UIEdgeInsets(top: 0, left: 1000, bottom: 0, right: 0) + + let containerView = UIView() + contentView.addSubview(containerView) + + let imageView = UIImageView() + imageView.image = UIImage(named: "emptySync") + containerView.addSubview(imageView) + imageView.snp.makeConstraints { (make) -> Void in + make.top.equalTo(containerView) + make.centerX.equalTo(containerView) + } + + let titleLabel = UILabel() + titleLabel.font = DynamicFontHelper.defaultHelper.DeviceFont + titleLabel.text = Strings.EmptySyncedTabsPanelStateTitle + titleLabel.textAlignment = NSTextAlignment.center + titleLabel.textColor = RemoteTabsPanelUX.EmptyStateTitleTextColor + containerView.addSubview(titleLabel) + + let instructionsLabel = UILabel() + instructionsLabel.font = DynamicFontHelper.defaultHelper.DeviceFontSmallLight + instructionsLabel.text = error.localizedString() + instructionsLabel.textAlignment = NSTextAlignment.center + instructionsLabel.textColor = RemoteTabsPanelUX.EmptyStateInstructionsTextColor + instructionsLabel.numberOfLines = 0 + containerView.addSubview(instructionsLabel) + + titleLabel.snp.makeConstraints { make in + make.top.equalTo(imageView.snp.bottom).offset(RemoteTabsPanelUX.EmptyStateTopPaddingInBetweenItems) + make.centerX.equalTo(imageView) + } + + instructionsLabel.snp.makeConstraints { make in + make.top.equalTo(titleLabel.snp.bottom).offset(RemoteTabsPanelUX.EmptyStateTopPaddingInBetweenItems / 2) + make.centerX.equalTo(containerView) + make.width.equalTo(RemoteTabsPanelUX.EmptyStateInstructionsWidth) + } + + containerView.snp.makeConstraints { make in + // Let the container wrap around the content + make.top.equalTo(imageView.snp.top) + make.left.bottom.right.equalTo(instructionsLabel) + // And then center it in the overlay view that sits on top of the UITableView + make.centerX.equalTo(contentView) + + // Sets proper top constraint for iPhone 6 in portait and for iPad. + make.centerY.equalTo(contentView.snp.centerY).offset(HomePanelUX.EmptyTabContentOffset).priority(100) + + // Sets proper top constraint for iPhone 4, 5 in portrait. + make.top.greaterThanOrEqualTo(contentView.snp.top).offset(20).priority(1000) + } + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +// MARK: - + +class RemoteTabsNotLoggedInCell: UITableViewCell { + static let Identifier = "RemoteTabsNotLoggedInCell" + var homePanel: HomePanel? + var instructionsLabel: UILabel + var signInButton: UIButton + var titleLabel: UILabel + var emptyStateImageView: UIImageView + + init(homePanel: HomePanel?) { + let titleLabel = UILabel() + let instructionsLabel = UILabel() + let signInButton = UIButton() + let imageView = UIImageView() + + self.instructionsLabel = instructionsLabel + self.signInButton = signInButton + self.titleLabel = titleLabel + self.emptyStateImageView = imageView + + super.init(style: .default, reuseIdentifier: RemoteTabsErrorCell.Identifier) + + self.homePanel = homePanel + let createAnAccountButton = UIButton(type: .system) + + imageView.image = UIImage(named: "emptySync") + contentView.addSubview(imageView) + + titleLabel.font = DynamicFontHelper.defaultHelper.DeviceFont + titleLabel.text = Strings.EmptySyncedTabsPanelStateTitle + titleLabel.textAlignment = NSTextAlignment.center + titleLabel.textColor = RemoteTabsPanelUX.EmptyStateTitleTextColor + contentView.addSubview(titleLabel) + + instructionsLabel.font = DynamicFontHelper.defaultHelper.DeviceFontSmallLight + instructionsLabel.text = Strings.EmptySyncedTabsPanelStateDescription + instructionsLabel.textAlignment = NSTextAlignment.center + instructionsLabel.textColor = RemoteTabsPanelUX.EmptyStateInstructionsTextColor + instructionsLabel.numberOfLines = 0 + contentView.addSubview(instructionsLabel) + + signInButton.backgroundColor = RemoteTabsPanelUX.EmptyStateSignInButtonColor + signInButton.setTitle(NSLocalizedString("Sign in", comment: "See http://mzl.la/1Qtkf0j"), for: UIControlState()) + signInButton.setTitleColor(RemoteTabsPanelUX.EmptyStateSignInButtonTitleColor, for: UIControlState()) + signInButton.titleLabel?.font = UIFont.preferredFont(forTextStyle: UIFontTextStyle.subheadline) + signInButton.layer.cornerRadius = RemoteTabsPanelUX.EmptyStateSignInButtonCornerRadius + signInButton.clipsToBounds = true + signInButton.addTarget(self, action: #selector(RemoteTabsNotLoggedInCell.SELsignIn), for: UIControlEvents.touchUpInside) + contentView.addSubview(signInButton) + + createAnAccountButton.setTitle(NSLocalizedString("Create an account", comment: "See http://mzl.la/1Qtkf0j"), for: UIControlState()) + createAnAccountButton.titleLabel?.font = UIFont.preferredFont(forTextStyle: UIFontTextStyle.caption1) + createAnAccountButton.addTarget(self, action: #selector(RemoteTabsNotLoggedInCell.SELcreateAnAccount), for: UIControlEvents.touchUpInside) + contentView.addSubview(createAnAccountButton) + + imageView.snp.makeConstraints { (make) -> Void in + make.centerX.equalTo(instructionsLabel) + + // Sets proper top constraint for iPhone 6 in portait and for iPad. + make.centerY.equalTo(contentView).offset(HomePanelUX.EmptyTabContentOffset + 30).priority(100) + + // Sets proper top constraint for iPhone 4, 5 in portrait. + make.top.greaterThanOrEqualTo(contentView.snp.top).priority(1000) + } + + titleLabel.snp.makeConstraints { make in + make.top.equalTo(imageView.snp.bottom).offset(RemoteTabsPanelUX.EmptyStateTopPaddingInBetweenItems) + make.centerX.equalTo(imageView) + } + + createAnAccountButton.snp.makeConstraints { (make) -> Void in + make.centerX.equalTo(signInButton) + make.top.equalTo(signInButton.snp.bottom).offset(RemoteTabsPanelUX.EmptyStateTopPaddingInBetweenItems) + } + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + @objc fileprivate func SELsignIn() { + if let homePanel = self.homePanel { + homePanel.homePanelDelegate?.homePanelDidRequestToSignIn(homePanel) + } + } + + @objc fileprivate func SELcreateAnAccount() { + if let homePanel = self.homePanel { + homePanel.homePanelDelegate?.homePanelDidRequestToCreateAccount(homePanel) + } + } + + override func updateConstraints() { + if UIInterfaceOrientationIsLandscape(UIApplication.shared.statusBarOrientation) && !(DeviceInfo.deviceModel().range(of: "iPad") != nil) { + instructionsLabel.snp.remakeConstraints { make in + make.top.equalTo(titleLabel.snp.bottom).offset(RemoteTabsPanelUX.EmptyStateTopPaddingInBetweenItems) + make.width.equalTo(RemoteTabsPanelUX.EmptyStateInstructionsWidth) + + // Sets proper landscape layout for bigger phones: iPhone 6 and on. + make.left.lessThanOrEqualTo(contentView.snp.left).offset(80).priority(100) + + // Sets proper landscape layout for smaller phones: iPhone 4 & 5. + make.right.lessThanOrEqualTo(contentView.snp.centerX).offset(-30).priority(1000) + } + + signInButton.snp.remakeConstraints { make in + make.height.equalTo(RemoteTabsPanelUX.EmptyStateSignInButtonHeight) + make.width.equalTo(RemoteTabsPanelUX.EmptyStateSignInButtonWidth) + make.centerY.equalTo(emptyStateImageView).offset(2*RemoteTabsPanelUX.EmptyStateTopPaddingInBetweenItems) + + // Sets proper landscape layout for bigger phones: iPhone 6 and on. + make.right.greaterThanOrEqualTo(contentView.snp.right).offset(-70).priority(100) + + // Sets proper landscape layout for smaller phones: iPhone 4 & 5. + make.left.greaterThanOrEqualTo(contentView.snp.centerX).offset(10).priority(1000) + } + } else { + instructionsLabel.snp.remakeConstraints { make in + make.top.equalTo(titleLabel.snp.bottom).offset(RemoteTabsPanelUX.EmptyStateTopPaddingInBetweenItems) + make.centerX.equalTo(contentView) + make.width.equalTo(RemoteTabsPanelUX.EmptyStateInstructionsWidth) + } + + signInButton.snp.remakeConstraints { make in + make.centerX.equalTo(contentView) + make.top.equalTo(instructionsLabel.snp.bottom).offset(RemoteTabsPanelUX.EmptyStateTopPaddingInBetweenItems) + make.height.equalTo(RemoteTabsPanelUX.EmptyStateSignInButtonHeight) + make.width.equalTo(RemoteTabsPanelUX.EmptyStateSignInButtonWidth) + } + } + super.updateConstraints() + } +} + +fileprivate class RemoteTabsTableViewController: UITableViewController { + weak var remoteTabsPanel: RemoteTabsPanel? + var profile: Profile! + var tableViewDelegate: RemoteTabsPanelDataSource? { + didSet { + tableView.dataSource = tableViewDelegate + tableView.delegate = tableViewDelegate + } + } + + fileprivate lazy var longPressRecognizer: UILongPressGestureRecognizer = { + return UILongPressGestureRecognizer(target: self, action: #selector(RemoteTabsTableViewController.longPress(_:))) + }() + + override func viewDidLoad() { + super.viewDidLoad() + tableView.addGestureRecognizer(longPressRecognizer) + tableView.register(TwoLineHeaderFooterView.self, forHeaderFooterViewReuseIdentifier: RemoteClientIdentifier) + tableView.register(TwoLineTableViewCell.self, forCellReuseIdentifier: RemoteTabIdentifier) + + tableView.rowHeight = RemoteTabsPanelUX.RowHeight + tableView.separatorInset = UIEdgeInsets.zero + + tableView.delegate = nil + tableView.dataSource = nil + + refreshControl = UIRefreshControl() + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + refreshControl?.addTarget(self, action: #selector(RemoteTabsTableViewController.refreshTabs), for: .valueChanged) + refreshTabs() + } + + override func viewDidDisappear(_ animated: Bool) { + super.viewDidDisappear(animated) + refreshControl?.removeTarget(self, action: #selector(RemoteTabsTableViewController.refreshTabs), for: .valueChanged) + } + + fileprivate func startRefreshing() { + if let refreshControl = self.refreshControl { + let height = -refreshControl.bounds.size.height + tableView.setContentOffset(CGPoint(x: 0, y: height), animated: true) + refreshControl.beginRefreshing() + } + } + + func endRefreshing() { + if self.refreshControl?.isRefreshing ?? false { + self.refreshControl?.endRefreshing() + } + + self.tableView.isScrollEnabled = true + self.tableView.reloadData() + } + + func updateDelegateClientAndTabData(_ clientAndTabs: [ClientAndTabs]) { + guard let remoteTabsPanel = remoteTabsPanel else { return } + if clientAndTabs.count == 0 { + self.tableViewDelegate = RemoteTabsPanelErrorDataSource(homePanel: remoteTabsPanel, error: .noClients) + } else { + let nonEmptyClientAndTabs = clientAndTabs.filter { $0.tabs.count > 0 } + if nonEmptyClientAndTabs.count == 0 { + self.tableViewDelegate = RemoteTabsPanelErrorDataSource(homePanel: remoteTabsPanel, error: .noTabs) + } else { + self.tableViewDelegate = RemoteTabsPanelClientAndTabsDataSource(homePanel: remoteTabsPanel, clientAndTabs: nonEmptyClientAndTabs) + tableView.allowsSelection = true + } + } + } + + @objc fileprivate func refreshTabs() { + guard let remoteTabsPanel = remoteTabsPanel else { return } + + assert(Thread.isMainThread) + + tableView.isScrollEnabled = false + tableView.allowsSelection = false + tableView.tableFooterView = UIView(frame: CGRect.zero) + + // Short circuit if the user is not logged in + if !profile.hasSyncableAccount() { + self.tableViewDelegate = RemoteTabsPanelErrorDataSource(homePanel: remoteTabsPanel, error: .notLoggedIn) + self.endRefreshing() + return + } + + self.profile.getCachedClientsAndTabs().uponQueue(DispatchQueue.main) { result in + if let clientAndTabs = result.successValue { + self.updateDelegateClientAndTabData(clientAndTabs) + } + + // Fetch the tabs from the cloud if it has been more than 5 seconds since the last sync. + let lastSyncTime = self.profile.prefs.timestampForKey(PrefsKeys.KeyLastRemoteTabSyncTime) ?? 0 + if Date.now() > lastSyncTime && Date.now() - lastSyncTime > OneSecondInMilliseconds * 5 { + self.startRefreshing() + self.profile.getClientsAndTabs().uponQueue(DispatchQueue.main) { result in + // We set the last sync time to now, regardless of whether the sync was successful, to avoid trying to sync over + // and over again in cases whether the client is unable to sync (e.g. when there is no network connectivity). + self.profile.prefs.setTimestamp(Date.now(), forKey: PrefsKeys.KeyLastRemoteTabSyncTime) + if let clientAndTabs = result.successValue { + self.updateDelegateClientAndTabData(clientAndTabs) + } + self.endRefreshing() + } + } else { + // If we failed before and didn't sync, show the failure delegate + if let _ = result.failureValue { + self.tableViewDelegate = RemoteTabsPanelErrorDataSource(homePanel: remoteTabsPanel, error: .failedToSync) + } + + self.endRefreshing() + } + } + } + + @objc fileprivate func longPress(_ longPressGestureRecognizer: UILongPressGestureRecognizer) { + guard longPressGestureRecognizer.state == UIGestureRecognizerState.began else { return } + let touchPoint = longPressGestureRecognizer.location(in: tableView) + guard let indexPath = tableView.indexPathForRow(at: touchPoint) else { return } + presentContextMenu(for: indexPath) + } +} + +extension RemoteTabsTableViewController: HomePanelContextMenu { + func presentContextMenu(for site: Site, with indexPath: IndexPath, completionHandler: @escaping () -> PhotonActionSheet?) { + guard let contextMenu = completionHandler() else { return } + self.present(contextMenu, animated: true, completion: nil) + } + + func getSiteDetails(for indexPath: IndexPath) -> Site? { + guard let tab = (tableViewDelegate as? RemoteTabsPanelClientAndTabsDataSource)?.tabAtIndexPath(indexPath) else { return nil } + return Site(url: String(describing: tab.URL), title: tab.title) + } + + func getContextMenuActions(for site: Site, with indexPath: IndexPath) -> [PhotonActionSheetItem]? { + return getDefaultContextMenuActions(for: site, homePanelDelegate: remoteTabsPanel?.homePanelDelegate) + } +} diff --git a/mobile/ios/Client/Frontend/Intro/Intro.xcassets/Contents.json b/mobile/ios/Client/Frontend/Intro/Intro.xcassets/Contents.json new file mode 100644 index 0000000000..da4a164c91 --- /dev/null +++ b/mobile/ios/Client/Frontend/Intro/Intro.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/mobile/ios/Client/Frontend/Intro/Intro.xcassets/tour-Mail.imageset/Contents.json b/mobile/ios/Client/Frontend/Intro/Intro.xcassets/tour-Mail.imageset/Contents.json new file mode 100644 index 0000000000..a60ac59d12 --- /dev/null +++ b/mobile/ios/Client/Frontend/Intro/Intro.xcassets/tour-Mail.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "tour-Mail.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "tour-Mail@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "tour-Mail@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/mobile/ios/Client/Frontend/Intro/Intro.xcassets/tour-Mail.imageset/tour-Mail.png b/mobile/ios/Client/Frontend/Intro/Intro.xcassets/tour-Mail.imageset/tour-Mail.png new file mode 100644 index 0000000000..8acb0bd09d Binary files /dev/null and b/mobile/ios/Client/Frontend/Intro/Intro.xcassets/tour-Mail.imageset/tour-Mail.png differ diff --git a/mobile/ios/Client/Frontend/Intro/Intro.xcassets/tour-Mail.imageset/tour-Mail@2x.png b/mobile/ios/Client/Frontend/Intro/Intro.xcassets/tour-Mail.imageset/tour-Mail@2x.png new file mode 100644 index 0000000000..4e2c4a07a8 Binary files /dev/null and b/mobile/ios/Client/Frontend/Intro/Intro.xcassets/tour-Mail.imageset/tour-Mail@2x.png differ diff --git a/mobile/ios/Client/Frontend/Intro/Intro.xcassets/tour-Mail.imageset/tour-Mail@3x.png b/mobile/ios/Client/Frontend/Intro/Intro.xcassets/tour-Mail.imageset/tour-Mail@3x.png new file mode 100644 index 0000000000..f70c072e49 Binary files /dev/null and b/mobile/ios/Client/Frontend/Intro/Intro.xcassets/tour-Mail.imageset/tour-Mail@3x.png differ diff --git a/mobile/ios/Client/Frontend/Intro/Intro.xcassets/tour-Private.imageset/Contents.json b/mobile/ios/Client/Frontend/Intro/Intro.xcassets/tour-Private.imageset/Contents.json new file mode 100644 index 0000000000..4515371a3e --- /dev/null +++ b/mobile/ios/Client/Frontend/Intro/Intro.xcassets/tour-Private.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "tour-Private.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "tour-Private@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "tour-Private@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/mobile/ios/Client/Frontend/Intro/Intro.xcassets/tour-Private.imageset/tour-Private.png b/mobile/ios/Client/Frontend/Intro/Intro.xcassets/tour-Private.imageset/tour-Private.png new file mode 100644 index 0000000000..f69229e433 Binary files /dev/null and b/mobile/ios/Client/Frontend/Intro/Intro.xcassets/tour-Private.imageset/tour-Private.png differ diff --git a/mobile/ios/Client/Frontend/Intro/Intro.xcassets/tour-Private.imageset/tour-Private@2x.png b/mobile/ios/Client/Frontend/Intro/Intro.xcassets/tour-Private.imageset/tour-Private@2x.png new file mode 100644 index 0000000000..866cfb568e Binary files /dev/null and b/mobile/ios/Client/Frontend/Intro/Intro.xcassets/tour-Private.imageset/tour-Private@2x.png differ diff --git a/mobile/ios/Client/Frontend/Intro/Intro.xcassets/tour-Private.imageset/tour-Private@3x.png b/mobile/ios/Client/Frontend/Intro/Intro.xcassets/tour-Private.imageset/tour-Private@3x.png new file mode 100644 index 0000000000..f1eb654812 Binary files /dev/null and b/mobile/ios/Client/Frontend/Intro/Intro.xcassets/tour-Private.imageset/tour-Private@3x.png differ diff --git a/mobile/ios/Client/Frontend/Intro/Intro.xcassets/tour-Search.imageset/Contents.json b/mobile/ios/Client/Frontend/Intro/Intro.xcassets/tour-Search.imageset/Contents.json new file mode 100644 index 0000000000..13d08e5fa9 --- /dev/null +++ b/mobile/ios/Client/Frontend/Intro/Intro.xcassets/tour-Search.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "tour-Search.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "tour-Search@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "tour-Search@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/mobile/ios/Client/Frontend/Intro/Intro.xcassets/tour-Search.imageset/tour-Search.png b/mobile/ios/Client/Frontend/Intro/Intro.xcassets/tour-Search.imageset/tour-Search.png new file mode 100644 index 0000000000..00d93b48d8 Binary files /dev/null and b/mobile/ios/Client/Frontend/Intro/Intro.xcassets/tour-Search.imageset/tour-Search.png differ diff --git a/mobile/ios/Client/Frontend/Intro/Intro.xcassets/tour-Search.imageset/tour-Search@2x.png b/mobile/ios/Client/Frontend/Intro/Intro.xcassets/tour-Search.imageset/tour-Search@2x.png new file mode 100644 index 0000000000..c6408bfea9 Binary files /dev/null and b/mobile/ios/Client/Frontend/Intro/Intro.xcassets/tour-Search.imageset/tour-Search@2x.png differ diff --git a/mobile/ios/Client/Frontend/Intro/Intro.xcassets/tour-Search.imageset/tour-Search@3x.png b/mobile/ios/Client/Frontend/Intro/Intro.xcassets/tour-Search.imageset/tour-Search@3x.png new file mode 100644 index 0000000000..c68fb44788 Binary files /dev/null and b/mobile/ios/Client/Frontend/Intro/Intro.xcassets/tour-Search.imageset/tour-Search@3x.png differ diff --git a/mobile/ios/Client/Frontend/Intro/Intro.xcassets/tour-Sync.imageset/Contents.json b/mobile/ios/Client/Frontend/Intro/Intro.xcassets/tour-Sync.imageset/Contents.json new file mode 100644 index 0000000000..195cc0030f --- /dev/null +++ b/mobile/ios/Client/Frontend/Intro/Intro.xcassets/tour-Sync.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "tour-Sync.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "tour-Sync@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "tour-Sync@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/mobile/ios/Client/Frontend/Intro/Intro.xcassets/tour-Sync.imageset/tour-Sync.png b/mobile/ios/Client/Frontend/Intro/Intro.xcassets/tour-Sync.imageset/tour-Sync.png new file mode 100644 index 0000000000..9dade46278 Binary files /dev/null and b/mobile/ios/Client/Frontend/Intro/Intro.xcassets/tour-Sync.imageset/tour-Sync.png differ diff --git a/mobile/ios/Client/Frontend/Intro/Intro.xcassets/tour-Sync.imageset/tour-Sync@2x.png b/mobile/ios/Client/Frontend/Intro/Intro.xcassets/tour-Sync.imageset/tour-Sync@2x.png new file mode 100644 index 0000000000..44a69fa456 Binary files /dev/null and b/mobile/ios/Client/Frontend/Intro/Intro.xcassets/tour-Sync.imageset/tour-Sync@2x.png differ diff --git a/mobile/ios/Client/Frontend/Intro/Intro.xcassets/tour-Sync.imageset/tour-Sync@3x.png b/mobile/ios/Client/Frontend/Intro/Intro.xcassets/tour-Sync.imageset/tour-Sync@3x.png new file mode 100644 index 0000000000..4853bd12e1 Binary files /dev/null and b/mobile/ios/Client/Frontend/Intro/Intro.xcassets/tour-Sync.imageset/tour-Sync@3x.png differ diff --git a/mobile/ios/Client/Frontend/Intro/Intro.xcassets/tour-Welcome.imageset/Contents.json b/mobile/ios/Client/Frontend/Intro/Intro.xcassets/tour-Welcome.imageset/Contents.json new file mode 100644 index 0000000000..cb2f3b376d --- /dev/null +++ b/mobile/ios/Client/Frontend/Intro/Intro.xcassets/tour-Welcome.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "tour-Welcome.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "tour-Welcome@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "tour-Welcome@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/mobile/ios/Client/Frontend/Intro/Intro.xcassets/tour-Welcome.imageset/tour-Welcome.png b/mobile/ios/Client/Frontend/Intro/Intro.xcassets/tour-Welcome.imageset/tour-Welcome.png new file mode 100644 index 0000000000..ca86833f8f Binary files /dev/null and b/mobile/ios/Client/Frontend/Intro/Intro.xcassets/tour-Welcome.imageset/tour-Welcome.png differ diff --git a/mobile/ios/Client/Frontend/Intro/Intro.xcassets/tour-Welcome.imageset/tour-Welcome@2x.png b/mobile/ios/Client/Frontend/Intro/Intro.xcassets/tour-Welcome.imageset/tour-Welcome@2x.png new file mode 100644 index 0000000000..276af880a2 Binary files /dev/null and b/mobile/ios/Client/Frontend/Intro/Intro.xcassets/tour-Welcome.imageset/tour-Welcome@2x.png differ diff --git a/mobile/ios/Client/Frontend/Intro/Intro.xcassets/tour-Welcome.imageset/tour-Welcome@3x.png b/mobile/ios/Client/Frontend/Intro/Intro.xcassets/tour-Welcome.imageset/tour-Welcome@3x.png new file mode 100644 index 0000000000..5c3d37dcb6 Binary files /dev/null and b/mobile/ios/Client/Frontend/Intro/Intro.xcassets/tour-Welcome.imageset/tour-Welcome@3x.png differ diff --git a/mobile/ios/Client/Frontend/Intro/IntroViewController.swift b/mobile/ios/Client/Frontend/Intro/IntroViewController.swift new file mode 100644 index 0000000000..631638c6d3 --- /dev/null +++ b/mobile/ios/Client/Frontend/Intro/IntroViewController.swift @@ -0,0 +1,437 @@ +/* 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 UIKit +import SnapKit + +struct IntroViewControllerUX { + static let Width = 375 + static let Height = 667 + static let SyncButtonTopPadding = 5 + static let MinimumFontScale: CGFloat = 0.5 + + static let CardSlides = ["tour-Welcome", "tour-Search", "tour-Private", "tour-Mail", "tour-Sync"] + static let NumberOfCards = CardSlides.count + + static let PagerCenterOffsetFromScrollViewBottom = UIScreen.main.bounds.width <= 320 ? 20 : 30 + + static let StartBrowsingButtonTitle = NSLocalizedString("Start Browsing", tableName: "Intro", comment: "See http://mzl.la/1T8gxwo") + static let StartBrowsingButtonColor = UIColor(rgb: 0x4990E2) + static let StartBrowsingButtonHeight = 56 + static let SignInButtonTitle = NSLocalizedString("Sign in to Firefox", tableName: "Intro", comment: "See http://mzl.la/1T8gxwo") + static let SignInButtonColor = UIColor(rgb: 0x45A1FF) + static let SignInButtonHeight = 60 + static let SignInButtonWidth = 290 + + static let CardTextLineHeight = UIScreen.main.bounds.width <= 320 ? CGFloat(2) : CGFloat(6) + static let CardTextWidth = UIScreen.main.bounds.width <= 320 ? 240 : 280 + static let CardTitleHeight = 50 + + static let CardTitleWelcome = NSLocalizedString("Intro.Slides.Welcome.Title", tableName: "Intro", value: "Thanks for choosing Firefox!", comment: "Title for the first panel 'Welcome' in the First Run tour.") + static let CardTitleSearch = NSLocalizedString("Intro.Slides.Search.Title", tableName: "Intro", value: "Your search, your way", comment: "Title for the second panel 'Search' in the First Run tour.") + static let CardTitlePrivate = NSLocalizedString("Intro.Slides.Private.Title", tableName: "Intro", value: "Browse like no one’s watching", comment: "Title for the third panel 'Private Browsing' in the First Run tour.") + static let CardTitleMail = NSLocalizedString("Intro.Slides.Mail.Title", tableName: "Intro", value: "You’ve got mail… options", comment: "Title for the fourth panel 'Mail' in the First Run tour.") + static let CardTitleSync = NSLocalizedString("Intro.Slides.Sync.Title", tableName: "Intro", value: "Pick up where you left off", comment: "Title for the fifth panel 'Sync' in the First Run tour.") + + static let CardTextWelcome = NSLocalizedString("Intro.Slides.Welcome.Description", tableName: "Intro", value: "A modern mobile browser from Mozilla, the non-profit committed to a free and open web.", comment: "Description for the 'Welcome' panel in the First Run tour.") + static let CardTextSearch = NSLocalizedString("Intro.Slides.Search.Description", tableName: "Intro", value: "Searching for something different? Choose another default search engine (or add your own) in Settings.", comment: "Description for the 'Favorite Search Engine' panel in the First Run tour.") + static let CardTextPrivate = NSLocalizedString("Intro.Slides.Private.Description", tableName: "Intro", value: "Tap the mask icon to slip into Private Browsing mode.", comment: "Description for the 'Private Browsing' panel in the First Run tour.") + static let CardTextMail = NSLocalizedString("Intro.Slides.Mail.Description", tableName: "Intro", value: "Use any email app — not just Mail — with Firefox.", comment: "Description for the 'Mail' panel in the First Run tour.") + static let CardTextSync = NSLocalizedString("Intro.Slides.Sync.Description", tableName: "Intro", value: "Use Sync to find the bookmarks, passwords, and other things you save to Firefox on all your devices.", comment: "Description for the 'Sync' panel in the First Run tour.") + + static let FadeDuration = 0.25 +} + +let IntroViewControllerSeenProfileKey = "IntroViewControllerSeen" + +protocol IntroViewControllerDelegate: class { + func introViewControllerDidFinish(_ introViewController: IntroViewController, requestToLogin: Bool) +} + +class IntroViewController: UIViewController, UIScrollViewDelegate { + weak var delegate: IntroViewControllerDelegate? + + var slides = [UIImage]() + var cards = [UIImageView]() + var introViews = [UIView]() + var titleLabels = [UILabel]() + var textLabels = [UILabel]() + + var startBrowsingButton: UIButton! + var introView: UIView? + var slideContainer: UIView! + var pageControl: UIPageControl! + var backButton: UIButton! + var forwardButton: UIButton! + var signInButton: UIButton! + + fileprivate var scrollView: IntroOverlayScrollView! + + var slideVerticalScaleFactor: CGFloat = 1.0 + + override func viewDidLoad() { + view.backgroundColor = UIColor.white + + // scale the slides down for iPhone 4S + if view.frame.height <= 480 { + slideVerticalScaleFactor = 1.33 + slideVerticalScaleFactor = 1.33 //4S + } else if view.frame.height <= 568 { + slideVerticalScaleFactor = 1.15 //SE + } + + for slideName in IntroViewControllerUX.CardSlides { + slides.append(UIImage(named: slideName)!) + } + + startBrowsingButton = UIButton() + startBrowsingButton.backgroundColor = UIColor.clear + startBrowsingButton.setTitle(IntroViewControllerUX.StartBrowsingButtonTitle, for: UIControlState()) + startBrowsingButton.setTitleColor(IntroViewControllerUX.StartBrowsingButtonColor, for: UIControlState()) + startBrowsingButton.addTarget(self, action: #selector(IntroViewController.SELstartBrowsing), for: UIControlEvents.touchUpInside) + startBrowsingButton.accessibilityIdentifier = "IntroViewController.startBrowsingButton" + + view.addSubview(startBrowsingButton) + startBrowsingButton.snp.makeConstraints { (make) -> Void in + if #available(iOS 11.0, *) { + make.left.right.bottom.equalTo(self.view.safeAreaLayoutGuide) + } else { + make.left.right.bottom.equalTo(self.view) + } + make.height.equalTo(IntroViewControllerUX.StartBrowsingButtonHeight) + } + + scrollView = IntroOverlayScrollView() + scrollView.backgroundColor = UIColor.clear + scrollView.accessibilityLabel = NSLocalizedString("Intro Tour Carousel", comment: "Accessibility label for the introduction tour carousel") + scrollView.delegate = self + scrollView.bounces = false + scrollView.isPagingEnabled = true + scrollView.showsHorizontalScrollIndicator = false + scrollView.contentSize = CGSize(width: scaledWidthOfSlide * CGFloat(IntroViewControllerUX.NumberOfCards), height: scaledHeightOfSlide) + scrollView.accessibilityIdentifier = "IntroViewController.scrollView" + view.addSubview(scrollView) + + slideContainer = UIView() + for i in 0.. Void in + make.left.right.top.equalTo(self.view) + make.bottom.equalTo(startBrowsingButton.snp.top) + } + self.view.layoutIfNeeded() + self.scrollView.layoutIfNeeded() + + pageControl = UIPageControl() + pageControl.pageIndicatorTintColor = UIColor.black.withAlphaComponent(0.3) + pageControl.currentPageIndicatorTintColor = UIColor.black + pageControl.numberOfPages = IntroViewControllerUX.NumberOfCards + pageControl.accessibilityIdentifier = "IntroViewController.pageControl" + pageControl.addTarget(self, action: #selector(IntroViewController.changePage), for: UIControlEvents.valueChanged) + + view.addSubview(pageControl) + pageControl.snp.makeConstraints { (make) -> Void in + make.centerX.equalTo(self.scrollView) + make.centerY.equalTo(self.startBrowsingButton.snp.top).offset(-IntroViewControllerUX.PagerCenterOffsetFromScrollViewBottom) + } + + func addCard(title: String, text: String, introView: UIView) { + self.introViews.append(introView) + + let titleLabel = UILabel() + titleLabel.numberOfLines = 2 + titleLabel.adjustsFontSizeToFitWidth = true + titleLabel.minimumScaleFactor = IntroViewControllerUX.MinimumFontScale + titleLabel.textAlignment = NSTextAlignment.center + titleLabel.text = title + titleLabels.append(titleLabel) + introView.addSubview(titleLabel) + titleLabel.snp.makeConstraints { (make ) -> Void in + make.top.equalTo(introView).offset(20) + make.centerX.equalTo(introView) + make.height.equalTo(IntroViewControllerUX.CardTitleHeight) + make.width.equalTo(IntroViewControllerUX.CardTextWidth) + } + + let textLabel = UILabel() + textLabel.numberOfLines = 5 + textLabel.attributedText = attributedStringForLabel(text) + textLabel.adjustsFontSizeToFitWidth = true + textLabel.minimumScaleFactor = IntroViewControllerUX.MinimumFontScale + textLabel.lineBreakMode = .byTruncatingTail + textLabels.append(textLabel) + introView.addSubview(textLabel) + textLabel.snp.makeConstraints({ (make) -> Void in + make.top.equalTo(titleLabel.snp.bottom).offset(20) + make.centerX.equalTo(introView) + make.width.equalTo(IntroViewControllerUX.CardTextWidth) + }) + } + addCard(title: IntroViewControllerUX.CardTitleWelcome, text: IntroViewControllerUX.CardTextWelcome, introView: UIView()) + addCard(title: IntroViewControllerUX.CardTitleSearch, text: IntroViewControllerUX.CardTextSearch, introView: UIView()) + addCard(title: IntroViewControllerUX.CardTitlePrivate, text: IntroViewControllerUX.CardTextPrivate, introView: UIView()) + addCard(title: IntroViewControllerUX.CardTitleMail, text: IntroViewControllerUX.CardTextMail, introView: UIView()) + + // Sync card, with sign in to sync button. + signInButton = UIButton() + signInButton.backgroundColor = IntroViewControllerUX.SignInButtonColor + signInButton.setTitle(IntroViewControllerUX.SignInButtonTitle, for: UIControlState()) + signInButton.setTitleColor(UIColor.white, for: UIControlState()) + signInButton.clipsToBounds = true + signInButton.addTarget(self, action: #selector(IntroViewController.SELlogin), for: UIControlEvents.touchUpInside) + signInButton.snp.makeConstraints { (make) -> Void in + make.height.equalTo(IntroViewControllerUX.SignInButtonHeight) + } + + let syncCardView = UIView() + // introViews.append(syncCardView) + addCard(title: IntroViewControllerUX.CardTitleSync, text: IntroViewControllerUX.CardTextSync, introView: syncCardView) + + syncCardView.addSubview(signInButton) + syncCardView.bringSubview(toFront: signInButton) + signInButton.snp.makeConstraints { (make) -> Void in + make.centerX.equalTo(syncCardView) + make.width.equalTo(IntroViewControllerUX.CardTextWidth) // TODO Talk to UX about small screen sizes + make.bottom.equalTo(syncCardView) + } + + // We need a reference to the sync pages textlabel so we can adjust the constraints + if let index = introViews.index(of: syncCardView), index < textLabels.count { + let syncTextLabel = textLabels[index] + syncTextLabel.snp.makeConstraints { make in + make.bottom.equalTo(signInButton.snp.top).offset(-IntroViewControllerUX.SyncButtonTopPadding) + } + } + // Add all the cards to the view, make them invisible with zero alpha + for introView in introViews { + introView.alpha = 0 + self.view.addSubview(introView) + introView.snp.makeConstraints { (make) -> Void in + make.top.equalTo(self.slideContainer.snp.bottom) + make.bottom.equalTo(self.startBrowsingButton.snp.top) + make.left.right.equalTo(self.view) + } + } + + // Make whole screen scrollable by bringing the scrollview to the top + view.bringSubview(toFront: scrollView) + view.sendSubview(toBack: pageControl) + scrollView?.bringSubview(toFront: syncCardView) + signInButton.superview?.bringSubview(toFront: signInButton) + + // Activate the first card + setActiveIntroView(introViews[0], forPage: 0) + setupDynamicFonts() + } + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + NotificationCenter.default.addObserver(self, selector: #selector(SELDynamicFontChanged(_:)), name: NotificationDynamicFontChanged, object: nil) + } + + override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + NotificationCenter.default.removeObserver(self, name: NotificationDynamicFontChanged, object: nil) + } + + func SELDynamicFontChanged(_ notification: Notification) { + guard notification.name == NotificationDynamicFontChanged else { return } + setupDynamicFonts() + } + + override func viewDidLayoutSubviews() { + super.viewDidLayoutSubviews() + + scrollView.snp.remakeConstraints { (make) -> Void in + make.left.right.top.equalTo(self.view) + make.bottom.equalTo(self.startBrowsingButton.snp.top) + } + + for i in 0.. 1.0 ? 1.0 : percentageOfScroll + let whiteComponent = UIColor.white.components + let grayComponent = UIColor(rgb: 0xF2F2F2).components + + let page = Int(scrollView.contentOffset.x / scrollView.frame.size.width) + pageControl.currentPage = page + + let newRed = (1.0 - percentageOfScroll) * whiteComponent.red + percentageOfScroll * grayComponent.red + let newGreen = (1.0 - percentageOfScroll) * whiteComponent.green + percentageOfScroll * grayComponent.green + let newBlue = (1.0 - percentageOfScroll) * whiteComponent.blue + percentageOfScroll * grayComponent.blue + let newColor = UIColor(red: newRed, green: newGreen, blue: newBlue, alpha: 1.0) + slideContainer.backgroundColor = newColor + } + + fileprivate func setActiveIntroView(_ newIntroView: UIView, forPage page: Int) { + if introView != newIntroView { + UIView.animate(withDuration: IntroViewControllerUX.FadeDuration, animations: { () -> Void in + self.introView?.alpha = 0 + self.introView = newIntroView + newIntroView.alpha = 1.0 + if page == 0 { + self.startBrowsingButton.alpha = 0 + } else { + self.startBrowsingButton.alpha = 1 + } + }, completion: { _ in + if page == (IntroViewControllerUX.NumberOfCards - 1) { + self.scrollView.signinButton = self.signInButton + } else { + self.scrollView.signinButton = nil + } + }) + } + } + + fileprivate var scaledWidthOfSlide: CGFloat { + return view.frame.width + } + + fileprivate var scaledHeightOfSlide: CGFloat { + return (view.frame.width / slides[0].size.width) * slides[0].size.height / slideVerticalScaleFactor + } + + fileprivate func attributedStringForLabel(_ text: String) -> NSMutableAttributedString { + let paragraphStyle = NSMutableParagraphStyle() + paragraphStyle.lineSpacing = IntroViewControllerUX.CardTextLineHeight + paragraphStyle.alignment = .center + + let string = NSMutableAttributedString(string: text) + string.addAttribute(NSParagraphStyleAttributeName, value: paragraphStyle, range: NSRange(location: 0, length: string.length)) + return string + } + + fileprivate func setupDynamicFonts() { + startBrowsingButton.titleLabel?.font = UIFont(name: "FiraSans-Regular", size: DynamicFontHelper.defaultHelper.IntroStandardFontSize) + signInButton.titleLabel?.font = UIFont(name: "FiraSans-Regular", size: DynamicFontHelper.defaultHelper.IntroBigFontSize) + + for titleLabel in titleLabels { + titleLabel.font = UIFont(name: "FiraSans-Medium", size: DynamicFontHelper.defaultHelper.IntroBigFontSize) + } + + for label in textLabels { + label.font = UIFont(name: "FiraSans-UltraLight", size: DynamicFontHelper.defaultHelper.IntroStandardFontSize) + } + } +} + +fileprivate class IntroOverlayScrollView: UIScrollView { + weak var signinButton: UIButton? + + fileprivate override func point(inside point: CGPoint, with event: UIEvent?) -> Bool { + if let signinFrame = signinButton?.frame { + let convertedFrame = convert(signinFrame, from: signinButton?.superview) + if convertedFrame.contains(point) { + return false + } + } + + return CGRect(origin: self.frame.origin, size: CGSize(width: self.contentSize.width, height: self.frame.size.height)).contains(point) + } +} + +extension UIColor { + var components:(red: CGFloat, green: CGFloat, blue: CGFloat, alpha: CGFloat) { + var r: CGFloat = 0 + var g: CGFloat = 0 + var b: CGFloat = 0 + var a: CGFloat = 0 + getRed(&r, green: &g, blue: &b, alpha: &a) + return (r, g, b, a) + } +} diff --git a/mobile/ios/Client/Frontend/Login Management/LoginListViewController.swift b/mobile/ios/Client/Frontend/Login Management/LoginListViewController.swift new file mode 100644 index 0000000000..a68fc8211a --- /dev/null +++ b/mobile/ios/Client/Frontend/Login Management/LoginListViewController.swift @@ -0,0 +1,639 @@ +/* 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 UIKit +import SnapKit +import Storage +import Shared +import SwiftKeychainWrapper +import Deferred + +private struct LoginListUX { + static let RowHeight: CGFloat = 58 + static let SearchHeight: CGFloat = 58 + static let selectionButtonFont = UIFont.systemFont(ofSize: 16) + static let selectionButtonTextColor = UIColor.white + static let selectionButtonBackground = UIConstants.HighlightBlue + static let NoResultsFont: UIFont = UIFont.systemFont(ofSize: 16) + static let NoResultsTextColor: UIColor = UIColor.lightGray +} + +private extension UITableView { + var allIndexPaths: [IndexPath] { + return (0..>? + + fileprivate let loadingStateView = LoadingLoginsView() + + fileprivate var deleteAlert: UIAlertController? + + // Titles for selection/deselect/delete buttons + fileprivate let deselectAllTitle = NSLocalizedString("Deselect All", tableName: "LoginManager", comment: "Label for the button used to deselect all logins.") + fileprivate let selectAllTitle = NSLocalizedString("Select All", tableName: "LoginManager", comment: "Label for the button used to select all logins.") + fileprivate let deleteLoginTitle = NSLocalizedString("Delete", tableName: "LoginManager", comment: "Label for the button used to delete the current login.") + + fileprivate lazy var selectionButton: UIButton = { + let button = UIButton() + button.titleLabel?.font = LoginListUX.selectionButtonFont + button.setTitle(self.selectAllTitle, for: UIControlState()) + button.setTitleColor(LoginListUX.selectionButtonTextColor, for: UIControlState()) + button.backgroundColor = LoginListUX.selectionButtonBackground + button.addTarget(self, action: #selector(LoginListViewController.tappedSelectionButton), for: .touchUpInside) + return button + }() + + fileprivate var selectionButtonHeightConstraint: Constraint? + fileprivate var selectedIndexPaths = [IndexPath]() + + fileprivate let tableView = UITableView() + + weak var settingsDelegate: SettingsDelegate? + + init(profile: Profile) { + self.profile = profile + super.init(nibName: nil, bundle: nil) + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + + let notificationCenter = NotificationCenter.default + notificationCenter.addObserver(self, selector: #selector(LoginListViewController.remoteLoginsDidChange), name: NotificationDataRemoteLoginChangesWereApplied, object: nil) + notificationCenter.addObserver(self, selector: #selector(LoginListViewController.dismissAlertController), name: NSNotification.Name.UIApplicationDidEnterBackground, object: nil) + + automaticallyAdjustsScrollViewInsets = false + self.view.backgroundColor = UIColor.white + navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .edit, target: self, action: #selector(LoginListViewController.beginEditing)) + + self.title = NSLocalizedString("Logins", tableName: "LoginManager", comment: "Title for Logins List View screen.") + + searchView.delegate = self + tableView.register(LoginTableViewCell.self, forCellReuseIdentifier: LoginCellIdentifier) + + view.addSubview(searchView) + view.addSubview(tableView) + view.addSubview(loadingStateView) + view.addSubview(selectionButton) + + loadingStateView.isHidden = true + + searchView.snp.makeConstraints { make in + make.top.equalTo(self.topLayoutGuide.snp.bottom) + make.left.right.equalTo(self.view) + make.height.equalTo(LoginListUX.SearchHeight) + } + + tableView.snp.makeConstraints { make in + make.top.equalTo(searchView.snp.bottom) + make.left.right.equalTo(self.view) + make.bottom.equalTo(self.selectionButton.snp.top) + } + + selectionButton.snp.makeConstraints { make in + make.left.right.bottom.equalTo(self.view) + make.top.equalTo(self.tableView.snp.bottom) + make.bottom.equalTo(self.view) + selectionButtonHeightConstraint = make.height.equalTo(0).constraint + } + + loadingStateView.snp.makeConstraints { make in + make.edges.equalTo(tableView) + } + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + tableView.accessibilityIdentifier = "Login List" + tableView.dataSource = loginDataSource + tableView.allowsMultipleSelectionDuringEditing = true + tableView.delegate = self + tableView.tableFooterView = UIView() + + KeyboardHelper.defaultHelper.addDelegate(self) + + searchView.isEditing ? loadLogins(searchView.inputField.text) : loadLogins() + } + + override func viewDidLayoutSubviews() { + super.viewDidLayoutSubviews() + self.loginDataSource.emptyStateView.searchBarHeight = searchView.frame.height + self.loadingStateView.searchBarHeight = searchView.frame.height + } + + deinit { + let notificationCenter = NotificationCenter.default + notificationCenter.removeObserver(self, name: NotificationProfileDidFinishSyncing, object: nil) + notificationCenter.removeObserver(self, name: NotificationDataLoginDidChange, object: nil) + notificationCenter.removeObserver(self, name: NotificationDataRemoteLoginChangesWereApplied, object: nil) + notificationCenter.removeObserver(self, name: NSNotification.Name.UIApplicationDidEnterBackground, object: nil) + } + + fileprivate func toggleDeleteBarButton() { + // Show delete bar button item if we have selected any items + if loginSelectionController.selectedCount > 0 { + if navigationItem.rightBarButtonItem == nil { + navigationItem.rightBarButtonItem = UIBarButtonItem(title: deleteLoginTitle, style: .plain, target: self, action: #selector(LoginListViewController.tappedDelete)) + navigationItem.rightBarButtonItem?.tintColor = UIColor.red + } + } else { + navigationItem.rightBarButtonItem = nil + } + } + + fileprivate func toggleSelectionTitle() { + if loginSelectionController.selectedCount == loginDataSource.count { + selectionButton.setTitle(deselectAllTitle, for: UIControlState()) + } else { + selectionButton.setTitle(selectAllTitle, for: UIControlState()) + } + } + + // Wrap the SQLiteLogins method to allow us to cancel it from our end. + fileprivate func queryLogins(_ query: String) -> Deferred> { + let deferred = Deferred>() + profile.logins.searchLoginsWithQuery(query) >>== { logins in + deferred.fillIfUnfilled(Maybe(success: logins.asArray())) + succeed() + } + return deferred + } +} + +// MARK: - Selectors +private extension LoginListViewController { + @objc func remoteLoginsDidChange() { + DispatchQueue.main.async { + self.loadLogins() + } + } + + @objc func dismissAlertController() { + self.deleteAlert?.dismiss(animated: false, completion: nil) + } + + func loadLogins(_ query: String? = nil) { + loadingStateView.isHidden = false + + // Fill in an in-flight query and re-query + activeLoginQuery?.fillIfUnfilled(Maybe(success: [])) + activeLoginQuery = queryLogins(query ?? "") + activeLoginQuery! >>== self.loginDataSource.setLogins + } + + @objc func beginEditing() { + navigationItem.rightBarButtonItem = nil + navigationItem.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem: .cancel, target: self, action: #selector(LoginListViewController.cancelSelection)) + selectionButtonHeightConstraint?.update(offset: UIConstants.ToolbarHeight) + self.view.layoutIfNeeded() + tableView.setEditing(true, animated: true) + } + + @objc func cancelSelection() { + // Update selection and select all button + loginSelectionController.deselectAll() + toggleSelectionTitle() + selectionButtonHeightConstraint?.update(offset: 0) + self.view.layoutIfNeeded() + + tableView.setEditing(false, animated: true) + navigationItem.leftBarButtonItem = nil + navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .edit, target: self, action: #selector(LoginListViewController.beginEditing)) + } + + @objc func tappedDelete() { + profile.logins.hasSyncedLogins().uponQueue(DispatchQueue.main) { yes in + self.deleteAlert = UIAlertController.deleteLoginAlertWithDeleteCallback({ [unowned self] _ in + // Delete here + let guidsToDelete = self.loginSelectionController.selectedIndexPaths.map { indexPath in + self.loginDataSource.loginAtIndexPath(indexPath)!.guid + } + + self.profile.logins.removeLoginsWithGUIDs(guidsToDelete).uponQueue(DispatchQueue.main) { _ in + self.cancelSelection() + self.loadLogins() + } + }, hasSyncedLogins: yes.successValue ?? true) + + self.present(self.deleteAlert!, animated: true, completion: nil) + } + } + + @objc func tappedSelectionButton() { + // If we haven't selected everything yet, select all + if loginSelectionController.selectedCount < loginDataSource.count { + // Find all unselected indexPaths + let unselectedPaths = tableView.allIndexPaths.filter { indexPath in + return !loginSelectionController.indexPathIsSelected(indexPath) + } + loginSelectionController.selectIndexPaths(unselectedPaths) + unselectedPaths.forEach { indexPath in + self.tableView.selectRow(at: indexPath, animated: true, scrollPosition: .none) + } + } + + // If everything has been selected, deselect all + else { + loginSelectionController.deselectAll() + tableView.allIndexPaths.forEach { indexPath in + self.tableView.deselectRow(at: indexPath, animated: true) + } + } + + toggleSelectionTitle() + toggleDeleteBarButton() + } +} + +// MARK: - LoginDataSourceObserver +extension LoginListViewController: LoginDataSourceObserver { + func loginSectionsDidUpdate() { + loadingStateView.isHidden = true + tableView.reloadData() + activeLoginQuery = nil + navigationItem.rightBarButtonItem?.isEnabled = loginDataSource.count > 0 + restoreSelectedRows() + } + + func restoreSelectedRows() { + for path in self.loginSelectionController.selectedIndexPaths { + tableView.selectRow(at: path, animated: false, scrollPosition: .none) + } + } +} + +// MARK: - UITableViewDelegate +extension LoginListViewController: UITableViewDelegate { + + func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { + // Force the headers to be hidden + return 0 + } + + func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { + return LoginListUX.RowHeight + } + + func tableView(_ tableView: UITableView, editingStyleForRowAt indexPath: IndexPath) -> UITableViewCellEditingStyle { + return .none + } + + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + if tableView.isEditing { + loginSelectionController.selectIndexPath(indexPath) + toggleSelectionTitle() + toggleDeleteBarButton() + } else { + tableView.deselectRow(at: indexPath, animated: true) + let login = loginDataSource.loginAtIndexPath(indexPath)! + let detailViewController = LoginDetailViewController(profile: profile, login: login) + detailViewController.settingsDelegate = settingsDelegate + navigationController?.pushViewController(detailViewController, animated: true) + } + } + + func tableView(_ tableView: UITableView, didDeselectRowAt indexPath: IndexPath) { + if tableView.isEditing { + loginSelectionController.deselectIndexPath(indexPath) + toggleSelectionTitle() + toggleDeleteBarButton() + } + } +} + +// MARK: - KeyboardHelperDelegate +extension LoginListViewController: KeyboardHelperDelegate { + + func keyboardHelper(_ keyboardHelper: KeyboardHelper, keyboardWillShowWithState state: KeyboardState) { + let coveredHeight = state.intersectionHeightForView(tableView) + tableView.contentInset.bottom = coveredHeight + } + + func keyboardHelper(_ keyboardHelper: KeyboardHelper, keyboardDidShowWithState state: KeyboardState) { + } + + func keyboardHelper(_ keyboardHelper: KeyboardHelper, keyboardWillHideWithState state: KeyboardState) { + tableView.contentInset.bottom = 0 + } +} + +// MARK: - SearchInputViewDelegate +extension LoginListViewController: SearchInputViewDelegate { + + @objc func searchInputView(_ searchView: SearchInputView, didChangeTextTo text: String) { + loadLogins(text) + } + + @objc func searchInputViewBeganEditing(_ searchView: SearchInputView) { + // Trigger a cancel for editing + cancelSelection() + + // Hide the edit button while we're searching + navigationItem.rightBarButtonItem = nil + loadLogins() + } + + @objc func searchInputViewFinishedEditing(_ searchView: SearchInputView) { + // Show the edit after we're done with the search + navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .edit, target: self, action: #selector(LoginListViewController.beginEditing)) + loadLogins() + } +} + +/// Controller that keeps track of selected indexes +fileprivate class ListSelectionController: NSObject { + + fileprivate unowned let tableView: UITableView + + fileprivate(set) var selectedIndexPaths = [IndexPath]() + + var selectedCount: Int { + return selectedIndexPaths.count + } + + init(tableView: UITableView) { + self.tableView = tableView + super.init() + } + + func selectIndexPath(_ indexPath: IndexPath) { + selectedIndexPaths.append(indexPath) + } + + func indexPathIsSelected(_ indexPath: IndexPath) -> Bool { + return selectedIndexPaths.contains(indexPath) { path1, path2 in + return path1.row == path2.row && path1.section == path2.section + } + } + + func deselectIndexPath(_ indexPath: IndexPath) { + guard let foundSelectedPath = (selectedIndexPaths.filter { $0.row == indexPath.row && $0.section == indexPath.section }).first, + let indexToRemove = selectedIndexPaths.index(of: foundSelectedPath) else { + return + } + + selectedIndexPaths.remove(at: indexToRemove) + } + + func deselectAll() { + selectedIndexPaths.removeAll() + } + + func selectIndexPaths(_ indexPaths: [IndexPath]) { + selectedIndexPaths += indexPaths + } +} + +protocol LoginDataSourceObserver: class { + func loginSectionsDidUpdate() +} + +/// Data source for handling LoginData objects from a Cursor +class LoginDataSource: NSObject, UITableViewDataSource { + + var count: Int = 0 + + weak var dataObserver: LoginDataSourceObserver? + + fileprivate let emptyStateView = NoLoginsView() + + fileprivate var sections = [Character: [Login]]() { + didSet { + assert(Thread.isMainThread, "Must be assigned to from the main thread or else data will be out of sync with reloadData.") + self.dataObserver?.loginSectionsDidUpdate() + } + } + + fileprivate var titles = [Character]() + + fileprivate func loginsForSection(_ section: Int) -> [Login]? { + let titleForSectionIndex = titles[section] + return sections[titleForSectionIndex] + } + + func loginAtIndexPath(_ indexPath: IndexPath) -> Login? { + let titleForSectionIndex = titles[indexPath.section] + return sections[titleForSectionIndex]?[indexPath.row] + } + + @objc func numberOfSections(in tableView: UITableView) -> Int { + let numOfSections = sections.count + if numOfSections == 0 { + tableView.backgroundView = emptyStateView + tableView.separatorStyle = .none + } else { + tableView.backgroundView = nil + tableView.separatorStyle = .singleLine + } + return numOfSections + } + + @objc func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return loginsForSection(section)?.count ?? 0 + } + + @objc func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let cell = tableView.dequeueReusableCell(withIdentifier: LoginCellIdentifier, for: indexPath) as! LoginTableViewCell + let login = loginAtIndexPath(indexPath)! + cell.style = .noIconAndBothLabels + cell.updateCellWithLogin(login) + return cell + } + + @objc func sectionIndexTitles(for tableView: UITableView) -> [String]? { + return titles.map { String($0) } + } + + @objc func tableView(_ tableView: UITableView, sectionForSectionIndexTitle title: String, at index: Int) -> Int { + return titles.index(of: Character(title)) ?? 0 + } + + @objc func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { + return String(titles[section]) + } + + func setLogins(_ logins: [Login]) { + // NB: Make sure we call the callback on the main thread so it can be synced up with a reloadData to + // prevent race conditions between data/UI indexing. + return computeSectionsFromLogins(logins).uponQueue(DispatchQueue.main) { result in + guard let (titles, sections) = result.successValue else { + self.count = 0 + self.titles = [] + self.sections = [:] + return + } + + self.count = logins.count + self.titles = titles + self.sections = sections + } + } + + fileprivate func computeSectionsFromLogins(_ logins: [Login]) -> Deferred> { + guard logins.count > 0 else { + return deferMaybe( ([Character](), [Character: [Login]]()) ) + } + + var domainLookup = [GUID: (baseDomain: String?, host: String?, hostname: String)]() + var sections = [Character: [Login]]() + var titleSet = Set() + + // Small helper method for using the precomputed base domain to determine the title/section of the + // given login. + func titleForLogin(_ login: Login) -> Character { + // Fallback to hostname if we can't extract a base domain. + let titleString = domainLookup[login.guid]?.baseDomain?.uppercased() ?? login.hostname + return titleString.characters.first ?? Character("") + } + + // Rules for sorting login URLS: + // 1. Compare base domains + // 2. If bases are equal, compare hosts + // 3. If login URL was invalid, revert to full hostname + func sortByDomain(_ loginA: Login, loginB: Login) -> Bool { + guard let domainsA = domainLookup[loginA.guid], + let domainsB = domainLookup[loginB.guid] else { + return false + } + + guard let baseDomainA = domainsA.baseDomain, + let baseDomainB = domainsB.baseDomain, + let hostA = domainsA.host, + let hostB = domainsB.host else { + return domainsA.hostname < domainsB.hostname + } + + if baseDomainA == baseDomainB { + return hostA < hostB + } else { + return baseDomainA < baseDomainB + } + } + + return deferDispatchAsync(DispatchQueue.global(qos: DispatchQoS.userInteractive.qosClass)) { + // Precompute the baseDomain, host, and hostname values for sorting later on. At the moment + // baseDomain() is a costly call because of the ETLD lookup tables. + logins.forEach { login in + domainLookup[login.guid] = ( + login.hostname.asURL?.baseDomain, + login.hostname.asURL?.host, + login.hostname + ) + } + + // 1. Temporarily insert titles into a Set to get duplicate removal for 'free'. + logins.forEach { titleSet.insert(titleForLogin($0)) } + + // 2. Setup an empty list for each title found. + titleSet.forEach { sections[$0] = [Login]() } + + // 3. Go through our logins and put them in the right section. + logins.forEach { sections[titleForLogin($0)]?.append($0) } + + // 4. Go through each section and sort. + sections.forEach { sections[$0] = $1.sorted(by: sortByDomain) } + + return deferMaybe( (Array(titleSet).sorted(), sections) ) + } + } +} + +/// Empty state view when there is no logins to display. +fileprivate class NoLoginsView: UIView { + + // We use the search bar height to maintain visual balance with the whitespace on this screen. The + // title label is centered visually using the empty view + search bar height as the size to center with. + var searchBarHeight: CGFloat = 0 { + didSet { + setNeedsUpdateConstraints() + } + } + + lazy var titleLabel: UILabel = { + let label = UILabel() + label.font = LoginListUX.NoResultsFont + label.textColor = LoginListUX.NoResultsTextColor + label.text = NSLocalizedString("No logins found", tableName: "LoginManager", comment: "Label displayed when no logins are found after searching.") + return label + }() + + override init(frame: CGRect) { + super.init(frame: frame) + addSubview(titleLabel) + } + + fileprivate override func updateConstraints() { + super.updateConstraints() + titleLabel.snp.remakeConstraints { make in + make.centerX.equalTo(self) + make.centerY.equalTo(self).offset(-(searchBarHeight / 2)) + } + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +/// View to display to the user while we are loading the logins +fileprivate class LoadingLoginsView: UIView { + + var searchBarHeight: CGFloat = 0 { + didSet { + setNeedsUpdateConstraints() + } + } + + lazy var indicator: UIActivityIndicatorView = { + let indicator = UIActivityIndicatorView(activityIndicatorStyle: .gray) + indicator.hidesWhenStopped = false + return indicator + }() + + override init(frame: CGRect) { + super.init(frame: frame) + addSubview(indicator) + backgroundColor = UIColor.white + indicator.startAnimating() + } + + fileprivate override func updateConstraints() { + super.updateConstraints() + indicator.snp.remakeConstraints { make in + make.centerX.equalTo(self) + make.centerY.equalTo(self).offset(-(searchBarHeight / 2)) + } + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} diff --git a/mobile/ios/Client/Frontend/Login/LoginViewController.swift b/mobile/ios/Client/Frontend/Login/LoginViewController.swift new file mode 100644 index 0000000000..e67949e690 --- /dev/null +++ b/mobile/ios/Client/Frontend/Login/LoginViewController.swift @@ -0,0 +1,35 @@ +/* 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 UIKit +import Snap + +class LoginViewController: UIViewController { + var accountManager: AccountProfileManager! + + override func loadView() { + view = LoginView() + } + + override func viewDidLoad() { + super.viewDidLoad() + + let loginView = view as LoginView + + loginView.didClickLogin = { [unowned self] in + self.accountManager.login(loginView.username, password: loginView.password, { err in + switch err { + case .badAuth: + println("Invalid username and/or password") + default: + println("Connection error") + } + }) + } + } + + override func prefersStatusBarHidden() -> Bool { + return true + } +} diff --git a/mobile/ios/Client/Frontend/Menu/Menu.xcassets/Contents.json b/mobile/ios/Client/Frontend/Menu/Menu.xcassets/Contents.json new file mode 100644 index 0000000000..da4a164c91 --- /dev/null +++ b/mobile/ios/Client/Frontend/Menu/Menu.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-Bookmark-Remove.imageset/Contents.json b/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-Bookmark-Remove.imageset/Contents.json new file mode 100644 index 0000000000..fc91820b86 --- /dev/null +++ b/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-Bookmark-Remove.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "bookmark-remove.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "bookmark-remove@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "bookmark-remove@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-Bookmark-Remove.imageset/bookmark-remove.png b/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-Bookmark-Remove.imageset/bookmark-remove.png new file mode 100644 index 0000000000..8d66b36d99 Binary files /dev/null and b/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-Bookmark-Remove.imageset/bookmark-remove.png differ diff --git a/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-Bookmark-Remove.imageset/bookmark-remove@2x.png b/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-Bookmark-Remove.imageset/bookmark-remove@2x.png new file mode 100644 index 0000000000..bdb615f03e Binary files /dev/null and b/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-Bookmark-Remove.imageset/bookmark-remove@2x.png differ diff --git a/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-Bookmark-Remove.imageset/bookmark-remove@3x.png b/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-Bookmark-Remove.imageset/bookmark-remove@3x.png new file mode 100644 index 0000000000..388cddb0b7 Binary files /dev/null and b/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-Bookmark-Remove.imageset/bookmark-remove@3x.png differ diff --git a/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-Bookmark.imageset/Contents.json b/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-Bookmark.imageset/Contents.json new file mode 100644 index 0000000000..e7fc60dd35 --- /dev/null +++ b/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-Bookmark.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "menu-Bookmark.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "menu-Bookmark@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "menu-Bookmark@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-Bookmark.imageset/menu-Bookmark.png b/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-Bookmark.imageset/menu-Bookmark.png new file mode 100644 index 0000000000..214afcb29e Binary files /dev/null and b/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-Bookmark.imageset/menu-Bookmark.png differ diff --git a/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-Bookmark.imageset/menu-Bookmark@2x.png b/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-Bookmark.imageset/menu-Bookmark@2x.png new file mode 100644 index 0000000000..028721ac4b Binary files /dev/null and b/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-Bookmark.imageset/menu-Bookmark@2x.png differ diff --git a/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-Bookmark.imageset/menu-Bookmark@3x.png b/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-Bookmark.imageset/menu-Bookmark@3x.png new file mode 100644 index 0000000000..d4d18ab6c6 Binary files /dev/null and b/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-Bookmark.imageset/menu-Bookmark@3x.png differ diff --git a/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-CloseTabs.imageset/Contents.json b/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-CloseTabs.imageset/Contents.json new file mode 100644 index 0000000000..b79aa0cff0 --- /dev/null +++ b/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-CloseTabs.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "menu-CloseTabs.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "menu-CloseTabs@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "menu-CloseTabs@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-CloseTabs.imageset/menu-CloseTabs.png b/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-CloseTabs.imageset/menu-CloseTabs.png new file mode 100644 index 0000000000..fa1588c857 Binary files /dev/null and b/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-CloseTabs.imageset/menu-CloseTabs.png differ diff --git a/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-CloseTabs.imageset/menu-CloseTabs@2x.png b/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-CloseTabs.imageset/menu-CloseTabs@2x.png new file mode 100644 index 0000000000..89941db856 Binary files /dev/null and b/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-CloseTabs.imageset/menu-CloseTabs@2x.png differ diff --git a/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-CloseTabs.imageset/menu-CloseTabs@3x.png b/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-CloseTabs.imageset/menu-CloseTabs@3x.png new file mode 100644 index 0000000000..6423d6c5ad Binary files /dev/null and b/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-CloseTabs.imageset/menu-CloseTabs@3x.png differ diff --git a/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-Copy-Link.imageset/Contents.json b/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-Copy-Link.imageset/Contents.json new file mode 100644 index 0000000000..6deda9a625 --- /dev/null +++ b/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-Copy-Link.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "link.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "link@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "link@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-Copy-Link.imageset/link.png b/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-Copy-Link.imageset/link.png new file mode 100644 index 0000000000..87263b50da Binary files /dev/null and b/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-Copy-Link.imageset/link.png differ diff --git a/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-Copy-Link.imageset/link@2x.png b/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-Copy-Link.imageset/link@2x.png new file mode 100644 index 0000000000..b12aa6a1ba Binary files /dev/null and b/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-Copy-Link.imageset/link@2x.png differ diff --git a/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-Copy-Link.imageset/link@3x.png b/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-Copy-Link.imageset/link@3x.png new file mode 100644 index 0000000000..36a280869b Binary files /dev/null and b/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-Copy-Link.imageset/link@3x.png differ diff --git a/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-FindInPage.imageset/Contents.json b/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-FindInPage.imageset/Contents.json new file mode 100644 index 0000000000..216ea4a88f --- /dev/null +++ b/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-FindInPage.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "search.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "search@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "search@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-FindInPage.imageset/search.png b/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-FindInPage.imageset/search.png new file mode 100644 index 0000000000..17732b06f6 Binary files /dev/null and b/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-FindInPage.imageset/search.png differ diff --git a/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-FindInPage.imageset/search@2x.png b/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-FindInPage.imageset/search@2x.png new file mode 100644 index 0000000000..47e4d1c0fa Binary files /dev/null and b/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-FindInPage.imageset/search@2x.png differ diff --git a/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-FindInPage.imageset/search@3x.png b/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-FindInPage.imageset/search@3x.png new file mode 100644 index 0000000000..bcc8e6e96e Binary files /dev/null and b/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-FindInPage.imageset/search@3x.png differ diff --git a/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-Home.imageset/Contents.json b/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-Home.imageset/Contents.json new file mode 100644 index 0000000000..47bcb3b1a1 --- /dev/null +++ b/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-Home.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "bottomNav-Home.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "bottomNav-Home@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "bottomNav-Home@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-Home.imageset/bottomNav-Home.png b/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-Home.imageset/bottomNav-Home.png new file mode 100644 index 0000000000..db2543ba83 Binary files /dev/null and b/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-Home.imageset/bottomNav-Home.png differ diff --git a/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-Home.imageset/bottomNav-Home@2x.png b/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-Home.imageset/bottomNav-Home@2x.png new file mode 100644 index 0000000000..86076fe209 Binary files /dev/null and b/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-Home.imageset/bottomNav-Home@2x.png differ diff --git a/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-Home.imageset/bottomNav-Home@3x.png b/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-Home.imageset/bottomNav-Home@3x.png new file mode 100644 index 0000000000..bd27a9895b Binary files /dev/null and b/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-Home.imageset/bottomNav-Home@3x.png differ diff --git a/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-More-Options.imageset/Contents.json b/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-More-Options.imageset/Contents.json new file mode 100644 index 0000000000..cc7787d280 --- /dev/null +++ b/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-More-Options.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "action.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "action@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "action@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-More-Options.imageset/action.png b/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-More-Options.imageset/action.png new file mode 100644 index 0000000000..600de87ec3 Binary files /dev/null and b/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-More-Options.imageset/action.png differ diff --git a/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-More-Options.imageset/action@2x.png b/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-More-Options.imageset/action@2x.png new file mode 100644 index 0000000000..7e71c9c059 Binary files /dev/null and b/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-More-Options.imageset/action@2x.png differ diff --git a/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-More-Options.imageset/action@3x.png b/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-More-Options.imageset/action@3x.png new file mode 100644 index 0000000000..e1ad99e304 Binary files /dev/null and b/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-More-Options.imageset/action@3x.png differ diff --git a/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-NewPrivateTab.imageset/Contents.json b/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-NewPrivateTab.imageset/Contents.json new file mode 100644 index 0000000000..da3489bf6a --- /dev/null +++ b/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-NewPrivateTab.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "new-private-tab.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "new-private-tab@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "new-private-tab@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-NewPrivateTab.imageset/new-private-tab.png b/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-NewPrivateTab.imageset/new-private-tab.png new file mode 100644 index 0000000000..642ead018f Binary files /dev/null and b/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-NewPrivateTab.imageset/new-private-tab.png differ diff --git a/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-NewPrivateTab.imageset/new-private-tab@2x.png b/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-NewPrivateTab.imageset/new-private-tab@2x.png new file mode 100644 index 0000000000..138e7c232b Binary files /dev/null and b/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-NewPrivateTab.imageset/new-private-tab@2x.png differ diff --git a/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-NewPrivateTab.imageset/new-private-tab@3x.png b/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-NewPrivateTab.imageset/new-private-tab@3x.png new file mode 100644 index 0000000000..64f917fb17 Binary files /dev/null and b/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-NewPrivateTab.imageset/new-private-tab@3x.png differ diff --git a/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-NewTab.imageset/Contents.json b/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-NewTab.imageset/Contents.json new file mode 100644 index 0000000000..99dafff517 --- /dev/null +++ b/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-NewTab.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "menu-NewTab.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "menu-NewTab@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "menu-NewTab@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-NewTab.imageset/menu-NewTab.png b/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-NewTab.imageset/menu-NewTab.png new file mode 100644 index 0000000000..523b0b0f60 Binary files /dev/null and b/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-NewTab.imageset/menu-NewTab.png differ diff --git a/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-NewTab.imageset/menu-NewTab@2x.png b/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-NewTab.imageset/menu-NewTab@2x.png new file mode 100644 index 0000000000..3a0458caa4 Binary files /dev/null and b/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-NewTab.imageset/menu-NewTab@2x.png differ diff --git a/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-NewTab.imageset/menu-NewTab@3x.png b/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-NewTab.imageset/menu-NewTab@3x.png new file mode 100644 index 0000000000..dc27e70819 Binary files /dev/null and b/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-NewTab.imageset/menu-NewTab@3x.png differ diff --git a/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-NightMode.imageset/Contents.json b/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-NightMode.imageset/Contents.json new file mode 100644 index 0000000000..68d6cc2c43 --- /dev/null +++ b/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-NightMode.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "menu-NightMode.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "menu-NightMode@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "menu-NightMode@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-NightMode.imageset/menu-NightMode.png b/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-NightMode.imageset/menu-NightMode.png new file mode 100644 index 0000000000..9e57f24a9f Binary files /dev/null and b/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-NightMode.imageset/menu-NightMode.png differ diff --git a/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-NightMode.imageset/menu-NightMode@2x.png b/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-NightMode.imageset/menu-NightMode@2x.png new file mode 100644 index 0000000000..10266d165c Binary files /dev/null and b/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-NightMode.imageset/menu-NightMode@2x.png differ diff --git a/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-NightMode.imageset/menu-NightMode@3x.png b/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-NightMode.imageset/menu-NightMode@3x.png new file mode 100644 index 0000000000..dbf016df46 Binary files /dev/null and b/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-NightMode.imageset/menu-NightMode@3x.png differ diff --git a/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-NoImageMode.imageset/Contents.json b/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-NoImageMode.imageset/Contents.json new file mode 100644 index 0000000000..2976d3f86b --- /dev/null +++ b/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-NoImageMode.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "menu-NoImageMode.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "menu-NoImageMode@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "menu-NoImageMode@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-NoImageMode.imageset/menu-NoImageMode.png b/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-NoImageMode.imageset/menu-NoImageMode.png new file mode 100644 index 0000000000..9dd70569c0 Binary files /dev/null and b/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-NoImageMode.imageset/menu-NoImageMode.png differ diff --git a/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-NoImageMode.imageset/menu-NoImageMode@2x.png b/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-NoImageMode.imageset/menu-NoImageMode@2x.png new file mode 100644 index 0000000000..030e03915d Binary files /dev/null and b/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-NoImageMode.imageset/menu-NoImageMode@2x.png differ diff --git a/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-NoImageMode.imageset/menu-NoImageMode@3x.png b/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-NoImageMode.imageset/menu-NoImageMode@3x.png new file mode 100644 index 0000000000..3189391554 Binary files /dev/null and b/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-NoImageMode.imageset/menu-NoImageMode@3x.png differ diff --git a/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-RequestDesktopSite.imageset/Contents.json b/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-RequestDesktopSite.imageset/Contents.json new file mode 100644 index 0000000000..9485891d1e --- /dev/null +++ b/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-RequestDesktopSite.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "menu-RequestDesktopSite.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "menu-RequestDesktopSite@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "menu-RequestDesktopSite@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-RequestDesktopSite.imageset/menu-RequestDesktopSite.png b/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-RequestDesktopSite.imageset/menu-RequestDesktopSite.png new file mode 100644 index 0000000000..2759d5b19d Binary files /dev/null and b/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-RequestDesktopSite.imageset/menu-RequestDesktopSite.png differ diff --git a/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-RequestDesktopSite.imageset/menu-RequestDesktopSite@2x.png b/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-RequestDesktopSite.imageset/menu-RequestDesktopSite@2x.png new file mode 100644 index 0000000000..8798268a5c Binary files /dev/null and b/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-RequestDesktopSite.imageset/menu-RequestDesktopSite@2x.png differ diff --git a/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-RequestDesktopSite.imageset/menu-RequestDesktopSite@3x.png b/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-RequestDesktopSite.imageset/menu-RequestDesktopSite@3x.png new file mode 100644 index 0000000000..661c56625c Binary files /dev/null and b/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-RequestDesktopSite.imageset/menu-RequestDesktopSite@3x.png differ diff --git a/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-ScanQRCode.imageset/Contents.json b/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-ScanQRCode.imageset/Contents.json new file mode 100644 index 0000000000..5b775b8303 --- /dev/null +++ b/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-ScanQRCode.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "qrscan.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "qrscan@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "qrscan@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-ScanQRCode.imageset/qrscan.png b/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-ScanQRCode.imageset/qrscan.png new file mode 100644 index 0000000000..87c6dca468 Binary files /dev/null and b/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-ScanQRCode.imageset/qrscan.png differ diff --git a/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-ScanQRCode.imageset/qrscan@2x.png b/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-ScanQRCode.imageset/qrscan@2x.png new file mode 100644 index 0000000000..ca37a13800 Binary files /dev/null and b/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-ScanQRCode.imageset/qrscan@2x.png differ diff --git a/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-ScanQRCode.imageset/qrscan@3x.png b/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-ScanQRCode.imageset/qrscan@3x.png new file mode 100644 index 0000000000..742e243be0 Binary files /dev/null and b/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-ScanQRCode.imageset/qrscan@3x.png differ diff --git a/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-Send-to-Device.imageset/Contents.json b/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-Send-to-Device.imageset/Contents.json new file mode 100644 index 0000000000..b507a7807b --- /dev/null +++ b/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-Send-to-Device.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "sendtodevice.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "sendtodevice@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "sendtodevice@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-Send-to-Device.imageset/sendtodevice.png b/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-Send-to-Device.imageset/sendtodevice.png new file mode 100644 index 0000000000..5430ffbe38 Binary files /dev/null and b/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-Send-to-Device.imageset/sendtodevice.png differ diff --git a/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-Send-to-Device.imageset/sendtodevice@2x.png b/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-Send-to-Device.imageset/sendtodevice@2x.png new file mode 100644 index 0000000000..11bfea7fb5 Binary files /dev/null and b/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-Send-to-Device.imageset/sendtodevice@2x.png differ diff --git a/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-Send-to-Device.imageset/sendtodevice@3x.png b/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-Send-to-Device.imageset/sendtodevice@3x.png new file mode 100644 index 0000000000..665fb857aa Binary files /dev/null and b/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-Send-to-Device.imageset/sendtodevice@3x.png differ diff --git a/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-Send.imageset/Contents.json b/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-Send.imageset/Contents.json new file mode 100644 index 0000000000..432bea0a81 --- /dev/null +++ b/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-Send.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "sendMenu.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "sendMenu@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "sendMenu@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-Send.imageset/sendMenu.png b/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-Send.imageset/sendMenu.png new file mode 100644 index 0000000000..d2b170d967 Binary files /dev/null and b/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-Send.imageset/sendMenu.png differ diff --git a/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-Send.imageset/sendMenu@2x.png b/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-Send.imageset/sendMenu@2x.png new file mode 100644 index 0000000000..66878a3752 Binary files /dev/null and b/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-Send.imageset/sendMenu@2x.png differ diff --git a/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-Send.imageset/sendMenu@3x.png b/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-Send.imageset/sendMenu@3x.png new file mode 100644 index 0000000000..8a88d79989 Binary files /dev/null and b/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-Send.imageset/sendMenu@3x.png differ diff --git a/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-Settings.imageset/Contents.json b/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-Settings.imageset/Contents.json new file mode 100644 index 0000000000..2622c41053 --- /dev/null +++ b/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-Settings.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "menu-Settings.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "menu-Settings@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "menu-Settings@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-Settings.imageset/menu-Settings.png b/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-Settings.imageset/menu-Settings.png new file mode 100644 index 0000000000..a48cdc780c Binary files /dev/null and b/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-Settings.imageset/menu-Settings.png differ diff --git a/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-Settings.imageset/menu-Settings@2x.png b/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-Settings.imageset/menu-Settings@2x.png new file mode 100644 index 0000000000..89be79c697 Binary files /dev/null and b/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-Settings.imageset/menu-Settings@2x.png differ diff --git a/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-Settings.imageset/menu-Settings@3x.png b/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-Settings.imageset/menu-Settings@3x.png new file mode 100644 index 0000000000..071bd1e0a2 Binary files /dev/null and b/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-Settings.imageset/menu-Settings@3x.png differ diff --git a/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-Show-Tabs.imageset/Contents.json b/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-Show-Tabs.imageset/Contents.json new file mode 100644 index 0000000000..980051f93d --- /dev/null +++ b/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-Show-Tabs.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "showtabs.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "showtabs@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "showtabs@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-Show-Tabs.imageset/showtabs.png b/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-Show-Tabs.imageset/showtabs.png new file mode 100644 index 0000000000..2842180043 Binary files /dev/null and b/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-Show-Tabs.imageset/showtabs.png differ diff --git a/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-Show-Tabs.imageset/showtabs@2x.png b/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-Show-Tabs.imageset/showtabs@2x.png new file mode 100644 index 0000000000..cc4d4409ce Binary files /dev/null and b/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-Show-Tabs.imageset/showtabs@2x.png differ diff --git a/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-Show-Tabs.imageset/showtabs@3x.png b/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-Show-Tabs.imageset/showtabs@3x.png new file mode 100644 index 0000000000..78da39ad85 Binary files /dev/null and b/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-Show-Tabs.imageset/showtabs@3x.png differ diff --git a/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-TrackingProtection.imageset/Contents.json b/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-TrackingProtection.imageset/Contents.json new file mode 100644 index 0000000000..f8738ba83e --- /dev/null +++ b/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-TrackingProtection.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "trackingprotection.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "trackingprotection@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "trackingprotection@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-TrackingProtection.imageset/trackingprotection.png b/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-TrackingProtection.imageset/trackingprotection.png new file mode 100644 index 0000000000..96ce77c44f Binary files /dev/null and b/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-TrackingProtection.imageset/trackingprotection.png differ diff --git a/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-TrackingProtection.imageset/trackingprotection@2x.png b/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-TrackingProtection.imageset/trackingprotection@2x.png new file mode 100644 index 0000000000..77888df088 Binary files /dev/null and b/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-TrackingProtection.imageset/trackingprotection@2x.png differ diff --git a/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-TrackingProtection.imageset/trackingprotection@3x.png b/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-TrackingProtection.imageset/trackingprotection@3x.png new file mode 100644 index 0000000000..2b8521064f Binary files /dev/null and b/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-TrackingProtection.imageset/trackingprotection@3x.png differ diff --git a/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-ViewMobile.imageset/Contents.json b/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-ViewMobile.imageset/Contents.json new file mode 100644 index 0000000000..4da5077fa8 --- /dev/null +++ b/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-ViewMobile.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "menu-ViewMobile.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "menu-ViewMobile@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "menu-ViewMobile@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-ViewMobile.imageset/menu-ViewMobile.png b/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-ViewMobile.imageset/menu-ViewMobile.png new file mode 100644 index 0000000000..0892f0b459 Binary files /dev/null and b/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-ViewMobile.imageset/menu-ViewMobile.png differ diff --git a/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-ViewMobile.imageset/menu-ViewMobile@2x.png b/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-ViewMobile.imageset/menu-ViewMobile@2x.png new file mode 100644 index 0000000000..2098daf05a Binary files /dev/null and b/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-ViewMobile.imageset/menu-ViewMobile@2x.png differ diff --git a/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-ViewMobile.imageset/menu-ViewMobile@3x.png b/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-ViewMobile.imageset/menu-ViewMobile@3x.png new file mode 100644 index 0000000000..3bb68fd2f0 Binary files /dev/null and b/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-ViewMobile.imageset/menu-ViewMobile@3x.png differ diff --git a/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-panel-Bookmarks.imageset/Contents.json b/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-panel-Bookmarks.imageset/Contents.json new file mode 100644 index 0000000000..43fd6f1765 --- /dev/null +++ b/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-panel-Bookmarks.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "bookmark-filled.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "bookmark-filled@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "bookmark-filled@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-panel-Bookmarks.imageset/bookmark-filled.png b/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-panel-Bookmarks.imageset/bookmark-filled.png new file mode 100644 index 0000000000..4a3fa09981 Binary files /dev/null and b/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-panel-Bookmarks.imageset/bookmark-filled.png differ diff --git a/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-panel-Bookmarks.imageset/bookmark-filled@2x.png b/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-panel-Bookmarks.imageset/bookmark-filled@2x.png new file mode 100644 index 0000000000..c7fda8e906 Binary files /dev/null and b/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-panel-Bookmarks.imageset/bookmark-filled@2x.png differ diff --git a/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-panel-Bookmarks.imageset/bookmark-filled@3x.png b/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-panel-Bookmarks.imageset/bookmark-filled@3x.png new file mode 100644 index 0000000000..f05b80bcc1 Binary files /dev/null and b/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-panel-Bookmarks.imageset/bookmark-filled@3x.png differ diff --git a/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-panel-History.imageset/Contents.json b/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-panel-History.imageset/Contents.json new file mode 100644 index 0000000000..e972e4e90c --- /dev/null +++ b/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-panel-History.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "history.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "history@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "history@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-panel-History.imageset/history.png b/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-panel-History.imageset/history.png new file mode 100644 index 0000000000..1f3e68ab2b Binary files /dev/null and b/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-panel-History.imageset/history.png differ diff --git a/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-panel-History.imageset/history@2x.png b/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-panel-History.imageset/history@2x.png new file mode 100644 index 0000000000..8650cba5a9 Binary files /dev/null and b/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-panel-History.imageset/history@2x.png differ diff --git a/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-panel-History.imageset/history@3x.png b/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-panel-History.imageset/history@3x.png new file mode 100644 index 0000000000..2b626ad30a Binary files /dev/null and b/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-panel-History.imageset/history@3x.png differ diff --git a/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-panel-ReadingList.imageset/Contents.json b/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-panel-ReadingList.imageset/Contents.json new file mode 100644 index 0000000000..c2dfe5e38a --- /dev/null +++ b/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-panel-ReadingList.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "reading-list.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "reading-list@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "reading-list@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-panel-ReadingList.imageset/reading-list.png b/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-panel-ReadingList.imageset/reading-list.png new file mode 100644 index 0000000000..05f93e98cc Binary files /dev/null and b/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-panel-ReadingList.imageset/reading-list.png differ diff --git a/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-panel-ReadingList.imageset/reading-list@2x.png b/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-panel-ReadingList.imageset/reading-list@2x.png new file mode 100644 index 0000000000..05e1576f58 Binary files /dev/null and b/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-panel-ReadingList.imageset/reading-list@2x.png differ diff --git a/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-panel-ReadingList.imageset/reading-list@3x.png b/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-panel-ReadingList.imageset/reading-list@3x.png new file mode 100644 index 0000000000..0038a91124 Binary files /dev/null and b/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-panel-ReadingList.imageset/reading-list@3x.png differ diff --git a/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-panel-TopSites.imageset/Contents.json b/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-panel-TopSites.imageset/Contents.json new file mode 100644 index 0000000000..d0b4f6a6ab --- /dev/null +++ b/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-panel-TopSites.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "topsites.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "topsites@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "topsites@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-panel-TopSites.imageset/topsites.png b/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-panel-TopSites.imageset/topsites.png new file mode 100644 index 0000000000..aed2cf5bd9 Binary files /dev/null and b/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-panel-TopSites.imageset/topsites.png differ diff --git a/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-panel-TopSites.imageset/topsites@2x.png b/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-panel-TopSites.imageset/topsites@2x.png new file mode 100644 index 0000000000..06ad523d13 Binary files /dev/null and b/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-panel-TopSites.imageset/topsites@2x.png differ diff --git a/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-panel-TopSites.imageset/topsites@3x.png b/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-panel-TopSites.imageset/topsites@3x.png new file mode 100644 index 0000000000..5f1d13a29e Binary files /dev/null and b/mobile/ios/Client/Frontend/Menu/Menu.xcassets/menu-panel-TopSites.imageset/topsites@3x.png differ diff --git a/mobile/ios/Client/Frontend/Metadata Parsing/MetadataHelper.js b/mobile/ios/Client/Frontend/Metadata Parsing/MetadataHelper.js new file mode 100644 index 0000000000..e01bec44b3 --- /dev/null +++ b/mobile/ios/Client/Frontend/Metadata Parsing/MetadataHelper.js @@ -0,0 +1,76 @@ +/* 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/. */ + +(function() { +"use strict"; + +if (!window.__firefox__) { + Object.defineProperty(window, '__firefox__', { + enumerable: false, + configurable: false, + writable: false, + value: {} + }); +} + +function MetadataWrapper(metadataparser) { + var dataURIRegex = /^\s*data:([a-z]+\/[a-z]+(;[a-z\-]+\=[a-z\-]+)?)?(;base64)?,[a-z0-9\!\$\&\'\,\(\)\*\+\,\;\=\-\.\_\~\:\@\/\?\%\s]*\s*$/i; + + function isDataURI(s) { + return !!s.match(dataURIRegex); + } + + function getDataUri(url, callback) { + var image = new Image(); + image.onload = function() { + try { + var canvas = document.createElement('canvas'); + canvas.width = this.naturalWidth; // or 'width' if you want a special/scaled size + canvas.height = this.naturalHeight; // or 'height' if you want a special/scaled size + canvas.getContext('2d').drawImage(this, 0, 0); + var dataURI = canvas.toDataURL(); + callback(dataURI); + } catch (exception) { + callback(false); + } + }; + + image.src = url; + } + + function metadataCallback(metadata) { + window.__firefox__.pageMetadata = metadata; + webkit.messageHandlers.metadataMessageHandler.postMessage(metadata); + } + + this.extractMetadata = function() { + var metadata = metadataparser.getMetadata(window.document, document.URL); + var imageURL = metadata["image"]; + if (imageURL) { + if (isDataURI(imageURL)) { + metadata["image_data_uri"] = imageURL; + metadataCallback(metadata); + } else { + getDataUri(imageURL, function(dataURI) { + if (dataURI) { + metadata["image_data_uri"] = dataURI; + } + metadataCallback(metadata); + }); + } + } else { + metadataCallback(metadata); + } + }; +} + +Object.defineProperty(window.__firefox__, 'metadata', { + enumerable: false, + configurable: false, + writable: false, + value: Object.freeze(new MetadataWrapper(metadataparser)) +}); +metadataparser = undefined; + +})(); diff --git a/mobile/ios/Client/Frontend/Metadata Parsing/MetadataParserHelper.swift b/mobile/ios/Client/Frontend/Metadata Parsing/MetadataParserHelper.swift new file mode 100644 index 0000000000..ad258e0785 --- /dev/null +++ b/mobile/ios/Client/Frontend/Metadata Parsing/MetadataParserHelper.swift @@ -0,0 +1,55 @@ +/* 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 +import WebKit + +private let log = Logger.browserLogger + +class MetadataParserHelper: TabContentScript { + private weak var tab: Tab? + private let profile: Profile + + class func name() -> String { + return "MetadataParserHelper" + } + + required init(tab: Tab, profile: Profile) { + self.tab = tab + self.profile = profile + + tab.injectUserScriptWith(fileName: "page-metadata-parser") + tab.injectUserScriptWith(fileName: "MetadataHelper") + } + + func scriptMessageHandlerName() -> String? { + return "metadataMessageHandler" + } + + func userContentController(_ userContentController: WKUserContentController, didReceiveScriptMessage message: WKScriptMessage) { + // Get the metadata out of the page-metadata-parser, and into a type safe struct as soon + // as possible. + guard let dict = message.body as? [String: Any], + let tab = self.tab, + let pageURL = tab.url?.displayURL, + let pageMetadata = PageMetadata.fromDictionary(dict) else { + log.debug("Page contains no metadata!") + return + } + + let userInfo: [String: Any] = [ + "isPrivate": self.tab?.isPrivate ?? true, + "pageMetadata": pageMetadata, + "tabURL": pageURL + ] + + tab.pageMetadata = pageMetadata + + TabEvent.post(.didLoadPageMetadata(pageMetadata), for: tab) + NotificationCenter.default.post(name: NotificationOnPageMetadataFetched, object: nil, userInfo: userInfo) + } +} diff --git a/mobile/ios/Client/Frontend/Metadata Parsing/page-metadata-parser.js b/mobile/ios/Client/Frontend/Metadata Parsing/page-metadata-parser.js new file mode 100644 index 0000000000..78b10d3e29 --- /dev/null +++ b/mobile/ios/Client/Frontend/Metadata Parsing/page-metadata-parser.js @@ -0,0 +1 @@ +var metadataparser=function(t){function r(e){if(n[e])return n[e].exports;var o=n[e]={exports:{},id:e,loaded:!1};return t[e].call(o.exports,o,o.exports,r),o.loaded=!0,o.exports}var n={};return r.m=t,r.c=n,r.p="",r(0)}([function(t,r,n){t.exports=n(1)},function(t,r,n){"use strict";function e(t){return t&&t.__esModule?t:{default:t}}function o(t){return t.replace(/www[a-zA-Z0-9]*\./,"").replace(".co.",".").split(".").slice(0,-1).join(" ")}function u(t){return function(r,n){for(var e=0,o=void 0,u=0;ue&&(e=x,o=f(m))}}catch(t){v=!0,y=t}finally{try{!l&&g.return&&g.return()}finally{if(v)throw y}}}}if(!o&&t.defaultValue&&(o=t.defaultValue(n)),o){if(t.processors){var j=!0,k=!1,M=void 0;try{for(var T,P=(0,s.default)(t.processors);!(j=(T=P.next()).done);j=!0){var E=T.value;o=E(o,n)}}catch(t){k=!0,M=t}finally{try{!j&&P.return&&P.return()}finally{if(k)throw M}}}return o.trim&&(o=o.trim()),o}}}function i(t,r,n){var e={},o={url:r},i=n||m;return(0,f.default)(i).map(function(r){var n=i[r],c=u(n);e[r]=c(t,o)}),e}var c=n(2),f=e(c),a=n(37),s=e(a),l=n(59),p=e(l),v=n(66),d=e(v),y=n(70),h=y.makeUrlAbsolute,g=y.parseUrl,m={description:{rules:[['meta[property="og:description"]',function(t){return t.getAttribute("content")}],['meta[name="description"]',function(t){return t.getAttribute("content")}]]},icon:{rules:[['link[rel="apple-touch-icon"]',function(t){return t.getAttribute("href")}],['link[rel="apple-touch-icon-precomposed"]',function(t){return t.getAttribute("href")}],['link[rel="icon"]',function(t){return t.getAttribute("href")}],['link[rel="fluid-icon"]',function(t){return t.getAttribute("href")}],['link[rel="shortcut icon"]',function(t){return t.getAttribute("href")}],['link[rel="Shortcut Icon"]',function(t){return t.getAttribute("href")}],['link[rel="mask-icon"]',function(t){return t.getAttribute("href")}]],scorers:[function(t,r){var n=t.getAttribute("sizes");if(n){var e=n.match(/\d+/g);if(e)return e.reduce(function(t,r){return t*r})}}],defaultValue:function(t){return"favicon.ico"},processors:[function(t,r){return h(r.url,t)}]},image:{rules:[['meta[property="og:image:secure_url"]',function(t){return t.getAttribute("content")}],['meta[property="og:image:url"]',function(t){return t.getAttribute("content")}],['meta[property="og:image"]',function(t){return t.getAttribute("content")}],['meta[name="twitter:image"]',function(t){return t.getAttribute("content")}],['meta[property="twitter:image"]',function(t){return t.getAttribute("content")}],['meta[name="thumbnail"]',function(t){return t.getAttribute("content")}]],processors:[function(t,r){return h(r.url,t)}]},keywords:{rules:[['meta[name="keywords"]',function(t){return t.getAttribute("content")}]],processors:[function(t,r){return t.split(",").map(function(t){return t.trim()})}]},title:{rules:[['meta[property="og:title"]',function(t){return t.getAttribute("content")}],['meta[name="twitter:title"]',function(t){return t.getAttribute("content")}],['meta[property="twitter:title"]',function(t){return t.getAttribute("content")}],['meta[name="hdl"]',function(t){return t.getAttribute("content")}],["title",function(t){return t.text}]]},type:{rules:[['meta[property="og:type"]',function(t){return t.getAttribute("content")}]]},url:{rules:[["a.amp-canurl",function(t){return t.getAttribute("href")}],['meta[property="og:url"]',function(t){return t.getAttribute("content")}],['link[rel="canonical"]',function(t){return t.getAttribute("href")}]],defaultValue:function(t){return t.url},processors:[function(t,r){return h(r.url,t)}]},provider:{rules:[['meta[property="og:site_name"]',function(t){return t.getAttribute("content")}]],defaultValue:function(t){return o(g(t.url))}}};t.exports={buildRuleSet:u,getMetadata:i,getProvider:o,metadataRuleSets:m}},function(t,r,n){t.exports={default:n(3),__esModule:!0}},function(t,r,n){n(4),t.exports=n(24).Object.keys},function(t,r,n){var e=n(5),o=n(7);n(22)("keys",function(){return function(t){return o(e(t))}})},function(t,r,n){var e=n(6);t.exports=function(t){return Object(e(t))}},function(t,r){t.exports=function(t){if(void 0==t)throw TypeError("Can't call method on "+t);return t}},function(t,r,n){var e=n(8),o=n(21);t.exports=Object.keys||function(t){return e(t,o)}},function(t,r,n){var e=n(9),o=n(10),u=n(13)(!1),i=n(17)("IE_PROTO");t.exports=function(t,r){var n,c=o(t),f=0,a=[];for(n in c)n!=i&&e(c,n)&&a.push(n);for(;r.length>f;)e(c,n=r[f++])&&(~u(a,n)||a.push(n));return a}},function(t,r){var n={}.hasOwnProperty;t.exports=function(t,r){return n.call(t,r)}},function(t,r,n){var e=n(11),o=n(6);t.exports=function(t){return e(o(t))}},function(t,r,n){var e=n(12);t.exports=Object("z").propertyIsEnumerable(0)?Object:function(t){return"String"==e(t)?t.split(""):Object(t)}},function(t,r){var n={}.toString;t.exports=function(t){return n.call(t).slice(8,-1)}},function(t,r,n){var e=n(10),o=n(14),u=n(16);t.exports=function(t){return function(r,n,i){var c,f=e(r),a=o(f.length),s=u(i,a);if(t&&n!=n){for(;a>s;)if(c=f[s++],c!=c)return!0}else for(;a>s;s++)if((t||s in f)&&f[s]===n)return t||s||0;return!t&&-1}}},function(t,r,n){var e=n(15),o=Math.min;t.exports=function(t){return t>0?o(e(t),9007199254740991):0}},function(t,r){var n=Math.ceil,e=Math.floor;t.exports=function(t){return isNaN(t=+t)?0:(t>0?e:n)(t)}},function(t,r,n){var e=n(15),o=Math.max,u=Math.min;t.exports=function(t,r){return t=e(t),t<0?o(t+r,0):u(t,r)}},function(t,r,n){var e=n(18)("keys"),o=n(20);t.exports=function(t){return e[t]||(e[t]=o(t))}},function(t,r,n){var e=n(19),o="__core-js_shared__",u=e[o]||(e[o]={});t.exports=function(t){return u[t]||(u[t]={})}},function(t,r){var n=t.exports="undefined"!=typeof window&&window.Math==Math?window:"undefined"!=typeof self&&self.Math==Math?self:Function("return this")();"number"==typeof __g&&(__g=n)},function(t,r){var n=0,e=Math.random();t.exports=function(t){return"Symbol(".concat(void 0===t?"":t,")_",(++n+e).toString(36))}},function(t,r){t.exports="constructor,hasOwnProperty,isPrototypeOf,propertyIsEnumerable,toLocaleString,toString,valueOf".split(",")},function(t,r,n){var e=n(23),o=n(24),u=n(33);t.exports=function(t,r){var n=(o.Object||{})[t]||Object[t],i={};i[t]=r(n),e(e.S+e.F*u(function(){n(1)}),"Object",i)}},function(t,r,n){var e=n(19),o=n(24),u=n(25),i=n(27),c="prototype",f=function(t,r,n){var a,s,l,p=t&f.F,v=t&f.G,d=t&f.S,y=t&f.P,h=t&f.B,g=t&f.W,m=v?o:o[r]||(o[r]={}),x=m[c],b=v?e:d?e[r]:(e[r]||{})[c];v&&(n=r);for(a in n)s=!p&&b&&void 0!==b[a],s&&a in m||(l=s?b[a]:n[a],m[a]=v&&"function"!=typeof b[a]?n[a]:h&&s?u(l,e):g&&b[a]==l?function(t){var r=function(r,n,e){if(this instanceof t){switch(arguments.length){case 0:return new t;case 1:return new t(r);case 2:return new t(r,n)}return new t(r,n,e)}return t.apply(this,arguments)};return r[c]=t[c],r}(l):y&&"function"==typeof l?u(Function.call,l):l,y&&((m.virtual||(m.virtual={}))[a]=l,t&f.R&&x&&!x[a]&&i(x,a,l)))};f.F=1,f.G=2,f.S=4,f.P=8,f.B=16,f.W=32,f.U=64,f.R=128,t.exports=f},function(t,r){var n=t.exports={version:"2.5.1"};"number"==typeof __e&&(__e=n)},function(t,r,n){var e=n(26);t.exports=function(t,r,n){if(e(t),void 0===r)return t;switch(n){case 1:return function(n){return t.call(r,n)};case 2:return function(n,e){return t.call(r,n,e)};case 3:return function(n,e,o){return t.call(r,n,e,o)}}return function(){return t.apply(r,arguments)}}},function(t,r){t.exports=function(t){if("function"!=typeof t)throw TypeError(t+" is not a function!");return t}},function(t,r,n){var e=n(28),o=n(36);t.exports=n(32)?function(t,r,n){return e.f(t,r,o(1,n))}:function(t,r,n){return t[r]=n,t}},function(t,r,n){var e=n(29),o=n(31),u=n(35),i=Object.defineProperty;r.f=n(32)?Object.defineProperty:function(t,r,n){if(e(t),r=u(r,!0),e(n),o)try{return i(t,r,n)}catch(t){}if("get"in n||"set"in n)throw TypeError("Accessors not supported!");return"value"in n&&(t[r]=n.value),t}},function(t,r,n){var e=n(30);t.exports=function(t){if(!e(t))throw TypeError(t+" is not an object!");return t}},function(t,r){t.exports=function(t){return"object"==typeof t?null!==t:"function"==typeof t}},function(t,r,n){t.exports=!n(32)&&!n(33)(function(){return 7!=Object.defineProperty(n(34)("div"),"a",{get:function(){return 7}}).a})},function(t,r,n){t.exports=!n(33)(function(){return 7!=Object.defineProperty({},"a",{get:function(){return 7}}).a})},function(t,r){t.exports=function(t){try{return!!t()}catch(t){return!0}}},function(t,r,n){var e=n(30),o=n(19).document,u=e(o)&&e(o.createElement);t.exports=function(t){return u?o.createElement(t):{}}},function(t,r,n){var e=n(30);t.exports=function(t,r){if(!e(t))return t;var n,o;if(r&&"function"==typeof(n=t.toString)&&!e(o=n.call(t)))return o;if("function"==typeof(n=t.valueOf)&&!e(o=n.call(t)))return o;if(!r&&"function"==typeof(n=t.toString)&&!e(o=n.call(t)))return o;throw TypeError("Can't convert object to primitive value")}},function(t,r){t.exports=function(t,r){return{enumerable:!(1&t),configurable:!(2&t),writable:!(4&t),value:r}}},function(t,r,n){t.exports={default:n(38),__esModule:!0}},function(t,r,n){n(39),n(54),t.exports=n(56)},function(t,r,n){n(40);for(var e=n(19),o=n(27),u=n(43),i=n(52)("toStringTag"),c="CSSRuleList,CSSStyleDeclaration,CSSValueList,ClientRectList,DOMRectList,DOMStringList,DOMTokenList,DataTransferItemList,FileList,HTMLAllCollection,HTMLCollection,HTMLFormElement,HTMLSelectElement,MediaList,MimeTypeArray,NamedNodeMap,NodeList,PaintRequestList,Plugin,PluginArray,SVGLengthList,SVGNumberList,SVGPathSegList,SVGPointList,SVGStringList,SVGTransformList,SourceBufferList,StyleSheetList,TextTrackCueList,TextTrackList,TouchList".split(","),f=0;f=t.length?(this._t=void 0,o(1)):"keys"==r?o(0,n):"values"==r?o(0,t[n]):o(0,[n,t[n]])},"values"),u.Arguments=u.Array,e("keys"),e("values"),e("entries")},function(t,r){t.exports=function(){}},function(t,r){t.exports=function(t,r){return{value:r,done:!!t}}},function(t,r){t.exports={}},function(t,r,n){"use strict";var e=n(45),o=n(23),u=n(46),i=n(27),c=n(9),f=n(43),a=n(47),s=n(51),l=n(53),p=n(52)("iterator"),v=!([].keys&&"next"in[].keys()),d="@@iterator",y="keys",h="values",g=function(){return this};t.exports=function(t,r,n,m,x,b,A){a(n,r,m);var w,S,_,O=function(t){if(!v&&t in M)return M[t];switch(t){case y:return function(){return new n(this,t)};case h:return function(){return new n(this,t)}}return function(){return new n(this,t)}},L=r+" Iterator",j=x==h,k=!1,M=t.prototype,T=M[p]||M[d]||x&&M[x],P=T||O(x),E=x?j?O("entries"):P:void 0,R="Array"==r?M.entries||T:T;if(R&&(_=l(R.call(new t)),_!==Object.prototype&&_.next&&(s(_,L,!0),e||c(_,p)||i(_,p,g))),j&&T&&T.name!==h&&(k=!0,P=function(){return T.call(this)}),e&&!A||!v&&!k&&M[p]||i(M,p,P),f[r]=P,f[L]=g,x)if(w={values:j?P:O(h),keys:b?P:O(y),entries:E},A)for(S in w)S in M||u(M,S,w[S]);else o(o.P+o.F*(v||k),r,w);return w}},function(t,r){t.exports=!0},function(t,r,n){t.exports=n(27)},function(t,r,n){"use strict";var e=n(48),o=n(36),u=n(51),i={};n(27)(i,n(52)("iterator"),function(){return this}),t.exports=function(t,r,n){t.prototype=e(i,{next:o(1,n)}),u(t,r+" Iterator")}},function(t,r,n){var e=n(29),o=n(49),u=n(21),i=n(17)("IE_PROTO"),c=function(){},f="prototype",a=function(){var t,r=n(34)("iframe"),e=u.length,o="<",i=">";for(r.style.display="none",n(50).appendChild(r),r.src="javascript:",t=r.contentWindow.document,t.open(),t.write(o+"script"+i+"document.F=Object"+o+"/script"+i),t.close(),a=t.F;e--;)delete a[f][u[e]];return a()};t.exports=Object.create||function(t,r){var n;return null!==t?(c[f]=e(t),n=new c,c[f]=null,n[i]=t):n=a(),void 0===r?n:o(n,r)}},function(t,r,n){var e=n(28),o=n(29),u=n(7);t.exports=n(32)?Object.defineProperties:function(t,r){o(t);for(var n,i=u(r),c=i.length,f=0;c>f;)e.f(t,n=i[f++],r[n]);return t}},function(t,r,n){var e=n(19).document;t.exports=e&&e.documentElement},function(t,r,n){var e=n(28).f,o=n(9),u=n(52)("toStringTag");t.exports=function(t,r,n){t&&!o(t=n?t:t.prototype,u)&&e(t,u,{configurable:!0,value:r})}},function(t,r,n){var e=n(18)("wks"),o=n(20),u=n(19).Symbol,i="function"==typeof u,c=t.exports=function(t){return e[t]||(e[t]=i&&u[t]||(i?u:o)("Symbol."+t))};c.store=e},function(t,r,n){var e=n(9),o=n(5),u=n(17)("IE_PROTO"),i=Object.prototype;t.exports=Object.getPrototypeOf||function(t){return t=o(t),e(t,u)?t[u]:"function"==typeof t.constructor&&t instanceof t.constructor?t.constructor.prototype:t instanceof Object?i:null}},function(t,r,n){"use strict";var e=n(55)(!0);n(44)(String,"String",function(t){this._t=String(t),this._i=0},function(){var t,r=this._t,n=this._i;return n>=r.length?{value:void 0,done:!0}:(t=e(r,n),this._i+=t.length,{value:t,done:!1})})},function(t,r,n){var e=n(15),o=n(6);t.exports=function(t){return function(r,n){var u,i,c=String(o(r)),f=e(n),a=c.length;return f<0||f>=a?t?"":void 0:(u=c.charCodeAt(f),u<55296||u>56319||f+1===a||(i=c.charCodeAt(f+1))<56320||i>57343?t?c.charAt(f):u:t?c.slice(f,f+2):(u-55296<<10)+(i-56320)+65536)}}},function(t,r,n){var e=n(29),o=n(57);t.exports=n(24).getIterator=function(t){var r=o(t);if("function"!=typeof r)throw TypeError(t+" is not iterable!");return e(r.call(t))}},function(t,r,n){var e=n(58),o=n(52)("iterator"),u=n(43);t.exports=n(24).getIteratorMethod=function(t){if(void 0!=t)return t[o]||t["@@iterator"]||u[e(t)]}},function(t,r,n){var e=n(12),o=n(52)("toStringTag"),u="Arguments"==e(function(){return arguments}()),i=function(t,r){try{return t[r]}catch(t){}};t.exports=function(t){var r,n,c;return void 0===t?"Undefined":null===t?"Null":"string"==typeof(n=i(r=Object(t),o))?n:u?e(r):"Object"==(c=e(r))&&"function"==typeof r.callee?"Arguments":c}},function(t,r,n){t.exports={default:n(60),__esModule:!0}},function(t,r,n){n(54),n(61),t.exports=n(24).Array.from},function(t,r,n){"use strict";var e=n(25),o=n(23),u=n(5),i=n(62),c=n(63),f=n(14),a=n(64),s=n(57);o(o.S+o.F*!n(65)(function(t){Array.from(t)}),"Array",{from:function(t){var r,n,o,l,p=u(t),v="function"==typeof this?this:Array,d=arguments.length,y=d>1?arguments[1]:void 0,h=void 0!==y,g=0,m=s(p);if(h&&(y=e(y,d>2?arguments[2]:void 0,2)),void 0==m||v==Array&&c(m))for(r=f(p.length),n=new v(r);r>g;g++)a(n,g,h?y(p[g],g):p[g]);else for(l=m.call(p),n=new v;!(o=l.next()).done;g++)a(n,g,h?i(l,y,[o.value,g],!0):o.value);return n.length=g,n}})},function(t,r,n){var e=n(29);t.exports=function(t,r,n,o){try{return o?r(e(n)[0],n[1]):r(n)}catch(r){var u=t.return;throw void 0!==u&&e(u.call(t)),r}}},function(t,r,n){var e=n(43),o=n(52)("iterator"),u=Array.prototype;t.exports=function(t){return void 0!==t&&(e.Array===t||u[o]===t)}},function(t,r,n){"use strict";var e=n(28),o=n(36);t.exports=function(t,r,n){r in t?e.f(t,r,o(0,n)):t[r]=n}},function(t,r,n){var e=n(52)("iterator"),o=!1;try{var u=[7][e]();u.return=function(){o=!0},Array.from(u,function(){throw 2})}catch(t){}t.exports=function(t,r){if(!r&&!o)return!1;var n=!1;try{var u=[7],i=u[e]();i.next=function(){return{done:n=!0}},u[e]=function(){return i},t(u)}catch(t){}return n}},function(t,r,n){"use strict";function e(t){return t&&t.__esModule?t:{default:t}}r.__esModule=!0;var o=n(67),u=e(o),i=n(37),c=e(i);r.default=function(){function t(t,r){var n=[],e=!0,o=!1,u=void 0;try{for(var i,f=(0,c.default)(t);!(e=(i=f.next()).done)&&(n.push(i.value),!r||n.length!==r);e=!0);}catch(t){o=!0,u=t}finally{try{!e&&f.return&&f.return()}finally{if(o)throw u}}return n}return function(r,n){if(Array.isArray(r))return r;if((0,u.default)(Object(r)))return t(r,n);throw new TypeError("Invalid attempt to destructure non-iterable instance")}}()},function(t,r,n){t.exports={default:n(68),__esModule:!0}},function(t,r,n){n(39),n(54),t.exports=n(69)},function(t,r,n){var e=n(58),o=n(52)("iterator"),u=n(43);t.exports=n(24).isIterable=function(t){var r=Object(t);return void 0!==r[o]||"@@iterator"in r||u.hasOwnProperty(e(r))}},function(t,r,n){(function(r){"use strict";if(void 0!==r.URL)t.exports={makeUrlAbsolute:function(t,r){return new URL(r,t).href},parseUrl:function(t){return new URL(t).host}};else{var e=n(71);t.exports={makeUrlAbsolute:function(t,r){var n=e.parse(r);return null===n.host?e.resolve(t,r):r},parseUrl:function(t){return e.parse(t).hostname}}}}).call(r,function(){return this}())},function(t,r){t.exports=window}]); \ No newline at end of file diff --git a/mobile/ios/Client/Frontend/OldStrings.swift b/mobile/ios/Client/Frontend/OldStrings.swift new file mode 100644 index 0000000000..6169ebe2cb --- /dev/null +++ b/mobile/ios/Client/Frontend/OldStrings.swift @@ -0,0 +1,65 @@ +/* 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 + +/// These were supposed to be strings for prelanded features, but they've been sitting here for awhile. +/// We should audit and remove them (bug 1262956). +/// NOTE: DO NOT LAND NEW STRINGS HERE! Land them in Shared/Strings.swift instead. +private func prelandedStrings() { + // Bug 1182303 - Checkbox to block alert spam. + _ = NSLocalizedString("Disable additional page dialogs", comment: "Pending feature; currently unused string! Checkbox label shown after multiple alerts are shown") + + // Bug 1186013 - Prompt for going to clipboard URL + _ = NSLocalizedString("Go to copied URL?", comment: "Pending feature; currently unused string! Prompt message shown when browser is opened with URL on the clipboard") + _ = NSLocalizedString("Go", comment: "Pending feature; currently unused string! Button to browse to URL on the clipboard when browser is opened") + + // strings for lightweight themes + _ = NSLocalizedString("Theme", tableName: "LightweightThemes", comment: "Pending feature; currently unused string! Settings row to enter theme settings") + _ = NSLocalizedString("Themes", tableName: "LightweightThemes", comment: "Pending feature; currently unused string! sub header for theme options in theme chooser") + _ = NSLocalizedString("Photos", tableName: "LightweightThemes", comment: "Pending feature; currently unused string! Sub header for photo options in theme chooser") + _ = NSLocalizedString("Preview", tableName: "LightweightThemes", comment: "Pending feature; currently unused string! Button to show preview of selected theme") + _ = NSLocalizedString("Set", tableName: "LightweightThemes", comment: "Pending feature; currently unused string! Button to set selected theme as current theme") + _ = NSLocalizedString("Allow Firefox to use my Photos", tableName: "LightweightThemes", comment: "Pending feature; currently unused string! Button shown when user wishes to view photos for theme but has not given Firefox permission to do so yet.") + _ = NSLocalizedString("Choose", tableName: "LightweightThemes", comment: "Pending feature; currently unused string! Back button shown to return back to theme settings from photo picker") + + // accessibility strings for lightweight themes + _ = NSLocalizedString("Choose Theme", tableName: "LightweightThemes", comment: "Pending feature; currently unused string! Accessibility label for settings row to enter theme settings") + _ = NSLocalizedString("Default Themes", tableName: "LightweightThemes", comment: "Pending feature; currently unused string! Accessibility label for themes subheader") + _ = NSLocalizedString("My Photos", tableName: "LightweightThemes", comment: "Pending feature; currently unused string! Accessibility label for photo viewer") + _ = NSLocalizedString("Preview Theme", tableName: "LightweightThemes", comment: "Pending feature; currently unused string! Accessibility label for theme preview button") + _ = NSLocalizedString("Set Theme", tableName: "LightweightThemes", comment: "Pending feature; currently unused string! Accessibility label for set theme button") + _ = NSLocalizedString("Back to Themes", tableName: "LightweightThemes", comment: "Pending feature; currently unused string! Accessibility label for back button to theme chooser") + + // Bug 1198418 - Touch ID Passcode Strings + _ = NSLocalizedString("Turn off your passcode.", tableName: "AuthenticationManager", comment: "Touch ID prompt subtitle when turning off passcode") + _ = NSLocalizedString("Use your fingerprint to access Private Browsing now.", tableName: "AuthenticationManager", comment: "Touch ID prompt subtitle when accessing private browsing") +} + +/// Old strings that will be removed in a future version. We keep these around for l10n backwards compatibility. +private func obsoleteStrings() { + // v1.0 + _ = NSLocalizedString("Browse multiple Web pages at the same time with tabs.", tableName: "Intro", comment: "See http://mzl.la/1T8gxwo") + _ = NSLocalizedString("Personalize your Firefox just the way you like in Settings.", tableName: "Intro", comment: "See http://mzl.la/1T8gxwo") + _ = NSLocalizedString("Connect Firefox everywhere you use it.", tableName: "Intro", comment: "See http://mzl.la/1T8gxwo") + _ = NSLocalizedString("Show search suggestions", comment: "Label for show search suggestions setting.") + _ = NSLocalizedString("Sign in", comment: "Text message / button in the settings table view") + + // v3.0 + _ = NSLocalizedString("History will be removed from all your connected devices. This cannot be undone.", tableName: "ClearHistoryConfirm", comment: "Description of the confirmation dialog shown when a user tries to clear history that's synced to another device.") + _ = NSLocalizedString("Remove history from your Firefox Account?", tableName: "ClearHistoryConfirm", comment: "Title of the confirmation dialog shown when a user tries to clear history that's synced to another device.") + _ = NSLocalizedString("Clear", tableName: "ClearHistoryConfirm", comment: "The button that clears history even when Sync is connected.") + _ = NSLocalizedString("Clear Private Data", comment: "Label used as an item in Settings. When touched it will open a dialog prompting the user to make sure they want to clear all of their private data.") + _ = NSLocalizedString("Clear Private Data", tableName: "ClearPrivateData", comment: "Navigation title in settings.") + _ = NSLocalizedString("Clear Private Data", tableName: "ClearPrivateData", comment: "Button in settings that clears private data for the selected items.") + _ = NSLocalizedString("Opening %@", comment: "Opening an external URL") + _ = NSLocalizedString("This will open in another application", comment: "Opening an external app") + _ = NSLocalizedString("Not now", comment: "Button to not save the user's password") + _ = NSLocalizedString("Update", comment: "Button to update the user's password") + _ = NSLocalizedString("Yes", comment: "Button to save the user's password") + _ = NSLocalizedString("Do you want to save the password for %@ on %@?", comment: "Prompt for saving a password. The first parameter is the username being saved. The second parameter is the hostname of the site.") + _ = NSLocalizedString("Do you want to save the password on %@?", comment: "Prompt for saving a password with no username. The parameter is the hostname of the site.") + _ = NSLocalizedString("Do you want to update the password for %@ on %@?", comment: "Prompt for updating a password. The first parameter is the username being saved. The second parameter is the hostname of the site.") + _ = NSLocalizedString("Do you want to update the password on %@?", comment: "Prompt for updating a password with on username. The parameter is the hostname of the site.") +} \ No newline at end of file diff --git a/mobile/ios/Client/Frontend/Reader/FSReadingList.h b/mobile/ios/Client/Frontend/Reader/FSReadingList.h new file mode 100644 index 0000000000..1df191d029 --- /dev/null +++ b/mobile/ios/Client/Frontend/Reader/FSReadingList.h @@ -0,0 +1,7 @@ +/* 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/. */ + +#pragma once + +extern NSString* const FSReadingListAddReadingListItemNotification; diff --git a/mobile/ios/Client/Frontend/Reader/FSReadingList.m b/mobile/ios/Client/Frontend/Reader/FSReadingList.m new file mode 100644 index 0000000000..719c9ce214 --- /dev/null +++ b/mobile/ios/Client/Frontend/Reader/FSReadingList.m @@ -0,0 +1,60 @@ +/* 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 +#import + +#import "Swizzling.h" + +NSString* const FSReadingListAddReadingListItemNotification = @"FSReadingListAddReadingListItemNotification"; + +@interface FSReadingList: NSObject ++ (id) sharedInstance; ++ (BOOL)supportsURL:(NSURL *)URL; +- (BOOL)addReadingListItemWithURL:(NSURL *)URL title:(NSString *)title previewText:(NSString *)previewText error:(NSError **)error; +@end + +@implementation FSReadingList ++ (id) sharedInstance { + static FSReadingList *sharedFSReadingList = nil; + @synchronized (self) { + if (sharedFSReadingList == nil) { + sharedFSReadingList = [FSReadingList new]; + } + } + return sharedFSReadingList; +} + ++ (BOOL)supportsURL:(NSURL *)URL { + return [[URL scheme] isEqualToString: @"http"] || [[URL scheme] isEqualToString: @"https"]; +} + +- (BOOL)addReadingListItemWithURL:(NSURL *)URL title:(NSString *)title previewText:(NSString *)previewText error:(NSError **)error { + if (error != NULL) { + *error = nil; + } + // To keep this as simple as possible and have as little as possible coupling between this Objective-C + // singleton and our Swift world, we simply send out a notification that our AppDelegate (which has access + // to the browser profile and reading list service) can respond to. + [[NSNotificationCenter defaultCenter] postNotificationName: FSReadingListAddReadingListItemNotification + object:self userInfo: @{@"URL": URL, @"Title": title}]; + return YES; +} +@end + +// This class extension on SSReadingList implements an initialize method that will be called when the class +// is instantiated. It swizzles defaultReadingList to our own implementation which returns a shared instance +// of the FSReadingList. ("FirefoxServices" Reading List) + +@implementation SSReadingList (Firefox) ++ (void) initialize { + if ([SSReadingList class] == self) { + SwizzleClassMethods([SSReadingList class], @selector(defaultReadingList), @selector(defaultFSReadingList)); + } +} + ++ (id) defaultFSReadingList { + return [FSReadingList sharedInstance]; +} +@end diff --git a/mobile/ios/Client/Frontend/Reader/ReadabilityService.swift b/mobile/ios/Client/Frontend/Reader/ReadabilityService.swift new file mode 100644 index 0000000000..70b394c543 --- /dev/null +++ b/mobile/ios/Client/Frontend/Reader/ReadabilityService.swift @@ -0,0 +1,112 @@ +/* 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 WebKit + +private let ReadabilityServiceSharedInstance = ReadabilityService() + +private let ReadabilityTaskDefaultTimeout = 15 +private let ReadabilityServiceDefaultConcurrency = 1 + +enum ReadabilityOperationResult { + case success(ReadabilityResult) + case error(NSError) + case timeout +} + +class ReadabilityOperation: Operation, WKNavigationDelegate, ReadabilityTabHelperDelegate { + var url: URL + var semaphore: DispatchSemaphore + var result: ReadabilityOperationResult? + var tab: Tab! + var readerModeCache: ReaderModeCache + + init(url: URL, readerModeCache: ReaderModeCache) { + self.url = url + self.semaphore = DispatchSemaphore(value: 0) + self.readerModeCache = readerModeCache + } + + override func main() { + if self.isCancelled { + return + } + + // Setup a tab, attach a Readability helper. Kick all this off on the main thread since UIKit + // and WebKit are not safe from other threads. + + DispatchQueue.main.async(execute: { () -> Void in + let configuration = WKWebViewConfiguration() + self.tab = Tab(configuration: configuration) + self.tab.createWebview() + self.tab.navigationDelegate = self + + if let readabilityTabHelper = ReadabilityTabHelper(tab: self.tab) { + readabilityTabHelper.delegate = self + self.tab.addContentScript(readabilityTabHelper, name: ReadabilityTabHelper.name()) + } + + // Load the page in the webview. This either fails with a navigation error, or we get a readability + // callback. Or it takes too long, in which case the semaphore times out. + self.tab.loadRequest(URLRequest(url: self.url)) + }) + let timeout = DispatchTime.now() + Double(Int64(Double(16) * Double(NSEC_PER_SEC))) / Double(NSEC_PER_SEC) + if semaphore.wait(timeout: timeout) == .timedOut { + result = ReadabilityOperationResult.timeout + } + + // Maybe this is where we should store stuff in the cache / run a callback? + + if let result = self.result { + switch result { + case .timeout: + // Don't do anything on timeout + break + case .success(let readabilityResult): + do { + try readerModeCache.put(url, readabilityResult) + } catch let error as NSError { + print("Failed to store readability results in the cache: \(error.localizedDescription)") + // TODO Fail + } + case .error(_): + // TODO Not entitely sure what to do on error. Needs UX discussion and followup bug. + break + } + } + } + + func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) { + result = ReadabilityOperationResult.error(error as NSError) + semaphore.signal() + } + + func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error) { + result = ReadabilityOperationResult.error(error as NSError) + semaphore.signal() + } + + func readabilityTabHelper(_ readabilityTabHelper: ReadabilityTabHelper, didFinishWithReadabilityResult readabilityResult: ReadabilityResult) { + result = ReadabilityOperationResult.success(readabilityResult) + semaphore.signal() + } +} + +class ReadabilityService { + class var sharedInstance: ReadabilityService { + return ReadabilityServiceSharedInstance + } + + var queue: OperationQueue + + init() { + queue = OperationQueue() + queue.maxConcurrentOperationCount = ReadabilityServiceDefaultConcurrency + } + + func process(_ url: URL, cache: ReaderModeCache) { + queue.addOperation(ReadabilityOperation(url: url, readerModeCache: cache)) + } +} diff --git a/mobile/ios/Client/Frontend/Reader/ReadabilityTabHelper.js b/mobile/ios/Client/Frontend/Reader/ReadabilityTabHelper.js new file mode 100644 index 0000000000..3509fa3756 --- /dev/null +++ b/mobile/ios/Client/Frontend/Reader/ReadabilityTabHelper.js @@ -0,0 +1,30 @@ +/* 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/. */ + +(function() { + // To keep this file readable and Readability.js separate, since we import it from an external + // repository, we merge it in this file programatically. We do not include it as a user script + // because that means pages can mess with it; by including it below, it is part of an anonymous + // function that only exists once. + + %READABILITYJS% + + var uri = { + spec: document.location.href, + host: document.location.host, + prePath: document.location.protocol + "//" + document.location.host, // TODO This is incomplete, needs username/password and port + scheme: document.location.protocol.substr(0, document.location.protocol.indexOf(":")), + pathBase: document.location.protocol + "//" + document.location.host + location.pathname.substr(0, location.pathname.lastIndexOf("/") + 1) + } + + // document.cloneNode() can cause the webview to break (bug 1128774). + // Serialize and then parse the document instead. + var docStr = new XMLSerializer().serializeToString(document); + var doc = new DOMParser().parseFromString(docStr, "text/html"); + + var readability = new Readability(uri, doc); + var readabilityResult = readability.parse(); + + webkit.messageHandlers.readabilityMessageHandler.postMessage(readabilityResult); +})(); diff --git a/mobile/ios/Client/Frontend/Reader/ReadabilityTabHelper.swift b/mobile/ios/Client/Frontend/Reader/ReadabilityTabHelper.swift new file mode 100644 index 0000000000..d649affbdb --- /dev/null +++ b/mobile/ios/Client/Frontend/Reader/ReadabilityTabHelper.swift @@ -0,0 +1,39 @@ +/* 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 WebKit + +protocol ReadabilityTabHelperDelegate { + func readabilityTabHelper(_ readabilityTabHelper: ReadabilityTabHelper, didFinishWithReadabilityResult result: ReadabilityResult) +} + +class ReadabilityTabHelper: TabContentScript { + var delegate: ReadabilityTabHelperDelegate? + + class func name() -> String { + return "ReadabilityTabHelper" + } + + init?(tab: Tab) { + if let readabilityPath = Bundle.main.path(forResource: "Readability", ofType: "js"), + let readabilitySource = try? NSMutableString(contentsOfFile: readabilityPath, encoding: String.Encoding.utf8.rawValue), + let readabilityTabHelperPath = Bundle.main.path(forResource: ReadabilityTabHelper.name(), ofType: "js"), + let readabilityTabHelperSource = try? NSMutableString(contentsOfFile: readabilityTabHelperPath, encoding: String.Encoding.utf8.rawValue) { + readabilityTabHelperSource.replaceOccurrences(of: "%READABILITYJS%", with: readabilitySource as String, options: NSString.CompareOptions.literal, range: NSRange(location: 0, length: readabilityTabHelperSource.length)) + let userScript = WKUserScript(source: readabilityTabHelperSource as String, injectionTime: WKUserScriptInjectionTime.atDocumentEnd, forMainFrameOnly: true) + tab.webView!.configuration.userContentController.addUserScript(userScript) + } + } + + func scriptMessageHandlerName() -> String? { + return "readabilityMessageHandler" + } + + func userContentController(_ userContentController: WKUserContentController, didReceiveScriptMessage message: WKScriptMessage) { + if let readabilityResult = ReadabilityResult(object: message.body as AnyObject?) { + delegate?.readabilityTabHelper(self, didFinishWithReadabilityResult: readabilityResult) + } + } +} diff --git a/mobile/ios/Client/Frontend/Reader/Reader.css b/mobile/ios/Client/Frontend/Reader/Reader.css new file mode 100644 index 0000000000..a70fdda48f --- /dev/null +++ b/mobile/ios/Client/Frontend/Reader/Reader.css @@ -0,0 +1,787 @@ +/* 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/. */ + +@font-face { + font-family: sans-serif; + src: url('/reader-mode/fonts/FiraSans-Regular.ttf'); +} + +@font-face { + font-family: sans-serif; + src: url('/reader-mode/fonts/FiraSans-Book.ttf'); + font-weight: medium; + } + +@font-face { + font-family: sans-serif; + src: url('/reader-mode/fonts/FiraSans-Bold.ttf'); + font-weight: bold; +} + +@font-face { + font-family: sans-serif; + src: url('/reader-mode/fonts/FiraSans-Italic.ttf'); + font-style: italic; +} + +@font-face { + font-family: sans-serif; + src: url('/reader-mode/fonts/FiraSans-BoldItalic.ttf'); + font-weight: bold; + font-style: italic; +} + +@font-face { + font-family: serif; + src: url('/reader-mode/fonts/CharisSILR.ttf'); +} + +@font-face { + font-family: serif; + src: url('/reader-mode/fonts/CharisSILB.ttf'); + font-weight: bold; +} + +@font-face { + font-family: serif; + src: url('/reader-mode/fonts/CharisSILI.ttf'); + font-style: italic; +} + +@font-face { + font-family: serif; + src: url('/reader-mode/fonts/CharisSILBI.ttf'); + font-weight: bold; + font-style: italic; +} + +html { + -moz-text-size-adjust: none; + -webkit-text-size-adjust: none; +} + +body { + padding: 2vw; + transition-property: background-color, color; + transition-duration: 0.4s; + margin-left: auto; + margin-right: auto; +} + +.light { + background-color: #ffffff; + color: #222222; +} + +.dark { + background-color: #333333; + color: #eeeeee; +} + +.sepia { + background-color: #F0E6DC; + color: #333333; +} + +.sans-serif { + font-family: sans-serif; +} + +.serif { + font-family: serif; +} + +.message { + margin-top: 40px; + display: none; + text-align: center; + width: 100%; + font-size: 16px; +} + +#reader-header { + text-align: start; + display: none; +} + +.domain, +.credits { + font-family: sans-serif; +} + +.domain { + margin-top: 10px; + padding-bottom: 10px; + color: #00acff !important; + text-decoration: none; +} + +.domain-border { + margin-top: 15px; + border-bottom: 1.5px solid #777777; + width: 50%; +} + +.header > h1 { + font-size: 1.5em; + font-weight: 700; + line-height: 1.1em; + width: 100%; + margin: 0px; + margin-top: 0px; + margin-bottom: 16px; + padding: 0px; +} + +.header > .credits { + padding: 0px; + margin: 0px; + margin-bottom: 24px; + font-style: italic; +} + +.light > .header > .domain { + color: #ee7600; + border-bottom-color: #d0d0d0; +} + +.light > .header > h1 { + color: #222222; +} + +.light > .header > .credits { + color: #333333; +} + +.dark > .header > .domain { + color: #ff9400; + border-bottom-color: #777777; +} + +.dark > .header > h1 { + color: #eeeeee; +} + +.dark > .header > .credits { + color: #aaaaaa; +} + +.font-size1 > .header > h1 { + font-size: 24px; +} + +.font-size2 > .header > h1 { + font-size: 28px; +} + +.font-size3 > .header > h1 { + font-size: 32px; +} + +.font-size4 > .header > h1 { + font-size: 36px; +} + +.font-size5 > .header > h1 { + font-size: 40px; +} + +.font-size6 > .header > h1 { + font-size: 44px; +} + +.font-size7 > .header > h1 { + font-size: 48px; +} + +.font-size8 > .header > h1 { + font-size: 52px; +} + +.font-size9 > .header > h1 { + font-size: 56px; +} + +.font-size10 > .header > h1 { + font-size: 60px; +} + +.font-size11 > .header > h1 { + font-size: 64px; +} + +.font-size12 > .header > h1 { + font-size: 68px; +} + +.font-size13 > .header > h1 { + font-size: 72px; +} + +/* This covers caption, domain, and credits + texts in the reader UI */ + +.font-size1 > .content .wp-caption-text, +.font-size1 > .content figcaption, +.font-size1 > .header > .domain, +.font-size1 > .header > .credits { + font-size: 12px; +} + +.font-size2 > .content .wp-caption-text, +.font-size2 > .content figcaption, +.font-size2 > .header > .domain, +.font-size2 > .header > .credits { + font-size: 14px; +} + +.font-size3 > .content .wp-caption-text, +.font-size3 > .content figcaption, +.font-size3 > .header > .domain, +.font-size3 > .header > .credits { + font-size: 16px; +} + +.font-size4 > .content .wp-caption-text, +.font-size4 > .content figcaption, +.font-size4 > .header > .domain, +.font-size4 > .header > .credits { + font-size: 18px; +} + +.font-size5 > .content .wp-caption-text, +.font-size5 > .content figcaption, +.font-size5 > .header > .domain, +.font-size5 > .header > .credits { + font-size: 20px; +} + +.font-size6 > .content .wp-caption-text, +.font-size6 > .content figcaption, +.font-size6 > .header > .domain, +.font-size6 > .header > .credits { + font-size: 22px; +} + +.font-size7 > .content .wp-caption-text, +.font-size7 > .content figcaption, +.font-size7 > .header > .domain, +.font-size7 > .header > .credits { + font-size: 25px; +} + +.font-size8 > .content .wp-caption-text, +.font-size8 > .content figcaption, +.font-size8 > .header > .domain, +.font-size8 > .header > .credits { + font-size: 28px; +} + +.font-size9 > .content .wp-caption-text, +.font-size9 > .content figcaption, +.font-size9 > .header > .domain, +.font-size9 > .header > .credits { + font-size: 31px; +} + +.font-size10 > .content .wp-caption-text, +.font-size10 > .content figcaption, +.font-size10 > .header > .domain, +.font-size10 > .header > .credits { + font-size: 35px; +} + +.font-size11 > .content .wp-caption-text, +.font-size11 > .content figcaption, +.font-size11 > .header > .domain, +.font-size11 > .header > .credits { + font-size: 40px; +} + +.font-size12 > .content .wp-caption-text, +.font-size12 > .content figcaption, +.font-size12 > .header > .domain, +.font-size12 > .header > .credits { + font-size: 45px; +} + +.font-size13 > .content .wp-caption-text, +.font-size13 > .content figcaption, +.font-size13 > .header > .domain, +.font-size13 > .header > .credits { + font-size: 50px; +} + +#reader-content { + display: none; +} + +.content a { + text-decoration: none !important; + font-weight: normal; +} + +.light > .content a, +.light > .content a:visited, +.light > .content a:hover, +.light > .content a:active { + color: #00acff !important; +} + +.dark > .content a, +.dark > .content a:visited, +.dark > .content a:hover, +.dark > .content a:active { + color: #00acff !important; +} + +.sepia > .content a, +.sepia > .content a:visited, +.sepia > .content a:hover, +.sepia > .content a:active { + color: #00acff !important; +} + +.content * { + max-width: 100% !important; + height: auto !important; +} + +.content p { + line-height: 1.4em !important; + margin: 0px !important; + margin-bottom: 20px !important; +} + +/* Covers all images showing edge-to-edge using a + an optional caption text */ +.content .wp-caption, +.content figure { + display: block !important; + width: 100% !important; + margin: 0px !important; + margin-bottom: 32px !important; +} + +/* Images marked to be shown edge-to-edge with an + optional captio ntext */ +.content p > img:only-child, +.content p > a:only-child > img:only-child, +.content .wp-caption img, +.content figure img { + max-width: none !important; + height: auto !important; + display: block !important; + margin-top: 0px !important; + margin-bottom: 32px !important; +} + +/* If image is place inside one of these blocks + there's no need to add margin at the bottom */ +.content .wp-caption img, +.content figure img { + margin-bottom: 0px !important; +} + +/* Image caption text */ +.content .caption, +.content .wp-caption-text, +.content figcaption { + font-family: sans-serif; + margin: 0px !important; + padding-top: 4px !important; +} + +.light > .content .caption, +.light > .content .wp-caption-text, +.light > .content figcaption { + color: #898989; +} + +.dark > .content .caption, +.dark > .content .wp-caption-text, +.dark > .content figcaption { + color: #aaaaaa; +} + +/* Ensure all pre-formatted code inside the reader content + are properly wrapped inside content width */ +.content code, +.content pre { + white-space: pre-wrap !important; + margin-bottom: 20px !important; + word-break: break-all; +} + +.content blockquote { + margin: 0px !important; + margin-bottom: 20px !important; + padding: 0px !important; + -moz-padding-start: 16px !important; + -webkit-padding-start: 16px !important; + border: 0px !important; + border-left: 2px solid !important; +} + +.light > .content blockquote { + color: #898989 !important; + border-left-color: #d0d0d0 !important; +} + +.dark > .content blockquote { + color: #aaaaaa !important; + border-left-color: #777777 !important; +} + +.content ul, +.content ol { + margin: 0px !important; + margin-bottom: 20px !important; + padding: 0px !important; + line-height: 1.5em; +} + +.content ul { + -moz-padding-start: 30px !important; + -webkit-padding-start: 30px !important; + list-style: disk !important; +} + +.content ol { + -moz-padding-start: 35px !important; + -webkit-padding-start: 35px !important; + list-style: decimal !important; +} + +.font-size1-sample, +.font-size1 > .content { + font-size: 10px !important; +} + +.font-size2-sample, +.font-size2 > .content { + font-size: 11px !important; +} + +.font-size3-sample, +.font-size3 > .content { + font-size: 12px !important; +} + +.font-size4-sample, +.font-size4 > .content { + font-size: 14px !important; +} + +.font-size5-sample, +.font-size5 > .content { + font-size: 16px !important; +} + +.font-size6-sample, +.font-size6 > .content { + font-size: 18px !important; +} + +.font-size7-sample, +.font-size7 > .content { + font-size: 21px !important; +} + +.font-size8-sample, +.font-size8 > .content { + font-size: 24px !important; +} + +.font-size9-sample, +.font-size9 > .content { + font-size: 28px !important; +} + +.font-size10-sample, +.font-size10 > .content { + font-size: 32px !important; +} + +.font-size11-sample, +.font-size11 > .content { + font-size: 37px !important; +} + +.font-size12-sample, +.font-size12 > .content { + font-size: 42px !important; +} + +.font-size13-sample, +.font-size13 > .content { + font-size: 48px !important; +} + +.toolbar { + font-family: "Clear Sans",sans-serif; + transition-property: visibility, opacity; + transition-duration: 0.7s; + visibility: visible; + opacity: 1.0; + position: fixed; + width: 100%; + bottom: 0px; + left: 0px; + margin: 0; + padding: 0; + list-style: none; + background-color: #EBEBF0; + -moz-user-select: none; +} + +.toolbar-hidden { + transition-property: visibility, opacity; + transition-duration: 0.7s; + visibility: hidden; + opacity: 0.0; +} + +.toolbar > * { + float: right; + width: 33%; +} + +.button { + color: white; + display: block; + background-position: center; + background-size: 30px 24px; + background-repeat: no-repeat; +} + +.dropdown { + text-align: center; + display: inline-block; + list-style: none; + margin: 0px; + padding: 0px; +} + +.dropdown li { + margin: 0px; + padding: 0px; +} + +.dropdown-toggle { + margin: 0px; + padding: 0px; +} + +.dropdown-popup { + text-align: start; + position: absolute; + left: 0px; + z-index: 1000; + float: left; + background: #EBEBF0; + margin-top: 12px; + margin-bottom: 10px; + padding-top: 4px; + padding-bottom: 8px; + font-size: 14px; + box-shadow: 0px -1px 12px #333; + border-radius: 3px; + visibility: hidden; +} + +.dropdown-popup > hr { + width: 100%; + height: 0px; + border: 0px; + border-top: 1px solid #B5B5B5; + margin: 0; +} + +.open > .dropdown-popup { + margin-top: 0px; + margin-bottom: 6px; + bottom: 100%; + visibility: visible; +} + +.dropdown-arrow { + position: absolute; + width: 40px; + height: 18px; + bottom: -18px; + background-image: url('chrome://browser/skin/images/reader-dropdown-arrow-mdpi.png'); + background-size: 40px 18px; + background-position: center; + display: block; +} + +#font-type-buttons, +.segmented-button { + display: flex; + flex-direction: row; + list-style: none; + padding: 10px 5px; + white-space: nowrap; +} + +#font-type-buttons > li, +.segmented-button > li { + width: 50px; /* combined with flex, this acts as a minimum width */ + flex: 1 0 auto; + text-align: center; + line-height: 20px; +} + +#font-type-buttons > li { + padding: 10px 0; +} + +.segmented-button > li { + border-left: 1px solid #B5B5B5; +} + +.segmented-button > li:first-child { + border-left: 0px; +} + +#font-type-buttons > li > a, +.segmented-button > li > a { + vertical-align: middle; + text-decoration: none; + color: black; +} + +#font-type-buttons > li > a { + display: inline-block; + font-size: 48px; + line-height: 50px; + margin-bottom: 5px; + border-bottom: 3px solid transparent; +} + +.segmented-button > li > a { + display: block; + padding: 5px 0; + font-family: "Clear Sans",sans-serif; + font-weight: lighter; +} + +#font-type-buttons > li > a:active, +#font-type-buttons > li.selected > a { + border-color: #ff9400; +} + +.segmented-button > li > a:active, +.segmented-button > li.selected > a { + font-weight: bold; +} + +#font-type-buttons > li > .sans-serif { + font-weight: lighter; +} + +#font-type-buttons > li > div { + color: #666; + font-size: 12px; +} + +.toggle-button.on { + background-image: url('chrome://browser/skin/images/reader-toggle-on-icon-mdpi.png'); +} + +.toggle-button { + background-image: url('chrome://browser/skin/images/reader-toggle-off-icon-mdpi.png'); +} + +.share-button { + background-image: url('chrome://browser/skin/images/reader-share-icon-mdpi.png'); +} + +.style-button { + background-image: url('chrome://browser/skin/images/reader-style-icon-mdpi.png'); +} + +@media screen and (min-resolution: 1.25dppx) { + .dropdown-arrow { + background-image: url('chrome://browser/skin/images/reader-dropdown-arrow-hdpi.png'); + } + + .step-control > .plus-button { + background-image: url('chrome://browser/skin/images/reader-plus-icon-hdpi.png'); + } + + .step-control > .minus-button { + background-image: url('chrome://browser/skin/images/reader-minus-icon-hdpi.png'); + } + + .toggle-button.on { + background-image: url('chrome://browser/skin/images/reader-toggle-on-icon-hdpi.png'); + } + + .toggle-button { + background-image: url('chrome://browser/skin/images/reader-toggle-off-icon-hdpi.png'); + } + + .share-button { + background-image: url('chrome://browser/skin/images/reader-share-icon-hdpi.png'); + } + + .style-button { + background-image: url('chrome://browser/skin/images/reader-style-icon-hdpi.png'); + } +} + +@media screen and (min-resolution: 2dppx) { + .dropdown-arrow { + background-image: url('chrome://browser/skin/images/reader-dropdown-arrow-xhdpi.png'); + } + + .step-control > .plus-button { + background-image: url('chrome://browser/skin/images/reader-plus-icon-xhdpi.png'); + } + + .step-control > .minus-button { + background-image: url('chrome://browser/skin/images/reader-minus-icon-xhdpi.png'); + } + + .toggle-button.on { + background-image: url('chrome://browser/skin/images/reader-toggle-on-icon-xhdpi.png'); + } + + .toggle-button { + background-image: url('chrome://browser/skin/images/reader-toggle-off-icon-xhdpi.png'); + } + + .share-button { + background-image: url('chrome://browser/skin/images/reader-share-icon-xhdpi.png'); + } + + .style-button { + background-image: url('chrome://browser/skin/images/reader-style-icon-xhdpi.png'); + } +} + +@media screen and (orientation: portrait) { + .button { + height: 48px; + } +} + +@media screen and (orientation: landscape) { + .button { + height: 40px; + } +} + +@media screen and (min-width: 960px) { + .button { + width: 56px; + height: 56px; + } + + .toolbar > * { + width: 56px; + } +} diff --git a/mobile/ios/Client/Frontend/Reader/Reader.html b/mobile/ios/Client/Frontend/Reader/Reader.html new file mode 100644 index 0000000000..4685716703 --- /dev/null +++ b/mobile/ios/Client/Frontend/Reader/Reader.html @@ -0,0 +1,46 @@ + + + + + + + + + + %READER-TITLE% + + + +
+

%READER-TITLE%

+
%READER-CREDITS%
+
+ +
+ %READER-CONTENT% +
+ +
+ %READER-MESSAGE% +
+ +
    +
  • + +
  • +
+ + + + diff --git a/mobile/ios/Client/Frontend/Reader/ReaderMode.js b/mobile/ios/Client/Frontend/Reader/ReaderMode.js new file mode 100644 index 0000000000..dae82781cf --- /dev/null +++ b/mobile/ios/Client/Frontend/Reader/ReaderMode.js @@ -0,0 +1,222 @@ +/* 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/. */ + +(function() { + "use strict"; + + if (!window.__firefox__) { + Object.defineProperty(window, '__firefox__', { + enumerable: false, + configurable: false, + writable: false, + value: {} + }); + } + + var readabilityResult = null; + var currentStyle = null; + + var readerModeURL = /^http:\/\/localhost:\d+\/reader-mode\/page/; + + var BLOCK_IMAGES_SELECTOR = ".content p > img:only-child, " + + ".content p > a:only-child > img:only-child, " + + ".content .wp-caption img, " + + ".content figure img"; + + function debug(s) { + if (!window.__firefox__.reader.DEBUG) { + return; + } + console.log(s); + } + + function checkReadability() { + if (document.location.href.match(readerModeURL)) { + debug({Type: "ReaderModeStateChange", Value: "Active"}); + webkit.messageHandlers.readerModeMessageHandler.postMessage({Type: "ReaderModeStateChange", Value: "Active"}); + return; + } + + if ((document.location.protocol === "http:" || document.location.protocol === "https:") && document.location.pathname !== "/") { + // Short circuit in case we already ran Readability. This mostly happens when going + // back/forward: the page will be cached and the result will still be there. + if (readabilityResult && readabilityResult.content) { + debug({Type: "ReaderModeStateChange", Value: "Available"}); + webkit.messageHandlers.readerModeMessageHandler.postMessage({Type: "ReaderModeStateChange", Value: "Available"}); + return; + } + + var uri = { + spec: document.location.href, + host: document.location.host, + prePath: document.location.protocol + "//" + document.location.host, // TODO This is incomplete, needs username/password and port + scheme: document.location.protocol.substr(0, document.location.protocol.indexOf(":")), + pathBase: document.location.protocol + "//" + document.location.host + location.pathname.substr(0, location.pathname.lastIndexOf("/") + 1) + } + + // document.cloneNode() can cause the webview to break (bug 1128774). + // Serialize and then parse the document instead. + var docStr = new XMLSerializer().serializeToString(document); + var doc = new DOMParser().parseFromString(docStr, "text/html"); + var readability = new Readability(uri, doc); + readabilityResult = readability.parse(); + + debug({Type: "ReaderModeStateChange", Value: readabilityResult !== null ? "Available" : "Unavailable"}); + webkit.messageHandlers.readerModeMessageHandler.postMessage({Type: "ReaderModeStateChange", Value: readabilityResult !== null ? "Available" : "Unavailable"}); + + return; + } + + debug({Type: "ReaderModeStateChange", Value: "Unavailable"}); + webkit.messageHandlers.readerModeMessageHandler.postMessage({Type: "ReaderModeStateChange", Value: "Unavailable"}); + } + + // Readerize the document. Since we did the actual readerization already in checkReadability, we + // can simply return the results we already have. + function readerize() { + return readabilityResult; + } + + // TODO The following code only makes sense in about:reader context. It may be a good idea to move + // it out of this file and into for example a Reader.js. + + function setStyle(style) { + // Configure the theme (light, dark) + if (currentStyle != null) { + document.body.classList.remove(currentStyle.theme); + } + document.body.classList.add(style.theme); + + // Configure the font size (1-5) + if (currentStyle != null) { + document.body.classList.remove("font-size" + currentStyle.fontSize); + } + document.body.classList.add("font-size" + style.fontSize); + + // Configure the font type + if (currentStyle != null) { + document.body.classList.remove(currentStyle.fontType); + } + document.body.classList.add(style.fontType); + + // Remember the style + currentStyle = style; + } + + function updateImageMargins() { + var contentElement = document.getElementById('reader-content'); + + var windowWidth = window.innerWidth; + var contentWidth = contentElement.offsetWidth; + var maxWidthStyle = windowWidth + "px !important"; + + var setImageMargins = function(img) { + if (!img._originalWidth) { + img._originalWidth = img.offsetWidth; + } + + var imgWidth = img._originalWidth; + + // If the image is taking more than half of the screen, just make + // it fill edge-to-edge. + if (imgWidth < contentWidth && imgWidth > windowWidth * 0.55) { + imgWidth = windowWidth; + } + + var sideMargin = Math.max((contentWidth - windowWidth) / 2, (contentWidth - imgWidth) / 2); + + var imageStyle = sideMargin + "px !important"; + var widthStyle = imgWidth + "px !important"; + + var cssText = "max-width: " + maxWidthStyle + ";" + + "width: " + widthStyle + ";" + + "margin-left: " + imageStyle + ";" + + "margin-right: " + imageStyle + ";"; + + img.style.cssText = cssText; + } + + var imgs = document.querySelectorAll(BLOCK_IMAGES_SELECTOR); + for (var i = imgs.length; --i >= 0;) { + var img = imgs[i]; + if (img.width > 0) { + setImageMargins(img); + } else { + img.onload = function() { + setImageMargins(img); + } + } + } + } + + function showContent() { + // Make the reader visible + var messageElement = document.getElementById('reader-message'); + messageElement.style.display = "none"; + var headerElement = document.getElementById('reader-header'); + headerElement.style.display = "block" + var contentElement = document.getElementById('reader-content'); + contentElement.style.display = "block"; + } + + function configureReader() { + // Configure the reader with the initial style that was injected in the page. + var style = JSON.parse(document.body.getAttribute("data-readerStyle")); + setStyle(style); + + // The order here is important. Because updateImageMargins depends on contentElement.offsetWidth which + // will not be set until contentElement is visible. If this leads to annoying content reflowing then we + // need to look at an alternative way to do + showContent(); + updateImageMargins(); + } + + Object.defineProperty(window.__firefox__, 'reader', { + enumerable: false, + configurable: false, + writable: false, + value: { + // If this is http or https content, and not an index page, then try to run Readability. If anything + // fails, the app will never be notified and we don't show the button. That is ok for now since there + // is no error feedback possible anyway. + DEBUG: false + } + }); + + Object.defineProperty(window.__firefox__.reader, 'checkReadability', { + enumerable: false, + configurable: false, + writable: false, + value: checkReadability + }); + + Object.defineProperty(window.__firefox__.reader, 'readerize', { + enumerable: false, + configurable: false, + writable: false, + value: readerize + }); + + Object.defineProperty(window.__firefox__.reader, 'setStyle', { + enumerable: false, + configurable: false, + writable: false, + value: setStyle + }); + + window.addEventListener('load', function(event) { + // If this is an about:reader page that we are loading, apply the initial style to the page. + if (document.location.href.match(readerModeURL)) { + configureReader(); + } + }); + + + window.addEventListener('pageshow', function(event) { + // If this is an about:reader page that we are showing, fire an event to the native code + if (document.location.href.match(readerModeURL)) { + webkit.messageHandlers.readerModeMessageHandler.postMessage({Type: "ReaderPageEvent", Value: "PageShow"}); + } + }); +})(); diff --git a/mobile/ios/Client/Frontend/Reader/ReaderMode.swift b/mobile/ios/Client/Frontend/Reader/ReaderMode.swift new file mode 100644 index 0000000000..5efa3a71a2 --- /dev/null +++ b/mobile/ios/Client/Frontend/Reader/ReaderMode.swift @@ -0,0 +1,296 @@ +/* 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 WebKit +import SwiftyJSON + +let ReaderModeProfileKeyStyle = "readermode.style" + +enum ReaderModeMessageType: String { + case stateChange = "ReaderModeStateChange" + case pageEvent = "ReaderPageEvent" +} + +enum ReaderPageEvent: String { + case pageShow = "PageShow" +} + +enum ReaderModeState: String { + case available = "Available" + case unavailable = "Unavailable" + case active = "Active" +} + +enum ReaderModeTheme: String { + case light = "light" + case dark = "dark" + case sepia = "sepia" +} + +enum ReaderModeFontType: String { + case serif = "serif" + case sansSerif = "sans-serif" +} + +enum ReaderModeFontSize: Int { + case size1 = 1 + case size2 = 2 + case size3 = 3 + case size4 = 4 + case size5 = 5 + case size6 = 6 + case size7 = 7 + case size8 = 8 + case size9 = 9 + case size10 = 10 + case size11 = 11 + case size12 = 12 + case size13 = 13 + + func isSmallest() -> Bool { + return self == ReaderModeFontSize.size1 + } + + func smaller() -> ReaderModeFontSize { + if isSmallest() { + return self + } else { + return ReaderModeFontSize(rawValue: self.rawValue - 1)! + } + } + + func isLargest() -> Bool { + return self == ReaderModeFontSize.size13 + } + + static var defaultSize: ReaderModeFontSize { + switch UIApplication.shared.preferredContentSizeCategory { + case UIContentSizeCategory.extraSmall: + return .size1 + case UIContentSizeCategory.small: + return .size2 + case UIContentSizeCategory.medium: + return .size3 + case UIContentSizeCategory.large: + return .size5 + case UIContentSizeCategory.extraLarge: + return .size7 + case UIContentSizeCategory.extraExtraLarge: + return .size9 + case UIContentSizeCategory.extraExtraExtraLarge: + return .size12 + default: + return .size5 + } + } + + func bigger() -> ReaderModeFontSize { + if isLargest() { + return self + } else { + return ReaderModeFontSize(rawValue: self.rawValue + 1)! + } + } +} + +struct ReaderModeStyle { + var theme: ReaderModeTheme + var fontType: ReaderModeFontType + var fontSize: ReaderModeFontSize + + /// Encode the style to a JSON dictionary that can be passed to ReaderMode.js + func encode() -> String { + return JSON(["theme": theme.rawValue, "fontType": fontType.rawValue, "fontSize": fontSize.rawValue]).stringValue() ?? "" + } + + /// Encode the style to a dictionary that can be stored in the profile + func encodeAsDictionary() -> [String: Any] { + return ["theme": theme.rawValue, "fontType": fontType.rawValue, "fontSize": fontSize.rawValue] + } + + init(theme: ReaderModeTheme, fontType: ReaderModeFontType, fontSize: ReaderModeFontSize) { + self.theme = theme + self.fontType = fontType + self.fontSize = fontSize + } + + /// Initialize the style from a dictionary, taken from the profile. Returns nil if the object cannot be decoded. + init?(dict: [String: Any]) { + let themeRawValue = dict["theme"] as? String + let fontTypeRawValue = dict["fontType"] as? String + let fontSizeRawValue = dict["fontSize"] as? Int + if themeRawValue == nil || fontTypeRawValue == nil || fontSizeRawValue == nil { + return nil + } + + let theme = ReaderModeTheme(rawValue: themeRawValue!) + let fontType = ReaderModeFontType(rawValue: fontTypeRawValue!) + let fontSize = ReaderModeFontSize(rawValue: fontSizeRawValue!) + if theme == nil || fontType == nil || fontSize == nil { + return nil + } + + self.theme = theme! + self.fontType = fontType! + self.fontSize = fontSize! + } +} + +let DefaultReaderModeStyle = ReaderModeStyle(theme: .light, fontType: .sansSerif, fontSize: ReaderModeFontSize.defaultSize) + +/// This struct captures the response from the Readability.js code. +struct ReadabilityResult { + var domain = "" + var url = "" + var content = "" + var title = "" + var credits = "" + + init?(object: AnyObject?) { + if let dict = object as? NSDictionary { + if let uri = dict["uri"] as? NSDictionary { + if let url = uri["spec"] as? String { + self.url = url + } + if let host = uri["host"] as? String { + self.domain = host + } + } + if let content = dict["content"] as? String { + self.content = content + } + if let title = dict["title"] as? String { + self.title = title + } + if let credits = dict["byline"] as? String { + self.credits = credits + } + } else { + return nil + } + } + + /// Initialize from a JSON encoded string + init?(string: String) { + let object = JSON(parseJSON: string) + let domain = object["domain"].string + let url = object["url"].string + let content = object["content"].string + let title = object["title"].string + let credits = object["credits"].string + + if domain == nil || url == nil || content == nil || title == nil || credits == nil { + return nil + } + + self.domain = domain! + self.url = url! + self.content = content! + self.title = title! + self.credits = credits! + } + + /// Encode to a dictionary, which can then for example be json encoded + func encode() -> [String: Any] { + return ["domain": domain, "url": url, "content": content, "title": title, "credits": credits] + } + + /// Encode to a JSON encoded string + func encode() -> String { + let dict: [String: Any] = self.encode() + return JSON(object: dict).stringValue()! + } +} + +/// Delegate that contains callbacks that we have added on top of the built-in WKWebViewDelegate +protocol ReaderModeDelegate { + func readerMode(_ readerMode: ReaderMode, didChangeReaderModeState state: ReaderModeState, forTab tab: Tab) + func readerMode(_ readerMode: ReaderMode, didDisplayReaderizedContentForTab tab: Tab) +} + +let ReaderModeNamespace = "window.__firefox__.reader" + +class ReaderMode: TabContentScript { + var delegate: ReaderModeDelegate? + + fileprivate weak var tab: Tab? + var state: ReaderModeState = ReaderModeState.unavailable + fileprivate var originalURL: URL? + + class func name() -> String { + return "ReaderMode" + } + + required init(tab: Tab) { + self.tab = tab + + // This is a WKUserScript at the moment because webView.evaluateJavaScript() fails with an unspecified error. Possibly script size related. + if let path = Bundle.main.path(forResource: "Readability", ofType: "js") { + if let source = try? NSString(contentsOfFile: path, encoding: String.Encoding.utf8.rawValue) as String { + let userScript = WKUserScript(source: source, injectionTime: WKUserScriptInjectionTime.atDocumentEnd, forMainFrameOnly: true) + tab.webView!.configuration.userContentController.addUserScript(userScript) + } + } + + // This is executed after a page has been loaded. It executes Readability and then fires a script message to let us know if the page is compatible with reader mode. + if let path = Bundle.main.path(forResource: "ReaderMode", ofType: "js") { + if let source = try? NSString(contentsOfFile: path, encoding: String.Encoding.utf8.rawValue) as String { + let userScript = WKUserScript(source: source, injectionTime: WKUserScriptInjectionTime.atDocumentEnd, forMainFrameOnly: true) + tab.webView!.configuration.userContentController.addUserScript(userScript) + } + } + } + + func scriptMessageHandlerName() -> String? { + return "readerModeMessageHandler" + } + + fileprivate func handleReaderPageEvent(_ readerPageEvent: ReaderPageEvent) { + switch readerPageEvent { + case .pageShow: + if let tab = tab { + delegate?.readerMode(self, didDisplayReaderizedContentForTab: tab) + } + } + } + + fileprivate func handleReaderModeStateChange(_ state: ReaderModeState) { + self.state = state + guard let tab = tab else { + return + } + delegate?.readerMode(self, didChangeReaderModeState: state, forTab: tab) + } + + func userContentController(_ userContentController: WKUserContentController, didReceiveScriptMessage message: WKScriptMessage) { + if let msg = message.body as? Dictionary { + if let messageType = ReaderModeMessageType(rawValue: msg["Type"] ?? "") { + switch messageType { + case .pageEvent: + if let readerPageEvent = ReaderPageEvent(rawValue: msg["Value"] ?? "Invalid") { + handleReaderPageEvent(readerPageEvent) + } + break + case .stateChange: + if let readerModeState = ReaderModeState(rawValue: msg["Value"] ?? "Invalid") { + handleReaderModeStateChange(readerModeState) + } + break + } + } + } + } + + var style: ReaderModeStyle = DefaultReaderModeStyle { + didSet { + if state == ReaderModeState.active { + tab?.webView?.evaluateJavaScript("\(ReaderModeNamespace).setStyle(\(style.encode()))", completionHandler: { (object, error) -> Void in + return + }) + } + } + } +} diff --git a/mobile/ios/Client/Frontend/Reader/ReaderModeCache.swift b/mobile/ios/Client/Frontend/Reader/ReaderModeCache.swift new file mode 100644 index 0000000000..f1285537ca --- /dev/null +++ b/mobile/ios/Client/Frontend/Reader/ReaderModeCache.swift @@ -0,0 +1,142 @@ +/* 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 + +private let DiskReaderModeCacheSharedInstance = DiskReaderModeCache() +private let MemoryReaderModeCacheSharedInstance = MemoryReaderModeCache() + +let ReaderModeCacheErrorDomain = "com.mozilla.client.readermodecache." +enum ReaderModeCacheErrorCode: Int { + case noPathsFound = 0 +} + +// NSObject wrapper around ReadabilityResult Swift struct for adding into the NSCache +private class ReadabilityResultWrapper: NSObject { + let result: ReadabilityResult + + init(readabilityResult: ReadabilityResult) { + self.result = readabilityResult + super.init() + } +} + +protocol ReaderModeCache { + func put(_ url: URL, _ readabilityResult: ReadabilityResult) throws + + func get(_ url: URL) throws -> ReadabilityResult + + func delete(_ url: URL, error: NSErrorPointer) + + func contains(_ url: URL) -> Bool +} + +/// A non-persistent cache for readerized content for times when you don't want to write reader data to disk. +/// For example, when the user is in a private tab, we want to make sure that we leave no trace on the file system +class MemoryReaderModeCache: ReaderModeCache { + var cache: NSCache + + init(cache: NSCache = NSCache()) { + self.cache = cache + } + + class var sharedInstance: ReaderModeCache { + return MemoryReaderModeCacheSharedInstance + } + + func put(_ url: URL, _ readabilityResult: ReadabilityResult) throws { + cache.setObject(ReadabilityResultWrapper(readabilityResult: readabilityResult), forKey: url as AnyObject) + } + + func get(_ url: URL) throws -> ReadabilityResult { + guard let resultWrapper = cache.object(forKey: url as AnyObject) as? ReadabilityResultWrapper else { + throw NSError(domain: ReaderModeCacheErrorDomain, code: ReaderModeCacheErrorCode.noPathsFound.rawValue, userInfo: nil) + } + return resultWrapper.result + } + + func delete(_ url: URL, error: NSErrorPointer) { + cache.removeObject(forKey: url as AnyObject) + } + + func contains(_ url: URL) -> Bool { + return cache.object(forKey: url as AnyObject) != nil + } +} + +/// Really basic persistent cache to store readerized content. Has a simple hashed structure +/// to avoid storing many items in the same directory. +/// +/// This currently lives in ~/Library/Caches so that the data can be pruned in case the OS needs +/// more space. Whether that is a good idea or not is not sure. We have a bug on file to investigate +/// and improve at a later time. +class DiskReaderModeCache: ReaderModeCache { + class var sharedInstance: ReaderModeCache { + return DiskReaderModeCacheSharedInstance + } + + func put(_ url: URL, _ readabilityResult: ReadabilityResult) throws { + guard let (cacheDirectoryPath, contentFilePath) = cachePathsForURL(url) else { + throw NSError(domain: ReaderModeCacheErrorDomain, code: ReaderModeCacheErrorCode.noPathsFound.rawValue, userInfo: nil) + } + + try FileManager.default.createDirectory(atPath: cacheDirectoryPath, withIntermediateDirectories: true, attributes: nil) + let string: String = readabilityResult.encode() + try string.write(toFile: contentFilePath, atomically: true, encoding: String.Encoding(rawValue: String.Encoding.utf8.rawValue)) + return + } + + func get(_ url: URL) throws -> ReadabilityResult { + if let (_, contentFilePath) = cachePathsForURL(url), FileManager.default.fileExists(atPath: contentFilePath) { + let string = try NSString(contentsOfFile: contentFilePath, encoding: String.Encoding.utf8.rawValue) + if let value = ReadabilityResult(string: string as String) { + return value + } + } + + throw NSError(domain: ReaderModeCacheErrorDomain, code: ReaderModeCacheErrorCode.noPathsFound.rawValue, userInfo: nil) + } + + func delete(_ url: URL, error: NSErrorPointer) { + guard let (cacheDirectoryPath, _) = cachePathsForURL(url) else { return } + + if FileManager.default.fileExists(atPath: cacheDirectoryPath) { + do { + try FileManager.default.removeItem(atPath: cacheDirectoryPath) + } catch let error1 as NSError { + error?.pointee = error1 + } + } + } + + func contains(_ url: URL) -> Bool { + if let (_, contentFilePath) = cachePathsForURL(url), FileManager.default.fileExists(atPath: contentFilePath) { + return true + } + + return false + } + + fileprivate func cachePathsForURL(_ url: URL) -> (cacheDirectoryPath: String, contentFilePath: String)? { + let paths = NSSearchPathForDirectoriesInDomains(FileManager.SearchPathDirectory.cachesDirectory, FileManager.SearchPathDomainMask.userDomainMask, true) + if !paths.isEmpty, let hashedPath = hashedPathForURL(url) { + let cacheDirectoryURL = URL(fileURLWithPath: NSString.path(withComponents: [paths[0], "ReaderView", hashedPath])) + return (cacheDirectoryURL.path, cacheDirectoryURL.appendingPathComponent("content.json").path) + } + + return nil + } + + fileprivate func hashedPathForURL(_ url: URL) -> String? { + guard let hash = hashForURL(url) else { return nil } + + return NSString.path(withComponents: [hash.substring(with: NSRange(location: 0, length: 2)), hash.substring(with: NSRange(location: 2, length: 2)), hash.substring(from: 4)]) as String + } + + fileprivate func hashForURL(_ url: URL) -> NSString? { + guard let data = url.absoluteString.data(using: String.Encoding.utf8) else { return nil } + + return data.sha1.hexEncodedString as NSString? + } +} diff --git a/mobile/ios/Client/Frontend/Reader/ReaderModeHandlers.swift b/mobile/ios/Client/Frontend/Reader/ReaderModeHandlers.swift new file mode 100644 index 0000000000..e756aa1e1a --- /dev/null +++ b/mobile/ios/Client/Frontend/Reader/ReaderModeHandlers.swift @@ -0,0 +1,81 @@ +/* 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 GCDWebServers + +struct ReaderModeHandlers { + static var readerModeCache: ReaderModeCache = DiskReaderModeCache.sharedInstance + + static func register(_ webServer: WebServer, profile: Profile) { + // Register our fonts and css, which we want to expose to web content that we present in the WebView + webServer.registerMainBundleResourcesOfType("ttf", module: "reader-mode/fonts") + webServer.registerMainBundleResource("Reader.css", module: "reader-mode/styles") + + // Register a handler that simply lets us know if a document is in the cache or not. This is called from the + // reader view interstitial page to find out when it can stop showing the 'Loading...' page and instead load + // the readerized content. + webServer.registerHandlerForMethod("GET", module: "reader-mode", resource: "page-exists") { (request: GCDWebServerRequest?) -> GCDWebServerResponse! in + guard let stringURL = request?.query["url"] as? String, + let url = URL(string: stringURL) else { + return GCDWebServerResponse(statusCode: 500) + } + + let status = readerModeCache.contains(url) ? 200 : 404 + return GCDWebServerResponse(statusCode: status) + } + + // Register the handler that accepts /reader-mode/page?url=http://www.example.com requests. + webServer.registerHandlerForMethod("GET", module: "reader-mode", resource: "page") { (request: GCDWebServerRequest?) -> GCDWebServerResponse! in + if let url = request?.query["url"] as? String { + if let url = URL(string: url), url.isWebPage() { + do { + let readabilityResult = try readerModeCache.get(url) + // We have this page in our cache, so we can display it. Just grab the correct style from the + // profile and then generate HTML from the Readability results. + var readerModeStyle = DefaultReaderModeStyle + if let dict = profile.prefs.dictionaryForKey(ReaderModeProfileKeyStyle) { + if let style = ReaderModeStyle(dict: dict) { + readerModeStyle = style + } + } + if let html = ReaderModeUtils.generateReaderContent(readabilityResult, initialStyle: readerModeStyle), + let response = GCDWebServerDataResponse(html: html) { + // Apply a Content Security Policy that disallows everything except images from anywhere and fonts and css from our internal server + response.setValue("default-src 'none'; img-src *; style-src http://localhost:*; font-src http://localhost:*", forAdditionalHeader: "Content-Security-Policy") + return response + } + } catch _ { + // This page has not been converted to reader mode yet. This happens when you for example add an + // item via the app extension and the application has not yet had a change to readerize that + // page in the background. + // + // What we do is simply queue the page in the ReadabilityService and then show our loading + // screen, which will periodically call page-exists to see if the readerized content has + // become available. + ReadabilityService.sharedInstance.process(url, cache: readerModeCache) + if let readerViewLoadingPath = Bundle.main.path(forResource: "ReaderViewLoading", ofType: "html") { + do { + let readerViewLoading = try NSMutableString(contentsOfFile: readerViewLoadingPath, encoding: String.Encoding.utf8.rawValue) + readerViewLoading.replaceOccurrences(of: "%ORIGINAL-URL%", with: url.absoluteString, + options: NSString.CompareOptions.literal, range: NSRange(location: 0, length: readerViewLoading.length)) + readerViewLoading.replaceOccurrences(of: "%LOADING-TEXT%", with: NSLocalizedString("Loading content…", comment: "Message displayed when the reader mode page is loading. This message will appear only when sharing to Firefox reader mode from another app."), + options: NSString.CompareOptions.literal, range: NSRange(location: 0, length: readerViewLoading.length)) + readerViewLoading.replaceOccurrences(of: "%LOADING-FAILED-TEXT%", with: NSLocalizedString("The page could not be displayed in Reader View.", comment: "Message displayed when the reader mode page could not be loaded. This message will appear only when sharing to Firefox reader mode from another app."), + options: NSString.CompareOptions.literal, range: NSRange(location: 0, length: readerViewLoading.length)) + readerViewLoading.replaceOccurrences(of: "%LOAD-ORIGINAL-TEXT%", with: NSLocalizedString("Load original page", comment: "Link for going to the non-reader page when the reader view could not be loaded. This message will appear only when sharing to Firefox reader mode from another app."), + options: NSString.CompareOptions.literal, range: NSRange(location: 0, length: readerViewLoading.length)) + return GCDWebServerDataResponse(html: readerViewLoading as String) + } catch _ { + } + } + } + } + } + + let errorString = NSLocalizedString("There was an error converting the page", comment: "Error displayed when reader mode cannot be enabled") + return GCDWebServerDataResponse(html: errorString) // TODO Needs a proper error page + } + } +} diff --git a/mobile/ios/Client/Frontend/Reader/ReaderModeStyleViewController.swift b/mobile/ios/Client/Frontend/Reader/ReaderModeStyleViewController.swift new file mode 100644 index 0000000000..bd169c9b17 --- /dev/null +++ b/mobile/ios/Client/Frontend/Reader/ReaderModeStyleViewController.swift @@ -0,0 +1,379 @@ +/* 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 UIKit +import Shared + +private struct ReaderModeStyleViewControllerUX { + static let RowHeight = 50 + + static let Width = 270 + static let Height = 4 * RowHeight + + static let FontTypeRowBackground = UIColor(rgb: 0xfbfbfb) + + static let FontTypeTitleSelectedColor = UIColor(rgb: 0x333333) + static let FontTypeTitleNormalColor = UIColor.lightGray // TODO THis needs to be 44% of 0x333333 + + static let FontSizeRowBackground = UIColor(rgb: 0xf4f4f4) + static let FontSizeLabelColor = UIColor(rgb: 0x333333) + static let FontSizeButtonTextColorEnabled = UIColor(rgb: 0x333333) + static let FontSizeButtonTextColorDisabled = UIColor.lightGray // TODO THis needs to be 44% of 0x333333 + + static let ThemeRowBackgroundColor = UIColor.white + static let ThemeTitleColorLight = UIColor(rgb: 0x333333) + static let ThemeTitleColorDark = UIColor.white + static let ThemeTitleColorSepia = UIColor(rgb: 0x333333) + static let ThemeBackgroundColorLight = UIColor.white + static let ThemeBackgroundColorDark = UIColor(rgb: 0x333333) + static let ThemeBackgroundColorSepia = UIColor(rgb: 0xF0E6DC) + + static let BrightnessRowBackground = UIColor(rgb: 0xf4f4f4) + static let BrightnessSliderTintColor = UIColor(rgb: 0xe66000) + static let BrightnessSliderWidth = 140 + static let BrightnessIconOffset = 10 +} + +// MARK: - + +protocol ReaderModeStyleViewControllerDelegate { + func readerModeStyleViewController(_ readerModeStyleViewController: ReaderModeStyleViewController, didConfigureStyle style: ReaderModeStyle) +} + +// MARK: - + +class ReaderModeStyleViewController: UIViewController { + var delegate: ReaderModeStyleViewControllerDelegate? + var readerModeStyle: ReaderModeStyle = DefaultReaderModeStyle + + fileprivate var fontTypeButtons: [FontTypeButton]! + fileprivate var fontSizeLabel: FontSizeLabel! + fileprivate var fontSizeButtons: [FontSizeButton]! + fileprivate var themeButtons: [ThemeButton]! + + override func viewDidLoad() { + // Our preferred content size has a fixed width and height based on the rows + padding + + preferredContentSize = CGSize(width: ReaderModeStyleViewControllerUX.Width, height: ReaderModeStyleViewControllerUX.Height) + + popoverPresentationController?.backgroundColor = ReaderModeStyleViewControllerUX.FontTypeRowBackground + + // Font type row + + let fontTypeRow = UIView() + view.addSubview(fontTypeRow) + fontTypeRow.backgroundColor = ReaderModeStyleViewControllerUX.FontTypeRowBackground + + fontTypeRow.snp.makeConstraints { (make) -> Void in + make.top.equalTo(self.view) + make.left.right.equalTo(self.view) + make.height.equalTo(ReaderModeStyleViewControllerUX.RowHeight) + } + + fontTypeButtons = [ + FontTypeButton(fontType: ReaderModeFontType.sansSerif), + FontTypeButton(fontType: ReaderModeFontType.serif) + ] + + setupButtons(fontTypeButtons, inRow: fontTypeRow, action: #selector(ReaderModeStyleViewController.SELchangeFontType(_:))) + + // Font size row + + let fontSizeRow = UIView() + view.addSubview(fontSizeRow) + fontSizeRow.backgroundColor = ReaderModeStyleViewControllerUX.FontSizeRowBackground + + fontSizeRow.snp.makeConstraints { (make) -> Void in + make.top.equalTo(fontTypeRow.snp.bottom) + make.left.right.equalTo(self.view) + make.height.equalTo(ReaderModeStyleViewControllerUX.RowHeight) + } + + fontSizeLabel = FontSizeLabel() + fontSizeRow.addSubview(fontSizeLabel) + + fontSizeLabel.snp.makeConstraints { (make) -> Void in + make.center.equalTo(fontSizeRow) + return + } + + fontSizeButtons = [ + FontSizeButton(fontSizeAction: FontSizeAction.smaller), + FontSizeButton(fontSizeAction: FontSizeAction.reset), + FontSizeButton(fontSizeAction: FontSizeAction.bigger) + ] + + setupButtons(fontSizeButtons, inRow: fontSizeRow, action: #selector(ReaderModeStyleViewController.SELchangeFontSize(_:))) + + // Theme row + + let themeRow = UIView() + view.addSubview(themeRow) + + themeRow.snp.makeConstraints { (make) -> Void in + make.top.equalTo(fontSizeRow.snp.bottom) + make.left.right.equalTo(self.view) + make.height.equalTo(ReaderModeStyleViewControllerUX.RowHeight) + } + + themeButtons = [ + ThemeButton(theme: ReaderModeTheme.light), + ThemeButton(theme: ReaderModeTheme.dark), + ThemeButton(theme: ReaderModeTheme.sepia) + ] + + setupButtons(themeButtons, inRow: themeRow, action: #selector(ReaderModeStyleViewController.SELchangeTheme(_:))) + + // Brightness row + + let brightnessRow = UIView() + view.addSubview(brightnessRow) + brightnessRow.backgroundColor = ReaderModeStyleViewControllerUX.BrightnessRowBackground + + brightnessRow.snp.makeConstraints { (make) -> Void in + make.top.equalTo(themeRow.snp.bottom) + make.left.right.equalTo(self.view) + make.height.equalTo(ReaderModeStyleViewControllerUX.RowHeight) + } + + let slider = UISlider() + brightnessRow.addSubview(slider) + slider.accessibilityLabel = NSLocalizedString("Brightness", comment: "Accessibility label for brightness adjustment slider in Reader Mode display settings") + slider.tintColor = ReaderModeStyleViewControllerUX.BrightnessSliderTintColor + slider.addTarget(self, action: #selector(ReaderModeStyleViewController.SELchangeBrightness(_:)), for: UIControlEvents.valueChanged) + + slider.snp.makeConstraints { make in + make.center.equalTo(brightnessRow) + make.width.equalTo(ReaderModeStyleViewControllerUX.BrightnessSliderWidth) + } + + let brightnessMinImageView = UIImageView(image: UIImage(named: "brightnessMin")) + brightnessRow.addSubview(brightnessMinImageView) + + brightnessMinImageView.snp.makeConstraints { (make) -> Void in + make.centerY.equalTo(slider) + make.right.equalTo(slider.snp.left).offset(-ReaderModeStyleViewControllerUX.BrightnessIconOffset) + } + + let brightnessMaxImageView = UIImageView(image: UIImage(named: "brightnessMax")) + brightnessRow.addSubview(brightnessMaxImageView) + + brightnessMaxImageView.snp.makeConstraints { (make) -> Void in + make.centerY.equalTo(slider) + make.left.equalTo(slider.snp.right).offset(ReaderModeStyleViewControllerUX.BrightnessIconOffset) + } + + selectFontType(readerModeStyle.fontType) + updateFontSizeButtons() + selectTheme(readerModeStyle.theme) + slider.value = Float(UIScreen.main.brightness) + } + + /// Setup constraints for a row of buttons. Left to right. They are all given the same width. + fileprivate func setupButtons(_ buttons: [UIButton], inRow row: UIView, action: Selector) { + for (idx, button) in buttons.enumerated() { + row.addSubview(button) + button.addTarget(self, action: action, for: UIControlEvents.touchUpInside) + button.snp.makeConstraints { make in + make.top.equalTo(row.snp.top) + if idx == 0 { + make.left.equalTo(row.snp.left) + } else { + make.left.equalTo(buttons[idx - 1].snp.right) + } + make.bottom.equalTo(row.snp.bottom) + make.width.equalTo(self.preferredContentSize.width / CGFloat(buttons.count)) + } + } + } + + func SELchangeFontType(_ button: FontTypeButton) { + selectFontType(button.fontType) + delegate?.readerModeStyleViewController(self, didConfigureStyle: readerModeStyle) + } + + fileprivate func selectFontType(_ fontType: ReaderModeFontType) { + readerModeStyle.fontType = fontType + for button in fontTypeButtons { + button.isSelected = (button.fontType == fontType) + } + for button in themeButtons { + button.fontType = fontType + } + fontSizeLabel.fontType = fontType + } + + func SELchangeFontSize(_ button: FontSizeButton) { + switch button.fontSizeAction { + case .smaller: + readerModeStyle.fontSize = readerModeStyle.fontSize.smaller() + case .bigger: + readerModeStyle.fontSize = readerModeStyle.fontSize.bigger() + case .reset: + readerModeStyle.fontSize = ReaderModeFontSize.defaultSize + } + updateFontSizeButtons() + delegate?.readerModeStyleViewController(self, didConfigureStyle: readerModeStyle) + } + + fileprivate func updateFontSizeButtons() { + for button in fontSizeButtons { + switch button.fontSizeAction { + case .bigger: + button.isEnabled = !readerModeStyle.fontSize.isLargest() + break + case .smaller: + button.isEnabled = !readerModeStyle.fontSize.isSmallest() + break + case .reset: + break + } + } + } + + func SELchangeTheme(_ button: ThemeButton) { + selectTheme(button.theme) + delegate?.readerModeStyleViewController(self, didConfigureStyle: readerModeStyle) + } + + fileprivate func selectTheme(_ theme: ReaderModeTheme) { + readerModeStyle.theme = theme + } + + func SELchangeBrightness(_ slider: UISlider) { + UIScreen.main.brightness = CGFloat(slider.value) + } +} + +// MARK: - + +class FontTypeButton: UIButton { + var fontType: ReaderModeFontType = .sansSerif + + convenience init(fontType: ReaderModeFontType) { + self.init(frame: CGRect.zero) + self.fontType = fontType + setTitleColor(ReaderModeStyleViewControllerUX.FontTypeTitleSelectedColor, for: UIControlState.selected) + setTitleColor(ReaderModeStyleViewControllerUX.FontTypeTitleNormalColor, for: UIControlState()) + backgroundColor = ReaderModeStyleViewControllerUX.FontTypeRowBackground + accessibilityHint = NSLocalizedString("Changes font type.", comment: "Accessibility hint for the font type buttons in reader mode display settings") + switch fontType { + case .sansSerif: + setTitle(NSLocalizedString("Sans-serif", comment: "Font type setting in the reading view settings"), for: UIControlState()) + let f = UIFont(name: "FiraSans-Book", size: DynamicFontHelper.defaultHelper.ReaderStandardFontSize) + titleLabel?.font = f + case .serif: + setTitle(NSLocalizedString("Serif", comment: "Font type setting in the reading view settings"), for: UIControlState()) + let f = UIFont(name: "Charis SIL", size: DynamicFontHelper.defaultHelper.ReaderStandardFontSize) + titleLabel?.font = f + } + } +} + +// MARK: - + +enum FontSizeAction { + case smaller + case reset + case bigger +} + +class FontSizeButton: UIButton { + var fontSizeAction: FontSizeAction = .bigger + + convenience init(fontSizeAction: FontSizeAction) { + self.init(frame: CGRect.zero) + self.fontSizeAction = fontSizeAction + + setTitleColor(ReaderModeStyleViewControllerUX.FontSizeButtonTextColorEnabled, for: UIControlState.normal) + setTitleColor(ReaderModeStyleViewControllerUX.FontSizeButtonTextColorDisabled, for: UIControlState.disabled) + + switch fontSizeAction { + case .smaller: + let smallerFontLabel = NSLocalizedString("-", comment: "Button for smaller reader font size. Keep this extremely short! This is shown in the reader mode toolbar.") + let smallerFontAccessibilityLabel = NSLocalizedString("Decrease text size", comment: "Accessibility label for button decreasing font size in display settings of reader mode") + setTitle(smallerFontLabel, for: UIControlState()) + accessibilityLabel = smallerFontAccessibilityLabel + case .bigger: + let largerFontLabel = NSLocalizedString("+", comment: "Button for larger reader font size. Keep this extremely short! This is shown in the reader mode toolbar.") + let largerFontAccessibilityLabel = NSLocalizedString("Increase text size", comment: "Accessibility label for button increasing font size in display settings of reader mode") + setTitle(largerFontLabel, for: UIControlState()) + accessibilityLabel = largerFontAccessibilityLabel + case .reset: + accessibilityLabel = Strings.ReaderModeResetFontSizeAccessibilityLabel + } + + // TODO Does this need to change with the selected font type? Not sure if makes sense for just +/- + titleLabel?.font = UIFont(name: "FiraSans-Light", size: DynamicFontHelper.defaultHelper.ReaderBigFontSize) + } +} + +// MARK: - + +class FontSizeLabel: UILabel { + override init(frame: CGRect) { + super.init(frame: frame) + let fontSizeLabel = NSLocalizedString("Aa", comment: "Button for reader mode font size. Keep this extremely short! This is shown in the reader mode toolbar.") + text = fontSizeLabel + isAccessibilityElement = false + } + + required init?(coder aDecoder: NSCoder) { + // TODO + fatalError("init(coder:) has not been implemented") + } + + var fontType: ReaderModeFontType = .sansSerif { + didSet { + switch fontType { + case .sansSerif: + font = UIFont(name: "FiraSans-Book", size: DynamicFontHelper.defaultHelper.ReaderBigFontSize) + case .serif: + font = UIFont(name: "Charis SIL", size: DynamicFontHelper.defaultHelper.ReaderBigFontSize) + } + } + } +} + +// MARK: - + +class ThemeButton: UIButton { + var theme: ReaderModeTheme! + + convenience init(theme: ReaderModeTheme) { + self.init(frame: CGRect.zero) + self.theme = theme + + setTitle(theme.rawValue, for: UIControlState()) + + accessibilityHint = NSLocalizedString("Changes color theme.", comment: "Accessibility hint for the color theme setting buttons in reader mode display settings") + + switch theme { + case .light: + setTitle(NSLocalizedString("Light", comment: "Light theme setting in Reading View settings"), for: UIControlState()) + setTitleColor(ReaderModeStyleViewControllerUX.ThemeTitleColorLight, for: UIControlState.normal) + backgroundColor = ReaderModeStyleViewControllerUX.ThemeBackgroundColorLight + case .dark: + setTitle(NSLocalizedString("Dark", comment: "Dark theme setting in Reading View settings"), for: UIControlState()) + setTitleColor(ReaderModeStyleViewControllerUX.ThemeTitleColorDark, for: UIControlState()) + backgroundColor = ReaderModeStyleViewControllerUX.ThemeBackgroundColorDark + case .sepia: + setTitle(NSLocalizedString("Sepia", comment: "Sepia theme setting in Reading View settings"), for: UIControlState()) + setTitleColor(ReaderModeStyleViewControllerUX.ThemeTitleColorSepia, for: UIControlState.normal) + backgroundColor = ReaderModeStyleViewControllerUX.ThemeBackgroundColorSepia + } + } + + var fontType: ReaderModeFontType = .sansSerif { + didSet { + switch fontType { + case .sansSerif: + titleLabel?.font = UIFont(name: "FiraSans-Book", size: DynamicFontHelper.defaultHelper.ReaderStandardFontSize) + case .serif: + titleLabel?.font = UIFont(name: "Charis SIL", size: DynamicFontHelper.defaultHelper.ReaderStandardFontSize) + } + } + } +} diff --git a/mobile/ios/Client/Frontend/Reader/ReaderModeUtils.swift b/mobile/ios/Client/Frontend/Reader/ReaderModeUtils.swift new file mode 100644 index 0000000000..ebf8419b47 --- /dev/null +++ b/mobile/ios/Client/Frontend/Reader/ReaderModeUtils.swift @@ -0,0 +1,57 @@ +/* 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 + +struct ReaderModeUtils { + + static let DomainPrefixesToSimplify = ["www.", "mobile.", "m.", "blog."] + + static func simplifyDomain(_ domain: String) -> String { + for prefix in DomainPrefixesToSimplify { + if domain.hasPrefix(prefix) { + return domain.substring(from: domain.characters.index(domain.startIndex, offsetBy: prefix.characters.count)) + } + } + return domain + } + + static func generateReaderContent(_ readabilityResult: ReadabilityResult, initialStyle: ReaderModeStyle) -> String? { + if let stylePath = Bundle.main.path(forResource: "Reader", ofType: "css") { + do { + let css = try NSString(contentsOfFile: stylePath, encoding: String.Encoding.utf8.rawValue) + if let tmplPath = Bundle.main.path(forResource: "Reader", ofType: "html") { + do { + let tmpl = try NSMutableString(contentsOfFile: tmplPath, encoding: String.Encoding.utf8.rawValue) + tmpl.replaceOccurrences(of: "%READER-CSS%", with: css as String, + options: NSString.CompareOptions(), range: NSRange(location: 0, length: tmpl.length)) + + tmpl.replaceOccurrences(of: "%READER-STYLE%", with: initialStyle.encode(), + options: NSString.CompareOptions(), range: NSRange(location: 0, length: tmpl.length)) + + tmpl.replaceOccurrences(of: "%READER-DOMAIN%", with: simplifyDomain(readabilityResult.domain), + options: NSString.CompareOptions(), range: NSRange(location: 0, length: tmpl.length)) + + tmpl.replaceOccurrences(of: "%READER-URL%", with: readabilityResult.url, + options: NSString.CompareOptions(), range: NSRange(location: 0, length: tmpl.length)) + + tmpl.replaceOccurrences(of: "%READER-TITLE%", with: readabilityResult.title, + options: NSString.CompareOptions(), range: NSRange(location: 0, length: tmpl.length)) + + tmpl.replaceOccurrences(of: "%READER-CREDITS%", with: readabilityResult.credits, + options: NSString.CompareOptions(), range: NSRange(location: 0, length: tmpl.length)) + + tmpl.replaceOccurrences(of: "%READER-CONTENT%", with: readabilityResult.content, + options: NSString.CompareOptions(), range: NSRange(location: 0, length: tmpl.length)) + + return tmpl as String + } catch _ { + } + } + } catch _ { + } + } + return nil + } +} diff --git a/mobile/ios/Client/Frontend/Reader/ReaderViewLoading.html b/mobile/ios/Client/Frontend/Reader/ReaderViewLoading.html new file mode 100644 index 0000000000..c1e5879cbf --- /dev/null +++ b/mobile/ios/Client/Frontend/Reader/ReaderViewLoading.html @@ -0,0 +1,86 @@ + + + + + + + + + +
+

%LOADING-TEXT%

+ +
+ + + + diff --git a/mobile/ios/Client/Frontend/Settings/AdvanceAccountSettingViewController.swift b/mobile/ios/Client/Frontend/Settings/AdvanceAccountSettingViewController.swift new file mode 100644 index 0000000000..8fac12bc88 --- /dev/null +++ b/mobile/ios/Client/Frontend/Settings/AdvanceAccountSettingViewController.swift @@ -0,0 +1,183 @@ +/* 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 UIKit +import Shared +import SnapKit +import Foundation +import FxA +import Account + +class AdvanceAccountSettingViewController: SettingsTableViewController { + fileprivate let SectionHeaderIdentifier = "SectionHeaderIdentifier" + + fileprivate var customSyncUrl: String? + + override func viewDidLoad() { + super.viewDidLoad() + title = Strings.SettingsAdvanceAccountSectionName + self.customSyncUrl = self.profile.prefs.stringForKey(PrefsKeys.KeyCustomSyncWeb) + } + + func clearCustomAccountPrefs() { + self.profile.prefs.setBool(false, forKey: PrefsKeys.KeyUseCustomSyncService) + self.profile.prefs.setString("", forKey: PrefsKeys.KeyCustomSyncToken) + self.profile.prefs.setString("", forKey: PrefsKeys.KeyCustomSyncProfile) + self.profile.prefs.setString("", forKey: PrefsKeys.KeyCustomSyncOauth) + self.profile.prefs.setString("", forKey: PrefsKeys.KeyCustomSyncAuth) + self.profile.prefs.setString("", forKey: PrefsKeys.KeyCustomSyncWeb) + + // To help prevent the account being in a strange state, we force it to + // log out when user clears their custom server preferences. + self.profile.removeAccount() + } + + func setCustomAccountPrefs(_ data: Data, url: URL) { + guard let settings = (try? JSONSerialization.jsonObject(with: data, options: .allowFragments)) as? [String:Any], + let customSyncToken = settings["sync_tokenserver_base_url"] as? String, + let customSyncProfile = settings["profile_server_base_url"] as? String, + let customSyncOauth = settings["oauth_server_base_url"] as? String, + let customSyncAuth = settings["auth_server_base_url"] as? String else { + return + } + + self.profile.prefs.setBool(true, forKey: PrefsKeys.KeyUseCustomSyncService) + self.profile.prefs.setString(customSyncToken, forKey: PrefsKeys.KeyCustomSyncToken) + self.profile.prefs.setString(customSyncProfile, forKey: PrefsKeys.KeyCustomSyncProfile) + self.profile.prefs.setString(customSyncOauth, forKey: PrefsKeys.KeyCustomSyncOauth) + self.profile.prefs.setString(customSyncAuth, forKey: PrefsKeys.KeyCustomSyncAuth) + self.profile.prefs.setString(url.absoluteString, forKey: PrefsKeys.KeyCustomSyncWeb) + self.profile.removeAccount() + self.displaySuccessAlert() + } + + func setCustomAccountPrefs() { + guard let urlString = self.customSyncUrl, let url = URL(string: urlString) else { + // If the user attempts to set a nil url, clear all the custom service perferences + // and use default FxA servers. + self.displayNoServiceSetAlert() + return + } + + // FxA stores its server configuation under a well-known path. This attempts to download the configuration + // and save it into the users preferences. + let syncConfigureString = urlString + "/.well-known/fxa-client-configuration" + guard let syncConfigureURL = URL(string: syncConfigureString) else { + return + } + + URLSession.shared.dataTask(with: syncConfigureURL, completionHandler: {(data, response, error) in + guard let data = data, error == nil else { + // Something went wrong while downloading or parsing the configuration. + self.displayErrorAlert() + return + } + self.setCustomAccountPrefs(data, url: url) + }).resume() + } + + func displaySuccessAlert() { + let alertController = UIAlertController(title: "", message: Strings.SettingsAdvanceAccountUrlUpdatedAlertMessage, preferredStyle: .alert) + let defaultAction = UIAlertAction(title: Strings.SettingsAdvanceAccountUrlUpdatedAlertOk, style: .default, handler: nil) + alertController.addAction(defaultAction) + self.present(alertController, animated: true) + } + + func displayErrorAlert() { + self.profile.prefs.setBool(false, forKey: PrefsKeys.KeyUseCustomSyncService) + DispatchQueue.main.async { + self.tableView.reloadRows(at: [IndexPath(row: 0, section: 0)], with: UITableViewRowAnimation.automatic) + } + let alertController = UIAlertController(title: Strings.SettingsAdvanceAccountUrlErrorAlertTitle, message: Strings.SettingsAdvanceAccountUrlErrorAlertMessage, preferredStyle: .alert) + let defaultAction = UIAlertAction(title: Strings.SettingsAdvanceAccountUrlErrorAlertOk, style: .default, handler: nil) + alertController.addAction(defaultAction) + self.present(alertController, animated: true) + } + + func displayNoServiceSetAlert() { + self.profile.prefs.setBool(false, forKey: PrefsKeys.KeyUseCustomSyncService) + DispatchQueue.main.async { + self.tableView.reloadRows(at: [IndexPath(row: 0, section: 0)], with: UITableViewRowAnimation.automatic) + } + let alertController = UIAlertController(title: Strings.SettingsAdvanceAccountUrlErrorAlertTitle, message: Strings.SettingsAdvanceAccountEmptyUrlErrorAlertMessage, preferredStyle: .alert) + let defaultAction = UIAlertAction(title: Strings.SettingsAdvanceAccountUrlUpdatedAlertOk, style: .default, handler: nil) + alertController.addAction(defaultAction) + self.present(alertController, animated: true) + } + + override func generateSettings() -> [SettingSection] { + let prefs = profile.prefs + let customSyncSetting = CustomSyncWebPageSetting(prefs: prefs, + prefKey: PrefsKeys.KeyCustomSyncWeb, + placeholder: Strings.SettingsAdvanceAccountUrlPlaceholder, + accessibilityIdentifier: "CustomSyncSetting", + settingDidChange: { fieldText in + self.customSyncUrl = fieldText + if let customSyncUrl = self.customSyncUrl, customSyncUrl.isEmpty { + self.clearCustomAccountPrefs() + return + } + }) + + var basicSettings: [Setting] = [] + basicSettings += [ + CustomSyncEnableSetting( + prefs: prefs, + settingDidChange: { result in + if result == true { + // Reload the table data to ensure that the updated custom url is set + self.tableView?.reloadData() + self.setCustomAccountPrefs() + } + }), + customSyncSetting + ] + + let settings: [SettingSection] = [ + SettingSection(title: NSAttributedString(string: ""), children: basicSettings), + SettingSection(title: NSAttributedString(string: Strings.SettingsAdvanceAccountSectionFooter), children: []) + ] + + return settings + } + + override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { + let headerView = tableView.dequeueReusableHeaderFooterView(withIdentifier: SectionHeaderIdentifier) as! SettingsTableSectionHeaderFooterView + let sectionSetting = settings[section] + headerView.titleLabel.text = sectionSetting.title?.string + + switch section { + // Hide the bottom border for the FxA custom server notes. + case 1: + headerView.titleAlignment = .top + headerView.titleLabel.numberOfLines = 0 + headerView.showBottomBorder = false + default: + return super.tableView(tableView, viewForHeaderInSection: section) + } + return headerView + } +} + +class CustomSyncEnableSetting: BoolSetting { + init(prefs: Prefs, settingDidChange: ((Bool?) -> Void)? = nil) { + super.init( + prefs: prefs, prefKey: PrefsKeys.KeyUseCustomSyncService, defaultValue: false, + attributedTitleText: NSAttributedString(string: Strings.SettingsAdvanceAccountUseCustomAccountsServiceTitle), + settingDidChange: settingDidChange + ) + } +} + +class CustomSyncWebPageSetting: WebPageSetting { + override init(prefs: Prefs, prefKey: String, defaultValue: String? = nil, placeholder: String, accessibilityIdentifier: String, settingDidChange: ((String?) -> Void)? = nil) { + super.init(prefs: prefs, + prefKey: prefKey, + defaultValue: defaultValue, + placeholder: placeholder, + accessibilityIdentifier: accessibilityIdentifier, + settingDidChange: settingDidChange) + textField.clearButtonMode = UITextFieldViewMode.always + } +} diff --git a/mobile/ios/Client/Frontend/Settings/AppSettingsOptions.swift b/mobile/ios/Client/Frontend/Settings/AppSettingsOptions.swift new file mode 100644 index 0000000000..fe90023f33 --- /dev/null +++ b/mobile/ios/Client/Frontend/Settings/AppSettingsOptions.swift @@ -0,0 +1,1092 @@ +/* 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 Account +import SwiftKeychainWrapper +import LocalAuthentication + +// This file contains all of the settings available in the main settings screen of the app. + +private var ShowDebugSettings: Bool = false +private var DebugSettingsClickCount: Int = 0 + +// For great debugging! +class HiddenSetting: Setting { + unowned let settings: SettingsTableViewController + + init(settings: SettingsTableViewController) { + self.settings = settings + super.init(title: nil) + } + + override var hidden: Bool { + return !ShowDebugSettings + } +} + +// Sync setting for connecting a Firefox Account. Shown when we don't have an account. +class ConnectSetting: WithoutAccountSetting { + override var accessoryType: UITableViewCellAccessoryType { return .disclosureIndicator } + + override var title: NSAttributedString? { + return NSAttributedString(string: Strings.FxASignIntoSync, attributes: [NSForegroundColorAttributeName: UIConstants.TableViewRowTextColor]) + } + + override var accessibilityIdentifier: String? { return "SignInToSync" } + + override func onClick(_ navigationController: UINavigationController?) { + let viewController = FxAContentViewController(profile: profile) + viewController.delegate = self + viewController.url = settings.profile.accountConfiguration.signInURL + navigationController?.pushViewController(viewController, animated: true) + } + + override func onConfigureCell(_ cell: UITableViewCell) { + super.onConfigureCell(cell) + + if AppConstants.MOZ_SHOW_FXA_AVATAR { + let image = UIImage(named: "FxA-Default") + cell.imageView?.image = image?.createScaled(CGSize(width: 30, height: 30)) + cell.imageView?.layer.cornerRadius = (cell.imageView?.frame.size.width)! / 2 + cell.imageView?.layer.masksToBounds = true + } + } +} + +// Sync setting for disconnecting a Firefox Account. Shown when we have an account. +class DisconnectSetting: WithAccountSetting { + override var accessoryType: UITableViewCellAccessoryType { return .none } + override var textAlignment: NSTextAlignment { return .center } + + override var title: NSAttributedString? { + return NSAttributedString(string: Strings.SettingsDisconnectSyncButton, attributes: [NSForegroundColorAttributeName: UIConstants.DestructiveRed]) + } + + override var accessibilityIdentifier: String? { return "SignOut" } + + override func onClick(_ navigationController: UINavigationController?) { + let alertController = UIAlertController( + title: Strings.SettingsDisconnectSyncAlertTitle, + message: NSLocalizedString("Firefox will stop syncing with your account, but won’t delete any of your browsing data on this device.", comment: "Text of the 'sign out firefox account' alert"), + preferredStyle: UIAlertControllerStyle.alert) + alertController.addAction( + UIAlertAction(title: NSLocalizedString("Cancel", comment: "Label for Cancel button"), style: .cancel) { (action) in + // Do nothing. + }) + alertController.addAction( + UIAlertAction(title: Strings.SettingsDisconnectDestructiveAction, style: .destructive) { (action) in + FxALoginHelper.sharedInstance.applicationDidDisconnect(UIApplication.shared) + self.settings.settings = self.settings.generateSettings() + self.settings.SELfirefoxAccountDidChange() + LeanPlumClient.shared.set(attributes: [LPAttributeKey.signedInSync: self.profile.hasAccount()]) + }) + navigationController?.present(alertController, animated: true, completion: nil) + } +} + +class SyncNowSetting: WithAccountSetting { + static let NotificationUserInitiatedSyncManually = "NotificationUserInitiatedSyncManually" + let imageView = UIImageView(frame: CGRect(x: 0, y: 0, width: 30, height: 30)) + let syncIconWrapper = UIImage.createWithColor(CGSize(width: 30, height: 30), color: UIColor.clear) + let syncBlueIcon = UIImage(named: "FxA-Sync-Blue")?.createScaled(CGSize(width: 20, height: 20)) + let syncIcon = UIImage(named: "FxA-Sync")?.createScaled(CGSize(width: 20, height: 20)) + + // Animation used to rotate the Sync icon 360 degrees while syncing is in progress. + let continuousRotateAnimation = CABasicAnimation(keyPath: "transform.rotation") + + override init(settings: SettingsTableViewController) { + super.init(settings: settings) + NotificationCenter.default.addObserver(self, selector: #selector(SyncNowSetting.stopRotateSyncIcon), name: NotificationProfileDidFinishSyncing, object: nil) + } + + fileprivate lazy var timestampFormatter: DateFormatter = { + let formatter = DateFormatter() + formatter.dateFormat = "yyyy-MM-dd HH:mm:ss" + return formatter + }() + + fileprivate var syncNowTitle: NSAttributedString { + if !DeviceInfo.hasConnectivity() { + return NSAttributedString( + string: Strings.FxANoInternetConnection, + attributes: [ + NSForegroundColorAttributeName: UIColor.red, + NSFontAttributeName: DynamicFontHelper.defaultHelper.DefaultMediumFont + ] + ) + } + + return NSAttributedString( + string: NSLocalizedString("Sync Now", comment: "Sync Firefox Account"), + attributes: [ + NSForegroundColorAttributeName: self.enabled ? UIConstants.TableViewRowSyncTextColor : UIColor.gray, + NSFontAttributeName: DynamicFontHelper.defaultHelper.DefaultStandardFont + ] + ) + } + + fileprivate let syncingTitle = NSAttributedString(string: Strings.SyncingMessageWithEllipsis, attributes: [NSForegroundColorAttributeName: UIConstants.TableViewRowSyncTextColor, NSFontAttributeName: UIFont.systemFont(ofSize: DynamicFontHelper.defaultHelper.DefaultStandardFontSize, weight: UIFontWeightRegular)]) + + func startRotateSyncIcon() { + DispatchQueue.main.async { + self.imageView.layer.add(self.continuousRotateAnimation, forKey: "rotateKey") + } + } + + func stopRotateSyncIcon() { + DispatchQueue.main.async { + self.imageView.layer.removeAllAnimations() + } + } + + override var accessoryType: UITableViewCellAccessoryType { return .none } + + override var style: UITableViewCellStyle { return .value1 } + + override var image: UIImage? { + if !AppConstants.MOZ_SHOW_FXA_AVATAR { + return nil + } + + guard let syncStatus = profile.syncManager.syncDisplayState else { + return syncIcon + } + + switch syncStatus { + case .inProgress: + return syncBlueIcon + default: + return syncIcon + } + } + + override var title: NSAttributedString? { + guard let syncStatus = profile.syncManager.syncDisplayState else { + return syncNowTitle + } + + switch syncStatus { + case .bad(let message): + guard let message = message else { return syncNowTitle } + return NSAttributedString(string: message, attributes: [NSForegroundColorAttributeName: UIConstants.TableViewRowErrorTextColor, NSFontAttributeName: DynamicFontHelper.defaultHelper.DefaultStandardFont]) + case .warning(let message): + return NSAttributedString(string: message, attributes: [NSForegroundColorAttributeName: UIConstants.TableViewRowWarningTextColor, NSFontAttributeName: DynamicFontHelper.defaultHelper.DefaultStandardFont]) + case .inProgress: + return syncingTitle + default: + return syncNowTitle + } + } + + override var status: NSAttributedString? { + guard let timestamp = profile.syncManager.lastSyncFinishTime else { + return nil + } + + let formattedLabel = timestampFormatter.string(from: Date.fromTimestamp(timestamp)) + let attributedString = NSMutableAttributedString(string: formattedLabel) + let attributes = [NSForegroundColorAttributeName: UIColor.gray, NSFontAttributeName: UIFont.systemFont(ofSize: 12, weight: UIFontWeightRegular)] + let range = NSRange(location: 0, length: attributedString.length) + attributedString.setAttributes(attributes, range: range) + return attributedString + } + + override var enabled: Bool { + if !DeviceInfo.hasConnectivity() { + return false + } + + return profile.hasSyncableAccount() + } + + fileprivate lazy var troubleshootButton: UIButton = { + let troubleshootButton = UIButton(type: UIButtonType.roundedRect) + troubleshootButton.setTitle(Strings.FirefoxSyncTroubleshootTitle, for: .normal) + troubleshootButton.addTarget(self, action: #selector(self.troubleshoot), for: .touchUpInside) + troubleshootButton.tintColor = UIConstants.TableViewRowActionAccessoryColor + troubleshootButton.titleLabel?.font = DynamicFontHelper.defaultHelper.DefaultSmallFont + troubleshootButton.sizeToFit() + return troubleshootButton + }() + + fileprivate lazy var warningIcon: UIImageView = { + let imageView = UIImageView(image: UIImage(named: "AmberCaution")) + imageView.sizeToFit() + return imageView + }() + + fileprivate lazy var errorIcon: UIImageView = { + let imageView = UIImageView(image: UIImage(named: "RedCaution")) + imageView.sizeToFit() + return imageView + }() + + fileprivate let syncSUMOURL = SupportUtils.URLForTopic("sync-status-ios") + + @objc fileprivate func troubleshoot() { + let viewController = SettingsContentViewController() + viewController.url = syncSUMOURL + settings.navigationController?.pushViewController(viewController, animated: true) + } + + override func onConfigureCell(_ cell: UITableViewCell) { + cell.textLabel?.attributedText = title + cell.textLabel?.numberOfLines = 0 + cell.textLabel?.lineBreakMode = .byWordWrapping + if let syncStatus = profile.syncManager.syncDisplayState { + switch syncStatus { + case .bad(let message): + if let _ = message { + // add the red warning symbol + // add a link to the MANA page + cell.detailTextLabel?.attributedText = nil + cell.accessoryView = troubleshootButton + addIcon(errorIcon, toCell: cell) + } else { + cell.detailTextLabel?.attributedText = status + cell.accessoryView = nil + } + case .warning(_): + // add the amber warning symbol + // add a link to the MANA page + cell.detailTextLabel?.attributedText = nil + cell.accessoryView = troubleshootButton + addIcon(warningIcon, toCell: cell) + case .good: + cell.detailTextLabel?.attributedText = status + fallthrough + default: + cell.accessoryView = nil + } + } else { + cell.accessoryView = nil + } + cell.accessoryType = accessoryType + cell.isUserInteractionEnabled = !profile.syncManager.isSyncing && DeviceInfo.hasConnectivity() + + if AppConstants.MOZ_SHOW_FXA_AVATAR { + // Animation that loops continously until stopped + continuousRotateAnimation.fromValue = 0.0 + continuousRotateAnimation.toValue = CGFloat(Double.pi) + continuousRotateAnimation.isRemovedOnCompletion = true + continuousRotateAnimation.duration = 0.5 + continuousRotateAnimation.repeatCount = Float.infinity + + // To ensure sync icon is aligned properly with user's avatar, an image is created with proper + // dimensions and color, then the scaled sync icon is added as a subview. + imageView.contentMode = .center + imageView.image = image + + cell.imageView?.subviews.forEach({ $0.removeFromSuperview() }) + cell.imageView?.image = syncIconWrapper + cell.imageView?.addSubview(imageView) + + if let syncStatus = profile.syncManager.syncDisplayState { + switch syncStatus { + case .inProgress: + self.startRotateSyncIcon() + default: + self.stopRotateSyncIcon() + } + } + } + } + + fileprivate func addIcon(_ image: UIImageView, toCell cell: UITableViewCell) { + cell.contentView.addSubview(image) + + cell.textLabel?.snp.updateConstraints { make in + make.leading.equalTo(image.snp.trailing).offset(5) + make.trailing.lessThanOrEqualTo(cell.contentView) + make.centerY.equalTo(cell.contentView) + } + + image.snp.makeConstraints { make in + make.leading.equalTo(cell.contentView).offset(17) + make.top.equalTo(cell.textLabel!).offset(2) + } + } + + override func onClick(_ navigationController: UINavigationController?) { + if !DeviceInfo.hasConnectivity() { + return + } + + NotificationCenter.default.post(name: Notification.Name(rawValue: SyncNowSetting.NotificationUserInitiatedSyncManually), object: nil) + profile.syncManager.syncEverything(why: .syncNow) + } +} + +// Sync setting that shows the current Firefox Account status. +class AccountStatusSetting: WithAccountSetting { + override init(settings: SettingsTableViewController) { + super.init(settings: settings) + NotificationCenter.default.addObserver(self, selector: #selector(AccountStatusSetting.updateAccount(notification:)), name: NotificationFirefoxAccountProfileChanged, object: nil) + } + + func updateAccount(notification: Notification) { + DispatchQueue.main.async { + self.settings.tableView.reloadData() + } + } + + override var image: UIImage? { + if !AppConstants.MOZ_SHOW_FXA_AVATAR { + return nil + } + + if let image = profile.getAccount()?.fxaProfile?.avatar.image { + return image.createScaled(CGSize(width: 30, height: 30)) + } + + let image = UIImage(named: "placeholder-avatar") + return image?.createScaled(CGSize(width: 30, height: 30)) + } + + override var accessoryType: UITableViewCellAccessoryType { + if let account = profile.getAccount() { + switch account.actionNeeded { + case .needsVerification: + // We link to the resend verification email page. + return .disclosureIndicator + case .needsPassword: + // We link to the re-enter password page. + return .disclosureIndicator + case .none: + // We link to FxA web /settings. + return .disclosureIndicator + case .needsUpgrade: + // In future, we'll want to link to an upgrade page. + return .none + } + } + return .disclosureIndicator + } + + override var title: NSAttributedString? { + if let account = profile.getAccount() { + + if let displayName = account.fxaProfile?.displayName { + return NSAttributedString(string: displayName, attributes: [NSFontAttributeName: DynamicFontHelper.defaultHelper.DefaultStandardFontBold, NSForegroundColorAttributeName: UIConstants.TableViewRowSyncTextColor]) + } + + if let email = account.fxaProfile?.email { + return NSAttributedString(string: email, attributes: [NSFontAttributeName: DynamicFontHelper.defaultHelper.DefaultStandardFontBold, NSForegroundColorAttributeName: UIConstants.TableViewRowSyncTextColor]) + } + + return NSAttributedString(string: account.email, attributes: [NSFontAttributeName: DynamicFontHelper.defaultHelper.DefaultStandardFontBold, NSForegroundColorAttributeName: UIConstants.TableViewRowSyncTextColor]) + } + return nil + } + + override var status: NSAttributedString? { + if let account = profile.getAccount() { + var string: String + + switch account.actionNeeded { + case .none: + return nil + case .needsVerification: + string = NSLocalizedString("Verify your email address.", comment: "Text message in the settings table view") + break + case .needsPassword: + string = NSLocalizedString("Enter your password to connect.", comment: "Text message in the settings table view") + break + case .needsUpgrade: + string = NSLocalizedString("Upgrade Firefox to connect.", comment: "Text message in the settings table view") + break + } + + let orange = UIColor(red: 255.0 / 255, green: 149.0 / 255, blue: 0.0 / 255, alpha: 1) + let range = NSRange(location: 0, length: string.characters.count) + let attrs = [NSForegroundColorAttributeName: orange] + let res = NSMutableAttributedString(string: string) + res.setAttributes(attrs, range: range) + return res + } + return nil + } + + override func onClick(_ navigationController: UINavigationController?) { + let viewController = FxAContentViewController(profile: profile) + viewController.delegate = self + + if let account = profile.getAccount() { + switch account.actionNeeded { + case .none, .needsVerification: + var cs = URLComponents(url: account.configuration.settingsURL, resolvingAgainstBaseURL: false) + cs?.queryItems?.append(URLQueryItem(name: "email", value: account.email)) + if let url = try? cs?.asURL() { + viewController.url = url + } + case .needsPassword: + var cs = URLComponents(url: account.configuration.forceAuthURL, resolvingAgainstBaseURL: false) + cs?.queryItems?.append(URLQueryItem(name: "email", value: account.email)) + if let url = try? cs?.asURL() { + viewController.url = url + } + case .needsUpgrade: + // In future, we'll want to link to an upgrade page. + return + } + } + navigationController?.pushViewController(viewController, animated: true) + } + + override func onConfigureCell(_ cell: UITableViewCell) { + super.onConfigureCell(cell) + + if AppConstants.MOZ_SHOW_FXA_AVATAR { + cell.imageView?.subviews.forEach({ $0.removeFromSuperview() }) + cell.imageView?.frame = CGRect(x: 0, y: 0, width: 30, height: 30) + cell.imageView?.layer.cornerRadius = (cell.imageView?.frame.height)! / 2 + cell.imageView?.layer.masksToBounds = true + cell.imageView?.image = image + } + } +} + +// For great debugging! +class RequirePasswordDebugSetting: WithAccountSetting { + override var hidden: Bool { + if !ShowDebugSettings { + return true + } + if let account = profile.getAccount(), account.actionNeeded != FxAActionNeeded.needsPassword { + return false + } + return true + } + + override var title: NSAttributedString? { + return NSAttributedString(string: NSLocalizedString("Debug: require password", comment: "Debug option"), attributes: [NSForegroundColorAttributeName: UIConstants.TableViewRowTextColor]) + } + + override func onClick(_ navigationController: UINavigationController?) { + profile.getAccount()?.makeSeparated() + settings.tableView.reloadData() + } +} + +// For great debugging! +class RequireUpgradeDebugSetting: WithAccountSetting { + override var hidden: Bool { + if !ShowDebugSettings { + return true + } + if let account = profile.getAccount(), account.actionNeeded != FxAActionNeeded.needsUpgrade { + return false + } + return true + } + + override var title: NSAttributedString? { + return NSAttributedString(string: NSLocalizedString("Debug: require upgrade", comment: "Debug option"), attributes: [NSForegroundColorAttributeName: UIConstants.TableViewRowTextColor]) + } + + override func onClick(_ navigationController: UINavigationController?) { + profile.getAccount()?.makeDoghouse() + settings.tableView.reloadData() + } +} + +// For great debugging! +class ForgetSyncAuthStateDebugSetting: WithAccountSetting { + override var hidden: Bool { + if !ShowDebugSettings { + return true + } + if let _ = profile.getAccount() { + return false + } + return true + } + + override var title: NSAttributedString? { + return NSAttributedString(string: NSLocalizedString("Debug: forget Sync auth state", comment: "Debug option"), attributes: [NSForegroundColorAttributeName: UIConstants.TableViewRowTextColor]) + } + + override func onClick(_ navigationController: UINavigationController?) { + profile.getAccount()?.syncAuthState.invalidate() + settings.tableView.reloadData() + } +} + +class DeleteExportedDataSetting: HiddenSetting { + override var title: NSAttributedString? { + // Not localized for now. + return NSAttributedString(string: "Debug: delete exported databases", attributes: [NSForegroundColorAttributeName: UIConstants.TableViewRowTextColor]) + } + + override func onClick(_ navigationController: UINavigationController?) { + let documentsPath = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0] + let fileManager = FileManager.default + do { + let files = try fileManager.contentsOfDirectory(atPath: documentsPath) + for file in files { + if file.startsWith("browser.") || file.startsWith("logins.") { + try fileManager.removeItemInDirectory(documentsPath, named: file) + } + } + } catch { + print("Couldn't delete exported data: \(error).") + } + } +} + +class ExportBrowserDataSetting: HiddenSetting { + override var title: NSAttributedString? { + // Not localized for now. + return NSAttributedString(string: "Debug: copy databases to app container", attributes: [NSForegroundColorAttributeName: UIConstants.TableViewRowTextColor]) + } + + override func onClick(_ navigationController: UINavigationController?) { + let documentsPath = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0] + do { + let log = Logger.syncLogger + try self.settings.profile.files.copyMatching(fromRelativeDirectory: "", toAbsoluteDirectory: documentsPath) { file in + log.debug("Matcher: \(file)") + return file.startsWith("browser.") || file.startsWith("logins.") || file.startsWith("metadata.") + } + } catch { + print("Couldn't export browser data: \(error).") + } + } +} + +/* + FeatureSwitchSetting is a boolean switch for features that are enabled via a FeatureSwitch. + These are usually features behind a partial release and not features released to the entire population. + */ +class FeatureSwitchSetting: BoolSetting { + let featureSwitch: FeatureSwitch + let prefs: Prefs + + init(prefs: Prefs, featureSwitch: FeatureSwitch, with title: NSAttributedString) { + self.featureSwitch = featureSwitch + self.prefs = prefs + super.init(prefs: prefs, defaultValue: featureSwitch.isMember(prefs), attributedTitleText: title) + } + + override var hidden: Bool { + return !ShowDebugSettings + } + + override func displayBool(_ control: UISwitch) { + control.isOn = featureSwitch.isMember(prefs) + } + + override func writeBool(_ control: UISwitch) { + self.featureSwitch.setMembership(control.isOn, for: self.prefs) + } + +} + +class EnableBookmarkMergingSetting: HiddenSetting { + override var title: NSAttributedString? { + // Not localized for now. + return NSAttributedString(string: "Enable Bidirectional Bookmark Sync ", attributes: [NSForegroundColorAttributeName: UIConstants.TableViewRowTextColor]) + } + + override func onClick(_ navigationController: UINavigationController?) { + AppConstants.shouldMergeBookmarks = true + } +} + +class ForceCrashSetting: HiddenSetting { + override var title: NSAttributedString? { + return NSAttributedString(string: "Debug: Force Crash", attributes: [NSForegroundColorAttributeName: UIConstants.TableViewRowTextColor]) + } + + override func onClick(_ navigationController: UINavigationController?) { + Sentry.shared.crash() + } +} + +// Show the current version of Firefox +class VersionSetting: Setting { + unowned let settings: SettingsTableViewController + + init(settings: SettingsTableViewController) { + self.settings = settings + super.init(title: nil) + } + + override var title: NSAttributedString? { + let appVersion = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as! String + let buildNumber = Bundle.main.object(forInfoDictionaryKey: "CFBundleVersion") as! String + return NSAttributedString(string: String(format: NSLocalizedString("Version %@ (%@)", comment: "Version number of Firefox shown in settings"), appVersion, buildNumber), attributes: [NSForegroundColorAttributeName: UIConstants.TableViewRowTextColor]) + } + + override func onConfigureCell(_ cell: UITableViewCell) { + super.onConfigureCell(cell) + cell.selectionStyle = .none + } + + override func onClick(_ navigationController: UINavigationController?) { + DebugSettingsClickCount += 1 + if DebugSettingsClickCount >= 5 { + DebugSettingsClickCount = 0 + ShowDebugSettings = !ShowDebugSettings + settings.tableView.reloadData() + } + } +} + +// Opens the the license page in a new tab +class LicenseAndAcknowledgementsSetting: Setting { + override var title: NSAttributedString? { + return NSAttributedString(string: NSLocalizedString("Licenses", comment: "Settings item that opens a tab containing the licenses. See http://mzl.la/1NSAWCG"), attributes: [NSForegroundColorAttributeName: UIConstants.TableViewRowTextColor]) + } + + override var url: URL? { + return URL(string: WebServer.sharedInstance.URLForResource("license", module: "about")) + } + + override func onClick(_ navigationController: UINavigationController?) { + setUpAndPushSettingsContentViewController(navigationController) + } +} + +// Opens about:rights page in the content view controller +class YourRightsSetting: Setting { + override var title: NSAttributedString? { + return NSAttributedString(string: NSLocalizedString("Your Rights", comment: "Your Rights settings section title"), attributes: + [NSForegroundColorAttributeName: UIConstants.TableViewRowTextColor]) + } + + override var url: URL? { + return URL(string: "https://www.mozilla.org/about/legal/terms/firefox/") + } + + override func onClick(_ navigationController: UINavigationController?) { + setUpAndPushSettingsContentViewController(navigationController) + } +} + +// Opens the on-boarding screen again +class ShowIntroductionSetting: Setting { + let profile: Profile + + override var accessibilityIdentifier: String? { return "ShowTour" } + + init(settings: SettingsTableViewController) { + self.profile = settings.profile + super.init(title: NSAttributedString(string: NSLocalizedString("Show Tour", comment: "Show the on-boarding screen again from the settings"), attributes: [NSForegroundColorAttributeName: UIConstants.TableViewRowTextColor])) + } + + override func onClick(_ navigationController: UINavigationController?) { + navigationController?.dismiss(animated: true, completion: { + if let appDelegate = UIApplication.shared.delegate as? AppDelegate { + appDelegate.browserViewController.presentIntroViewController(true) + } + }) + } +} + +class SendFeedbackSetting: Setting { + override var title: NSAttributedString? { + return NSAttributedString(string: NSLocalizedString("Send Feedback", comment: "Menu item in settings used to open input.mozilla.org where people can submit feedback"), attributes: [NSForegroundColorAttributeName: UIConstants.TableViewRowTextColor]) + } + + override var url: URL? { + let appVersion = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as! String + return URL(string: "https://input.mozilla.org/feedback/fxios/\(appVersion)") + } + + override func onClick(_ navigationController: UINavigationController?) { + setUpAndPushSettingsContentViewController(navigationController) + } +} + +class SendAnonymousUsageDataSetting: BoolSetting { + init(prefs: Prefs, delegate: SettingsDelegate?) { + super.init( + prefs: prefs, prefKey: AppConstants.PrefSendUsageData, defaultValue: true, + attributedTitleText: NSAttributedString(string: Strings.SendUsageSettingTitle), + attributedStatusText: createStatusText(), + settingDidChange: { + AdjustIntegration.setEnabled($0) + LeanPlumClient.shared.set(attributes: [LPAttributeKey.telemetryOptIn: $0]) + LeanPlumClient.shared.set(enabled: $0) + } + ) + } + + private func createStatusText() -> NSAttributedString { + let statusText = NSMutableAttributedString() + statusText.append(NSAttributedString(string: Strings.SendUsageSettingMessage, attributes: [NSForegroundColorAttributeName: UIConstants.TableViewHeaderTextColor])) + statusText.append(NSAttributedString(string: " ")) + statusText.append(NSAttributedString(string: Strings.SendUsageSettingLink, attributes: [NSForegroundColorAttributeName: UIConstants.HighlightBlue])) + return statusText + } + + override var url: URL? { + return SupportUtils.URLForTopic("adjust") + } + + override func onClick(_ navigationController: UINavigationController?) { + setUpAndPushSettingsContentViewController(navigationController) + } +} + +// Opens the the SUMO page in a new tab +class OpenSupportPageSetting: Setting { + init(delegate: SettingsDelegate?) { + super.init(title: NSAttributedString(string: NSLocalizedString("Help", comment: "Show the SUMO support page from the Support section in the settings. see http://mzl.la/1dmM8tZ"), attributes: [NSForegroundColorAttributeName: UIConstants.TableViewRowTextColor]), + delegate: delegate) + } + + override func onClick(_ navigationController: UINavigationController?) { + navigationController?.dismiss(animated: true) { + if let url = URL(string: "https://support.mozilla.org/products/ios") { + self.delegate?.settingsOpenURLInNewTab(url) + } + } + } +} + +// Opens the search settings pane +class SearchSetting: Setting { + let profile: Profile + + override var accessoryType: UITableViewCellAccessoryType { return .disclosureIndicator } + + override var style: UITableViewCellStyle { return .value1 } + + override var status: NSAttributedString { return NSAttributedString(string: profile.searchEngines.defaultEngine.shortName) } + + override var accessibilityIdentifier: String? { return "Search" } + + init(settings: SettingsTableViewController) { + self.profile = settings.profile + super.init(title: NSAttributedString(string: NSLocalizedString("Search", comment: "Open search section of settings"), attributes: [NSForegroundColorAttributeName: UIConstants.TableViewRowTextColor])) + } + + override func onClick(_ navigationController: UINavigationController?) { + let viewController = SearchSettingsTableViewController() + viewController.model = profile.searchEngines + viewController.profile = profile + navigationController?.pushViewController(viewController, animated: true) + } +} + +class LoginsSetting: Setting { + let profile: Profile + var tabManager: TabManager! + weak var navigationController: UINavigationController? + weak var settings: AppSettingsTableViewController? + + override var accessoryType: UITableViewCellAccessoryType { return .disclosureIndicator } + + override var accessibilityIdentifier: String? { return "Logins" } + + init(settings: SettingsTableViewController, delegate: SettingsDelegate?) { + self.profile = settings.profile + self.tabManager = settings.tabManager + self.navigationController = settings.navigationController + self.settings = settings as? AppSettingsTableViewController + + let loginsTitle = NSLocalizedString("Logins", comment: "Label used as an item in Settings. When touched, the user will be navigated to the Logins/Password manager.") + super.init(title: NSAttributedString(string: loginsTitle, attributes: [NSForegroundColorAttributeName: UIConstants.TableViewRowTextColor]), + delegate: delegate) + } + + func deselectRow () { + if let selectedRow = self.settings?.tableView.indexPathForSelectedRow { + self.settings?.tableView.deselectRow(at: selectedRow, animated: true) + } + } + + override func onClick(_: UINavigationController?) { + guard let authInfo = KeychainWrapper.sharedAppContainerKeychain.authenticationInfo() else { + settings?.navigateToLoginsList() + LeanPlumClient.shared.track(event: .openedLogins) + return + } + + if authInfo.requiresValidation() { + AppAuthenticator.presentAuthenticationUsingInfo(authInfo, + touchIDReason: AuthenticationStrings.loginsTouchReason, + success: { + self.settings?.navigateToLoginsList() + LeanPlumClient.shared.track(event: .openedLogins) + }, + cancel: { + self.deselectRow() + }, + fallback: { + AppAuthenticator.presentPasscodeAuthentication(self.navigationController, delegate: self.settings) + self.deselectRow() + }) + } else { + settings?.navigateToLoginsList() + LeanPlumClient.shared.track(event: .openedLogins) + } + } +} + +class TouchIDPasscodeSetting: Setting { + let profile: Profile + var tabManager: TabManager! + + override var accessoryType: UITableViewCellAccessoryType { return .disclosureIndicator } + + override var accessibilityIdentifier: String? { return "TouchIDPasscode" } + + init(settings: SettingsTableViewController, delegate: SettingsDelegate? = nil) { + self.profile = settings.profile + self.tabManager = settings.tabManager + let localAuthContext = LAContext() + + let title: String + if localAuthContext.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: nil) { + if #available(iOS 11.0, *), localAuthContext.biometryType == .faceID { + title = AuthenticationStrings.faceIDPasscodeSetting + } else { + title = AuthenticationStrings.touchIDPasscodeSetting + } + } else { + title = AuthenticationStrings.passcode + } + super.init(title: NSAttributedString(string: title, attributes: [NSForegroundColorAttributeName: UIConstants.TableViewRowTextColor]), + delegate: delegate) + } + + override func onClick(_ navigationController: UINavigationController?) { + let viewController = AuthenticationSettingsViewController() + viewController.profile = profile + navigationController?.pushViewController(viewController, animated: true) + } +} + +@available(iOS 11, *) +class ContentBlockerSetting: Setting { + let profile: Profile + var tabManager: TabManager! + override var accessoryType: UITableViewCellAccessoryType { return .disclosureIndicator } + override var accessibilityIdentifier: String? { return "TrackingProtection" } + + init(settings: SettingsTableViewController) { + self.profile = settings.profile + self.tabManager = settings.tabManager + super.init(title: NSAttributedString(string: Strings.SettingsTrackingProtectionSectionName, attributes: [NSForegroundColorAttributeName: UIConstants.TableViewRowTextColor])) + } + + override func onClick(_ navigationController: UINavigationController?) { + let viewController = ContentBlockerSettingViewController(prefs: profile.prefs) + viewController.profile = profile + viewController.tabManager = tabManager + navigationController?.pushViewController(viewController, animated: true) + } +} + +class ClearPrivateDataSetting: Setting { + let profile: Profile + var tabManager: TabManager! + + override var accessoryType: UITableViewCellAccessoryType { return .disclosureIndicator } + + override var accessibilityIdentifier: String? { return "ClearPrivateData" } + + init(settings: SettingsTableViewController) { + self.profile = settings.profile + self.tabManager = settings.tabManager + + let clearTitle = Strings.SettingsClearPrivateDataSectionName + super.init(title: NSAttributedString(string: clearTitle, attributes: [NSForegroundColorAttributeName: UIConstants.TableViewRowTextColor])) + } + + override func onClick(_ navigationController: UINavigationController?) { + let viewController = ClearPrivateDataTableViewController() + viewController.profile = profile + viewController.tabManager = tabManager + navigationController?.pushViewController(viewController, animated: true) + } +} + +class PrivacyPolicySetting: Setting { + override var title: NSAttributedString? { + return NSAttributedString(string: NSLocalizedString("Privacy Policy", comment: "Show Firefox Browser Privacy Policy page from the Privacy section in the settings. See https://www.mozilla.org/privacy/firefox/"), attributes: [NSForegroundColorAttributeName: UIConstants.TableViewRowTextColor]) + } + + override var url: URL? { + return URL(string: "https://www.mozilla.org/privacy/firefox/") + } + + override func onClick(_ navigationController: UINavigationController?) { + setUpAndPushSettingsContentViewController(navigationController) + } +} + +class ChinaSyncServiceSetting: WithoutAccountSetting { + override var accessoryType: UITableViewCellAccessoryType { return .none } + var prefs: Prefs { return settings.profile.prefs } + let prefKey = "useChinaSyncService" + + override var title: NSAttributedString? { + return NSAttributedString(string: "本地同步服务", attributes: [NSForegroundColorAttributeName: UIConstants.TableViewRowTextColor]) + } + + override var status: NSAttributedString? { + return NSAttributedString(string: "禁用后使用全球服务同步数据", attributes: [NSForegroundColorAttributeName: UIConstants.TableViewHeaderTextColor]) + } + + override func onConfigureCell(_ cell: UITableViewCell) { + super.onConfigureCell(cell) + let control = UISwitch() + control.onTintColor = UIConstants.ControlTintColor + control.addTarget(self, action: #selector(ChinaSyncServiceSetting.switchValueChanged(_:)), for: UIControlEvents.valueChanged) + control.isOn = prefs.boolForKey(prefKey) ?? self.profile.isChinaEdition + cell.accessoryView = control + cell.selectionStyle = .none + } + + @objc func switchValueChanged(_ toggle: UISwitch) { + prefs.setObject(toggle.isOn, forKey: prefKey) + } +} + +class StageSyncServiceDebugSetting: WithoutAccountSetting { + override var accessoryType: UITableViewCellAccessoryType { return .none } + var prefs: Prefs { return settings.profile.prefs } + + var prefKey: String = "useStageSyncService" + + override var hidden: Bool { + if !ShowDebugSettings { + return true + } + if let _ = profile.getAccount() { + return true + } + return false + } + + override var title: NSAttributedString? { + return NSAttributedString(string: NSLocalizedString("Debug: use stage servers", comment: "Debug option"), attributes: [NSForegroundColorAttributeName: UIConstants.TableViewRowTextColor]) + } + + override var status: NSAttributedString? { + // Derive the configuration we display from the profile. Currently, this could be either a custom + // FxA server or FxA stage servers. + let isOn = prefs.boolForKey(prefKey) ?? false + let isCustomSync = prefs.boolForKey(PrefsKeys.KeyUseCustomSyncService) ?? false + + var configurationURL = ProductionFirefoxAccountConfiguration().authEndpointURL + if isCustomSync { + configurationURL = CustomFirefoxAccountConfiguration(prefs: profile.prefs).authEndpointURL + } else if isOn { + configurationURL = StageFirefoxAccountConfiguration().authEndpointURL + } + + return NSAttributedString(string: configurationURL.absoluteString, attributes: [NSForegroundColorAttributeName: UIConstants.TableViewHeaderTextColor]) + } + + override func onConfigureCell(_ cell: UITableViewCell) { + super.onConfigureCell(cell) + let control = UISwitch() + control.onTintColor = UIConstants.ControlTintColor + control.addTarget(self, action: #selector(StageSyncServiceDebugSetting.switchValueChanged(_:)), for: UIControlEvents.valueChanged) + control.isOn = prefs.boolForKey(prefKey) ?? false + cell.accessoryView = control + cell.selectionStyle = .none + } + + @objc func switchValueChanged(_ toggle: UISwitch) { + prefs.setObject(toggle.isOn, forKey: prefKey) + settings.tableView.reloadData() + } +} + +class HomePageSetting: Setting { + let profile: Profile + var tabManager: TabManager! + + override var accessoryType: UITableViewCellAccessoryType { return .disclosureIndicator } + + override var accessibilityIdentifier: String? { return "Homepage" } + + init(settings: SettingsTableViewController) { + self.profile = settings.profile + self.tabManager = settings.tabManager + + super.init(title: NSAttributedString(string: Strings.SettingsHomePageSectionName, attributes: [NSForegroundColorAttributeName: UIConstants.TableViewRowTextColor])) + } + + override func onClick(_ navigationController: UINavigationController?) { + let viewController = HomePageSettingsViewController() + viewController.profile = profile + viewController.tabManager = tabManager + navigationController?.pushViewController(viewController, animated: true) + } +} + +class NewTabPageSetting: Setting { + let profile: Profile + + override var accessoryType: UITableViewCellAccessoryType { return .disclosureIndicator } + + override var accessibilityIdentifier: String? { return "NewTab" } + + init(settings: SettingsTableViewController) { + self.profile = settings.profile + + super.init(title: NSAttributedString(string: Strings.SettingsNewTabSectionName, attributes: [NSForegroundColorAttributeName: UIConstants.TableViewRowTextColor])) + } + + override func onClick(_ navigationController: UINavigationController?) { + let viewController = NewTabContentSettingsViewController() + viewController.profile = profile + navigationController?.pushViewController(viewController, animated: true) + } +} + +class OpenWithSetting: Setting { + let profile: Profile + + override var accessoryType: UITableViewCellAccessoryType { return .disclosureIndicator } + + override var accessibilityIdentifier: String? { return "OpenWith.Setting" } + + init(settings: SettingsTableViewController) { + self.profile = settings.profile + + super.init(title: NSAttributedString(string: Strings.SettingsOpenWithSectionName, attributes: [NSForegroundColorAttributeName: UIConstants.TableViewRowTextColor])) + } + + override func onClick(_ navigationController: UINavigationController?) { + let viewController = OpenWithSettingsViewController(prefs: profile.prefs) + navigationController?.pushViewController(viewController, animated: true) + } +} + +class AdvanceAccountSetting: HiddenSetting { + let profile: Profile + + override var accessoryType: UITableViewCellAccessoryType { return .disclosureIndicator } + + override var accessibilityIdentifier: String? { return "AdvanceAccount.Setting" } + + override var title: NSAttributedString? { + return NSAttributedString(string: Strings.SettingsAdvanceAccountTitle, attributes: [NSForegroundColorAttributeName: UIConstants.TableViewRowTextColor]) + } + + override init(settings: SettingsTableViewController) { + self.profile = settings.profile + super.init(settings: settings) + } + + override func onClick(_ navigationController: UINavigationController?) { + let viewController = AdvanceAccountSettingViewController() + viewController.profile = profile + navigationController?.pushViewController(viewController, animated: true) + } + + override var hidden: Bool { + return !ShowDebugSettings || profile.hasAccount() + } +} diff --git a/mobile/ios/Client/Frontend/Settings/AppSettingsTableViewController.swift b/mobile/ios/Client/Frontend/Settings/AppSettingsTableViewController.swift new file mode 100644 index 0000000000..98873d8dc0 --- /dev/null +++ b/mobile/ios/Client/Frontend/Settings/AppSettingsTableViewController.swift @@ -0,0 +1,172 @@ +/* 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 UIKit +import Shared +import Account + +/// App Settings Screen (triggered by tapping the 'Gear' in the Tab Tray Controller) +class AppSettingsTableViewController: SettingsTableViewController { + fileprivate let SectionHeaderIdentifier = "SectionHeaderIdentifier" + + override func viewDidLoad() { + super.viewDidLoad() + + navigationItem.title = NSLocalizedString("Settings", comment: "Title in the settings view controller title bar") + navigationItem.leftBarButtonItem = UIBarButtonItem( + title: NSLocalizedString("Done", comment: "Done button on left side of the Settings view controller title bar"), + style: UIBarButtonItemStyle.done, + target: navigationController, action: #selector((navigationController as! SettingsNavigationController).SELdone)) + navigationItem.leftBarButtonItem?.accessibilityIdentifier = "AppSettingsTableViewController.navigationItem.leftBarButtonItem" + + tableView.accessibilityIdentifier = "AppSettingsTableViewController.tableView" + + // Refresh the user's FxA profile upon viewing settings. This will update their avatar, + // display name, etc. + if AppConstants.MOZ_SHOW_FXA_AVATAR { + profile.getAccount()?.updateProfile() + } + } + + override func generateSettings() -> [SettingSection] { + var settings = [SettingSection]() + + let privacyTitle = NSLocalizedString("Privacy", comment: "Privacy section title") + let accountDebugSettings = [ + // Debug settings: + RequirePasswordDebugSetting(settings: self), + RequireUpgradeDebugSetting(settings: self), + ForgetSyncAuthStateDebugSetting(settings: self), + StageSyncServiceDebugSetting(settings: self), + ] + + let prefs = profile.prefs + var generalSettings: [Setting] = [ + SearchSetting(settings: self), + NewTabPageSetting(settings: self), + HomePageSetting(settings: self), + OpenWithSetting(settings: self), + BoolSetting(prefs: prefs, prefKey: "blockPopups", defaultValue: true, + titleText: NSLocalizedString("Block Pop-up Windows", comment: "Block pop-up windows setting")), + BoolSetting(prefs: prefs, prefKey: "saveLogins", defaultValue: true, + titleText: NSLocalizedString("Save Logins", comment: "Setting to enable the built-in password manager")), + ] + + let accountChinaSyncSetting: [Setting] + if !profile.isChinaEdition { + accountChinaSyncSetting = [] + } else { + accountChinaSyncSetting = [ + // Show China sync service setting: + ChinaSyncServiceSetting(settings: self) + ] + } + // There is nothing to show in the Customize section if we don't include the compact tab layout + // setting on iPad. When more options are added that work on both device types, this logic can + // be changed. + + if AppConstants.MOZ_CLIPBOARD_BAR { + generalSettings += [ + BoolSetting(prefs: prefs, prefKey: "showClipboardBar", defaultValue: false, + titleText: Strings.SettingsOfferClipboardBarTitle, + statusText: Strings.SettingsOfferClipboardBarStatus) + ] + } + + var accountSectionTitle: NSAttributedString? + if AppConstants.MOZ_SHOW_FXA_AVATAR { + accountSectionTitle = NSAttributedString(string: Strings.FxAFirefoxAccount) + } + + let footerText = !profile.hasAccount() ? NSAttributedString(string: Strings.FxASyncUsageDetails) : nil + settings += [ + SettingSection(title: accountSectionTitle, footerTitle: footerText, children: [ + // Without a Firefox Account: + ConnectSetting(settings: self), + AdvanceAccountSetting(settings: self), + // With a Firefox Account: + AccountStatusSetting(settings: self), + SyncNowSetting(settings: self) + ] + accountChinaSyncSetting + accountDebugSettings)] + + settings += [ SettingSection(title: NSAttributedString(string: NSLocalizedString("General", comment: "General settings section title")), children: generalSettings)] + + var privacySettings = [Setting]() + privacySettings.append(LoginsSetting(settings: self, delegate: settingsDelegate)) + privacySettings.append(TouchIDPasscodeSetting(settings: self)) + + privacySettings.append(ClearPrivateDataSetting(settings: self)) + + privacySettings += [ + BoolSetting(prefs: prefs, + prefKey: "settings.closePrivateTabs", + defaultValue: false, + titleText: NSLocalizedString("Close Private Tabs", tableName: "PrivateBrowsing", comment: "Setting for closing private tabs"), + statusText: NSLocalizedString("When Leaving Private Browsing", tableName: "PrivateBrowsing", comment: "Will be displayed in Settings under 'Close Private Tabs'")) + ] + + if #available(iOS 11, *) { + privacySettings.append(ContentBlockerSetting(settings: self)) + } + + privacySettings += [ + PrivacyPolicySetting() + ] + + settings += [ + SettingSection(title: NSAttributedString(string: privacyTitle), children: privacySettings), + SettingSection(title: NSAttributedString(string: NSLocalizedString("Support", comment: "Support section title")), children: [ + ShowIntroductionSetting(settings: self), + SendFeedbackSetting(), + SendAnonymousUsageDataSetting(prefs: prefs, delegate: settingsDelegate), + OpenSupportPageSetting(delegate: settingsDelegate), + ]), + SettingSection(title: NSAttributedString(string: NSLocalizedString("About", comment: "About settings section title")), children: [ + VersionSetting(settings: self), + LicenseAndAcknowledgementsSetting(), + YourRightsSetting(), + ExportBrowserDataSetting(settings: self), + DeleteExportedDataSetting(settings: self), + EnableBookmarkMergingSetting(settings: self), + ForceCrashSetting(settings: self) + ])] + + if profile.hasAccount() { + settings += [SettingSection(title: nil, footerTitle: NSAttributedString(string: ""), children: [DisconnectSetting(settings: self)])] + } + + return settings + } + + override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { + let headerView = super.tableView(tableView, viewForHeaderInSection: section) as! SettingsTableSectionHeaderFooterView + // Prevent the top border from showing for the General section. + if !profile.hasAccount() { + switch section { + case 1: + headerView.showTopBorder = false + default: + break + } + } + return headerView + } +} + +extension AppSettingsTableViewController { + func navigateToLoginsList() { + let viewController = LoginListViewController(profile: profile) + viewController.settingsDelegate = settingsDelegate + navigationController?.pushViewController(viewController, animated: true) + } +} + +extension AppSettingsTableViewController: PasscodeEntryDelegate { + @objc func passcodeValidationDidSucceed() { + navigationController?.dismiss(animated: true) { + self.navigateToLoginsList() + } + } +} diff --git a/mobile/ios/Client/Frontend/Settings/ClearPrivateDataTableViewController.swift b/mobile/ios/Client/Frontend/Settings/ClearPrivateDataTableViewController.swift new file mode 100644 index 0000000000..338b100b0a --- /dev/null +++ b/mobile/ios/Client/Frontend/Settings/ClearPrivateDataTableViewController.swift @@ -0,0 +1,176 @@ +/* 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 UIKit +import Shared + +private let SectionToggles = 0 +private let SectionButton = 1 +private let NumberOfSections = 2 +private let SectionHeaderFooterIdentifier = "SectionHeaderFooterIdentifier" +private let TogglesPrefKey = "clearprivatedata.toggles" + +private let log = Logger.browserLogger + +private let HistoryClearableIndex = 0 + +class ClearPrivateDataTableViewController: UITableViewController { + fileprivate var clearButton: UITableViewCell? + + var profile: Profile! + var tabManager: TabManager! + + fileprivate typealias DefaultCheckedState = Bool + + fileprivate lazy var clearables: [(clearable: Clearable, checked: DefaultCheckedState)] = { + return [ + (HistoryClearable(profile: self.profile), true), + (CacheClearable(tabManager: self.tabManager), true), + (CookiesClearable(tabManager: self.tabManager), true), + (SiteDataClearable(tabManager: self.tabManager), true), + ] + }() + + fileprivate lazy var toggles: [Bool] = { + if let savedToggles = self.profile.prefs.arrayForKey(TogglesPrefKey) as? [Bool] { + return savedToggles + } + + return self.clearables.map { $0.checked } + }() + + fileprivate var clearButtonEnabled = true { + didSet { + clearButton?.textLabel?.textColor = clearButtonEnabled ? UIConstants.DestructiveRed : UIColor.lightGray + } + } + + override func viewDidLoad() { + super.viewDidLoad() + + title = Strings.SettingsClearPrivateDataTitle + + tableView.register(SettingsTableSectionHeaderFooterView.self, forHeaderFooterViewReuseIdentifier: SectionHeaderFooterIdentifier) + + tableView.separatorColor = UIConstants.TableViewSeparatorColor + tableView.backgroundColor = UIConstants.TableViewHeaderBackgroundColor + let footer = SettingsTableSectionHeaderFooterView(frame: CGRect(x: 0, y: 0, width: tableView.bounds.width, height: UIConstants.TableViewHeaderFooterHeight)) + footer.showBottomBorder = false + tableView.tableFooterView = footer + } + + override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let cell = UITableViewCell(style: UITableViewCellStyle.default, reuseIdentifier: nil) + + if indexPath.section == SectionToggles { + cell.textLabel?.text = clearables[indexPath.item].clearable.label + let control = UISwitch() + control.onTintColor = UIConstants.ControlTintColor + control.addTarget(self, action: #selector(ClearPrivateDataTableViewController.switchValueChanged(_:)), for: UIControlEvents.valueChanged) + control.isOn = toggles[indexPath.item] + cell.accessoryView = control + cell.selectionStyle = .none + control.tag = indexPath.item + } else { + assert(indexPath.section == SectionButton) + cell.textLabel?.text = Strings.SettingsClearPrivateDataClearButton + cell.textLabel?.textAlignment = NSTextAlignment.center + cell.textLabel?.textColor = UIConstants.DestructiveRed + cell.accessibilityTraits = UIAccessibilityTraitButton + cell.accessibilityIdentifier = "ClearPrivateData" + clearButton = cell + } + + return cell + } + + override func numberOfSections(in tableView: UITableView) -> Int { + return NumberOfSections + } + + override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + if section == SectionToggles { + return clearables.count + } + + assert(section == SectionButton) + return 1 + } + + override func tableView(_ tableView: UITableView, shouldHighlightRowAt indexPath: IndexPath) -> Bool { + guard indexPath.section == SectionButton else { return false } + + // Highlight the button only if it's enabled. + return clearButtonEnabled + } + + override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + guard indexPath.section == SectionButton else { return } + + func clearPrivateData(_ action: UIAlertAction) { + let toggles = self.toggles + self.clearables + .enumerated() + .flatMap { (i, pair) in + guard toggles[i] else { + return nil + } + log.debug("Clearing \(pair.clearable).") + return pair.clearable.clear() + } + .allSucceed() + .upon { result in + assert(result.isSuccess, "Private data cleared successfully") + + LeanPlumClient.shared.track(event: .clearPrivateData) + + self.profile.prefs.setObject(self.toggles, forKey: TogglesPrefKey) + + DispatchQueue.main.async { + // Disable the Clear Private Data button after it's clicked. + self.clearButtonEnabled = false + self.tableView.deselectRow(at: indexPath, animated: true) + } + } + } + + // We have been asked to clear history and we have an account. + // (Whether or not it's in a good state is irrelevant.) + if self.toggles[HistoryClearableIndex] && profile.hasAccount() { + profile.syncManager.hasSyncedHistory().uponQueue(DispatchQueue.main) { yes in + // Err on the side of warning, but this shouldn't fail. + let alert: UIAlertController + if yes.successValue ?? true { + // Our local database contains some history items that have been synced. + // Warn the user before clearing. + alert = UIAlertController.clearSyncedHistoryAlert(okayCallback: clearPrivateData) + } else { + alert = UIAlertController.clearPrivateDataAlert(okayCallback: clearPrivateData) + } + self.present(alert, animated: true, completion: nil) + return + } + } else { + let alert = UIAlertController.clearPrivateDataAlert(okayCallback: clearPrivateData) + self.present(alert, animated: true, completion: nil) + } + + tableView.deselectRow(at: indexPath, animated: false) + } + + override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { + return tableView.dequeueReusableHeaderFooterView(withIdentifier: SectionHeaderFooterIdentifier) as! SettingsTableSectionHeaderFooterView + } + + override func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { + return UIConstants.TableViewHeaderFooterHeight + } + + @objc func switchValueChanged(_ toggle: UISwitch) { + toggles[toggle.tag] = toggle.isOn + + // Dim the clear button if no clearables are selected. + clearButtonEnabled = toggles.contains(true) + } +} diff --git a/mobile/ios/Client/Frontend/Settings/Clearables.swift b/mobile/ios/Client/Frontend/Settings/Clearables.swift new file mode 100644 index 0000000000..1e0fbfb553 --- /dev/null +++ b/mobile/ios/Client/Frontend/Settings/Clearables.swift @@ -0,0 +1,149 @@ +/* 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 WebKit +import Deferred +import SDWebImage +import CoreSpotlight + +private let log = Logger.browserLogger + +// Removed Clearables as part of Bug 1226654, but keeping the string around. +private let removedSavedLoginsLabel = NSLocalizedString("Saved Logins", tableName: "ClearPrivateData", comment: "Settings item for clearing passwords and login data") + +// A base protocol for something that can be cleared. +protocol Clearable { + func clear() -> Success + var label: String { get } +} + +class ClearableError: MaybeErrorType { + fileprivate let msg: String + init(msg: String) { + self.msg = msg + } + + var description: String { return msg } +} + +// Clears our browsing history, including favicons and thumbnails. +class HistoryClearable: Clearable { + let profile: Profile + init(profile: Profile) { + self.profile = profile + } + + var label: String { + return NSLocalizedString("Browsing History", tableName: "ClearPrivateData", comment: "Settings item for clearing browsing history") + } + + func clear() -> Success { + return profile.history.clearHistory().bindQueue(.main) { success in + SDImageCache.shared().clearDisk() + SDImageCache.shared().clearMemory() + self.profile.recentlyClosedTabs.clearTabs() + CSSearchableIndex.default().deleteAllSearchableItems() + NotificationCenter.default.post(name: NotificationPrivateDataClearedHistory, object: nil) + log.debug("HistoryClearable succeeded: \(success).") + return Deferred(value: success) + } + } +} + +struct ClearableErrorType: MaybeErrorType { + let err: Error + + init(err: Error) { + self.err = err + } + + var description: String { + return "Couldn't clear: \(err)." + } +} + +// Clear the web cache. Note, this has to close all open tabs in order to ensure the data +// cached in them isn't flushed to disk. +class CacheClearable: Clearable { + let tabManager: TabManager + init(tabManager: TabManager) { + self.tabManager = tabManager + } + + var label: String { + return NSLocalizedString("Cache", tableName: "ClearPrivateData", comment: "Settings item for clearing the cache") + } + + func clear() -> Success { + let dataTypes = Set([WKWebsiteDataTypeDiskCache, WKWebsiteDataTypeMemoryCache]) + WKWebsiteDataStore.default().removeData(ofTypes: dataTypes, modifiedSince: Date.distantPast, completionHandler: {}) + + log.debug("CacheClearable succeeded.") + return succeed() + } +} + +private func deleteLibraryFolderContents(_ folder: String) throws { + let manager = FileManager.default + let library = manager.urls(for: FileManager.SearchPathDirectory.libraryDirectory, in: .userDomainMask)[0] + let dir = library.appendingPathComponent(folder) + let contents = try manager.contentsOfDirectory(atPath: dir.path) + for content in contents { + do { + try manager.removeItem(at: dir.appendingPathComponent(content)) + } catch where ((error as NSError).userInfo[NSUnderlyingErrorKey] as? NSError)?.code == Int(EPERM) { + // "Not permitted". We ignore this. + log.debug("Couldn't delete some library contents.") + } + } +} + +private func deleteLibraryFolder(_ folder: String) throws { + let manager = FileManager.default + let library = manager.urls(for: FileManager.SearchPathDirectory.libraryDirectory, in: .userDomainMask)[0] + let dir = library.appendingPathComponent(folder) + try manager.removeItem(at: dir) +} + +// Removes all app cache storage. +class SiteDataClearable: Clearable { + let tabManager: TabManager + init(tabManager: TabManager) { + self.tabManager = tabManager + } + + var label: String { + return NSLocalizedString("Offline Website Data", tableName: "ClearPrivateData", comment: "Settings item for clearing website data") + } + + func clear() -> Success { + let dataTypes = Set([WKWebsiteDataTypeOfflineWebApplicationCache]) + WKWebsiteDataStore.default().removeData(ofTypes: dataTypes, modifiedSince: Date.distantPast, completionHandler: {}) + + log.debug("SiteDataClearable succeeded.") + return succeed() + } +} + +// Remove all cookies stored by the site. This includes localStorage, sessionStorage, and WebSQL/IndexedDB. +class CookiesClearable: Clearable { + let tabManager: TabManager + init(tabManager: TabManager) { + self.tabManager = tabManager + } + + var label: String { + return NSLocalizedString("Cookies", tableName: "ClearPrivateData", comment: "Settings item for clearing cookies") + } + + func clear() -> Success { + let dataTypes = Set([WKWebsiteDataTypeCookies, WKWebsiteDataTypeLocalStorage, WKWebsiteDataTypeSessionStorage, WKWebsiteDataTypeWebSQLDatabases, WKWebsiteDataTypeIndexedDBDatabases]) + WKWebsiteDataStore.default().removeData(ofTypes: dataTypes, modifiedSince: Date.distantPast, completionHandler: {}) + + log.debug("CookiesClearable succeeded.") + return succeed() + } +} diff --git a/mobile/ios/Client/Frontend/Settings/CustomSearchViewController.swift b/mobile/ios/Client/Frontend/Settings/CustomSearchViewController.swift new file mode 100644 index 0000000000..dfcdf071bd --- /dev/null +++ b/mobile/ios/Client/Frontend/Settings/CustomSearchViewController.swift @@ -0,0 +1,252 @@ +/* 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 UIKit +import Shared +import SnapKit +import Storage +import SDWebImage +import Deferred + +private let log = Logger.browserLogger + +class CustomSearchError: MaybeErrorType { + + enum Reason { + case DuplicateEngine, FormInput + } + + var reason: Reason! + + internal var description: String { + return "Search Engine Not Added" + } + + init(_ reason: Reason) { + self.reason = reason + } +} + +class CustomSearchViewController: SettingsTableViewController { + + fileprivate var urlString: String? + fileprivate var engineTitle = "" + fileprivate lazy var spinnerView: UIActivityIndicatorView = { + let spinner = UIActivityIndicatorView(activityIndicatorStyle: .gray) + spinner.hidesWhenStopped = true + return spinner + }() + + override func viewDidLoad() { + super.viewDidLoad() + title = Strings.SettingsAddCustomEngineTitle + view.addSubview(spinnerView) + spinnerView.snp.makeConstraints { make in + make.center.equalTo(self.view.snp.center) + } + } + + var successCallback: (() -> Void)? + + fileprivate func addSearchEngine(_ searchQuery: String, title: String) { + spinnerView.startAnimating() + + let trimmedQuery = searchQuery.trimmingCharacters(in: .whitespacesAndNewlines) + let trimmedTitle = title.trimmingCharacters(in: .whitespacesAndNewlines) + + createEngine(forQuery: trimmedQuery, andName: trimmedTitle).uponQueue(DispatchQueue.main) { result in + self.spinnerView.stopAnimating() + guard let engine = result.successValue else { + let alert: UIAlertController + let error = result.failureValue as? CustomSearchError + + alert = (error?.reason == .DuplicateEngine) ? + ThirdPartySearchAlerts.duplicateCustomEngine() : ThirdPartySearchAlerts.incorrectCustomEngineForm() + + self.navigationItem.rightBarButtonItem?.isEnabled = true + self.present(alert, animated: true, completion: nil) + return + } + self.profile.searchEngines.addSearchEngine(engine) + + CATransaction.begin() // Use transaction to call callback after animation has been completed + CATransaction.setCompletionBlock(self.successCallback) + _ = self.navigationController?.popViewController(animated: true) + CATransaction.commit() + } + } + + func createEngine(forQuery query: String, andName name: String) -> Deferred> { + let deferred = Deferred>() + guard let template = getSearchTemplate(withString: query), + let url = URL(string: template.addingPercentEncoding(withAllowedCharacters: CharacterSet.urlFragmentAllowed)!), url.isWebPage() else { + deferred.fill(Maybe(failure: CustomSearchError(.FormInput))) + return deferred + } + + // ensure we haven't already stored this template + guard engineExists(name: name, template: template) == false else { + deferred.fill(Maybe(failure: CustomSearchError(.DuplicateEngine))) + return deferred + } + + FaviconFetcher.fetchFavImageForURL(forURL: url, profile: profile).uponQueue(DispatchQueue.main) { result in + let image = result.successValue ?? FaviconFetcher.getDefaultFavicon(url) + let engine = OpenSearchEngine(engineID: nil, shortName: name, image: image, searchTemplate: template, suggestTemplate: nil, isCustomEngine: true) + + //Make sure a valid scheme is used + let url = engine.searchURLForQuery("test") + let maybe = (url == nil) ? Maybe(failure: CustomSearchError(.FormInput)) : Maybe(success: engine) + deferred.fill(maybe) + } + return deferred + } + + private func engineExists(name: String, template: String) -> Bool { + return profile.searchEngines.orderedEngines.contains { (engine) -> Bool in + return engine.shortName == name || engine.searchTemplate == template + } + } + + func getSearchTemplate(withString query: String) -> String? { + let SearchTermComponent = "%s" //Placeholder in User Entered String + let placeholder = "{searchTerms}" //Placeholder looked for when using Custom Search Engine in OpenSearch.swift + + if query.contains(SearchTermComponent) { + return query.replacingOccurrences(of: SearchTermComponent, with: placeholder) + } + return nil + } + + override func generateSettings() -> [SettingSection] { + + func URLFromString(_ string: String?) -> URL? { + guard let string = string else { + return nil + } + return URL(string: string) + } + + let titleField = CustomSearchEngineTextView(placeholder: Strings.SettingsAddCustomEngineTitlePlaceholder, settingIsValid: { text in + return text != nil && text != "" + }, settingDidChange: {fieldText in + guard let title = fieldText else { + return + } + self.engineTitle = title + }) + titleField.textField.accessibilityIdentifier = "customEngineTitle" + + let urlField = CustomSearchEngineTextView(placeholder: Strings.SettingsAddCustomEngineURLPlaceholder, height: 133, settingIsValid: { text in + //Can check url text text validity here. + return true + }, settingDidChange: {fieldText in + self.urlString = fieldText + }) + + urlField.textField.autocapitalizationType = .none + urlField.textField.accessibilityIdentifier = "customEngineUrl" + + let settings: [SettingSection] = [ + SettingSection(title: NSAttributedString(string: Strings.SettingsAddCustomEngineTitleLabel), children: [titleField]), + SettingSection(title: NSAttributedString(string: Strings.SettingsAddCustomEngineURLLabel), footerTitle: NSAttributedString(string: "http://youtube.com/search?q=%s"), children: [urlField]) + ] + + self.navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .save, target: self, action: #selector(self.addCustomSearchEngine(_:))) + self.navigationItem.rightBarButtonItem?.accessibilityIdentifier = "customEngineSaveButton" + + return settings + } + + func addCustomSearchEngine(_ nav: UINavigationController?) { + self.view.endEditing(true) + navigationItem.rightBarButtonItem?.isEnabled = false + if let url = self.urlString { + self.addSearchEngine(url, title: self.engineTitle) + } + } +} + +class CustomSearchEngineTextView: Setting, UITextViewDelegate { + + fileprivate let Padding: CGFloat = 8 + fileprivate let TextLabelHeight: CGFloat = 44 + fileprivate var TextFieldHeight: CGFloat = 44 + + fileprivate let defaultValue: String? + fileprivate let placeholder: String + fileprivate let settingDidChange: ((String?) -> Void)? + fileprivate let settingIsValid: ((String?) -> Bool)? + + let textField = UITextView() + let placeholderLabel = UILabel() + + init(defaultValue: String? = nil, placeholder: String, height: CGFloat = 44, settingIsValid isValueValid: ((String?) -> Bool)? = nil, settingDidChange: ((String?) -> Void)? = nil) { + self.defaultValue = defaultValue + self.TextFieldHeight = height + self.settingDidChange = settingDidChange + self.settingIsValid = isValueValid + self.placeholder = placeholder + textField.addSubview(placeholderLabel) + super.init(cellHeight: TextFieldHeight) + } + + override func onConfigureCell(_ cell: UITableViewCell) { + super.onConfigureCell(cell) + if let id = accessibilityIdentifier { + textField.accessibilityIdentifier = id + "TextField" + } + + placeholderLabel.adjustsFontSizeToFitWidth = true + placeholderLabel.textColor = UIColor(red: 0.0, green: 0.0, blue: 0.0980392, alpha: 0.22) + placeholderLabel.text = placeholder + placeholderLabel.frame = CGRect(x: 0, y: 0, width: textField.frame.width, height: TextLabelHeight) + textField.font = placeholderLabel.font + + textField.textContainer.lineFragmentPadding = 0 + textField.keyboardType = .URL + textField.autocorrectionType = .no + textField.delegate = self + cell.isUserInteractionEnabled = true + cell.accessibilityTraits = UIAccessibilityTraitNone + cell.contentView.addSubview(textField) + cell.selectionStyle = .none + + textField.snp.makeConstraints { make in + make.height.equalTo(TextFieldHeight) + make.left.right.equalTo(cell.contentView).inset(Padding) + } + } + + override func onClick(_ navigationController: UINavigationController?) { + textField.becomeFirstResponder() + } + + fileprivate func isValid(_ value: String?) -> Bool { + guard let test = settingIsValid else { + return true + } + return test(prepareValidValue(userInput: value)) + } + + func prepareValidValue(userInput value: String?) -> String? { + return value + } + + func textViewDidBeginEditing(_ textView: UITextView) { + placeholderLabel.isHidden = textField.text != "" + } + + func textViewDidChange(_ textView: UITextView) { + placeholderLabel.isHidden = textField.text != "" + settingDidChange?(textView.text) + let color = isValid(textField.text) ? UIConstants.TableViewRowTextColor : UIConstants.DestructiveRed + textField.textColor = color + } + + func textViewDidEndEditing(_ textView: UITextView) { + placeholderLabel.isHidden = textField.text != "" + settingDidChange?(textView.text) + } +} diff --git a/mobile/ios/Client/Frontend/Settings/FxAContentViewController.swift b/mobile/ios/Client/Frontend/Settings/FxAContentViewController.swift new file mode 100644 index 0000000000..0884e28b39 --- /dev/null +++ b/mobile/ios/Client/Frontend/Settings/FxAContentViewController.swift @@ -0,0 +1,278 @@ +/* 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 SnapKit +import UIKit +import WebKit +import SwiftyJSON + +protocol FxAContentViewControllerDelegate: class { + func contentViewControllerDidSignIn(_ viewController: FxAContentViewController, withFlags: FxALoginFlags) + func contentViewControllerDidCancel(_ viewController: FxAContentViewController) +} + +/** + * A controller that manages a single web view connected to the Firefox + * Accounts (Desktop) Sync postMessage interface. + * + * The postMessage interface is not really documented, but it is simple + * enough. I reverse engineered it from the Desktop Firefox code and the + * fxa-content-server git repository. + */ +class FxAContentViewController: SettingsContentViewController, WKScriptMessageHandler { + fileprivate enum RemoteCommand: String { + case canLinkAccount = "can_link_account" + case loaded = "loaded" + case login = "login" + case sessionStatus = "session_status" + case signOut = "sign_out" + } + + weak var delegate: FxAContentViewControllerDelegate? + + let profile: Profile + + init(profile: Profile, fxaOptions: FxALaunchParams? = nil) { + self.profile = profile + + super.init(backgroundColor: UIColor(red: 242 / 255.0, green: 242 / 255.0, blue: 242 / 255.0, alpha: 1.0), title: NSAttributedString(string: "Firefox Accounts")) + + if AppConstants.MOZ_FXA_DEEP_LINK_FORM_FILL { + self.url = self.createFxAURLWith(fxaOptions, profile: profile) + } else { + self.url = profile.accountConfiguration.signInURL + } + + NotificationCenter.default.addObserver(self, selector: #selector(FxAContentViewController.userDidVerify(_:)), name: NotificationFirefoxAccountVerified, object: nil) + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + } + + override func viewDidDisappear(_ animated: Bool) { + super.viewDidDisappear(animated) + + if AppConstants.MOZ_SHOW_FXA_AVATAR { + profile.getAccount()?.updateProfile() + } + + // If the FxAContentViewController was launched from a FxA deferred link + // onboarding might not have been shown. Check to see if it needs to be + // displayed and don't animate. + if let appDelegate = UIApplication.shared.delegate as? AppDelegate { + appDelegate.browserViewController.presentIntroViewController(false, animated: false) + } + } + + override func makeWebView() -> WKWebView { + // Inject our setup code after the page loads. + let source = getJS() + let userScript = WKUserScript( + source: source, + injectionTime: WKUserScriptInjectionTime.atDocumentEnd, + forMainFrameOnly: true + ) + + // Handle messages from the content server (via our user script). + let contentController = WKUserContentController() + contentController.addUserScript(userScript) + contentController.add(LeakAvoider(delegate: self), name: "accountsCommandHandler") + + let config = WKWebViewConfiguration() + config.userContentController = contentController + + let webView = WKWebView( + frame: CGRect(x: 0, y: 0, width: 1, height: 1), + configuration: config + ) + webView.allowsLinkPreview = false + webView.navigationDelegate = self + webView.accessibilityLabel = NSLocalizedString("Web content", comment: "Accessibility label for the main web content view") + + // Don't allow overscrolling. + webView.scrollView.bounces = false + return webView + } + + // Send a message to the content server. + func injectData(_ type: String, content: [String: Any]) { + let data = [ + "type": type, + "content": content, + ] as [String: Any] + let json = JSON(data).stringValue() ?? "" + let script = "window.postMessage(\(json), '\(self.url.absoluteString)');" + webView.evaluateJavaScript(script, completionHandler: nil) + } + + fileprivate func onCanLinkAccount(_ data: JSON) { + // // We need to confirm a relink - see shouldAllowRelink for more + // let ok = shouldAllowRelink(accountData.email); + let ok = true + injectData("message", content: ["status": "can_link_account", "data": ["ok": ok]]) + } + + // We're not signed in to a Firefox Account at this time, which we signal by returning an error. + fileprivate func onSessionStatus(_ data: JSON) { + injectData("message", content: ["status": "error"]) + } + + // We're not signed in to a Firefox Account at this time. We should never get a sign out message! + fileprivate func onSignOut(_ data: JSON) { + injectData("message", content: ["status": "error"]) + } + + // The user has signed in to a Firefox Account. We're done! + fileprivate func onLogin(_ data: JSON) { + injectData("message", content: ["status": "login"]) + + let app = UIApplication.shared + let helper = FxALoginHelper.sharedInstance + helper.delegate = self + helper.application(app, didReceiveAccountJSON: data) + + if profile.hasAccount() { + LeanPlumClient.shared.set(attributes: [LPAttributeKey.signedInSync: true]) + } + + LeanPlumClient.shared.track(event: .signsInFxa) + } + + @objc fileprivate func userDidVerify(_ notification: Notification) { + guard let account = profile.getAccount() else { + return + } + // We can't verify against the actionNeeded of the account, + // because of potential race conditions. + // However, we restrict visibility of this method, and make sure + // we only Notify via the FxALoginStateMachine. + let flags = FxALoginFlags(pushEnabled: account.pushRegistration != nil, + verified: true) + LeanPlumClient.shared.set(attributes: [LPAttributeKey.signedInSync: true]) + DispatchQueue.main.async { + self.delegate?.contentViewControllerDidSignIn(self, withFlags: flags) + } + } + + // The content server page is ready to be shown. + fileprivate func onLoaded() { + self.timer?.invalidate() + self.timer = nil + self.isLoaded = true + } + + // Handle a message coming from the content server. + func handleRemoteCommand(_ rawValue: String, data: JSON) { + if let command = RemoteCommand(rawValue: rawValue) { + if !isLoaded && command != .loaded { + // Work around https://github.com/mozilla/fxa-content-server/issues/2137 + onLoaded() + } + + switch command { + case .loaded: + onLoaded() + case .login: + onLogin(data) + case .canLinkAccount: + onCanLinkAccount(data) + case .sessionStatus: + onSessionStatus(data) + case .signOut: + onSignOut(data) + } + } + } + + // Dispatch webkit messages originating from our child webview. + func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) { + // Make sure we're communicating with a trusted page. That is, ensure the origin of the + // message is the same as the origin of the URL we initially loaded in this web view. + // Note that this exploit wouldn't be possible if we were using WebChannels; see + // https://developer.mozilla.org/en-US/docs/Mozilla/JavaScript_code_modules/WebChannel.jsm + let origin = message.frameInfo.securityOrigin + guard origin.`protocol` == url.scheme && origin.host == url.host && origin.port == (url.port ?? 0) else { + print("Ignoring message - \(origin) does not match expected origin: \(url.origin ?? "nil")") + return + } + + if message.name == "accountsCommandHandler" { + let body = JSON(message.body) + let detail = body["detail"] + handleRemoteCommand(detail["command"].stringValue, data: detail["data"]) + } + } + + // Configure the FxA signin url based on any passed options. + public func createFxAURLWith(_ fxaOptions: FxALaunchParams?, profile: Profile) -> URL { + let profileUrl = profile.accountConfiguration.signInURL + + guard let launchParams = fxaOptions else { + return profileUrl + } + + // Only append `signin`, `entrypoint` and `utm_*` parameters. Note that you can't + // override the service and context params. + var params = launchParams.query + params.removeValue(forKey: "service") + params.removeValue(forKey: "context") + let queryURL = params.filter { $0.key == "signin" || $0.key == "entrypoint" || $0.key.range(of: "utm_") != nil }.map({ + return "\($0.key)=\($0.value)" + }).joined(separator: "&") + + return URL(string: "\(profileUrl)&\(queryURL)") ?? profileUrl + } + + fileprivate func getJS() -> String { + let fileRoot = Bundle.main.path(forResource: "FxASignIn", ofType: "js") + return (try! NSString(contentsOfFile: fileRoot!, encoding: String.Encoding.utf8.rawValue)) as String + } + + override func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { + // Ignore for now. + } + + override func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) { + // Ignore for now. + } +} + +extension FxAContentViewController: FxAPushLoginDelegate { + func accountLoginDidSucceed(withFlags flags: FxALoginFlags) { + DispatchQueue.main.async { + self.delegate?.contentViewControllerDidSignIn(self, withFlags: flags) + } + } + + func accountLoginDidFail() { + DispatchQueue.main.async { + self.delegate?.contentViewControllerDidCancel(self) + } + } +} + +/* +LeakAvoider prevents leaks with WKUserContentController +http://stackoverflow.com/questions/26383031/wkwebview-causes-my-view-controller-to-leak +*/ + +class LeakAvoider: NSObject, WKScriptMessageHandler { + weak var delegate: WKScriptMessageHandler? + + init(delegate: WKScriptMessageHandler) { + self.delegate = delegate + super.init() + } + + func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) { + self.delegate?.userContentController(userContentController, didReceive: message) + } +} diff --git a/mobile/ios/Client/Frontend/Settings/HomePageSettingsViewController.swift b/mobile/ios/Client/Frontend/Settings/HomePageSettingsViewController.swift new file mode 100644 index 0000000000..02ff37fdfd --- /dev/null +++ b/mobile/ios/Client/Frontend/Settings/HomePageSettingsViewController.swift @@ -0,0 +1,98 @@ +/* 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 UIKit +import Shared +import SnapKit + +private let log = Logger.browserLogger + +class HomePageSettingsViewController: SettingsTableViewController { + + override func viewDidLoad() { + super.viewDidLoad() + title = Strings.SettingsHomePageTitle + } + + override func generateSettings() -> [SettingSection] { + let prefs = profile.prefs + let helper = HomePageHelper(prefs: prefs) + func setHomePage(_ url: URL?) -> ((UINavigationController?) -> Void) { + weak var tableView: UITableView? = self.tableView + return { nav in + helper.currentURL = url + tableView?.reloadData() + } + } + + func isHomePage(_ url: URL?) -> (() -> Bool) { + return { + return url?.isWebPage() ?? false + } + } + + func URLFromString(_ string: String?) -> URL? { + guard let string = string else { + return nil + } + return URL(string: string) + } + + let currentTabURL = self.tabManager.selectedTab?.url?.displayURL + let clipboardURL = URLFromString(UIPasteboard.general.string) + + var basicSettings: [Setting] = [ + WebPageSetting(prefs: prefs, + prefKey: HomePageConstants.HomePageURLPrefKey, + placeholder: helper.defaultURLString ?? Strings.SettingsHomePagePlaceholder, + accessibilityIdentifier: "HomePageSetting"), + ButtonSetting(title: NSAttributedString(string: Strings.SettingsHomePageUseCurrentPage), + accessibilityIdentifier: "UseCurrentTab", + isEnabled: isHomePage(currentTabURL), + onClick: setHomePage(currentTabURL)), + ButtonSetting(title: NSAttributedString(string: Strings.SettingsHomePageUseCopiedLink), + accessibilityIdentifier: "UseCopiedLink", + isEnabled: isHomePage(clipboardURL), + onClick: setHomePage(clipboardURL)), + ] + + basicSettings += [ + ButtonSetting(title: NSAttributedString(string: Strings.SettingsHomePageClear), + destructive: true, + accessibilityIdentifier: "ClearHomePage", + onClick: setHomePage(nil)), + ] + + return [SettingSection(title: NSAttributedString(string: Strings.SettingsHomePageURLSectionTitle), children: basicSettings)] + } +} + +class WebPageSetting: StringSetting { + init(prefs: Prefs, prefKey: String, defaultValue: String? = nil, placeholder: String, accessibilityIdentifier: String, settingDidChange: ((String?) -> Void)? = nil) { + super.init(prefs: prefs, + prefKey: prefKey, + defaultValue: defaultValue, + placeholder: placeholder, + accessibilityIdentifier: accessibilityIdentifier, + settingIsValid: WebPageSetting.isURLOrEmpty, + settingDidChange: settingDidChange) + textField.keyboardType = .URL + textField.autocapitalizationType = .none + textField.autocorrectionType = .no + } + + override func prepareValidValue(userInput value: String?) -> String? { + guard let value = value else { + return nil + } + return URIFixup.getURL(value)?.absoluteString + } + + static func isURLOrEmpty(_ string: String?) -> Bool { + guard let string = string, !string.isEmpty else { + return true + } + return URL(string: string)?.isWebPage() ?? false + } +} diff --git a/mobile/ios/Client/Frontend/Settings/LoginDetailViewController.swift b/mobile/ios/Client/Frontend/Settings/LoginDetailViewController.swift new file mode 100644 index 0000000000..92badff5f2 --- /dev/null +++ b/mobile/ios/Client/Frontend/Settings/LoginDetailViewController.swift @@ -0,0 +1,376 @@ +/* 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 Storage +import Shared +import SwiftKeychainWrapper + +enum InfoItem: Int { + case websiteItem = 0 + case usernameItem = 1 + case passwordItem = 2 + case lastModifiedSeparator = 3 + case deleteItem = 4 + + var indexPath: IndexPath { + return IndexPath(row: rawValue, section: 0) + } +} + +private struct LoginDetailUX { + static let InfoRowHeight: CGFloat = 58 + static let DeleteRowHeight: CGFloat = 44 + static let SeparatorHeight: CGFloat = 44 +} + +class LoginDetailViewController: SensitiveViewController { + + fileprivate let profile: Profile + + fileprivate let tableView = UITableView() + + fileprivate var login: Login { + didSet { + tableView.reloadData() + } + } + + fileprivate var editingInfo: Bool = false { + didSet { + if editingInfo != oldValue { + tableView.reloadData() + } + } + } + + fileprivate let LoginCellIdentifier = "LoginCell" + fileprivate let DefaultCellIdentifier = "DefaultCellIdentifier" + fileprivate let SeparatorIdentifier = "SeparatorIdentifier" + + // Used to temporarily store a reference to the cell the user is showing the menu controller for + fileprivate var menuControllerCell: LoginTableViewCell? + + fileprivate weak var websiteField: UITextField? + fileprivate weak var usernameField: UITextField? + fileprivate weak var passwordField: UITextField? + + fileprivate var deleteAlert: UIAlertController? + + weak var settingsDelegate: SettingsDelegate? + + init(profile: Profile, login: Login) { + self.login = login + self.profile = profile + super.init(nibName: nil, bundle: nil) + + NotificationCenter.default.addObserver(self, selector: #selector(LoginDetailViewController.dismissAlertController), name: NSNotification.Name.UIApplicationDidEnterBackground, object: nil) + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + + navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .edit, target: self, action: #selector(LoginDetailViewController.SELedit)) + + tableView.register(LoginTableViewCell.self, forCellReuseIdentifier: LoginCellIdentifier) + tableView.register(UITableViewCell.self, forCellReuseIdentifier: DefaultCellIdentifier) + tableView.register(SettingsTableSectionHeaderFooterView.self, forHeaderFooterViewReuseIdentifier: SeparatorIdentifier) + + view.addSubview(tableView) + tableView.snp.makeConstraints { make in + make.edges.equalTo(self.view) + } + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + + tableView.separatorColor = UIConstants.TableViewSeparatorColor + tableView.backgroundColor = UIConstants.TableViewHeaderBackgroundColor + tableView.accessibilityIdentifier = "Login Detail List" + tableView.delegate = self + tableView.dataSource = self + + // Add empty footer view to prevent seperators from being drawn past the last item. + tableView.tableFooterView = UIView() + + // Add a line on top of the table view so when the user pulls down it looks 'correct'. + let topLine = UIView(frame: CGRect(origin: CGPoint.zero, size: CGSize(width: tableView.frame.width, height: 0.5))) + topLine.backgroundColor = UIConstants.TableViewSeparatorColor + tableView.tableHeaderView = topLine + + // Normally UITableViewControllers handle responding to content inset changes from keyboard events when editing + // but since we don't use the tableView's editing flag for editing we handle this ourselves. + KeyboardHelper.defaultHelper.addDelegate(self) + } + + deinit { + let notificationCenter = NotificationCenter.default + notificationCenter.removeObserver(self, name: NotificationProfileDidFinishSyncing, object: nil) + notificationCenter.removeObserver(self, name: NSNotification.Name.UIApplicationDidEnterBackground, object: nil) + } + + override func viewDidLayoutSubviews() { + super.viewDidLayoutSubviews() + + // The following hacks are to prevent the default cell seperators from displaying. We want to + // hide the default seperator for the website/last modified cells since the last modified cell + // draws its own separators. The last item in the list draws its seperator full width. + + // Prevent seperators from showing by pushing them off screen by the width of the cell + let itemsToHideSeperators: [InfoItem] = [.passwordItem, .lastModifiedSeparator] + itemsToHideSeperators.forEach { item in + let cell = tableView.cellForRow(at: IndexPath(row: item.rawValue, section: 0)) + cell?.separatorInset = UIEdgeInsets(top: 0, left: cell?.bounds.width ?? 0, bottom: 0, right: 0) + } + + // Rows to display full width seperator + let itemsToShowFullWidthSeperator: [InfoItem] = [.deleteItem] + itemsToShowFullWidthSeperator.forEach { item in + let cell = tableView.cellForRow(at: IndexPath(row: item.rawValue, section: 0)) + cell?.separatorInset = UIEdgeInsets.zero + cell?.layoutMargins = UIEdgeInsets.zero + cell?.preservesSuperviewLayoutMargins = false + } + } +} + +// MARK: - UITableViewDataSource +extension LoginDetailViewController: UITableViewDataSource { + + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + switch InfoItem(rawValue: indexPath.row)! { + case .usernameItem: + let loginCell = dequeueLoginCellForIndexPath(indexPath) + loginCell.style = .noIconAndBothLabels + loginCell.highlightedLabelTitle = NSLocalizedString("username", tableName: "LoginManager", comment: "Label displayed above the username row in Login Detail View.") + loginCell.descriptionLabel.text = login.username + loginCell.descriptionLabel.keyboardType = .emailAddress + loginCell.descriptionLabel.returnKeyType = .next + loginCell.editingDescription = editingInfo + usernameField = loginCell.descriptionLabel + usernameField?.accessibilityIdentifier = "usernameField" + return loginCell + + case .passwordItem: + let loginCell = dequeueLoginCellForIndexPath(indexPath) + loginCell.style = .noIconAndBothLabels + loginCell.highlightedLabelTitle = NSLocalizedString("password", tableName: "LoginManager", comment: "Label displayed above the password row in Login Detail View.") + loginCell.descriptionLabel.text = login.password + loginCell.descriptionLabel.returnKeyType = .default + loginCell.displayDescriptionAsPassword = true + loginCell.editingDescription = editingInfo + passwordField = loginCell.descriptionLabel + passwordField?.accessibilityIdentifier = "passwordField" + return loginCell + + case .websiteItem: + let loginCell = dequeueLoginCellForIndexPath(indexPath) + loginCell.style = .noIconAndBothLabels + loginCell.highlightedLabelTitle = NSLocalizedString("website", tableName: "LoginManager", comment: "Label displayed above the website row in Login Detail View.") + loginCell.descriptionLabel.text = login.hostname + websiteField = loginCell.descriptionLabel + websiteField?.accessibilityIdentifier = "websiteField" + return loginCell + + case .lastModifiedSeparator: + let footer = tableView.dequeueReusableHeaderFooterView(withIdentifier: SeparatorIdentifier) as! SettingsTableSectionHeaderFooterView + footer.titleAlignment = .top + let lastModified = NSLocalizedString("Last modified %@", tableName: "LoginManager", comment: "Footer label describing when the current login was last modified with the timestamp as the parameter.") + let formattedLabel = String(format: lastModified, Date.fromMicrosecondTimestamp(login.timePasswordChanged).toRelativeTimeString()) + footer.titleLabel.text = formattedLabel + let cell = wrapFooter(footer, withCellFromTableView: tableView, atIndexPath: indexPath) + return cell + + case .deleteItem: + let deleteCell = tableView.dequeueReusableCell(withIdentifier: DefaultCellIdentifier, for: indexPath) + deleteCell.textLabel?.text = NSLocalizedString("Delete", tableName: "LoginManager", comment: "Label for the button used to delete the current login.") + deleteCell.textLabel?.textAlignment = NSTextAlignment.center + deleteCell.textLabel?.textColor = UIConstants.DestructiveRed + deleteCell.accessibilityTraits = UIAccessibilityTraitButton + return deleteCell + } + } + + fileprivate func dequeueLoginCellForIndexPath(_ indexPath: IndexPath) -> LoginTableViewCell { + let loginCell = tableView.dequeueReusableCell(withIdentifier: LoginCellIdentifier, for: indexPath) as! LoginTableViewCell + loginCell.selectionStyle = .none + loginCell.delegate = self + return loginCell + } + + fileprivate func wrapFooter(_ footer: UITableViewHeaderFooterView, withCellFromTableView tableView: UITableView, atIndexPath indexPath: IndexPath) -> UITableViewCell { + let cell = tableView.dequeueReusableCell(withIdentifier: DefaultCellIdentifier, for: indexPath) + cell.selectionStyle = .none + cell.addSubview(footer) + footer.snp.makeConstraints { make in + make.edges.equalTo(cell) + } + return cell + } + + func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return 5 + } +} + +// MARK: - UITableViewDelegate +extension LoginDetailViewController: UITableViewDelegate { + private func showMenuOnSingleTap(forIndexPath indexPath: IndexPath) { + guard let item = InfoItem(rawValue: indexPath.row) else { return } + if ![InfoItem.passwordItem, InfoItem.websiteItem, InfoItem.usernameItem].contains(item) { + return + } + + guard let cell = tableView.cellForRow(at: indexPath) as? LoginTableViewCell else { return } + + cell.becomeFirstResponder() + + let menu = UIMenuController.shared + menu.setTargetRect(cell.frame, in: self.tableView) + menu.setMenuVisible(true, animated: true) + } + + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + if indexPath == InfoItem.deleteItem.indexPath { + deleteLogin() + } else if !editingInfo { + showMenuOnSingleTap(forIndexPath: indexPath) + } + tableView.deselectRow(at: indexPath, animated: true) + } + + func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { + switch InfoItem(rawValue: indexPath.row)! { + case .usernameItem, .passwordItem, .websiteItem: + return LoginDetailUX.InfoRowHeight + case .lastModifiedSeparator: + return LoginDetailUX.SeparatorHeight + case .deleteItem: + return LoginDetailUX.DeleteRowHeight + } + } +} + +// MARK: - KeyboardHelperDelegate +extension LoginDetailViewController: KeyboardHelperDelegate { + + func keyboardHelper(_ keyboardHelper: KeyboardHelper, keyboardWillShowWithState state: KeyboardState) { + let coveredHeight = state.intersectionHeightForView(tableView) + tableView.contentInset.bottom = coveredHeight + } + + func keyboardHelper(_ keyboardHelper: KeyboardHelper, keyboardDidShowWithState state: KeyboardState) { + } + + func keyboardHelper(_ keyboardHelper: KeyboardHelper, keyboardWillHideWithState state: KeyboardState) { + tableView.contentInset.bottom = 0 + } +} + +// MARK: - Selectors +extension LoginDetailViewController { + + @objc func dismissAlertController() { + self.deleteAlert?.dismiss(animated: false, completion: nil) + } + + func deleteLogin() { + profile.logins.hasSyncedLogins().uponQueue(DispatchQueue.main) { yes in + self.deleteAlert = UIAlertController.deleteLoginAlertWithDeleteCallback({ [unowned self] _ in + self.profile.logins.removeLoginByGUID(self.login.guid).uponQueue(DispatchQueue.main) { _ in + _ = self.navigationController?.popViewController(animated: true) + } + }, hasSyncedLogins: yes.successValue ?? true) + + self.present(self.deleteAlert!, animated: true, completion: nil) + } + } + + func SELonProfileDidFinishSyncing() { + // Reload details after syncing. + profile.logins.getLoginDataForGUID(login.guid).uponQueue(DispatchQueue.main) { result in + if let syncedLogin = result.successValue { + self.login = syncedLogin + } + } + } + + func SELedit() { + editingInfo = true + + let cell = tableView.cellForRow(at: InfoItem.usernameItem.indexPath) as! LoginTableViewCell + cell.descriptionLabel.becomeFirstResponder() + + navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(LoginDetailViewController.SELdoneEditing)) + } + + func SELdoneEditing() { + editingInfo = false + navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .edit, target: self, action: #selector(LoginDetailViewController.SELedit)) + + defer { + // Required to get UI to reload with changed state + tableView.reloadData() + } + + // We only care to update if we changed something + guard let username = usernameField?.text, + let password = passwordField?.text, username != login.username || password != login.password else { + return + } + + // Keep a copy of the old data in case we fail and need to revert back + let oldPassword = login.password + let oldUsername = login.username + login.update(password: password, username: username) + + if login.isValid.isSuccess { + profile.logins.updateLoginByGUID(login.guid, new: login, significant: true) + } else if let oldUsername = oldUsername { + login.update(password: oldPassword, username: oldUsername) + } + } +} + +// MARK: - Cell Delegate +extension LoginDetailViewController: LoginTableViewCellDelegate { + + fileprivate func cellForItem(_ item: InfoItem) -> LoginTableViewCell? { + return tableView.cellForRow(at: item.indexPath) as? LoginTableViewCell + } + + func didSelectOpenAndFillForCell(_ cell: LoginTableViewCell) { + guard let url = (self.login.formSubmitURL?.asURL ?? self.login.hostname.asURL) else { + return + } + + navigationController?.dismiss(animated: true, completion: { + self.settingsDelegate?.settingsOpenURLInNewTab(url) + }) + } + + func shouldReturnAfterEditingDescription(_ cell: LoginTableViewCell) -> Bool { + let usernameCell = cellForItem(.usernameItem) + let passwordCell = cellForItem(.passwordItem) + + if cell == usernameCell { + passwordCell?.descriptionLabel.becomeFirstResponder() + } + + return false + } + + func infoItemForCell(_ cell: LoginTableViewCell) -> InfoItem? { + if let index = tableView.indexPath(for: cell), + let item = InfoItem(rawValue: index.row) { + return item + } + return nil + } +} diff --git a/mobile/ios/Client/Frontend/Settings/NewTabChoiceViewController.swift b/mobile/ios/Client/Frontend/Settings/NewTabChoiceViewController.swift new file mode 100644 index 0000000000..bbe671a3fc --- /dev/null +++ b/mobile/ios/Client/Frontend/Settings/NewTabChoiceViewController.swift @@ -0,0 +1,84 @@ +/* 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 + +/// Screen presented to the user when selecting the page that is displayed when the user goes to a new tab. +class NewTabChoiceViewController: UITableViewController { + + let newTabOptions: [NewTabPage] = [.blankPage, .topSites, .bookmarks, .history, .homePage] + + let prefs: Prefs + var currentChoice: NewTabPage! + var hasHomePage: Bool! + + fileprivate let BasicCheckmarkCell = "BasicCheckmarkCell" + fileprivate var authenticationInfo: AuthenticationKeychainInfo? + + init(prefs: Prefs) { + self.prefs = prefs + super.init(nibName: nil, bundle: nil) + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + title = Strings.SettingsNewTabTitle + + tableView.accessibilityIdentifier = "NewTabPage.Setting.Options" + + tableView.register(UITableViewCell.self, forCellReuseIdentifier: BasicCheckmarkCell) + tableView.backgroundColor = UIConstants.TableViewHeaderBackgroundColor + + let headerFooterFrame = CGRect(origin: CGPoint.zero, size: CGSize(width: self.view.frame.width, height: UIConstants.TableViewHeaderFooterHeight)) + let headerView = SettingsTableSectionHeaderFooterView(frame: headerFooterFrame) + headerView.showTopBorder = false + headerView.showBottomBorder = true + + let footerView = SettingsTableSectionHeaderFooterView(frame: headerFooterFrame) + footerView.showTopBorder = true + footerView.showBottomBorder = false + + tableView.tableHeaderView = headerView + tableView.tableFooterView = footerView + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + self.currentChoice = NewTabAccessors.getNewTabPage(prefs) + self.hasHomePage = HomePageAccessors.getHomePage(prefs) != nil + tableView.reloadData() + } + + override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + self.prefs.setString(currentChoice.rawValue, forKey: NewTabAccessors.PrefKey) + } + + override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let cell = tableView.dequeueReusableCell(withIdentifier: BasicCheckmarkCell, for: indexPath) + + let option = newTabOptions[indexPath.row] + let enabled = (option != .homePage) || hasHomePage + + cell.accessoryType = (currentChoice == option) ? .checkmark : .none + cell.textLabel?.attributedText = NSAttributedString.tableRowTitle(option.settingTitle, enabled: enabled) + cell.isUserInteractionEnabled = enabled + + return cell + } + + override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return newTabOptions.count + } + + override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + currentChoice = newTabOptions[indexPath.row] + tableView.reloadData() + } +} diff --git a/mobile/ios/Client/Frontend/Settings/NewTabContentSettingsViewController.swift b/mobile/ios/Client/Frontend/Settings/NewTabContentSettingsViewController.swift new file mode 100644 index 0000000000..1e97d23389 --- /dev/null +++ b/mobile/ios/Client/Frontend/Settings/NewTabContentSettingsViewController.swift @@ -0,0 +1,55 @@ +/* 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 + +class CurrentTabSetting: Setting { + let profile: Profile + + override var accessoryType: UITableViewCellAccessoryType { return .disclosureIndicator } + + override var style: UITableViewCellStyle { return .value1 } + + override var accessibilityIdentifier: String? { return "NewTabOption" } + + init(profile: Profile) { + self.profile = profile + super.init(title: NSAttributedString(string: NewTabAccessors.getNewTabPage(profile.prefs).settingTitle, attributes: [NSForegroundColorAttributeName: UIConstants.TableViewRowTextColor])) + } + + override func onClick(_ navigationController: UINavigationController?) { + let viewController = NewTabChoiceViewController(prefs: profile.prefs) + navigationController?.pushViewController(viewController, animated: true) + } +} + +class NewTabContentSettingsViewController: SettingsTableViewController { + + init() { + super.init(style: .grouped) + + self.title = Strings.SettingsNewTabTitle + hasSectionSeparatorLine = false + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func generateSettings() -> [SettingSection] { + let tabSetting = CurrentTabSetting(profile: profile) + let firstSection = SettingSection(title: NSAttributedString(string: Strings.SettingsNewTabSectionName), footerTitle: nil, children: [tabSetting]) + + let isPocketEnabledDefault = Pocket.IslocaleSupported(Locale.current.identifier) + let pocketSetting = BoolSetting(prefs: profile.prefs, prefKey: PrefsKeys.ASPocketStoriesVisible, defaultValue: isPocketEnabledDefault, attributedTitleText: NSAttributedString(string: Strings.SettingsNewTabPocket)) + let bookmarks = BoolSetting(prefs: profile.prefs, prefKey: PrefsKeys.ASBookmarkHighlightsVisible, defaultValue: true, attributedTitleText: NSAttributedString(string: Strings.SettingsNewTabHighlightsBookmarks)) + let history = BoolSetting(prefs: profile.prefs, prefKey: PrefsKeys.ASRecentHighlightsVisible, defaultValue: true, attributedTitleText: NSAttributedString(string: Strings.SettingsNewTabHiglightsHistory)) + let options = AppConstants.MOZ_POCKET_STORIES ? [pocketSetting, bookmarks, history] : [bookmarks, history] + let secondSection = SettingSection(title: NSAttributedString(string: Strings.SettingsNewTabASTitle), footerTitle: nil, children: options) + + return [firstSection, secondSection] + } +} + diff --git a/mobile/ios/Client/Frontend/Settings/SearchEnginePicker.swift b/mobile/ios/Client/Frontend/Settings/SearchEnginePicker.swift new file mode 100644 index 0000000000..602b3475ee --- /dev/null +++ b/mobile/ios/Client/Frontend/Settings/SearchEnginePicker.swift @@ -0,0 +1,47 @@ +/* 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 UIKit + +class SearchEnginePicker: UITableViewController { + weak var delegate: SearchEnginePickerDelegate? + var engines: [OpenSearchEngine]! + var selectedSearchEngineName: String? + + override func viewDidLoad() { + super.viewDidLoad() + + navigationItem.title = NSLocalizedString("Default Search Engine", comment: "Title for default search engine picker.") + navigationItem.leftBarButtonItem = UIBarButtonItem(title: NSLocalizedString("Cancel", comment: "Label for Cancel button"), style: .plain, target: self, action: #selector(SearchEnginePicker.cancel)) + } + + override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return engines.count + } + + override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let engine = engines[indexPath.item] + let cell = UITableViewCell(style: UITableViewCellStyle.default, reuseIdentifier: nil) + cell.textLabel?.text = engine.shortName + cell.imageView?.image = engine.image.createScaled(CGSize(width: OpenSearchEngine.PreferredIconSize, height: OpenSearchEngine.PreferredIconSize)) + if engine.shortName == selectedSearchEngineName { + cell.accessoryType = UITableViewCellAccessoryType.checkmark + } + return cell + } + + override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + let engine = engines[indexPath.item] + delegate?.searchEnginePicker(self, didSelectSearchEngine: engine) + tableView.cellForRow(at: indexPath)?.accessoryType = UITableViewCellAccessoryType.checkmark + } + + override func tableView(_ tableView: UITableView, didDeselectRowAt indexPath: IndexPath) { + tableView.cellForRow(at: indexPath)?.accessoryType = UITableViewCellAccessoryType.none + } + + func cancel() { + delegate?.searchEnginePicker(self, didSelectSearchEngine: nil) + } +} diff --git a/mobile/ios/Client/Frontend/Settings/SearchSettingsTableViewController.swift b/mobile/ios/Client/Frontend/Settings/SearchSettingsTableViewController.swift new file mode 100644 index 0000000000..26356e1d00 --- /dev/null +++ b/mobile/ios/Client/Frontend/Settings/SearchSettingsTableViewController.swift @@ -0,0 +1,331 @@ +/* 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 UIKit +import SDWebImage +import Shared + +protocol SearchEnginePickerDelegate: class { + func searchEnginePicker(_ searchEnginePicker: SearchEnginePicker?, didSelectSearchEngine engine: OpenSearchEngine?) +} + +class SearchSettingsTableViewController: UITableViewController { + fileprivate let SectionDefault = 0 + fileprivate let ItemDefaultEngine = 0 + fileprivate let ItemDefaultSuggestions = 1 + fileprivate let ItemAddCustomSearch = 2 + fileprivate let NumberOfItemsInSectionDefault = 2 + fileprivate let SectionOrder = 1 + fileprivate let NumberOfSections = 2 + fileprivate let IconSize = CGSize(width: OpenSearchEngine.PreferredIconSize, height: OpenSearchEngine.PreferredIconSize) + fileprivate let SectionHeaderIdentifier = "SectionHeaderIdentifier" + + fileprivate var showDeletion = false + + var profile: Profile? + var tabManager: TabManager? + + fileprivate var isEditable: Bool { + // If the default engine is a custom one, make sure we have more than one since we can't edit the default. + // Otherwise, enable editing if we have at least one custom engine. + let customEngineCount = model.orderedEngines.filter({$0.isCustomEngine}).count + return model.defaultEngine.isCustomEngine ? customEngineCount > 1 : customEngineCount > 0 + } + + var model: SearchEngines! + + override func viewDidLoad() { + super.viewDidLoad() + + navigationItem.title = NSLocalizedString("Search", comment: "Navigation title for search settings.") + + // To allow re-ordering the list of search engines at all times. + tableView.isEditing = true + // So that we push the default search engine controller on selection. + tableView.allowsSelectionDuringEditing = true + + tableView.register(SettingsTableSectionHeaderFooterView.self, forHeaderFooterViewReuseIdentifier: SectionHeaderIdentifier) + + // Insert Done button if being presented outside of the Settings Nav stack + if !(self.navigationController is SettingsNavigationController) { + self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: Strings.SettingsSearchDoneButton, style: .done, target: self, action: #selector(self.dismissAnimated)) + } + + let footer = SettingsTableSectionHeaderFooterView(frame: CGRect(x: 0, y: 0, width: tableView.bounds.width, height: 44)) + footer.showBottomBorder = false + tableView.tableFooterView = footer + + tableView.separatorColor = UIConstants.TableViewSeparatorColor + tableView.backgroundColor = UIConstants.TableViewHeaderBackgroundColor + + navigationItem.rightBarButtonItem = UIBarButtonItem(title: Strings.SettingsSearchEditButton, style: .plain, target: self, + action: #selector(SearchSettingsTableViewController.beginEditing)) + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + // Only show the Edit button if custom search engines are in the list. + // Otherwise, there is nothing to delete. + navigationItem.rightBarButtonItem?.isEnabled = isEditable + tableView.reloadData() + } + + override func viewDidDisappear(_ animated: Bool) { + super.viewDidDisappear(animated) + setEditing(false, animated: false) + } + + override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + var cell: UITableViewCell! + var engine: OpenSearchEngine! + + if indexPath.section == SectionDefault { + switch indexPath.item { + case ItemDefaultEngine: + engine = model.defaultEngine + cell = UITableViewCell(style: UITableViewCellStyle.default, reuseIdentifier: nil) + cell.editingAccessoryType = UITableViewCellAccessoryType.disclosureIndicator + cell.accessibilityLabel = NSLocalizedString("Default Search Engine", comment: "Accessibility label for default search engine setting.") + cell.accessibilityValue = engine.shortName + cell.textLabel?.text = engine.shortName + cell.imageView?.image = engine.image.createScaled(IconSize) + cell.imageView?.layer.cornerRadius = 4 + cell.imageView?.layer.masksToBounds = true + case ItemDefaultSuggestions: + cell = UITableViewCell(style: UITableViewCellStyle.default, reuseIdentifier: nil) + cell.textLabel?.text = NSLocalizedString("Show Search Suggestions", comment: "Label for show search suggestions setting.") + let toggle = UISwitch() + toggle.onTintColor = UIConstants.ControlTintColor + toggle.addTarget(self, action: #selector(SearchSettingsTableViewController.didToggleSearchSuggestions(_:)), for: UIControlEvents.valueChanged) + toggle.isOn = model.shouldShowSearchSuggestions + cell.editingAccessoryView = toggle + cell.selectionStyle = .none + default: + // Should not happen. + break + } + } else { + // The default engine is not a quick search engine. + let index = indexPath.item + 1 + if index < model.orderedEngines.count { + engine = model.orderedEngines[index] + + cell = UITableViewCell(style: UITableViewCellStyle.default, reuseIdentifier: nil) + cell.showsReorderControl = true + + let toggle = UISwitch() + toggle.onTintColor = UIConstants.ControlTintColor + // This is an easy way to get from the toggle control to the corresponding index. + toggle.tag = index + toggle.addTarget(self, action: #selector(SearchSettingsTableViewController.didToggleEngine(_:)), for: UIControlEvents.valueChanged) + toggle.isOn = model.isEngineEnabled(engine) + + cell.editingAccessoryView = toggle + cell.textLabel?.text = engine.shortName + cell.textLabel?.adjustsFontSizeToFitWidth = true + cell.textLabel?.minimumScaleFactor = 0.5 + cell.imageView?.image = engine.image.createScaled(IconSize) + cell.imageView?.layer.cornerRadius = 4 + cell.imageView?.layer.masksToBounds = true + cell.selectionStyle = .none + } else { + cell = UITableViewCell(style: UITableViewCellStyle.default, reuseIdentifier: nil) + cell.editingAccessoryType = UITableViewCellAccessoryType.disclosureIndicator + cell.accessibilityLabel = Strings.SettingsAddCustomEngineTitle + cell.accessibilityIdentifier = "customEngineViewButton" + cell.textLabel?.text = Strings.SettingsAddCustomEngine + } + } + + // So that the seperator line goes all the way to the left edge. + cell.separatorInset = UIEdgeInsets.zero + + return cell + } + + override func numberOfSections(in tableView: UITableView) -> Int { + return NumberOfSections + } + + override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + if section == SectionDefault { + return NumberOfItemsInSectionDefault + } else { + // The first engine -- the default engine -- is not shown in the quick search engine list. + // But the option to add Custom Engine is. + return AppConstants.MOZ_CUSTOM_SEARCH_ENGINE ? model.orderedEngines.count : model.orderedEngines.count - 1 + } + } + + override func tableView(_ tableView: UITableView, willSelectRowAt indexPath: IndexPath) -> IndexPath? { + if indexPath.section == SectionDefault && indexPath.item == ItemDefaultEngine { + let searchEnginePicker = SearchEnginePicker() + // Order alphabetically, so that picker is always consistently ordered. + // Every engine is a valid choice for the default engine, even the current default engine. + searchEnginePicker.engines = model.orderedEngines.sorted { e, f in e.shortName < f.shortName } + searchEnginePicker.delegate = self + searchEnginePicker.selectedSearchEngineName = model.defaultEngine.shortName + navigationController?.pushViewController(searchEnginePicker, animated: true) + } else if indexPath.item + 1 == model.orderedEngines.count { + let customSearchEngineForm = CustomSearchViewController() + customSearchEngineForm.profile = self.profile + customSearchEngineForm.successCallback = { + guard let window = self.view.window else { return } + SimpleToast().showAlertWithText(Strings.ThirdPartySearchEngineAdded, bottomContainer: window) + } + navigationController?.pushViewController(customSearchEngineForm, animated: true) + } + return nil + } + + // Don't show delete button on the left. + override func tableView(_ tableView: UITableView, editingStyleForRowAt indexPath: IndexPath) -> UITableViewCellEditingStyle { + if indexPath.section == SectionDefault || indexPath.item + 1 == model.orderedEngines.count { + return UITableViewCellEditingStyle.none + } + + let index = indexPath.item + 1 + let engine = model.orderedEngines[index] + return (self.showDeletion && engine.isCustomEngine) ? .delete : .none + } + + // Don't reserve space for the delete button on the left. + override func tableView(_ tableView: UITableView, shouldIndentWhileEditingRowAt indexPath: IndexPath) -> Bool { + return false + } + + // Hide a thin vertical line that iOS renders between the accessoryView and the reordering control. + override func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) { + if cell.isEditing { + for v in cell.subviews where v.frame.width == 1.0 { + v.backgroundColor = UIColor.clear + } + } + } + + override func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { + return 44 + } + + override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { + let headerView = tableView.dequeueReusableHeaderFooterView(withIdentifier: SectionHeaderIdentifier) as! SettingsTableSectionHeaderFooterView + var sectionTitle: String + if section == SectionDefault { + sectionTitle = NSLocalizedString("Default Search Engine", comment: "Title for default search engine settings section.") + } else { + sectionTitle = NSLocalizedString("Quick-Search Engines", comment: "Title for quick-search engines settings section.") + } + headerView.titleLabel.text = sectionTitle + + return headerView + } + + override func tableView(_ tableView: UITableView, canMoveRowAt indexPath: IndexPath) -> Bool { + if indexPath.section == SectionDefault || indexPath.item + 1 == model.orderedEngines.count { + return false + } else { + return true + } + } + + override func tableView(_ tableView: UITableView, moveRowAt indexPath: IndexPath, to newIndexPath: IndexPath) { + // The first engine (default engine) is not shown in the list, so the indices are off-by-1. + let index = indexPath.item + 1 + let newIndex = newIndexPath.item + 1 + let engine = model.orderedEngines.remove(at: index) + model.orderedEngines.insert(engine, at: newIndex) + tableView.reloadData() + } + + // Snap to first or last row of the list of engines. + override func tableView(_ tableView: UITableView, targetIndexPathForMoveFromRowAt sourceIndexPath: IndexPath, toProposedIndexPath proposedDestinationIndexPath: IndexPath) -> IndexPath { + // You can't drag or drop on the default engine. + if sourceIndexPath.section == SectionDefault || proposedDestinationIndexPath.section == SectionDefault { + return sourceIndexPath + } + + //Can't drag/drop over "Add Custom Engine button" + if sourceIndexPath.item + 1 == model.orderedEngines.count || proposedDestinationIndexPath.item + 1 == model.orderedEngines.count { + return sourceIndexPath + } + + if sourceIndexPath.section != proposedDestinationIndexPath.section { + var row = 0 + if sourceIndexPath.section < proposedDestinationIndexPath.section { + row = tableView.numberOfRows(inSection: sourceIndexPath.section) - 1 + } + return IndexPath(row: row, section: sourceIndexPath.section) + } + return proposedDestinationIndexPath + } + + override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath) { + if editingStyle == .delete { + let index = indexPath.item + 1 + let engine = model.orderedEngines[index] + model.deleteCustomEngine(engine) + tableView.deleteRows(at: [indexPath], with: .right) + + // End editing if we are no longer edit since we've deleted all editable cells. + if !isEditable { + finishEditing() + } + } + } + + override func setEditing(_ editing: Bool, animated: Bool) { + showDeletion = editing + UIView.performWithoutAnimation { + self.navigationItem.rightBarButtonItem?.title = editing ? Strings.SettingsSearchDoneButton : Strings.SettingsSearchEditButton + } + navigationItem.rightBarButtonItem?.isEnabled = isEditable + navigationItem.rightBarButtonItem?.action = editing ? + #selector(SearchSettingsTableViewController.finishEditing) : #selector(SearchSettingsTableViewController.beginEditing) + tableView.reloadData() + } +} + +// MARK: - Selectors +extension SearchSettingsTableViewController { + func didToggleEngine(_ toggle: UISwitch) { + let engine = model.orderedEngines[toggle.tag] // The tag is 1-based. + if toggle.isOn { + model.enableEngine(engine) + } else { + model.disableEngine(engine) + } + } + + func didToggleSearchSuggestions(_ toggle: UISwitch) { + // Setting the value in settings dismisses any opt-in. + model.shouldShowSearchSuggestions = toggle.isOn + } + + func cancel() { + _ = navigationController?.popViewController(animated: true) + } + + func dismissAnimated() { + self.dismiss(animated: true, completion: nil) + } + + func beginEditing() { + setEditing(true, animated: false) + } + + func finishEditing() { + setEditing(false, animated: false) + } +} + +extension SearchSettingsTableViewController: SearchEnginePickerDelegate { + func searchEnginePicker(_ searchEnginePicker: SearchEnginePicker?, didSelectSearchEngine searchEngine: OpenSearchEngine?) { + if let engine = searchEngine { + model.defaultEngine = engine + self.tableView.reloadData() + UnifiedTelemetry.recordEvent(category: .action, method: .change, object: .setting, value: "defaultSearchEngine", extras: ["to": engine.engineID ?? "custom"]) + } + _ = navigationController?.popViewController(animated: true) + } +} diff --git a/mobile/ios/Client/Frontend/Settings/Settings.xcassets/AmberCaution.imageset/AmberCaution.png b/mobile/ios/Client/Frontend/Settings/Settings.xcassets/AmberCaution.imageset/AmberCaution.png new file mode 100644 index 0000000000..db432a6afd Binary files /dev/null and b/mobile/ios/Client/Frontend/Settings/Settings.xcassets/AmberCaution.imageset/AmberCaution.png differ diff --git a/mobile/ios/Client/Frontend/Settings/Settings.xcassets/AmberCaution.imageset/AmberCaution@2x.png b/mobile/ios/Client/Frontend/Settings/Settings.xcassets/AmberCaution.imageset/AmberCaution@2x.png new file mode 100644 index 0000000000..12c8b7d7b5 Binary files /dev/null and b/mobile/ios/Client/Frontend/Settings/Settings.xcassets/AmberCaution.imageset/AmberCaution@2x.png differ diff --git a/mobile/ios/Client/Frontend/Settings/Settings.xcassets/AmberCaution.imageset/AmberCaution@3x.png b/mobile/ios/Client/Frontend/Settings/Settings.xcassets/AmberCaution.imageset/AmberCaution@3x.png new file mode 100644 index 0000000000..3ad7311520 Binary files /dev/null and b/mobile/ios/Client/Frontend/Settings/Settings.xcassets/AmberCaution.imageset/AmberCaution@3x.png differ diff --git a/mobile/ios/Client/Frontend/Settings/Settings.xcassets/AmberCaution.imageset/Contents.json b/mobile/ios/Client/Frontend/Settings/Settings.xcassets/AmberCaution.imageset/Contents.json new file mode 100644 index 0000000000..113bee3719 --- /dev/null +++ b/mobile/ios/Client/Frontend/Settings/Settings.xcassets/AmberCaution.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "AmberCaution.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "AmberCaution@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "AmberCaution@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/mobile/ios/Client/Frontend/Settings/Settings.xcassets/Contents.json b/mobile/ios/Client/Frontend/Settings/Settings.xcassets/Contents.json new file mode 100644 index 0000000000..da4a164c91 --- /dev/null +++ b/mobile/ios/Client/Frontend/Settings/Settings.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/mobile/ios/Client/Frontend/Settings/Settings.xcassets/FxA-Default.imageset/Contents.json b/mobile/ios/Client/Frontend/Settings/Settings.xcassets/FxA-Default.imageset/Contents.json new file mode 100644 index 0000000000..2f661f8eee --- /dev/null +++ b/mobile/ios/Client/Frontend/Settings/Settings.xcassets/FxA-Default.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "FxA-Default.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "FxA-Default@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "FxA-Default@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/mobile/ios/Client/Frontend/Settings/Settings.xcassets/FxA-Default.imageset/FxA-Default.png b/mobile/ios/Client/Frontend/Settings/Settings.xcassets/FxA-Default.imageset/FxA-Default.png new file mode 100644 index 0000000000..39b92bca05 Binary files /dev/null and b/mobile/ios/Client/Frontend/Settings/Settings.xcassets/FxA-Default.imageset/FxA-Default.png differ diff --git a/mobile/ios/Client/Frontend/Settings/Settings.xcassets/FxA-Default.imageset/FxA-Default@2x.png b/mobile/ios/Client/Frontend/Settings/Settings.xcassets/FxA-Default.imageset/FxA-Default@2x.png new file mode 100644 index 0000000000..afde6016ab Binary files /dev/null and b/mobile/ios/Client/Frontend/Settings/Settings.xcassets/FxA-Default.imageset/FxA-Default@2x.png differ diff --git a/mobile/ios/Client/Frontend/Settings/Settings.xcassets/FxA-Default.imageset/FxA-Default@3x.png b/mobile/ios/Client/Frontend/Settings/Settings.xcassets/FxA-Default.imageset/FxA-Default@3x.png new file mode 100644 index 0000000000..6e403c6b21 Binary files /dev/null and b/mobile/ios/Client/Frontend/Settings/Settings.xcassets/FxA-Default.imageset/FxA-Default@3x.png differ diff --git a/mobile/ios/Client/Frontend/Settings/Settings.xcassets/FxA-Sync-Blue.imageset/Contents.json b/mobile/ios/Client/Frontend/Settings/Settings.xcassets/FxA-Sync-Blue.imageset/Contents.json new file mode 100644 index 0000000000..5581f58e3d --- /dev/null +++ b/mobile/ios/Client/Frontend/Settings/Settings.xcassets/FxA-Sync-Blue.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "FxA-Sync-Blue.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "FxA-Sync-Blue@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "FxA-Sync-Blue@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/mobile/ios/Client/Frontend/Settings/Settings.xcassets/FxA-Sync-Blue.imageset/FxA-Sync-Blue.png b/mobile/ios/Client/Frontend/Settings/Settings.xcassets/FxA-Sync-Blue.imageset/FxA-Sync-Blue.png new file mode 100644 index 0000000000..f467f4e311 Binary files /dev/null and b/mobile/ios/Client/Frontend/Settings/Settings.xcassets/FxA-Sync-Blue.imageset/FxA-Sync-Blue.png differ diff --git a/mobile/ios/Client/Frontend/Settings/Settings.xcassets/FxA-Sync-Blue.imageset/FxA-Sync-Blue@2x.png b/mobile/ios/Client/Frontend/Settings/Settings.xcassets/FxA-Sync-Blue.imageset/FxA-Sync-Blue@2x.png new file mode 100644 index 0000000000..cb37121e15 Binary files /dev/null and b/mobile/ios/Client/Frontend/Settings/Settings.xcassets/FxA-Sync-Blue.imageset/FxA-Sync-Blue@2x.png differ diff --git a/mobile/ios/Client/Frontend/Settings/Settings.xcassets/FxA-Sync-Blue.imageset/FxA-Sync-Blue@3x.png b/mobile/ios/Client/Frontend/Settings/Settings.xcassets/FxA-Sync-Blue.imageset/FxA-Sync-Blue@3x.png new file mode 100644 index 0000000000..9340efa913 Binary files /dev/null and b/mobile/ios/Client/Frontend/Settings/Settings.xcassets/FxA-Sync-Blue.imageset/FxA-Sync-Blue@3x.png differ diff --git a/mobile/ios/Client/Frontend/Settings/Settings.xcassets/FxA-Sync.imageset/Contents.json b/mobile/ios/Client/Frontend/Settings/Settings.xcassets/FxA-Sync.imageset/Contents.json new file mode 100644 index 0000000000..471da4f80c --- /dev/null +++ b/mobile/ios/Client/Frontend/Settings/Settings.xcassets/FxA-Sync.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "FxA-Sync.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "FxA-Sync@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "FxA-Sync@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/mobile/ios/Client/Frontend/Settings/Settings.xcassets/FxA-Sync.imageset/FxA-Sync.png b/mobile/ios/Client/Frontend/Settings/Settings.xcassets/FxA-Sync.imageset/FxA-Sync.png new file mode 100644 index 0000000000..bc6494247e Binary files /dev/null and b/mobile/ios/Client/Frontend/Settings/Settings.xcassets/FxA-Sync.imageset/FxA-Sync.png differ diff --git a/mobile/ios/Client/Frontend/Settings/Settings.xcassets/FxA-Sync.imageset/FxA-Sync@2x.png b/mobile/ios/Client/Frontend/Settings/Settings.xcassets/FxA-Sync.imageset/FxA-Sync@2x.png new file mode 100644 index 0000000000..5df878d331 Binary files /dev/null and b/mobile/ios/Client/Frontend/Settings/Settings.xcassets/FxA-Sync.imageset/FxA-Sync@2x.png differ diff --git a/mobile/ios/Client/Frontend/Settings/Settings.xcassets/FxA-Sync.imageset/FxA-Sync@3x.png b/mobile/ios/Client/Frontend/Settings/Settings.xcassets/FxA-Sync.imageset/FxA-Sync@3x.png new file mode 100644 index 0000000000..2b57e082f2 Binary files /dev/null and b/mobile/ios/Client/Frontend/Settings/Settings.xcassets/FxA-Sync.imageset/FxA-Sync@3x.png differ diff --git a/mobile/ios/Client/Frontend/Settings/Settings.xcassets/RedCaution.imageset/Contents.json b/mobile/ios/Client/Frontend/Settings/Settings.xcassets/RedCaution.imageset/Contents.json new file mode 100644 index 0000000000..808d9ef9ca --- /dev/null +++ b/mobile/ios/Client/Frontend/Settings/Settings.xcassets/RedCaution.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "RedCaution.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "RedCaution@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "RedCaution@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/mobile/ios/Client/Frontend/Settings/Settings.xcassets/RedCaution.imageset/RedCaution.png b/mobile/ios/Client/Frontend/Settings/Settings.xcassets/RedCaution.imageset/RedCaution.png new file mode 100644 index 0000000000..212b345f0d Binary files /dev/null and b/mobile/ios/Client/Frontend/Settings/Settings.xcassets/RedCaution.imageset/RedCaution.png differ diff --git a/mobile/ios/Client/Frontend/Settings/Settings.xcassets/RedCaution.imageset/RedCaution@2x.png b/mobile/ios/Client/Frontend/Settings/Settings.xcassets/RedCaution.imageset/RedCaution@2x.png new file mode 100644 index 0000000000..54d324f2a6 Binary files /dev/null and b/mobile/ios/Client/Frontend/Settings/Settings.xcassets/RedCaution.imageset/RedCaution@2x.png differ diff --git a/mobile/ios/Client/Frontend/Settings/Settings.xcassets/RedCaution.imageset/RedCaution@3x.png b/mobile/ios/Client/Frontend/Settings/Settings.xcassets/RedCaution.imageset/RedCaution@3x.png new file mode 100644 index 0000000000..e8a1056c52 Binary files /dev/null and b/mobile/ios/Client/Frontend/Settings/Settings.xcassets/RedCaution.imageset/RedCaution@3x.png differ diff --git a/mobile/ios/Client/Frontend/Settings/Settings.xcassets/placeholder-avatar.imageset/Contents.json b/mobile/ios/Client/Frontend/Settings/Settings.xcassets/placeholder-avatar.imageset/Contents.json new file mode 100644 index 0000000000..7d5c7a1a3f --- /dev/null +++ b/mobile/ios/Client/Frontend/Settings/Settings.xcassets/placeholder-avatar.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "placeholder-avatar.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "placeholder-avatar@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "placeholder-avatar@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/mobile/ios/Client/Frontend/Settings/Settings.xcassets/placeholder-avatar.imageset/placeholder-avatar.png b/mobile/ios/Client/Frontend/Settings/Settings.xcassets/placeholder-avatar.imageset/placeholder-avatar.png new file mode 100644 index 0000000000..67b66e1eb1 Binary files /dev/null and b/mobile/ios/Client/Frontend/Settings/Settings.xcassets/placeholder-avatar.imageset/placeholder-avatar.png differ diff --git a/mobile/ios/Client/Frontend/Settings/Settings.xcassets/placeholder-avatar.imageset/placeholder-avatar@2x.png b/mobile/ios/Client/Frontend/Settings/Settings.xcassets/placeholder-avatar.imageset/placeholder-avatar@2x.png new file mode 100644 index 0000000000..b57a44f6c8 Binary files /dev/null and b/mobile/ios/Client/Frontend/Settings/Settings.xcassets/placeholder-avatar.imageset/placeholder-avatar@2x.png differ diff --git a/mobile/ios/Client/Frontend/Settings/Settings.xcassets/placeholder-avatar.imageset/placeholder-avatar@3x.png b/mobile/ios/Client/Frontend/Settings/Settings.xcassets/placeholder-avatar.imageset/placeholder-avatar@3x.png new file mode 100644 index 0000000000..ce2ea04dab Binary files /dev/null and b/mobile/ios/Client/Frontend/Settings/Settings.xcassets/placeholder-avatar.imageset/placeholder-avatar@3x.png differ diff --git a/mobile/ios/Client/Frontend/Settings/SettingsContentViewController.swift b/mobile/ios/Client/Frontend/Settings/SettingsContentViewController.swift new file mode 100644 index 0000000000..db2b15d2f6 --- /dev/null +++ b/mobile/ios/Client/Frontend/Settings/SettingsContentViewController.swift @@ -0,0 +1,182 @@ +/* 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 SnapKit +import UIKit +import WebKit + +let DefaultTimeoutTimeInterval = 10.0 // Seconds. We'll want some telemetry on load times in the wild. +private var TODOPageLoadErrorString = NSLocalizedString("Could not load page.", comment: "Error message that is shown in settings when there was a problem loading") + +/** + * A controller that manages a single web view and provides a way for + * the user to navigate back to Settings. + */ +class SettingsContentViewController: UIViewController, WKNavigationDelegate { + let interstitialBackgroundColor: UIColor + var settingsTitle: NSAttributedString? + var url: URL! + var timer: Timer? + + var isLoaded: Bool = false { + didSet { + if isLoaded { + UIView.transition(from: interstitialView, to: webView, + duration: 0.5, + options: UIViewAnimationOptions.transitionCrossDissolve, + completion: { finished in + self.interstitialView.removeFromSuperview() + self.interstitialSpinnerView.stopAnimating() + }) + } + } + } + + fileprivate var isError: Bool = false { + didSet { + if isError { + interstitialErrorView.isHidden = false + UIView.transition(from: interstitialSpinnerView, to: interstitialErrorView, + duration: 0.5, + options: UIViewAnimationOptions.transitionCrossDissolve, + completion: { finished in + self.interstitialSpinnerView.removeFromSuperview() + self.interstitialSpinnerView.stopAnimating() + }) + } + } + } + + // The view shown while the content is loading in the background web view. + fileprivate var interstitialView: UIView! + fileprivate var interstitialSpinnerView: UIActivityIndicatorView! + fileprivate var interstitialErrorView: UILabel! + + // The web view that displays content. + var webView: WKWebView! + + fileprivate func startLoading(_ timeout: Double = DefaultTimeoutTimeInterval) { + if self.isLoaded { + return + } + if timeout > 0 { + self.timer = Timer.scheduledTimer(timeInterval: timeout, target: self, selector: #selector(SettingsContentViewController.SELdidTimeOut), userInfo: nil, repeats: false) + } else { + self.timer = nil + } + self.webView.load(URLRequest(url: url)) + self.interstitialSpinnerView.startAnimating() + } + + init(backgroundColor: UIColor = UIColor.white, title: NSAttributedString? = nil) { + interstitialBackgroundColor = backgroundColor + settingsTitle = title + super.init(nibName: nil, bundle: nil) + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + + // This background agrees with the web page background. + // Keeping the background constant prevents a pop of mismatched color. + view.backgroundColor = interstitialBackgroundColor + + self.webView = makeWebView() + view.addSubview(webView) + self.webView.snp.remakeConstraints { make in + make.edges.equalTo(self.view) + } + + // Destructuring let causes problems. + let ret = makeInterstitialViews() + self.interstitialView = ret.0 + self.interstitialSpinnerView = ret.1 + self.interstitialErrorView = ret.2 + view.addSubview(interstitialView) + self.interstitialView.snp.remakeConstraints { make in + make.edges.equalTo(self.view) + } + + startLoading() + } + + func makeWebView() -> WKWebView { + let config = WKWebViewConfiguration() + let webView = WKWebView( + frame: CGRect(x: 0, y: 0, width: 1, height: 1), + configuration: config + ) + webView.allowsLinkPreview = false + webView.navigationDelegate = self + return webView + } + + fileprivate func makeInterstitialViews() -> (UIView, UIActivityIndicatorView, UILabel) { + let view = UIView() + + // Keeping the background constant prevents a pop of mismatched color. + view.backgroundColor = interstitialBackgroundColor + + let spinner = UIActivityIndicatorView(activityIndicatorStyle: UIActivityIndicatorViewStyle.gray) + view.addSubview(spinner) + + let error = UILabel() + if let _ = settingsTitle { + error.text = TODOPageLoadErrorString + error.textColor = UIColor.red // Firefox Orange! + error.textAlignment = NSTextAlignment.center + } + error.isHidden = true + view.addSubview(error) + + spinner.snp.makeConstraints { make in + make.center.equalTo(view) + return + } + + error.snp.makeConstraints { make in + make.center.equalTo(view) + make.left.equalTo(view.snp.left).offset(20) + make.right.equalTo(view.snp.right).offset(-20) + make.height.equalTo(44) + return + } + + return (view, spinner, error) + } + + func SELdidTimeOut() { + self.timer = nil + self.isError = true + } + + func webView(_ webView: WKWebView, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) { + // If this is a request to our local web server, use our private credentials. + if challenge.protectionSpace.host == "localhost" && challenge.protectionSpace.port == Int(WebServer.sharedInstance.server.port) { + completionHandler(.useCredential, WebServer.sharedInstance.credentials) + return + } + completionHandler(URLSession.AuthChallengeDisposition.performDefaultHandling, nil) + } + + func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error) { + SELdidTimeOut() + } + + func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) { + SELdidTimeOut() + } + + func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { + self.timer?.invalidate() + self.timer = nil + self.isLoaded = true + } +} diff --git a/mobile/ios/Client/Frontend/Settings/SettingsNavigationController.swift b/mobile/ios/Client/Frontend/Settings/SettingsNavigationController.swift new file mode 100644 index 0000000000..c62036b34a --- /dev/null +++ b/mobile/ios/Client/Frontend/Settings/SettingsNavigationController.swift @@ -0,0 +1,31 @@ +/* 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 UIKit + +class SettingsNavigationController: UINavigationController { + var popoverDelegate: PresentingModalViewControllerDelegate? + + func SELdone() { + if let delegate = popoverDelegate { + delegate.dismissPresentedModalViewController(self, animated: true) + } else { + self.dismiss(animated: true, completion: nil) + } + } + + override var preferredStatusBarStyle: UIStatusBarStyle { + return UIStatusBarStyle.default + } +} + +protocol PresentingModalViewControllerDelegate { + func dismissPresentedModalViewController(_ modalViewController: UIViewController, animated: Bool) +} + +class ModalSettingsNavigationController: UINavigationController { + override var preferredStatusBarStyle: UIStatusBarStyle { + return UIStatusBarStyle.default + } +} diff --git a/mobile/ios/Client/Frontend/Settings/SettingsTableViewController.swift b/mobile/ios/Client/Frontend/Settings/SettingsTableViewController.swift new file mode 100644 index 0000000000..fee3289e5b --- /dev/null +++ b/mobile/ios/Client/Frontend/Settings/SettingsTableViewController.swift @@ -0,0 +1,738 @@ +/* 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 Account +import Shared +import UIKit + +// A base setting class that shows a title. You probably want to subclass this, not use it directly. +class Setting: NSObject { + fileprivate var _title: NSAttributedString? + fileprivate var _footerTitle: NSAttributedString? + fileprivate var _cellHeight: CGFloat? + fileprivate var _image: UIImage? + + weak var delegate: SettingsDelegate? + + // The url the SettingsContentViewController will show, e.g. Licenses and Privacy Policy. + var url: URL? { return nil } + + // The title shown on the pref. + var title: NSAttributedString? { return _title } + var footerTitle: NSAttributedString? { return _footerTitle } + var cellHeight: CGFloat? { return _cellHeight} + fileprivate(set) var accessibilityIdentifier: String? + + // An optional second line of text shown on the pref. + var status: NSAttributedString? { return nil } + + // Whether or not to show this pref. + var hidden: Bool { return false } + + var style: UITableViewCellStyle { return .subtitle } + + var accessoryType: UITableViewCellAccessoryType { return .none } + + var textAlignment: NSTextAlignment { return .natural } + + var image: UIImage? { return _image } + + fileprivate(set) var enabled: Bool = true + + // Called when the cell is setup. Call if you need the default behaviour. + func onConfigureCell(_ cell: UITableViewCell) { + cell.detailTextLabel?.attributedText = status + cell.detailTextLabel?.numberOfLines = 0 + cell.textLabel?.attributedText = title + cell.textLabel?.textAlignment = textAlignment + cell.textLabel?.numberOfLines = 1 + cell.textLabel?.lineBreakMode = .byTruncatingTail + cell.accessoryType = accessoryType + cell.accessoryView = nil + cell.selectionStyle = enabled ? .default : .none + cell.accessibilityIdentifier = accessibilityIdentifier + cell.imageView?.image = _image + if let title = title?.string { + if let detailText = cell.detailTextLabel?.text { + cell.accessibilityLabel = "\(title), \(detailText)" + } else if let status = status?.string { + cell.accessibilityLabel = "\(title), \(status)" + } else { + cell.accessibilityLabel = title + } + } + cell.accessibilityTraits = UIAccessibilityTraitButton + cell.indentationWidth = 0 + cell.layoutMargins = UIEdgeInsets.zero + // So that the separator line goes all the way to the left edge. + cell.separatorInset = UIEdgeInsets.zero + } + + // Called when the pref is tapped. + func onClick(_ navigationController: UINavigationController?) { return } + + // Helper method to set up and push a SettingsContentViewController + func setUpAndPushSettingsContentViewController(_ navigationController: UINavigationController?) { + if let url = self.url { + let viewController = SettingsContentViewController() + viewController.settingsTitle = self.title + viewController.url = url + navigationController?.pushViewController(viewController, animated: true) + } + } + + init(title: NSAttributedString? = nil, footerTitle: NSAttributedString? = nil, cellHeight: CGFloat? = nil, delegate: SettingsDelegate? = nil, enabled: Bool? = nil) { + self._title = title + self._footerTitle = footerTitle + self._cellHeight = cellHeight + self.delegate = delegate + self.enabled = enabled ?? true + } +} + +// A setting in the sections panel. Contains a sublist of Settings +class SettingSection: Setting { + fileprivate let children: [Setting] + + init(title: NSAttributedString? = nil, footerTitle: NSAttributedString? = nil, cellHeight: CGFloat? = nil, children: [Setting]) { + self.children = children + super.init(title: title, footerTitle: footerTitle, cellHeight: cellHeight) + } + + var count: Int { + var count = 0 + for setting in children where !setting.hidden { + count += 1 + } + return count + } + + subscript(val: Int) -> Setting? { + var i = 0 + for setting in children where !setting.hidden { + if i == val { + return setting + } + i += 1 + } + return nil + } +} + +private class PaddedSwitch: UIView { + fileprivate static let Padding: CGFloat = 8 + + init(switchView: UISwitch) { + super.init(frame: CGRect.zero) + + addSubview(switchView) + + frame.size = CGSize(width: switchView.frame.width + PaddedSwitch.Padding, height: switchView.frame.height) + switchView.frame.origin = CGPoint(x: PaddedSwitch.Padding, y: 0) + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +// A helper class for settings with a UISwitch. +// Takes and optional settingsDidChange callback and status text. +class BoolSetting: Setting { + let prefKey: String? // Sometimes a subclass will manage its own pref setting. In that case the prefkey will be nil + + fileprivate let prefs: Prefs + fileprivate let defaultValue: Bool + fileprivate let settingDidChange: ((Bool) -> Void)? + fileprivate let statusText: NSAttributedString? + + init(prefs: Prefs, prefKey: String? = nil, defaultValue: Bool, attributedTitleText: NSAttributedString, attributedStatusText: NSAttributedString? = nil, settingDidChange: ((Bool) -> Void)? = nil) { + self.prefs = prefs + self.prefKey = prefKey + self.defaultValue = defaultValue + self.settingDidChange = settingDidChange + self.statusText = attributedStatusText + super.init(title: attributedTitleText) + } + + convenience init(prefs: Prefs, prefKey: String? = nil, defaultValue: Bool, titleText: String, statusText: String? = nil, settingDidChange: ((Bool) -> Void)? = nil) { + var statusTextAttributedString: NSAttributedString? + if let statusTextString = statusText { + statusTextAttributedString = NSAttributedString(string: statusTextString, attributes: [NSForegroundColorAttributeName: UIConstants.TableViewHeaderTextColor]) + } + self.init(prefs: prefs, prefKey: prefKey, defaultValue: defaultValue, attributedTitleText: NSAttributedString(string: titleText, attributes: [NSForegroundColorAttributeName: UIConstants.TableViewRowTextColor]), attributedStatusText: statusTextAttributedString, settingDidChange: settingDidChange) + } + + override var status: NSAttributedString? { + return statusText + } + + override func onConfigureCell(_ cell: UITableViewCell) { + super.onConfigureCell(cell) + + let control = UISwitch() + control.onTintColor = UIConstants.SystemBlueColor + control.addTarget(self, action: #selector(BoolSetting.switchValueChanged(_:)), for: UIControlEvents.valueChanged) + displayBool(control) + if let title = title { + if let status = status { + control.accessibilityLabel = "\(title.string), \(status.string)" + } else { + control.accessibilityLabel = title.string + } + cell.accessibilityLabel = nil + } + cell.accessoryView = PaddedSwitch(switchView: control) + cell.selectionStyle = .none + } + + @objc func switchValueChanged(_ control: UISwitch) { + writeBool(control) + settingDidChange?(control.isOn) + UnifiedTelemetry.recordEvent(category: .action, method: .change, object: .setting, value: self.prefKey, extras: ["to": control.isOn]) + } + + // These methods allow a subclass to control how the pref is saved + func displayBool(_ control: UISwitch) { + guard let key = prefKey else { + return + } + control.isOn = prefs.boolForKey(key) ?? defaultValue + } + + func writeBool(_ control: UISwitch) { + guard let key = prefKey else { + return + } + prefs.setBool(control.isOn, forKey: key) + } +} + +/// A helper class for a setting backed by a UITextField. +/// This takes an optional settingIsValid and settingDidChange callback +/// If settingIsValid returns false, the Setting will not change and the text remains red. +class StringSetting: Setting, UITextFieldDelegate { + + let prefKey: String + fileprivate let Padding: CGFloat = 8 + + fileprivate let prefs: Prefs + fileprivate let defaultValue: String? + fileprivate let placeholder: String + fileprivate let settingDidChange: ((String?) -> Void)? + fileprivate let settingIsValid: ((String?) -> Bool)? + + let textField = UITextField() + + init(prefs: Prefs, prefKey: String, defaultValue: String? = nil, placeholder: String, accessibilityIdentifier: String, settingIsValid isValueValid: ((String?) -> Bool)? = nil, settingDidChange: ((String?) -> Void)? = nil) { + self.prefs = prefs + self.prefKey = prefKey + self.defaultValue = defaultValue + self.settingDidChange = settingDidChange + self.settingIsValid = isValueValid + self.placeholder = placeholder + + super.init() + self.accessibilityIdentifier = accessibilityIdentifier + } + + override func onConfigureCell(_ cell: UITableViewCell) { + super.onConfigureCell(cell) + if let id = accessibilityIdentifier { + textField.accessibilityIdentifier = id + "TextField" + } + textField.placeholder = placeholder + textField.textAlignment = .center + textField.delegate = self + textField.addTarget(self, action: #selector(textFieldDidChange), for: .editingChanged) + cell.isUserInteractionEnabled = true + cell.accessibilityTraits = UIAccessibilityTraitNone + cell.contentView.addSubview(textField) + + textField.snp.makeConstraints { make in + make.height.equalTo(44) + make.trailing.equalTo(cell.contentView).offset(-Padding) + make.leading.equalTo(cell.contentView).offset(Padding) + } + textField.text = prefs.stringForKey(prefKey) ?? defaultValue + textFieldDidChange(textField) + } + + override func onClick(_ navigationController: UINavigationController?) { + textField.becomeFirstResponder() + } + + fileprivate func isValid(_ value: String?) -> Bool { + guard let test = settingIsValid else { + return true + } + return test(prepareValidValue(userInput: value)) + } + + /// This gives subclasses an opportunity to treat the user input string + /// before it is saved or tested. + /// Default implementation does nothing. + func prepareValidValue(userInput value: String?) -> String? { + return value + } + + @objc func textFieldDidChange(_ textField: UITextField) { + let color = isValid(textField.text) ? UIConstants.TableViewRowTextColor : UIConstants.DestructiveRed + textField.textColor = color + } + + @objc func textFieldShouldReturn(_ textField: UITextField) -> Bool { + return isValid(textField.text) + } + + @objc func textFieldDidEndEditing(_ textField: UITextField) { + let text = textField.text + if !isValid(text) { + return + } + if let text = prepareValidValue(userInput: text) { + prefs.setString(text, forKey: prefKey) + } else { + prefs.removeObjectForKey(prefKey) + } + // Call settingDidChange with text or nil. + settingDidChange?(text) + } +} + +class CheckmarkSetting: Setting { + let onChanged: () -> Void + let isEnabled: () -> Bool + private let subtitle: NSAttributedString? + + override var status: NSAttributedString? { + return subtitle + } + + init(title: NSAttributedString, subtitle: NSAttributedString?, accessibilityIdentifier: String? = nil, isEnabled: @escaping () -> Bool, onChanged: @escaping () -> Void) { + self.subtitle = subtitle + self.onChanged = onChanged + self.isEnabled = isEnabled + super.init(title: title) + self.accessibilityIdentifier = accessibilityIdentifier + } + + override func onConfigureCell(_ cell: UITableViewCell) { + super.onConfigureCell(cell) + cell.accessoryType = isEnabled() ? .checkmark : .none + cell.selectionStyle = .none + } + + override func onClick(_ navigationController: UINavigationController?) { + // Force editing to end for any focused text fields so they can finish up validation first. + navigationController?.view.endEditing(true) + if !isEnabled() { + onChanged() + } + } +} + +/// A helper class for a setting backed by a UITextField. +/// This takes an optional isEnabled and mandatory onClick callback +/// isEnabled is called on each tableview.reloadData. If it returns +/// false then the 'button' appears disabled. +class ButtonSetting: Setting { + let onButtonClick: (UINavigationController?) -> Void + let destructive: Bool + let isEnabled: (() -> Bool)? + + init(title: NSAttributedString?, destructive: Bool = false, accessibilityIdentifier: String, isEnabled: (() -> Bool)? = nil, onClick: @escaping (UINavigationController?) -> Void) { + self.onButtonClick = onClick + self.destructive = destructive + self.isEnabled = isEnabled + super.init(title: title) + self.accessibilityIdentifier = accessibilityIdentifier + } + + override func onConfigureCell(_ cell: UITableViewCell) { + super.onConfigureCell(cell) + + if isEnabled?() ?? true { + cell.textLabel?.textColor = destructive ? UIConstants.DestructiveRed : UIConstants.HighlightBlue + } else { + cell.textLabel?.textColor = UIConstants.TableViewDisabledRowTextColor + } + cell.textLabel?.textAlignment = NSTextAlignment.center + cell.accessibilityTraits = UIAccessibilityTraitButton + cell.selectionStyle = .none + } + + override func onClick(_ navigationController: UINavigationController?) { + // Force editing to end for any focused text fields so they can finish up validation first. + navigationController?.view.endEditing(true) + if isEnabled?() ?? true { + onButtonClick(navigationController) + } + } +} + +// A helper class for prefs that deal with sync. Handles reloading the tableView data if changes to +// the fxAccount happen. +class AccountSetting: Setting, FxAContentViewControllerDelegate { + unowned var settings: SettingsTableViewController + + var profile: Profile { + return settings.profile + } + + override var title: NSAttributedString? { return nil } + + init(settings: SettingsTableViewController) { + self.settings = settings + super.init(title: nil) + } + + override func onConfigureCell(_ cell: UITableViewCell) { + super.onConfigureCell(cell) + if settings.profile.getAccount() != nil { + cell.selectionStyle = .none + } + } + + override var accessoryType: UITableViewCellAccessoryType { return .none } + + func contentViewControllerDidSignIn(_ viewController: FxAContentViewController, withFlags flags: FxALoginFlags) { + // This method will get called twice: once when the user signs in, and once + // when the account is verified by email – on this device or another. + // If the user hasn't dismissed the fxa content view controller, + // then we should only do that (thus finishing the sign in/verification process) + // once the account is verified. + // By the time we get to here, we should be syncing or just about to sync in the + // background, most likely from FxALoginHelper. + if flags.verified { + _ = settings.navigationController?.popToRootViewController(animated: true) + // Reload the data to reflect the new Account immediately. + settings.tableView.reloadData() + // And start advancing the Account state in the background as well. + settings.SELrefresh() + } + } + + func contentViewControllerDidCancel(_ viewController: FxAContentViewController) { + NSLog("didCancel") + _ = settings.navigationController?.popToRootViewController(animated: true) + } +} + +class WithAccountSetting: AccountSetting { + override var hidden: Bool { return !profile.hasAccount() } +} + +class WithoutAccountSetting: AccountSetting { + override var hidden: Bool { return profile.hasAccount() } +} + +@objc +protocol SettingsDelegate: class { + func settingsOpenURLInNewTab(_ url: URL) +} + +// The base settings view controller. +class SettingsTableViewController: UITableViewController { + + typealias SettingsGenerator = (SettingsTableViewController, SettingsDelegate?) -> [SettingSection] + + fileprivate let Identifier = "CellIdentifier" + fileprivate let SectionHeaderIdentifier = "SectionHeaderIdentifier" + var settings = [SettingSection]() + + weak var settingsDelegate: SettingsDelegate? + + var profile: Profile! + var tabManager: TabManager! + + var hasSectionSeparatorLine = true + + /// Used to calculate cell heights. + fileprivate lazy var dummyToggleCell: UITableViewCell = { + let cell = UITableViewCell(style: .subtitle, reuseIdentifier: "dummyCell") + cell.accessoryView = UISwitch() + return cell + }() + + override func viewDidLoad() { + super.viewDidLoad() + + tableView.register(UITableViewCell.self, forCellReuseIdentifier: Identifier) + tableView.register(SettingsTableSectionHeaderFooterView.self, forHeaderFooterViewReuseIdentifier: SectionHeaderIdentifier) + tableView.separatorColor = UIConstants.TableViewSeparatorColor + tableView.backgroundColor = UIConstants.TableViewHeaderBackgroundColor + tableView.tableFooterView = UIView(frame: CGRect(x: 0, y: 0, width: view.frame.width, height: 30)) + tableView.estimatedRowHeight = 44 + tableView.estimatedSectionHeaderHeight = 44 + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + + settings = generateSettings() + + NotificationCenter.default.addObserver(self, selector: #selector(SettingsTableViewController.SELsyncDidChangeState), name: NotificationProfileDidStartSyncing, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(SettingsTableViewController.SELsyncDidChangeState), name: NotificationProfileDidFinishSyncing, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(SettingsTableViewController.SELfirefoxAccountDidChange), name: NotificationFirefoxAccountChanged, object: nil) + + tableView.reloadData() + } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + SELrefresh() + } + + override func viewDidDisappear(_ animated: Bool) { + super.viewDidDisappear(animated) + NotificationCenter.default.removeObserver(self, name: NotificationProfileDidStartSyncing, object: nil) + NotificationCenter.default.removeObserver(self, name: NotificationProfileDidFinishSyncing, object: nil) + NotificationCenter.default.removeObserver(self, name: NotificationFirefoxAccountChanged, object: nil) + } + + // Override to provide settings in subclasses + func generateSettings() -> [SettingSection] { + return [] + } + + @objc fileprivate func SELsyncDidChangeState() { + DispatchQueue.main.async { + self.tableView.reloadData() + } + } + + @objc fileprivate func SELrefresh() { + // Through-out, be aware that modifying the control while a refresh is in progress is /not/ supported and will likely crash the app. + if let account = self.profile.getAccount() { + account.advance().upon { state in + DispatchQueue.main.async { () -> Void in + self.tableView.reloadData() + } + } + } else { + self.tableView.reloadData() + } + } + + @objc func SELfirefoxAccountDidChange() { + self.tableView.reloadData() + } + + override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let section = settings[indexPath.section] + if let setting = section[indexPath.row] { + var cell: UITableViewCell! + if let _ = setting.status { + // Work around http://stackoverflow.com/a/9999821 and http://stackoverflow.com/a/25901083 by using a new cell. + // I could not make any setNeedsLayout solution work in the case where we disconnect and then connect a new account. + // Be aware that dequeing and then ignoring a cell appears to cause issues; only deque a cell if you're going to return it. + cell = UITableViewCell(style: setting.style, reuseIdentifier: nil) + } else { + cell = tableView.dequeueReusableCell(withIdentifier: Identifier, for: indexPath) + } + setting.onConfigureCell(cell) + return cell + } + return tableView.dequeueReusableCell(withIdentifier: Identifier, for: indexPath) + } + + override func numberOfSections(in tableView: UITableView) -> Int { + return settings.count + } + + override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + let section = settings[section] + return section.count + } + + override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { + let headerView = tableView.dequeueReusableHeaderFooterView(withIdentifier: SectionHeaderIdentifier) as! SettingsTableSectionHeaderFooterView + let sectionSetting = settings[section] + if let sectionTitle = sectionSetting.title?.string { + headerView.titleLabel.text = sectionTitle.uppercased() + } + // Hide the top border for the top section to avoid having a double line at the top + if section == 0 || !hasSectionSeparatorLine { + headerView.showTopBorder = false + } else { + headerView.showTopBorder = true + } + + return headerView + } + + override func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? { + let sectionSetting = settings[section] + guard let sectionFooter = sectionSetting.footerTitle?.string else { + return nil + } + let footerView = tableView.dequeueReusableHeaderFooterView(withIdentifier: SectionHeaderIdentifier) as! SettingsTableSectionHeaderFooterView + footerView.titleLabel.text = sectionFooter + footerView.titleAlignment = .top + footerView.showBottomBorder = false + return footerView + } + + // To hide a footer dynamically requires returning nil from viewForFooterInSection + // and setting the height to zero. + // However, we also want the height dynamically calculated, there is a magic constant + // for that: `UITableViewAutomaticDimension`. + override func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat { + let sectionSetting = settings[section] + if let _ = sectionSetting.footerTitle?.string { + return UITableViewAutomaticDimension + } + return 0 + } + + override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { + let section = settings[indexPath.section] + // Workaround for calculating the height of default UITableViewCell cells with a subtitle under + // the title text label. + if let setting = section[indexPath.row], setting is BoolSetting && setting.status != nil { + return calculateStatusCellHeightForSetting(setting) + } + if let setting = section[indexPath.row], let height = setting.cellHeight { + return height + } + + return UITableViewAutomaticDimension + } + + override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + let section = settings[indexPath.section] + if let setting = section[indexPath.row], setting.enabled { + setting.onClick(navigationController) + } + } + + fileprivate func calculateStatusCellHeightForSetting(_ setting: Setting) -> CGFloat { + dummyToggleCell.layoutSubviews() + + let topBottomMargin: CGFloat = 10 + let width = dummyToggleCell.contentView.frame.width - 2 * dummyToggleCell.separatorInset.left + + return + heightForLabel(dummyToggleCell.textLabel!, width: width, text: setting.title?.string) + + heightForLabel(dummyToggleCell.detailTextLabel!, width: width, text: setting.status?.string) + + 2 * topBottomMargin + } + + fileprivate func heightForLabel(_ label: UILabel, width: CGFloat, text: String?) -> CGFloat { + guard let text = text else { return 0 } + + let size = CGSize(width: width, height: CGFloat.greatestFiniteMagnitude) + let attrs = [NSFontAttributeName: label.font as Any] + let boundingRect = NSString(string: text).boundingRect(with: size, + options: NSStringDrawingOptions.usesLineFragmentOrigin, attributes: attrs, context: nil) + return boundingRect.height + } +} + +struct SettingsTableSectionHeaderFooterViewUX { + static let titleHorizontalPadding: CGFloat = 15 + static let titleVerticalPadding: CGFloat = 6 + static let titleVerticalLongPadding: CGFloat = 20 +} + +class SettingsTableSectionHeaderFooterView: UITableViewHeaderFooterView { + + enum TitleAlignment { + case top + case bottom + } + + var titleAlignment: TitleAlignment = .bottom { + didSet { + remakeTitleAlignmentConstraints() + } + } + + var showTopBorder: Bool = true { + didSet { + topBorder.isHidden = !showTopBorder + } + } + + var showBottomBorder: Bool = true { + didSet { + bottomBorder.isHidden = !showBottomBorder + } + } + + lazy var titleLabel: UILabel = { + var headerLabel = UILabel() + headerLabel.textColor = UIConstants.TableViewHeaderTextColor + headerLabel.font = UIFont.systemFont(ofSize: 12.0, weight: UIFontWeightRegular) + headerLabel.numberOfLines = 0 + return headerLabel + }() + + fileprivate lazy var topBorder: UIView = { + let topBorder = UIView() + topBorder.backgroundColor = UIConstants.SeparatorColor + return topBorder + }() + + fileprivate lazy var bottomBorder: UIView = { + let bottomBorder = UIView() + bottomBorder.backgroundColor = UIConstants.SeparatorColor + return bottomBorder + }() + + override init(reuseIdentifier: String?) { + super.init(reuseIdentifier: reuseIdentifier) + contentView.backgroundColor = UIConstants.TableViewHeaderBackgroundColor + addSubview(titleLabel) + addSubview(topBorder) + addSubview(bottomBorder) + + setupInitialConstraints() + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func setupInitialConstraints() { + bottomBorder.snp.makeConstraints { make in + make.bottom.left.right.equalTo(self) + make.height.equalTo(0.5) + } + + topBorder.snp.makeConstraints { make in + make.top.left.right.equalTo(self) + make.height.equalTo(0.5) + } + + remakeTitleAlignmentConstraints() + } + + override func prepareForReuse() { + super.prepareForReuse() + showTopBorder = true + showBottomBorder = true + titleLabel.text = nil + titleAlignment = .bottom + } + + fileprivate func remakeTitleAlignmentConstraints() { + switch titleAlignment { + case .top: + titleLabel.snp.remakeConstraints { make in + make.left.right.equalTo(self).inset(SettingsTableSectionHeaderFooterViewUX.titleHorizontalPadding) + make.top.equalTo(self).offset(SettingsTableSectionHeaderFooterViewUX.titleVerticalPadding) + make.bottom.equalTo(self).offset(-SettingsTableSectionHeaderFooterViewUX.titleVerticalLongPadding) + } + case .bottom: + titleLabel.snp.remakeConstraints { make in + make.left.right.equalTo(self).inset(SettingsTableSectionHeaderFooterViewUX.titleHorizontalPadding) + make.bottom.equalTo(self).offset(-SettingsTableSectionHeaderFooterViewUX.titleVerticalPadding) + make.top.equalTo(self).offset(SettingsTableSectionHeaderFooterViewUX.titleVerticalLongPadding) + } + } + } +} + diff --git a/mobile/ios/Client/Frontend/Share/ShareExtensionHelper.swift b/mobile/ios/Client/Frontend/Share/ShareExtensionHelper.swift new file mode 100644 index 0000000000..442cb6beae --- /dev/null +++ b/mobile/ios/Client/Frontend/Share/ShareExtensionHelper.swift @@ -0,0 +1,148 @@ +/* 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 OnePasswordExtension + +private let log = Logger.browserLogger + +class ShareExtensionHelper: NSObject { + fileprivate weak var selectedTab: Tab? + + fileprivate let selectedURL: URL + fileprivate var onePasswordExtensionItem: NSExtensionItem! + fileprivate let browserFillIdentifier = "org.appextension.fill-browser-action" + + init(url: URL, tab: Tab?) { + self.selectedURL = tab?.canonicalURL?.displayURL ?? url + self.selectedTab = tab + } + + func createActivityViewController(_ completionHandler: @escaping (_ completed: Bool, _ activityType: String?) -> Void) -> UIActivityViewController { + var activityItems = [AnyObject]() + + let printInfo = UIPrintInfo(dictionary: nil) + + let absoluteString = selectedTab?.url?.absoluteString ?? selectedURL.absoluteString + printInfo.jobName = absoluteString + printInfo.outputType = .general + activityItems.append(printInfo) + + if let tab = selectedTab { + activityItems.append(TabPrintPageRenderer(tab: tab)) + } + + if let title = selectedTab?.title { + activityItems.append(TitleActivityItemProvider(title: title)) + } + activityItems.append(self) + + let activityViewController = UIActivityViewController(activityItems: activityItems, applicationActivities: nil) + + // Hide 'Add to Reading List' which currently uses Safari. + // We would also hide View Later, if possible, but the exclusion list doesn't currently support + // third-party activity types (rdar://19430419). + activityViewController.excludedActivityTypes = [ + UIActivityType.addToReadingList, + ] + + // This needs to be ready by the time the share menu has been displayed and + // activityViewController(activityViewController:, activityType:) is called, + // which is after the user taps the button. So a million cycles away. + findLoginExtensionItem() + + activityViewController.completionWithItemsHandler = { activityType, completed, returnedItems, activityError in + if !completed { + completionHandler(completed, activityType.map { $0.rawValue }) + return + } + // Bug 1392418 - When copying a url using the share extension there are 2 urls in the pasteboard. + // Make sure the pasteboard only has one url. + if let url = UIPasteboard.general.urls?.first { + UIPasteboard.general.urls = [url] + } + + if self.isPasswordManagerActivityType(activityType.map { $0.rawValue }) { + if let logins = returnedItems { + self.fillPasswords(logins as [AnyObject]) + } + } + + completionHandler(completed, activityType.map { $0.rawValue }) + } + return activityViewController + } +} + +extension ShareExtensionHelper: UIActivityItemSource { + func activityViewControllerPlaceholderItem(_ activityViewController: UIActivityViewController) -> Any { + return selectedURL + } + + // IMPORTANT: This method needs Swift compiler optimization DISABLED to prevent a nasty + // crash from happening in release builds. It seems as though the check for `nil` may + // get removed by the optimizer which leads to a crash when that happens. + @_semantics("optimize.sil.never") func activityViewController(_ activityViewController: UIActivityViewController, itemForActivityType activityType: UIActivityType) -> Any? { + // activityType actually is nil sometimes (in the simulator at least) + if activityType != nil && isPasswordManagerActivityType(activityType.rawValue) { + return onePasswordExtensionItem + } else { + // Return the URL for the selected tab. If we are in reader view then decode + // it so that we copy the original and not the internal localhost one. + return selectedURL.isReaderModeURL ? selectedURL.decodeReaderModeURL : selectedURL + } + } + + func activityViewController(_ activityViewController: UIActivityViewController, dataTypeIdentifierForActivityType activityType: UIActivityType?) -> String { + if let type = activityType, isPasswordManagerActivityType(type.rawValue) { + return browserFillIdentifier + } + return activityType == nil ? browserFillIdentifier : kUTTypeURL as String + } +} + +private extension ShareExtensionHelper { + + func isPasswordManagerActivityType(_ activityType: String?) -> Bool { + // A 'password' substring covers the most cases, such as pwsafe and 1Password. + // com.agilebits.onepassword-ios.extension + // com.app77.ios.pwsafe2.find-login-action-password-actionExtension + // If your extension's bundle identifier does not contain "password", simply submit a pull request by adding your bundle identifier. + return (activityType?.range(of: "password") != nil) + || (activityType == "com.lastpass.ilastpass.LastPassExt") + || (activityType == "in.sinew.Walletx.WalletxExt") + || (activityType == "com.8bit.bitwarden.find-login-action-extension") + + } + + func findLoginExtensionItem() { + guard let selectedWebView = selectedTab?.webView else { + return + } + + // Add 1Password to share sheet + OnePasswordExtension.shared().createExtensionItem(forWebView: selectedWebView, completion: {(extensionItem, error) -> Void in + if extensionItem == nil { + log.error("Failed to create the password manager extension item: \(error.debugDescription).") + return + } + + // Set the 1Password extension item property + self.onePasswordExtensionItem = extensionItem + }) + } + + func fillPasswords(_ returnedItems: [AnyObject]) { + guard let selectedWebView = selectedTab?.webView else { + return + } + + OnePasswordExtension.shared().fillReturnedItems(returnedItems, intoWebView: selectedWebView, completion: { (success, returnedItemsError) -> Void in + if !success { + log.error("Failed to fill item into webview: \(returnedItemsError ??? "nil").") + } + }) + } +} diff --git a/mobile/ios/Client/Frontend/Share/TitleActivityItemProvider.swift b/mobile/ios/Client/Frontend/Share/TitleActivityItemProvider.swift new file mode 100644 index 0000000000..1e58531d36 --- /dev/null +++ b/mobile/ios/Client/Frontend/Share/TitleActivityItemProvider.swift @@ -0,0 +1,33 @@ +/* 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 + +/// This Activity Item Provider subclass does two things that are non-standard behaviour: +/// +/// * We return NSNull if the calling activity is not supposed to see the title. For example the Copy action, which should only paste the URL. We also include Message and Mail to have parity with what Safari exposes. +/// * We set the subject of the item to the title, this means it will correctly be used when sharing to for example Mail. Again parity with Safari. +/// +/// Note that not all applications use the Subject. For example OmniFocus ignores it, so we need to do both. + +class TitleActivityItemProvider: UIActivityItemProvider { + static let activityTypesToIgnore = [UIActivityType.copyToPasteboard, UIActivityType.message, UIActivityType.mail] + + init(title: String) { + super.init(placeholderItem: title) + } + + override var item: Any { + if let activityType = activityType { + if TitleActivityItemProvider.activityTypesToIgnore.contains(activityType) { + return NSNull() + } + } + return placeholderItem! as AnyObject + } + + override func activityViewController(_ activityViewController: UIActivityViewController, subjectForActivityType activityType: UIActivityType?) -> String { + return placeholderItem as! String + } +} diff --git a/mobile/ios/Client/Frontend/Strings.swift b/mobile/ios/Client/Frontend/Strings.swift new file mode 100644 index 0000000000..3590c9f1f8 --- /dev/null +++ b/mobile/ios/Client/Frontend/Strings.swift @@ -0,0 +1,462 @@ +/* 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 + +public struct Strings {} + +/// Return the main application bundle. Even if called from an extension. If for some reason we cannot find the +/// application bundle, the current bundle is returned, which will then result in an English base language string. +private func applicationBundle() -> Bundle { + let bundle = Bundle.main + guard bundle.bundleURL.pathExtension == "appex", let applicationBundleURL = (bundle.bundleURL as NSURL).deletingLastPathComponent?.deletingLastPathComponent() else { + return bundle + } + return Bundle(url: applicationBundleURL) ?? bundle +} + +// SendTo extension. +extension Strings { + public static let SendToCancelButton = NSLocalizedString("SendTo.Cancel.Button", bundle: applicationBundle(), value: "Cancel", comment: "Button title for cancelling SendTo screen") + public static let SendToErrorOKButton = NSLocalizedString("SendTo.Error.OK.Button", bundle: applicationBundle(), value: "OK", comment: "OK button to dismiss the error prompt.") + public static let SendToErrorTitle = NSLocalizedString("SendTo.Error.Title", bundle: applicationBundle(), value: "The link you are trying to share cannot be shared.", comment: "Title of error prompt displayed when an invalid URL is shared.") + public static let SendToErrorMessage = NSLocalizedString("SendTo.Error.Message", bundle: applicationBundle(), value: "Only HTTP and HTTPS links can be shared.", comment: "Message in error prompt explaining why the URL is invalid.") +} + +// ShareTo extension. +extension Strings { + public static let ShareToCancelButton = NSLocalizedString("ShareTo.Cancel.Button", bundle: applicationBundle(), value: "Cancel", comment: "Button title for cancelling Share screen") +} + +// Top Sites. +extension Strings { + public static let TopSitesEmptyStateDescription = NSLocalizedString("TopSites.EmptyState.Description", value: "Your most visited sites will show up here.", comment: "Description label for the empty Top Sites state.") + public static let TopSitesEmptyStateTitle = NSLocalizedString("TopSites.EmptyState.Title", value: "Welcome to Top Sites", comment: "The title for the empty Top Sites state") + public static let TopSitesRemoveButtonAccessibilityLabel = NSLocalizedString("TopSites.RemovePage.Button", value: "Remove page — %@", comment: "Button shown in editing mode to remove this site from the top sites panel.") +} + +// Activity Stream. +extension Strings { + public static let HighlightIntroTitle = NSLocalizedString("ActivityStream.HighlightIntro.Title", value: "Be on the Lookout", comment: "The title that appears for the introduction to highlights in AS.") + public static let HighlightIntroDescription = NSLocalizedString("ActivityStream.HighlightIntro.Description", value: "Firefox will place things here that you’ve discovered on the web so you can find your way back to the great articles, videos, bookmarks and other pages", comment: "The detailed text that explains what highlights are in AS.") + public static let ASPageControlButton = NSLocalizedString("ActivityStream.PageControl.Button", value: "Next Page", comment: "The page control button that lets you switch between pages in top sites") + public static let ASHighlightsTitle = NSLocalizedString("ActivityStream.Highlights.Title", value: "Highlights", comment: "Section title label for the Highlights section") + public static let ASPocketTitle = NSLocalizedString("ActivityStream.Pocket.SectionTitle", value: "Recommended by Pocket", comment: "Section title label for Recommended by Pocket section") + public static let ASTopSitesTitle = NSLocalizedString("ActivityStream.TopSites.SectionTitle", value: "Top Sites", comment: "Section title label for Top Sites") + public static let HighlightVistedText = NSLocalizedString("ActivityStream.Highlights.Visited", value: "Visited", comment: "The description of a highlight if it is a site the user has visited") + public static let HighlightBookmarkText = NSLocalizedString("ActivityStream.Highlights.Bookmark", value: "Bookmarked", comment: "The description of a highlight if it is a site the user has bookmarked") + public static let PocketTrendingText = NSLocalizedString("ActivityStream.Pocket.Trending", value: "Trending", comment: "The description of a Pocket Story") +} + +// Home Panel Context Menu. +extension Strings { + public static let OpenInNewTabContextMenuTitle = NSLocalizedString("HomePanel.ContextMenu.OpenInNewTab", value: "Open in New Tab", comment: "The title for the Open in New Tab context menu action for sites in Home Panels") + public static let OpenInNewPrivateTabContextMenuTitle = NSLocalizedString("HomePanel.ContextMenu.OpenInNewPrivateTab", value: "Open in New Private Tab", comment: "The title for the Open in New Private Tab context menu action for sites in Home Panels") + public static let BookmarkContextMenuTitle = NSLocalizedString("HomePanel.ContextMenu.Bookmark", value: "Bookmark", comment: "The title for the Bookmark context menu action for sites in Home Panels") + public static let RemoveBookmarkContextMenuTitle = NSLocalizedString("HomePanel.ContextMenu.RemoveBookmark", value: "Remove Bookmark", comment: "The title for the Remove Bookmark context menu action for sites in Home Panels") + public static let DeleteFromHistoryContextMenuTitle = NSLocalizedString("HomePanel.ContextMenu.DeleteFromHistory", value: "Delete from History", comment: "The title for the Delete from History context menu action for sites in Home Panels") + public static let ShareContextMenuTitle = NSLocalizedString("HomePanel.ContextMenu.Share", value: "Share", comment: "The title for the Share context menu action for sites in Home Panels") + public static let RemoveContextMenuTitle = NSLocalizedString("HomePanel.ContextMenu.Remove", value: "Remove", comment: "The title for the Remove context menu action for sites in Home Panels") + public static let PinTopsiteActionTitle = NSLocalizedString("ActivityStream.ContextMenu.PinTopsite", value: "Pin to Top Sites", comment: "The title for the pinning a topsite action") + public static let RemovePinTopsiteActionTitle = NSLocalizedString("ActivityStream.ContextMenu.RemovePinTopsite", value: "Remove Pinned Site", comment: "The title for removing a pinned topsite action") +} + +// PhotonActionSheet Strings +extension Strings { + public static let CancelButtonTitle = NSLocalizedString("PhotonMenu.cancel", value: "Cancel", comment: "Button for closing the menu action sheet") + +} + +// Settings. +extension Strings { + public static let SettingsClearPrivateDataClearButton = NSLocalizedString("Settings.ClearPrivateData.Clear.Button", value: "Clear Private Data", comment: "Button in settings that clears private data for the selected items.") + public static let SettingsClearPrivateDataSectionName = NSLocalizedString("Settings.ClearPrivateData.SectionName", value: "Clear Private Data", comment: "Label used as an item in Settings. When touched it will open a dialog prompting the user to make sure they want to clear all of their private data.") + public static let SettingsClearPrivateDataTitle = NSLocalizedString("Settings.ClearPrivateData.Title", value: "Clear Private Data", comment: "Title displayed in header of the setting panel.") + public static let SettingsDisconnectSyncAlertTitle = NSLocalizedString("Settings.Disconnect.Title", value: "Disconnect Sync?", comment: "Title of the alert when prompting the user asking to disconnect.") + public static let SettingsDisconnectSyncButton = NSLocalizedString("Settings.Disconnect.Button", value: "Disconnect Sync", comment: "Button displayed at the bottom of settings page allowing users to Disconnect from FxA") + public static let SettingsDisconnectDestructiveAction = NSLocalizedString("Settings.Disconnect.DestructiveButton", value: "Disconnect", comment: "Destructive action button in alert when user is prompted for disconnect") + public static let SettingsSearchDoneButton = NSLocalizedString("Settings.Search.Done.Button", value: "Done", comment: "Button displayed at the top of the search settings.") + public static let SettingsSearchEditButton = NSLocalizedString("Settings.Search.Edit.Button", value: "Edit", comment: "Button displayed at the top of the search settings.") + public static let UseTouchID = NSLocalizedString("Use Touch ID", tableName: "AuthenticationManager", comment: "List section title for when to use Touch ID") + public static let UseFaceID = NSLocalizedString("Use Face ID", tableName: "AuthenticationManager", comment: "List section title for when to use Face ID") +} + +// Error pages. +extension Strings { + public static let ErrorPagesAdvancedButton = NSLocalizedString("ErrorPages.Advanced.Button", value: "Advanced", comment: "Label for button to perform advanced actions on the error page") + public static let ErrorPagesAdvancedWarning1 = NSLocalizedString("ErrorPages.AdvancedWarning1.Text", value: "Warning: we can’t confirm your connection to this website is secure.", comment: "Warning text when clicking the Advanced button on error pages") + public static let ErrorPagesAdvancedWarning2 = NSLocalizedString("ErrorPages.AdvancedWarning2.Text", value: "It may be a misconfiguration or tampering by an attacker. Proceed if you accept the potential risk.", comment: "Additional warning text when clicking the Advanced button on error pages") + public static let ErrorPagesCertWarningDescription = NSLocalizedString("ErrorPages.CertWarning.Description", value: "The owner of %@ has configured their website improperly. To protect your information from being stolen, Firefox has not connected to this website.", comment: "Warning text on the certificate error page") + public static let ErrorPagesCertWarningTitle = NSLocalizedString("ErrorPages.CertWarning.Title", value: "This Connection is Untrusted", comment: "Title on the certificate error page") + public static let ErrorPagesGoBackButton = NSLocalizedString("ErrorPages.GoBack.Button", value: "Go Back", comment: "Label for button to go back from the error page") + public static let ErrorPagesVisitOnceButton = NSLocalizedString("ErrorPages.VisitOnce.Button", value: "Visit site anyway", comment: "Button label to temporarily continue to the site from the certificate error page") +} + +// Logins Helper. +extension Strings { + public static let LoginsHelperSaveLoginButtonTitle = NSLocalizedString("LoginsHelper.SaveLogin.Button", value: "Save Login", comment: "Button to save the user's password") + public static let LoginsHelperDontSaveButtonTitle = NSLocalizedString("LoginsHelper.DontSave.Button", value: "Don’t Save", comment: "Button to not save the user's password") + public static let LoginsHelperUpdateButtonTitle = NSLocalizedString("LoginsHelper.Update.Button", value: "Update", comment: "Button to update the user's password") +} + +// History Panel +extension Strings { + public static let SyncedTabsTableViewCellTitle = NSLocalizedString("HistoryPanel.SyncedTabsCell.Title", value: "Synced Devices", comment: "Title for the Synced Tabs Cell in the History Panel") + public static let HistoryBackButtonTitle = NSLocalizedString("HistoryPanel.HistoryBackButton.Title", value: "History", comment: "Title for the Back to History button in the History Panel") + public static let EmptySyncedTabsPanelStateTitle = NSLocalizedString("HistoryPanel.EmptySyncedTabsState.Title", value: "Firefox Sync", comment: "Title for the empty synced tabs state in the History Panel") + public static let EmptySyncedTabsPanelStateDescription = NSLocalizedString("HistoryPanel.EmptySyncedTabsState.Description", value: "Sign in to view open tabs on your other devices.", comment: "Description for the empty synced tabs state in the History Panel") + public static let EmptySyncedTabsPanelNullStateDescription = NSLocalizedString("HistoryPanel.EmptySyncedTabsNullState.Description", value: "Your tabs from other devices show up here.", comment: "Description for the empty synced tabs null state in the History Panel") + public static let SyncedTabsTableViewCellDescription = NSLocalizedString("HistoryPanel.SyncedTabsCell.Description.Pluralized", value: "%d device(s) connected", comment: "Description that corresponds with a number of devices connected for the Synced Tabs Cell in the History Panel") + public static let HistoryPanelEmptyStateTitle = NSLocalizedString("HistoryPanel.EmptyState.Title", value: "Websites you’ve visited recently will show up here.", comment: "Title for the History Panel empty state.") + public static let RecentlyClosedTabsButtonTitle = NSLocalizedString("HistoryPanel.RecentlyClosedTabsButton.Title", value: "Recently Closed", comment: "Title for the Recently Closed button in the History Panel") + public static let RecentlyClosedTabsPanelTitle = NSLocalizedString("RecentlyClosedTabsPanel.Title", value: "Recently Closed", comment: "Title for the Recently Closed Tabs Panel") + public static let FirefoxHomePage = NSLocalizedString("Firefox.HomePage.Title", value: "Firefox Home Page", comment: "Title for firefox about:home page in tab history list") +} + +// Syncing +extension Strings { + public static let SyncingMessageWithEllipsis = NSLocalizedString("Sync.SyncingEllipsis.Label", value: "Syncing…", comment: "Message displayed when the user's account is syncing with ellipsis at the end") + public static let SyncingMessageWithoutEllipsis = NSLocalizedString("Sync.Syncing.Label", value: "Syncing", comment: "Message displayed when the user's account is syncing with no ellipsis") + + public static let FirstTimeSyncLongTime = NSLocalizedString("Sync.FirstTimeMessage.Label", value: "Your first sync may take a while", comment: "Message displayed when the user syncs for the first time") + + public static let FirefoxSyncOfflineTitle = NSLocalizedString("SyncState.Offline.Title", value: "Sync is offline", comment: "Title for Sync status message when Sync failed due to being offline") + public static let FirefoxSyncNotStartedTitle = NSLocalizedString("SyncState.NotStarted.Title", value: "Sync is unavailable", comment: "Title for Sync status message when Sync failed to start.") + public static let FirefoxSyncPartialTitle = NSLocalizedString("SyncState.Partial.Title", value: "Sync is experiencing issues syncing %@", comment: "Title for Sync status message when a component of Sync failed to complete, where %@ represents the name of the component, i.e. Sync is experiencing issues syncing Bookmarks") + public static let FirefoxSyncFailedTitle = NSLocalizedString("SyncState.Failed.Title", value: "Syncing has failed", comment: "Title for Sync status message when synchronization failed to complete") + public static let FirefoxSyncTroubleshootTitle = NSLocalizedString("Settings.TroubleShootSync.Title", value: "Troubleshoot", comment: "Title of link to help page to find out how to solve Sync issues") + + public static func localizedStringForSyncComponent(_ componentName: String) -> String? { + switch componentName { + case "bookmarks": + return NSLocalizedString("SyncState.Bookmark.Title", value: "Bookmarks", comment: "The Bookmark sync component, used in SyncState.Partial.Title") + case "clients": + return NSLocalizedString("SyncState.Clients.Title", value: "Remote Clients", comment: "The Remote Clients sync component, used in SyncState.Partial.Title") + case "tabs": + return NSLocalizedString("SyncState.Tabs.Title", value: "Tabs", comment: "The Tabs sync component, used in SyncState.Partial.Title") + case "logins": + return NSLocalizedString("SyncState.Logins.Title", value: "Logins", comment: "The Logins sync component, used in SyncState.Partial.Title") + case "history": + return NSLocalizedString("SyncState.History.Title", value: "History", comment: "The History sync component, used in SyncState.Partial.Title") + default: return nil + } + } +} + +// Firefox Account +extension Strings { + // Settings strings + public static let FxAFirefoxAccount = NSLocalizedString("FxA.FirefoxAccount", value: "Firefox Account", comment: "Settings section title for Firefox Account") + public static let FxASignIntoSync = NSLocalizedString("FxA.SignIntoSync", value: "Sign in to Sync", comment: "Button label to sign into Sync") + public static let FxATakeYourWebWithYou = NSLocalizedString("FxA.TakeYourWebWithYou", value: "Take Your Web With You", comment: "Call to action for sign into sync button") + public static let FxASyncUsageDetails = NSLocalizedString("FxA.SyncExplain", value: "Get your tabs, bookmarks, and passwords from your other devices.", comment: "Label explaining what sync does") + public static let FxAAccountVerificationRequired = NSLocalizedString("FxA.AccountVerificationRequired", value: "Account Verification Required", comment: "Label stating your account is not verified") + public static let FxAAccountVerificationDetails = NSLocalizedString("FxA.AccountVerificationDetails", value: "Wrong email? Disconnect below to start over.", comment: "Label stating how to disconnect account") + public static let FxAManageAccount = NSLocalizedString("FxA.ManageAccount", value: "Manage Account", comment: "Button label to goto Firefox Account settings") + public static let FxASyncNow = NSLocalizedString("FxA.SyncNow", value: "Sync Now", comment: "Button label to Sync your Firefox Account") + public static let FxANoInternetConnection = NSLocalizedString("FxA.NoInternetConnection", value: "No Internet Connection", comment: "Label when no internet is present") + + // Surface error strings + public static let FxAAccountVerificationRequiredSurface = NSLocalizedString("FxA.AccountVerificationRequiredSurface", value: "You need to verify %@. Check your email for the verification link from Firefox.", comment: "Message explaining that user needs to check email for Firefox Account verfication link.") + public static let FxAResendEmail = NSLocalizedString("FxA.ResendEmail", value: "Resend Email", comment: "Button label to resend email") +} + +//Hotkey Titles +extension Strings { + public static let ReloadPageTitle = NSLocalizedString("Hotkeys.Reload.DiscoveryTitle", value: "Reload Page", comment: "Label to display in the Discoverability overlay for keyboard shortcuts") + public static let BackTitle = NSLocalizedString("Hotkeys.Back.DiscoveryTitle", value: "Back", comment: "Label to display in the Discoverability overlay for keyboard shortcuts") + public static let ForwardTitle = NSLocalizedString("Hotkeys.Forward.DiscoveryTitle", value: "Forward", comment: "Label to display in the Discoverability overlay for keyboard shortcuts") + + public static let FindTitle = NSLocalizedString("Hotkeys.Find.DiscoveryTitle", value: "Find", comment: "Label to display in the Discoverability overlay for keyboard shortcuts") + public static let SelectLocationBarTitle = NSLocalizedString("Hotkeys.SelectLocationBar.DiscoveryTitle", value: "Select Location Bar", comment: "Label to display in the Discoverability overlay for keyboard shortcuts") + public static let NewTabTitle = NSLocalizedString("Hotkeys.NewTab.DiscoveryTitle", value: "New Tab", comment: "Label to display in the Discoverability overlay for keyboard shortcuts") + public static let NewPrivateTabTitle = NSLocalizedString("Hotkeys.NewPrivateTab.DiscoveryTitle", value: "New Private Tab", comment: "Label to display in the Discoverability overlay for keyboard shortcuts") + public static let CloseTabTitle = NSLocalizedString("Hotkeys.CloseTab.DiscoveryTitle", value: "Close Tab", comment: "Label to display in the Discoverability overlay for keyboard shortcuts") + public static let ShowNextTabTitle = NSLocalizedString("Hotkeys.ShowNextTab.DiscoveryTitle", value: "Show Next Tab", comment: "Label to display in the Discoverability overlay for keyboard shortcuts") + public static let ShowPreviousTabTitle = NSLocalizedString("Hotkeys.ShowPreviousTab.DiscoveryTitle", value: "Show Previous Tab", comment: "Label to display in the Discoverability overlay for keyboard shortcuts") +} + +// Home page. +extension Strings { + public static let SettingsHomePageSectionName = NSLocalizedString("Settings.HomePage.SectionName", value: "Homepage", comment: "Label used as an item in Settings. When touched it will open a dialog to configure the home page and its uses.") + public static let SettingsHomePageTitle = NSLocalizedString("Settings.HomePage.Title", value: "Homepage Settings", comment: "Title displayed in header of the setting panel.") + public static let SettingsHomePageURLSectionTitle = NSLocalizedString("Settings.HomePage.URL.Title", value: "Current Homepage", comment: "Title of the setting section containing the URL of the current home page.") + public static let SettingsHomePageUseCurrentPage = NSLocalizedString("Settings.HomePage.UseCurrent.Button", value: "Use Current Page", comment: "Button in settings to use the current page as home page.") + public static let SettingsHomePagePlaceholder = NSLocalizedString("Settings.HomePage.URL.Placeholder", value: "Enter a webpage", comment: "Placeholder text in the homepage setting when no homepage has been set.") + public static let SettingsHomePageUseCopiedLink = NSLocalizedString("Settings.HomePage.UseCopiedLink.Button", value: "Use Copied Link", comment: "Button in settings to use the current link on the clipboard as home page.") + public static let SettingsHomePageUseDefault = NSLocalizedString("Settings.HomePage.UseDefault.Button", value: "Use Default", comment: "Button in settings to use the default home page. If no default is set, then this button isn't shown.") + public static let SettingsHomePageClear = NSLocalizedString("Settings.HomePage.Clear.Button", value: "Clear", comment: "Button in settings to clear the home page.") + public static let SetHomePageDialogTitle = NSLocalizedString("HomePage.Set.Dialog.Title", value: "Do you want to use this web page as your home page?", comment: "Alert dialog title when the user opens the home page for the first time.") + public static let SetHomePageDialogMessage = NSLocalizedString("HomePage.Set.Dialog.Message", value: "You can change this at any time in Settings", comment: "Alert dialog body when the user opens the home page for the first time.") + public static let SetHomePageDialogYes = NSLocalizedString("HomePage.Set.Dialog.OK", value: "Set Homepage", comment: "Button accepting changes setting the home page for the first time.") + public static let SetHomePageDialogNo = NSLocalizedString("HomePage.Set.Dialog.Cancel", value: "Cancel", comment: "Button cancelling changes setting the home page for the first time.") +} + +// New tab choice settings +extension Strings { + public static let SettingsNewTabSectionName = NSLocalizedString("Settings.NewTab.SectionName", value: "New Tab", comment: "Label used as an item in Settings. When touched it will open a dialog to configure the new tab behaviour.") + public static let SettingsNewTabTitle = NSLocalizedString("Settings.NewTab.Title", value: "New Tab Settings", comment: "Title displayed in header of the setting panel.") + public static let SettingsNewTabTopSites = NSLocalizedString("Settings.NewTab.Option.TopSites", value: "Show your Top Sites", comment: "Option in settings to show top sites when you open a new tab") + public static let SettingsNewTabBookmarks = NSLocalizedString("Settings.NewTab.Option.Bookmarks", value: "Show your Bookmarks", comment: "Option in settings to show bookmarks when you open a new tab") + public static let SettingsNewTabHistory = NSLocalizedString("Settings.NewTab.Option.History", value: "Show your History", comment: "Option in settings to show history when you open a new tab") + public static let SettingsNewTabReadingList = NSLocalizedString("Settings.NewTab.Option.ReadingList", value: "Show your Reading List", comment: "Option in settings to show reading list when you open a new tab") + public static let SettingsNewTabBlankPage = NSLocalizedString("Settings.NewTab.Option.BlankPage", value: "Show a Blank Page", comment: "Option in settings to show a blank page when you open a new tab") + public static let SettingsNewTabHomePage = NSLocalizedString("Settings.NewTab.Option.HomePage", value: "Show your Homepage", comment: "Option in settings to show your homepage when you open a new tab") + public static let SettingsNewTabDescription = NSLocalizedString("Settings.NewTab.Description", value: "When you open a New Tab:", comment: "A description in settings of what the new tab choice means") + // AS Panel settings + public static let SettingsNewTabASTitle = NSLocalizedString("Settings.NewTab.Option.ASTitle", value: "Additional Content", comment: "The title of the section in newtab that lets you modify the topsites panel") + public static let SettingsNewTabPocket = NSLocalizedString("Settings.NewTab.Option.Pocket", value: "Recommended by Pocket", comment: "Option in settings to turn on off pocket recommendations") + public static let SettingsNewTabHiglightsHistory = NSLocalizedString("Settings.NewTab.Option.HighlightsHistory", value: "Visited", comment: "Option in settings to turn off history in the highlights section") + public static let SettingsNewTabHighlightsBookmarks = NSLocalizedString("Settings.NewTab.Option.HighlightsBookmarks", value: "Recent Bookmarks", comment: "Option in the settings to turn off recent bookmarks in the Highlights section") +} + +// Custom account settings - These strings did not make it for the v10 l10n deadline so we have turned them into regular strings. These strings will come back localized in a next version. + +extension Strings { + // Settings.AdvanceAccount.SectionName + // Label used as an item in Settings. When touched it will open a dialog to setup advance Firefox account settings. + public static let SettingsAdvanceAccountSectionName = "Account Settings" + + // Settings.AdvanceAccount.SectionFooter + // Details for using custom Firefox Account service. + public static let SettingsAdvanceAccountSectionFooter = "To use a custom Firefox Account and sync servers, specify the root Url of the Firefox Account site. This will download the configuration and setup this device to use the new service. After the new service has been set, you will need to create a new Firefox Account or login with an existing one." + + // Settings.AdvanceAccount.SectionName + // Title displayed in header of the setting panel. + public static let SettingsAdvanceAccountTitle = "Advance Account Settings" + + // Settings.AdvanceAccount.UrlPlaceholder + // Title displayed in header of the setting panel. + public static let SettingsAdvanceAccountUrlPlaceholder = "Custom Account Url" + + // Settings.AdvanceAccount.UpdatedAlertMessage + // Messaged displayed when sync service has been successfully set. + public static let SettingsAdvanceAccountUrlUpdatedAlertMessage = "Firefox account service updated. To begin using custom server, please log out and re-login." + + // Settings.AdvanceAccount.UpdatedAlertOk + // Ok button on custom sync service updated alert + public static let SettingsAdvanceAccountUrlUpdatedAlertOk = "OK" + + // Settings.AdvanceAccount.ErrorAlertTitle + // Error alert message title. + public static let SettingsAdvanceAccountUrlErrorAlertTitle = "Error" + + // Settings.AdvanceAccount.ErrorAlertMessage + // Messaged displayed when sync service has an error setting a custom sync url. + public static let SettingsAdvanceAccountUrlErrorAlertMessage = "There was an error while attempting to parse the url. Please make sure that it is a valid Firefox Account root url." + + // Settings.AdvanceAccount.ErrorAlertOk + // Ok button on custom sync service error alert. + public static let SettingsAdvanceAccountUrlErrorAlertOk = "OK" + + // Settings.AdvanceAccount.UseCustomAccountsServiceTitle + // Toggle switch to use custom FxA server + public static let SettingsAdvanceAccountUseCustomAccountsServiceTitle = "Use Custom Account Service" + + // Settings.AdvanceAccount.UrlEmptyErrorAlertMessage + // No custom service set. + public static let SettingsAdvanceAccountEmptyUrlErrorAlertMessage = "Please enter a custom account url before enabling." +} + +// Open With Settings +extension Strings { + public static let SettingsOpenWithSectionName = NSLocalizedString("Settings.OpenWith.SectionName", value: "Mail App", comment: "Label used as an item in Settings. When touched it will open a dialog to configure the open with (mail links) behaviour.") + public static let SettingsOpenWithPageTitle = NSLocalizedString("Settings.OpenWith.PageTitle", value: "Open mail links with", comment: "Title for Open With Settings") +} + +// Third Party Search Engines +extension Strings { + public static let ThirdPartySearchEngineAdded = NSLocalizedString("Search.ThirdPartyEngines.AddSuccess", value: "Added Search engine!", comment: "The success message that appears after a user sucessfully adds a new search engine") + public static let ThirdPartySearchAddTitle = NSLocalizedString("Search.ThirdPartyEngines.AddTitle", value: "Add Search Provider?", comment: "The title that asks the user to Add the search provider") + public static let ThirdPartySearchAddMessage = NSLocalizedString("Search.ThirdPartyEngines.AddMessage", value: "The new search engine will appear in the quick search bar.", comment: "The message that asks the user to Add the search provider explaining where the search engine will appear") + public static let ThirdPartySearchCancelButton = NSLocalizedString("Search.ThirdPartyEngines.Cancel", value: "Cancel", comment: "The cancel button if you do not want to add a search engine.") + public static let ThirdPartySearchOkayButton = NSLocalizedString("Search.ThirdPartyEngines.OK", value: "OK", comment: "The confirmation button") + public static let ThirdPartySearchFailedTitle = NSLocalizedString("Search.ThirdPartyEngines.FailedTitle", value: "Failed", comment: "A title explaining that we failed to add a search engine") + public static let ThirdPartySearchFailedMessage = NSLocalizedString("Search.ThirdPartyEngines.FailedMessage", value: "The search provider could not be added.", comment: "A title explaining that we failed to add a search engine") + public static let CustomEngineFormErrorTitle = NSLocalizedString("Search.ThirdPartyEngines.FormErrorTitle", value: "Failed", comment: "A title stating that we failed to add custom search engine.") + public static let CustomEngineFormErrorMessage = NSLocalizedString("Search.ThirdPartyEngines.FormErrorMessage", value: "Please fill all fields correctly.", comment: "A message explaining fault in custom search engine form.") + public static let CustomEngineDuplicateErrorTitle = NSLocalizedString("Search.ThirdPartyEngines.DuplicateErrorTitle", value: "Failed", comment: "A title stating that we failed to add custom search engine.") + public static let CustomEngineDuplicateErrorMessage = NSLocalizedString("Search.ThirdPartyEngines.DuplicateErrorMessage", value: "A search engine with this title or URL has already been added.", comment: "A message explaining fault in custom search engine form.") +} + +// Bookmark Management +extension Strings { + public static let BookmarksTitle = NSLocalizedString("Bookmarks.Title.Label", value: "Title", comment: "The label for the title of a bookmark") + public static let BookmarksURL = NSLocalizedString("Bookmarks.URL.Label", value: "URL", comment: "The label for the URL of a bookmark") + public static let BookmarksFolder = NSLocalizedString("Bookmarks.Folder.Label", value: "Folder", comment: "The label to show the location of the folder where the bookmark is located") + public static let BookmarksNewFolder = NSLocalizedString("Bookmarks.NewFolder.Label", value: "New Folder", comment: "The button to create a new folder") + public static let BookmarksFolderName = NSLocalizedString("Bookmarks.FolderName.Label", value: "Folder Name", comment: "The label for the title of the new folder") + public static let BookmarksFolderLocation = NSLocalizedString("Bookmarks.FolderLocation.Label", value: "Location", comment: "The label for the location of the new folder") +} + +// Tabs Delete All Undo Toast +extension Strings { + public static let TabsDeleteAllUndoTitle = NSLocalizedString("Tabs.DeleteAllUndo.Title", value: "%d tab(s) closed", comment: "The label indicating that all the tabs were closed") + public static let TabsDeleteAllUndoAction = NSLocalizedString("Tabs.DeleteAllUndo.Button", value: "Undo", comment: "The button to undo the delete all tabs") +} + +//Clipboard Toast +extension Strings { + public static let GoToCopiedLink = NSLocalizedString("ClipboardToast.GoToCopiedLink.Title", value: "Go to copied link?", comment: "Message displayed when the user has a copied link on the clipboard") + public static let GoButtonTittle = NSLocalizedString("ClipboardToast.GoToCopiedLink.Button", value: "Go", comment: "The button to open a new tab with the copied link") + + public static let SettingsOfferClipboardBarTitle = NSLocalizedString("Settings.OfferClipboardBar.Title", value: "Offer to Open Copied Links", comment: "Title of setting to enable the Go to Copied URL feature. See https://bug1223660.bmoattachments.org/attachment.cgi?id=8898349") + public static let SettingsOfferClipboardBarStatus = NSLocalizedString("Settings.OfferClipboardBar.Status", value: "When Opening Firefox", comment: "Description displayed under the ”Offer to Open Copied Link” option. See https://bug1223660.bmoattachments.org/attachment.cgi?id=8898349") +} + +// errors +extension Strings { + public static let UnableToDownloadError = NSLocalizedString("Downloads.Error.Message", value: "Downloads aren’t supported in Firefox yet.", comment: "The message displayed to a user when they try and perform the download of an asset that Firefox cannot currently handle.") + public static let UnableToAddPassErrorTitle = NSLocalizedString("AddPass.Error.Title", value: "Failed to Add Pass", comment: "Title of the 'Add Pass Failed' alert. See https://support.apple.com/HT204003 for context on Wallet.") + public static let UnableToAddPassErrorMessage = NSLocalizedString("AddPass.Error.Message", value: "An error occured while adding the pass to Wallet. Please try again later.", comment: "Text of the 'Add Pass Failed' alert. See https://support.apple.com/HT204003 for context on Wallet.") + public static let UnableToAddPassErrorDismiss = NSLocalizedString("AddPass.Error.Dismiss", value: "OK", comment: "Button to dismiss the 'Add Pass Failed' alert. See https://support.apple.com/HT204003 for context on Wallet.") + public static let UnableToOpenURLError = NSLocalizedString("OpenURL.Error.Message", value: "Firefox cannot open the page because it has an invalid address.", comment: "The message displayed to a user when they try to open a URL that cannot be handled by Firefox, or any external app.") + public static let UnableToOpenURLErrorTitle = NSLocalizedString("OpenURL.Error.Title", value: "Cannot Open Page", comment: "Title of the message shown when the user attempts to navigate to an invalid link.") +} + +// open in +extension Strings { + public static let OpenInDownloadHelperAlertTitle = NSLocalizedString("Downloads.Alert.Title", value: "Firefox Downloads", comment: "The title of the alert box asking the user if they want to use another app to open a file.") + public static let OpenInDownloadHelperAlertMessage = NSLocalizedString("Downloads.Alert.Message", value: "Firefox is unable to download or display this file. Would you like to open it in another app?", comment: "The message of the alert box asking the user if they want to use another app to open a file.") + public static let OpenInDownloadHelperAlertConfirm = NSLocalizedString("Downloads.Alert.Confirm", value: "Yes", comment: "The label of the button the user will press to be presented with a list of other apps to open a file in") + public static let OpenInDownloadHelperAlertCancel = NSLocalizedString("Downloads.Alert.Cancel", value: "No", comment: "The label of the button the user will press to reject the option to open a file in another application") +} + +// Add Custom Search Engine +extension Strings { + public static let SettingsAddCustomEngine = NSLocalizedString("Settings.AddCustomEngine", value: "Add Search Engine", comment: "The button text in Search Settings that opens the Custom Search Engine view.") + public static let SettingsAddCustomEngineTitle = NSLocalizedString("Settings.AddCustomEngine.Title", value: "Add Search Engine", comment: "The title of the Custom Search Engine view.") + public static let SettingsAddCustomEngineTitleLabel = NSLocalizedString("Settings.AddCustomEngine.TitleLabel", value: "Title", comment: "The title for the field which sets the title for a custom search engine.") + public static let SettingsAddCustomEngineURLLabel = NSLocalizedString("Settings.AddCustomEngine.URLLabel", value: "URL", comment: "The title for URL Field") + public static let SettingsAddCustomEngineTitlePlaceholder = NSLocalizedString("Settings.AddCustomEngine.TitlePlaceholder", value: "Search Engine", comment: "The placeholder for Title Field when saving a custom search engine.") + public static let SettingsAddCustomEngineURLPlaceholder = NSLocalizedString("Settings.AddCustomEngine.URLPlaceholder", value: "URL (Replace Query with %s)", comment: "The placeholder for URL Field when saving a custom search engine") + public static let SettingsAddCustomEngineSaveButtonText = NSLocalizedString("Settings.AddCustomEngine.SaveButtonText", value: "Save", comment: "The text on the Save button when saving a custom search engine") +} + +// Context menu ButtonToast instances. +extension Strings { + public static let ContextMenuButtonToastNewTabOpenedLabelText = NSLocalizedString("ContextMenu.ButtonToast.NewTabOpened.LabelText", value: "New Tab opened", comment: "The label text in the Button Toast for switching to a fresh New Tab.") + public static let ContextMenuButtonToastNewTabOpenedButtonText = NSLocalizedString("ContextMenu.ButtonToast.NewTabOpened.ButtonText", value: "Switch", comment: "The button text in the Button Toast for switching to a fresh New Tab.") + public static let ContextMenuButtonToastNewPrivateTabOpenedLabelText = NSLocalizedString("ContextMenu.ButtonToast.NewPrivateTabOpened.LabelText", value: "New Private Tab opened", comment: "The label text in the Button Toast for switching to a fresh New Private Tab.") + public static let ContextMenuButtonToastNewPrivateTabOpenedButtonText = NSLocalizedString("ContextMenu.ButtonToast.NewPrivateTabOpened.ButtonText", value: "Switch", comment: "The button text in the Button Toast for switching to a fresh New Private Tab.") +} + +// Sent tabs notifications. These are displayed when the app is backgrounded or the device is locked. +extension Strings { + // zero tabs + public static let SentTab_NoTabArrivingNotification_title = NSLocalizedString("SentTab.NoTabArrivingNotification.title", value: "Firefox Sync", comment: "Title of notification received after a spurious message from FxA has been received.") + public static let SentTab_NoTabArrivingNotification_body = + NSLocalizedString("SentTab.NoTabArrivingNotification.body", value: "Tap to begin", comment: "Body of notification received after a spurious message from FxA has been received.") + + // one or more tabs + public static let SentTab_TabArrivingNotification_NoDevice_title = NSLocalizedString("SentTab_TabArrivingNotification_NoDevice_title", value: "Tab received", comment: "Title of notification shown when the device is sent one or more tabs from an unnamed device.") + public static let SentTab_TabArrivingNotification_NoDevice_body = NSLocalizedString("SentTab_TabArrivingNotification_NoDevice_body", value: "New tab arrived from another device.", comment: "Body of notification shown when the device is sent one or more tabs from an unnamed device.") + public static let SentTab_TabArrivingNotification_WithDevice_title = NSLocalizedString("SentTab_TabArrivingNotification_WithDevice_title", value: "Tab received from %@", comment: "Title of notification shown when the device is sent one or more tabs from the named device. %@ is the placeholder for the device name. This device name will be localized by that device.") + public static let SentTab_TabArrivingNotification_WithDevice_body = NSLocalizedString("SentTab_TabArrivingNotification_WithDevice_body", value: "New tab arrived in %@", comment: "Body of notification shown when the device is sent one or more tabs from the named device. %@ is the placeholder for the app name.") +} + +// Additional messages sent via Push from FxA +extension Strings { + public static let FxAPush_DeviceDisconnected_ThisDevice_title = NSLocalizedString("FxAPush_DeviceDisconnected_ThisDevice_title", value: "Sync Disconnected", comment: "Title of a notification displayed when this device has been disconnected by another device.") + public static let FxAPush_DeviceDisconnected_ThisDevice_body = NSLocalizedString("FxAPush_DeviceDisconnected_ThisDevice_body", value: "This device has been successfully disconnected from Firefox Sync.", comment: "Body of a notification displayed when this device has been disconnected from FxA by another device.") + public static let FxAPush_DeviceDisconnected_title = NSLocalizedString("FxAPush_DeviceDisconnected_title", value: "Sync Disconnected", comment: "Title of a notification displayed when named device has been disconnected from FxA.") + public static let FxAPush_DeviceDisconnected_body = NSLocalizedString("FxAPush_DeviceDisconnected_body", value: "%@ has been successfully disconnected.", comment: "Body of a notification displayed when named device has been disconnected from FxA. %@ refers to the name of the disconnected device.") + + public static let FxAPush_DeviceDisconnected_UnknownDevice_body = NSLocalizedString("FxAPush_DeviceDisconnected_UnknownDevice_body", value: "A device has disconnected from Firefox Sync", comment: "Body of a notification displayed when unnamed device has been disconnected from FxA.") + + public static let FxAPush_DeviceConnected_title = NSLocalizedString("FxAPush_DeviceConnected_title", value: "Sync Connected", comment: "Title of a notification displayed when another device has connected to FxA.") + public static let FxAPush_DeviceConnected_body = NSLocalizedString("FxAPush_DeviceConnected_body", value: "Firefox Sync has connected to %@", comment: "Title of a notification displayed when another device has connected to FxA. %@ refers to the name of the newly connected device.") +} + +// Reader Mode. +extension Strings { + public static let ReaderModeAvailableVoiceOverAnnouncement = NSLocalizedString("ReaderMode.Available.VoiceOverAnnouncement", value: "Reader Mode available", comment: "Accessibility message e.g. spoken by VoiceOver when Reader Mode becomes available.") + public static let ReaderModeResetFontSizeAccessibilityLabel = NSLocalizedString("Reset text size", comment: "Accessibility label for button resetting font size in display settings of reader mode") +} + +// QR Code scanner. +extension Strings { + public static let ScanQRCodeViewTitle = NSLocalizedString("ScanQRCode.View.Title", value: "Scan QR Code", comment: "Title for the QR code scanner view.") + public static let ScanQRCodeInstructionsLabel = NSLocalizedString("ScanQRCode.Instructions.Label", value: "Align QR code within frame to scan", comment: "Text for the instructions label, displayed in the QR scanner view") + public static let ScanQRCodeInvalidDataErrorMessage = NSLocalizedString("ScanQRCode.InvalidDataError.Message", value: "The data is invalid", comment: "Text of the prompt user the data is invalid") + public static let ScanQRCodePermissionErrorMessage = NSLocalizedString("ScanQRCode.PermissionError.Message", value: "Please allow Firefox to access your device’s camera in ‘Settings’ -> ‘Privacy’ -> ‘Camera’.", comment: "Text of the prompt user to setup the camera authorization.") + public static let ScanQRCodeErrorOKButton = NSLocalizedString("ScanQRCode.Error.OK.Button", value: "OK", comment: "OK button to dismiss the error prompt.") +} + +// App menu. +extension Strings { + public static let AppMenuAddToReadingListTitleString = NSLocalizedString("Menu.AddToReadingList.Title", tableName: "Menu", value: "Add to Reading List", comment: "Label for the button, displayed in the menu, used to add a page to the reading list.") + public static let AppMenuShowTabsTitleString = NSLocalizedString("Menu.ShowTabs.Title", tableName: "Menu", value: "Show Tabs", comment: "Label for the button, displayed in the menu, used to open the tabs tray") + public static let AppMenuSharePageTitleString = NSLocalizedString("Menu.SharePageAction.Title", tableName: "Menu", value: "Share Page With…", comment: "Label for the button, displayed in the menu, used to open the share dialog.") + public static let AppMenuCopyURLTitleString = NSLocalizedString("Menu.CopyURL.Title", tableName: "Menu", value: "Copy URL", comment: "Label for the button, displayed in the menu, used to copy the page url to the clipboard.") + public static let AppMenuNewTabTitleString = NSLocalizedString("Menu.NewTabAction.Title", tableName: "Menu", value: "Open New Tab", comment: "Label for the button, displayed in the menu, used to open a new tab") + public static let AppMenuNewPrivateTabTitleString = NSLocalizedString("Menu.NewPrivateTabAction.Title", tableName: "Menu", value: "Open New Private Tab", comment: "Label for the button, displayed in the menu, used to open a new private tab.") + public static let AppMenuAddBookmarkTitleString = NSLocalizedString("Menu.AddBookmarkAction.Title", tableName: "Menu", value: "Bookmark This Page", comment: "Label for the button, displayed in the menu, used to create a bookmark for the current website.") + public static let AppMenuRemoveBookmarkTitleString = NSLocalizedString("Menu.RemoveBookmarkAction.Title", tableName: "Menu", value: "Remove Bookmark", comment: "Label for the button, displayed in the menu, used to delete an existing bookmark for the current website.") + public static let AppMenuFindInPageTitleString = NSLocalizedString("Menu.FindInPageAction.Title", tableName: "Menu", value: "Find in Page", comment: "Label for the button, displayed in the menu, used to open the toolbar to search for text within the current page.") + public static let AppMenuViewDesktopSiteTitleString = NSLocalizedString("Menu.ViewDekstopSiteAction.Title", tableName: "Menu", value: "Request Desktop Site", comment: "Label for the button, displayed in the menu, used to request the desktop version of the current website.") + public static let AppMenuViewMobileSiteTitleString = NSLocalizedString("Menu.ViewMobileSiteAction.Title", tableName: "Menu", value: "Request Mobile Site", comment: "Label for the button, displayed in the menu, used to request the mobile version of the current website.") + public static let AppMenuScanQRCodeTitleString = NSLocalizedString("Menu.ScanQRCodeAction.Title", tableName: "Menu", value: "Scan QR Code", comment: "Label for the button, displayed in the menu, used to open the QR code scanner.") + public static let AppMenuSettingsTitleString = NSLocalizedString("Menu.OpenSettingsAction.Title", tableName: "Menu", value: "Settings", comment: "Label for the button, displayed in the menu, used to open the Settings menu.") + public static let AppMenuCloseAllTabsTitleString = NSLocalizedString("Menu.CloseAllTabsAction.Title", tableName: "Menu", value: "Close All Tabs", comment: "Label for the button, displayed in the menu, used to close all tabs currently open.") + public static let AppMenuOpenHomePageTitleString = NSLocalizedString("Menu.OpenHomePageAction.Title", tableName: "Menu", value: "Open Homepage", comment: "Label for the button, displayed in the menu, used to navigate to the home page.") + public static let AppMenuTopSitesTitleString = NSLocalizedString("Menu.OpenTopSitesAction.AccessibilityLabel", tableName: "Menu", value: "Top Sites", comment: "Accessibility label for the button, displayed in the menu, used to open the Top Sites home panel.") + public static let AppMenuBookmarksTitleString = NSLocalizedString("Menu.OpenBookmarksAction.AccessibilityLabel", tableName: "Menu", value: "Bookmarks", comment: "Accessibility label for the button, displayed in the menu, used to open the Bbookmarks home panel.") + public static let AppMenuHistoryTitleString = NSLocalizedString("Menu.OpenHistoryAction.AccessibilityLabel", tableName: "Menu", value: "History", comment: "Accessibility label for the button, displayed in the menu, used to open the History home panel.") + public static let AppMenuReadingListTitleString = NSLocalizedString("Menu.OpenReadingListAction.AccessibilityLabel", tableName: "Menu", value: "Reading List", comment: "Accessibility label for the button, displayed in the menu, used to open the Reading list home panel.") + public static let AppMenuButtonAccessibilityLabel = NSLocalizedString("Toolbar.Menu.AccessibilityLabel", value: "Menu", comment: "Accessibility label for the Menu button.") + public static let TabTrayDeleteMenuButtonAccessibilityLabel = NSLocalizedString("Toolbar.Menu.CloseAllTabs", value: "Close All Tabs", comment: "Accessibility label for the Close All Tabs menu button.") + public static let AppMenuNightModeEnable = NSLocalizedString("Menu.NightModeTurnOn.Label", value: "Enable Night Mode", comment: "Label for the button, displayed in the menu, turns on night mode.") + public static let AppMenuNightModeDisable = NSLocalizedString("Menu.NightModeTurnOff.Label", value: "Disable Night Mode", comment: "Label for the button, displayed in the menu, turns off night mode") + public static let AppMenuNoImageModeEnable = NSLocalizedString("Menu.NoImageModeHideImages.Label", value: "Hide Images", comment: "Label for the button, displayed in the menu, hides images on the webpage when pressed.") + public static let AppMenuNoImageModeDisable = NSLocalizedString("Menu.NoImageModeShowImages.Label", value: "Show Images", comment: "Label for the button, displayed in the menu, shows images on the webpage when pressed") + public static let AppMenuCopyURLConfirmMessage = NSLocalizedString("Menu.CopyURL.Confirm", value: "URL Copied To Clipboard", comment: "Toast displayed to user after copy url pressed.") + public static let AppMenuAddBookmarkConfirmMessage = NSLocalizedString("Menu.AddBookmark.Confirm", value: "Bookmark Added", comment: "Toast displayed to the user after a bookmark has been added.") + public static let AppMenuRemoveBookmarkConfirmMessage = NSLocalizedString("Menu.RemoveBookmark.Confirm", value: "Bookmark Removed", comment: "Toast displayed to the user after a bookmark has been removed.") + public static let AppMenuAddToReadingListConfirmMessage = NSLocalizedString("Menu.AddToReadingList.Confirm", value: "Added To Reading List", comment: "Toast displayed to the user after adding the item to their reading list.") + public static let SendToDeviceTitle = NSLocalizedString("Send to Device", tableName: "3DTouchActions", comment: "Label for preview action on Tab Tray Tab to send the current tab to another device") +} + +// Snackbar shown when tapping app store link +extension Strings { + public static let ExternalLinkAppStoreConfirmationTitle = NSLocalizedString("ExternalLink.AppStore.ConfirmationTitle", value: "Open this link in the App Store app?", comment: "Question shown to user when tapping a link that opens the App Store app") +} + +// ContentBlocker/TrackingProtection strings +extension Strings { + public static let SettingsTrackingProtectionSectionName = NSLocalizedString("Settings.TrackingProtection.SectionName", value: "Tracking Protection", comment: "Row in top-level of settings that gets tapped to show the tracking protection settings detail view.") + + public static let TrackingProtectionOptionAlwaysOn = NSLocalizedString("Settings.TrackingProtectionOption.OnLabel", value: "Always On", comment: "Settings option to specify that Tracking Protection is always on.") + public static let TrackingProtectionOptionAlwaysOff = NSLocalizedString("Settings.TrackingProtectionOption.OffLabel", value: "Never", comment: "Settings option to specify that Tracking Protection is always off.") + public static let TrackingProtectionOptionOnInPrivateBrowsing = NSLocalizedString("Settings.TrackingProtectionOption.OnInPrivateBrowsingLabel", value: "Private Browsing Mode Only", comment: "Settings option to specify that Tracking Protection is on only in Private Browsing mode.") + + public static let TrackingProtectionOptionOnOffHeader = NSLocalizedString("Settings.TrackingProtectionOption.EnabledStateHeaderLabel", value: "Use Tracking Protection to Block Known Trackers", comment: "Description label shown at the top of tracking protection options screen.") + public static let TrackingProtectionOptionOnOffFooter = NSLocalizedString("Settings.TrackingProtectionOption.EnabledStateFooterLabel", value: "Tracking is the collection of your browsing data across multiple websites. Tracking can be used to build a profile and display content based on your browsing and personal information.", comment: "Description label shown on tracking protection options screen.") + public static let TrackingProtectionOptionBlockListsTitle = NSLocalizedString("Settings.TrackingProtection.BlockListsTitle", value: "Block Lists", comment: "Title for tracking protection options section where Basic/Strict block list can be selected") + public static let TrackingProtectionOptionBlockListsHeader = NSLocalizedString("Settings.TrackingProtection.BlockListsHeader", value: "You can choose which list Firefox will use to block Web elements that may track your browsing activity.", comment: "Header description for tracking protection options section where Basic/Strict block list can be selected") + + public static let TrackingProtectionOptionBlockListTypeBasic = NSLocalizedString("Settings.TrackingProtectionOption.BlockListBasic", value: "Basic (Recommended)", comment: "Tracking protection settings option for using the basic blocklist.") + public static let TrackingProtectionOptionBlockListTypeBasicDescription = NSLocalizedString("Settings.TrackingProtectionOption.BlockListBasicDescription", value: "Allows some trackers so websites function properly.", comment: "Tracking protection settings option description for using the basic blocklist.") + public static let TrackingProtectionOptionBlockListTypeStrict = NSLocalizedString("Settings.TrackingProtectionOption.BlockListStrict", value: "Strict", comment: "Tracking protection settings option for using the strict blocklist.") + public static let TrackingProtectionOptionBlockListTypeStrictDescription = NSLocalizedString("Settings.TrackingProtectionOption.BlockListStrictDescription", value: "Blocks known trackers. Some websites may not function properly.", comment: "Tracking protection settings option description for using the strict blocklist.") +} + +// Settings Home +extension Strings { + public static let SendUsageSettingTitle = NSLocalizedString("Settings.SendUsage.Title", value: "Send Usage Data", comment: "The title for the setting to send usage data.") + public static let SendUsageSettingLink = NSLocalizedString("Settings.SendUsage.Link", value: "Learn More.", comment: "title for a link that explains how mozilla collects telemetry") + public static let SendUsageSettingMessage = NSLocalizedString("Settings.SendUsage.Message", value: "Mozilla strives to only collect what we need to provide and improve Firefox for everyone.", comment: "A short description that explains why mozilla collects usage data.") +} + +// Do not track +extension Strings { + public static let SettingsDoNotTrackTitle = NSLocalizedString("Settings.DNT.Title", value: "Send websites a Do Not Track signal that you don’t want to be tracked", comment: "DNT Settings title") + public static let SettingsDoNotTrackOptionOnWithTP = NSLocalizedString("Settings.DNT.OptionOnWithTP", value: "Only when using Tracking Protection", comment: "DNT Settings option for only turning on when Tracking Protection is also on") + public static let SettingsDoNotTrackOptionAlwaysOn = NSLocalizedString("Settings.DNT.OptionAlwaysOn", value: "Always", comment: "DNT Settings option for always on") +} + +// MARK: Deprecated Strings (to be removed in next version) +private let logOut = NSLocalizedString("Log Out", comment: "Button in settings screen to disconnect from your account") +private let logOutQuestion = NSLocalizedString("Log Out?", comment: "Title of the 'log out firefox account' alert") +private let logOutDestructive = NSLocalizedString("Log Out", comment: "Disconnect button in the 'log out firefox account' alert") diff --git a/mobile/ios/Client/Frontend/UIConstants.swift b/mobile/ios/Client/Frontend/UIConstants.swift new file mode 100644 index 0000000000..4cc806f598 --- /dev/null +++ b/mobile/ios/Client/Frontend/UIConstants.swift @@ -0,0 +1,127 @@ +/* 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 + +// A browser color represents the color of UI in both Private browsing mode and normal mode +struct BrowserColor { + let normalColor: Int + let PBMColor: Int + init(normal: Int, pbm: Int) { + self.normalColor = normal + self.PBMColor = pbm + } + func color(isPBM: Bool) -> UIColor { + return UIColor(rgb: isPBM ? PBMColor : normalColor) + } +} + +public struct UIConstants { + static let AboutHomePage = URL(string: "\(WebServer.sharedInstance.base)/about/home/")! + + // Photon Colors. Remove old colors once we've completly transitioned + static let BrowserUI = BrowserColor(normal: 0xf9f9fa, pbm: 0x38383D) + static let TextColor = BrowserColor(normal: 0xffffff, pbm: 0x414146) + static let URLBarDivider = BrowserColor(normal: 0xE4E4E4, pbm: 0x414146) + static let TabTrayBG = UIColor(rgb: 0x272727) + static let locationBarBG = UIColor(rgb: 0xD7D7DB) + static let LoadingStartColor = BrowserColor(normal: 0x00DCFC, pbm: 0x9400ff) + static let LoadingEndColor = BrowserColor(normal: 0x0A84FF, pbm: 0xff1ad9) + + //Photon UI sizes + static let TopToolbarHeight: CGFloat = 56 + // The loading bar starts with one color and then animates to the second one + static let LoadingBarStart = BrowserColor(normal: 0x00DCFC, pbm: 0x9f00ff) + static let LoadingBarEnd = BrowserColor(normal: 0x0A84FF, pbm: 0xff1ad9) + + static let TextSelectionBG = UIColor(rgb: 0xE4E4E4) + + static let TopTabsBG = TabTrayBG + + static let AppBackgroundColor = UIColor(rgb: 0xf9f9fa) + static let SystemBlueColor = UIColor(rgb: 0x0297F8) + static let PrivateModePurple = UIColor(red: 207 / 255, green: 104 / 255, blue: 255 / 255, alpha: 1) + static let PrivateModeLocationBackgroundColor = UIColor(red: 31 / 255, green: 31 / 255, blue: 31 / 255, alpha: 1) + static let PrivateModeLocationBorderColor = UIColor(red: 255, green: 255, blue: 255, alpha: 0.15) + static let PrivateModeActionButtonTintColor = UIColor(red: 255, green: 255, blue: 255, alpha: 0.8) + static let PrivateModeTextHighlightColor = UIColor(red: 207 / 255, green: 104 / 255, blue: 255 / 255, alpha: 1) + static let PrivateModeInputHighlightColor = UIColor(red: 120 / 255, green: 120 / 255, blue: 165 / 255, alpha: 1) + static let PrivateModeAssistantToolbarBackgroundColor = UIColor(red: 89 / 255, green: 89 / 255, blue: 89 / 255, alpha: 1) + static let PrivateModeToolbarTintColor = UIColor(red: 74 / 255, green: 74 / 255, blue: 74 / 255, alpha: 1) + + static var ToolbarHeight: CGFloat = 46 + static var BottomToolbarHeight: CGFloat { + get { + var bottomInset: CGFloat = 0.0 + if #available(iOS 11, *) { + if let window = UIApplication.shared.keyWindow { + bottomInset = window.safeAreaInsets.bottom + } + } + return ToolbarHeight + bottomInset + } + } + static let DefaultRowHeight: CGFloat = 58 + static let DefaultPadding: CGFloat = 10 + static let SnackbarButtonHeight: CGFloat = 48 + + // Static fonts + static let DefaultChromeSize: CGFloat = 16 + static let DefaultChromeSmallSize: CGFloat = 11 + static let PasscodeEntryFontSize: CGFloat = 36 + static let DefaultChromeFont: UIFont = UIFont.systemFont(ofSize: DefaultChromeSize, weight: UIFontWeightRegular) + static let DefaultChromeBoldFont = UIFont.systemFont(ofSize: DefaultChromeSize, weight: UIFontWeightHeavy) + static let DefaultChromeSmallFontBold = UIFont.boldSystemFont(ofSize: DefaultChromeSmallSize) + static let PasscodeEntryFont = UIFont.systemFont(ofSize: PasscodeEntryFontSize, weight: UIFontWeightBold) + + // These highlight colors are currently only used on Snackbar buttons when they're pressed + static let HighlightColor = UIColor(red: 205/255, green: 223/255, blue: 243/255, alpha: 0.9) + static let HighlightText = UIColor(red: 42/255, green: 121/255, blue: 213/255, alpha: 1.0) + + static let PanelBackgroundColor = UIColor.white + static let SeparatorColor = UIColor(rgb: 0xcccccc) + static let HighlightBlue = UIColor(red: 76/255, green: 158/255, blue: 255/255, alpha: 1) + static let DestructiveRed = UIColor(red: 255/255, green: 64/255, blue: 0/255, alpha: 1.0) + static let BorderColor = UIColor.darkGray + static let BackgroundColor = AppBackgroundColor + + // These colours are used on the Menu + static let MenuToolbarBackgroundColorNormal = AppBackgroundColor + static let MenuToolbarBackgroundColorPrivate = UIColor(red: 74/255, green: 74/255, blue: 74/255, alpha: 1.0) + static let MenuToolbarTintColorNormal = BackgroundColor + static let MenuToolbarTintColorPrivate = UIColor.white + static let MenuBackgroundColorNormal = UIColor(red: 223/255, green: 223/255, blue: 223/255, alpha: 1.0) + static let MenuBackgroundColorPrivate = UIColor(red: 59/255, green: 59/255, blue: 59/255, alpha: 1.0) + static let MenuSelectedItemTintColor = UIColor(red: 0.30, green: 0.62, blue: 1.0, alpha: 1.0) + static let MenuDisabledItemTintColor = UIColor.lightGray + + // settings + static let TableViewHeaderBackgroundColor = AppBackgroundColor + static let TableViewHeaderTextColor = UIColor(rgb: 0x737373) + static let TableViewRowTextColor = UIColor(rgb: 0x0c0c0d) + static let TableViewDisabledRowTextColor = UIColor.lightGray + static let TableViewSeparatorColor = UIColor(rgb: 0xD1D1D4) + static let TableViewHeaderFooterHeight = CGFloat(44) + static let TableViewRowErrorTextColor = UIColor(red: 255/255, green: 0/255, blue: 26/255, alpha: 1.0) + static let TableViewRowWarningTextColor = UIColor(red: 245/255, green: 166/255, blue: 35/255, alpha: 1.0) + static let TableViewRowActionAccessoryColor = UIColor(red: 0.29, green: 0.56, blue: 0.89, alpha: 1.0) + static let TableViewRowSyncTextColor = UIColor(red: 51/255, green: 51/255, blue: 51/255, alpha: 1.0) + + // Firefox Orange + static let ControlTintColor = SystemBlueColor + + // List of Default colors to use for Favicon backgrounds + static let DefaultColorStrings = ["2e761a", "399320", "40a624", "57bd35", "70cf5b", "90e07f", "b1eea5", "881606", "aa1b08", "c21f09", "d92215", "ee4b36", "f67964", "ffa792", "025295", "0568ba", "0675d3", "0996f8", "2ea3ff", "61b4ff", "95cdff", "00736f", "01908b", "01a39d", "01bdad", "27d9d2", "58e7e6", "89f4f5", "c84510", "e35b0f", "f77100", "ff9216", "ffad2e", "ffc446", "ffdf81", "911a2e", "b7223b", "cf2743", "ea385e", "fa526e", "ff7a8d", "ffa7b3" ] + + // Passcode dot gray + static let PasscodeDotColor = UIColor(rgb: 0x4A4A4A) + + /// JPEG compression quality for persisted screenshots. Must be between 0-1. + static let ScreenshotQuality: Float = 0.3 + static let ActiveScreenshotQuality: CGFloat = 0.5 + + static let OKString = NSLocalizedString("OK", comment: "OK button") + static let CancelString = NSLocalizedString("Cancel", comment: "Label for Cancel button") +} diff --git a/mobile/ios/Client/Frontend/Widgets/ActivityStreamHighlightCell.swift b/mobile/ios/Client/Frontend/Widgets/ActivityStreamHighlightCell.swift new file mode 100644 index 0000000000..c42392b13e --- /dev/null +++ b/mobile/ios/Client/Frontend/Widgets/ActivityStreamHighlightCell.swift @@ -0,0 +1,237 @@ +/* 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 UIKit +import Shared +import Storage + +struct ActivityStreamHighlightCellUX { + static let LabelColor = UIAccessibilityDarkerSystemColorsEnabled() ? UIColor.black : UIColor(rgb: 0x353535) + static let BorderWidth: CGFloat = 0.5 + static let CellSideOffset = 20 + static let TitleLabelOffset = 2 + static let CellTopBottomOffset = 12 + static let SiteImageViewSize: CGSize = UIDevice.current.userInterfaceIdiom == .pad ? CGSize(width: 99, height: 120) : CGSize(width: 99, height: 90) + static let StatusIconSize = 12 + static let FaviconSize = CGSize(width: 45, height: 45) + static let DescriptionLabelColor = UIColor(rgb: 0x919191) + static let SelectedOverlayColor = UIColor(white: 0.0, alpha: 0.25) + static let CornerRadius: CGFloat = 3 + static let BorderColor = UIColor(white: 0, alpha: 0.1) +} + +class ActivityStreamHighlightCell: UICollectionViewCell { + + fileprivate lazy var titleLabel: UILabel = { + let titleLabel = UILabel() + titleLabel.font = DynamicFontHelper.defaultHelper.MediumSizeHeavyWeightAS + titleLabel.textColor = ActivityStreamHighlightCellUX.LabelColor + titleLabel.textAlignment = .left + titleLabel.numberOfLines = 3 + return titleLabel + }() + + fileprivate lazy var descriptionLabel: UILabel = { + let descriptionLabel = UILabel() + descriptionLabel.font = DynamicFontHelper.defaultHelper.SmallSizeRegularWeightAS + descriptionLabel.textColor = ActivityStreamHighlightCellUX.DescriptionLabelColor + descriptionLabel.textAlignment = .left + descriptionLabel.numberOfLines = 1 + return descriptionLabel + }() + + fileprivate lazy var domainLabel: UILabel = { + let descriptionLabel = UILabel() + descriptionLabel.font = DynamicFontHelper.defaultHelper.SmallSizeRegularWeightAS + descriptionLabel.textColor = ActivityStreamHighlightCellUX.DescriptionLabelColor + descriptionLabel.textAlignment = .left + descriptionLabel.numberOfLines = 1 + descriptionLabel.setContentCompressionResistancePriority(1000, for: UILayoutConstraintAxis.vertical) + return descriptionLabel + }() + + lazy var siteImageView: UIImageView = { + let siteImageView = UIImageView() + siteImageView.contentMode = UIViewContentMode.scaleAspectFit + siteImageView.clipsToBounds = true + siteImageView.contentMode = UIViewContentMode.center + siteImageView.layer.cornerRadius = ActivityStreamHighlightCellUX.CornerRadius + siteImageView.layer.borderColor = ActivityStreamHighlightCellUX.BorderColor.cgColor + siteImageView.layer.borderWidth = ActivityStreamHighlightCellUX.BorderWidth + siteImageView.layer.masksToBounds = true + return siteImageView + }() + + fileprivate lazy var statusIcon: UIImageView = { + let statusIcon = UIImageView() + statusIcon.contentMode = UIViewContentMode.scaleAspectFit + statusIcon.clipsToBounds = true + statusIcon.layer.cornerRadius = ActivityStreamHighlightCellUX.CornerRadius + return statusIcon + }() + + fileprivate lazy var selectedOverlay: UIView = { + let selectedOverlay = UIView() + selectedOverlay.backgroundColor = ActivityStreamHighlightCellUX.SelectedOverlayColor + selectedOverlay.isHidden = true + return selectedOverlay + }() + + override var isSelected: Bool { + didSet { + self.selectedOverlay.isHidden = !isSelected + } + } + + override init(frame: CGRect) { + super.init(frame: frame) + + layer.shouldRasterize = true + layer.rasterizationScale = UIScreen.main.scale + + isAccessibilityElement = true + + contentView.addSubview(siteImageView) + contentView.addSubview(descriptionLabel) + contentView.addSubview(selectedOverlay) + contentView.addSubview(titleLabel) + contentView.addSubview(statusIcon) + contentView.addSubview(domainLabel) + + siteImageView.snp.makeConstraints { make in + make.top.equalTo(contentView) + make.leading.trailing.equalTo(contentView) + make.centerX.equalTo(contentView) + make.height.equalTo(ActivityStreamHighlightCellUX.SiteImageViewSize) + } + + selectedOverlay.snp.makeConstraints { make in + make.edges.equalTo(contentView) + } + + domainLabel.snp.makeConstraints { make in + make.leading.equalTo(siteImageView) + make.trailing.equalTo(contentView) + make.top.equalTo(siteImageView.snp.bottom).offset(5) + } + + titleLabel.snp.makeConstraints { make in + make.leading.equalTo(siteImageView) + make.trailing.equalTo(contentView) + make.top.equalTo(domainLabel.snp.bottom).offset(5) + } + + descriptionLabel.snp.makeConstraints { make in + make.leading.equalTo(statusIcon.snp.trailing).offset(ActivityStreamHighlightCellUX.TitleLabelOffset) + make.bottom.equalTo(contentView) + } + + statusIcon.snp.makeConstraints { make in + make.size.equalTo(ActivityStreamHighlightCellUX.StatusIconSize) + make.centerY.equalTo(descriptionLabel.snp.centerY) + make.leading.equalTo(siteImageView) + } + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func prepareForReuse() { + super.prepareForReuse() + self.siteImageView.image = nil + contentView.backgroundColor = UIColor.clear + siteImageView.backgroundColor = UIColor.clear + } + + func configureWithSite(_ site: Site) { + if let mediaURLStr = site.metadata?.mediaURL, + let mediaURL = URL(string: mediaURLStr) { + self.siteImageView.sd_setImage(with: mediaURL) + self.siteImageView.contentMode = .scaleAspectFill + } else { + let itemURL = site.tileURL + self.siteImageView.setFavicon(forSite: site, onCompletion: { [weak self] (color, url) in + if itemURL == url { + self?.siteImageView.image = self?.siteImageView.image?.createScaled(ActivityStreamHighlightCellUX.FaviconSize) + self?.siteImageView.backgroundColor = color + } + }) + self.siteImageView.contentMode = .center + } + + self.domainLabel.text = site.tileURL.hostSLD + self.titleLabel.text = site.title.characters.count <= 1 ? site.url : site.title + + if let bookmarked = site.bookmarked, bookmarked { + self.descriptionLabel.text = Strings.HighlightBookmarkText + self.statusIcon.image = UIImage(named: "context_bookmark") + } else { + self.descriptionLabel.text = Strings.HighlightVistedText + self.statusIcon.image = UIImage(named: "context_viewed") + } + } + + func configureWithPocketStory(_ pocketStory: PocketStory) { + self.siteImageView.sd_setImage(with: pocketStory.imageURL) + self.siteImageView.contentMode = .scaleAspectFill + + self.domainLabel.text = pocketStory.domain + self.titleLabel.text = pocketStory.title + + self.descriptionLabel.text = Strings.PocketTrendingText + self.statusIcon.image = UIImage(named: "context_pocket") + } +} + +struct HighlightIntroCellUX { + static let margin: CGFloat = 20 + static let foxImageWidth: CGFloat = 168 +} + +class HighlightIntroCell: UICollectionViewCell { + + lazy var titleLabel: UILabel = { + let textLabel = UILabel() + textLabel.font = DynamicFontHelper.defaultHelper.MediumSizeBoldFontAS + textLabel.textColor = UIColor.black + textLabel.numberOfLines = 1 + textLabel.adjustsFontSizeToFitWidth = true + textLabel.minimumScaleFactor = 0.8 + return textLabel + }() + + lazy var descriptionLabel: UILabel = { + let label = UILabel() + label.font = DynamicFontHelper.defaultHelper.MediumSizeRegularWeightAS + label.textColor = UIColor.darkGray + label.numberOfLines = 0 + return label + }() + + override init(frame: CGRect) { + super.init(frame: frame) + + contentView.addSubview(titleLabel) + contentView.addSubview(descriptionLabel) + + titleLabel.text = Strings.HighlightIntroTitle + descriptionLabel.text = Strings.HighlightIntroDescription + + let titleInsets = UIEdgeInsets(top: HighlightIntroCellUX.margin, left: 0, bottom: 0, right: 0) + titleLabel.snp.makeConstraints { make in + make.leading.top.trailing.equalTo(self.contentView).inset(titleInsets) + } + + descriptionLabel.snp.makeConstraints { make in + make.leading.trailing.equalTo(titleLabel) + make.top.equalTo(titleLabel.snp.bottom).offset(HighlightIntroCellUX.margin/2) + } + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + +} diff --git a/mobile/ios/Client/Frontend/Widgets/AutocompleteTextField.swift b/mobile/ios/Client/Frontend/Widgets/AutocompleteTextField.swift new file mode 100644 index 0000000000..ce6b4b9fd0 --- /dev/null +++ b/mobile/ios/Client/Frontend/Widgets/AutocompleteTextField.swift @@ -0,0 +1,280 @@ +/* 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/. */ + +// This code is loosely based on https://github.com/Antol/APAutocompleteTextField + +import UIKit +import Shared + +/// Delegate for the text field events. Since AutocompleteTextField owns the UITextFieldDelegate, +/// callers must use this instead. +protocol AutocompleteTextFieldDelegate: class { + func autocompleteTextField(_ autocompleteTextField: AutocompleteTextField, didEnterText text: String) + func autocompleteTextFieldShouldReturn(_ autocompleteTextField: AutocompleteTextField) -> Bool + func autocompleteTextFieldShouldClear(_ autocompleteTextField: AutocompleteTextField) -> Bool + func autocompleteTextFieldDidBeginEditing(_ autocompleteTextField: AutocompleteTextField) +} + +struct AutocompleteTextFieldUX { + static let HighlightColor = UIColor(rgb: 0xccdded) +} + +class AutocompleteTextField: UITextField, UITextFieldDelegate { + var autocompleteDelegate: AutocompleteTextFieldDelegate? + + // AutocompleteTextLabel repersents the actual autocomplete text. + // The textfields "text" property only contains the entered text, while this label holds the autocomplete text + // This makes sure that the autocomplete doesnt mess with keyboard suggestions provided by third party keyboards. + private var autocompleteTextLabel: UILabel? + private var hideCursor: Bool = false + + var isSelectionActive: Bool { + return autocompleteTextLabel != nil + } + + // This variable is a solution to get the right behavior for refocusing + // the AutocompleteTextField. The initial transition into Overlay Mode + // doesn't involve the user interacting with AutocompleteTextField. + // Thus, we update shouldApplyCompletion in touchesBegin() to reflect whether + // the highlight is active and then the text field is updated accordingly + // in touchesEnd() (eg. applyCompletion() is called or not) + fileprivate var notifyTextChanged: (() -> Void)? + private var lastReplacement: String? + + var highlightColor = AutocompleteTextFieldUX.HighlightColor + + override var text: String? { + didSet { + super.text = text + self.textDidChange(self) + } + } + + override var accessibilityValue: String? { + get { + return (self.text ?? "") + (self.autocompleteTextLabel?.text ?? "") + } + set(value) { + super.accessibilityValue = value + } + } + + override init(frame: CGRect) { + super.init(frame: frame) + commonInit() + } + + required init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + commonInit() + } + + fileprivate func commonInit() { + super.delegate = self + super.addTarget(self, action: #selector(AutocompleteTextField.textDidChange(_:)), for: UIControlEvents.editingChanged) + notifyTextChanged = debounce(0.1, action: { + if self.isEditing { + self.autocompleteDelegate?.autocompleteTextField(self, didEnterText: self.normalizeString(self.text ?? "")) + } + }) + } + + override var keyCommands: [UIKeyCommand]? { + return [ + UIKeyCommand(input: UIKeyInputLeftArrow, modifierFlags: .init(rawValue: 0), action: #selector(self.handleKeyCommand(sender:))), + UIKeyCommand(input: UIKeyInputRightArrow, modifierFlags: .init(rawValue: 0), action: #selector(self.handleKeyCommand(sender:))) + ] + } + + func handleKeyCommand(sender: UIKeyCommand) { + switch sender.input { + case UIKeyInputLeftArrow: + if isSelectionActive { + applyCompletion() + + // Set the current position to the beginning of the text. + selectedTextRange = textRange(from: beginningOfDocument, to: beginningOfDocument) + } else if let range = selectedTextRange { + if range.start == beginningOfDocument { + return + } + + guard let cursorPosition = position(from: range.start, offset: -1) else { + return + } + + selectedTextRange = textRange(from: cursorPosition, to: cursorPosition) + } + return + case UIKeyInputRightArrow: + if isSelectionActive { + applyCompletion() + + // Set the current position to the end of the text. + selectedTextRange = textRange(from: endOfDocument, to: endOfDocument) + } else if let range = selectedTextRange { + if range.end == endOfDocument { + return + } + + guard let cursorPosition = position(from: range.end, offset: 1) else { + return + } + + selectedTextRange = textRange(from: cursorPosition, to: cursorPosition) + } + return + default: + return + } + } + + func highlightAll() { + let text = self.text + self.text = "" + setAutocompleteSuggestion(text ?? "") + selectedTextRange = textRange(from: endOfDocument, to: endOfDocument) + } + + fileprivate func normalizeString(_ string: String) -> String { + return string.lowercased().stringByTrimmingLeadingCharactersInSet(CharacterSet.whitespaces) + } + + /// Commits the completion by setting the text and removing the highlight. + fileprivate func applyCompletion() { + + // Clear the current completion, then set the text without the attributed style. + let text = (self.text ?? "") + (self.autocompleteTextLabel?.text ?? "") + removeCompletion() + self.text = text + hideCursor = false + // Move the cursor to the end of the completion. + selectedTextRange = textRange(from: endOfDocument, to: endOfDocument) + } + + /// Removes the autocomplete-highlighted + fileprivate func removeCompletion() { + autocompleteTextLabel?.removeFromSuperview() + autocompleteTextLabel = nil + } + + // `shouldChangeCharactersInRange` is called before the text changes, and textDidChange is called after. + // Since the text has changed, remove the completion here, and textDidChange will fire the callback to + // get the new autocompletion. + func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { + lastReplacement = string + return true + } + + func setAutocompleteSuggestion(_ suggestion: String?) { + let text = self.text ?? "" + + guard let suggestion = suggestion, isEditing && markedTextRange == nil else { + hideCursor = false + return + } + + let normalized = normalizeString(text) + guard suggestion.startsWith(normalized) && normalized.characters.count < suggestion.characters.count else { + hideCursor = false + return + } + + let suggestionText = suggestion.substring(from: suggestion.characters.index(suggestion.startIndex, offsetBy: normalized.characters.count)) + let autocompleteText = NSMutableAttributedString(string: suggestionText) + autocompleteText.addAttribute(NSBackgroundColorAttributeName, value: highlightColor, range: NSRange(location: 0, length: suggestionText.characters.count)) + autocompleteTextLabel?.removeFromSuperview() // should be nil. But just in case + autocompleteTextLabel = createAutocompleteLabelWith(autocompleteText) + if let l = autocompleteTextLabel { + addSubview(l) + hideCursor = true + forceResetCursor() + } + } + + override func caretRect(for position: UITextPosition) -> CGRect { + return hideCursor ? CGRect.zero : super.caretRect(for: position) + } + + private func createAutocompleteLabelWith(_ autocompleteText: NSAttributedString) -> UILabel { + let label = UILabel() + var frame = self.bounds + label.attributedText = autocompleteText + label.font = self.font + label.accessibilityIdentifier = "autocomplete" + label.backgroundColor = self.backgroundColor + label.textColor = self.textColor + + let enteredTextSize = self.attributedText?.boundingRect(with: self.frame.size, options: NSStringDrawingOptions.usesLineFragmentOrigin, context: nil) + frame.origin.x = (enteredTextSize?.width.rounded() ?? 0) + frame.size.width = self.frame.size.width - frame.origin.x + frame.size.height = self.frame.size.height - 1 + label.frame = frame + return label + } + + func textFieldDidBeginEditing(_ textField: UITextField) { + autocompleteDelegate?.autocompleteTextFieldDidBeginEditing(self) + } + + func textFieldShouldEndEditing(_ textField: UITextField) -> Bool { + applyCompletion() + return true + } + + func textFieldShouldReturn(_ textField: UITextField) -> Bool { + applyCompletion() + return autocompleteDelegate?.autocompleteTextFieldShouldReturn(self) ?? true + } + + func textFieldShouldClear(_ textField: UITextField) -> Bool { + removeCompletion() + return autocompleteDelegate?.autocompleteTextFieldShouldClear(self) ?? true + } + + override func setMarkedText(_ markedText: String?, selectedRange: NSRange) { + // Clear the autocompletion if any provisionally inserted text has been + // entered (e.g., a partial composition from a Japanese keyboard). + removeCompletion() + super.setMarkedText(markedText, selectedRange: selectedRange) + } + + func textDidChange(_ textField: UITextField) { + hideCursor = autocompleteTextLabel != nil + removeCompletion() + + let isAtEnd = selectedTextRange?.start == endOfDocument + let isEmpty = lastReplacement?.isEmpty ?? true + if !isEmpty, isAtEnd, markedTextRange == nil { + notifyTextChanged?() + } else { + hideCursor = false + } + } + + // Reset the cursor to the end of the text field. + // This forces `caretRect(for position: UITextPosition)` to be called which will decide if we should show the cursor + // This exists because ` caretRect(for position: UITextPosition)` is not called after we apply an autocompletion. + private func forceResetCursor() { + selectedTextRange = nil + selectedTextRange = textRange(from: endOfDocument, to: endOfDocument) + } + + override func deleteBackward() { + lastReplacement = nil + hideCursor = false + if isSelectionActive { + removeCompletion() + forceResetCursor() + } else { + super.deleteBackward() + } + } + + override func touchesBegan(_ touches: Set, with event: UIEvent?) { + applyCompletion() + super.touchesBegan(touches, with: event) + } + +} diff --git a/mobile/ios/Client/Frontend/Widgets/ChevronView.swift b/mobile/ios/Client/Frontend/Widgets/ChevronView.swift new file mode 100644 index 0000000000..6892073159 --- /dev/null +++ b/mobile/ios/Client/Frontend/Widgets/ChevronView.swift @@ -0,0 +1,123 @@ +/* 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 + +enum ChevronDirection { + case left + case up + case right + case down +} + +enum ChevronStyle { + case angular + case rounded +} + +class ChevronView: UIView { + fileprivate let Padding: CGFloat = 2.5 + fileprivate var direction = ChevronDirection.right + fileprivate var lineCapStyle = CGLineCap.round + fileprivate var lineJoinStyle = CGLineJoin.round + + var lineWidth: CGFloat = 3.0 + + var style: ChevronStyle = .rounded { + didSet { + switch style { + case .rounded: + lineCapStyle = CGLineCap.round + lineJoinStyle = CGLineJoin.round + case .angular: + lineCapStyle = CGLineCap.butt + lineJoinStyle = CGLineJoin.miter + + } + } + } + + init(direction: ChevronDirection) { + super.init(frame: CGRect.zero) + + self.direction = direction + if UIApplication.shared.userInterfaceLayoutDirection == .rightToLeft { + if direction == .left { + self.direction = .right + } else if direction == .right { + self.direction = .left + } + } + self.backgroundColor = UIColor.clear + self.contentMode = UIViewContentMode.redraw + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func draw(_ rect: CGRect) { + super.draw(rect) + + let strokeLength = (rect.size.height / 2) - Padding + + let path: UIBezierPath + + switch direction { + case .left: + path = drawLeftChevronAt(CGPoint(x: rect.size.width - (strokeLength + Padding), y: strokeLength + Padding), strokeLength: strokeLength) + case .up: + path = drawUpChevronAt(CGPoint(x: (rect.size.width - Padding) - strokeLength, y: (strokeLength / 2) + Padding), strokeLength: strokeLength) + case .right: + path = drawRightChevronAt(CGPoint(x: rect.size.width - Padding, y: strokeLength + Padding), strokeLength: strokeLength) + case .down: + path = drawDownChevronAt(CGPoint(x: (rect.size.width - Padding) - strokeLength, y: (strokeLength * 1.5) + Padding), strokeLength: strokeLength) + } + + tintColor.set() + + // The line thickness needs to be proportional to the distance from the arrow head to the tips. Making it half seems about right. + path.lineCapStyle = lineCapStyle + path.lineJoinStyle = lineJoinStyle + path.lineWidth = lineWidth + path.stroke() + } + + fileprivate func drawUpChevronAt(_ origin: CGPoint, strokeLength: CGFloat) -> UIBezierPath { + return drawChevron(CGPoint(x: origin.x-strokeLength, y: origin.y+strokeLength), + head: CGPoint(x: origin.x, y: origin.y), + rightTip: CGPoint(x: origin.x+strokeLength, y: origin.y+strokeLength)) + } + + fileprivate func drawDownChevronAt(_ origin: CGPoint, strokeLength: CGFloat) -> UIBezierPath { + return drawChevron(CGPoint(x: origin.x-strokeLength, y: origin.y-strokeLength), + head: CGPoint(x: origin.x, y: origin.y), + rightTip: CGPoint(x: origin.x+strokeLength, y: origin.y-strokeLength)) + } + + fileprivate func drawLeftChevronAt(_ origin: CGPoint, strokeLength: CGFloat) -> UIBezierPath { + return drawChevron(CGPoint(x: origin.x+strokeLength, y: origin.y-strokeLength), + head: CGPoint(x: origin.x, y: origin.y), + rightTip: CGPoint(x: origin.x+strokeLength, y: origin.y+strokeLength)) + } + + fileprivate func drawRightChevronAt(_ origin: CGPoint, strokeLength: CGFloat) -> UIBezierPath { + return drawChevron(CGPoint(x: origin.x-strokeLength, y: origin.y+strokeLength), + head: CGPoint(x: origin.x, y: origin.y), + rightTip: CGPoint(x: origin.x-strokeLength, y: origin.y-strokeLength)) + } + + fileprivate func drawChevron(_ leftTip: CGPoint, head: CGPoint, rightTip: CGPoint) -> UIBezierPath { + let path = UIBezierPath() + + // Left tip + path.move(to: leftTip) + // Arrow head + path.addLine(to: head) + // Right tip + path.addLine(to: rightTip) + + return path + } +} diff --git a/mobile/ios/Client/Frontend/Widgets/ErrorToast.swift b/mobile/ios/Client/Frontend/Widgets/ErrorToast.swift new file mode 100644 index 0000000000..e3808aa216 --- /dev/null +++ b/mobile/ios/Client/Frontend/Widgets/ErrorToast.swift @@ -0,0 +1,55 @@ +/* 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 SnapKit + +private struct ErrorToastDefaultUX { + static let cornerRadius: CGFloat = 40 + static let fillColor = UIColor(red: 186/255, green: 32/255, blue: 36/255, alpha: 1) + static let margins = UIEdgeInsets(top: 10, left: 12, bottom: 10, right: 12) + static let textColor = UIColor.white +} + +class ErrorToast: UIView { + lazy var textLabel: UILabel = { + let label = UILabel() + label.textColor = ErrorToastDefaultUX.textColor + label.textAlignment = .center + label.numberOfLines = 0 + return label + }() + + var cornerRadius: CGFloat = ErrorToastDefaultUX.cornerRadius { + didSet { + setNeedsDisplay() + } + } + + var fillColor: UIColor = ErrorToastDefaultUX.fillColor { + didSet { + setNeedsDisplay() + } + } + + override init(frame: CGRect) { + super.init(frame: frame) + isOpaque = false + addSubview(textLabel) + textLabel.snp.makeConstraints { make in + make.edges.equalTo(self).inset(ErrorToastDefaultUX.margins) + } + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func draw(_ rect: CGRect) { + super.draw(rect) + fillColor.setFill() + let path = UIBezierPath(roundedRect: rect, cornerRadius: cornerRadius) + path.fill() + } +} diff --git a/mobile/ios/Client/Frontend/Widgets/GradientProgressBar.swift b/mobile/ios/Client/Frontend/Widgets/GradientProgressBar.swift new file mode 100644 index 0000000000..1f9cbe432f --- /dev/null +++ b/mobile/ios/Client/Frontend/Widgets/GradientProgressBar.swift @@ -0,0 +1,187 @@ +/* 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/. */ + +// ADAPTED FROM: +// +// GradientProgressBar.swift +// GradientProgressBar +// +// Created by Felix Mau on 01.03.17. +// Copyright © 2017 Felix Mau. All rights reserved. +// + +import Foundation +import UIKit + +open class GradientProgressBar: UIProgressView { + + private struct DefaultValues { + static let backgroundColor = UIColor.clear + static let animationDuration = 0.2 // CALayer default animation duration + } + + var gradientColors: [CGColor] = [] + // Alpha mask for visible part of gradient. + private var alphaMaskLayer: CALayer = CALayer() + + // Gradient layer. + open var gradientLayer: CAGradientLayer = CAGradientLayer() + + // Duration for "setProgress(animated: true)" + open var animationDuration = DefaultValues.animationDuration + + // Workaround to handle orientation change, as "layoutSubviews()" gets triggered each time + // the progress value is changed. + override open var bounds: CGRect { + didSet { + updateAlphaMaskLayerWidth() + } + } + + // Update layer mask on direct changes to progress value. + override open var progress: Float { + didSet { + updateAlphaMaskLayerWidth() + } + } + + func setGradientColors(startColor: UIColor, endColor: UIColor) { + gradientColors = [startColor, endColor, startColor, endColor, startColor, endColor, startColor].map { $0.cgColor } + gradientLayer.colors = gradientColors + } + + func commonInit() { + setupProgressViewColors() + setupAlphaMaskLayer() + setupGradientLayer() + + layer.insertSublayer(gradientLayer, at: 0) + updateAlphaMaskLayerWidth() + } + + override public init(frame: CGRect) { + gradientColors = [] + super.init(frame: frame) + commonInit() + } + + required public init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + commonInit() + } + + // MARK: - Setup UIProgressView + + private func setupProgressViewColors() { + backgroundColor = DefaultValues.backgroundColor + trackTintColor = .clear + progressTintColor = .clear + } + + // MARK: - Setup layers + + private func setupAlphaMaskLayer() { + alphaMaskLayer.frame = bounds + alphaMaskLayer.cornerRadius = 3 + + alphaMaskLayer.anchorPoint = CGPoint(x: 0, y: 0) + alphaMaskLayer.position = CGPoint(x: 0, y: 0) + + alphaMaskLayer.backgroundColor = UIColor.white.cgColor + } + + private func setupGradientLayer() { + // Apply "alphaMaskLayer" as a mask to the gradient layer in order to show only parts of the current "progress" + gradientLayer.mask = alphaMaskLayer + + gradientLayer.frame = CGRect(x: bounds.origin.x, y: bounds.origin.y, width: bounds.size.width * 2, height: bounds.size.height) + gradientLayer.colors = gradientColors + gradientLayer.locations = [0.0, 0.2, 0.4, 0.6, 0.8, 1.0, 1.0] + gradientLayer.startPoint = CGPoint(x: 0, y: 0) + gradientLayer.endPoint = CGPoint(x: 1, y: 0) + gradientLayer.drawsAsynchronously = true + } + + func hideProgressBar() { + guard progress == 1 else { + return + } + + CATransaction.begin() + let moveAnimation = CABasicAnimation(keyPath: "position") + moveAnimation.duration = DefaultValues.animationDuration + moveAnimation.fromValue = gradientLayer.position + moveAnimation.toValue = CGPoint(x: gradientLayer.frame.width, y: gradientLayer.position.y) + moveAnimation.fillMode = kCAFillModeForwards + moveAnimation.isRemovedOnCompletion = false + + CATransaction.setCompletionBlock { + self.resetProgressBar() + } + + gradientLayer.add(moveAnimation, forKey: "position") + + CATransaction.commit() + } + + func resetProgressBar() { + // Call on super instead so no animation layers are created + super.setProgress(0, animated: false) + isHidden = true // The URLBar will unhide the view before starting the next animation. + } + + override open func layoutSubviews() { + super.layoutSubviews() + self.gradientLayer.frame = CGRect(x: bounds.origin.x - 4, y: bounds.origin.y, width: bounds.size.width * 2, height: bounds.size.height) + } + + func animateGradient() { + let gradientChangeAnimation = CABasicAnimation(keyPath: "locations") + gradientChangeAnimation.duration = DefaultValues.animationDuration * 4 + gradientChangeAnimation.toValue = [0.0, 0.2, 0.4, 0.6, 0.8, 1.0, 1.0] + gradientChangeAnimation.fromValue = [0.0, 0.0, 0.0, 0.2, 0.4, 0.6, 0.8] + gradientChangeAnimation.fillMode = kCAFillModeForwards + gradientChangeAnimation.isRemovedOnCompletion = false + gradientChangeAnimation.repeatCount = .infinity + gradientLayer.add(gradientChangeAnimation, forKey: "colorChange") + } + + // MARK: - Update gradient + + open func updateAlphaMaskLayerWidth(animated: Bool = false) { + CATransaction.begin() + // Workaround for non animated progress change + // Source: https://stackoverflow.com/a/16381287/3532505 + CATransaction.setAnimationDuration(animated ? DefaultValues.animationDuration : 0.0) + alphaMaskLayer.frame = bounds.updateWidth(byPercentage: CGFloat(progress)) + if progress == 1 { + // Delay calling hide until the last animation has completed + CATransaction.setCompletionBlock({ + DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + DefaultValues.animationDuration, execute: { + self.hideProgressBar() + }) + }) + } + CATransaction.commit() + } + + override open func setProgress(_ progress: Float, animated: Bool) { + if progress < self.progress && self.progress != 1 { + return + } + // Setup animations + gradientLayer.removeAnimation(forKey: "position") + if gradientLayer.animation(forKey: "colorChange") == nil { + animateGradient() + } + super.setProgress(progress, animated: animated) + updateAlphaMaskLayerWidth(animated: animated) + } +} + +extension CGRect { + func updateWidth(byPercentage percentage: CGFloat) -> CGRect { + return CGRect(x: origin.x, y: origin.y, width: size.width * percentage, height: size.height) + } +} diff --git a/mobile/ios/Client/Frontend/Widgets/HistoryBackButton.swift b/mobile/ios/Client/Frontend/Widgets/HistoryBackButton.swift new file mode 100644 index 0000000000..867c0bf320 --- /dev/null +++ b/mobile/ios/Client/Frontend/Widgets/HistoryBackButton.swift @@ -0,0 +1,70 @@ +/* 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 SnapKit +import Shared + +private struct HistoryBackButtonUX { + static let HistoryHistoryBackButtonHeaderChevronInset: CGFloat = 10 + static let HistoryHistoryBackButtonHeaderChevronSize: CGFloat = 20 + static let HistoryHistoryBackButtonHeaderChevronLineWidth: CGFloat = 3.0 +} + +class HistoryBackButton: UIButton { + lazy var title: UILabel = { + let label = UILabel() + label.textColor = UIConstants.HighlightBlue + label.text = Strings.HistoryBackButtonTitle + return label + }() + + lazy var chevron: ChevronView = { + let chevron = ChevronView(direction: .left) + chevron.tintColor = UIConstants.HighlightBlue + chevron.lineWidth = HistoryBackButtonUX.HistoryHistoryBackButtonHeaderChevronLineWidth + return chevron + }() + + lazy var topBorder: UIView = self.createBorderView() + lazy var bottomBorder: UIView = self.createBorderView() + + override init(frame: CGRect) { + super.init(frame: frame) + isUserInteractionEnabled = true + + addSubview(topBorder) + addSubview(chevron) + addSubview(title) + + backgroundColor = UIColor.white + + chevron.snp.makeConstraints { make in + make.leading.equalTo(self).offset(HistoryBackButtonUX.HistoryHistoryBackButtonHeaderChevronInset) + make.centerY.equalTo(self) + make.size.equalTo(HistoryBackButtonUX.HistoryHistoryBackButtonHeaderChevronSize) + } + + title.snp.makeConstraints { make in + make.leading.equalTo(chevron.snp.trailing).offset(HistoryBackButtonUX.HistoryHistoryBackButtonHeaderChevronInset) + make.trailing.greaterThanOrEqualTo(self).offset(-HistoryBackButtonUX.HistoryHistoryBackButtonHeaderChevronInset) + make.centerY.equalTo(self) + } + + topBorder.snp.makeConstraints { make in + make.leading.trailing.equalTo(self) + make.top.equalTo(self).offset(-0.5) + make.height.equalTo(0.5) + } + } + + fileprivate func createBorderView() -> UIView { + let view = UIView() + view.backgroundColor = SiteTableViewControllerUX.HeaderBorderColor + return view + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} diff --git a/mobile/ios/Client/Frontend/Widgets/InnerStrokedView.swift b/mobile/ios/Client/Frontend/Widgets/InnerStrokedView.swift new file mode 100644 index 0000000000..aeb88b51ed --- /dev/null +++ b/mobile/ios/Client/Frontend/Widgets/InnerStrokedView.swift @@ -0,0 +1,49 @@ +/* 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 UIKit + +/// A transparent view with a rectangular border with rounded corners, stroked +/// with a semi-transparent white border. +class InnerStrokedView: UIView { + var color = UIColor.white.withAlphaComponent(0.2) { + didSet { + setNeedsDisplay() + } + } + + var strokeWidth: CGFloat = 1.0 { + didSet { + setNeedsDisplay() + } + } + + var cornerRadius: CGFloat = 4 { + didSet { + setNeedsDisplay() + } + } + + override init(frame: CGRect) { + super.init(frame: frame) + backgroundColor = UIColor.clear + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func draw(_ rect: CGRect) { + let halfWidth = strokeWidth / 2 as CGFloat + + let path = UIBezierPath(roundedRect: CGRect(x: halfWidth, + y: halfWidth, + width: rect.width - strokeWidth, + height: rect.height - strokeWidth), + cornerRadius: cornerRadius) + color.setStroke() + path.lineWidth = strokeWidth + path.stroke() + } +} diff --git a/mobile/ios/Client/Frontend/Widgets/InsetButton.swift b/mobile/ios/Client/Frontend/Widgets/InsetButton.swift new file mode 100644 index 0000000000..a567c92ebc --- /dev/null +++ b/mobile/ios/Client/Frontend/Widgets/InsetButton.swift @@ -0,0 +1,14 @@ +/* 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/. */ + +/** + * Button whose insets are included in its intrinsic size. + */ +class InsetButton: UIButton { + override var intrinsicContentSize: CGSize { + let size = super.intrinsicContentSize + return CGSize(width: size.width + titleEdgeInsets.left + titleEdgeInsets.right, + height: size.height + titleEdgeInsets.top + titleEdgeInsets.bottom) + } +} diff --git a/mobile/ios/Client/Frontend/Widgets/LoginTableViewCell.swift b/mobile/ios/Client/Frontend/Widgets/LoginTableViewCell.swift new file mode 100644 index 0000000000..9b29468e6e --- /dev/null +++ b/mobile/ios/Client/Frontend/Widgets/LoginTableViewCell.swift @@ -0,0 +1,387 @@ +/* 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 UIKit +import SnapKit +import Storage + +protocol LoginTableViewCellDelegate: class { + func didSelectOpenAndFillForCell(_ cell: LoginTableViewCell) + func shouldReturnAfterEditingDescription(_ cell: LoginTableViewCell) -> Bool + func infoItemForCell(_ cell: LoginTableViewCell) -> InfoItem? +} + +private struct LoginTableViewCellUX { + static let highlightedLabelFont = UIFont.systemFont(ofSize: 12) + static let highlightedLabelTextColor = UIConstants.SystemBlueColor + static let highlightedLabelEditingTextColor = UIConstants.TableViewHeaderTextColor + + static let descriptionLabelFont = UIFont.systemFont(ofSize: 16) + static let descriptionLabelTextColor = UIColor.black + + static let HorizontalMargin: CGFloat = 14 + static let IconImageSize: CGFloat = 34 + + static let indentWidth: CGFloat = 44 + static let IndentAnimationDuration: TimeInterval = 0.2 + + static let editingDescriptionIndent: CGFloat = IconImageSize + HorizontalMargin +} + +enum LoginTableViewCellStyle { + case iconAndBothLabels + case noIconAndBothLabels + case iconAndDescriptionLabel +} + +class LoginTableViewCell: UITableViewCell { + + fileprivate let labelContainer = UIView() + + weak var delegate: LoginTableViewCellDelegate? + + // In order for context menu handling, this is required + override var canBecomeFirstResponder: Bool { + return true + } + + override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool { + guard let item = delegate?.infoItemForCell(self) else { + return false + } + + // Menu actions for password + if item == .passwordItem { + let showRevealOption = self.descriptionLabel.isSecureTextEntry ? (action == MenuHelper.SelectorReveal) : (action == MenuHelper.SelectorHide) + return action == MenuHelper.SelectorCopy || showRevealOption + } + + // Menu actions for Website + if item == .websiteItem { + return action == MenuHelper.SelectorCopy || action == MenuHelper.SelectorOpenAndFill + } + + // Menu actions for Username + if item == .usernameItem { + return action == MenuHelper.SelectorCopy + } + + return false + } + + lazy var descriptionLabel: UITextField = { + let label = UITextField() + label.font = LoginTableViewCellUX.descriptionLabelFont + label.textColor = LoginTableViewCellUX.descriptionLabelTextColor + label.textAlignment = .left + label.backgroundColor = UIColor.white + label.isUserInteractionEnabled = false + label.autocapitalizationType = .none + label.autocorrectionType = .no + label.accessibilityElementsHidden = true + label.adjustsFontSizeToFitWidth = false + label.delegate = self + label.isAccessibilityElement = true + return label + }() + + // Exposing this label as internal/public causes the Xcode 7.2.1 compiler optimizer to + // produce a EX_BAD_ACCESS error when dequeuing the cell. For now, this label is made private + // and the text property is exposed using a get/set property below. + fileprivate lazy var highlightedLabel: UILabel = { + let label = UILabel() + label.font = LoginTableViewCellUX.highlightedLabelFont + label.textColor = LoginTableViewCellUX.highlightedLabelTextColor + label.textAlignment = .left + label.backgroundColor = UIColor.white + label.numberOfLines = 1 + return label + }() + + fileprivate lazy var iconImageView: UIImageView = { + let imageView = UIImageView() + imageView.backgroundColor = UIColor.white + imageView.contentMode = .scaleAspectFit + return imageView + }() + + fileprivate var showingIndent: Bool = false + + fileprivate var customIndentView = UIView() + + fileprivate var customCheckmarkIcon = UIImageView(image: UIImage(named: "loginUnselected")) + + /// Override the default accessibility label since it won't include the description by default + /// since it's a UITextField acting as a label. + override var accessibilityLabel: String? { + get { + if descriptionLabel.isSecureTextEntry { + return highlightedLabel.text ?? "" + } else { + return "\(highlightedLabel.text ?? ""), \(descriptionLabel.text ?? "")" + } + } + set { + // Ignore sets + } + } + + var style: LoginTableViewCellStyle = .iconAndBothLabels { + didSet { + if style != oldValue { + configureLayoutForStyle(style) + } + } + } + + var descriptionTextSize: CGSize? { + guard let descriptionText = descriptionLabel.text else { + return nil + } + + let attributes = [ + NSFontAttributeName: LoginTableViewCellUX.descriptionLabelFont + ] + + return descriptionText.size(attributes: attributes) + } + + var displayDescriptionAsPassword: Bool = false { + didSet { + descriptionLabel.isSecureTextEntry = displayDescriptionAsPassword + } + } + + var editingDescription: Bool = false { + didSet { + if editingDescription != oldValue { + descriptionLabel.isUserInteractionEnabled = editingDescription + + highlightedLabel.textColor = editingDescription ? + LoginTableViewCellUX.highlightedLabelEditingTextColor : LoginTableViewCellUX.highlightedLabelTextColor + + // Trigger a layout configuration if we changed to editing/not editing the description. + configureLayoutForStyle(self.style) + } + } + } + + var highlightedLabelTitle: String? { + get { + return highlightedLabel.text + } + set(newTitle) { + highlightedLabel.text = newTitle + } + } + + override init(style: UITableViewCellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + + indentationWidth = 0 + selectionStyle = .none + + contentView.backgroundColor = UIColor.white + labelContainer.backgroundColor = UIColor.white + + labelContainer.addSubview(highlightedLabel) + labelContainer.addSubview(descriptionLabel) + + contentView.addSubview(iconImageView) + contentView.addSubview(labelContainer) + + customIndentView.addSubview(customCheckmarkIcon) + addSubview(customIndentView) + + configureLayoutForStyle(self.style) + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func prepareForReuse() { + super.prepareForReuse() + delegate = nil + descriptionLabel.isSecureTextEntry = false + descriptionLabel.keyboardType = .default + descriptionLabel.returnKeyType = .default + descriptionLabel.isUserInteractionEnabled = false + } + + override func layoutSubviews() { + super.layoutSubviews() + + // Adjust indent frame + var indentFrame = CGRect( + origin: CGPoint.zero, + size: CGSize(width: LoginTableViewCellUX.indentWidth, height: frame.height)) + + if !showingIndent { + indentFrame.origin.x = -LoginTableViewCellUX.indentWidth + } + + customIndentView.frame = indentFrame + customCheckmarkIcon.frame.center = CGPoint(x: indentFrame.width / 2, y: indentFrame.height / 2) + + // Adjust content view frame based on indent + var contentFrame = self.contentView.frame + contentFrame.origin.x += showingIndent ? LoginTableViewCellUX.indentWidth : 0 + contentView.frame = contentFrame + } + + fileprivate func configureLayoutForStyle(_ style: LoginTableViewCellStyle) { + switch style { + case .iconAndBothLabels: + iconImageView.snp.remakeConstraints { make in + make.centerY.equalTo(contentView) + make.left.equalTo(contentView).offset(LoginTableViewCellUX.HorizontalMargin) + make.height.width.equalTo(LoginTableViewCellUX.IconImageSize) + } + + labelContainer.snp.remakeConstraints { make in + make.centerY.equalTo(contentView) + make.right.equalTo(contentView).offset(-LoginTableViewCellUX.HorizontalMargin) + make.left.equalTo(iconImageView.snp.right).offset(LoginTableViewCellUX.HorizontalMargin) + } + + highlightedLabel.snp.remakeConstraints { make in + make.left.top.equalTo(labelContainer) + make.bottom.equalTo(descriptionLabel.snp.top) + make.width.equalTo(labelContainer) + } + + descriptionLabel.snp.remakeConstraints { make in + make.left.bottom.equalTo(labelContainer) + make.top.equalTo(highlightedLabel.snp.bottom) + make.width.equalTo(labelContainer) + } + case .iconAndDescriptionLabel: + iconImageView.snp.remakeConstraints { make in + make.centerY.equalTo(contentView) + make.left.equalTo(contentView).offset(LoginTableViewCellUX.HorizontalMargin) + make.height.width.equalTo(LoginTableViewCellUX.IconImageSize) + } + + labelContainer.snp.remakeConstraints { make in + make.centerY.equalTo(contentView) + make.right.equalTo(contentView).offset(-LoginTableViewCellUX.HorizontalMargin) + make.left.equalTo(iconImageView.snp.right).offset(LoginTableViewCellUX.HorizontalMargin) + } + + highlightedLabel.snp.remakeConstraints { make in + make.height.width.equalTo(0) + } + + descriptionLabel.snp.remakeConstraints { make in + make.top.left.bottom.equalTo(labelContainer) + make.width.equalTo(labelContainer) + } + case .noIconAndBothLabels: + // Currently we only support modifying the description for this layout which is why + // we factor in the editingOffset when calculating the constraints. + let editingOffset = editingDescription ? LoginTableViewCellUX.editingDescriptionIndent : 0 + + iconImageView.snp.remakeConstraints { make in + make.centerY.equalTo(contentView) + make.left.equalTo(contentView).offset(LoginTableViewCellUX.HorizontalMargin) + make.height.width.equalTo(0) + } + + labelContainer.snp.remakeConstraints { make in + make.centerY.equalTo(contentView) + make.right.equalTo(contentView).offset(-LoginTableViewCellUX.HorizontalMargin) + make.left.equalTo(iconImageView.snp.right).offset(editingOffset) + } + + highlightedLabel.snp.remakeConstraints { make in + make.left.top.equalTo(labelContainer) + make.bottom.equalTo(descriptionLabel.snp.top) + make.width.equalTo(labelContainer) + } + + descriptionLabel.snp.remakeConstraints { make in + make.left.bottom.equalTo(labelContainer) + make.top.equalTo(highlightedLabel.snp.bottom) + make.width.equalTo(labelContainer) + } + } + + setNeedsUpdateConstraints() + } + + override func setEditing(_ editing: Bool, animated: Bool) { + showingIndent = editing + + let adjustConstraints = { [unowned self] in + + // Shift over content view + var contentFrame = self.contentView.frame + contentFrame.origin.x += editing ? LoginTableViewCellUX.indentWidth : -LoginTableViewCellUX.indentWidth + self.contentView.frame = contentFrame + + // Shift over custom indent view + var indentFrame = self.customIndentView.frame + indentFrame.origin.x += editing ? LoginTableViewCellUX.indentWidth : -LoginTableViewCellUX.indentWidth + self.customIndentView.frame = indentFrame + } + + animated ? UIView.animate(withDuration: LoginTableViewCellUX.IndentAnimationDuration, animations: adjustConstraints) : adjustConstraints() + } + + override func setSelected(_ selected: Bool, animated: Bool) { + super.setSelected(selected, animated: animated) + customCheckmarkIcon.image = UIImage(named: selected ? "loginSelected" : "loginUnselected") + } +} + +// MARK: - Menu Selectors +extension LoginTableViewCell: MenuHelperInterface { + + func menuHelperReveal() { + displayDescriptionAsPassword = false + } + + func menuHelperSecure() { + displayDescriptionAsPassword = true + } + + func menuHelperCopy() { + // Copy description text to clipboard + UIPasteboard.general.string = descriptionLabel.text + } + + func menuHelperOpenAndFill() { + delegate?.didSelectOpenAndFillForCell(self) + } +} + +// MARK: - Cell Decorators +extension LoginTableViewCell { + func updateCellWithLogin(_ login: LoginData) { + descriptionLabel.text = login.hostname + highlightedLabel.text = login.username + iconImageView.image = UIImage(named: "faviconFox") + } +} + +extension LoginTableViewCell: UITextFieldDelegate { + func textFieldShouldReturn(_ textField: UITextField) -> Bool { + return self.delegate?.shouldReturnAfterEditingDescription(self) ?? true + } + + func textFieldShouldBeginEditing(_ textField: UITextField) -> Bool { + if descriptionLabel.isSecureTextEntry { + displayDescriptionAsPassword = false + } + return true + } + + func textFieldDidEndEditing(_ textField: UITextField) { + if descriptionLabel.isSecureTextEntry { + displayDescriptionAsPassword = true + } + } +} diff --git a/mobile/ios/Client/Frontend/Widgets/PhotonActionSheet.swift b/mobile/ios/Client/Frontend/Widgets/PhotonActionSheet.swift new file mode 100644 index 0000000000..cfcf610bcf --- /dev/null +++ b/mobile/ios/Client/Frontend/Widgets/PhotonActionSheet.swift @@ -0,0 +1,496 @@ +/* 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 Storage +import SnapKit +import Shared + +private struct PhotonActionSheetUX { + static let MaxWidth: CGFloat = 414 + static let Padding: CGFloat = 10 + static let SectionVerticalPadding: CGFloat = 13 + static let HeaderHeight: CGFloat = 80 + static let RowHeight: CGFloat = 44 + static let LabelColor = UIAccessibilityDarkerSystemColorsEnabled() ? UIColor.black : UIColor(rgb: 0x353535) + static let DescriptionLabelColor = UIColor(rgb: 0x919191) + static let PlaceholderImage = UIImage(named: "defaultTopSiteIcon") + static let BorderWidth: CGFloat = 0.5 + static let BorderColor = UIColor(white: 0, alpha: 0.1) + static let CornerRadius: CGFloat = 10 + static let SiteImageViewSize = 52 + static let IconSize = CGSize(width: 24, height: 24) + static let HeaderName = "PhotonActionSheetHeaderView" + static let CellName = "PhotonActionSheetCell" + static let CancelButtonHeight: CGFloat = 56 + static let TablePadding: CGFloat = 6 +} + +public struct PhotonActionSheetItem { + public fileprivate(set) var title: String + public fileprivate(set) var iconString: String + public fileprivate(set) var isEnabled: Bool // Used by toggles like nightmode to switch tint color + public fileprivate(set) var handler: ((PhotonActionSheetItem) -> Void)? + + init(title: String, iconString: String, isEnabled: Bool = false, handler: ((PhotonActionSheetItem) -> Void)?) { + self.title = title + self.iconString = iconString + self.isEnabled = isEnabled + self.handler = handler + } +} + +private enum PresentationStyle { + case centered // used in the home panels + case bottom // used to display the menu +} + +class PhotonActionSheet: UIViewController, UITableViewDelegate, UITableViewDataSource, UIGestureRecognizerDelegate { + fileprivate(set) var actions: [[PhotonActionSheetItem]] + + private var site: Site? + private let style: PresentationStyle + private lazy var showCancelButton: Bool = { + return self.style == .bottom && self.modalPresentationStyle != .popover + }() + var tableView = UITableView(frame: CGRect.zero, style: .grouped) + private var tintColor = UIColor(rgb: 0x272727) + private var outerScrollView = UIScrollView() + + lazy var tapRecognizer: UITapGestureRecognizer = { + let tapRecognizer = UITapGestureRecognizer() + tapRecognizer.addTarget(self, action: #selector(PhotonActionSheet.dismiss(_:))) + tapRecognizer.numberOfTapsRequired = 1 + tapRecognizer.cancelsTouchesInView = false + tapRecognizer.delegate = self + return tapRecognizer + }() + + lazy var cancelButton: UIButton = { + let button = UIButton() + button.setTitle(Strings.CancelButtonTitle, for: .normal) + button.backgroundColor = UIConstants.AppBackgroundColor + button.setTitleColor(UIConstants.SystemBlueColor, for: .normal) + button.layer.cornerRadius = PhotonActionSheetUX.CornerRadius + button.titleLabel?.font = DynamicFontHelper.defaultHelper.DeviceFontExtraLargeBold + button.addTarget(self, action: #selector(PhotonActionSheet.dismiss(_:)), for: .touchUpInside) + button.accessibilityIdentifier = "PhotonMenu.cancel" + return button + }() + + init(site: Site, actions: [PhotonActionSheetItem]) { + self.site = site + self.actions = [actions] + self.style = .centered + super.init(nibName: nil, bundle: nil) + } + + init(actions: [[PhotonActionSheetItem]]) { + self.actions = actions + self.style = .bottom + super.init(nibName: nil, bundle: nil) + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + var photonTransitionDelegate: UIViewControllerTransitioningDelegate? { + didSet { + self.transitioningDelegate = photonTransitionDelegate + } + } + + override func viewDidLoad() { + super.viewDidLoad() + + if style == .centered { + applyBackgroundBlur() + self.tintColor = UIConstants.SystemBlueColor + } + view.addGestureRecognizer(tapRecognizer) + view.addSubview(tableView) + + view.accessibilityIdentifier = "Action Sheet" + tableView.bounces = false + tableView.delegate = self + tableView.dataSource = self + tableView.sectionFooterHeight = 0 + tableView.keyboardDismissMode = UIScrollViewKeyboardDismissMode.onDrag + tableView.register(PhotonActionSheetCell.self, forCellReuseIdentifier: PhotonActionSheetUX.CellName) + tableView.register(PhotonActionSheetHeaderView.self, forHeaderFooterViewReuseIdentifier: PhotonActionSheetUX.HeaderName) + tableView.register(PhotonActionSheetSeparator.self, forHeaderFooterViewReuseIdentifier: "SeparatorSectionHeader") + tableView.isScrollEnabled = true + tableView.showsVerticalScrollIndicator = false + tableView.layer.cornerRadius = PhotonActionSheetUX.CornerRadius + tableView.separatorStyle = .none + tableView.cellLayoutMarginsFollowReadableWidth = false + tableView.accessibilityIdentifier = "Context Menu" + let footer = UIView(frame: CGRect(origin: CGPoint.zero, size: CGSize(width: tableView.frame.width, height: PhotonActionSheetUX.TablePadding))) + tableView.tableFooterView = footer + tableView.tableHeaderView = footer.clone() + + // In a popover the popover provides the blur background + // Not using a background color allows the view to style correctly with the popover arrow + if self.popoverPresentationController == nil { + tableView.backgroundColor = UIConstants.AppBackgroundColor.withAlphaComponent(0.7) + let blurEffect = UIBlurEffect(style: .light) + let blurEffectView = UIVisualEffectView(effect: blurEffect) + tableView.backgroundView = blurEffectView + } else { + tableView.backgroundColor = .clear + } + + let width = min(self.view.frame.size.width, PhotonActionSheetUX.MaxWidth) - (PhotonActionSheetUX.Padding * 2) + let height = actionSheetHeight() + + if self.modalPresentationStyle == .popover { + self.preferredContentSize = CGSize(width: width, height: height) + } + + if self.showCancelButton { + self.view.addSubview(cancelButton) + cancelButton.snp.makeConstraints { make in + make.centerX.equalTo(self.view.snp.centerX) + make.width.equalTo(width) + make.height.equalTo(PhotonActionSheetUX.CancelButtonHeight) + if #available(iOS 11, *) { + let bottomPad: CGFloat + if let window = UIApplication.shared.keyWindow, window.safeAreaInsets.bottom != 0 { + // for iPhone X and similar + bottomPad = 0 + } else { + bottomPad = PhotonActionSheetUX.Padding + } + make.bottom.equalTo(self.view.safeAreaLayoutGuide.snp.bottom).offset(-bottomPad) + } else { + make.bottom.equalTo(self.view.snp.bottom).offset(-PhotonActionSheetUX.Padding) + } + } + } + + if style == .bottom && self.modalPresentationStyle == .popover { + // We are showing the menu in a popOver + self.actions = actions.map({ $0.reversed() }).reversed() + tableView.frame = CGRect(origin: CGPoint.zero, size: self.preferredContentSize) + return + } + + tableView.snp.makeConstraints { make in + make.centerX.equalTo(self.view.snp.centerX) + switch style { + case .bottom: + make.bottom.equalTo(cancelButton.snp.top).offset(-PhotonActionSheetUX.Padding) + case .centered: + make.centerY.equalTo(self.view.snp.centerY) + } + make.width.equalTo(width) + make.height.equalTo(min(height, view.bounds.height - PhotonActionSheetUX.CancelButtonHeight - (PhotonActionSheetUX.Padding * 6))) + } + } + + private func applyBackgroundBlur() { + let appDelegate = UIApplication.shared.delegate as! AppDelegate + if let screenshot = appDelegate.window?.screenshot() { + let blurredImage = screenshot.applyBlur(withRadius: 5, + blurType: BOXFILTER, + tintColor: UIColor.black.withAlphaComponent(0.2), + saturationDeltaFactor: 1.8, + maskImage: nil) + let imageView = UIImageView(image: blurredImage) + view.addSubview(imageView) + } + } + + fileprivate func actionSheetHeight() -> CGFloat { + let count = actions.reduce(0) { $1.count + $0 } + let headerHeight = (style == .centered) ? PhotonActionSheetUX.HeaderHeight : 0 + let separatorHeight = actions.count > 1 ? (actions.count - 1) * Int(PhotonActionSheetUX.SectionVerticalPadding) : 0 + return CGFloat(separatorHeight) + headerHeight + ( PhotonActionSheetUX.TablePadding * 2) + CGFloat(count) * PhotonActionSheetUX.RowHeight + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + } + + func dismiss(_ gestureRecognizer: UIGestureRecognizer?) { + self.dismiss(animated: true, completion: nil) + } + + deinit { + tableView.dataSource = nil + tableView.delegate = nil + } + + override func updateViewConstraints() { + if !self.showCancelButton { + tableView.frame = CGRect(origin: CGPoint.zero, size: self.preferredContentSize) + } + super.updateViewConstraints() + } + + override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { + super.traitCollectionDidChange(previousTraitCollection) + + if self.traitCollection.verticalSizeClass != previousTraitCollection?.verticalSizeClass + || self.traitCollection.horizontalSizeClass != previousTraitCollection?.horizontalSizeClass { + updateViewConstraints() + } + } + + func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool { + if tableView.frame.contains(touch.location(in: self.view)) { + return false + } + return true + } + + func numberOfSections(in tableView: UITableView) -> Int { + return actions.count + } + + func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return actions[section].count + } + + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + let action = actions[indexPath.section][indexPath.row] + guard let handler = action.handler else { + self.dismiss(nil) + return + } + self.dismiss(nil) + return handler(action) + } + + func tableView(_ tableView: UITableView, hasFullWidthSeparatorForRowAtIndexPath indexPath: IndexPath) -> Bool { + return false + } + + func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { + return PhotonActionSheetUX.RowHeight + } + + func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { + // If we have multiple sections show a separator for each one except the first. + if section > 0 { + return PhotonActionSheetUX.SectionVerticalPadding + } + return self.site != nil ? PhotonActionSheetUX.HeaderHeight : 0 + } + + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let cell = tableView.dequeueReusableCell(withIdentifier: PhotonActionSheetUX.CellName, for: indexPath) as! PhotonActionSheetCell + let action = actions[indexPath.section][indexPath.row] + + cell.tintColor = action.isEnabled ? UIConstants.SystemBlueColor : self.tintColor + cell.configureCell(action.title, imageString: action.iconString) + return cell + } + + func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { + // If we have multiple sections show a separator for each one except the first. + if section > 0 { + return tableView.dequeueReusableHeaderFooterView(withIdentifier: "SeparatorSectionHeader") + } + guard let site = site else { + return nil + } + let header = tableView.dequeueReusableHeaderFooterView(withIdentifier: PhotonActionSheetUX.HeaderName) as! PhotonActionSheetHeaderView + header.tintColor = self.tintColor + header.configureWithSite(site) + return header + } +} + +private class PhotonActionSheetHeaderView: UITableViewHeaderFooterView { + static let Padding: CGFloat = 12 + static let VerticalPadding: CGFloat = 2 + + lazy var titleLabel: UILabel = { + let titleLabel = UILabel() + titleLabel.font = DynamicFontHelper.defaultHelper.MediumSizeBoldFontAS + titleLabel.textAlignment = .left + titleLabel.numberOfLines = 2 + return titleLabel + }() + + lazy var descriptionLabel: UILabel = { + let titleLabel = UILabel() + titleLabel.font = DynamicFontHelper.defaultHelper.MediumSizeRegularWeightAS + titleLabel.textAlignment = .left + titleLabel.numberOfLines = 1 + return titleLabel + }() + + lazy var siteImageView: UIImageView = { + let siteImageView = UIImageView() + siteImageView.contentMode = UIViewContentMode.center + siteImageView.clipsToBounds = true + siteImageView.layer.cornerRadius = PhotonActionSheetUX.CornerRadius + siteImageView.layer.borderColor = PhotonActionSheetUX.BorderColor.cgColor + siteImageView.layer.borderWidth = PhotonActionSheetUX.BorderWidth + return siteImageView + }() + + override init(reuseIdentifier: String?) { + super.init(reuseIdentifier: reuseIdentifier) + + self.backgroundView = UIView() + self.backgroundView?.backgroundColor = .clear + contentView.addSubview(siteImageView) + + siteImageView.snp.remakeConstraints { make in + make.centerY.equalTo(contentView) + make.leading.equalTo(contentView).offset(PhotonActionSheetHeaderView.Padding) + make.size.equalTo(PhotonActionSheetUX.SiteImageViewSize) + } + + let stackView = UIStackView(arrangedSubviews: [titleLabel, descriptionLabel]) + stackView.spacing = PhotonActionSheetHeaderView.VerticalPadding + stackView.alignment = .leading + stackView.axis = .vertical + + contentView.addSubview(stackView) + + stackView.snp.makeConstraints { make in + make.leading.equalTo(siteImageView.snp.trailing).offset(PhotonActionSheetHeaderView.Padding) + make.trailing.equalTo(contentView).inset(PhotonActionSheetHeaderView.Padding) + make.centerY.equalTo(siteImageView.snp.centerY) + } + + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func prepareForReuse() { + self.siteImageView.image = nil + self.siteImageView.backgroundColor = UIColor.clear + } + + func configureWithSite(_ site: Site) { + self.siteImageView.setFavicon(forSite: site) { (color, url) in + self.siteImageView.backgroundColor = color + self.siteImageView.image = self.siteImageView.image?.createScaled(PhotonActionSheetUX.IconSize) + } + self.titleLabel.text = site.title.characters.count <= 1 ? site.url : site.title + self.descriptionLabel.text = site.tileURL.baseDomain + } +} + +private struct PhotonActionSheetCellUX { + static let LabelColor = UIConstants.SystemBlueColor + static let BorderWidth: CGFloat = CGFloat(0.5) + static let CellSideOffset = 20 + static let TitleLabelOffset = 10 + static let CellTopBottomOffset = 12 + static let StatusIconSize = 24 + static let SelectedOverlayColor = UIColor(white: 0.0, alpha: 0.25) + static let CornerRadius: CGFloat = 3 +} + +private class PhotonActionSheetSeparator: UITableViewHeaderFooterView { + + let separatorLineView = UIView() + + override init(reuseIdentifier: String?) { + super.init(reuseIdentifier: reuseIdentifier) + self.backgroundView = UIView() + self.backgroundView?.backgroundColor = .clear + separatorLineView.backgroundColor = UIColor.lightGray + self.contentView.addSubview(separatorLineView) + separatorLineView.snp.makeConstraints { make in + make.leading.trailing.equalTo(self) + make.centerY.equalTo(self) + make.height.equalTo(0.5) + } + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +private class PhotonActionSheetCell: UITableViewCell { + lazy var titleLabel: UILabel = { + let titleLabel = UILabel() + titleLabel.font = DynamicFontHelper.defaultHelper.LargeSizeRegularWeightAS + titleLabel.minimumScaleFactor = 0.75 // Scale the font if we run out of space + titleLabel.textColor = PhotonActionSheetCellUX.LabelColor + titleLabel.textAlignment = .left + titleLabel.numberOfLines = 1 + titleLabel.adjustsFontSizeToFitWidth = true + return titleLabel + }() + + lazy var statusIcon: UIImageView = { + let siteImageView = UIImageView() + siteImageView.contentMode = UIViewContentMode.scaleAspectFit + siteImageView.clipsToBounds = true + siteImageView.layer.cornerRadius = PhotonActionSheetCellUX.CornerRadius + return siteImageView + }() + + lazy var selectedOverlay: UIView = { + let selectedOverlay = UIView() + selectedOverlay.backgroundColor = PhotonActionSheetCellUX.SelectedOverlayColor + selectedOverlay.isHidden = true + return selectedOverlay + }() + + override var isSelected: Bool { + didSet { + self.selectedOverlay.isHidden = !isSelected + } + } + + override func prepareForReuse() { + self.statusIcon.image = nil + } + + override init(style: UITableViewCellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + + isAccessibilityElement = true + + contentView.addSubview(selectedOverlay) + contentView.addSubview(titleLabel) + contentView.addSubview(statusIcon) + backgroundColor = .clear + + selectedOverlay.snp.makeConstraints { make in + make.edges.equalTo(contentView) + } + + titleLabel.snp.makeConstraints { make in + make.leading.equalTo(statusIcon.snp.trailing).offset(16) + make.trailing.equalTo(contentView) + make.centerY.equalTo(contentView) + } + + statusIcon.snp.makeConstraints { make in + make.size.equalTo(PhotonActionSheetCellUX.StatusIconSize) + make.leading.equalTo(contentView).offset(16) + make.centerY.equalTo(contentView) + } + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func configureCell(_ label: String, imageString: String) { + titleLabel.text = label + titleLabel.textColor = self.tintColor + accessibilityIdentifier = imageString + accessibilityLabel = label + if let image = UIImage(named: imageString)?.withRenderingMode(.alwaysTemplate) { + statusIcon.image = image + statusIcon.tintColor = self.tintColor + } + } +} + diff --git a/mobile/ios/Client/Frontend/Widgets/PhotonActionSheetAnimator.swift b/mobile/ios/Client/Frontend/Widgets/PhotonActionSheetAnimator.swift new file mode 100644 index 0000000000..10e730e0c5 --- /dev/null +++ b/mobile/ios/Client/Frontend/Widgets/PhotonActionSheetAnimator.swift @@ -0,0 +1,79 @@ +/* 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 UIKit + +class PhotonActionSheetAnimator: NSObject, UIViewControllerAnimatedTransitioning { + + var presenting: Bool = false + let animationDuration = 0.4 + + lazy var shadow: UIView = { + let shadow = UIView() + shadow.backgroundColor = UIColor(white: 0, alpha: 0.5) + return shadow + }() + + func animateTransition(using transitionContext: UIViewControllerContextTransitioning) { + let screens = (from: transitionContext.viewController(forKey: UITransitionContextViewControllerKey.from)!, to: transitionContext.viewController(forKey: UITransitionContextViewControllerKey.to)!) + + guard let actionSheet = (self.presenting ? screens.to : screens.from) as? PhotonActionSheet else { + return + } + + let bottomViewController = (self.presenting ? screens.from : screens.to) as UIViewController + animateWitVC(actionSheet, presentingVC: bottomViewController, transitionContext: transitionContext) + } + + func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval { + return animationDuration + } +} + +extension PhotonActionSheetAnimator: UIViewControllerTransitioningDelegate { + func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? { + self.presenting = true + return self + } + + func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? { + self.presenting = false + return self + } +} + +extension PhotonActionSheetAnimator { + fileprivate func animateWitVC(_ actionSheet: PhotonActionSheet, presentingVC viewController: UIViewController, transitionContext: UIViewControllerContextTransitioning) { + let containerView = transitionContext.containerView + + if presenting { + shadow.frame = containerView.bounds + containerView.addSubview(shadow) + actionSheet.view.frame = CGRect(origin: CGPoint(x: 0, y: containerView.frame.size.height), size: containerView.frame.size) + self.shadow.alpha = 0 + containerView.addSubview(actionSheet.view) + actionSheet.view.layoutIfNeeded() + + UIView.animate(withDuration: transitionDuration(using: transitionContext), delay: 0, usingSpringWithDamping: 0.8, initialSpringVelocity: 0.3, options: [], animations: { () -> Void in + self.shadow.alpha = 1 + actionSheet.view.frame = containerView.bounds + actionSheet.view.layoutIfNeeded() + }, completion: { (completed) -> Void in + transitionContext.completeTransition(completed) + }) + + } else { + UIView.animate(withDuration: transitionDuration(using: transitionContext), delay: 0, usingSpringWithDamping: 1.2, initialSpringVelocity: 0.0, options: [], animations: { () -> Void in + self.shadow.alpha = 0 + actionSheet.view.frame = CGRect(origin: CGPoint(x: 0, y: containerView.frame.size.height), size: containerView.frame.size) + actionSheet.view.layoutIfNeeded() + }, completion: { (completed) -> Void in + actionSheet.view.removeFromSuperview() + self.shadow.removeFromSuperview() + transitionContext.completeTransition(completed) + }) + } + } +} + diff --git a/mobile/ios/Client/Frontend/Widgets/PhotonActionSheetProtocol.swift b/mobile/ios/Client/Frontend/Widgets/PhotonActionSheetProtocol.swift new file mode 100644 index 0000000000..7ffaa6f609 --- /dev/null +++ b/mobile/ios/Client/Frontend/Widgets/PhotonActionSheetProtocol.swift @@ -0,0 +1,232 @@ +/* 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 + +protocol PhotonActionSheetProtocol { + var tabManager: TabManager { get } + var profile: Profile { get } +} + +private let log = Logger.browserLogger + +extension PhotonActionSheetProtocol { + typealias PresentableVC = UIViewController & UIPopoverPresentationControllerDelegate + typealias MenuAction = () -> Void + typealias IsPrivateTab = Bool + typealias URLOpenAction = (URL?, IsPrivateTab) -> Void + + func presentSheetWith(actions: [[PhotonActionSheetItem]], on viewController: PresentableVC, from view: UIView, supressPopover: Bool = false) { + let sheet = PhotonActionSheet(actions: actions) + sheet.modalPresentationStyle = (UIDevice.current.userInterfaceIdiom == .pad && !supressPopover) ? .popover : .overCurrentContext + sheet.photonTransitionDelegate = PhotonActionSheetAnimator() + + if let popoverVC = sheet.popoverPresentationController, sheet.modalPresentationStyle == .popover { + popoverVC.delegate = viewController + popoverVC.sourceView = view + popoverVC.sourceRect = CGRect(x: view.frame.width/2, y: view.frame.size.height * 0.75, width: 1, height: 1) + popoverVC.permittedArrowDirections = UIPopoverArrowDirection.up + popoverVC.backgroundColor = UIConstants.AppBackgroundColor.withAlphaComponent(0.7) + } + viewController.present(sheet, animated: true, completion: nil) + } + + //Returns a list of actions which is used to build a menu + //OpenURL is a closure that can open a given URL in some view controller. It is up to the class using the menu to know how to open it + func getHomePanelActions() -> [PhotonActionSheetItem] { + guard let tab = self.tabManager.selectedTab else { return [] } + + let openTopSites = PhotonActionSheetItem(title: Strings.AppMenuTopSitesTitleString, iconString: "menu-panel-TopSites") { action in + tab.loadRequest(PrivilegedRequest(url: HomePanelType.topSites.localhostURL) as URLRequest) + } + + let openBookmarks = PhotonActionSheetItem(title: Strings.AppMenuBookmarksTitleString, iconString: "menu-panel-Bookmarks") { action in + tab.loadRequest(PrivilegedRequest(url: HomePanelType.bookmarks.localhostURL) as URLRequest) + UnifiedTelemetry.recordEvent(category: .action, method: .view, object: .bookmarksPanel, value: .appMenu) + } + + let openHistory = PhotonActionSheetItem(title: Strings.AppMenuHistoryTitleString, iconString: "menu-panel-History") { action in + tab.loadRequest(PrivilegedRequest(url: HomePanelType.history.localhostURL) as URLRequest) + } + + let openReadingList = PhotonActionSheetItem(title: Strings.AppMenuReadingListTitleString, iconString: "menu-panel-ReadingList") { action in + tab.loadRequest(PrivilegedRequest(url: HomePanelType.readingList.localhostURL) as URLRequest) + } + + let openHomePage = PhotonActionSheetItem(title: Strings.AppMenuOpenHomePageTitleString, iconString: "menu-Home") { _ in + HomePageHelper(prefs: self.profile.prefs).openHomePage(tab) + } + + var actions = [openTopSites, openBookmarks, openReadingList, openHistory] + if HomePageHelper(prefs: self.profile.prefs).isHomePageAvailable { + actions.insert(openHomePage, at: 0) + } + + return actions + } + + /* + Returns a list of actions which is used to build the general browser menu + These items repersent global options that are presented in the menu + TODO: These icons should all have the icons and use Strings.swift + */ + + typealias PageOptionsVC = QRCodeViewControllerDelegate & SettingsDelegate & PresentingModalViewControllerDelegate & UIViewController + + func getOtherPanelActions(vcDelegate: PageOptionsVC) -> [PhotonActionSheetItem] { + var noImageMode: PhotonActionSheetItem? = nil + if #available(iOS 11, *) { + let noImageEnabled = NoImageModeHelper.isActivated(profile.prefs) + let noImageText = noImageEnabled ? Strings.AppMenuNoImageModeDisable : Strings.AppMenuNoImageModeEnable + noImageMode = PhotonActionSheetItem(title: noImageText, iconString: "menu-NoImageMode", isEnabled: noImageEnabled) { action in + NoImageModeHelper.toggle(profile: self.profile, tabManager: self.tabManager) + } + } + + let nightModeEnabled = NightModeHelper.isActivated(profile.prefs) + let nightModeText = nightModeEnabled ? Strings.AppMenuNightModeDisable : Strings.AppMenuNightModeEnable + let nightMode = PhotonActionSheetItem(title: nightModeText, iconString: "menu-NightMode", isEnabled: nightModeEnabled) { action in + NightModeHelper.toggle(self.profile.prefs, tabManager: self.tabManager) + } + + let openSettings = PhotonActionSheetItem(title: Strings.AppMenuSettingsTitleString, iconString: "menu-Settings") { action in + let settingsTableViewController = AppSettingsTableViewController() + settingsTableViewController.profile = self.profile + settingsTableViewController.tabManager = self.tabManager + settingsTableViewController.settingsDelegate = vcDelegate + + let controller = SettingsNavigationController(rootViewController: settingsTableViewController) + controller.popoverDelegate = vcDelegate + controller.modalPresentationStyle = UIModalPresentationStyle.formSheet + vcDelegate.present(controller, animated: true, completion: nil) + } + + if let noImageMode = noImageMode { + return [noImageMode, nightMode, openSettings] + } + return [nightMode, openSettings] + } + + func getTabActions(tab: Tab, buttonView: UIView, + presentShareMenu: @escaping (URL, Tab, UIView, UIPopoverArrowDirection) -> Void, + findInPage: @escaping () -> Void, + presentableVC: PresentableVC, + success: @escaping (String) -> Void) -> Array<[PhotonActionSheetItem]> { + + let toggleActionTitle = tab.desktopSite ? Strings.AppMenuViewMobileSiteTitleString : Strings.AppMenuViewDesktopSiteTitleString + let toggleDesktopSite = PhotonActionSheetItem(title: toggleActionTitle, iconString: "menu-RequestDesktopSite") { action in + tab.toggleDesktopSite() + } + + let addReadingList = PhotonActionSheetItem(title: Strings.AppMenuAddToReadingListTitleString, iconString: "addToReadingList") { action in + guard let url = tab.url?.displayURL else { return } + + self.profile.readingList?.createRecordWithURL(url.absoluteString, title: tab.title ?? "", addedBy: UIDevice.current.name) + UnifiedTelemetry.recordEvent(category: .action, method: .add, object: .readingListItem, value: .pageActionMenu) + success(Strings.AppMenuAddToReadingListConfirmMessage) + } + + let findInPageAction = PhotonActionSheetItem(title: Strings.AppMenuFindInPageTitleString, iconString: "menu-FindInPage") { action in + findInPage() + } + + let bookmarkPage = PhotonActionSheetItem(title: Strings.AppMenuAddBookmarkTitleString, iconString: "menu-Bookmark") { action in + //TODO: can all this logic go somewhere else? + guard let url = tab.canonicalURL?.displayURL else { return } + let absoluteString = url.absoluteString + let shareItem = ShareItem(url: absoluteString, title: tab.title, favicon: tab.displayFavicon) + _ = self.profile.bookmarks.shareItem(shareItem) + var userData = [QuickActions.TabURLKey: shareItem.url] + if let title = shareItem.title { + userData[QuickActions.TabTitleKey] = title + } + QuickActions.sharedInstance.addDynamicApplicationShortcutItemOfType(.openLastBookmark, + withUserData: userData, + toApplication: UIApplication.shared) + tab.isBookmarked = true + UnifiedTelemetry.recordEvent(category: .action, method: .add, object: .bookmark, value: .pageActionMenu) + success(Strings.AppMenuAddBookmarkConfirmMessage) + } + + let removeBookmark = PhotonActionSheetItem(title: Strings.AppMenuRemoveBookmarkTitleString, iconString: "menu-Bookmark-Remove") { action in + //TODO: can all this logic go somewhere else? + guard let url = tab.url?.displayURL else { return } + let absoluteString = url.absoluteString + self.profile.bookmarks.modelFactory >>== { + $0.removeByURL(absoluteString).uponQueue(.main) { res in + if res.isSuccess { + tab.isBookmarked = false + UnifiedTelemetry.recordEvent(category: .action, method: .delete, object: .bookmark, value: .pageActionMenu) + success(Strings.AppMenuRemoveBookmarkConfirmMessage) + } + } + } + } + + let pinToTopSites = PhotonActionSheetItem(title: Strings.PinTopsiteActionTitle, iconString: "action_pin") { action in + guard let url = tab.url?.displayURL, + let sql = self.profile.history as? SQLiteHistory else { return } + let absoluteString = url.absoluteString + + sql.getSitesForURLs([absoluteString]) >>== { result in + guard let siteOp = result.asArray().first, let site = siteOp else { + log.warning("Could not get site for \(absoluteString)") + return + } + + _ = self.profile.history.addPinnedTopSite(site).value + } + } + + let sendToDevice = PhotonActionSheetItem(title: Strings.SendToDeviceTitle, iconString: "menu-Send-to-Device") { action in + guard let bvc = presentableVC as? PresentableVC & InstructionsViewControllerDelegate & ClientPickerViewControllerDelegate else { return } + if !self.profile.hasAccount() { + let instructionsViewController = InstructionsViewController() + instructionsViewController.delegate = bvc + let navigationController = UINavigationController(rootViewController: instructionsViewController) + navigationController.modalPresentationStyle = .formSheet + bvc.present(navigationController, animated: true, completion: nil) + return + } + + let clientPickerViewController = ClientPickerViewController() + clientPickerViewController.clientPickerDelegate = bvc + clientPickerViewController.profile = self.profile + clientPickerViewController.profileNeedsShutdown = false + let navigationController = UINavigationController(rootViewController: clientPickerViewController) + navigationController.modalPresentationStyle = .formSheet + bvc.present(navigationController, animated: true, completion: nil) + } + + let share = PhotonActionSheetItem(title: Strings.AppMenuSharePageTitleString, iconString: "action_share") { action in + guard let url = tab.canonicalURL?.displayURL else { return } + presentShareMenu(url, tab, buttonView, .up) + } + + let closeTab = PhotonActionSheetItem(title: Strings.CloseTabTitle, iconString: "action_remove") { action in + self.tabManager.removeTab(tab) + } + + let copyURL = PhotonActionSheetItem(title: Strings.AppMenuCopyURLTitleString, iconString: "menu-Copy-Link") { _ in + UIPasteboard.general.url = tab.canonicalURL?.displayURL + success(Strings.AppMenuCopyURLConfirmMessage) + } + + var topActions: [PhotonActionSheetItem] = [] + + // Disable bookmarking and reading list if the URL is too long. + if !tab.urlIsTooLong { + topActions.append(tab.isBookmarked ? removeBookmark : bookmarkPage) + + if tab.readerModeAvailableOrActive { + topActions.append(addReadingList) + } + } + + return [topActions, [copyURL, findInPageAction, toggleDesktopSite, pinToTopSites, sendToDevice, closeTab], [share]] + } +} + diff --git a/mobile/ios/Client/Frontend/Widgets/RoundedToolbar.swift b/mobile/ios/Client/Frontend/Widgets/RoundedToolbar.swift new file mode 100644 index 0000000000..ea56aace36 --- /dev/null +++ b/mobile/ios/Client/Frontend/Widgets/RoundedToolbar.swift @@ -0,0 +1,33 @@ +/* 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 UIKit + +class RoundedToolbar: UIToolbar { + + fileprivate var layerBackgroundColor: UIColor? + + override var backgroundColor: UIColor? { + get { return layerBackgroundColor } + + set { + layerBackgroundColor = newValue + } + } + + var cornerRadius: CGSize = CGSize.zero + + var cornersToRound: UIRectCorner = [.allCorners] + + /** + * The toolbar on the menu requires rounded corners on the top and bottom so we need a custom + * view to do this + */ + override func draw(_ rect: CGRect) { + super.draw(rect) + layer.sublayers?.filter { $0.mask != nil } .forEach { $0.removeFromSuperlayer() } + addRoundedCorners(cornersToRound, cornerRadius: cornerRadius, color: layerBackgroundColor ?? UIColor.white) + } + +} diff --git a/mobile/ios/Client/Frontend/Widgets/SearchInputView.swift b/mobile/ios/Client/Frontend/Widgets/SearchInputView.swift new file mode 100644 index 0000000000..95e63b895a --- /dev/null +++ b/mobile/ios/Client/Frontend/Widgets/SearchInputView.swift @@ -0,0 +1,205 @@ +/* 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 SnapKit + +private struct SearchInputViewUX { + + static let horizontalSpacing: CGFloat = 16 + static let titleFont: UIFont = UIFont.systemFont(ofSize: 16) + static let titleColor: UIColor = UIColor.lightGray + static let inputColor: UIColor = UIConstants.HighlightBlue + static let borderColor: UIColor = UIConstants.SeparatorColor + static let borderLineWidth: CGFloat = 0.5 + static let closeButtonSize: CGFloat = 36 +} + +@objc protocol SearchInputViewDelegate: class { + + func searchInputView(_ searchView: SearchInputView, didChangeTextTo text: String) + + func searchInputViewBeganEditing(_ searchView: SearchInputView) + + func searchInputViewFinishedEditing(_ searchView: SearchInputView) +} + +class SearchInputView: UIView { + + weak var delegate: SearchInputViewDelegate? + + var showBottomBorder: Bool = true { + didSet { + bottomBorder.isHidden = !showBottomBorder + } + } + + lazy var inputField: UITextField = { + let textField = UITextField() + textField.delegate = self + textField.textColor = SearchInputViewUX.inputColor + textField.tintColor = SearchInputViewUX.inputColor + textField.addTarget(self, action: #selector(SearchInputView.inputTextDidChange(_:)), for: .editingChanged) + textField.accessibilityLabel = NSLocalizedString("Search Input Field", tableName: "LoginManager", comment: "Accessibility label for the search input field in the Logins list") + textField.autocorrectionType = .no + textField.autocapitalizationType = .none + return textField + }() + + lazy var titleLabel: UILabel = { + let label = UILabel() + label.text = NSLocalizedString("Search", tableName: "LoginManager", comment: "Title for the search field at the top of the Logins list screen") + label.font = SearchInputViewUX.titleFont + label.textColor = SearchInputViewUX.titleColor + return label + }() + + lazy var searchIcon: UIImageView = { + return UIImageView(image: UIImage(named: "quickSearch")) + }() + + fileprivate lazy var closeButton: UIButton = { + let button = UIButton() + button.addTarget(self, action: #selector(SearchInputView.tappedClose), for: .touchUpInside) + button.setImage(UIImage(named: "clear"), for: UIControlState()) + button.accessibilityLabel = NSLocalizedString("Clear Search", tableName: "LoginManager", + comment: "Accessibility message e.g. spoken by VoiceOver after the user taps the close button in the search field to clear the search and exit search mode") + return button + }() + + fileprivate var centerContainer = UIView() + + fileprivate lazy var bottomBorder: UIView = { + let border = UIView() + border.backgroundColor = SearchInputViewUX.borderColor + return border + }() + + fileprivate lazy var overlay: UIView = { + let view = UIView() + view.backgroundColor = UIColor.white + view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(SearchInputView.tappedSearch))) + + view.isAccessibilityElement = true + view.accessibilityLabel = NSLocalizedString("Enter Search Mode", tableName: "LoginManager", comment: "Accessibility label for entering search mode for logins") + return view + }() + + fileprivate(set) var isEditing = false { + didSet { + if isEditing { + overlay.isHidden = true + inputField.isHidden = false + inputField.accessibilityElementsHidden = false + closeButton.isHidden = false + closeButton.accessibilityElementsHidden = false + } else { + overlay.isHidden = false + inputField.isHidden = true + inputField.accessibilityElementsHidden = true + closeButton.isHidden = true + closeButton.accessibilityElementsHidden = true + } + } + } + + override init(frame: CGRect) { + super.init(frame: frame) + + backgroundColor = UIColor.white + isUserInteractionEnabled = true + + addSubview(inputField) + addSubview(closeButton) + + centerContainer.addSubview(searchIcon) + centerContainer.addSubview(titleLabel) + overlay.addSubview(centerContainer) + addSubview(overlay) + addSubview(bottomBorder) + + setupConstraints() + + setEditing(false) + } + + fileprivate func setupConstraints() { + centerContainer.snp.makeConstraints { make in + make.center.equalTo(overlay) + } + + overlay.snp.makeConstraints { make in + make.edges.equalTo(self) + } + + searchIcon.snp.makeConstraints { make in + make.right.equalTo(titleLabel.snp.left).offset(-SearchInputViewUX.horizontalSpacing) + make.centerY.equalTo(centerContainer) + } + + titleLabel.snp.makeConstraints { make in + make.center.equalTo(centerContainer) + } + + inputField.snp.makeConstraints { make in + make.left.equalTo(self).offset(SearchInputViewUX.horizontalSpacing) + make.centerY.equalTo(self) + make.right.equalTo(closeButton.snp.left).offset(-SearchInputViewUX.horizontalSpacing) + } + + closeButton.snp.makeConstraints { make in + make.right.equalTo(self).offset(-SearchInputViewUX.horizontalSpacing) + make.centerY.equalTo(self) + make.size.equalTo(SearchInputViewUX.closeButtonSize) + } + + bottomBorder.snp.makeConstraints { make in + make.left.right.bottom.equalTo(self) + make.height.equalTo(SearchInputViewUX.borderLineWidth) + } + } + + // didSet callbacks don't trigger when a property is being set in the init() call + // but calling a method that does works fine. + fileprivate func setEditing(_ editing: Bool) { + isEditing = editing + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +// MARK: - Selectors +extension SearchInputView { + + func tappedSearch() { + isEditing = true + inputField.becomeFirstResponder() + delegate?.searchInputViewBeganEditing(self) + } + + func tappedClose() { + isEditing = false + delegate?.searchInputViewFinishedEditing(self) + inputField.text = nil + inputField.resignFirstResponder() + } + + func inputTextDidChange(_ textField: UITextField) { + delegate?.searchInputView(self, didChangeTextTo: textField.text ?? "") + } +} + +// MARK: - UITextFieldDelegate +extension SearchInputView: UITextFieldDelegate { + + func textFieldDidEndEditing(_ textField: UITextField) { + // If there is no text, go back to showing the title view + if (textField.text?.characters.count ?? 0) == 0 { + isEditing = false + delegate?.searchInputViewFinishedEditing(self) + } + } +} diff --git a/mobile/ios/Client/Frontend/Widgets/SeparatorTableCell.swift b/mobile/ios/Client/Frontend/Widgets/SeparatorTableCell.swift new file mode 100644 index 0000000000..2ffad23255 --- /dev/null +++ b/mobile/ios/Client/Frontend/Widgets/SeparatorTableCell.swift @@ -0,0 +1,29 @@ +/* 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 UIKit + +class SeparatorTableCell: UITableViewCell { + override var textLabel: UILabel? { + return nil + } + + override var detailTextLabel: UILabel? { + return nil + } + + override init(style: UITableViewCellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + + self.selectionStyle = UITableViewCellSelectionStyle.none + self.indentationWidth = 0 + self.separatorInset = UIEdgeInsets.zero + self.layoutMargins = UIEdgeInsets.zero + self.backgroundColor = UIConstants.PanelBackgroundColor // So we get a gentle white and grey stripe. + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} diff --git a/mobile/ios/Client/Frontend/Widgets/SiteTableViewController.swift b/mobile/ios/Client/Frontend/Widgets/SiteTableViewController.swift new file mode 100644 index 0000000000..db9de263f9 --- /dev/null +++ b/mobile/ios/Client/Frontend/Widgets/SiteTableViewController.swift @@ -0,0 +1,160 @@ +/* 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 UIKit +import Storage + +struct SiteTableViewControllerUX { + static let HeaderHeight = CGFloat(32) + static let RowHeight = CGFloat(44) + static let HeaderBorderColor = UIColor(rgb: 0xCFD5D9).withAlphaComponent(0.8) + static let HeaderTextColor = UIAccessibilityDarkerSystemColorsEnabled() ? UIColor.black : UIColor(rgb: 0x232323) + static let HeaderBackgroundColor = UIColor(rgb: 0xf7f8f7) + static let HeaderFont = UIFont.systemFont(ofSize: 12, weight: UIFontWeightMedium) + static let HeaderTextMargin = CGFloat(16) +} + +class SiteTableViewHeader: UITableViewHeaderFooterView { + // I can't get drawRect to play nicely with the glass background. As a fallback + // we just use views for the top and bottom borders. + let topBorder = UIView() + let bottomBorder = UIView() + let titleLabel = UILabel() + + override var textLabel: UILabel? { + return titleLabel + } + + override init(reuseIdentifier: String?) { + super.init(reuseIdentifier: reuseIdentifier) + + topBorder.backgroundColor = SiteTableViewControllerUX.HeaderBorderColor + bottomBorder.backgroundColor = SiteTableViewControllerUX.HeaderBorderColor + contentView.backgroundColor = SiteTableViewControllerUX.HeaderBackgroundColor + + titleLabel.font = DynamicFontHelper.defaultHelper.DeviceFontMediumBold + titleLabel.textColor = SiteTableViewControllerUX.HeaderTextColor + titleLabel.textAlignment = .left + + addSubview(topBorder) + addSubview(bottomBorder) + contentView.addSubview(titleLabel) + + topBorder.snp.makeConstraints { make in + make.left.right.equalTo(self) + make.top.equalTo(self).offset(-0.5) + make.height.equalTo(0.5) + } + + bottomBorder.snp.makeConstraints { make in + make.left.right.bottom.equalTo(self) + make.height.equalTo(0.5) + } + + // A table view will initialize the header with CGSizeZero before applying the actual size. Hence, the label's constraints + // must not impose a minimum width on the content view. + titleLabel.snp.makeConstraints { make in + make.left.equalTo(contentView).offset(SiteTableViewControllerUX.HeaderTextMargin).priority(1000) + make.right.equalTo(contentView).offset(-SiteTableViewControllerUX.HeaderTextMargin).priority(1000) + make.left.greaterThanOrEqualTo(contentView) // Fallback for when the left space constraint breaks + make.right.lessThanOrEqualTo(contentView) // Fallback for when the right space constraint breaks + make.centerY.equalTo(contentView) + } + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +/** + * Provides base shared functionality for site rows and headers. + */ +class SiteTableViewController: UIViewController, UITableViewDelegate, UITableViewDataSource { + fileprivate let CellIdentifier = "CellIdentifier" + fileprivate let HeaderIdentifier = "HeaderIdentifier" + var profile: Profile! { + didSet { + reloadData() + } + } + var data: Cursor = Cursor(status: .success, msg: "No data set") + var tableView = UITableView() + + override func viewDidLoad() { + super.viewDidLoad() + + view.addSubview(tableView) + tableView.snp.makeConstraints { make in + make.edges.equalTo(self.view) + return + } + + tableView.delegate = self + tableView.dataSource = self + tableView.register(SiteTableViewCell.self, forCellReuseIdentifier: CellIdentifier) + tableView.register(SiteTableViewHeader.self, forHeaderFooterViewReuseIdentifier: HeaderIdentifier) + tableView.layoutMargins = UIEdgeInsets.zero + tableView.keyboardDismissMode = UIScrollViewKeyboardDismissMode.onDrag + tableView.backgroundColor = UIConstants.PanelBackgroundColor + tableView.separatorColor = UIConstants.SeparatorColor + tableView.accessibilityIdentifier = "SiteTable" + tableView.cellLayoutMarginsFollowReadableWidth = false + + // Set an empty footer to prevent empty cells from appearing in the list. + tableView.tableFooterView = UIView() + } + + deinit { + // The view might outlive this view controller thanks to animations; + // explicitly nil out its references to us to avoid crashes. Bug 1218826. + tableView.dataSource = nil + tableView.delegate = nil + } + + override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { + coordinator.animate(alongsideTransition: { context in + //The AS context menu does not behave correctly. Dismiss it when rotating. + if let _ = self.presentedViewController as? PhotonActionSheet { + self.presentedViewController?.dismiss(animated: true, completion: nil) + } + }, completion: nil) + } + + func reloadData() { + if data.status != .success { + print("Err: \(data.statusMessage)", terminator: "\n") + } else { + self.tableView.reloadData() + } + } + + func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return data.count + } + + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let cell = tableView.dequeueReusableCell(withIdentifier: CellIdentifier, for: indexPath) + if self.tableView(tableView, hasFullWidthSeparatorForRowAtIndexPath: indexPath) { + cell.separatorInset = UIEdgeInsets.zero + } + return cell + } + + func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { + return tableView.dequeueReusableHeaderFooterView(withIdentifier: HeaderIdentifier) + } + + func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { + return SiteTableViewControllerUX.HeaderHeight + } + + func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { + return SiteTableViewControllerUX.RowHeight + } + + func tableView(_ tableView: UITableView, hasFullWidthSeparatorForRowAtIndexPath indexPath: IndexPath) -> Bool { + return false + } +} diff --git a/mobile/ios/Client/Frontend/Widgets/SnackBar.swift b/mobile/ios/Client/Frontend/Widgets/SnackBar.swift new file mode 100644 index 0000000000..5f9609522c --- /dev/null +++ b/mobile/ios/Client/Frontend/Widgets/SnackBar.swift @@ -0,0 +1,319 @@ +/* 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 SnapKit +import Shared + +class SnackBarUX { + static var MaxWidth: CGFloat = 400 +} + +/** + * A specialized version of UIButton for use in SnackBars. These are displayed evenly + * spaced in the bottom of the bar. The main convenience of these is that you can pass + * in a callback in the constructor (although these also style themselves appropriately). + * + *``SnackButton(title: "OK", { _ in print("OK", terminator: "\n") })`` + */ +class SnackButton: UIButton { + let callback: (_ bar: SnackBar) -> Void + fileprivate var bar: SnackBar! + + /** + * An image to show as the background when a button is pressed. This is currently a 1x1 pixel blue color + */ + lazy var highlightImg: UIImage = { + let size = CGSize(width: 1, height: 1) + return UIImage.createWithColor(size, color: UIConstants.HighlightColor) + }() + + init(title: String, accessibilityIdentifier: String, callback: @escaping (_ bar: SnackBar) -> Void) { + self.callback = callback + + super.init(frame: CGRect.zero) + + setTitle(title, for: UIControlState()) + titleLabel?.font = DynamicFontHelper.defaultHelper.DefaultMediumFont + setBackgroundImage(highlightImg, for: .highlighted) + setTitleColor(UIConstants.HighlightText, for: .highlighted) + + addTarget(self, action: #selector(SnackButton.onClick), for: .touchUpInside) + + self.accessibilityIdentifier = accessibilityIdentifier + } + + override init(frame: CGRect) { + self.callback = { bar in } + super.init(frame: frame) + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func onClick() { + callback(bar) + } + +} + +/** + * Presents some information to the user. Can optionally include some buttons and an image. Usage: + * + * ``let bar = SnackBar(text: "This is some text in the snackbar.", + * img: UIImage(named: "bookmark"), + * buttons: [ + * SnackButton(title: "OK", { _ in print("OK", terminator: "\n") }), + * SnackButton(title: "Cancel", { _ in print("Cancel", terminator: "\n") }), + * SnackButton(title: "Maybe", { _ in print("Maybe", terminator: "\n") }) + * ] + * )`` + */ +class SnackBar: UIView { + let imageView: UIImageView + let textLabel: UILabel + let contentView: UIView + let backgroundView: UIView + let buttonsView: Toolbar + fileprivate var buttons = [SnackButton]() + // The Constraint for the bottom of this snackbar. We use this to transition it + var bottom: Constraint? + + convenience init(text: String, img: UIImage?, buttons: [SnackButton]?) { + var attributes = [String: AnyObject]() + attributes[NSFontAttributeName] = DynamicFontHelper.defaultHelper.DefaultMediumFont + attributes[NSBackgroundColorAttributeName] = UIColor.clear + let attrText = NSAttributedString(string: text, attributes: attributes) + self.init(attrText: attrText, img: img, buttons: buttons) + } + + init(attrText: NSAttributedString, img: UIImage?, buttons: [SnackButton]?) { + imageView = UIImageView() + textLabel = UILabel() + contentView = UIView() + buttonsView = Toolbar() + backgroundView = UIVisualEffectView(effect: UIBlurEffect(style: UIBlurEffectStyle.extraLight)) + + super.init(frame: CGRect.zero) + + imageView.image = img + textLabel.attributedText = attrText + if let buttons = buttons { + for button in buttons { + addButton(button) + } + } + setup() + } + + fileprivate override init(frame: CGRect) { + imageView = UIImageView() + textLabel = UILabel() + contentView = UIView() + buttonsView = Toolbar() + backgroundView = UIVisualEffectView(effect: UIBlurEffect(style: UIBlurEffectStyle.extraLight)) + + super.init(frame: frame) + } + + fileprivate func setup() { + textLabel.backgroundColor = nil + + addSubview(backgroundView) + addSubview(contentView) + contentView.addSubview(imageView) + contentView.addSubview(textLabel) + addSubview(buttonsView) + + self.backgroundColor = UIColor.clear + buttonsView.drawTopBorder = true + buttonsView.drawBottomBorder = false + buttonsView.drawSeperators = true + + imageView.contentMode = UIViewContentMode.left + + textLabel.font = DynamicFontHelper.defaultHelper.DefaultMediumFont + textLabel.lineBreakMode = NSLineBreakMode.byWordWrapping + textLabel.numberOfLines = 0 + textLabel.backgroundColor = UIColor.clear + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func layoutSubviews() { + super.layoutSubviews() + let imageWidth: CGFloat + if let img = imageView.image { + imageWidth = img.size.width + UIConstants.DefaultPadding * 2 + } else { + imageWidth = 0 + } + self.textLabel.preferredMaxLayoutWidth = contentView.frame.width - (imageWidth + UIConstants.DefaultPadding) + super.layoutSubviews() + } + + fileprivate func drawLine(_ context: CGContext, start: CGPoint, end: CGPoint) { + context.setStrokeColor(UIConstants.BorderColor.cgColor) + context.setLineWidth(1) + context.move(to: CGPoint(x: start.x, y: start.y)) + context.addLine(to: CGPoint(x: end.x, y: end.y)) + context.strokePath() + } + + override func draw(_ rect: CGRect) { + let context = UIGraphicsGetCurrentContext() + drawLine(context!, start: CGPoint(x: 0, y: 1), end: CGPoint(x: frame.size.width, y: 1)) + } + + /** + * Called to check if the snackbar should be removed or not. By default, Snackbars persist forever. + * Override this class or use a class like CountdownSnackbar if you want things expire + * - returns: true if the snackbar should be kept alive + */ + func shouldPersist(_ tab: Tab) -> Bool { + return true + } + + override func updateConstraints() { + super.updateConstraints() + + backgroundView.snp.remakeConstraints { make in + make.bottom.left.right.equalTo(self) + // Offset it by the width of the top border line so we can see the line from the super view + make.top.equalTo(self).offset(1) + } + + contentView.snp.remakeConstraints { make in + make.top.left.right.equalTo(self).inset(UIEdgeInsets(equalInset: UIConstants.DefaultPadding)) + } + + if let img = imageView.image { + imageView.snp.remakeConstraints { make in + make.left.centerY.equalTo(contentView) + // To avoid doubling the padding, the textview doesn't have an inset on its left side. + // Instead, it relies on the imageView to tell it where its left side should be. + make.width.equalTo(img.size.width + UIConstants.DefaultPadding) + make.height.equalTo(img.size.height + UIConstants.DefaultPadding) + } + } else { + imageView.snp.remakeConstraints { make in + make.width.height.equalTo(0) + make.top.left.equalTo(self) + make.bottom.lessThanOrEqualTo(contentView.snp.bottom) + } + } + + textLabel.snp.remakeConstraints { make in + make.top.equalTo(contentView) + make.left.equalTo(self.imageView.snp.right) + make.trailing.equalTo(contentView) + make.bottom.lessThanOrEqualTo(contentView.snp.bottom) + } + + buttonsView.snp.remakeConstraints { make in + make.top.equalTo(contentView.snp.bottom).offset(UIConstants.DefaultPadding) + make.bottom.equalTo(self.snp.bottom) + make.left.right.equalTo(self) + if self.buttonsView.subviews.count > 0 { + make.height.equalTo(UIConstants.SnackbarButtonHeight) + } else { + make.height.equalTo(0) + } + } + } + + var showing: Bool { + return alpha != 0 && self.superview != nil + } + + /** + * Helper for animating the Snackbar showing on screen. + */ + func show() { + alpha = 1 + bottom?.update(offset: 0) + } + + /** + * Helper for animating the Snackbar leaving the screen. + */ + func hide() { + alpha = 0 + var h = frame.height + if h == 0 { + h = UIConstants.ToolbarHeight + } + bottom?.update(offset: h) + } + + fileprivate func addButton(_ snackButton: SnackButton) { + snackButton.bar = self + buttonsView.addButtons([snackButton]) + buttonsView.setNeedsUpdateConstraints() + } +} + +/** + * A special version of a snackbar that persists for at least a timeout. After that + * it will dismiss itself on the next page load where this tab isn't showing. As long as + * you stay on the current tab though, it will persist until you interact with it. + */ +class TimerSnackBar: SnackBar { + fileprivate var prevURL: URL? + fileprivate var timer: Timer? + fileprivate var timeout: TimeInterval + + init(timeout: TimeInterval = 10, attrText: NSAttributedString, img: UIImage?, buttons: [SnackButton]?) { + self.timeout = timeout + super.init(attrText: attrText, img: img, buttons: buttons) + } + + override init(frame: CGRect) { + self.timeout = 0 + super.init(frame: frame) + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + static func showAppStoreConfirmationBar(forTab tab: Tab, appStoreURL: URL) { + let msg = NSAttributedString(string: Strings.ExternalLinkAppStoreConfirmationTitle, attributes: [NSForegroundColorAttributeName: UIConstants.TableViewRowTextColor]) + let bar = TimerSnackBar(attrText: msg, + img: UIImage(named: "defaultFavicon"), + buttons: [ + SnackButton(title: UIConstants.OKString, accessibilityIdentifier: "ConfirmOpenInAppStore", callback: { bar in + tab.removeSnackbar(bar) + UIApplication.shared.openURL(appStoreURL) + }), + SnackButton(title: UIConstants.CancelString, accessibilityIdentifier: "CancelOpenInAppStore", callback: { bar in + tab.removeSnackbar(bar) + }) + ]) + + tab.addSnackbar(bar) + } + + override func show() { + self.timer = Timer(timeInterval: timeout, target: self, selector: #selector(TimerSnackBar.SELTimerDone), userInfo: nil, repeats: false) + RunLoop.current.add(self.timer!, forMode: RunLoopMode.defaultRunLoopMode) + super.show() + } + + @objc + func SELTimerDone() { + self.timer = nil + } + + override func shouldPersist(_ tab: Tab) -> Bool { + if !showing { + return timer != nil + } + + return super.shouldPersist(tab) + } +} diff --git a/mobile/ios/Client/Frontend/Widgets/TabsButton.swift b/mobile/ios/Client/Frontend/Widgets/TabsButton.swift new file mode 100644 index 0000000000..1641626ed4 --- /dev/null +++ b/mobile/ios/Client/Frontend/Widgets/TabsButton.swift @@ -0,0 +1,255 @@ +/* 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 SnapKit +import Shared +import XCGLogger + +private let log = Logger.browserLogger + +struct TabsButtonUX { + static let TitleColor: UIColor = UIColor(rgb: 0x272727) + static let TitleBackgroundColor: UIColor = UIColor.white + static let CornerRadius: CGFloat = 2 + static let TitleFont: UIFont = UIConstants.DefaultChromeSmallFontBold + static let BorderStrokeWidth: CGFloat = 3 + static let BorderColor: UIColor = UIColor.darkGray + static let TitleInsets = UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10) + + static let Themes: [String: Theme] = { + var themes = [String: Theme]() + var theme = Theme() + theme.borderColor = UIColor(rgb: 0xD2D2D4) + theme.backgroundColor = UIColor(rgb: 0x38383D) + theme.textColor = UIColor(rgb: 0xD2D2D4) + theme.highlightButtonColor = UIConstants.PrivateModePurple + theme.highlightTextColor = TabsButtonUX.TitleColor + theme.highlightBorderColor = UIConstants.PrivateModePurple + themes[Theme.PrivateMode] = theme + + theme = Theme() + theme.borderColor = UIColor(rgb: 0x272727) + theme.backgroundColor = UIConstants.AppBackgroundColor + theme.textColor = UIColor(rgb: 0x272727) + theme.highlightButtonColor = TabsButtonUX.TitleColor + theme.highlightTextColor = TabsButtonUX.TitleBackgroundColor + theme.highlightBorderColor = TabsButtonUX.TitleColor + themes[Theme.NormalMode] = theme + + return themes + }() +} + +class TabsButton: UIButton { + + var textColor = UIColor.white { + didSet { + countLabel.textColor = textColor + borderView.color = textColor + } + } + var titleBackgroundColor = UIColor.white { + didSet { + labelBackground.backgroundColor = titleBackgroundColor + } + } + var highlightTextColor: UIColor? + var highlightBackgroundColor: UIColor? + + override var isHighlighted: Bool { + didSet { + if isHighlighted { + countLabel.textColor = textColor + borderView.color = titleBackgroundColor + labelBackground.backgroundColor = titleBackgroundColor + } else { + countLabel.textColor = textColor + borderView.color = textColor + labelBackground.backgroundColor = titleBackgroundColor + } + } + } + + override var transform: CGAffineTransform { + didSet { + clonedTabsButton?.transform = transform + } + } + + lazy var countLabel: UILabel = { + let label = UILabel() + label.font = TabsButtonUX.TitleFont + label.layer.cornerRadius = TabsButtonUX.CornerRadius + label.textAlignment = NSTextAlignment.center + label.isUserInteractionEnabled = false + return label + }() + + lazy var insideButton: UIView = { + let view = UIView() + view.clipsToBounds = false + view.isUserInteractionEnabled = false + return view + }() + + fileprivate lazy var labelBackground: UIView = { + let background = UIView() + background.layer.cornerRadius = TabsButtonUX.CornerRadius + background.isUserInteractionEnabled = false + return background + }() + + fileprivate lazy var borderView: InnerStrokedView = { + let border = InnerStrokedView() + border.strokeWidth = TabsButtonUX.BorderStrokeWidth + border.cornerRadius = TabsButtonUX.CornerRadius + border.isUserInteractionEnabled = false + return border + }() + + // Used to temporarily store the cloned button so we can respond to layout changes during animation + fileprivate weak var clonedTabsButton: TabsButton? + + override init(frame: CGRect) { + super.init(frame: frame) + insideButton.addSubview(labelBackground) + insideButton.addSubview(borderView) + insideButton.addSubview(countLabel) + addSubview(insideButton) + isAccessibilityElement = true + accessibilityTraits |= UIAccessibilityTraitButton + } + + override func updateConstraints() { + super.updateConstraints() + labelBackground.snp.remakeConstraints { (make) -> Void in + make.edges.equalTo(insideButton) + } + borderView.snp.remakeConstraints { (make) -> Void in + make.edges.equalTo(insideButton) + } + countLabel.snp.remakeConstraints { (make) -> Void in + make.edges.equalTo(insideButton) + } + insideButton.snp.remakeConstraints { (make) -> Void in + make.size.equalTo(24) + make.center.equalTo(self) + } + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func clone() -> UIView { + let button = TabsButton() + + button.accessibilityLabel = accessibilityLabel + button.countLabel.text = countLabel.text + + // Copy all of the styable properties over to the new TabsButton + button.countLabel.font = countLabel.font + button.countLabel.textColor = countLabel.textColor + button.countLabel.layer.cornerRadius = countLabel.layer.cornerRadius + + button.labelBackground.backgroundColor = labelBackground.backgroundColor + button.labelBackground.layer.cornerRadius = labelBackground.layer.cornerRadius + + button.borderView.strokeWidth = borderView.strokeWidth + button.borderView.color = borderView.color + button.borderView.cornerRadius = borderView.cornerRadius + + return button + } + + func updateTabCount(_ count: Int, animated: Bool = true) { + let count = max(count, 1) + let currentCount = self.countLabel.text + let infinity = "\u{221E}" + let countToBe = (count < 100) ? count.description : infinity + + // only animate a tab count change if the tab count has actually changed + if currentCount != count.description || (clonedTabsButton?.countLabel.text ?? count.description) != count.description { + if let _ = self.clonedTabsButton { + self.clonedTabsButton?.layer.removeAllAnimations() + self.clonedTabsButton?.removeFromSuperview() + insideButton.layer.removeAllAnimations() + } + + // make a 'clone' of the tabs button + let newTabsButton = clone() as! TabsButton + + self.clonedTabsButton = newTabsButton + newTabsButton.frame = self.bounds + newTabsButton.addTarget(self, action: #selector(TabsButton.cloneDidClickTabs), for: UIControlEvents.touchUpInside) + newTabsButton.countLabel.text = countToBe + newTabsButton.accessibilityValue = countToBe + newTabsButton.insideButton.frame = self.insideButton.frame + newTabsButton.snp.removeConstraints() + self.addSubview(newTabsButton) + newTabsButton.snp.makeConstraints { make in + make.center.equalTo(self) + } + + // Instead of changing the anchorPoint of the CALayer, lets alter the rotation matrix math to be + // a rotation around a non-origin point + let frame = self.insideButton.frame + let halfTitleHeight = frame.height / 2 + var newFlipTransform = CATransform3DIdentity + newFlipTransform = CATransform3DTranslate(newFlipTransform, 0, halfTitleHeight, 0) + newFlipTransform.m34 = -1.0 / 200.0 // add some perspective + newFlipTransform = CATransform3DRotate(newFlipTransform, CGFloat(-(Double.pi / 2)), 1.0, 0.0, 0.0) + newTabsButton.insideButton.layer.transform = newFlipTransform + + var oldFlipTransform = CATransform3DIdentity + oldFlipTransform = CATransform3DTranslate(oldFlipTransform, 0, halfTitleHeight, 0) + oldFlipTransform.m34 = -1.0 / 200.0 // add some perspective + oldFlipTransform = CATransform3DRotate(oldFlipTransform, CGFloat(-(Double.pi / 2)), 1.0, 0.0, 0.0) + + let animate = { + newTabsButton.insideButton.layer.transform = CATransform3DIdentity + self.insideButton.layer.transform = oldFlipTransform + self.insideButton.layer.opacity = 0 + } + + let completion: (Bool) -> Void = { completed in + let noActiveAnimations = self.insideButton.layer.animationKeys()?.isEmpty ?? true + if completed || noActiveAnimations { + newTabsButton.removeFromSuperview() + self.insideButton.layer.opacity = 1 + self.insideButton.layer.transform = CATransform3DIdentity + } + self.accessibilityLabel = NSLocalizedString("Show Tabs", comment: "Accessibility label for the tabs button in the (top) tab toolbar") + self.countLabel.text = countToBe + self.accessibilityValue = countToBe + } + + if animated { + UIView.animate(withDuration: 1.5, delay: 0, usingSpringWithDamping: 0.5, initialSpringVelocity: 0.0, options: UIViewAnimationOptions(), animations: animate, completion: completion) + } else { + completion(true) + } + } + } + func cloneDidClickTabs() { + sendActions(for: UIControlEvents.touchUpInside) + } +} + +extension TabsButton: Themeable { + func applyTheme(_ themeName: String) { + guard let theme = TabsButtonUX.Themes[themeName] else { + fatalError("Theme not found") + } + titleBackgroundColor = theme.backgroundColor! + textColor = theme.textColor! + + countLabel.textColor = textColor + borderView.color = textColor + labelBackground.backgroundColor = titleBackgroundColor + + } +} + diff --git a/mobile/ios/Client/Frontend/Widgets/Theme.swift b/mobile/ios/Client/Frontend/Widgets/Theme.swift new file mode 100644 index 0000000000..1fe0ae4916 --- /dev/null +++ b/mobile/ios/Client/Frontend/Widgets/Theme.swift @@ -0,0 +1,23 @@ +/* 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 + +struct Theme { + var URLFontColor: UIColor? + var hostFontColor: UIColor? + var backgroundColor: UIColor? + var textColor: UIColor? + var highlightColor: UIColor? + var tintColor: UIColor? + var buttonTintColor: UIColor? + var activeBorderColor: UIColor? + var borderColor: UIColor? + var highlightButtonColor: UIColor? + var highlightBorderColor: UIColor? + var highlightTextColor: UIColor? + var disabledButtonColor: UIColor? + + static let PrivateMode = "Private" + static let NormalMode = "Normal" +} diff --git a/mobile/ios/Client/Frontend/Widgets/ToggleButton.swift b/mobile/ios/Client/Frontend/Widgets/ToggleButton.swift new file mode 100644 index 0000000000..1e61b08ab2 --- /dev/null +++ b/mobile/ios/Client/Frontend/Widgets/ToggleButton.swift @@ -0,0 +1,126 @@ +/* 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 UIKit + +private struct UX { + static let TopColor = UIColor(red: 179 / 255, green: 83 / 255, blue: 253 / 255, alpha: 1) + static let BottomColor = UIColor(red: 146 / 255, green: 16 / 255, blue: 253, alpha: 1) + + // The amount of pixels the toggle button will expand over the normal size. This results in the larger -> contract animation. + static let ExpandDelta: CGFloat = 5 + static let ShowDuration: TimeInterval = 0.4 + static let HideDuration: TimeInterval = 0.2 + + static let BackgroundSize = CGSize(width: 32, height: 32) +} + +class ToggleButton: UIButton { + func setSelected(_ selected: Bool, animated: Bool = true) { + self.isSelected = selected + if animated { + animateSelection(selected) + } + } + + fileprivate func updateMaskPathForSelectedState(_ selected: Bool) { + let path = CGMutablePath() + if selected { + var rect = CGRect(origin: CGPoint.zero, size: UX.BackgroundSize) + rect.center = maskShapeLayer.position + path.addEllipse(in: rect) + } else { + path.addEllipse(in: CGRect(origin: maskShapeLayer.position, size: CGSize.zero)) + } + self.maskShapeLayer.path = path + } + + fileprivate func animateSelection(_ selected: Bool) { + var endFrame = CGRect(origin: CGPoint.zero, size: UX.BackgroundSize) + endFrame.center = maskShapeLayer.position + + if selected { + let animation = CAKeyframeAnimation(keyPath: "path") + + let startPath = CGMutablePath() + startPath.addEllipse(in: CGRect(origin: maskShapeLayer.position, size: CGSize.zero)) + + let largerPath = CGMutablePath() + let largerBounds = endFrame.insetBy(dx: -UX.ExpandDelta, dy: -UX.ExpandDelta) + largerPath.addEllipse(in: largerBounds) + + let endPath = CGMutablePath() + endPath.addEllipse(in: endFrame) + + animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseOut) + animation.values = [ + startPath, + largerPath, + endPath + ] + animation.duration = UX.ShowDuration + self.maskShapeLayer.path = endPath + self.maskShapeLayer.add(animation, forKey: "grow") + } else { + let animation = CABasicAnimation(keyPath: "path") + animation.duration = UX.HideDuration + animation.fillMode = kCAFillModeForwards + + let fromPath = CGMutablePath() + fromPath.addEllipse(in: endFrame) + animation.fromValue = fromPath + animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut) + + let toPath = CGMutablePath() + toPath.addEllipse(in: CGRect(origin: self.maskShapeLayer.bounds.center, size: CGSize.zero)) + + self.maskShapeLayer.path = toPath + self.maskShapeLayer.add(animation, forKey: "shrink") + } + } + + lazy fileprivate var backgroundView: UIView = { + let view = UIView() + view.isUserInteractionEnabled = false + view.layer.addSublayer(self.gradientLayer) + return view + }() + + lazy fileprivate var maskShapeLayer: CAShapeLayer = { + let circle = CAShapeLayer() + return circle + }() + + lazy fileprivate var gradientLayer: CAGradientLayer = { + let gradientLayer = CAGradientLayer() + gradientLayer.colors = [UX.TopColor.cgColor, UX.BottomColor.cgColor] + gradientLayer.mask = self.maskShapeLayer + return gradientLayer + }() + + override init(frame: CGRect) { + super.init(frame: frame) + contentMode = UIViewContentMode.redraw + insertSubview(backgroundView, belowSubview: imageView!) + } + + override func layoutSubviews() { + super.layoutSubviews() + let zeroFrame = CGRect(origin: CGPoint.zero, size: frame.size) + backgroundView.frame = zeroFrame + + // Make the gradient larger than normal to allow the mask transition to show when it blows up + // a little larger than the resting size + gradientLayer.bounds = backgroundView.frame.insetBy(dx: -UX.ExpandDelta, dy: -UX.ExpandDelta) + maskShapeLayer.bounds = backgroundView.frame + gradientLayer.position = CGPoint(x: zeroFrame.midX, y: zeroFrame.midY) + maskShapeLayer.position = CGPoint(x: zeroFrame.midX, y: zeroFrame.midY) + + updateMaskPathForSelectedState(isSelected) + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} diff --git a/mobile/ios/Client/Frontend/Widgets/Toolbar.swift b/mobile/ios/Client/Frontend/Widgets/Toolbar.swift new file mode 100644 index 0000000000..3799baaa93 --- /dev/null +++ b/mobile/ios/Client/Frontend/Widgets/Toolbar.swift @@ -0,0 +1,93 @@ +/* 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 SnapKit + +class Toolbar: UIView { + var drawTopBorder = false + var drawBottomBorder = false + var drawSeperators = false + + override init(frame: CGRect) { + super.init(frame: frame) + self.backgroundColor = UIColor.clear + + // Allow the view to redraw itself on rotation changes + contentMode = UIViewContentMode.redraw + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + fileprivate func drawLine(_ context: CGContext, width: CGFloat, start: CGPoint, end: CGPoint) { + context.setStrokeColor(UIConstants.BorderColor.cgColor) + context.setLineWidth(width * (1 / UIScreen.main.scale) ) + context.move(to: CGPoint(x: start.x, y: start.y)) + context.addLine(to: CGPoint(x: end.x, y: end.y)) + context.strokePath() + } + + override func draw(_ rect: CGRect) { + if let context = UIGraphicsGetCurrentContext() { + if drawTopBorder { + drawLine(context, width: 1, start: CGPoint(x: 0, y: 0), end: CGPoint(x: frame.width, y: 0)) + } + + if drawBottomBorder { + drawLine(context, width: 1, start: CGPoint(x: 0, y: frame.height), end: CGPoint(x: frame.width, y: frame.height)) + } + + if drawSeperators { + var skippedFirst = false + for view in subviews { + if skippedFirst { + let frame = view.frame + drawLine(context, + width: 0.5, + start: CGPoint(x: floor(frame.origin.x), y: 0), + end: CGPoint(x: floor(frame.origin.x), y: self.frame.height)) + } else { + skippedFirst = true + } + } + } + } + } + + func addButtons(_ buttons: [UIButton]) { + for button in buttons { + button.setTitleColor(UIColor.black, for: UIControlState()) + button.setTitleColor(UIColor.gray, for: UIControlState.disabled) + button.imageView?.contentMode = UIViewContentMode.scaleAspectFit + addSubview(button) + } + } + + override func updateConstraints() { + var prev: UIView? = nil + for view in self.subviews { + view.snp.remakeConstraints { make in + if let prev = prev { + make.left.equalTo(prev.snp.right) + } else { + make.left.equalTo(self) + } + prev = view + + var bottomInset: CGFloat = 0.0 + if #available(iOS 11, *) { + if let window = UIApplication.shared.keyWindow { + bottomInset = window.safeAreaInsets.bottom + } + } + make.top.equalTo(self) + make.height.equalTo(UIConstants.BottomToolbarHeight - bottomInset) + make.width.equalTo(self).dividedBy(self.subviews.count) + } + } + super.updateConstraints() + } +} diff --git a/mobile/ios/Client/Frontend/Widgets/TwoLineCell.swift b/mobile/ios/Client/Frontend/Widgets/TwoLineCell.swift new file mode 100644 index 0000000000..0654ef31d2 --- /dev/null +++ b/mobile/ios/Client/Frontend/Widgets/TwoLineCell.swift @@ -0,0 +1,263 @@ +/* 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 UIKit + +struct TwoLineCellUX { + static let ImageSize: CGFloat = 29 + static let ImageCornerRadius: CGFloat = 8 + static let BorderViewMargin: CGFloat = 16 + static let BadgeSize: CGFloat = 16 + static let BadgeMargin: CGFloat = 16 + static let BorderFrameSize: CGFloat = 32 + static let TextColor = UIAccessibilityDarkerSystemColorsEnabled() ? UIColor.black : UIColor(rgb: 0x333333) + static let DetailTextColor = UIAccessibilityDarkerSystemColorsEnabled() ? UIColor.darkGray : UIColor.gray + static let DetailTextTopMargin: CGFloat = 0 +} + +class TwoLineTableViewCell: UITableViewCell { + fileprivate let twoLineHelper = TwoLineCellHelper() + + let _textLabel = UILabel() + let _detailTextLabel = UILabel() + + // Override the default labels with our own to disable default UITableViewCell label behaviours like dynamic type + override var textLabel: UILabel? { + return _textLabel + } + + override var detailTextLabel: UILabel? { + return _detailTextLabel + } + + override init(style: UITableViewCellStyle, reuseIdentifier: String?) { + super.init(style: UITableViewCellStyle.subtitle, reuseIdentifier: reuseIdentifier) + + contentView.addSubview(_textLabel) + contentView.addSubview(_detailTextLabel) + + twoLineHelper.setUpViews(self, textLabel: textLabel!, detailTextLabel: detailTextLabel!, imageView: imageView!) + + indentationWidth = 0 + layoutMargins = UIEdgeInsets.zero + + separatorInset = UIEdgeInsets(top: 0, left: TwoLineCellUX.ImageSize + 2 * TwoLineCellUX.BorderViewMargin, bottom: 0, right: 0) + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func layoutSubviews() { + super.layoutSubviews() + twoLineHelper.layoutSubviews() + } + + override func prepareForReuse() { + super.prepareForReuse() + self.textLabel!.alpha = 1 + self.imageView!.alpha = 1 + self.selectionStyle = .default + separatorInset = UIEdgeInsets(top: 0, left: TwoLineCellUX.ImageSize + 2 * TwoLineCellUX.BorderViewMargin, bottom: 0, right: 0) + twoLineHelper.setupDynamicFonts() + } + + // Save background color on UITableViewCell "select" because it disappears in the default behavior + override func setHighlighted(_ highlighted: Bool, animated: Bool) { + let color = imageView?.backgroundColor + super.setHighlighted(highlighted, animated: animated) + imageView?.backgroundColor = color + } + + // Save background color on UITableViewCell "select" because it disappears in the default behavior + override func setSelected(_ selected: Bool, animated: Bool) { + let color = imageView?.backgroundColor + super.setSelected(selected, animated: animated) + imageView?.backgroundColor = color + } + + func setRightBadge(_ badge: UIImage?) { + if let badge = badge { + self.accessoryView = UIImageView(image: badge) + } else { + self.accessoryView = nil + } + twoLineHelper.hasRightBadge = badge != nil + } + + func setLines(_ text: String?, detailText: String?) { + twoLineHelper.setLines(text, detailText: detailText) + } + + func mergeAccessibilityLabels(_ views: [AnyObject?]? = nil) { + twoLineHelper.mergeAccessibilityLabels(views) + } +} + +class SiteTableViewCell: TwoLineTableViewCell { + let borderView = UIView() + + override init(style: UITableViewCellStyle, reuseIdentifier: String?) { + super.init(style: UITableViewCellStyle.subtitle, reuseIdentifier: reuseIdentifier) + twoLineHelper.setUpViews(self, textLabel: textLabel!, detailTextLabel: detailTextLabel!, imageView: imageView!) + } + + override func layoutSubviews() { + super.layoutSubviews() + twoLineHelper.layoutSubviews() + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +class TwoLineHeaderFooterView: UITableViewHeaderFooterView { + fileprivate let twoLineHelper = TwoLineCellHelper() + + // UITableViewHeaderFooterView includes textLabel and detailTextLabel, so we can't override + // them. Unfortunately, they're also used in ways that interfere with us just using them: I get + // hard crashes in layout if I just use them; it seems there's a battle over adding to the + // contentView. So we add our own members, and cover up the other ones. + let _textLabel = UILabel() + let _detailTextLabel = UILabel() + + let imageView = UIImageView() + + // Yes, this is strange. + override var textLabel: UILabel? { + return _textLabel + } + + // Yes, this is strange. + override var detailTextLabel: UILabel? { + return _detailTextLabel + } + + override init(reuseIdentifier: String?) { + super.init(reuseIdentifier: reuseIdentifier) + twoLineHelper.setUpViews(self, textLabel: _textLabel, detailTextLabel: _detailTextLabel, imageView: imageView) + + contentView.addSubview(_textLabel) + contentView.addSubview(_detailTextLabel) + contentView.addSubview(imageView) + + layoutMargins = UIEdgeInsets.zero + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func layoutSubviews() { + super.layoutSubviews() + twoLineHelper.layoutSubviews() + } + + override func prepareForReuse() { + super.prepareForReuse() + twoLineHelper.setupDynamicFonts() + } + + func mergeAccessibilityLabels(_ views: [AnyObject?]? = nil) { + twoLineHelper.mergeAccessibilityLabels(views) + } +} + +private class TwoLineCellHelper { + weak var container: UIView? + var textLabel: UILabel! + var detailTextLabel: UILabel! + var imageView: UIImageView! + var hasRightBadge: Bool = false + + // TODO: Not ideal. We should figure out a better way to get this initialized. + func setUpViews(_ container: UIView, textLabel: UILabel, detailTextLabel: UILabel, imageView: UIImageView) { + self.container = container + self.textLabel = textLabel + self.detailTextLabel = detailTextLabel + self.imageView = imageView + + if let headerView = self.container as? UITableViewHeaderFooterView { + headerView.contentView.backgroundColor = UIColor.clear + } else { + self.container?.backgroundColor = UIColor.clear + } + + textLabel.textColor = TwoLineCellUX.TextColor + detailTextLabel.textColor = TwoLineCellUX.DetailTextColor + setupDynamicFonts() + + imageView.contentMode = .scaleAspectFill + imageView.layer.cornerRadius = 6 //hmm + imageView.layer.masksToBounds = true + } + + func setupDynamicFonts() { + textLabel.font = DynamicFontHelper.defaultHelper.DeviceFontHistoryPanel + detailTextLabel.font = DynamicFontHelper.defaultHelper.SmallSizeRegularWeightAS + } + + func layoutSubviews() { + guard let container = self.container else { + return + } + let height = container.frame.height + let textLeft = TwoLineCellUX.ImageSize + 2 * TwoLineCellUX.BorderViewMargin + let textLabelHeight = textLabel.intrinsicContentSize.height + let detailTextLabelHeight = detailTextLabel.intrinsicContentSize.height + var contentHeight = textLabelHeight + if detailTextLabelHeight > 0 { + contentHeight += detailTextLabelHeight + TwoLineCellUX.DetailTextTopMargin + } + + let textRightInset: CGFloat = hasRightBadge ? (TwoLineCellUX.BadgeSize + TwoLineCellUX.BadgeMargin) : 0 + + imageView.frame = CGRect(x: TwoLineCellUX.BorderViewMargin, y: (height - TwoLineCellUX.ImageSize) / 2, width: TwoLineCellUX.ImageSize, height: TwoLineCellUX.ImageSize) + textLabel.frame = CGRect(x: textLeft, y: (height - contentHeight) / 2, + width: container.frame.width - textLeft - TwoLineCellUX.BorderViewMargin - textRightInset, height: textLabelHeight) + detailTextLabel.frame = CGRect(x: textLeft, y: textLabel.frame.maxY + TwoLineCellUX.DetailTextTopMargin, + width: container.frame.width - textLeft - TwoLineCellUX.BorderViewMargin - textRightInset, height: detailTextLabelHeight) + } + + func setLines(_ text: String?, detailText: String?) { + if text?.isEmpty ?? true { + textLabel.text = detailText + detailTextLabel.text = nil + } else { + textLabel.text = text + detailTextLabel.text = detailText + } + } + + func mergeAccessibilityLabels(_ labels: [AnyObject?]?) { + let labels = labels ?? [textLabel, imageView, detailTextLabel] + + let label = labels.map({ (label: AnyObject?) -> NSAttributedString? in + var label = label + if let view = label as? UIView { + label = view.value(forKey: "accessibilityLabel") as (AnyObject?) + } + + if let attrString = label as? NSAttributedString { + return attrString + } else if let string = label as? String { + return NSAttributedString(string: string) + } else { + return nil + } + }).filter({ + $0 != nil + }).reduce(NSMutableAttributedString(string: ""), { + if $0.length > 0 { + $0.append(NSAttributedString(string: ", ")) + } + $0.append($1!) + return $0 + }) + + container?.isAccessibilityElement = true + container?.setValue(NSAttributedString(attributedString: label), forKey: "accessibilityLabel") + } +} diff --git a/mobile/ios/Client/Helpers/DynamicFontHelper.swift b/mobile/ios/Client/Helpers/DynamicFontHelper.swift new file mode 100644 index 0000000000..8fd304011f --- /dev/null +++ b/mobile/ios/Client/Helpers/DynamicFontHelper.swift @@ -0,0 +1,195 @@ +/* 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 + +let NotificationDynamicFontChanged = Notification.Name("NotificationDynamicFontChanged") + +private let iPadFactor: CGFloat = 1.06 +private let iPhoneFactor: CGFloat = 0.88 + +class DynamicFontHelper: NSObject { + + static var defaultHelper: DynamicFontHelper { + struct Singleton { + static let instance = DynamicFontHelper() + } + return Singleton.instance + } + + override init() { + defaultStandardFontSize = UIFontDescriptor.preferredFontDescriptor(withTextStyle: UIFontTextStyle.body).pointSize // 14pt -> 17pt -> 23pt + deviceFontSize = defaultStandardFontSize * (UIDevice.current.userInterfaceIdiom == .pad ? iPadFactor : iPhoneFactor) + defaultMediumFontSize = UIFontDescriptor.preferredFontDescriptor(withTextStyle: UIFontTextStyle.footnote).pointSize // 12pt -> 13pt -> 19pt + defaultSmallFontSize = UIFontDescriptor.preferredFontDescriptor(withTextStyle: UIFontTextStyle.caption2).pointSize // 11pt -> 11pt -> 17pt + + super.init() + } + + /** + * Starts monitoring the ContentSizeCategory chantes + */ + func startObserving() { + NotificationCenter.default.addObserver(self, selector: #selector(DynamicFontHelper.SELcontentSizeCategoryDidChange(_:)), name: NSNotification.Name.UIContentSizeCategoryDidChange, object: nil) + } + + deinit { + NotificationCenter.default.removeObserver(self) + } + + /** + * Device specific + */ + fileprivate var deviceFontSize: CGFloat + var DeviceFontSize: CGFloat { + return deviceFontSize + } + var DeviceFont: UIFont { + return UIFont.systemFont(ofSize: deviceFontSize, weight: UIFontWeightMedium) + } + var DeviceFontLight: UIFont { + return UIFont.systemFont(ofSize: deviceFontSize, weight: UIFontWeightLight) + } + var DeviceFontSmall: UIFont { + return UIFont.systemFont(ofSize: deviceFontSize - 1, weight: UIFontWeightMedium) + } + var DeviceFontSmallLight: UIFont { + return UIFont.systemFont(ofSize: deviceFontSize - 1, weight: UIFontWeightLight) + } + var DeviceFontSmallHistoryPanel: UIFont { + return UIFont.systemFont(ofSize: deviceFontSize - 3, weight: UIFontWeightLight) + } + var DeviceFontHistoryPanel: UIFont { + return UIFont.systemFont(ofSize: deviceFontSize) + } + var DeviceFontSmallBold: UIFont { + return UIFont.boldSystemFont(ofSize: deviceFontSize - 1) + } + var DeviceFontLarge: UIFont { + return UIFont.systemFont(ofSize: deviceFontSize + 3) + } + var DeviceFontMedium: UIFont { + return UIFont.systemFont(ofSize: deviceFontSize + 1) + } + var DeviceFontLargeBold: UIFont { + return UIFont.boldSystemFont(ofSize: deviceFontSize + 2) + } + var DeviceFontMediumBold: UIFont { + return UIFont.boldSystemFont(ofSize: deviceFontSize + 1) + } + var DeviceFontExtraLargeBold: UIFont { + return UIFont.boldSystemFont(ofSize: deviceFontSize + 4) + } + + /* + Activity Stream supports dynamic fonts up to a certain point. Large fonts dont work. + Max out the supported font size. + Small = 14, medium = 18, larger = 20 + */ + + var MediumSizeRegularWeightAS: UIFont { + let size = min(deviceFontSize, 18) + return UIFont.systemFont(ofSize: size) + } + + var LargeSizeRegularWeightAS: UIFont { + let size = min(deviceFontSize + 2, 20) + return UIFont.systemFont(ofSize: size) + } + + var MediumSizeHeavyWeightAS: UIFont { + let size = min(deviceFontSize + 2, 18) + return UIFont.systemFont(ofSize: size, weight: UIFontWeightHeavy) + } + var SmallSizeMediumWeightAS: UIFont { + let size = min(defaultSmallFontSize, 14) + return UIFont.systemFont(ofSize: size, weight: UIFontWeightMedium) + } + + var MediumSizeBoldFontAS: UIFont { + let size = min(deviceFontSize, 18) + return UIFont.boldSystemFont(ofSize: size) + } + + var SmallSizeRegularWeightAS: UIFont { + let size = min(defaultSmallFontSize, 14) + return UIFont.systemFont(ofSize: size) + } + + /** + * Small + */ + fileprivate var defaultSmallFontSize: CGFloat + var DefaultSmallFontSize: CGFloat { + return defaultSmallFontSize + } + var DefaultSmallFont: UIFont { + return UIFont.systemFont(ofSize: defaultSmallFontSize, weight: UIFontWeightRegular) + } + var DefaultSmallFontBold: UIFont { + return UIFont.boldSystemFont(ofSize: defaultSmallFontSize) + } + + /** + * Medium + */ + fileprivate var defaultMediumFontSize: CGFloat + var DefaultMediumFontSize: CGFloat { + return defaultMediumFontSize + } + var DefaultMediumFont: UIFont { + return UIFont.systemFont(ofSize: defaultMediumFontSize, weight: UIFontWeightRegular) + } + var DefaultMediumBoldFont: UIFont { + return UIFont.boldSystemFont(ofSize: defaultMediumFontSize) + } + + /** + * Standard + */ + fileprivate var defaultStandardFontSize: CGFloat + var DefaultStandardFontSize: CGFloat { + return defaultStandardFontSize + } + var DefaultStandardFont: UIFont { + return UIFont.systemFont(ofSize: defaultStandardFontSize, weight: UIFontWeightRegular) + } + var DefaultStandardFontBold: UIFont { + return UIFont.boldSystemFont(ofSize: defaultStandardFontSize) + } + + /** + * Reader mode + */ + var ReaderStandardFontSize: CGFloat { + return defaultStandardFontSize - 2 + } + var ReaderBigFontSize: CGFloat { + return defaultStandardFontSize + 5 + } + + /** + * Intro mode + */ + var IntroStandardFontSize: CGFloat { + return min(defaultStandardFontSize - 1, 16) + } + var IntroBigFontSize: CGFloat { + return min(defaultStandardFontSize + 1, 18) + } + + func refreshFonts() { + defaultStandardFontSize = UIFontDescriptor.preferredFontDescriptor(withTextStyle: UIFontTextStyle.body).pointSize + deviceFontSize = defaultStandardFontSize * (UIDevice.current.userInterfaceIdiom == .pad ? iPadFactor : iPhoneFactor) + defaultMediumFontSize = UIFontDescriptor.preferredFontDescriptor(withTextStyle: UIFontTextStyle.footnote).pointSize + defaultSmallFontSize = UIFontDescriptor.preferredFontDescriptor(withTextStyle: UIFontTextStyle.caption2).pointSize + } + + func SELcontentSizeCategoryDidChange(_ notification: Notification) { + refreshFonts() + let notification = Notification(name: NotificationDynamicFontChanged, object: nil) + NotificationCenter.default.post(notification) + } +} diff --git a/mobile/ios/Client/Helpers/FxALoginHelper.swift b/mobile/ios/Client/Helpers/FxALoginHelper.swift new file mode 100644 index 0000000000..347de6b95c --- /dev/null +++ b/mobile/ios/Client/Helpers/FxALoginHelper.swift @@ -0,0 +1,356 @@ +/* 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 Account +import Deferred +import Foundation +import Shared +import SwiftyJSON +import Sync +import UserNotifications +import XCGLogger + +private let applicationDidRequestUserNotificationPermissionPrefKey = "applicationDidRequestUserNotificationPermissionPrefKey" + +private let log = Logger.browserLogger + +private let verificationPollingInterval = DispatchTimeInterval.seconds(3) +private let verificationMaxRetries = 100 // Poll every 3 seconds for 5 minutes. + +protocol FxAPushLoginDelegate: class { + func accountLoginDidFail() + + func accountLoginDidSucceed(withFlags flags: FxALoginFlags) +} + +/// Small struct to keep together the immediately actionable flags that the UI is likely to immediately +/// following a successful login. This is not supposed to be a long lived object. +struct FxALoginFlags { + let pushEnabled: Bool + let verified: Bool +} + +enum PushNotificationError: MaybeErrorType { + case registrationFailed + case userDisallowed + case wrongOSVersion + + var description: String { + switch self { + case .registrationFailed: + return "The OS was unable to complete APNS registration" + case .userDisallowed: + return "User refused permission for notifications" + case .wrongOSVersion: + return "The version of iOS is not recent enough" + } + } +} + +/// This class manages the from successful login for FxAccounts to +/// asking the user for notification permissions, registering for +/// remote push notifications (APNS), then creating an account and +/// storing it in the profile. +class FxALoginHelper { + static var sharedInstance: FxALoginHelper = { + return FxALoginHelper() + }() + + weak var delegate: FxAPushLoginDelegate? + + fileprivate weak var profile: Profile? + + fileprivate var account: FirefoxAccount! + + fileprivate var accountVerified: Bool! + + fileprivate var pushClient: PushClient? { + guard let pushConfiguration = self.getPushConfiguration() ?? self.profile?.accountConfiguration.pushConfiguration, + let accountConfiguration = self.profile?.accountConfiguration else { + log.error("Push server endpoint could not be found") + return nil + } + + // Experimental mode needs: a) the scheme to be Fennec, and b) the accountConfiguration to be flipped in debug mode. + let experimentalMode = (pushConfiguration.label == .fennec && accountConfiguration.label == .latestDev) + return PushClient(endpointURL: pushConfiguration.endpointURL, experimentalMode: experimentalMode) + } + + fileprivate var apnsTokenDeferred: Deferred>! + + // This should be called when the application has started. + // This configures the helper for logging into Firefox Accounts, and + // if already logged in, checking if anything needs to be done in response + // to changing of user settings and push notifications. + func application(_ application: UIApplication, didLoadProfile profile: Profile) { + self.profile = profile + self.account = profile.getAccount() + + self.apnsTokenDeferred = Deferred() + + guard let account = self.account else { + // There's no account, no further action. + return loginDidFail() + } + + // accountVerified is needed by delegates. + accountVerified = account.actionNeeded != .needsVerification + + guard AppConstants.MOZ_FXA_PUSH else { + return loginDidSucceed() + } + + if let _ = account.pushRegistration { + // We have an account, and it's already registered for push notifications. + return loginDidSucceed() + } + + // Now: we have an account that does not have push notifications set up. + // however, we need to deal with cases of asking for permissions too frequently. + let asked = profile.prefs.boolForKey(applicationDidRequestUserNotificationPermissionPrefKey) ?? true + let permitted = application.currentUserNotificationSettings!.types != .none + + // If we've never asked(*), then we should probably ask. + // If we've asked already, then we should not ask again. + // TODO: add UI to tell the user to go flip the Setting app. + // (*) if we asked in a prior release, and the user was ok with it, then there is no harm asking again. + // If the user denied permission, or flipped permissions in the Settings app, then + // we'll bug them once, but this is probably unavoidable. + if asked && !permitted { + return loginDidSucceed() + } + + // By the time we reach here, we haven't registered for APNS + // Either we've never asked the user, or the user declined, then re-enabled + // the notification in the Settings app. + requestUserNotifications(application) + } + + // This is called when the user logs into a new FxA account. + // It manages the asking for user permission for notification and registration + // for APNS and WebPush notifications. + func application(_ application: UIApplication, didReceiveAccountJSON data: JSON) { + if data["keyFetchToken"].stringValue() == nil || data["unwrapBKey"].stringValue() == nil { + // The /settings endpoint sends a partial "login"; ignore it entirely. + log.error("Ignoring didSignIn with keyFetchToken or unwrapBKey missing.") + return self.loginDidFail() + } + + assert(profile != nil, "Profile should still exist and be loaded into this FxAPushLoginStateMachine") + + guard let profile = profile, + let account = FirefoxAccount.from(profile.accountConfiguration, andJSON: data) else { + return self.loginDidFail() + } + accountVerified = data["verified"].bool ?? false + self.account = account + + if AppConstants.MOZ_SHOW_FXA_AVATAR { + account.updateProfile() + } + + let leanplum = LeanPlumClient.shared + if leanplum.isLPEnabled() && leanplum.isFxAPrePushEnabled() { + // If Leanplum A/B push notification tests are enabled, defer to them for + // displaying the pre-push permission dialog. If user dismisses it, we will still have + // another chance to prompt them. Afterwards, Leanplum calls `apnsRegisterDidSucceed` or + // `apnsRegisterDidFail` to finish setting up Autopush. + return readyForSyncing() + } + + requestUserNotifications(application) + } + + func getDeviceToken(_ application: UIApplication) -> Deferred> { + self.requestUserNotifications(application) + return self.apnsTokenDeferred + } + + func requestUserNotifications(_ application: UIApplication) { + if let deferred = self.apnsTokenDeferred, deferred.isFilled, + let token = deferred.value.successValue { + // If we have an account, then it'll go through ahead and register + // with autopush here. + // If not we'll just bail. The Deferred will do the rest. + return self.apnsRegisterDidSucceed(token) + } + DispatchQueue.main.async { + self.requestUserNotificationsMainThreadOnly(application) + } + } + + fileprivate func requestUserNotificationsMainThreadOnly(_ application: UIApplication) { + assert(Thread.isMainThread, "requestAuthorization should be run on the main thread") + let center = UNUserNotificationCenter.current() + return center.requestAuthorization(options: [.alert, .badge, .sound]) { (granted, error) in + guard error == nil else { + return self.application(application, canDisplayUserNotifications: false) + } + self.application(application, canDisplayUserNotifications: granted) + } + } + + // This is necessarily called from the AppDelegate. + // Once we have permission from the user to display notifications, we should + // try and register for APNS. If not, then start syncing. + func application(_ application: UIApplication, didRegisterUserNotificationSettings notificationSettings: UIUserNotificationSettings) { + let types = notificationSettings.types + let allowed = types != .none && types.rawValue != 0 + self.application(application, canDisplayUserNotifications: allowed) + } + + func application(_ application: UIApplication, canDisplayUserNotifications allowed: Bool) { + guard allowed else { + apnsTokenDeferred?.fillIfUnfilled(Maybe.failure(PushNotificationError.userDisallowed)) + return readyForSyncing() + } + + // Record that we have asked the user, and they have given an answer. + profile?.prefs.setBool(true, forKey: applicationDidRequestUserNotificationPermissionPrefKey) + + if AppConstants.MOZ_FXA_PUSH { + DispatchQueue.main.async { + application.registerForRemoteNotifications() + } + } else { + readyForSyncing() + } + } + + func getPushConfiguration() -> PushConfiguration? { + let label = PushConfigurationLabel(rawValue: AppConstants.scheme) + return label?.toConfiguration() + } + + func apnsRegisterDidSucceed(_ deviceToken: Data) { + let apnsToken = deviceToken.hexEncodedString + self.apnsTokenDeferred?.fillIfUnfilled(Maybe(success: apnsToken)) + self.apnsRegisterDidSucceed(apnsToken) + } + + fileprivate func apnsRegisterDidSucceed(_ apnsToken: String) { + guard self.account != nil else { + // If we aren't logged in to FxA at this point + // we should bail. + return loginDidFail() + } + + guard let pushClient = self.pushClient else { + return pushRegistrationDidFail() + } + + if let pushRegistration = account.pushRegistration { + // Currently, we don't support routine changing of push subscriptions + // then we can assume that if we've already registered with the + // push server, then we don't need to do it again. + _ = pushClient.updateUAID(apnsToken, withRegistration: pushRegistration) + return + } + + pushClient.register(apnsToken).upon { res in + guard let pushRegistration = res.successValue else { + return self.pushRegistrationDidFail() + } + return self.pushRegistrationDidSucceed(apnsToken: apnsToken, pushRegistration: pushRegistration) + } + } + + func apnsRegisterDidFail() { + self.apnsTokenDeferred?.fillIfUnfilled(Maybe(failure: PushNotificationError.registrationFailed)) + readyForSyncing() + } + + fileprivate func pushRegistrationDidSucceed(apnsToken: String, pushRegistration: PushRegistration) { + account.pushRegistration = pushRegistration + readyForSyncing() + } + + fileprivate func pushRegistrationDidFail() { + readyForSyncing() + } + + func readyForSyncing() { + guard let profile = self.profile, let account = self.account else { + return loginDidFail() + } + + profile.setAccount(account) + + awaitVerification() + loginDidSucceed() + } + + fileprivate func awaitVerification(_ attemptsLeft: Int = verificationMaxRetries) { + guard let account = account, + let profile = profile else { + return + } + + if attemptsLeft == 0 { + return + } + + // The only way we can tell if the account has been verified is to + // start a sync. If it works, then yay, + account.advance().upon { state in + guard state.actionNeeded == .needsVerification else { + // Verification has occurred remotely, and we can proceed. + // The state machine will have told any listening UIs that + // we're done. + return self.performVerifiedSync(profile, account: account) + } + + let queue = DispatchQueue.global(qos: DispatchQoS.background.qosClass) + queue.asyncAfter(deadline: DispatchTime.now() + verificationPollingInterval) { + self.awaitVerification(attemptsLeft - 1) + } + } + } + + fileprivate func loginDidSucceed() { + let flags = FxALoginFlags(pushEnabled: account?.pushRegistration != nil, verified: accountVerified) + delegate?.accountLoginDidSucceed(withFlags: flags) + } + + fileprivate func loginDidFail() { + delegate?.accountLoginDidFail() + } + + func performVerifiedSync(_ profile: Profile, account: FirefoxAccount) { + profile.syncManager.syncEverything(why: .didLogin) + } +} + +extension FxALoginHelper { + func applicationDidDisconnect(_ application: UIApplication) { + // According to https://developer.apple.com/documentation/uikit/uiapplication/1623093-unregisterforremotenotifications + // we should be calling: + application.unregisterForRemoteNotifications() + // However, https://forums.developer.apple.com/message/179264#179264 advises against it, suggesting there is + // a 24h period after unregistering where re-registering fails. This doesn't seem to be the case (for me) + // but this may be useful to know if QA/user-testing find this a problem. + + // Whatever, we should unregister from the autopush server. That means we definitely won't be getting any + // messages. + if let pushRegistration = self.account.pushRegistration, + let pushClient = self.pushClient { + _ = pushClient.unregister(pushRegistration) + } + + // TODO: fix Bug 1168690, to tell Sync to delete this client and its tabs. + // i.e. upload a {deleted: true} client record. + + // Tell FxA we're no longer attached. + self.account.destroyDevice() + + // Cleanup the database. + self.profile?.removeAccount() + + // Cleanup the FxALoginHelper. + self.account = nil + self.accountVerified = nil + + self.profile?.prefs.removeObjectForKey(PendingAccountDisconnectedKey) + } +} diff --git a/mobile/ios/Client/Helpers/MenuHelper.swift b/mobile/ios/Client/Helpers/MenuHelper.swift new file mode 100644 index 0000000000..5609db2037 --- /dev/null +++ b/mobile/ios/Client/Helpers/MenuHelper.swift @@ -0,0 +1,47 @@ +/* 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 + +@objc public protocol MenuHelperInterface { + @objc optional func menuHelperCopy() + @objc optional func menuHelperOpenAndFill() + @objc optional func menuHelperReveal() + @objc optional func menuHelperSecure() + @objc optional func menuHelperFindInPage() +} + +open class MenuHelper: NSObject { + open static let SelectorCopy: Selector = #selector(MenuHelperInterface.menuHelperCopy) + open static let SelectorHide: Selector = #selector(MenuHelperInterface.menuHelperSecure) + open static let SelectorOpenAndFill: Selector = #selector(MenuHelperInterface.menuHelperOpenAndFill) + open static let SelectorReveal: Selector = #selector(MenuHelperInterface.menuHelperReveal) + open static let SelectorFindInPage: Selector = #selector(MenuHelperInterface.menuHelperFindInPage) + + open class var defaultHelper: MenuHelper { + struct Singleton { + static let instance = MenuHelper() + } + return Singleton.instance + } + + open func setItems() { + let revealPasswordTitle = NSLocalizedString("Reveal", tableName: "LoginManager", comment: "Reveal password text selection menu item") + let revealPasswordItem = UIMenuItem(title: revealPasswordTitle, action: MenuHelper.SelectorReveal) + + let hidePasswordTitle = NSLocalizedString("Hide", tableName: "LoginManager", comment: "Hide password text selection menu item") + let hidePasswordItem = UIMenuItem(title: hidePasswordTitle, action: MenuHelper.SelectorHide) + + let copyTitle = NSLocalizedString("Copy", tableName: "LoginManager", comment: "Copy password text selection menu item") + let copyItem = UIMenuItem(title: copyTitle, action: MenuHelper.SelectorCopy) + + let openAndFillTitle = NSLocalizedString("Open & Fill", tableName: "LoginManager", comment: "Open and Fill website text selection menu item") + let openAndFillItem = UIMenuItem(title: openAndFillTitle, action: MenuHelper.SelectorOpenAndFill) + + let findInPageTitle = NSLocalizedString("Find in Page", tableName: "FindInPage", comment: "Text selection menu item") + let findInPageItem = UIMenuItem(title: findInPageTitle, action: MenuHelper.SelectorFindInPage) + + UIMenuController.shared.menuItems = [copyItem, revealPasswordItem, hidePasswordItem, openAndFillItem, findInPageItem] + } +} diff --git a/mobile/ios/Client/Helpers/TabEventHandler.swift b/mobile/ios/Client/Helpers/TabEventHandler.swift new file mode 100644 index 0000000000..89099ed83c --- /dev/null +++ b/mobile/ios/Client/Helpers/TabEventHandler.swift @@ -0,0 +1,178 @@ +/* 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 Storage + +/** + * A handler can be a plain old swift object. It does not need to extend any + * other object, but can. + * + * Handlers should register for tab events with the `registerFor` method, and + * cleanup with the `unregister` method. + * + * ``` + * class HandoffHandler { + * var tabObservers: TabObservers! + * + * init() { + * tabObservers = registerFor(.didLoadFavicon, .didLoadPageMetadata) + * } + * + * deinit { + * unregister(tabObservers) + * } + * } + * ``` + * + * Handlers can implement any or all `TabEventHandler` methods. If you + * implement a method, you should probably `registerFor` the event above. + * + * ``` + * extension HandoffHandler: TabEventHandler { + * func tab(_ tab: Tab, didLoadPageMetadata metadata: PageMetadata) { + * print("\(tab) has \(pageMetadata)") + * } + * + * func tab(_ tab: Tab, didLoadFavicon favicon: Favicon) { + * print("\(tab) has \(favicon)") + * } + * } + * ``` + * + * Tab events should probably be only posted from one place, to avoid cycles. + * + * ``` + * TabEvent.post(.didLoadPageMetadata(aPageMetadata), for: tab) + * ``` + * + * In this manner, we are able to use the notification center and have type safety. + * + */ +// As we want more events we add more here. +// Each event needs: +// 1. a method in the TabEventHandler. +// 2. a default implementation of the method – so not everyone needs to implement it +// 3. a TabEventLabel, which is needed for registration +// 4. a TabEvent, with whatever parameters are needed. +// i) a case to map the event to the event label (var label) +// ii) a case to map the event to the event handler (func handle:with:) +protocol TabEventHandler { + func tab(_ tab: Tab, didChangeURL url: URL) + func tab(_ tab: Tab, didLoadPageMetadata metadata: PageMetadata) + func tab(_ tab: Tab, didLoadFavicon favicon: Favicon?, with: Data?) + func tabDidGainFocus(_ tab: Tab) + func tabDidLoseFocus(_ tab: Tab) + func tabDidClose(_ tab: Tab) +} + +// Provide default implmentations, because we don't want to litter the code with +// empty methods, and `@objc optional` doesn't really work very well. +extension TabEventHandler { + func tab(_ tab: Tab, didChangeURL url: URL) {} + func tab(_ tab: Tab, didLoadPageMetadata metadata: PageMetadata) {} + func tab(_ tab: Tab, didLoadFavicon favicon: Favicon?, with: Data?) {} + func tabDidGainFocus(_ tab: Tab) {} + func tabDidLoseFocus(_ tab: Tab) {} + func tabDidClose(_ tab: Tab) {} +} + +enum TabEventLabel: String { + case didChangeURL + case didLoadPageMetadata + case didLoadFavicon + case didGainFocus + case didLoseFocus + case didClose +} + +enum TabEvent { + case didChangeURL(URL) + case didLoadPageMetadata(PageMetadata) + case didLoadFavicon(Favicon?, with: Data?) + case didGainFocus + case didLoseFocus + case didClose + + var label: TabEventLabel { + switch self { + case .didChangeURL: + return .didChangeURL + case .didLoadPageMetadata: + return .didLoadPageMetadata + case .didLoadFavicon: + return .didLoadFavicon + case .didGainFocus: + return .didGainFocus + case .didLoseFocus: + return .didLoseFocus + case .didClose: + return .didClose + } + } + + func handle(_ tab: Tab, with handler: TabEventHandler) { + switch self { + case .didChangeURL(let url): + handler.tab(tab, didChangeURL: url) + case .didLoadPageMetadata(let metadata): + handler.tab(tab, didLoadPageMetadata: metadata) + case .didLoadFavicon(let favicon, let data): + handler.tab(tab, didLoadFavicon: favicon, with: data) + case .didGainFocus: + handler.tabDidGainFocus(tab) + case .didLoseFocus: + handler.tabDidLoseFocus(tab) + case .didClose: + handler.tabDidClose(tab) + } + } +} + +// Hide some of the machinery away from the boiler plate above. +//////////////////////////////////////////////////////////////////////////////////////// +extension TabEventLabel { + var name: Notification.Name { + return Notification.Name(self.rawValue) + } +} + +extension TabEvent { + func notification(for tab: Any) -> Notification { + return Notification(name: label.name, object: tab, userInfo: ["payload": self]) + } + + /// Use this method to post notifications to any concerned listeners. + static func post(_ event: TabEvent, for tab: Any) { + center.post(event.notification(for: tab)) + } +} + +// These methods are used by TabEventHandler implementers. +// Their usage remains consistent, even as we add more event types and handler methods. +//////////////////////////////////////////////////////////////////////////////////////// +private let center = NotificationCenter() + +typealias TabObservers = [NSObjectProtocol] +extension TabEventHandler { + /// Implementations of handles should use this method to register for events. + /// `TabObservers` should be preserved for unregistering later. + func registerFor(_ tabEvents: TabEventLabel..., queue: OperationQueue? = nil) -> TabObservers { + return tabEvents.map { eventType in + center.addObserver(forName: eventType.name, object: nil, queue: queue) { notification in + guard let tab = notification.object as? Tab, + let event = notification.userInfo?["payload"] as? TabEvent else { + return + } + event.handle(tab, with: self) + } + } + } + + func unregister(_ observers: TabObservers) { + observers.forEach { observer in + center.removeObserver(observer) + } + } +} diff --git a/mobile/ios/Client/Helpers/UserActivityHandler.swift b/mobile/ios/Client/Helpers/UserActivityHandler.swift new file mode 100644 index 0000000000..dd052dc3d6 --- /dev/null +++ b/mobile/ios/Client/Helpers/UserActivityHandler.swift @@ -0,0 +1,71 @@ +/* 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 CoreSpotlight +import MobileCoreServices +import WebKit + +private let browsingActivityType: String = "org.mozilla.ios.firefox.browsing" + +private let searchableIndex = CSSearchableIndex(name: "firefox") + +class UserActivityHandler { + private var tabObservers: TabObservers! + + init() { + self.tabObservers = registerFor( + .didLoseFocus, + .didGainFocus, + .didChangeURL, + // .didLoadMetadata // TODO: Bug 1390294 + // .didLoadFavicon, // TODO: Bug 1390294 + .didClose, + queue: .main) + } + + deinit { + unregister(tabObservers) + } + + class func clearSearchIndex(completionHandler: ((Error?) -> Void)? = nil) { + searchableIndex.deleteAllSearchableItems(completionHandler: completionHandler) + } +} + +extension UserActivityHandler: TabEventHandler { + func tabDidGainFocus(_ tab: Tab) { + tab.userActivity?.becomeCurrent() + } + + func tabDidLoseFocus(_ tab: Tab) { + tab.userActivity?.resignCurrent() + } + + func tab(_ tab: Tab, didChangeURL url: URL) { + guard url.isWebPage(includeDataURIs: false), !url.isLocal else { + tab.userActivity?.resignCurrent() + tab.userActivity = nil + return + } + + tab.userActivity?.invalidate() + + let userActivity = NSUserActivity(activityType: browsingActivityType) + userActivity.webpageURL = url + userActivity.becomeCurrent() + + tab.userActivity = userActivity + } + + func tabDidClose(_ tab: Tab) { + guard let userActivity = tab.userActivity else { + return + } + tab.userActivity = nil + userActivity.invalidate() + } +} diff --git a/mobile/ios/Client/Info.plist b/mobile/ios/Client/Info.plist new file mode 100644 index 0000000000..a4b4c098e6 --- /dev/null +++ b/mobile/ios/Client/Info.plist @@ -0,0 +1,185 @@ + + + + + AdjustAppToken + $(ADJUST_APP_TOKEN) + AdjustEnvironment + $(ADJUST_ENVIRONMENT) + MozDevelopmentTeam + $(DEVELOPMENT_TEAM) + LeanplumAppId + + LeanplumProductionKey + + LeanplumDevelopmentKey + + AppIdentifierPrefix + $(APP_IDENTIFIER_PREFIX) + PocketEnvironmentAPIKey + $(POCKET_API_KEY) + MozWhatsNewTopic + whats-new-ios-10 + CFBundleDevelopmentRegion + en + CFBundleDisplayName + $(MOZ_BUNDLE_DISPLAY_NAME) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(MOZ_PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + 10.6 + CFBundleSignature + ???? + CFBundleURLTypes + + + CFBundleURLName + org.mozilla.Client + CFBundleURLSchemes + + firefox + $(MOZ_INTERNAL_URL_SCHEME) + + + + CFBundleVersion + 1 + ITSAppUsesNonExemptEncryption + + LSApplicationQueriesSchemes + + pocket + firefox-focus + firefox-klar + ymail + ms-outlook + airmail + italiaonline-mailto + mailru-mailto + mymail-mailto + readdle-spark + itms-books + org-appextension-feature-password-management + googlegmail + inbox-gmail + + LSRequiresIPhoneOS + + NSAppTransportSecurity + + NSAllowsArbitraryLoads + + + NSLocationWhenInUseUsageDescription + Websites you visit may request your location. + NSPhotoLibraryUsageDescription + This lets you save and upload photos. + NSCameraUsageDescription + This lets you take and upload photos. + NSMicrophoneUsageDescription + This lets you take and upload videos. + NSPhotoLibraryAddUsageDescription + This lets you save photos. + NSFaceIDUsageDescription + Firefox requires Face ID to access your saved logins. + NSUserActivityTypes + + org.mozilla.ios.firefox.browsing + + UIAppFonts + + CharisSILB.ttf + CharisSILBI.ttf + CharisSILI.ttf + CharisSILR.ttf + FiraSans-Bold.ttf + FiraSans-BoldItalic.ttf + FiraSans-Book.ttf + FiraSans-Italic.ttf + FiraSans-Light.ttf + FiraSans-Medium.ttf + FiraSans-Regular.ttf + FiraSans-SemiBold.ttf + FiraSans-UltraLight.ttf + + UIApplicationShortcutItems + + + UIApplicationShortcutItemIconFile + quick_action_new_tab + UIApplicationShortcutItemTitle + ShortcutItemTitleNewTab + UIApplicationShortcutItemType + $(PRODUCT_BUNDLE_IDENTIFIER).NewTab + + + UIApplicationShortcutItemIconFile + quick_action_new_private_tab + UIApplicationShortcutItemTitle + ShortcutItemTitleNewPrivateTab + UIApplicationShortcutItemType + $(PRODUCT_BUNDLE_IDENTIFIER).NewPrivateTab + + + UIApplicationShortcutItemIconFile + menu-ScanQRCode + UIApplicationShortcutItemTitle + ShortcutItemTitleQRCode + UIApplicationShortcutItemType + $(PRODUCT_BUNDLE_IDENTIFIER).QRCode + + + UIBackgroundModes + + audio + remote-notification + + UILaunchStoryboardName + LaunchScreen + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + SentryDSN + $(SENTRY_DSN) + UIViewControllerBasedStatusBarAppearance + + UTImportedTypeDeclarations + + + UTTypeConformsTo + + public.url + + UTTypeDescription + 1Password Fill Browser Action + UTTypeIdentifier + org.appextension.fill-browser-action + UTTypeSize64IconFile + + + + + diff --git a/mobile/ios/Client/MailSchemes.plist b/mobile/ios/Client/MailSchemes.plist new file mode 100644 index 0000000000..0bcc7aaeb8 --- /dev/null +++ b/mobile/ios/Client/MailSchemes.plist @@ -0,0 +1,60 @@ + + + + + + name + Mail + scheme + mailto: + + + scheme + ms-outlook:// + name + Outlook + + + name + Airmail + scheme + airmail:// + + + name + Mail.Ru + scheme + mailru-mailto:// + + + name + myMail + scheme + mymail-mailto:// + + + name + Spark + scheme + readdle-spark:// + + + name + YMail! + scheme + ymail:// + + + name + Gmail + scheme + googlegmail:// + + + name + Inbox by Gmail + scheme + inbox-gmail:// + + + diff --git a/mobile/ios/Client/TabEventHandlers.swift b/mobile/ios/Client/TabEventHandlers.swift new file mode 100644 index 0000000000..76ce60fcc6 --- /dev/null +++ b/mobile/ios/Client/TabEventHandlers.swift @@ -0,0 +1,13 @@ +/* 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 + +class TabEventHandlers { + static let `default` = TabEventHandlers() + + let handlers: [TabEventHandler] = [ + UserActivityHandler() + ] +} diff --git a/mobile/ios/Client/Telemetry/ActivityStreamTopics.swift b/mobile/ios/Client/Telemetry/ActivityStreamTopics.swift new file mode 100644 index 0000000000..ac13f976b2 --- /dev/null +++ b/mobile/ios/Client/Telemetry/ActivityStreamTopics.swift @@ -0,0 +1,52 @@ +/* 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 JSONSchema +import SyncTelemetry + +// MARK: Activity Stream +extension PingCentreTopic { + + static var ActivityStreamEvents: PingCentreTopic { + let schema = Schema([ + "type": "object", + "properties": [ + // AS-Specific properties + "event": ["type": "string"], + "page": ["type": "string"], + "source": ["type": "string"], + "action_position": ["type": "number"], + "share_provider": ["type": "string"], + + // Application metadata + "app_version": ["type": "string"], + "build": ["type": "string"], + "locale": ["type": "string"], + "release_channel": ["type": "string"] + ], + "required": ["app_version", "build", "locale", "event", "release_channel"] + ]) + + return PingCentreTopic(name: "activity-stream-mobile-events", schema: schema) + } + + static var ActivityStreamSessions: PingCentreTopic { + let schema = Schema([ + "type": "object", + "properties": [ + // AS-Specific properties + "session_duration": ["type": "number"], + + // Application metadata + "app_version": ["type": "string"], + "build": ["type": "string"], + "locale": ["type": "string"], + "release_channel": ["type": "string"] + ], + "required": ["app_version", "build", "locale", "session_duration", "release_channel"] + ]) + + return PingCentreTopic(name: "activity-stream-mobile-sessions", schema: schema) + } +} diff --git a/mobile/ios/Client/Telemetry/UnifiedTelemetry.swift b/mobile/ios/Client/Telemetry/UnifiedTelemetry.swift new file mode 100644 index 0000000000..5f5e9f89ff --- /dev/null +++ b/mobile/ios/Client/Telemetry/UnifiedTelemetry.swift @@ -0,0 +1,119 @@ +/* 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 Shared +import Telemetry + +// +// 'Unified Telemetry' is the name for Mozilla's telemetry system +// +class UnifiedTelemetry { + init(profile: Profile) { + NotificationCenter.default.addObserver(self, selector: #selector(uploadError(notification:)), name: Telemetry.notificationReportError, object: nil) + + let telemetryConfig = Telemetry.default.configuration + telemetryConfig.appName = "Fennec" + telemetryConfig.userDefaultsSuiteName = AppInfo.sharedContainerIdentifier + telemetryConfig.dataDirectory = .documentDirectory + telemetryConfig.updateChannel = AppConstants.BuildChannel.rawValue + let sendUsageData = profile.prefs.boolForKey(AppConstants.PrefSendUsageData) ?? true + telemetryConfig.isCollectionEnabled = sendUsageData + telemetryConfig.isUploadEnabled = sendUsageData + + telemetryConfig.measureUserDefaultsSetting(forKey: "profile.blockPopups", withDefaultValue: true) + telemetryConfig.measureUserDefaultsSetting(forKey: "profile.saveLogins", withDefaultValue: true) + telemetryConfig.measureUserDefaultsSetting(forKey: "profile.showClipboardBar", withDefaultValue: false) + telemetryConfig.measureUserDefaultsSetting(forKey: "profile.settings.closePrivateTabs", withDefaultValue: false) + telemetryConfig.measureUserDefaultsSetting(forKey: "profile.ASPocketStoriesVisible", withDefaultValue: true) + telemetryConfig.measureUserDefaultsSetting(forKey: "profile.ASBookmarkHighlightsVisible", withDefaultValue: true) + telemetryConfig.measureUserDefaultsSetting(forKey: "profile.ASRecentHighlightsVisible", withDefaultValue: true) + telemetryConfig.measureUserDefaultsSetting(forKey: "profile.prefkey.trackingprotection.enabled", withDefaultValue: "onInPrivateBrowsing") + telemetryConfig.measureUserDefaultsSetting(forKey: "profile.prefkey.trackingprotection.strength", withDefaultValue: "basic") + + let prefs = profile.prefs + Telemetry.default.beforeSerializePing(pingType: CorePingBuilder.PingType) { (inputDict) -> [String: Any?] in + var outputDict = inputDict // make a mutable copy + if let newTabChoice = prefs.stringForKey(NewTabAccessors.PrefKey) { + outputDict["defaultNewTabExperience"] = newTabChoice as AnyObject? + } + if let chosenEmailClient = prefs.stringForKey(PrefsKeys.KeyMailToOption) { + outputDict["defaultMailClient"] = chosenEmailClient as AnyObject? + } + return outputDict + } + + Telemetry.default.beforeSerializePing(pingType: MobileEventPingBuilder.PingType) { (inputDict) -> [String : Any?] in + var outputDict = inputDict + var settings: [String : Any?] = inputDict["settings"] as? [String : Any?] ?? [:] + + let searchEngines = SearchEngines(prefs: profile.prefs, files: profile.files) + settings["defaultSearchEngine"] = searchEngines.defaultEngine.engineID ?? "custom" + + outputDict["settings"] = settings + return outputDict + } + + Telemetry.default.add(pingBuilderType: CorePingBuilder.self) + Telemetry.default.add(pingBuilderType: MobileEventPingBuilder.self) + } + + @objc func uploadError(notification: NSNotification) { + guard !DeviceInfo.isSimulator(), let error = notification.userInfo?["error"] as? NSError else { return } + Sentry.shared.send(message: "Upload Error", tag: SentryTag.unifiedTelemetry, severity: .info, description: error.debugDescription) + } +} + +// Enums for Event telemetry. +extension UnifiedTelemetry { + public enum EventCategory: String { + case action = "action" + } + + public enum EventMethod: String { + case add = "add" + case background = "background" + case change = "change" + case delete = "delete" + case foreground = "foreground" + case open = "open" + case scan = "scan" + case tap = "tap" + case view = "view" + } + + public enum EventObject: String { + case app = "app" + case bookmark = "bookmark" + case bookmarksPanel = "bookmarks-panel" + case qrCodeText = "qr-code-text" + case qrCodeURL = "qr-code-url" + case readerModeCloseButton = "reader-mode-close-button" + case readerModeOpenButton = "reader-mode-open-button" + case readingListItem = "reading-list-item" + case setting = "setting" + } + + public enum EventValue: String { + case activityStream = "activity-stream" + case appMenu = "app-menu" + case awesomebarResults = "awesomebar-results" + case bookmarksPanel = "bookmarks-panel" + case homePanelTabButton = "home-panel-tab-button" + case markAsRead = "mark-as-read" + case markAsUnread = "mark-as-unread" + case pageActionMenu = "page-action-menu" + case readerModeToolbar = "reader-mode-toolbar" + case readingListPanel = "reading-list-panel" + case shareExtension = "share-extension" + case shareMenu = "share-menu" + } + + public static func recordEvent(category: EventCategory, method: EventMethod, object: EventObject, value: EventValue, extras: [String : Any?]? = nil) { + Telemetry.default.recordEvent(category: category.rawValue, method: method.rawValue, object: object.rawValue, value: value.rawValue, extras: extras) + } + + public static func recordEvent(category: EventCategory, method: EventMethod, object: EventObject, value: String? = nil, extras: [String : Any?]? = nil) { + Telemetry.default.recordEvent(category: category.rawValue, method: method.rawValue, object: object.rawValue, value: value, extras: extras) + } +} diff --git a/mobile/ios/Client/UIScrollViewSwizzled.swift b/mobile/ios/Client/UIScrollViewSwizzled.swift new file mode 100644 index 0000000000..e34977ef72 --- /dev/null +++ b/mobile/ios/Client/UIScrollViewSwizzled.swift @@ -0,0 +1,44 @@ +/* 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 Shared + +// Workaround for bug 1417152, whereby NaN bounds are being set on the scrollview when viewing PDFs in the web view. +// Is fixed in WebKit, remove this file when the fix arrives in iOS release. + +private let swizzling: (UIScrollView.Type) -> Void = { obj in + let originalSelector = #selector(setter: UIView.bounds) + let swizzledSelector = #selector(obj.swizzle_setBounds(bounds:)) + let originalMethod = class_getInstanceMethod(obj, originalSelector) + let swizzledMethod = class_getInstanceMethod(obj, swizzledSelector) + method_exchangeImplementations(originalMethod, swizzledMethod) +} + +extension UIScrollView { + open override class func initialize() { + // make sure this isn't a subclass + guard self == UIScrollView.self else { + return + } + swizzling(self) + } + + func swizzle_setBounds(bounds: CGRect) { + [bounds.origin.x, bounds.origin.y, bounds.size.width, bounds.size.height].forEach() { val in + if val.isNaN || val.isInfinite { + Sentry.shared.send(message: "Bad scrollview bounds detected [infinite/nan].") + return + } + } + + [bounds.size.width, bounds.size.height].forEach() { val in + if val < 0 { + Sentry.shared.send(message: "Bad scrollview bounds detected [negative size].") + return + } + } + + self.swizzle_setBounds(bounds: bounds) + } +} diff --git a/mobile/ios/Client/Utils/FaviconFetcher.swift b/mobile/ios/Client/Utils/FaviconFetcher.swift new file mode 100644 index 0000000000..49dd9ead2e --- /dev/null +++ b/mobile/ios/Client/Utils/FaviconFetcher.swift @@ -0,0 +1,322 @@ +/* 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 Storage +import Shared +import Alamofire +import XCGLogger +import Deferred +import SDWebImage +import Fuzi +import SwiftyJSON + +private let log = Logger.browserLogger +private let queue = DispatchQueue(label: "FaviconFetcher", attributes: DispatchQueue.Attributes.concurrent) + +class FaviconFetcherErrorType: MaybeErrorType { + let description: String + init(description: String) { + self.description = description + } +} + +/* A helper class to find the favicon associated with a URL. + * This will load the page and parse any icons it finds out of it. + * If that fails, it will attempt to find a favicon.ico in the root host domain. + */ +open class FaviconFetcher: NSObject, XMLParserDelegate { + open static var userAgent: String = "" + static let ExpirationTime = TimeInterval(60*60*24*7) // Only check for icons once a week + fileprivate static var characterToFaviconCache = [String: UIImage]() + static var defaultFavicon: UIImage = { + return UIImage(named: "defaultFavicon")! + }() + + static var colors: [String: UIColor] = [:] //An in-Memory data store that stores background colors domains. Stored using url.baseDomain. + + // Sites can be accessed via their baseDomain. + static var defaultIcons: [String: (color: UIColor, url: String)] = { + return FaviconFetcher.getDefaultIcons() + }() + + static let multiRegionDomains = ["craigslist", "google", "amazon"] + + class func getDefaultIconForURL(url: URL) -> (color: UIColor, url: String)? { + + // Problem: Sites like amazon exist with .ca/.de and many other tlds. + // Solution: They are stored in the default icons list as "amazon" instead of "amazon.com" this allows us to have favicons for every tld." + // Here, If the site is in the multiRegionDomain array look it up via its second level domain (amazon) instead of its baseDomain (amazon.com) + let hostName = url.hostSLD + if multiRegionDomains.contains(hostName), let icon = defaultIcons[hostName] { + return icon + } + if let name = url.baseDomain, let icon = defaultIcons[name] { + return icon + } + return nil + } + + class func getForURL(_ url: URL, profile: Profile) -> Deferred> { + let f = FaviconFetcher() + return f.loadFavicons(url, profile: profile) + } + + fileprivate func loadFavicons(_ url: URL, profile: Profile, oldIcons: [Favicon] = [Favicon]()) -> Deferred> { + if isIgnoredURL(url) { + return deferMaybe(FaviconFetcherErrorType(description: "Not fetching ignored URL to find favicons.")) + } + + let deferred = Deferred>() + + var oldIcons: [Favicon] = oldIcons + + queue.async { _ in + self.parseHTMLForFavicons(url).bind({ (result: Maybe<[Favicon]>) -> Deferred<[Maybe]> in + var deferreds = [Deferred>]() + if let icons = result.successValue { + deferreds = icons.map { self.getFavicon(url, icon: $0, profile: profile) } + } + return all(deferreds) + }).bind({ (results: [Maybe]) -> Deferred> in + for result in results { + if let icon = result.successValue { + oldIcons.append(icon) + } + } + + oldIcons = oldIcons.sorted { + return $0.width! > $1.width! + } + + return deferMaybe(oldIcons) + }).upon({ (result: Maybe<[Favicon]>) in + deferred.fill(result) + return + }) + } + + return deferred + } + + lazy fileprivate var alamofire: SessionManager = { + let configuration = URLSessionConfiguration.default + var defaultHeaders = SessionManager.default.session.configuration.httpAdditionalHeaders ?? [:] + defaultHeaders["User-Agent"] = userAgent + configuration.httpAdditionalHeaders = defaultHeaders + configuration.timeoutIntervalForRequest = 5 + return SessionManager(configuration: configuration) + }() + + fileprivate func fetchDataForURL(_ url: URL) -> Deferred> { + let deferred = Deferred>() + alamofire.request(url).response { response in + // Don't cancel requests just because our Manager is deallocated. + withExtendedLifetime(self.alamofire) { + if response.error == nil { + if let data = response.data { + deferred.fill(Maybe(success: data)) + return + } + } + let errorDescription = (response.error as NSError?)?.description ?? "No content." + deferred.fill(Maybe(failure: FaviconFetcherErrorType(description: errorDescription))) + } + } + return deferred + } + +///Users/fpatel/Documents/firefox-ios/Client/Utils/FaviconFetcher.swift:117:77: Cannot convert value of type 'String' to expected argument type 'String.Index' (aka 'String.CharacterView.Index') + // Loads and parses an html document and tries to find any known favicon-type tags for the page + fileprivate func parseHTMLForFavicons(_ url: URL) -> Deferred> { + return fetchDataForURL(url).bind({ result -> Deferred> in + var icons = [Favicon]() + guard let data = result.successValue, result.isSuccess, + let root = try? HTMLDocument(data: data as Data) else { + return deferMaybe([]) + } + var reloadUrl: URL? = nil + for meta in root.xpath("//head/meta") { + if let refresh = meta["http-equiv"], refresh == "Refresh", + let content = meta["content"], + let index = content.range(of: "URL="), + let url = NSURL(string: content.substring(from: index.upperBound)) { + reloadUrl = url as URL + } + } + + if let url = reloadUrl { + return self.parseHTMLForFavicons(url) + } + + var bestType = IconType.noneFound + for link in root.xpath("//head//link[contains(@rel, 'icon')]") { + var iconType: IconType? = nil + if let rel = link["rel"] { + switch rel { + case "shortcut icon": + iconType = .icon + case "icon": + iconType = .icon + case "apple-touch-icon": + iconType = .appleIcon + case "apple-touch-icon-precomposed": + iconType = .appleIconPrecomposed + default: + iconType = nil + } + } + + guard let href = link["href"], iconType != nil else { + continue //Skip the rest of the loop. But don't stop the loop + } + + if href.endsWith(".ico") { + iconType = .guess + } + + if let type = iconType, !bestType.isPreferredTo(type), let iconUrl = NSURL(string: href, relativeTo: url as URL), let absoluteString = iconUrl.absoluteString { + let icon = Favicon(url: absoluteString, date: NSDate() as Date, type: type) + // If we already have a list of Favicons going already, then add it… + if type == bestType { + icons.append(icon) + } else { + // otherwise, this is the first in a new best yet type. + icons = [icon] + bestType = type + } + } + + // If we haven't got any options icons, then use the default at the root of the domain. + if let url = NSURL(string: "/favicon.ico", relativeTo: url as URL), icons.isEmpty, let absoluteString = url.absoluteString { + let icon = Favicon(url: absoluteString, date: NSDate() as Date, type: .guess) + icons = [icon] + } + + } + return deferMaybe(icons) + }) + } + + func getFavicon(_ siteUrl: URL, icon: Favicon, profile: Profile) -> Deferred> { + let deferred = Deferred>() + let url = icon.url + let manager = SDWebImageManager.shared() + let site = Site(url: siteUrl.absoluteString, title: "") + + var fav = Favicon(url: url, type: icon.type) + if let url = url.asURL { + var fetch: SDWebImageOperation? + fetch = manager.loadImage(with: url, + options: .lowPriority, + progress: { (receivedSize, expectedSize, _) in + if receivedSize > FaviconManager.maximumFaviconSize || expectedSize > FaviconManager.maximumFaviconSize { + fetch?.cancel() + } + }, + completed: { (img, _, _, _, _, url) in + guard let url = url else { + deferred.fill(Maybe(failure: FaviconError())) + return + } + fav = Favicon(url: url.absoluteString, type: icon.type) + + if let img = img { + fav.width = Int(img.size.width) + fav.height = Int(img.size.height) + profile.favicons.addFavicon(fav, forSite: site) + } else { + fav.width = 0 + fav.height = 0 + } + + deferred.fill(Maybe(success: fav)) + }) + } else { + return deferMaybe(FaviconFetcherErrorType(description: "Invalid URL \(url)")) + } + + return deferred + } + + // Returns a single Favicon UIImage for a given URL + class func fetchFavImageForURL(forURL url: URL, profile: Profile) -> Deferred> { + let deferred = Deferred>() + FaviconFetcher.getForURL(url.domainURL, profile: profile).uponQueue(DispatchQueue.main) { result in + var iconURL: URL? + if let favicons = result.successValue, favicons.count > 0, let faviconImageURL = favicons.first?.url.asURL { + iconURL = faviconImageURL + } else { + return deferred.fill(Maybe(failure: FaviconError())) + } + SDWebImageManager.shared().loadImage(with: iconURL, options: .continueInBackground, progress: nil) { (image, _, _, _, _, _) in + if let image = image { + deferred.fill(Maybe(success: image)) + } else { + deferred.fill(Maybe(failure: FaviconError())) + } + } + } + return deferred + } + + // Returns the default favicon for a site based on the first letter of the site's domain + class func getDefaultFavicon(_ url: URL) -> UIImage { + guard let character = url.baseDomain?.characters.first else { + return defaultFavicon + } + + let faviconLetter = String(character).uppercased() + + if let cachedFavicon = characterToFaviconCache[faviconLetter] { + return cachedFavicon + } + + var faviconImage = UIImage() + let faviconLabel = UILabel(frame: CGRect(x: 0, y: 0, width: TwoLineCellUX.ImageSize, height: TwoLineCellUX.ImageSize)) + faviconLabel.text = faviconLetter + faviconLabel.textAlignment = .center + faviconLabel.font = UIFont.systemFont(ofSize: 18, weight: UIFontWeightMedium) + faviconLabel.textColor = UIColor.white + UIGraphicsBeginImageContextWithOptions(faviconLabel.bounds.size, false, 0.0) + faviconLabel.layer.render(in: UIGraphicsGetCurrentContext()!) + faviconImage = UIGraphicsGetImageFromCurrentImageContext()! + UIGraphicsEndImageContext() + + characterToFaviconCache[faviconLetter] = faviconImage + return faviconImage + } + + // Returns a color based on the url's hash + class func getDefaultColor(_ url: URL) -> UIColor { + guard let hash = url.baseDomain?.hashValue else { + return UIColor.gray + } + let index = abs(hash) % (UIConstants.DefaultColorStrings.count - 1) + let colorHex = UIConstants.DefaultColorStrings[index] + return UIColor(colorString: colorHex) + } + + // Default favicons and background colors provided via mozilla/tippy-top-sites + class func getDefaultIcons() -> [String: (color: UIColor, url: String)] { + let filePath = Bundle.main.path(forResource: "top_sites", ofType: "json") + let file = try! Data(contentsOf: URL(fileURLWithPath: filePath!)) + let json = JSON(file) + var icons: [String: (color: UIColor, url: String)] = [:] + json.forEach({ + guard let url = $0.1["domain"].string, let color = $0.1["background_color"].string, var path = $0.1["image_url"].string else { + return + } + path = path.replacingOccurrences(of: ".png", with: "") + let filePath = Bundle.main.path(forResource: "TopSites/" + path, ofType: "png") + if let filePath = filePath { + if color == "#fff" || color == "#FFF" { + icons[url] = (UIColor.clear, filePath) + } else { + icons[url] = (UIColor(colorString: color.replacingOccurrences(of: "#", with: "")), filePath) + } + } + }) + return icons + } +} diff --git a/mobile/ios/Client/Utils/Swizzling.h b/mobile/ios/Client/Utils/Swizzling.h new file mode 100644 index 0000000000..69a367ddfc --- /dev/null +++ b/mobile/ios/Client/Utils/Swizzling.h @@ -0,0 +1,8 @@ +/* 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/. */ + +#pragma once + +BOOL SwizzleInstanceMethods(Class class, SEL dstSel, SEL srcSel); +BOOL SwizzleClassMethods(Class class, SEL dstSel, SEL srcSel); diff --git a/mobile/ios/Client/Utils/Swizzling.m b/mobile/ios/Client/Utils/Swizzling.m new file mode 100644 index 0000000000..70b168f14a --- /dev/null +++ b/mobile/ios/Client/Utils/Swizzling.m @@ -0,0 +1,26 @@ +/* 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 +#import +#include + +BOOL SwizzleInstanceMethods(Class class, SEL dstSel, SEL srcSel) +{ + Method dstMethod = class_getInstanceMethod(class, dstSel); + Method srcMethod = class_getInstanceMethod(class, srcSel); + IMP srcIMP = method_getImplementation(srcMethod); + if (class_addMethod(class, dstSel, srcIMP, method_getTypeEncoding(srcMethod))) { + class_replaceMethod(class, dstSel, method_getImplementation(dstMethod), method_getTypeEncoding(dstMethod)); + } else { + method_exchangeImplementations(dstMethod, srcMethod); + } + return (method_getImplementation(srcMethod) == method_getImplementation(class_getInstanceMethod(class, dstSel))); +} + +BOOL SwizzleClassMethods(Class class, SEL dstSel, SEL srcSel) +{ + Class metaClass = object_getClass(class); + return SwizzleInstanceMethods(metaClass, dstSel, srcSel); +} diff --git a/mobile/ios/Client/Utils/Try.h b/mobile/ios/Client/Utils/Try.h new file mode 100644 index 0000000000..decd4a3262 --- /dev/null +++ b/mobile/ios/Client/Utils/Try.h @@ -0,0 +1,9 @@ +/* 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 + +@interface Try : NSObject +- (id) initWithTry: (void(^)()) tryBlock catch: (void(^)(NSException *exception)) catchBlock; +@end diff --git a/mobile/ios/Client/Utils/Try.m b/mobile/ios/Client/Utils/Try.m new file mode 100644 index 0000000000..4367dedaa7 --- /dev/null +++ b/mobile/ios/Client/Utils/Try.m @@ -0,0 +1,21 @@ +/* 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 "Try.h" + +@implementation Try + +- (id) initWithTry: (void(^)()) tryBlock catch: (void(^)(NSException *exception)) catchBlock { + if (self = [super init]) { + @try { + tryBlock(); + } + @catch (NSException *exception) { + catchBlock(exception); + } + } + return self; +} + +@end diff --git a/mobile/ios/Client/en.lproj/InfoPlist.strings b/mobile/ios/Client/en.lproj/InfoPlist.strings new file mode 100644 index 0000000000..b2805473f1 --- /dev/null +++ b/mobile/ios/Client/en.lproj/InfoPlist.strings @@ -0,0 +1,12 @@ +/* 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/. */ + +NSLocationWhenInUseUsageDescription = "Websites you visit may request your location."; +NSPhotoLibraryUsageDescription = "This lets you save and upload photos."; +NSPhotoLibraryAddUsageDescription = "This lets you save photos."; +NSCameraUsageDescription = "This lets you take and upload photos."; +NSMicrophoneUsageDescription = "This lets you take and upload videos."; +ShortcutItemTitleNewTab = "New Tab"; +ShortcutItemTitleNewPrivateTab = "New Private Tab"; +ShortcutItemTitleQRCode = "Scan QR Code"; diff --git a/mobile/ios/ClientTests/ActivityStreamTests.swift b/mobile/ios/ClientTests/ActivityStreamTests.swift new file mode 100644 index 0000000000..b6167d83a9 --- /dev/null +++ b/mobile/ios/ClientTests/ActivityStreamTests.swift @@ -0,0 +1,284 @@ +/* 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 UIKit +import XCTest +@testable import Client +import Shared +import Storage +import Deferred +import SyncTelemetry + +class ActivityStreamTests: XCTestCase { + var profile: MockProfile! + var panel: ActivityStreamPanel! + var mockPingClient: MockPingClient! + var telemetry: ActivityStreamTracker! + + override func setUp() { + super.setUp() + self.profile = MockProfile() + self.telemetry = ActivityStreamTracker(eventsTracker: MockPingClient(), sessionsTracker: MockPingClient()) + self.panel = ActivityStreamPanel(profile: profile, telemetry: self.telemetry) + } + + override func tearDown() { + mockPingClient = nil + } + + func testDeletionOfSingleSuggestedSite() { + let siteToDelete = panel.defaultTopSites()[0] + + panel.hideURLFromTopSites(siteToDelete) + let newSites = panel.defaultTopSites() + + XCTAssertFalse(newSites.contains(siteToDelete, f: { (a, b) -> Bool in + return a.url == b.url + })) + } + + func testDeletionOfAllDefaultSites() { + let defaultSites = panel.defaultTopSites() + defaultSites.forEach({ + panel.hideURLFromTopSites($0) + }) + + let newSites = panel.defaultTopSites() + XCTAssertTrue(newSites.isEmpty) + } +} + +// MARK: Telemetry Tests +extension ActivityStreamTests { + fileprivate func expectedPayloadForEvent(_ event: String, source: String, position: Int) -> [String: Any] { + return [ + "event": event, + "page": "NEW_TAB", + "source": source, + "action_position": position, + "app_version": AppInfo.appVersion, + "build": AppInfo.buildNumber, + "locale": Locale.current.identifier, + "release_channel": AppConstants.BuildChannel.rawValue + ] + } + + fileprivate func expectedBadStatePayload(state: String, source: String) -> [String: Any] { + return [ + "event": state, + "page": "NEW_TAB", + "source": source, + "app_version": AppInfo.appVersion, + "build": AppInfo.buildNumber, + "locale": Locale.current.identifier, + "release_channel": AppConstants.BuildChannel.rawValue + ] + } + + fileprivate func assertPayload(_ actual: [String: Any], matches: [String: Any]) { + XCTAssertTrue(actual.count == matches.count) + actual.enumerated().forEach { index, element in + if let actualValue = element.1 as? Int, + let matchesValue = matches[element.0] as? Int { + XCTAssertTrue(actualValue == matchesValue) + } + + if let actualValue = element.1 as? String, + let matchesValue = matches[element.0] as? String { + XCTAssertTrue(actualValue == matchesValue) + } + } + } + + func testHighlightEmitsEventOnTap() { + let mockSite = Site(url: "http://mozilla.org", title: "Mozilla") + panel.highlights = [mockSite] + panel.selectItemAtIndex(0, inSection: .highlights) + + let pingsSent = (telemetry.eventsTracker as! MockPingClient).pingsReceived + XCTAssertEqual(pingsSent.count, 1) + let eventPing = pingsSent[0] + assertPayload(eventPing, matches: expectedPayloadForEvent("CLICK", source: "HIGHLIGHTS", position: 0)) + } + + func testContextMenuOnTopSiteEmitsRemoveEvent() { + let mockSite = Site(url: "http://mozilla.org", title: "Mozilla") + let topSitesContextMenu = panel.contextMenu(for: mockSite, with: IndexPath(item: 0, section: ActivityStreamPanel.Section.topSites.rawValue)) + + let removeAction = topSitesContextMenu?.actions[0].find { $0.title == Strings.RemoveContextMenuTitle } + removeAction?.handler?(removeAction!) + + let pingsSent = (telemetry.eventsTracker as! MockPingClient).pingsReceived + XCTAssertEqual(pingsSent.count, 1) + let removePing = pingsSent[0] + assertPayload(removePing, matches: expectedPayloadForEvent("REMOVE", source: "TOP_SITES", position: 0)) + } + + func testContextMenuOnHighlightsEmitsRemoveDismissEvents() { + let mockSite = Site(url: "http://mozilla.org", title: "Mozilla") + let highlightsContextMenu = panel.contextMenu(for: mockSite, with: IndexPath(row: 0, section: ActivityStreamPanel.Section.highlights.rawValue)) + + let dismiss = highlightsContextMenu?.actions[0].find { $0.title == Strings.RemoveContextMenuTitle } + let delete = highlightsContextMenu?.actions[0].find { $0.title == Strings.DeleteFromHistoryContextMenuTitle } + + dismiss?.handler?(dismiss!) + delete?.handler?(delete!) + + // Check to see that they emitted telemetry events + let pingsSent = (telemetry.eventsTracker as! MockPingClient).pingsReceived + XCTAssertEqual(pingsSent.count, 2) + + let dismissPing = pingsSent[0] + assertPayload(dismissPing, matches: expectedPayloadForEvent("DISMISS", source: "HIGHLIGHTS", position: 0)) + + let deletePing = pingsSent[1] + assertPayload(deletePing, matches: expectedPayloadForEvent("DELETE", source: "HIGHLIGHTS", position: 0)) + } + + func testSessionReportedWhenViewAppearsAndDisappears() { + // Simulate the panel opening and closing with a second in between for some session_duration + panel.viewWillAppear(false) + var pingsSent = (telemetry.sessionsTracker as! MockPingClient).pingsReceived + XCTAssertEqual(pingsSent.count, 0) + + wait(1) + panel.viewDidDisappear(false) + + pingsSent = (telemetry.sessionsTracker as! MockPingClient).pingsReceived + XCTAssertEqual(pingsSent.count, 1) + + let eventPing = pingsSent[0] + XCTAssertNotNil(eventPing["session_duration"]) + } + + func testBadStateEventsForHighlights() { + let goodSite = Site(url: "http://mozilla.org", title: "Mozilla") + goodSite.icon = Favicon(url: "http://image", date: Date(), type: .local) + goodSite.metadata = PageMetadata(id: nil, + siteURL: "http://mozilla.org", + mediaURL: "http://image", + title: "Mozilla", + description: "Web", + type: nil, + providerName: nil, + mediaDataURI: nil) + let badSite = Site(url: "http://mozilla.org", title: "Mozilla") + profile.recommendations = MockRecommender(highlights: [goodSite, badSite]) + + // Since invalidateHighlights calls back into the main thread, we can't + // simply call .value on this to block since the app will dead lock when + // trying to call back onto a blocked main thread. + let expect = XCTestExpectation(description: "Sent bad highlight pings") + panel.getHighlights() >>> { + expect.fulfill() + } + + wait(for: [expect], timeout: 3) + let pingsSent = (self.telemetry.eventsTracker as! MockPingClient).pingsReceived + XCTAssertEqual(pingsSent.count, 2) + assertPayload(pingsSent[0], + matches: expectedBadStatePayload(state: "MISSING_METADATA_IMAGE", source: "HIGHLIGHTS")) + assertPayload(pingsSent[1], + matches: expectedBadStatePayload(state: "MISSING_FAVICON", source: "HIGHLIGHTS")) + } + + func testBadStateEventsForTopSites() { + let goodSite = Site(url: "http://mozilla.org", title: "Mozilla") + goodSite.icon = Favicon(url: "http://image", date: Date(), type: .local) + goodSite.metadata = PageMetadata(id: nil, + siteURL: "http://mozilla.org", + mediaURL: "http://image", + title: "Mozilla", + description: "Web", + type: nil, + providerName: nil, + mediaDataURI: nil) + let badSite = Site(url: "http://mozilla.org", title: "Mozilla") + profile.history = MockTopSitesHistory(sites: [goodSite, badSite]) + + // Since invalidateHighlights calls back into the main thread, we can't + // simply call .value on this to block since the app will dead lock when + // trying to call back onto a blocked main thread. + let expect = XCTestExpectation(description: "Sent bad top site pings") + panel.getTopSites() >>> { + expect.fulfill() + } + + wait(for: [expect], timeout: 3) + let pingsSent = (self.telemetry.eventsTracker as! MockPingClient).pingsReceived + XCTAssertEqual(pingsSent.count, 2) + assertPayload(pingsSent[0], + matches: expectedBadStatePayload(state: "MISSING_METADATA_IMAGE", source: "TOP_SITES")) + assertPayload(pingsSent[1], + matches: expectedBadStatePayload(state: "MISSING_FAVICON", source: "TOP_SITES")) + } +} + +class MockPingClient: PingCentreClient { + + var pingsReceived: [[String: Any]] = [] + + public func sendPing(_ data: [String : Any], validate: Bool) -> Success { + pingsReceived.append(data) + return succeed() + } + + public func sendBatch(_ data: [[String : Any]], validate: Bool) -> Success { + pingsReceived += data + return succeed() + } +} + +fileprivate class MockRecommender: HistoryRecommendations { + func repopulateHighlights() -> Success { + return succeed() + } + + var highlights: [Site] + + init(highlights: [Site]) { + self.highlights = highlights + } + + func getHighlights() -> Deferred>> { + return deferMaybe(ArrayCursor(data: highlights)) + } + + func getRecentBookmarks(_ limit: Int) -> Deferred>> { + return deferMaybe(ArrayCursor(data: [])) + } + + func repopulate(invalidateTopSites shouldInvalidateTopSites: Bool, invalidateHighlights shouldInvalidateHighlights: Bool) -> Success { + return succeed() + } + + func removeHighlightForURL(_ url: String) -> Success { + guard let foundSite = highlights.filter({ $0.url == url }).first else { + return succeed() + } + let foundIndex = highlights.index(of: foundSite)! + highlights.remove(at: foundIndex) + return succeed() + } +} + +fileprivate class MockTopSitesHistory: MockableHistory { + let mockTopSites: [Site] + + init(sites: [Site]) { + mockTopSites = sites + } + + override func getTopSitesWithLimit(_ limit: Int) -> Deferred>> { + return deferMaybe(ArrayCursor(data: mockTopSites)) + } + + override func getPinnedTopSites() -> Deferred>> { + return deferMaybe(ArrayCursor(data: [])) + } + + override func updateTopSitesCacheIfInvalidated() -> Deferred> { + return deferMaybe(true) + } +} diff --git a/mobile/ios/ClientTests/AuthenticatorTests.swift b/mobile/ios/ClientTests/AuthenticatorTests.swift new file mode 100644 index 0000000000..7535fffb07 --- /dev/null +++ b/mobile/ios/ClientTests/AuthenticatorTests.swift @@ -0,0 +1,173 @@ +/* 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 XCTest +import Shared +import Deferred +@testable import Client +@testable import Storage + +class MockFiles: FileAccessor { + init() { + let docPath = NSSearchPathForDirectoriesInDomains(FileManager.SearchPathDirectory.documentDirectory, FileManager.SearchPathDomainMask.userDomainMask, true)[0] + super.init(rootPath: (docPath as NSString).appendingPathComponent("testing")) + } +} + +class MockChallengeSender: NSObject, URLAuthenticationChallengeSender { + func use(_ credential: URLCredential, for challenge: URLAuthenticationChallenge) {} + func continueWithoutCredential(for challenge: URLAuthenticationChallenge) {} + func cancel(_ challenge: URLAuthenticationChallenge) {} +} + +class MockMalformableLogin: LoginData { + var guid: String + var credentials: URLCredential + var protectionSpace: URLProtectionSpace + var hostname: String + var username: String? + var password: String + var httpRealm: String? + var formSubmitURL: String? + var usernameField: String? + var passwordField: String? + var hasMalformedHostname = true + var isValid = Maybe(success: ()) + + static func createWithHostname(_ hostname: String, username: String, password: String, formSubmitURL: String) -> MockMalformableLogin { + return self.init(guid: Bytes.generateGUID(), hostname: hostname, username: username, password: password, formSubmitURL: formSubmitURL) + } + + required init(guid: String, hostname: String, username: String, password: String, formSubmitURL: String) { + self.guid = guid + self.credentials = URLCredential(user: username, password: password, persistence: URLCredential.Persistence.none) + self.hostname = hostname + self.password = password + self.username = username + self.protectionSpace = URLProtectionSpace(host: hostname, port: 0, protocol: nil, realm: nil, authenticationMethod: nil) + self.formSubmitURL = formSubmitURL + } + + func toDict() -> [String: String] { + // Not used for this mock + return [String: String]() + } + + func isSignificantlyDifferentFrom(_ login: LoginData) -> Bool { + // Not used for this mock + return true + } +} + +private let MainLoginColumns = "guid, username, password, hostname, httpRealm, formSubmitURL, usernameField, passwordField" + +class AuthenticatorTests: XCTestCase { + + fileprivate var db: BrowserDB! + fileprivate var logins: SQLiteLogins! + fileprivate var mockVC = UIViewController() + + override func setUp() { + super.setUp() + self.db = BrowserDB(filename: "testsqlitelogins.db", schema: LoginsSchema(), files: MockFiles()) + self.logins = SQLiteLogins(db: self.db) + self.logins.removeAll().succeeded() + } + + override func tearDown() { + self.logins.removeAll().succeeded() + } + + fileprivate func mockChallengeForURL(_ url: URL, username: String, password: String) -> URLAuthenticationChallenge { + let scheme = url.scheme + let host = url.host ?? "" + let port = (url as NSURL).port?.intValue ?? 80 + + let credential = URLCredential(user: username, password: password, persistence: .none) + let protectionSpace = URLProtectionSpace(host: host, + port: port, + protocol: scheme, + realm: "Secure Site", + authenticationMethod: nil) + return URLAuthenticationChallenge(protectionSpace: protectionSpace, + proposedCredential: credential, + previousFailureCount: 0, + failureResponse: nil, + error: nil, + sender: MockChallengeSender()) + } + + fileprivate func hostnameFactory(_ row: SDRow) -> String { + return row["hostname"] as! String + } + + fileprivate func rawQueryForAllLogins() -> Deferred>> { + let projection = MainLoginColumns + let sql = + "SELECT \(projection) FROM " + + "\(TableLoginsLocal) WHERE is_deleted = 0 " + + "UNION ALL " + + "SELECT \(projection) FROM " + + "\(TableLoginsMirror) WHERE is_overridden = 0 " + + "ORDER BY hostname ASC" + return db.runQuery(sql, args: nil, factory: hostnameFactory) + } + + func testChallengeMatchesLoginEntry() { + let login = Login.createWithHostname("https://securesite.com", username: "username", password: "password", formSubmitURL: "https://submit.me") + logins.addLogin(login).succeeded() + let challenge = mockChallengeForURL(URL(string: "https://securesite.com")!, username: "username", password: "password") + let result = Authenticator.findMatchingCredentialsForChallenge(challenge, fromLoginsProvider: logins).value.successValue! + XCTAssertNotNil(result) + XCTAssertEqual(result?.user, "username") + XCTAssertEqual(result?.password, "password") + } + + func testChallengeMatchesSingleMalformedLoginEntry() { + // Since Login has been updated to not store schemeless URL, write directly to simulate a malformed URL + let malformedLogin = MockMalformableLogin.createWithHostname("malformed.com", username: "username", password: "password", formSubmitURL: "https://submit.me") + logins.addLogin(malformedLogin).succeeded() + + // Pre-condition: Check that the hostname is malformed + let oldHostname = rawQueryForAllLogins().value.successValue![0] + XCTAssertEqual(oldHostname, "malformed.com") + + let challenge = mockChallengeForURL(URL(string: "https://malformed.com")!, username: "username", password: "password") + let result = Authenticator.findMatchingCredentialsForChallenge(challenge, fromLoginsProvider: logins).value.successValue! + XCTAssertNotNil(result) + XCTAssertEqual(result?.user, "username") + XCTAssertEqual(result?.password, "password") + + // Post-condition: Check that we updated the hostname to be not malformed + let newHostname = rawQueryForAllLogins().value.successValue![0] + XCTAssertEqual(newHostname, "https://malformed.com") + } + + func testChallengeMatchesDuplicateLoginEntries() { + // Since Login has been updated to not store schemeless URL, write directly to simulate a malformed URL + let malformedLogin = MockMalformableLogin.createWithHostname("malformed.com", username: "malformed_username", password: "malformed_password", formSubmitURL: "https://submit.me") + logins.addLogin(malformedLogin).succeeded() + + let login = Login.createWithHostname("https://malformed.com", username: "good_username", password: "good_password", formSubmitURL: "https://submit.me") + logins.addLogin(login).succeeded() + + // Pre-condition: Verify that both logins were stored + let hostnames = rawQueryForAllLogins().value.successValue! + XCTAssertEqual(hostnames.count, 2) + XCTAssertEqual(hostnames[0], "https://malformed.com") + XCTAssertEqual(hostnames[1], "malformed.com") + + let challenge = mockChallengeForURL(URL(string: "https://malformed.com")!, username: "username", password: "password") + let result = Authenticator.findMatchingCredentialsForChallenge(challenge, fromLoginsProvider: logins).value.successValue! + XCTAssertNotNil(result) + XCTAssertEqual(result?.user, "good_username") + XCTAssertEqual(result?.password, "good_password") + + // Post-condition: Verify that malformed URL was removed + let newHostnames = rawQueryForAllLogins().value.successValue! + XCTAssertEqual(newHostnames.count, 1) + XCTAssertEqual(newHostnames[0], "https://malformed.com") + } +} diff --git a/mobile/ios/ClientTests/ClientTests.swift b/mobile/ios/ClientTests/ClientTests.swift new file mode 100644 index 0000000000..7bee0d5a9b --- /dev/null +++ b/mobile/ios/ClientTests/ClientTests.swift @@ -0,0 +1,94 @@ +/* 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 UIKit +import XCTest + +import Shared +import Storage +import WebKit +import Alamofire +@testable import Client + +class ClientTests: XCTestCase { + + func testSyncUA() { + let ua = UserAgent.syncUserAgent + let device = DeviceInfo.deviceModel() + let systemVersion = UIDevice.current.systemVersion + let expectedRegex = "^Firefox-iOS-Sync/[0-9\\.]+b[0-9]* \\(\(device); iPhone OS \(systemVersion)\\) \\([-_A-Za-z0-9= \\(\\)]+\\)$" + let loc = ua.range(of: expectedRegex, options: NSString.CompareOptions.regularExpression) + XCTAssertTrue(loc != nil, "Sync UA is as expected. Was \(ua)") + } + + // Simple test to make sure the WKWebView UA matches the expected FxiOS pattern. + func testUserAgent() { + let appVersion = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as! String + + let compare: (String) -> Bool = { ua in + let range = ua.range(of: "^Mozilla/5\\.0 \\(.+\\) AppleWebKit/[0-9\\.]+ \\(KHTML, like Gecko\\) FxiOS/\(appVersion)b[0-9]* Mobile/[A-Za-z0-9]+ Safari/[0-9\\.]+$", options: NSString.CompareOptions.regularExpression) + return range != nil + } + + XCTAssertTrue(compare(UserAgent.defaultUserAgent()), "User agent computes correctly.") + XCTAssertTrue(compare(UserAgent.cachedUserAgent(checkiOSVersion: true)!), "User agent is cached correctly.") + + let expectation = self.expectation(description: "Found Firefox user agent") + + let webView = WKWebView() + webView.evaluateJavaScript("navigator.userAgent") { result, error in + let userAgent = result as! String + if compare(userAgent) { + expectation.fulfill() + } else { + XCTFail("User agent did not match expected pattern! \(userAgent)") + } + } + + waitForExpectations(timeout: 5, handler: nil) + } + + func testDesktopUserAgent() { + let compare: (String) -> Bool = { ua in + let range = ua.range(of: "^Mozilla/5\\.0 \\(Macintosh; Intel Mac OS X [0-9_]+\\) AppleWebKit/[0-9\\.]+ \\(KHTML, like Gecko\\) Safari/[0-9\\.]+$", options: NSString.CompareOptions.regularExpression) + return range != nil + } + + XCTAssertTrue(compare(UserAgent.desktopUserAgent()), "Desktop user agent computes correctly.") + } + + /// Our local server should only accept whitelisted hosts (localhost and 127.0.0.1). + /// All other localhost equivalents should return 403. + func testDisallowLocalhostAliases() { + // Allowed local hosts. The first two are equivalent since iOS forwards an + // empty host to localhost. + [ "localhost", + "", + "127.0.0.1", + ].forEach { XCTAssert(hostIsValid($0), "\($0) host should be valid.") } + + // Disallowed local hosts. WKWebView will direct them to our server, but the server + // should reject them. + [ "[::1]", + "2130706433", + "0", + "127.00.00.01", + "017700000001", + "0x7f.0x0.0x0.0x1" + ].forEach { XCTAssertFalse(hostIsValid($0), "\($0) host should not be valid.") } + } + + fileprivate func hostIsValid(_ host: String) -> Bool { + let expectation = self.expectation(description: "Validate host for \(host)") + let request = URLRequest(url: URL(string: "http://\(host):6571/about/license")!) + var response: HTTPURLResponse? + Alamofire.request(request).authenticate(usingCredential: WebServer.sharedInstance.credentials).response { (res) -> Void in + response = res.response + expectation.fulfill() + } + waitForExpectations(timeout: 100, handler: nil) + return response?.statusCode == 200 + } + +} diff --git a/mobile/ios/ClientTests/CustomSearchEnginesTest.swift b/mobile/ios/ClientTests/CustomSearchEnginesTest.swift new file mode 100644 index 0000000000..06c3ab88e8 --- /dev/null +++ b/mobile/ios/ClientTests/CustomSearchEnginesTest.swift @@ -0,0 +1,60 @@ +/* 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/. */ + +@testable import Client +import Shared +import Storage +import Sync +import UIKit + +import XCTest + +class CustomSearchEnginesTest: XCTestCase { + + func testgetSearchTemplate() { + let profile = MockBrowserProfile(localName: "customSearchTests") + let customSearchEngineForm = CustomSearchViewController() + customSearchEngineForm.profile = profile + + let template = customSearchEngineForm.getSearchTemplate(withString: "https://github.com/search=%s") + XCTAssertEqual(template, "https://github.com/search={searchTerms}") + + let badTemplate = customSearchEngineForm.getSearchTemplate(withString: "https://github.com/search=blah") + XCTAssertNil(badTemplate) + } + + func testaddSearchEngine() { + let profile = MockBrowserProfile(localName: "customSearchTests") + let customSearchEngineForm = CustomSearchViewController() + customSearchEngineForm.profile = profile + let q = "http://www.google.ca/?#q=%s" + let title = "YASE" + + let expectation = self.expectation(description: "Waiting on favicon fetching") + customSearchEngineForm.createEngine(forQuery: q, andName: title).uponQueue(DispatchQueue.main) { result in + XCTAssertNotNil(result.successValue, "Make sure the new engine is not nil") + let engine = result.successValue! + XCTAssertEqual(engine.shortName, title) + XCTAssertNotNil(engine.image) + XCTAssertEqual(engine.searchTemplate, "http://www.google.ca/?#q={searchTerms}") + expectation.fulfill() + } + waitForExpectations(timeout: 5, handler: nil) + } + + func testaddSearchEngineFailure() { + let profile = MockBrowserProfile(localName: "customSearchTests") + let customSearchEngineForm = CustomSearchViewController() + customSearchEngineForm.profile = profile + let q = "isthisvalid.com/hhh%s" + let title = "YASE" + + let expectation = self.expectation(description: "Waiting on favicon fetching") + customSearchEngineForm.createEngine(forQuery: q, andName: title).uponQueue(DispatchQueue.main) { result in + XCTAssertNil(result.successValue, "Make sure the new engine is nil") + expectation.fulfill() + } + waitForExpectations(timeout: 5, handler: nil) + } +} diff --git a/mobile/ios/ClientTests/FileAccessorTests.swift b/mobile/ios/ClientTests/FileAccessorTests.swift new file mode 100644 index 0000000000..79a6516f90 --- /dev/null +++ b/mobile/ios/ClientTests/FileAccessorTests.swift @@ -0,0 +1,76 @@ +/* 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 Storage +import XCTest + +class FileAccessorTests: XCTestCase { + fileprivate var testDir: String! + fileprivate var files: FileAccessor! + + override func setUp() { + let docPath: NSString = NSSearchPathForDirectoriesInDomains(FileManager.SearchPathDirectory.documentDirectory, FileManager.SearchPathDomainMask.userDomainMask, true)[0] as NSString + files = FileAccessor(rootPath: docPath.appendingPathComponent("filetest")) + + testDir = try! files.getAndEnsureDirectory() + try! files.removeFilesInDirectory() + } + + func testFileAccessor() { + // Test existence. + XCTAssertFalse(files.exists("foo"), "File doesn't exist") + createFile("foo") + XCTAssertTrue(files.exists("foo"), "File exists") + + // Test moving. + do { + try files.move("foo", toRelativePath: "bar") + XCTAssertFalse(files.exists("foo"), "Old doesn't exist") + XCTAssertTrue(files.exists("bar"), "New file exists") + } catch { + XCTFail("Unable to move 'foo' to 'bar' \(error)") + } + + do { + try files.move("bar", toRelativePath: "foo/bar") + XCTAssertFalse(files.exists("bar"), "Old doesn't exist") + XCTAssertTrue(files.exists("foo/bar"), "New file exists") + } catch { + XCTFail("Unable to move 'bar' to 'foo/bar' \(error)") + } + + // Test removal. + do { + XCTAssertTrue(files.exists("foo"), "File exists") + try files.remove("foo") + XCTAssertFalse(files.exists("foo"), "File removed") + } catch { + XCTFail("Unable to remove 'foo' \(error)") + } + + // Test directory creation and path. + do { + XCTAssertFalse(files.exists("foo"), "Directory doesn't exist") + let path = try files.getAndEnsureDirectory("foo") + var isDirectory = ObjCBool(false) + FileManager.default.fileExists(atPath: path, isDirectory: &isDirectory) + XCTAssertTrue(isDirectory.boolValue, "Directory exists") + } catch { + XCTFail("Unable to find directory 'foo' \(error)") + } + } + + fileprivate func createFile(_ filename: String) { + let path = (testDir as NSString).appendingPathComponent(filename) + let success: Bool + do { + try "foo".write(toFile: path, atomically: false, encoding: String.Encoding.utf8) + success = true + } catch _ { + success = false + } + XCTAssertTrue(success, "Wrote to \(path)") + } +} diff --git a/mobile/ios/ClientTests/FxADeepLinkingTests.swift b/mobile/ios/ClientTests/FxADeepLinkingTests.swift new file mode 100644 index 0000000000..8d97bf7216 --- /dev/null +++ b/mobile/ios/ClientTests/FxADeepLinkingTests.swift @@ -0,0 +1,42 @@ +/* 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 UIKit +import XCTest +@testable import Client +import Shared + +class FxADeepLinkingTests: XCTestCase { + var profile: MockProfile! + var vc: FxAContentViewController! + var expectUrl = URL(string: "https://accounts.firefox.com/signin?service=sync&context=fx_ios_v1&signin=test&utm_source=somesource&entrypoint=one") + + override func setUp() { + super.setUp() + self.profile = MockProfile() + self.vc = FxAContentViewController(profile: self.profile) + } + + func testLaunchWithNilOptions() { + let testUrl = self.vc.createFxAURLWith(nil, profile: self.profile) + // Should use default urls for nil options + XCTAssertEqual(testUrl, self.vc.profile.accountConfiguration.signInURL) + } + + func testLaunchWithOptions() { + let url = URL(string: "firefox://fxa-signin?signin=test&utm_source=somesource&entrypoint=one&ignore=this") + let query = url!.getQuery() + let fxaOptions = FxALaunchParams(query: query) + let testUrl = self.vc.createFxAURLWith(fxaOptions, profile: self.profile) + XCTAssertEqual(testUrl, expectUrl!) + } + + func testDoesntOverrideServiceContext() { + let url = URL(string: "firefox://fxa-signin?service=asdf&context=123&signin=test&entrypoint=one&utm_source=somesource&ignore=this") + let query = url!.getQuery() + let fxaOptions = FxALaunchParams(query: query) + let testUrl = self.vc.createFxAURLWith(fxaOptions, profile: self.profile) + XCTAssertEqual(testUrl, expectUrl!) + } +} diff --git a/mobile/ios/ClientTests/FxAPushMessageTest.swift b/mobile/ios/ClientTests/FxAPushMessageTest.swift new file mode 100644 index 0000000000..dedb71a77b --- /dev/null +++ b/mobile/ios/ClientTests/FxAPushMessageTest.swift @@ -0,0 +1,153 @@ +/* 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/. */ + +@testable import Account +@testable import Client +import Foundation +import FxA +import SwiftyJSON +import XCTest + +class FxAPushMessageTest: XCTestCase { + func testMessage_subscriptionDecryption() { + var userInfo: [AnyHashable: Any] = [ + "chid": "034f52789f7b44ecaf119cc59231cdc1", + "enc": "keyid=p256dh;salt=mYBHM3B_oXlEjV0HfgHZ1A", + "body": "_tZf65gKC23STnTNuhtSSbrg1LScGiLjO4GOIuHlCGIFFEzcwsB-J-s3pe3qu2d24A-sKwVyolmShlMBEvEX_34f6FgXMs3k35g4u5STKgJMQxZ8VFDjtQqQfxfSIEt35pdKaPwXKH2zbs0xHC3qEJ0YMc60Eq8uAuQF7FQZ7ts", + "cryptokey": "keyid=p256dh;dh=BNLZ2IWMNGioofzMBnSijySib0Pa-lwgBfLYIhqvvmKxprKgEh6JCDB2DBUmj9BuJCk6xJvBbPd-4x_8tb-qIPM;p256ecdsa=BP-OR33RzQSlrzD7_d1kYE9i9WjSIQAKTuhHxYNiPEF0i-wxeNIIwxthwU7zBTbumyxFUeydrmxcVKXugjBImRU", + "con": "aesgcm", + "ver": "gAAAAABY5hUl_Pi1bVI4ptqKzPWq_aulqXEO1eOx_ncUuwoz8zItWWLA4Ix1ZbDLTOkurqK1vE3N3y1ZXsp_MZ69QJYRuWg5V_3T_XUNVBxj8dnDcNAE-ep9_M5xeKiNdngdww-cqqMOxf-tI5ZoR2nQFqWSs51XGwMMIdsGNmUPqnR4w2Rzt6FYz-AsGCrHXJ3bmYbwpq4P", + ] + + let subscription = PushSubscription(channelID: "channel", + endpoint: URL(string: "https://example.ru")!, + p256dhPrivateKey: "UDnicgor_Il7cLNTqSt--SrEblNBbgPA2yXAQ_b31xI", + p256dhPublicKey: "BB4XdAhpVVU45NYSXHpRiubMYoaeb0A-y5aSGE437YKGHQihlvlZMv5D0ebK6WmFqzpIr217Kv9oCbZVDp1KGK4", + authKey: "0gH7RiYYMhfHDQ1L1X4RMw") + + let body = userInfo["body"] as! String + let enc = userInfo["enc"] as! String + let cryptokey = userInfo["cryptokey"] as! String + + guard let plaintext = subscription.aesgcm(payload: body, encryptionHeader: enc, cryptoHeader: cryptokey) else { + return XCTFail("Decryption failed") + } + + let json = JSON(parseJSON: plaintext) + + XCTAssertEqual(json["command"].stringValue, "fxaccounts:device_connected") + } + + func testMessageHandler() { + let subscription = PushSubscription(channelID: "channel", + endpoint: URL(string: "https://example.ru")!, + p256dhPrivateKey: "t7dcZIN4w37UYjE6u3lBLB0WOShxqelkbJFKKzMDSsE", + p256dhPublicKey: "BIXFDlhppL2lc5GcIXbGPa1iVdJn5ULYaF1ltJY9Qm17-tIC_9eEZXtalPpMRXsFmEKhdn2ttg3KQ3t-ztQ3ShQ", + authKey: "a8E3EO5F6FFWdv4hAGyGyw") + + let userInfo: [AnyHashable: Any] = [ + AnyHashable("chid"): "f6defa012a6249e58bbfbf2995f8d425", + AnyHashable("enc"): "keyid=p256dh;salt=nslkEsUqUg5sQ7_sRZfOjg", + AnyHashable("body"): "Q1t-ttSwQMxUI64Ls3vOU-hE_qg1AIUzyLQSpEkx-8JITh5UJ7oq25faEc8XPoTYQoaHQ2d--QIK_yVorbt-0Yr7IO4BmtSSX-e4kSx76fWzqKjEpEt7Vr3Av5seEBeoAT2FZRzehkFjNVWoTw", + AnyHashable("cryptokey"): "keyid=p256dh;dh=BNfUPK_8eUTZGOyXq07lthBfHeIxC2B7L_gF3cMGK1jVfDe9tlgxpHD_mbKrt3p12d7_O__wizhne2a1Eb7pZgk;p256ecdsa=BFSuld8S4PbRcgGe3OQPN9NyIOXx-ccUIMb0q6nIpH7Qf894wz0TIQTXQ7I7pWjZiN9KCdYVjNhyPtr1--37ois", + AnyHashable("con"): "aesgcm", + AnyHashable("ver"): "gAAAAABZLbG-m7EHhcMdrqs51SkESIZHsZjvw2QIu8LOeXxcKEy6wDVCprOKFAfJU44cinfJcDtCnO9EEyzpFt5e0HBDCLybGyThoZzmiod6zTLhTfAKZe-SyElSVCL0UDpJ_-U3UTUUHUaJXeRf0z6NvFM-uL39Jy-dwr3cuJoSDIcTPdChRPFiIS1hwokqMlxOn36azxOi", + ] + + let profile = MockProfile() + + let account = FirefoxAccount( + configuration: FirefoxAccountConfigurationLabel.production.toConfiguration(), + email: "testtest@test.com", + uid: "uid", + deviceRegistration: nil, + stateKeyLabel: "xxx", + state: SeparatedState()) + + let registration = PushRegistration(uaid: "uaid", secret: "secret", subscription: subscription) + + account.pushRegistration = registration + profile.setAccount(account) + + let handler = FxAPushMessageHandler(with: profile) + + let expectation = XCTestExpectation() + handler.handle(userInfo: userInfo).upon { maybe in + XCTAssertTrue(maybe.isSuccess) + XCTAssertEqual(maybe.successValue!, PushMessage.collectionChanged(collections: ["clients", "tabs"])) + expectation.fulfill() + } + wait(for: [expectation], timeout: 10) + } + + func createHandler(_ profile: Profile = MockProfile()) -> FxAPushMessageHandler { + let account = FirefoxAccount( + configuration: FirefoxAccountConfigurationLabel.production.toConfiguration(), + email: "testtest@test.com", + uid: "uid", + deviceRegistration: nil, + stateKeyLabel: "xxx", + state: SeparatedState()) + + profile.setAccount(account) + + return FxAPushMessageHandler(with: profile) + } + + func test_deviceConnected() { + let handler = createHandler() + + let expectation = XCTestExpectation() + handler.handle(plaintext: "{\"command\":\"fxaccounts:device_connected\",\"data\":{\"deviceName\": \"Use Nightly on Desktop\"}}").upon { maybe in + XCTAssertTrue(maybe.isSuccess) + guard let message = maybe.successValue else { + return expectation.fulfill() + } + XCTAssertEqual(message, PushMessage.deviceConnected("Use Nightly on Desktop")) + expectation.fulfill() + } + wait(for: [expectation], timeout: 10) + } + + func test_deviceDisconnected() { + let profile = MockProfile() + let handler = createHandler(profile) + let prefs = profile.prefs + + let expectation = XCTestExpectation() + handler.handle(plaintext: "{\"command\":\"fxaccounts:device_disconnected\",\"data\":{\"id\": \"not_this_device\"}}").upon { maybe in + XCTAssertTrue(maybe.isSuccess) + guard let message = maybe.successValue else { + return expectation.fulfill() + } + XCTAssertEqual(message.messageType, .deviceDisconnected) + XCTAssertFalse(prefs.boolForKey(PendingAccountDisconnectedKey) ?? false) + expectation.fulfill() + } + wait(for: [expectation], timeout: 10) + + } + + func test_thisDeviceDisconnected() { + let profile = MockProfile() + let handler = createHandler(profile) + + let deviceRegistration = FxADeviceRegistration(id: "this-device-id", version: 1, lastRegistered: 0) + profile.account?.deviceRegistration = deviceRegistration + + let prefs = profile.prefs + + let expectation = XCTestExpectation() + handler.handle(plaintext: "{\"command\":\"fxaccounts:device_disconnected\",\"data\":{\"id\": \"\(deviceRegistration.id)\"}}").upon { maybe in + guard let message = maybe.successValue else { + return expectation.fulfill() + } + XCTAssertEqual(message, PushMessage.thisDeviceDisconnected) + XCTAssertTrue(prefs.boolForKey(PendingAccountDisconnectedKey) ?? false) + expectation.fulfill() + } + wait(for: [expectation], timeout: 10) + + } +} diff --git a/mobile/ios/ClientTests/HomePageTests.swift b/mobile/ios/ClientTests/HomePageTests.swift new file mode 100644 index 0000000000..a4bc3416bf --- /dev/null +++ b/mobile/ios/ClientTests/HomePageTests.swift @@ -0,0 +1,16 @@ +/* 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/. */ + +@testable import Client +import XCTest + +class HomePageTests: XCTestCase { + let prefs = NSUserDefaultsPrefs(prefix: "PrefsTests") + + func testHomePageSettingForInternalURLs() { + let helper = HomePageHelper(prefs: prefs) + helper.currentURL = URL(string: "http://localhost:6571") + XCTAssertNil(prefs.stringForKey(HomePageConstants.HomePageURLPrefKey)) + } +} diff --git a/mobile/ios/ClientTests/Info.plist b/mobile/ios/ClientTests/Info.plist new file mode 100644 index 0000000000..9411e4a06e --- /dev/null +++ b/mobile/ios/ClientTests/Info.plist @@ -0,0 +1,24 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + BNDL + CFBundleShortVersionString + 10.6 + CFBundleSignature + ???? + CFBundleVersion + 1 + + diff --git a/mobile/ios/ClientTests/MockProfile.swift b/mobile/ios/ClientTests/MockProfile.swift new file mode 100644 index 0000000000..2e16e645b8 --- /dev/null +++ b/mobile/ios/ClientTests/MockProfile.swift @@ -0,0 +1,235 @@ +/* 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/. */ + +@testable import Client +import Foundation +import Account +import ReadingList +import Shared +import Storage +import Sync +import XCTest +import Deferred + +open class MockSyncManager: SyncManager { + open var isSyncing = false + open var lastSyncFinishTime: Timestamp? + open var syncDisplayState: SyncDisplayState? + + open func hasSyncedHistory() -> Deferred> { + return deferMaybe(true) + } + + private func completedWithStats(collection: String) -> Deferred> { + return deferMaybe(SyncStatus.completed(SyncEngineStatsSession(collection: collection))) + } + + open func syncClients() -> SyncResult { return completedWithStats(collection: "mock_clients") } + open func syncClientsThenTabs() -> SyncResult { return completedWithStats(collection: "mock_clientsandtabs") } + open func syncHistory() -> SyncResult { return completedWithStats(collection: "mock_history") } + open func syncLogins() -> SyncResult { return completedWithStats(collection: "mock_logins") } + open func mirrorBookmarks() -> SyncResult { return completedWithStats(collection: "mock_bookmarks") } + open func syncEverything(why: SyncReason) -> Success { + return succeed() + } + open func syncNamedCollections(why: SyncReason, names: [String]) -> Success { + return succeed() + } + open func beginTimedSyncs() {} + open func endTimedSyncs() {} + open func applicationDidBecomeActive() { + self.beginTimedSyncs() + } + open func applicationDidEnterBackground() { + self.endTimedSyncs() + } + + open func onNewProfile() { + } + + open func onAddedAccount() -> Success { + return succeed() + } + open func onRemovedAccount(_ account: FirefoxAccount?) -> Success { + return succeed() + } + + open func hasSyncedLogins() -> Deferred> { + return deferMaybe(true) + } +} + +open class MockTabQueue: TabQueue { + open func addToQueue(_ tab: ShareItem) -> Success { + return succeed() + } + + open func getQueuedTabs() -> Deferred>> { + return deferMaybe(ArrayCursor(data: [])) + } + + open func clearQueuedTabs() -> Success { + return succeed() + } +} + +open class MockPanelDataObservers: PanelDataObservers { + override init(profile: Profile) { + super.init(profile: profile) + self.activityStream = MockActivityStreamDataObserver(profile: profile) + } +} + +open class MockActivityStreamDataObserver: DataObserver { + public var profile: Profile + public weak var delegate: DataObserverDelegate? + + init(profile: Profile) { + self.profile = profile + } + + public func refreshIfNeeded(forceHighlights highlights: Bool, forceTopSites topsites: Bool) { + + } +} + +open class MockProfile: Profile { + // Read/Writeable properties for mocking + public var recommendations: HistoryRecommendations + public var places: BrowserHistory & Favicons & SyncableHistory & ResettableSyncStorage & HistoryRecommendations + public var files: FileAccessor + public var history: BrowserHistory & SyncableHistory & ResettableSyncStorage + public var logins: BrowserLogins & SyncableLogins & ResettableSyncStorage + public var syncManager: SyncManager! + + public lazy var panelDataObservers: PanelDataObservers = { + return MockPanelDataObservers(profile: self) + }() + + var db: BrowserDB + + fileprivate let name: String = "mockaccount" + + init() { + files = MockFiles() + syncManager = MockSyncManager() + logins = MockLogins(files: files) + db = BrowserDB(filename: "mock.db", schema: BrowserSchema(), files: files) + places = SQLiteHistory(db: self.db, prefs: MockProfilePrefs()) + recommendations = places + history = places + } + + public func localName() -> String { + return name + } + + public func reopen() { + } + + public func shutdown() { + } + + public var isShutdown: Bool = false + + public var favicons: Favicons { + return self.places + } + + lazy public var queue: TabQueue = { + return MockTabQueue() + }() + + lazy public var metadata: Metadata = { + return SQLiteMetadata(db: self.db) + }() + + lazy public var isChinaEdition: Bool = { + return Locale.current.identifier == "zh_CN" + }() + + lazy public var certStore: CertStore = { + return CertStore() + }() + + lazy public var bookmarks: BookmarksModelFactorySource & KeywordSearchSource & SyncableBookmarks & LocalItemSource & MirrorItemSource & ShareToDestination = { + // Make sure the rest of our tables are initialized before we try to read them! + // This expression is for side-effects only. + let p = self.places + + return MergedSQLiteBookmarks(db: self.db) + }() + + lazy public var searchEngines: SearchEngines = { + return SearchEngines(prefs: self.prefs, files: self.files) + }() + + lazy public var prefs: Prefs = { + return MockProfilePrefs() + }() + + lazy public var readingList: ReadingListService? = { + return ReadingListService(profileStoragePath: self.files.rootPath as String) + }() + + lazy public var recentlyClosedTabs: ClosedTabsStore = { + return ClosedTabsStore(prefs: self.prefs) + }() + + internal lazy var remoteClientsAndTabs: RemoteClientsAndTabs = { + return SQLiteRemoteClientsAndTabs(db: self.db) + }() + + fileprivate lazy var syncCommands: SyncCommands = { + return SQLiteRemoteClientsAndTabs(db: self.db) + }() + + public let accountConfiguration: FirefoxAccountConfiguration = ProductionFirefoxAccountConfiguration() + var account: FirefoxAccount? + + public func hasAccount() -> Bool { + return account != nil + } + + public func hasSyncableAccount() -> Bool { + return account?.actionNeeded == FxAActionNeeded.none + } + + public func getAccount() -> FirefoxAccount? { + return account + } + + public func setAccount(_ account: FirefoxAccount) { + self.account = account + self.syncManager.onAddedAccount() + } + + public func flushAccount() {} + + public func removeAccount() { + let old = self.account + self.account = nil + self.syncManager.onRemovedAccount(old) + } + + public func getClients() -> Deferred> { + return deferMaybe([]) + } + + public func getClientsAndTabs() -> Deferred> { + return deferMaybe([]) + } + + public func getCachedClientsAndTabs() -> Deferred> { + return deferMaybe([]) + } + + public func storeTabs(_ tabs: [RemoteTab]) -> Deferred> { + return deferMaybe(0) + } + + public func sendItems(_ items: [ShareItem], toClients clients: [RemoteClient]) -> Deferred> { + return deferMaybe(SyncStatus.notStarted(.offline)) + } +} diff --git a/mobile/ios/ClientTests/MockableHistory.swift b/mobile/ios/ClientTests/MockableHistory.swift new file mode 100644 index 0000000000..32ba2cfb79 --- /dev/null +++ b/mobile/ios/ClientTests/MockableHistory.swift @@ -0,0 +1,45 @@ +/* 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 Storage +import Deferred +import Shared + +/* + * A class that adheres to all the requirements for a profile's history property + * with all of the methods set to fatalError. Use this class if you're looking to + * mock out parts of the history API + */ +class MockableHistory: BrowserHistory, SyncableHistory, ResettableSyncStorage { + func getFrecentHistory() -> FrecentHistory { fatalError() } + func getTopSitesWithLimit(_ limit: Int) -> Deferred>> { fatalError() } + func addLocalVisit(_ visit: SiteVisit) -> Success { fatalError() } + func clearHistory() -> Success { fatalError() } + func removeHistoryForURL(_ url: String) -> Success { fatalError() } + func removeSiteFromTopSites(_ site: Site) -> Success { fatalError() } + func removeHostFromTopSites(_ host: String) -> Success { fatalError() } + func clearTopSitesCache() -> Success { fatalError() } + func removeFromPinnedTopSites(_ site: Site) -> Success { fatalError() } + func addPinnedTopSite(_ site: Site) -> Success { fatalError() } + func getPinnedTopSites() -> Deferred>> { fatalError() } + func getSitesByLastVisit(_ limit: Int) -> Deferred>> { fatalError() } + func setTopSitesNeedsInvalidation() { fatalError() } + func updateTopSitesCacheIfInvalidated() -> Deferred> { fatalError() } + func setTopSitesCacheSize(_ size: Int32) { fatalError() } + func onRemovedAccount() -> Success { fatalError() } + func ensurePlaceWithURL(_ url: String, hasGUID guid: GUID) -> Success { fatalError() } + func deleteByGUID(_ guid: GUID, deletedAt: Timestamp) -> Success { fatalError() } + func storeRemoteVisits(_ visits: [Visit], forGUID guid: GUID) -> Success { fatalError() } + func insertOrUpdatePlace(_ place: Place, modified: Timestamp) -> Deferred> { fatalError() } + func getModifiedHistoryToUpload() -> Deferred> { fatalError() } + func getDeletedHistoryToUpload() -> Deferred> { fatalError() } + func markAsSynchronized(_: [GUID], modified: Timestamp) -> Deferred> { fatalError() } + func markAsDeleted(_ guids: [GUID]) -> Success { fatalError() } + func doneApplyingRecordsAfterDownload() -> Success { fatalError() } + func doneUpdatingMetadataAfterUpload() -> Success { fatalError() } + func hasSyncedHistory() -> Deferred> { fatalError() } + func resetClient() -> Success { fatalError() } +} + diff --git a/mobile/ios/ClientTests/PanelDataObserversTests.swift b/mobile/ios/ClientTests/PanelDataObserversTests.swift new file mode 100644 index 0000000000..af25315fa7 --- /dev/null +++ b/mobile/ios/ClientTests/PanelDataObserversTests.swift @@ -0,0 +1,84 @@ +/* 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 XCTest +@testable import Client + +private class MockDataObserverDelegate: DataObserverDelegate { + var didInvalidateCount = 0 + var willInvalidateCount = 0 + var highlightsRefreshCount = 0 + var topSitesRefreshCount = 0 + + func didInvalidateDataSources(refresh forced: Bool, highlightsRefreshed: Bool, topSitesRefreshed: Bool) { + didInvalidateCount += 1 + if highlightsRefreshed { + highlightsRefreshCount += 1 + } + + if topSitesRefreshed { + topSitesRefreshCount += 1 + } + } + + func willInvalidateDataSources(forceHighlights highlights: Bool, forceTopSites topSites: Bool) { + willInvalidateCount += 1 + } +} + +class PanelDataObserversTests: XCTestCase { + func testActivityStreamDelegates() { + let profile = MockProfile() + let observer = ActivityStreamDataObserver(profile: profile) + let delegate = MockDataObserverDelegate() + observer.delegate = delegate + + NotificationCenter.default.post(name: NotificationFirefoxAccountChanged, + object: nil) + NotificationCenter.default.post(name: NotificationProfileDidFinishSyncing, + object: nil) + NotificationCenter.default.post(name: NotificationPrivateDataClearedHistory, + object: nil) + + waitForCondition(timeout: 5) { delegate.didInvalidateCount == 3 && delegate.willInvalidateCount == 3 } + } + + func testHighlightsCacheInvalidation20Min() { + let profile = MockProfile() + let observer = ActivityStreamDataObserver(profile: profile) + let delegate = MockDataObserverDelegate() + observer.delegate = delegate + + // Set to 20min since refresh + profile.prefs.setLong(Date.now() - (OneMinuteInMilliseconds * 20), forKey: PrefsKeys.ASLastInvalidation) + observer.refreshIfNeeded(forceHighlights: false, forceTopSites: false) + waitForCondition(timeout: 5) { delegate.highlightsRefreshCount == 1 } + } + + func testHighlightEmptyCache() { + let profile = MockProfile() + let observer = ActivityStreamDataObserver(profile: profile) + let delegate = MockDataObserverDelegate() + observer.delegate = delegate + + // Set to no validation key + profile.prefs.removeObjectForKey(PrefsKeys.ASLastInvalidation) + observer.refreshIfNeeded(forceHighlights: false, forceTopSites: false) + waitForCondition(timeout: 5) { delegate.highlightsRefreshCount == 1 } + } + + func testHighlightActiveCache() { + let profile = MockProfile() + let observer = ActivityStreamDataObserver(profile: profile) + let delegate = MockDataObserverDelegate() + observer.delegate = delegate + + // Set to 10min since refresh + profile.prefs.setLong(Date.now() - (OneMinuteInMilliseconds * 10), forKey: PrefsKeys.ASLastInvalidation) + observer.refreshIfNeeded(forceHighlights: false, forceTopSites: false) + waitForCondition(timeout: 5) { delegate.didInvalidateCount == 1 && delegate.highlightsRefreshCount == 0 } + } +} diff --git a/mobile/ios/ClientTests/PingCentreTests.swift b/mobile/ios/ClientTests/PingCentreTests.swift new file mode 100644 index 0000000000..056f865ca2 --- /dev/null +++ b/mobile/ios/ClientTests/PingCentreTests.swift @@ -0,0 +1,104 @@ +/* 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/. */ + +@testable import Client +import XCTest +import JSONSchema +import Alamofire +import Deferred +import Shared +@testable import SyncTelemetry + +private let mockTopic = PingCentreTopic(name: "ios-mock", schema: Schema([ + "type": "object", + "properties": [ + "title": ["type": "string"] + ], + "required": [ + "title" + ] +])) + +private var receivedNetworkRequests = [URLRequest]() + +// Used to mock the network so we don't need to rely on the interweb for our unit tests. +class MockingURLProtocol: URLProtocol { + override class func canInit(with request: URLRequest) -> Bool { + return request.url?.scheme == "https" && request.httpMethod == "POST" + } + + override class func canonicalRequest(for request: URLRequest) -> URLRequest { + return request + } + + override func startLoading() { + receivedNetworkRequests.append(request) + let response = HTTPURLResponse(url: request.url!, + statusCode: 200, + httpVersion: "HTTP/1.1", + headerFields: [:]) + + self.client?.urlProtocol(self, didReceive: response!, cacheStoragePolicy: .notAllowed) + self.client?.urlProtocol(self, didLoad: "".data(using: String.Encoding.utf8)!) + self.client?.urlProtocolDidFinishLoading(self) + } + + override func stopLoading() { + //no-op + } +} + +class PingCentreTests: XCTestCase { + var manager: SessionManager! + var client: PingCentreClient! + + override func setUp() { + super.setUp() + + let configuration = URLSessionConfiguration.default + configuration.protocolClasses!.insert(MockingURLProtocol.self, at: 0) + + self.manager = SessionManager(configuration: configuration) + self.client = DefaultPingCentreImpl(topic: mockTopic, endpoint: .staging, clientID: "fakeID", manager: self.manager) + } + + override func tearDown() { + receivedNetworkRequests = [] + } + + func testSendPing() { + let validPing = [ + "title": "Test!" + ] + let invalidPing = [String: AnyObject]() + + client.sendPing(validPing, validate: true).succeeded() + let validationError = client.sendPing(invalidPing, validate: true).value + XCTAssertNotNil(validationError.failureValue) + XCTAssertTrue(validationError.failureValue! is PingValidationError) + + // Double check that we actually sent the successful ping and not the invalid one + XCTAssertTrue(receivedNetworkRequests.count == 1) + } + + func testSendBatch() { + let validPingA = ["title": "A"] + let validPingB = ["title": "B"] + let invalidPingC = [String: AnyObject]() + + client.sendBatch([validPingA, validPingB], validate: true).succeeded() + let validationError = client.sendBatch([validPingA, invalidPingC], validate: true).value + + XCTAssertNotNil(validationError.failureValue) + XCTAssertTrue(validationError.failureValue! is PingValidationError) + + // Double check that we actually sent the successful ping and not the invalid one + XCTAssertTrue(receivedNetworkRequests.count == 1) + + client.sendBatch([], validate: true).succeeded() + + // Double check that we didn't send the empty payloads request + XCTAssertTrue(receivedNetworkRequests.count == 1) + } +} diff --git a/mobile/ios/ClientTests/PocketFeedTests.swift b/mobile/ios/ClientTests/PocketFeedTests.swift new file mode 100644 index 0000000000..6c07793cf3 --- /dev/null +++ b/mobile/ios/ClientTests/PocketFeedTests.swift @@ -0,0 +1,67 @@ +/* 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 UIKit +import GCDWebServers +import XCTest + +@testable import Client + +class PocketStoriesTests: XCTestCase { + + var pocketAPI: String! + let webServer: GCDWebServer = GCDWebServer() + + /// Setup a basic web server that binds to a random port and that has one default handler on /hello + fileprivate func setupWebServer() { + let path = Bundle(for: type(of: self)).path(forResource: "pocketglobalfeed", ofType: "json") + let data = try! Data(contentsOf: URL(fileURLWithPath: path!)) + + webServer.addHandler(forMethod: "GET", path: "/pocketglobalfeed", request: GCDWebServerRequest.self) { (request) -> GCDWebServerResponse! in + return GCDWebServerDataResponse(data: data, contentType: "application/json") + } + + if webServer.start(withPort: 0, bonjourName: nil) == false { + XCTFail("Can't start the GCDWebServer") + } + pocketAPI = "http://localhost:\(webServer.port)/pocketglobalfeed" + } + + override func setUp() { + super.setUp() + setupWebServer() + } + + override func tearDown() { + super.tearDown() + } + + func testPocketStoriesCaching() { + let expect = expectation(description: "Pocket") + let PocketFeed = Pocket(endPoint: pocketAPI) + + PocketFeed.globalFeed(items: 4).upon { result in + let items = result + XCTAssertEqual(items.count, 2, "We are fetching a static feed. There are only 2 items in it") + self.webServer.stop() // Stop the webserver so we can check caching + + // Try again now that the webserver is down + PocketFeed.globalFeed(items: 4).upon { result in + let items = result + XCTAssertEqual(items.count, 2, "We are fetching a static feed. There are only 2 items in it") + let item = items.first + //These are all not optional so they should never be nil. + //But lets check in case someone decides to change something + XCTAssertNotNil(item?.domain, "Why") + XCTAssertNotNil(item?.imageURL, "You") + XCTAssertNotNil(item?.storyDescription, "Do") + XCTAssertNotNil(item?.title, "This") + XCTAssertNotNil(item?.url, "?") + expect.fulfill() + } + } + waitForExpectations(timeout: 10, handler: nil) + } + +} diff --git a/mobile/ios/ClientTests/PrefsTests.swift b/mobile/ios/ClientTests/PrefsTests.swift new file mode 100644 index 0000000000..8330f21f55 --- /dev/null +++ b/mobile/ios/ClientTests/PrefsTests.swift @@ -0,0 +1,100 @@ +/* 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/. */ + +@testable import Client +import Foundation +import Shared + +import XCTest + +class PrefsTests: XCTestCase { + var prefs: NSUserDefaultsPrefs! + + override func setUp() { + super.setUp() + prefs = NSUserDefaultsPrefs(prefix: "PrefsTests") + } + + override func tearDown() { + prefs.clearAll() + super.tearDown() + } + + func testClearPrefs() { + prefs.setObject("foo", forKey: "bar") + XCTAssertEqual(prefs.stringForKey("bar")!, "foo") + + // Ensure clearing prefs is branch-specific. + let otherPrefs = NSUserDefaultsPrefs(prefix: "othermockaccount") + otherPrefs.clearAll() + XCTAssertEqual(prefs.stringForKey("bar")!, "foo") + + prefs.clearAll() + XCTAssertNil(prefs.stringForKey("bar")) + } + + func testStringForKey() { + XCTAssertNil(prefs.stringForKey("key")) + prefs.setObject("value", forKey: "key") + XCTAssertEqual(prefs.stringForKey("key")!, "value") + // Non-String values return nil. + prefs.setObject(1, forKey: "key") + XCTAssertNil(prefs.stringForKey("key")) + } + + func testBoolForKey() { + XCTAssertNil(prefs.boolForKey("key")) + prefs.setObject(true, forKey: "key") + XCTAssertEqual(prefs.boolForKey("key")!, true) + prefs.setObject(false, forKey: "key") + XCTAssertEqual(prefs.boolForKey("key")!, false) + // We would like non-Bool values to return nil, but I can't figure out how to differentiate. + // Instead, this documents the undesired behaviour. + prefs.setObject(1, forKey: "key") + XCTAssertEqual(prefs.boolForKey("key")!, true) + prefs.setObject("1", forKey: "key") + XCTAssertNil(prefs.boolForKey("key")) + prefs.setObject("x", forKey: "key") + XCTAssertNil(prefs.boolForKey("key")) + } + + func testStringArrayForKey() { + XCTAssertNil(prefs.stringArrayForKey("key")) + prefs.setObject(["value1", "value2"], forKey: "key") + XCTAssertEqual(prefs.stringArrayForKey("key")!, ["value1", "value2"]) + // Non-[String] values return nil. + prefs.setObject(1, forKey: "key") + XCTAssertNil(prefs.stringArrayForKey("key")) + // [Non-String] values return nil. + prefs.setObject([1, 2], forKey: "key") + XCTAssertNil(prefs.stringArrayForKey("key")) + } + + func testMockProfilePrefsRoundtripsTimestamps() { + let prefs = MockProfilePrefs().branch("baz") + let val: Timestamp = Date.now() + prefs.setLong(val, forKey: "foobar") + XCTAssertEqual(val, prefs.unsignedLongForKey("foobar")!) + } + + func testMockProfilePrefsKeys() { + let prefs = MockProfilePrefs().branch("baz") as! MockProfilePrefs + let val: Timestamp = Date.now() + prefs.setLong(val, forKey: "foobar") + XCTAssertEqual(val, (prefs.things["baz.foobar"] as! NSNumber).uint64Value) + } + + func testMockProfilePrefsClearAll() { + let prefs1 = MockProfilePrefs().branch("bar") as! MockProfilePrefs + let prefs2 = MockProfilePrefs().branch("baz") as! MockProfilePrefs + + // Ensure clearing prefs is branch-specific. + prefs1.setInt(123, forKey: "foo") + prefs2.clearAll() + XCTAssertEqual(123, prefs1.intForKey("foo")!) + + prefs1.clearAll() + XCTAssertNil(prefs1.intForKey("foo") as AnyObject?) + } +} diff --git a/mobile/ios/ClientTests/ProfileTest.swift b/mobile/ios/ClientTests/ProfileTest.swift new file mode 100644 index 0000000000..23b997fcb7 --- /dev/null +++ b/mobile/ios/ClientTests/ProfileTest.swift @@ -0,0 +1,28 @@ +/* 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/. */ + +@testable import Client +import Foundation +import Shared +import Storage +import SwiftKeychainWrapper + +import XCTest + +/* + * A base test type for tests that need a profile. + */ +class ProfileTest: XCTestCase { + func withTestProfile(_ callback: (_ profile: Profile) -> Void) { + callback(MockProfile()) + } + + func testNewProfileClearsExistingAuthenticationInfo() { + let authInfo = AuthenticationKeychainInfo(passcode: "1234") + KeychainWrapper.sharedAppContainerKeychain.setAuthenticationInfo(authInfo) + XCTAssertNotNil(KeychainWrapper.sharedAppContainerKeychain.authenticationInfo()) + let _ = BrowserProfile(localName: "my_profile", clear: true) + XCTAssertNil(KeychainWrapper.sharedAppContainerKeychain.authenticationInfo()) + } +} diff --git a/mobile/ios/ClientTests/RelativeDatesTests.swift b/mobile/ios/ClientTests/RelativeDatesTests.swift new file mode 100644 index 0000000000..ec89b4fa2a --- /dev/null +++ b/mobile/ios/ClientTests/RelativeDatesTests.swift @@ -0,0 +1,40 @@ +/* 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 XCTest + +class RelativeDatesTests: XCTestCase { + func testRelativeDates() { + let dateOrig = Date() + var date = Date(timeInterval: 0, since: dateOrig) + + XCTAssertEqual(date.toRelativeTimeString(), "just now") + + date = Date(timeInterval: -10, since: dateOrig) + XCTAssertEqual(date.toRelativeTimeString(), "just now") + + date = Date(timeInterval: -60, since: dateOrig) + XCTAssertEqual(date.toRelativeTimeString(), ("today at " + DateFormatter.localizedString(from: date, dateStyle: DateFormatter.Style.none, timeStyle: DateFormatter.Style.short))) + + let calendar = Calendar.autoupdatingCurrent + date = calendar.date(byAdding: .day, value: -1, to: dateOrig)! + XCTAssertEqual(date.toRelativeTimeString(), "yesterday") + + date = calendar.date(byAdding: .day, value: -2, to: dateOrig)! + XCTAssertEqual(date.toRelativeTimeString(), "this week") + + date = calendar.date(byAdding: .day, value: -7, to: dateOrig)! + XCTAssertEqual(date.toRelativeTimeString(), "more than a week ago") + + date = calendar.date(byAdding: .day, value: -7 * 5, to: dateOrig)! + XCTAssertEqual(date.toRelativeTimeString(), "more than a month ago") + + date = Date(timeInterval: -60 * 60 * 24 * 7 * 5 * 2, since: dateOrig) + XCTAssertEqual(date.toRelativeTimeString(), DateFormatter.localizedString(from: date, dateStyle: DateFormatter.Style.short, timeStyle: DateFormatter.Style.short)) + + date = Date(timeInterval: -60 * 60 * 24 * 7 * 5 * 12 * 2, since: dateOrig) + XCTAssertEqual(date.toRelativeTimeString(), DateFormatter.localizedString(from: date, dateStyle: DateFormatter.Style.short, timeStyle: DateFormatter.Style.short)) + } +} diff --git a/mobile/ios/ClientTests/ResetTests.swift b/mobile/ios/ClientTests/ResetTests.swift new file mode 100644 index 0000000000..06e8e794c8 --- /dev/null +++ b/mobile/ios/ClientTests/ResetTests.swift @@ -0,0 +1,78 @@ +/* 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/. */ + +@testable import Client +import Shared +@testable import Storage +import Sync +import UIKit + +import XCTest + +class MockBrowserProfile: BrowserProfile { + var peekSyncManager: BrowserSyncManager { + return self.syncManager as! BrowserSyncManager + } + + var peekTabs: SQLiteRemoteClientsAndTabs { + return self.remoteClientsAndTabs as! SQLiteRemoteClientsAndTabs + } +} + +class MockEngineStateChanges: EngineStateChanges { + var collections: [String] = [] + var enabled: [String] = [] + var disabled: [String] = [] + var clearWasCalled: Bool = false + + func collectionsThatNeedLocalReset() -> [String] { + return self.collections + } + + func enginesEnabled() -> [String] { + return self.enabled + } + + func enginesDisabled() -> [String] { + return self.disabled + } + + func clearLocalCommands() { + clearWasCalled = true + } +} + +func assertClientsHaveGUIDsFromStorage(_ storage: RemoteClientsAndTabs, expected: [GUID]) { + let recs = storage.getClients().value.successValue + XCTAssertNotNil(recs) + XCTAssertEqual(expected, recs!.map { $0.guid! }) +} + +class ResetTests: XCTestCase { + func testResetting() { + let profile = MockBrowserProfile(localName: "testResetTests") + + // Add a client. + let tabs = profile.peekTabs + XCTAssertTrue(tabs.insertOrUpdateClient(RemoteClient(guid: "abcdefghijkl", name: "Remote", modified: Date.now(), type: "mobile", formfactor: "tablet", os: "Windows", version: "55.0.1a", fxaDeviceId: "fxa1")).value.isSuccess) + _ = tabs.replaceRemoteDevices([RemoteDevice(id: "fxa1", name: "Device 1", type: "desktop", isCurrentDevice: false, lastAccessTime: 123)]).succeeded() + + // Verify that it's there. + assertClientsHaveGUIDsFromStorage(tabs, expected: ["abcdefghijkl"]) + + // Tell the sync manager that "clients" has changed syncID. + let e = MockEngineStateChanges() + e.collections.append("clients") + + XCTAssertTrue(profile.peekSyncManager.takeActionsOnEngineStateChanges(e).value.isSuccess) + + // We threw away the command. + XCTAssertTrue(e.clearWasCalled) + + // And now we have no local clients. + let empty = tabs.getClients().value.successValue + XCTAssertNotNil(empty) + XCTAssertEqual(empty!, []) + } +} diff --git a/mobile/ios/ClientTests/SearchEnginesTests.swift b/mobile/ios/ClientTests/SearchEnginesTests.swift new file mode 100644 index 0000000000..f03ecb8bee --- /dev/null +++ b/mobile/ios/ClientTests/SearchEnginesTests.swift @@ -0,0 +1,174 @@ +/* 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/. */ + +@testable import Client +import Foundation +import XCTest +import Shared + +private let DefaultSearchEngineName = "Google" +private let ExpectedEngineNames = ["Amazon.com", "Bing", "DuckDuckGo", "Google", "Twitter", "Wikipedia", "Yahoo"] + +class SearchEnginesTests: XCTestCase { + + func testIncludesExpectedEngines() { + // Verify that the set of shipped engines includes the expected subset. + let profile = MockProfile() + let engines = SearchEngines(prefs: profile.prefs, files: profile.files).orderedEngines + XCTAssertTrue((engines?.count)! >= ExpectedEngineNames.count) + + for engineName in ExpectedEngineNames { + XCTAssertTrue(((engines?.filter { engine in engine.shortName == engineName })?.count)! > 0) + } + } + + func testDefaultEngineOnStartup() { + // If this is our first run, Yahoo should be first for the en locale. + let profile = MockProfile() + let engines = SearchEngines(prefs: profile.prefs, files: profile.files) + XCTAssertEqual(engines.defaultEngine.shortName, DefaultSearchEngineName) + XCTAssertEqual(engines.orderedEngines[0].shortName, DefaultSearchEngineName) + } + + func testAddingAndDeletingCustomEngines() { + let testEngine = OpenSearchEngine(engineID: "ATester", shortName: "ATester", image: UIImage(), searchTemplate: "http://firefox.com/find?q={searchTerm}", suggestTemplate: nil, isCustomEngine: true) + let profile = MockProfile() + let engines = SearchEngines(prefs: profile.prefs, files: profile.files) + engines.addSearchEngine(testEngine) + XCTAssertEqual(engines.orderedEngines[1].engineID, testEngine.engineID) + + engines.deleteCustomEngine(testEngine) + let deleted = engines.orderedEngines.filter {$0 == testEngine} + XCTAssertEqual(deleted, []) + } + + func testDefaultEngine() { + let profile = MockProfile() + let engines = SearchEngines(prefs: profile.prefs, files: profile.files) + let engineSet = engines.orderedEngines + + engines.defaultEngine = (engineSet?[0])! + XCTAssertTrue(engines.isEngineDefault((engineSet?[0])!)) + XCTAssertFalse(engines.isEngineDefault((engineSet?[1])!)) + // The first ordered engine is the default. + XCTAssertEqual(engines.orderedEngines[0].shortName, engineSet?[0].shortName) + + engines.defaultEngine = (engineSet?[1])! + XCTAssertFalse(engines.isEngineDefault((engineSet?[0])!)) + XCTAssertTrue(engines.isEngineDefault((engineSet?[1])!)) + // The first ordered engine is the default. + XCTAssertEqual(engines.orderedEngines[0].shortName, engineSet?[1].shortName) + + let engines2 = SearchEngines(prefs: profile.prefs, files: profile.files) + // The default engine should have been persisted. + XCTAssertTrue(engines2.isEngineDefault((engineSet?[1])!)) + // The first ordered engine is the default. + XCTAssertEqual(engines.orderedEngines[0].shortName, engineSet?[1].shortName) + } + + func testOrderedEngines() { + let profile = MockProfile() + let engines = SearchEngines(prefs: profile.prefs, files: profile.files) + + engines.orderedEngines = [ExpectedEngineNames[4], ExpectedEngineNames[2], ExpectedEngineNames[0]].map { name in + for engine in engines.orderedEngines { + if engine.shortName == name { + return engine + } + } + XCTFail("Could not find engine: \(name)") + return engines.orderedEngines.first! + } + XCTAssertEqual(engines.orderedEngines[0].shortName, ExpectedEngineNames[4]) + XCTAssertEqual(engines.orderedEngines[1].shortName, ExpectedEngineNames[2]) + XCTAssertEqual(engines.orderedEngines[2].shortName, ExpectedEngineNames[0]) + + let engines2 = SearchEngines(prefs: profile.prefs, files: profile.files) + // The ordering should have been persisted. + XCTAssertEqual(engines2.orderedEngines[0].shortName, ExpectedEngineNames[4]) + XCTAssertEqual(engines2.orderedEngines[1].shortName, ExpectedEngineNames[2]) + XCTAssertEqual(engines2.orderedEngines[2].shortName, ExpectedEngineNames[0]) + + // Remaining engines should be appended in alphabetical order. + XCTAssertEqual(engines2.orderedEngines[3].shortName, ExpectedEngineNames[1]) + XCTAssertEqual(engines2.orderedEngines[4].shortName, ExpectedEngineNames[3]) + XCTAssertEqual(engines2.orderedEngines[5].shortName, ExpectedEngineNames[5]) + XCTAssertEqual(engines2.orderedEngines[6].shortName, ExpectedEngineNames[6]) + } + + func testQuickSearchEngines() { + let profile = MockProfile() + let engines = SearchEngines(prefs: profile.prefs, files: profile.files) + let engineSet = engines.orderedEngines + + // You can't disable the default engine. + engines.defaultEngine = (engineSet?[1])! + engines.disableEngine((engineSet?[1])!) + XCTAssertTrue(engines.isEngineEnabled((engineSet?[1])!)) + + // The default engine is not included in the quick search engines. + XCTAssertEqual(0, engines.quickSearchEngines.filter { engine in engine.shortName == engineSet?[1].shortName }.count) + + // Enable and disable work. + engines.enableEngine((engineSet?[0])!) + XCTAssertTrue(engines.isEngineEnabled((engineSet?[0])!)) + XCTAssertEqual(1, engines.quickSearchEngines.filter { engine in engine.shortName == engineSet?[0].shortName }.count) + + engines.disableEngine((engineSet?[0])!) + XCTAssertFalse(engines.isEngineEnabled((engineSet?[0])!)) + XCTAssertEqual(0, engines.quickSearchEngines.filter { engine in engine.shortName == engineSet?[0].shortName }.count) + + // Setting the default engine enables it. + engines.defaultEngine = (engineSet?[0])! + XCTAssertTrue(engines.isEngineEnabled((engineSet?[1])!)) + + // Setting the order may change the default engine, which enables it. + engines.orderedEngines = [(engineSet?[2])!, (engineSet?[1])!, (engineSet?[0])!] + XCTAssertTrue(engines.isEngineDefault((engineSet?[2])!)) + XCTAssertTrue(engines.isEngineEnabled((engineSet?[2])!)) + + // The enabling should be persisted. + engines.enableEngine((engineSet?[2])!) + engines.disableEngine((engineSet?[1])!) + engines.enableEngine((engineSet?[0])!) + + let engines2 = SearchEngines(prefs: profile.prefs, files: profile.files) + XCTAssertTrue(engines2.isEngineEnabled((engineSet?[2])!)) + XCTAssertFalse(engines2.isEngineEnabled((engineSet?[1])!)) + XCTAssertTrue(engines2.isEngineEnabled((engineSet?[0])!)) + } + + func testSearchSuggestionSettings() { + let profile = MockProfile() + let engines = SearchEngines(prefs: profile.prefs, files: profile.files) + + // By default, you should see search suggestions + XCTAssertTrue(engines.shouldShowSearchSuggestions) + + // Setting should be persisted. + engines.shouldShowSearchSuggestions = false + + let engines2 = SearchEngines(prefs: profile.prefs, files: profile.files) + XCTAssertFalse(engines2.shouldShowSearchSuggestions) + } + + func testDirectoriesForLanguageIdentifier() { + XCTAssertEqual( + SearchEngines.directoriesForLanguageIdentifier("nl", basePath: "/tmp", fallbackIdentifier: "en"), + ["/tmp/nl", "/tmp/en"] + ) + XCTAssertEqual( + SearchEngines.directoriesForLanguageIdentifier("en-US", basePath: "/tmp", fallbackIdentifier: "en"), + ["/tmp/en-US", "/tmp/en"] + ) + XCTAssertEqual( + SearchEngines.directoriesForLanguageIdentifier("es-MX", basePath: "/tmp", fallbackIdentifier: "en"), + ["/tmp/es-MX", "/tmp/es", "/tmp/en"] + ) + XCTAssertEqual( + SearchEngines.directoriesForLanguageIdentifier("zh-Hans-CN", basePath: "/tmp", fallbackIdentifier: "en"), + ["/tmp/zh-Hans-CN", "/tmp/zh-CN", "/tmp/zh", "/tmp/en"] + ) + } +} diff --git a/mobile/ios/ClientTests/SearchTests.swift b/mobile/ios/ClientTests/SearchTests.swift new file mode 100644 index 0000000000..3c008cede4 --- /dev/null +++ b/mobile/ios/ClientTests/SearchTests.swift @@ -0,0 +1,155 @@ +/* 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 GCDWebServers +@testable import Client +import UIKit + +import XCTest + +class SearchTests: XCTestCase { + func testParsing() { + let parser = OpenSearchParser(pluginMode: true) + let file = Bundle.main.path(forResource: "google", ofType: "xml", inDirectory: "SearchPlugins/en") + let engine: OpenSearchEngine! = parser.parse(file!, engineID: "google") + XCTAssertEqual(engine.shortName, "Google") + + // Test regular search queries. + XCTAssertEqual(engine.searchURLForQuery("foobar")!.absoluteString, "https://www.google.com/search?q=foobar&ie=utf-8&oe=utf-8&client=firefox-b") + + // Test search suggestion queries. + XCTAssertEqual(engine.suggestURLForQuery("foobar")!.absoluteString, "https://www.google.com/complete/search?client=firefox&q=foobar") + } + + func testURIFixup() { + // Check valid URLs. We can load these after some fixup. + checkValidURL("http://www.mozilla.org", afterFixup: "http://www.mozilla.org") + checkValidURL("about:", afterFixup: "about:") + checkValidURL("about:config", afterFixup: "about:config") + checkValidURL("about: config", afterFixup: "about:%20config") + checkValidURL("file:///f/o/o", afterFixup: "file:///f/o/o") + checkValidURL("ftp://ftp.mozilla.org", afterFixup: "ftp://ftp.mozilla.org") + checkValidURL("foo.bar", afterFixup: "http://foo.bar") + checkValidURL(" foo.bar ", afterFixup: "http://foo.bar") + checkValidURL("1.2.3", afterFixup: "http://1.2.3") + + // Check invalid URLs. These are passed along to the default search engine. + checkInvalidURL("foobar") + checkInvalidURL("foo bar") + checkInvalidURL("mozilla. org") + checkInvalidURL("123") + checkInvalidURL("a/b") + checkInvalidURL("创业咖啡") + checkInvalidURL("创业咖啡 中国") + checkInvalidURL("创业咖啡. 中国") + } + + func testURIFixupPunyCode() { + checkValidURL("http://创业咖啡.中国/", afterFixup: "http://xn--vhq70hq9bhxa.xn--fiqs8s/") + checkValidURL("创业咖啡.中国", afterFixup: "http://xn--vhq70hq9bhxa.xn--fiqs8s") + checkValidURL(" 创业咖啡.中国 ", afterFixup: "http://xn--vhq70hq9bhxa.xn--fiqs8s") + } + + fileprivate func checkValidURL(_ beforeFixup: String, afterFixup: String) { + XCTAssertEqual(URIFixup.getURL(beforeFixup)!.absoluteString, afterFixup) + } + + fileprivate func checkInvalidURL(_ beforeFixup: String) { + XCTAssertNil(URIFixup.getURL(beforeFixup)) + } + + func testSuggestClient() { + let webServerBase = startMockSuggestServer() + let engine = OpenSearchEngine(engineID: "mock", shortName: "Mock engine", image: UIImage(), searchTemplate: "", suggestTemplate: "\(webServerBase)?q={searchTerms}", + isCustomEngine: false) + let client = SearchSuggestClient(searchEngine: engine, userAgent: "Fx-testSuggestClient") + + let query1 = self.expectation(description: "foo query") + client.query("foo", callback: { response, error in + withExtendedLifetime(client) { + if error != nil { + XCTFail("Error: \(error?.description ?? "nil")") + } + + XCTAssertEqual(response![0], "foo") + XCTAssertEqual(response![1], "foo2") + XCTAssertEqual(response![2], "foo you") + + query1.fulfill() + } + }) + waitForExpectations(timeout: 10, handler: nil) + + let query2 = self.expectation(description: "foo bar query") + client.query("foo bar", callback: { response, error in + withExtendedLifetime(client) { + if error != nil { + XCTFail("Error: \(error?.description ?? "nil")") + } + + XCTAssertEqual(response![0], "foo bar soap") + XCTAssertEqual(response![1], "foo barstool") + XCTAssertEqual(response![2], "foo bartender") + + query2.fulfill() + } + }) + waitForExpectations(timeout: 10, handler: nil) + } + + func testExtractingOfSearchTermsFromURL() { + let parser = OpenSearchParser(pluginMode: true) + var file = Bundle.main.path(forResource: "google", ofType: "xml", inDirectory: "SearchPlugins/en") + let googleEngine: OpenSearchEngine! = parser.parse(file!, engineID: "google") + + // create URL + let searchTerm = "Foo Bar" + let encodedSeachTerm = searchTerm.replacingOccurrences(of: " ", with: "+") + let googleSearchURL = URL(string: "https://www.google.com/search?q=\(encodedSeachTerm)&ie=utf-8&oe=utf-8&gws_rd=cr&ei=I0UyVp_qK4HtUoytjagM") + let duckDuckGoSearchURL = URL(string: "https://duckduckgo.com/?q=\(encodedSeachTerm)&ia=about") + let invalidSearchURL = URL(string: "https://www.google.co.uk") + + // check it correctly matches google search term given google config + XCTAssertEqual(searchTerm, googleEngine.queryForSearchURL(googleSearchURL)) + + // check it doesn't match when the URL is not a search URL + XCTAssertNil(googleEngine.queryForSearchURL(invalidSearchURL)) + + // check that it matches given a different configuration + file = Bundle.main.path(forResource: "duckduckgo", ofType: "xml", inDirectory: "SearchPlugins/en") + let duckDuckGoEngine: OpenSearchEngine! = parser.parse(file!, engineID: "duckduckgo") + XCTAssertEqual(searchTerm, duckDuckGoEngine.queryForSearchURL(duckDuckGoSearchURL)) + + // check it doesn't match search URLs for different configurations + XCTAssertNil(duckDuckGoEngine.queryForSearchURL(googleSearchURL)) + + // check that if you pass in a nil URL that everything works + XCTAssertNil(duckDuckGoEngine.queryForSearchURL(nil)) + } + + fileprivate func startMockSuggestServer() -> String { + let webServer: GCDWebServer = GCDWebServer() + + webServer.addHandler(forMethod: "GET", path: "/", request: GCDWebServerRequest.self) { (request) -> GCDWebServerResponse! in + var suggestions: [String]! + let query = request?.query["q"] as! String + switch query { + case "foo": + suggestions = ["foo", "foo2", "foo you"] + case "foo bar": + suggestions = ["foo bar soap", "foo barstool", "foo bartender"] + default: + XCTFail("Unexpected query: \(query)") + } + return GCDWebServerDataResponse(jsonObject: [query, suggestions]) + } + + if !webServer.start(withPort: 0, bonjourName: nil) { + XCTFail("Can't start the GCDWebServer") + } + + return "http://localhost:\(webServer.port)" + } +} diff --git a/mobile/ios/ClientTests/StringExtensionsTests.swift b/mobile/ios/ClientTests/StringExtensionsTests.swift new file mode 100644 index 0000000000..cc0599efc2 --- /dev/null +++ b/mobile/ios/ClientTests/StringExtensionsTests.swift @@ -0,0 +1,74 @@ +/* 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 XCTest + +class StringExtensionsTests: XCTestCase { + + func testStartsWith() { + XCTAssertTrue("abcde".startsWith("abcde")) + XCTAssertTrue("abcde".startsWith("")) + XCTAssertTrue("abcde".startsWith("a")) + XCTAssertTrue("abcdea".startsWith("a")) + XCTAssertFalse("abcde".startsWith("fa")) + XCTAssertFalse("abcde".startsWith("af")) + XCTAssertFalse("abcde".startsWith("b")) + } + + func testEndsWith() { + XCTAssertTrue("abcde".endsWith("abcde")) + XCTAssertTrue("abcde".endsWith("")) + XCTAssertTrue("abcde".endsWith("e")) + XCTAssertTrue("abcdea".endsWith("a")) + XCTAssertFalse("abcde".endsWith("fe")) + XCTAssertFalse("abcde".endsWith("ef")) + XCTAssertFalse("abcde".endsWith("d")) + } + + func testEllipsize() { + // Odd maxLength. Note that we ellipsize with a Unicode join character to avoid wrapping. + XCTAssertEqual("abcd…\u{2060}fgh", "abcdefgh".ellipsize(maxLength: 7)) + + // Even maxLength. + XCTAssertEqual("abcd…\u{2060}ijkl", "abcdefghijkl".ellipsize(maxLength: 8)) + + // String shorter than maxLength. + XCTAssertEqual("abcd", "abcd".ellipsize(maxLength: 7)) + + // Empty String. + XCTAssertEqual("", "".ellipsize(maxLength: 8)) + + // maxLength < 2. + XCTAssertEqual("abcdefgh", "abcdefgh".ellipsize(maxLength: 0)) + } + + func testStringByTrimmingLeadingCharactersInSet() { + XCTAssertEqual("foo ", " foo ".stringByTrimmingLeadingCharactersInSet(CharacterSet.whitespaces)) + XCTAssertEqual("foo456", "123foo456".stringByTrimmingLeadingCharactersInSet(CharacterSet.decimalDigits)) + XCTAssertEqual("", "123456".stringByTrimmingLeadingCharactersInSet(CharacterSet.decimalDigits)) + } + + func testStringSplitWithNewline() { + XCTAssertEqual("", "".stringSplitWithNewline()) + XCTAssertEqual("foo", "foo".stringSplitWithNewline()) + XCTAssertEqual("aaa\n bbb", "aaa bbb".stringSplitWithNewline()) + XCTAssertEqual("Mark as\n Read", "Mark as Read".stringSplitWithNewline()) + XCTAssertEqual("aa\n bbbbbb", "aa bbbbbb".stringSplitWithNewline()) + } + + func testPercentEscaping() { + func roundtripTest(_ input: String, _ expected: String, file: StaticString = #file, line: UInt = #line) { + let observed = input.escape()! + XCTAssertEqual(observed, expected, "input is \(input)", file: file, line: line) + let roundtrip = observed.unescape() + XCTAssertEqual(roundtrip, input, "encoded is \(observed)", file: file, line: line) + } + + roundtripTest("https://mozilla.com", "https://mozilla.com") + roundtripTest("http://www.cnn.com/2017/09/25/politics/north-korea-fm-us-bombers/index.html", "http://www.cnn.com/2017/09/25/politics/north-korea-fm-us-bombers/index.html") + roundtripTest("http://mozilla.com/?a=foo&b=bar", "http://mozilla.com/%3Fa%3Dfoo%26b%3Dbar") + } + +} diff --git a/mobile/ios/ClientTests/SyncStatusResolverTests.swift b/mobile/ios/ClientTests/SyncStatusResolverTests.swift new file mode 100644 index 0000000000..44e298cb52 --- /dev/null +++ b/mobile/ios/ClientTests/SyncStatusResolverTests.swift @@ -0,0 +1,109 @@ +/* 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/. */ + +@testable import Client +@testable import Sync + +import Shared +import Storage +import XCTest + +private class RandomError: MaybeErrorType { + var description = "random_error" +} + +class SyncStatusResolverTests: XCTestCase { + + private func mockStatsForCollection(collection: String) -> SyncEngineStatsSession { + return SyncEngineStatsSession(collection: collection) + } + + func testAllCompleted() { + let results: EngineResults = [ + ("tabs", .completed(mockStatsForCollection(collection: "tabs"))), + ("clients", .completed(mockStatsForCollection(collection: "clients"))) + ] + let maybeResults = Maybe(success: results) + + let resolver = SyncStatusResolver(engineResults: maybeResults) + XCTAssertTrue(resolver.resolveResults() == SyncDisplayState.good) + } + + func testAllCompletedExceptOneDisabledRemotely() { + let results: EngineResults = [ + ("tabs", .completed(mockStatsForCollection(collection: "tabs"))), + ("clients", .notStarted(.engineRemotelyNotEnabled(collection: "clients"))) + ] + let maybeResults = Maybe(success: results) + + let resolver = SyncStatusResolver(engineResults: maybeResults) + XCTAssertTrue(resolver.resolveResults() == SyncDisplayState.good) + } + + func testAllCompletedExceptNotStartedBecauseNoAccount() { + let results: EngineResults = [ + ("tabs", .completed(mockStatsForCollection(collection: "tabs"))), + ("clients", .notStarted(.noAccount)) + ] + let maybeResults = Maybe(success: results) + + let resolver = SyncStatusResolver(engineResults: maybeResults) + XCTAssertTrue(resolver.resolveResults() == SyncDisplayState.warning(message: Strings.FirefoxSyncOfflineTitle)) + } + + func testAllCompletedExceptNotStartedBecauseOffline() { + let results: EngineResults = [ + ("tabs", .completed(mockStatsForCollection(collection: "tabs"))), + ("clients", .notStarted(.offline)) + ] + let maybeResults = Maybe(success: results) + + let resolver = SyncStatusResolver(engineResults: maybeResults) + XCTAssertTrue(resolver.resolveResults() == SyncDisplayState.bad(message: Strings.FirefoxSyncOfflineTitle)) + } + + func testOfflineAndNoAccount() { + let results: EngineResults = [ + ("tabs", .notStarted(.noAccount)), + ("clients", .notStarted(.offline)) + ] + + let maybeResults = Maybe(success: results) + + let resolver = SyncStatusResolver(engineResults: maybeResults) + XCTAssertTrue(resolver.resolveResults() == SyncDisplayState.bad(message: Strings.FirefoxSyncOfflineTitle)) + } + + func testAllPartial() { + let results: EngineResults = [ + ("tabs", .partial(SyncEngineStatsSession(collection: "tabs"))), + ("clients", .partial(SyncEngineStatsSession(collection: "clients"))) + ] + let maybeResults = Maybe(success: results) + + let resolver = SyncStatusResolver(engineResults: maybeResults) + XCTAssertTrue(resolver.resolveResults() == SyncDisplayState.good) + } + + func testBookmarkMergeError() { + let maybeResults: Maybe = Maybe(failure: BookmarksMergeError()) + let resolver = SyncStatusResolver(engineResults: maybeResults) + let expected = SyncDisplayState.warning(message: String(format: Strings.FirefoxSyncPartialTitle, Strings.localizedStringForSyncComponent("bookmarks") ?? "")) + XCTAssertTrue(resolver.resolveResults() == expected) + } + + func testBufferInvalidError() { + let maybeResults: Maybe = Maybe(failure: BufferInvalidError(inconsistencies: [:], validationDuration: 0)) + let resolver = SyncStatusResolver(engineResults: maybeResults) + let expected = SyncDisplayState.warning(message: String(format: Strings.FirefoxSyncPartialTitle, Strings.localizedStringForSyncComponent("bookmarks") ?? "")) + XCTAssertTrue(resolver.resolveResults() == expected) + } + + func testRandomFailure() { + let maybeResults: Maybe = Maybe(failure: RandomError()) + let resolver = SyncStatusResolver(engineResults: maybeResults) + let expected = SyncDisplayState.bad(message: nil) + XCTAssertTrue(resolver.resolveResults() == expected) + } +} diff --git a/mobile/ios/ClientTests/TabEventHandlerTests.swift b/mobile/ios/ClientTests/TabEventHandlerTests.swift new file mode 100644 index 0000000000..06f546e527 --- /dev/null +++ b/mobile/ios/ClientTests/TabEventHandlerTests.swift @@ -0,0 +1,87 @@ +/* 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 +@testable import Client +import WebKit + +import XCTest + +class TabEventHandlerTests: XCTestCase { + + func testEventDelivery() { + let tab = Tab(configuration: WKWebViewConfiguration()) + let handler = DummyHandler() + + XCTAssertNil(handler.isFocused) + + TabEvent.post(.didGainFocus, for: tab) + XCTAssertTrue(handler.isFocused!) + + TabEvent.post(.didLoseFocus, for: tab) + XCTAssertFalse(handler.isFocused!) + } + + func testUnregistration() { + let tab = Tab(configuration: WKWebViewConfiguration()) + let handler = DummyHandler() + + XCTAssertNil(handler.isFocused) + + TabEvent.post(.didGainFocus, for: tab) + XCTAssertTrue(handler.isFocused!) + + handler.doUnregister() + TabEvent.post(.didLoseFocus, for: tab) + // The event didn't reach us, so we should still be focused. + XCTAssertTrue(handler.isFocused!) + } + + func testOnlyRegisteredForEvents() { + let tab = Tab(configuration: WKWebViewConfiguration()) + let handler = DummyHandler() + handler.doUnregister() + + let tabObservers = handler.registerFor(.didGainFocus) + + XCTAssertNil(handler.isFocused) + + TabEvent.post(.didGainFocus, for: tab) + XCTAssertTrue(handler.isFocused!) + + TabEvent.post(.didLoseFocus, for: tab) + XCTAssertTrue(handler.isFocused!) + + handler.unregister(tabObservers) + } +} + + +class DummyHandler: TabEventHandler { + var tabObservers: TabObservers! + + // This is not how this should be written in production — the handler shouldn't be keeping track + // of individual tab state. + var isFocused: Bool? = nil + + init() { + tabObservers = registerFor(.didGainFocus, .didLoseFocus) + } + + deinit { + doUnregister() + } + + fileprivate func doUnregister() { + unregister(tabObservers) + } + + func tabDidGainFocus(_ tab: Tab) { + isFocused = true + } + + func tabDidLoseFocus(_ tab: Tab) { + isFocused = false + } +} diff --git a/mobile/ios/ClientTests/TabManagerTests.swift b/mobile/ios/ClientTests/TabManagerTests.swift new file mode 100644 index 0000000000..a169ce1226 --- /dev/null +++ b/mobile/ios/ClientTests/TabManagerTests.swift @@ -0,0 +1,457 @@ +/* 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/. */ + +@testable import Client +import Shared +import Storage +import UIKit +import WebKit +import Deferred + +import XCTest + +open class TabManagerMockProfile: MockProfile { + var numberOfTabsStored = 0 + override public func storeTabs(_ tabs: [RemoteTab]) -> Deferred> { + numberOfTabsStored = tabs.count + return deferMaybe(tabs.count) + } +} + +open class MockTabManagerStateDelegate: TabManagerStateDelegate { + var numberOfTabsStored = 0 + public func tabManagerWillStoreTabs(_ tabs: [Tab]) { + numberOfTabsStored = tabs.count + } +} + +struct MethodSpy { + let functionName: String + let method: ((_ tabs: [Tab?]) -> Void)? + + init(functionName: String) { + self.functionName = functionName + self.method = nil + } + + init(functionName: String, method: ((_ tabs: [Tab?]) -> Void)?) { + self.functionName = functionName + self.method = method + } +} + +open class MockTabManagerDelegate: TabManagerDelegate { + + //this array represents the order in which delegate methods should be called. + //each delegate method will pop the first struct from the array. If the method name doesn't match the struct then the order is incorrect + //Then it evaluates the method closure which will return true/false depending on if the tabs are correct + var methodCatchers: [MethodSpy] = [] + + func expect(_ methods: [MethodSpy]) { + self.methodCatchers = methods + } + + func verify(_ message: String) { + XCTAssertTrue(methodCatchers.isEmpty, message) + } + + func testDelegateMethodWithName(_ name: String, tabs: [Tab?]) { + guard let spy = self.methodCatchers.first else { + XCTAssert(false, "No method was availible in the queue. For the delegate method \(name) to use") + return + } + XCTAssertEqual(spy.functionName, name) + if let methodCheck = spy.method { + methodCheck(tabs) + } + methodCatchers.removeFirst() + } + + public func tabManager(_ tabManager: TabManager, didSelectedTabChange selected: Tab?, previous: Tab?) { + testDelegateMethodWithName(#function, tabs: [selected, previous]) + } + + public func tabManager(_ tabManager: TabManager, didAddTab tab: Tab) { + testDelegateMethodWithName(#function, tabs: [tab]) + } + + public func tabManager(_ tabManager: TabManager, didRemoveTab tab: Tab) { + testDelegateMethodWithName(#function, tabs: [tab]) + } + + public func tabManagerDidRestoreTabs(_ tabManager: TabManager) { + testDelegateMethodWithName(#function, tabs: []) + } + + public func tabManager(_ tabManager: TabManager, willRemoveTab tab: Tab) { + testDelegateMethodWithName(#function, tabs: [tab]) + } + + public func tabManager(_ tabManager: TabManager, willAddTab tab: Tab) { + testDelegateMethodWithName(#function, tabs: [tab]) + } + + public func tabManagerDidAddTabs(_ tabManager: TabManager) { + testDelegateMethodWithName(#function, tabs: []) + } + + public func tabManagerDidRemoveAllTabs(_ tabManager: TabManager, toast: ButtonToast?) { + testDelegateMethodWithName(#function, tabs: []) + } +} + +class TabManagerTests: XCTestCase { + + let willRemove = MethodSpy(functionName: "tabManager(_:willRemoveTab:)") + let didRemove = MethodSpy(functionName: "tabManager(_:didRemoveTab:)") + let willAdd = MethodSpy(functionName: "tabManager(_:willAddTab:)") + let didAdd = MethodSpy(functionName: "tabManager(_:didAddTab:)") + + override func setUp() { + super.setUp() + } + + override func tearDown() { + super.tearDown() + } + + func testTabManagerCallsTabManagerStateDelegateOnStoreChangesWithNormalTabs() { + let profile = TabManagerMockProfile() + let manager = TabManager(prefs: profile.prefs, imageStore: nil) + let stateDelegate = MockTabManagerStateDelegate() + manager.stateDelegate = stateDelegate + let configuration = WKWebViewConfiguration() + configuration.processPool = WKProcessPool() + + // test that non-private tabs are saved to the db + // add some non-private tabs to the tab manager + for _ in 0..<3 { + let tab = Tab(configuration: configuration) + tab.url = URL(string: "http://yahoo.com")! + manager.configureTab(tab, request: URLRequest(url: tab.url!), flushToDisk: false, zombie: false) + } + + manager.storeChanges() + + XCTAssertEqual(stateDelegate.numberOfTabsStored, 3, "Expected state delegate to have been called with 3 tabs, but called with \(stateDelegate.numberOfTabsStored)") + } + + func testTabManagerDoesNotCallTabManagerStateDelegateOnStoreChangesWithPrivateTabs() { + let profile = TabManagerMockProfile() + let manager = TabManager(prefs: profile.prefs, imageStore: nil) + let stateDelegate = MockTabManagerStateDelegate() + manager.stateDelegate = stateDelegate + let configuration = WKWebViewConfiguration() + configuration.processPool = WKProcessPool() + + // test that non-private tabs are saved to the db + // add some non-private tabs to the tab manager + for _ in 0..<3 { + let tab = Tab(configuration: configuration, isPrivate: true) + tab.url = URL(string: "http://yahoo.com")! + manager.configureTab(tab, request: URLRequest(url: tab.url!), flushToDisk: false, zombie: false) + } + + manager.storeChanges() + + XCTAssertEqual(stateDelegate.numberOfTabsStored, 0, "Expected state delegate to have been called with 3 tabs, but called with \(stateDelegate.numberOfTabsStored)") + } + + func testAddTab() { + let profile = TabManagerMockProfile() + let manager = TabManager(prefs: profile.prefs, imageStore: nil) + let delegate = MockTabManagerDelegate() + manager.addDelegate(delegate) + + delegate.expect([willAdd, didAdd]) + manager.addTab() + delegate.verify("Not all delegate methods were called") + } + + func testDidDeleteLastTab() { + let profile = TabManagerMockProfile() + let manager = TabManager(prefs: profile.prefs, imageStore: nil) + let delegate = MockTabManagerDelegate() + + //create the tab before adding the mock delegate. So we don't have to check delegate calls we dont care about + let tab = manager.addTab() + manager.selectTab(tab) + manager.addDelegate(delegate) + + let didSelect = MethodSpy(functionName: "tabManager(_:didSelectedTabChange:previous:)") { tabs in + let next = tabs[0]! + let previous = tabs[1]! + XCTAssertTrue(previous != next) + XCTAssertTrue(previous == tab) + XCTAssertFalse(next.isPrivate) + } + delegate.expect([willRemove, didRemove, willAdd, didAdd, didSelect]) + manager.removeTab(tab) + delegate.verify("Not all delegate methods were called") + } + + func testDidDeleteLastPrivateTab() { + let profile = TabManagerMockProfile() + let manager = TabManager(prefs: profile.prefs, imageStore: nil) + let delegate = MockTabManagerDelegate() + + //create the tab before adding the mock delegate. So we don't have to check delegate calls we dont care about + let tab = manager.addTab() + manager.selectTab(tab) + let privateTab = manager.addTab(isPrivate: true) + manager.selectTab(privateTab) + manager.addDelegate(delegate) + + let didSelect = MethodSpy(functionName: "tabManager(_:didSelectedTabChange:previous:)") { tabs in + let next = tabs[0]! + let previous = tabs[1]! + XCTAssertTrue(previous != next) + XCTAssertTrue(previous == privateTab) + XCTAssertTrue(next == tab) + XCTAssertTrue(previous.isPrivate) + XCTAssertTrue(manager.selectedTab == next) + } + delegate.expect([willRemove, didRemove, didSelect]) + manager.removeTab(privateTab) + delegate.verify("Not all delegate methods were called") + } + + func testDeletePrivateTabsOnExit() { + //setup + let profile = TabManagerMockProfile() + let manager = TabManager(prefs: profile.prefs, imageStore: nil) + profile.prefs.setBool(true, forKey: "settings.closePrivateTabs") + + // create one private and one normal tab + let tab = manager.addTab() + manager.selectTab(tab) + manager.selectTab(manager.addTab(isPrivate: true)) + + XCTAssertEqual(manager.selectedTab?.isPrivate, true, "The selected tab should be the private tab") + XCTAssertEqual(manager.privateTabs.count, 1, "There should only be one private tab") + + manager.selectTab(tab) + XCTAssertEqual(manager.privateTabs.count, 0, "If the normal tab is selected the private tab should have been deleted") + XCTAssertEqual(manager.normalTabs.count, 1, "The regular tab should stil be around") + + manager.selectTab(manager.addTab(isPrivate: true)) + XCTAssertEqual(manager.privateTabs.count, 1, "There should be one new private tab") + manager.willSwitchTabMode() + XCTAssertEqual(manager.privateTabs.count, 0, "After willSwitchTabMode there should be no more private tabs") + + manager.selectTab(manager.addTab(isPrivate: true)) + manager.selectTab(manager.addTab(isPrivate: true)) + XCTAssertEqual(manager.privateTabs.count, 2, "Private tabs should not be deleted when another one is added") + manager.selectTab(manager.addTab()) + XCTAssertEqual(manager.privateTabs.count, 0, "But once we add a normal tab we've switched out of private mode. Private tabs should be deleted") + XCTAssertEqual(manager.normalTabs.count, 2, "The original normal tab and the new one should both still exist") + + profile.prefs.setBool(false, forKey: "settings.closePrivateTabs") + manager.selectTab(manager.addTab(isPrivate: true)) + manager.selectTab(tab) + XCTAssertEqual(manager.selectedTab?.isPrivate, false, "The selected tab should not be private") + XCTAssertEqual(manager.privateTabs.count, 1, "If the flag is false then private tabs should still exist") + } + + func testDeleteNonSelectedTab() { + let profile = TabManagerMockProfile() + let manager = TabManager(prefs: profile.prefs, imageStore: nil) + let delegate = MockTabManagerDelegate() + + //create the tab before adding the mock delegate. So we don't have to check delegate calls we dont care about + let tab = manager.addTab() + manager.selectTab(tab) + manager.addTab() + let deleteTab = manager.addTab() + manager.addDelegate(delegate) + + delegate.expect([willRemove, didRemove]) + manager.removeTab(deleteTab) + + delegate.verify("Not all delegate methods were called") + } + + func testDeleteSelectedTab() { + let profile = TabManagerMockProfile() + let manager = TabManager(prefs: profile.prefs, imageStore: nil) + let delegate = MockTabManagerDelegate() + + func addTab(_ load: Bool) -> Tab { + let tab = manager.addTab() + if load { + tab.lastExecutedTime = Date.now() + } + return tab + } + + let tab0 = addTab(false) // not loaded + let tab1 = addTab(true) + let tab2 = addTab(true) + let tab3 = addTab(false) // not loaded + let tab4 = addTab(true) + + // starting at tab2, we should be selecting + // [ tab4, tab1, tab3, tab0 ] + + manager.selectTab(tab2) + manager.removeTab(manager.selectedTab!) + // Rule: most recently loaded. + XCTAssertEqual(manager.selectedTab, tab4) + + manager.removeTab(manager.selectedTab!) + // Rule: most recently loaded. + XCTAssertEqual(manager.selectedTab, tab1) + + manager.removeTab(manager.selectedTab!) + // Rule: next to the right. + XCTAssertEqual(manager.selectedTab, tab3) + + manager.removeTab(manager.selectedTab!) + // Rule: last one left. + XCTAssertEqual(manager.selectedTab, tab0) + } + + func testDeleteLastTab() { + let profile = TabManagerMockProfile() + let manager = TabManager(prefs: profile.prefs, imageStore: nil) + let delegate = MockTabManagerDelegate() + + //create the tab before adding the mock delegate. So we don't have to check delegate calls we dont care about + (0..<10).forEach {_ in manager.addTab() } + manager.selectTab(manager.tabs.last) + let deleteTab = manager.tabs.last + let newSelectedTab = manager.tabs[8] + manager.addDelegate(delegate) + + let didSelect = MethodSpy(functionName: "tabManager(_:didSelectedTabChange:previous:)") { tabs in + let next = tabs[0]! + let previous = tabs[1]! + XCTAssertEqual(deleteTab, previous) + XCTAssertEqual(next, newSelectedTab) + } + delegate.expect([willRemove, didRemove, didSelect]) + manager.removeTab(manager.tabs.last!) + + delegate.verify("Not all delegate methods were called") + } + + func testDelegatesCalledWhenRemovingPrivateTabs() { + //setup + let profile = TabManagerMockProfile() + let delegate = MockTabManagerDelegate() + let manager = TabManager(prefs: profile.prefs, imageStore: nil) + profile.prefs.setBool(true, forKey: "settings.closePrivateTabs") + + // create one private and one normal tab + let tab = manager.addTab() + let newTab = manager.addTab() + manager.selectTab(tab) + manager.selectTab(manager.addTab(isPrivate: true)) + manager.addDelegate(delegate) + + // Double check a few things + XCTAssertEqual(manager.selectedTab?.isPrivate, true, "The selected tab should be the private tab") + XCTAssertEqual(manager.privateTabs.count, 1, "There should only be one private tab") + + // switch to normal mode. Which should delete the private tabs + manager.willSwitchTabMode() + + //make sure tabs are cleared properly and indexes are reset + XCTAssertEqual(manager.privateTabs.count, 0, "Private tab should have been deleted") + XCTAssertEqual(manager.selectedIndex, -1, "The selected index should have been reset") + + // didSelect should still be called when switching between a nil tab + let didSelect = MethodSpy(functionName: "tabManager(_:didSelectedTabChange:previous:)") { tabs in + XCTAssertNil(tabs[1], "there should be no previous tab") + let next = tabs[0]! + XCTAssertFalse(next.isPrivate) + } + + // make sure delegate method is actually called + delegate.expect([didSelect]) + + // select the new tab to trigger the delegate methods + manager.selectTab(newTab) + + // check + delegate.verify("Not all delegate methods were called") + } + + func testDeleteFirstTab() { + let profile = TabManagerMockProfile() + let manager = TabManager(prefs: profile.prefs, imageStore: nil) + let delegate = MockTabManagerDelegate() + + //create the tab before adding the mock delegate. So we don't have to check delegate calls we dont care about + (0..<10).forEach {_ in manager.addTab() } + manager.selectTab(manager.tabs.first) + let deleteTab = manager.tabs.first + let newSelectedTab = manager.tabs[1] + manager.addDelegate(delegate) + + let didSelect = MethodSpy(functionName: "tabManager(_:didSelectedTabChange:previous:)") { tabs in + let next = tabs[0]! + let previous = tabs[1]! + XCTAssertEqual(deleteTab, previous) + XCTAssertEqual(next, newSelectedTab) + } + delegate.expect([willRemove, didRemove, didSelect]) + manager.removeTab(manager.tabs.first!) + delegate.verify("Not all delegate methods were called") + } + + // Private tabs and regular tabs are in the same tabs array. + // Make sure that when a private tab is added inbetween regular tabs it isnt accidently selected when removing a regular tab + func testTabsIndex() { + let profile = TabManagerMockProfile() + let manager = TabManager(prefs: profile.prefs, imageStore: nil) + let delegate = MockTabManagerDelegate() + + // We add 2 tabs. Then a private one before adding another normal tab and selecting it. + // Make sure that when the last one is deleted we dont switch to the private tab + manager.addTab() + let newSelected = manager.addTab() + manager.addTab(isPrivate: true) + let deleted = manager.addTab() + manager.selectTab(manager.tabs.last) + manager.addDelegate(delegate) + + let didSelect = MethodSpy(functionName: "tabManager(_:didSelectedTabChange:previous:)") { tabs in + let next = tabs[0]! + let previous = tabs[1]! + XCTAssertEqual(deleted, previous) + XCTAssertEqual(next, newSelected) + } + delegate.expect([willRemove, didRemove, didSelect]) + manager.removeTab(manager.tabs.last!) + + delegate.verify("Not all delegate methods were called") + } + + func testTabsIndexClosingFirst() { + let profile = TabManagerMockProfile() + let manager = TabManager(prefs: profile.prefs, imageStore: nil) + let delegate = MockTabManagerDelegate() + + // We add 2 tabs. Then a private one before adding another normal tab and selecting the first. + // Make sure that when the last one is deleted we dont switch to the private tab + let deleted = manager.addTab() + let newSelected = manager.addTab() + manager.addTab(isPrivate: true) + manager.addTab() + manager.selectTab(manager.tabs.first) + manager.addDelegate(delegate) + + let didSelect = MethodSpy(functionName: "tabManager(_:didSelectedTabChange:previous:)") { tabs in + let next = tabs[0]! + let previous = tabs[1]! + XCTAssertEqual(deleted, previous) + XCTAssertEqual(next, newSelected) + } + delegate.expect([willRemove, didRemove, didSelect]) + manager.removeTab(manager.tabs.first!) + delegate.verify("Not all delegate methods were called") + } + +} diff --git a/mobile/ios/ClientTests/TestBookmarks.swift b/mobile/ios/ClientTests/TestBookmarks.swift new file mode 100644 index 0000000000..b756c069eb --- /dev/null +++ b/mobile/ios/ClientTests/TestBookmarks.swift @@ -0,0 +1,42 @@ +/* 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/. */ + +@testable import Client +import Foundation +import Shared +import Storage + +import XCTest + +class TestBookmarks: ProfileTest { + func testBookmarks() { + withTestProfile { profile -> Void in + for i in 0...10 { + let bookmark = ShareItem(url: "http://www.example.com/\(i)", title: "Example \(i)", favicon: nil) + profile.bookmarks.shareItem(bookmark) + } + + let expectation = self.expectation(description: "asynchronous request") + profile.bookmarks.modelFactory >>== { + $0.modelForFolder(BookmarkRoots.MobileFolderGUID).upon { result in + guard let model = result.successValue else { + XCTFail("Should not have failed to get mock bookmarks.") + expectation.fulfill() + return + } + // 11 bookmarks plus our two suggested sites. + XCTAssertEqual(model.current.count, 11, "We create \(model.current.count) stub bookmarks in the Mobile Bookmarks folder.") + let bookmark = model.current[0] + XCTAssertTrue(bookmark is BookmarkItem) + XCTAssertTrue((bookmark as! BookmarkItem).url.hasPrefix("http://www.example.com/"), "Example URL found.") + expectation.fulfill() + } + } + + self.waitForExpectations(timeout: 10.0, handler:nil) + // This'll do. + try! profile.files.remove("mock.db") + } + } +} diff --git a/mobile/ios/ClientTests/TestFavicons.swift b/mobile/ios/ClientTests/TestFavicons.swift new file mode 100644 index 0000000000..b2f463bf7f --- /dev/null +++ b/mobile/ios/ClientTests/TestFavicons.swift @@ -0,0 +1,57 @@ +/* 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 XCTest +import Storage +import SDWebImage +@testable import Client +import Shared + +class TestFavicons: ProfileTest { + + fileprivate func addSite(_ favicons: Favicons, url: String, s: Bool = true) { + let expectation = self.expectation(description: "Wait for history") + let site = Site(url: url, title: "") + let icon = Favicon(url: url + "/icon.png", type: IconType.icon) + favicons.addFavicon(icon, forSite: site).upon { + XCTAssertEqual($0.isSuccess, s, "Icon added \(url)") + expectation.fulfill() + } + self.waitForExpectations(timeout: 100, handler: nil) + } + + func testFaviconFetcherParse() { + let expectation = self.expectation(description: "Wait for Favicons to be fetched") + + let profile = MockProfile() + // I want a site that also has an iOS app so I can get "apple-touch-icon-precomposed" icons as well + let url = URL(string: "https://instagram.com") + FaviconFetcher.getForURL(url!, profile: profile).uponQueue(DispatchQueue.main) { result in + guard let favicons = result.successValue, favicons.count > 0, let url = favicons.first?.url.asURL else { + XCTFail("Favicons were not found.") + return expectation.fulfill() + } + XCTAssertGreaterThan(favicons.count, 1, "Instagram should have more than one Favicon.") + SDWebImageManager.shared().loadImage(with: url, options: .retryFailed, progress: nil, completed: { (img, _, _, _, _, _) in + guard let image = img else { + XCTFail("Not a valid URL provided for a favicon.") + return expectation.fulfill() + } + XCTAssertNotEqual(image.size, CGSize(width: 0, height: 0)) + expectation.fulfill() + }) + + } + self.waitForExpectations(timeout: 3000, handler: nil) + } + + func testDefaultFavicons() { + let icon = FaviconFetcher.getDefaultIconForURL(url: URL(string: "http://www.google.de")!) + XCTAssertNotNil(icon) + let craigsListIcon = FaviconFetcher.getDefaultIconForURL(url: URL(string: "http://vancouver.craigslist.ca")!) + XCTAssertNotNil(craigsListIcon) + + } +} diff --git a/mobile/ios/ClientTests/TestHashExtensions.swift b/mobile/ios/ClientTests/TestHashExtensions.swift new file mode 100644 index 0000000000..3a711c8039 --- /dev/null +++ b/mobile/ios/ClientTests/TestHashExtensions.swift @@ -0,0 +1,19 @@ +/* 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 XCTest + +class TestHashExtensions: XCTestCase { + func testSha1() { + XCTAssertEqual("test1test2".sha1.hexEncodedString, "dff964f6e3c1761b6288f5c75c319d36fb09b2b9") + XCTAssertEqual("test2test3".sha1.hexEncodedString, "66cdfcbbf4ad73f40ae06140460ff9bb0aabaf5c") + } + + func testSha256() { + let data1: Data = "4f980b6f9baa6965f760d0bf2b2ccbee483032e5df01d77bbd9e25f7517a06b9".hexDecodedData + XCTAssertEqual("test1test2".sha256, data1) + XCTAssertEqual("test2test3".sha256, "fc3ea28dc1801e4180cec1022b55bee7795cf3c9fd430fb5237c9d8054218e81".hexDecodedData) + } +} diff --git a/mobile/ios/ClientTests/TestHistory.swift b/mobile/ios/ClientTests/TestHistory.swift new file mode 100644 index 0000000000..73eacdc865 --- /dev/null +++ b/mobile/ios/ClientTests/TestHistory.swift @@ -0,0 +1,219 @@ +/* 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/. */ + +@testable import Client +import Foundation +import Storage + +import XCTest + +class TestHistory: ProfileTest { + fileprivate func addSite(_ history: BrowserHistory, url: String, title: String, s: Bool = true) { + let site = Site(url: url, title: title) + let visit = SiteVisit(site: site, date: Date.nowMicroseconds()) + XCTAssertEqual(s, history.addLocalVisit(visit).value.isSuccess, "Site added: \(url).") + } + + fileprivate func innerCheckSites(_ history: BrowserHistory, callback: @escaping (_ cursor: Cursor) -> Void) { + // Retrieve the entry + history.getSitesByLastVisit(100).upon { + XCTAssertTrue($0.isSuccess) + callback($0.successValue!) + } + } + + fileprivate func checkSites(_ history: BrowserHistory, urls: [String: String], s: Bool = true) { + // Retrieve the entry. + if let cursor = history.getSitesByLastVisit(100).value.successValue { + XCTAssertEqual(cursor.status, CursorStatus.success, "Returned success \(cursor.statusMessage).") + XCTAssertEqual(cursor.count, urls.count, "Cursor has \(urls.count) entries.") + + for index in 0.. Void in + let h = profile.history + self.addSite(h, url: "http://url1/", title: "title") + self.addSite(h, url: "http://url1/", title: "title") + self.addSite(h, url: "http://url1/", title: "title 2") + self.addSite(h, url: "https://url2/", title: "title") + self.addSite(h, url: "https://url2/", title: "title") + self.checkSites(h, urls: ["http://url1/": "title 2", "https://url2/": "title"]) + self.checkVisits(h, url: "http://url1/") + self.checkVisits(h, url: "https://url2/") + self.clear(h) + } + } + + func testAboutUrls() { + withTestProfile { (profile) -> Void in + let h = profile.history + self.addSite(h, url: "about:home", title: "About Home", s: false) + self.clear(h) + } + } + + let NumThreads = 5 + let NumCmds = 10 + + func testInsertPerformance() { + withTestProfile { profile -> Void in + let h = profile.history + var j = 0 + + self.measure({ () -> Void in + for _ in 0...self.NumCmds { + self.addSite(h, url: "https://someurl\(j).com/", title: "title \(j)") + j += 1 + } + self.clear(h) + }) + } + } + + func testGetPerformance() { + withTestProfile { profile -> Void in + let h = profile.history + var j = 0 + var urls = [String: String]() + + self.clear(h) + for _ in 0...self.NumCmds { + self.addSite(h, url: "https://someurl\(j).com/", title: "title \(j)") + urls["https://someurl\(j).com/"] = "title \(j)" + j += 1 + } + + self.measure({ () -> Void in + self.checkSites(h, urls: urls) + return + }) + + self.clear(h) + } + } + + // Fuzzing tests. These fire random insert/query/clear commands into the history database from threads. The don't check + // the results. Just look for crashes. + func testRandomThreading() { + withTestProfile { profile -> Void in + let queue = DispatchQueue(label: "My Queue", qos: DispatchQoS.default, attributes: DispatchQueue.Attributes.concurrent, autoreleaseFrequency: DispatchQueue.AutoreleaseFrequency.inherit, target: nil) + var counter = 0 + + let expectation = self.expectation(description: "Wait for history") + for _ in 0.. Void in + counter += 1 + if counter == self.NumThreads { + self.clear(history) + expectation.fulfill() + } + }) + } + self.waitForExpectations(timeout: 10, handler: nil) + } + } + + // Same as testRandomThreading, but uses one history connection for all threads + func testRandomThreading2() { + withTestProfile { profile -> Void in + let queue = DispatchQueue(label: "My Queue", qos: DispatchQoS.default, attributes: DispatchQueue.Attributes.concurrent, autoreleaseFrequency: DispatchQueue.AutoreleaseFrequency.inherit, target: nil) + var history = profile.history as BrowserHistory + var counter = 0 + + let expectation = self.expectation(description: "Wait for history") + for _ in 0.. Void in + counter += 1 + if counter == self.NumThreads { + self.clear(history) + expectation.fulfill() + } + }) + } + self.waitForExpectations(timeout: 10, handler: nil) + } + } + + // Runs a random command on a database. Calls cb when finished. + fileprivate func runRandom(_ history: inout BrowserHistory, cmdIn: Int, cb: @escaping () -> Void) { + var cmd = cmdIn + if cmd < 0 { + cmd = Int(arc4random() % 5) + } + + switch cmd { + case 0...1: + let url = "https://randomurl.com/\(arc4random() % 100)" + let title = "title \(arc4random() % 100)" + addSite(history, url: url, title: title) + cb() + case 2...3: + innerCheckSites(history) { cursor in + for site in cursor { + _ = site! + } + } + cb() + default: + history.clearHistory().upon() { success in cb() } + } + } + + // Calls numCmds random methods on this database. val is a counter used by this interally (i.e. always pass zero for it). + // Calls cb when finished. + fileprivate func runMultiRandom(_ history: inout BrowserHistory, val: Int, numCmds: Int, cb: @escaping () -> Void) { + if val == numCmds { + cb() + return + } else { + runRandom(&history, cmdIn: -1) { [history]_ in + var history = history + self.runMultiRandom(&history, val: val+1, numCmds: numCmds, cb: cb) + } + } + } + + // Helper for starting a new thread running NumCmds random methods on it. Calls cb when done. + fileprivate func runRandom(_ history: inout BrowserHistory, queue: DispatchQueue, cb: @escaping () -> Void) { + queue.async { [history] in + var history = history + // Each thread creates its own history provider + self.runMultiRandom(&history, val: 0, numCmds: self.NumCmds) { _ in + DispatchQueue.main.async(execute: cb) + } + } + } +} diff --git a/mobile/ios/ClientTests/UIImageViewExtensionsTests.swift b/mobile/ios/ClientTests/UIImageViewExtensionsTests.swift new file mode 100644 index 0000000000..da8f596473 --- /dev/null +++ b/mobile/ios/ClientTests/UIImageViewExtensionsTests.swift @@ -0,0 +1,94 @@ +/* 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 UIKit +import XCTest +import Storage +import SDWebImage +import GCDWebServers +import Shared + +@testable import Client + +class UIImageViewExtensionsTests: XCTestCase { + + override func setUp() { + SDWebImageDownloader.shared().urlCredential = WebServer.sharedInstance.credentials + } + + func testsetIcon() { + let url = URL(string: "http://mozilla.com") + let imageView = UIImageView() + + let goodIcon = FaviconFetcher.getDefaultFavicon(url!) + let correctColor = FaviconFetcher.getDefaultColor(url!) + imageView.setIcon(nil, forURL: url) + XCTAssertEqual(imageView.image!, goodIcon, "The correct default favicon should be applied") + XCTAssertEqual(imageView.backgroundColor, correctColor, "The correct default color should be applied") + + imageView.setIcon(nil, forURL: URL(string: "http://mozilla.com/blahblah")) + XCTAssertEqual(imageView.image!, goodIcon, "The same icon should be applied to all urls with the same domain") + + imageView.setIcon(nil, forURL: URL(string: "b")) + XCTAssertEqual(imageView.image, FaviconFetcher.defaultFavicon, "The default favicon should be applied when no information is given about the icon") + } + + func testAsyncSetIcon() { + let originalImage = UIImage(named: "fxLogo")! + + WebServer.sharedInstance.registerHandlerForMethod("GET", module: "favicon", resource: "icon") { (request) -> GCDWebServerResponse! in + return GCDWebServerDataResponse(data: UIImagePNGRepresentation(originalImage), contentType: "image/png") + } + + let favImageView = UIImageView() + favImageView.setIcon(Favicon(url: "http://localhost:6571/favicon/icon", type: .guess), forURL: URL(string: "http://localhost:6571")) + + let expect = expectation(description: "UIImageView async load") + let time = Int64(2 * Double(NSEC_PER_SEC)) + DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + Double(time) / Double(NSEC_PER_SEC)) { + XCTAssert(originalImage.isStrictlyEqual(to: favImageView.image!), "The correct favicon should be applied to the UIImageView") + expect.fulfill() + } + waitForExpectations(timeout: 5, handler: nil) + } + + func testAsyncSetIconFail() { + let favImageView = UIImageView() + + let gFavURL = URL(string: "https://www.nofavicon.com/noicon.ico") + let gURL = URL(string: "http://nofavicon.com") + let correctImage = FaviconFetcher.getDefaultFavicon(gURL!) + + favImageView.setIcon(Favicon(url: gFavURL!.absoluteString, type: .guess), forURL: gURL) + + let expect = expectation(description: "UIImageView async load") + let time = Int64(2 * Double(NSEC_PER_SEC)) + DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + Double(time) / Double(NSEC_PER_SEC)) { + XCTAssert(correctImage.isStrictlyEqual(to: favImageView.image!), "The correct default favicon should be applied to the UIImageView") + expect.fulfill() + } + waitForExpectations(timeout: 5, handler: nil) + } + + func testDefaultIcons() { + let favImageView = UIImageView() + + let gFavURL = URL(string: "https://www.facebook.com/fav") //This will be fetched from tippy top sites + let gURL = URL(string: "http://www.facebook.com")! + let defaultItem = FaviconFetcher.defaultIcons[gURL.baseDomain!]! + let correctImage = UIImage(contentsOfFile: defaultItem.url)! + + favImageView.setIcon(Favicon(url: gFavURL!.absoluteString, type: .guess), forURL: gURL) + + let expect = expectation(description: "UIImageView async load") + let time = Int64(2 * Double(NSEC_PER_SEC)) + DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + Double(time) / Double(NSEC_PER_SEC)) { + XCTAssertEqual(favImageView.backgroundColor, defaultItem.color) + XCTAssert(correctImage.isStrictlyEqual(to: favImageView.image!), "The correct default favicon should be applied to the UIImageView") + expect.fulfill() + } + waitForExpectations(timeout: 5, handler: nil) + } +} diff --git a/mobile/ios/ClientTests/UIPasteboardExtensionsTests.swift b/mobile/ios/ClientTests/UIPasteboardExtensionsTests.swift new file mode 100644 index 0000000000..3fee731b2c --- /dev/null +++ b/mobile/ios/ClientTests/UIPasteboardExtensionsTests.swift @@ -0,0 +1,47 @@ +/* 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 MobileCoreServices +import UIKit +import XCTest + +class UIPasteboardExtensionsTests: XCTestCase { + + fileprivate var pasteboard: UIPasteboard! + + override func setUp() { + super.setUp() + pasteboard = UIPasteboard.withUniqueName() + } + + override func tearDown() { + super.tearDown() + UIPasteboard.remove(withName: pasteboard.name) + } + + func testAddPNGImage() { + let path = Bundle(for: self.classForCoder).path(forResource: "image", ofType: "png")! + let data = try! Data(contentsOf: URL(fileURLWithPath: path)) + let url = URL(string: "http://foo.bar")! + pasteboard.addImageWithData(data, forURL: url) + verifyPasteboard(expectedURL: url, expectedImageTypeKey: kUTTypePNG) + } + + func testAddGIFImage() { + let path = Bundle(for: self.classForCoder).path(forResource: "image", ofType: "gif")! + let data = try! Data(contentsOf: URL(fileURLWithPath: path)) + let url = URL(string: "http://foo.bar")! + pasteboard.addImageWithData(data, forURL: url) + verifyPasteboard(expectedURL: url, expectedImageTypeKey: kUTTypeGIF) + } + + fileprivate func verifyPasteboard(expectedURL: URL, expectedImageTypeKey: CFString) { + XCTAssertEqual(pasteboard.items.count, 1) + XCTAssertEqual(pasteboard.items[0].count, 2) + XCTAssertEqual(pasteboard.items[0][kUTTypeURL as String] as? URL, expectedURL) + XCTAssertNotNil(pasteboard.items[0][expectedImageTypeKey as String]) + } + +} diff --git a/mobile/ios/ClientTests/WebServerTests.swift b/mobile/ios/ClientTests/WebServerTests.swift new file mode 100644 index 0000000000..bedb7cdb14 --- /dev/null +++ b/mobile/ios/ClientTests/WebServerTests.swift @@ -0,0 +1,49 @@ +/* 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 UIKit +import GCDWebServers +import XCTest + +/// Minimal web server tests. This class can be used as a base class for tests that need a real web server. +/// Simply add additional handlers your test class' setUp() method. +class WebServerTests: XCTestCase { + let webServer: GCDWebServer = GCDWebServer() + var webServerBase: String! + + /// Setup a basic web server that binds to a random port and that has one default handler on /hello + fileprivate func setupWebServer() { + webServer.addHandler(forMethod: "GET", path: "/hello", request: GCDWebServerRequest.self) { (request) -> GCDWebServerResponse! in + return GCDWebServerDataResponse(html: "

Hello World

") + } + if webServer.start(withPort: 0, bonjourName: nil) == false { + XCTFail("Can't start the GCDWebServer") + } + webServerBase = "http://localhost:\(webServer.port)" + } + + override func setUp() { + super.setUp() + setupWebServer() + } + + override func tearDown() { + super.tearDown() + } + + func testWebServerIsRunning() { + XCTAssertTrue(webServer.isRunning) + } + + func testWebServerIsServingRequests() { + let response: NSString? + do { + response = try NSString(contentsOf: URL(string: "\(webServerBase!)/hello")!, encoding: String.Encoding.utf8.rawValue) + } catch _ { + response = nil + } + XCTAssertNotNil(response) + XCTAssertTrue(response == "

Hello World

") + } +} diff --git a/mobile/ios/ClientTests/XCTestCaseExtensions.swift b/mobile/ios/ClientTests/XCTestCaseExtensions.swift new file mode 100644 index 0000000000..caa11f1aa4 --- /dev/null +++ b/mobile/ios/ClientTests/XCTestCaseExtensions.swift @@ -0,0 +1,30 @@ +/* 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 XCTest + +extension XCTestCase { + func wait(_ time: TimeInterval) { + let expectation = self.expectation(description: "Wait") + let delayTime = DispatchTime.now() + Double(Int64(time * Double(NSEC_PER_SEC))) / Double(NSEC_PER_SEC) + DispatchQueue.main.asyncAfter(deadline: delayTime) { + expectation.fulfill() + } + waitForExpectations(timeout: time + 1, handler: nil) + } + + func waitForCondition(timeout: TimeInterval = 10, condition: () -> Bool) { + let timeoutTime = Date.timeIntervalSinceReferenceDate + timeout + + while !condition() { + if Date.timeIntervalSinceReferenceDate > timeoutTime { + XCTFail("Condition timed out") + return + } + + wait(0.1) + } + } +} diff --git a/mobile/ios/ClientTests/image.gif b/mobile/ios/ClientTests/image.gif new file mode 100644 index 0000000000..6ebee9cdae Binary files /dev/null and b/mobile/ios/ClientTests/image.gif differ diff --git a/mobile/ios/ClientTests/image.png b/mobile/ios/ClientTests/image.png new file mode 100644 index 0000000000..3ee62d6712 Binary files /dev/null and b/mobile/ios/ClientTests/image.png differ diff --git a/mobile/ios/ClientTests/pocketglobalfeed.json b/mobile/ios/ClientTests/pocketglobalfeed.json new file mode 100644 index 0000000000..2573bad8b3 --- /dev/null +++ b/mobile/ios/ClientTests/pocketglobalfeed.json @@ -0,0 +1,24 @@ +{ + "status": 1, + "list": [{ + "id": 2092, + "url": "https:\/\/pocket.co\/xMnD5u", + "dedupe_url": "https:\/\/www.wired.com\/story\/turn-off-your-push-notifications\/", + "title": "Turn Off Your Push Notifications. All of Them", + "excerpt": "Push notifications are ruining my life. Yours too, I bet. Download more than a few apps and the notifications become a non-stop, cacophonous waterfall of nonsense. Here's just part of an afternoon on my phone:", + "domain": "wired.com", + "image_src": "https:\/\/d33ypg4xwx0n86.cloudfront.net\/direct?url=https%3A%2F%2Fmedia.wired.com%2Fphotos%2F597267fd023c38366e1ae497%2Fmaster%2Fw_1200%2Cc_limit%2Fno_notifications-TA.gif&resize=w450", + "published_timestamp": "1332306000", + "sort_id": 0 + }, { + "id": 2091, + "url": "https:\/\/pocket.co\/sMnD5m", + "dedupe_url": "http:\/\/www.latimes.com\/business\/la-fi-agenda-best-buy-20170717-htmlstory.html", + "title": "Why the grim reaper of retail hasn't come to claim Best Buy", + "excerpt": "Five years ago Best Buy Co. looked like a retail dinosaur, another victim of e-commerce juggernaut Amazon.com and other online sellers.", + "domain": "latimes.com", + "image_src": "https:\/\/d33ypg4xwx0n86.cloudfront.net\/direct?url=http%3A%2F%2Fwww.trbimg.com%2Fimg-547cc080%2Fturbine%2Fla-fi-mh-radio-shack-20141201-004%2F&resize=w450", + "published_timestamp": "1500267600", + "sort_id": 1 + }] +} diff --git a/mobile/ios/Dangerfile b/mobile/ios/Dangerfile new file mode 100644 index 0000000000..3af0a87a34 --- /dev/null +++ b/mobile/ios/Dangerfile @@ -0,0 +1,2 @@ +# Lint files with Swiftlint. Check .swiftlint.yml for config +swiftlint.lint_files \ No newline at end of file diff --git a/mobile/ios/DeferredTestUtils.swift b/mobile/ios/DeferredTestUtils.swift new file mode 100644 index 0000000000..d5545347d3 --- /dev/null +++ b/mobile/ios/DeferredTestUtils.swift @@ -0,0 +1,25 @@ +/* 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 Shared +import Deferred +import XCTest + +protocol Succeedable { + var isSuccess: Bool { get } + var isFailure: Bool { get } +} + +extension Maybe: Succeedable {} + +extension Deferred where T: Succeedable { + func succeeded() { + XCTAssertTrue(self.value.isSuccess) + } + + func failed() { + XCTAssertTrue(self.value.isFailure) + } +} + diff --git a/mobile/ios/Docs/BUILDING.md b/mobile/ios/Docs/BUILDING.md new file mode 100644 index 0000000000..adcbd90d82 --- /dev/null +++ b/mobile/ios/Docs/BUILDING.md @@ -0,0 +1,118 @@ +Building Firefox for iOS +======================== + +Prerequisites, as of *February 14, 2017*: + +* Mac OS X 10.11.5 +* Xcode 8.2.1 with the iOS 10 SDK (Betas not supported) +* Carthage 0.15 or newer + +When running on a device: + +* A device that supports iOS 9.3 GM or later +* One of the following: + * A developer account and Admin access to the *Certificates, Identifiers & Profiles* section of the *iOS DevCenter* + * A free developer account (create an Apple ID for free and add as an account in Xcode) + +Get the Code +----------- + +``` +git clone https://github.com/mozilla/firefox-ios +cd firefox-ios +``` + +(If you have forked the repository, substitute the URL with your own repository location.) + +Pull in Dependencies +-------------------- + +We use Carthage to manage projects that we depend on. __The build will currently only work with Carthage v0.15 or newer__. If you do not already have Carthage installed, you need to grab it via Homebrew. Assuming you have Homebrew installed, execute the following: + +``` +brew update +brew upgrade +brew install carthage +``` + +You can now execute our `bootstrap.sh` script: + +``` +./bootstrap.sh +``` + +At this point you have checked out the source code for both the Firefox for iOS project and built it's dependencies. You can now build and run the application. + +Everything after this point is done from within Xcode. + +Run on the Simulator +----------------- + +* Open `Client.xcodeproj` and make sure you have the *Fennec* scheme and a simulated device selected. The app should run on any simulator. We just have not tested very well on the *Resizable iPad* and *Resizable iPhone* simulators. +* Select *Product -> Run* and the application should build and run on the selected simulator. + +Run on a Device with Xcode 8 and a Free Developer Account +--------------- + +> Only follow these instructions if you are using the free personal developer accounts. Simply add your Apple ID as an account in Xcode. + +Since the bundle identifier we use for Firefox is tied to our developer account, you'll need to generate your own identifier and update the existing configuration. + +1. Open Client/Configuration/Fennec.xcconfig +2. Change MOZ_BUNDLE_ID to your own bundle identifier. Just think of something unique: e.g., com.your_github_id.Fennec +3. Open the project editor in Xcode. +4. For the 'Client' target, in the 'Capabilities' section, turn off the capabilities 'Push Notifications' and 'Wallet'. +5. For each target, in the 'General' section, under 'Signing', select your personal development account. + +If you submit a patch, be sure to exclude these files because they are only relevant for your personal build. + +> If after building, Xcode fails to run the app with a vague `Security` error, open Settings -> Profiles on your iOS Device and Trust your personal developer profile. This may only happen on iOS 9. + +Run on a Device +--------------- + +These are instructions for development. Not production / distribution. + +> Before you try to run the application on a device, it is highly recommended that you first make sure that you can run applications on device in general. Just create one of the built-in iOS templates that Xcode provides and make sure you can run that on your device. If you can then it means you have done the basic setup like pairing your device, registering its UDID in the dev center, etc. + +Before you can run the application on your device, you need to setup a few things in the *Certificates, Identifiers & Profiles* section of the iOS Developer Center. + +> _Note_: When we mention `YOURREVERSEDOMAIN` below, use your own domain in reverse notation like `com.example` or if you do not have your own domain, just use something unique and personal like `io.github.yourgithubusername`. Please do not use existing domain names which you do not own. + +1. Create a Application Group. Name this group 'Fennec' and for its Identifier use `group.YOURREVERSEDOMAIN.Fennec` +2. Create a new App Id. Name it 'Fennec'. Give it an Explicit App ID and set its Bundle Identifier to `YOURREVERSEDOMAIN.Fennec`. In the App Services section, select *App Groups*. +3. Create a new App Id. Name it 'Fennec ShareTo'. Give it an Explicit App ID and set its Bundle Identifier to `YOURREVERSEDOMAIN.Fennec.ShareTo`. In the App Services section, select *App Groups*. +4. Create a new App Id. Name it 'Fennec SendTo'. Give it an Explicit App ID and set its Bundle Identifier to `YOURREVERSEDOMAIN.Fennec.SendTo`. In the App Services section, select *App Groups*. +5. Create a new App Id. Name it 'Fennec ViewLater'. Give it an Explicit App ID and set its Bundle Identifier to `YOURREVERSEDOMAIN.Fennec.ViewLater`. In the App Services section, select *App Groups*. +6. For all App Ids that you just created, edit their App Groups and make sure they are all part of the Fennec App Group that you created in step 1. + +Now we are going to create three Provisioning Profiles that are linked to the App Ids that we just created: + +1. Create a new *Development Provisioning Profile* and link it to the *Fennec* App ID that you created. Select the *Developer Certificates* and *Devices* that you wish to include in this profile. Finally, name this profile *Fennec*. +2. Create a new *Development Provisioning Profile* and link it to the *Fennec SendTo* App ID that you created. Select the *Developer Certificates* and *Devices* that you wish to include in this profile. Finally, name this profile *Fennec SendTo*. +3. Create a new *Development Provisioning Profile* and link it to the *Fennec ShareTo* App ID that you created. Select the *Developer Certificates* and *Devices* that you wish to include in this profile. Finally, name this profile *Fennec ShareTo*. +4. Create a new *Development Provisioning Profile* and link it to the *Fennec ViewLater* App ID that you created. Select the *Developer Certificates* and *Devices* that you wish to include in this profile. Finally, name this profile *Fennec ViewLater*. + +Now go to Xcode, *Preferences -> Accounts* and select your developer account. Hit the *View Details* button and then press the little reload button in the bottom left corner. This should sync the Provisioning Profiles and you should see the three profiles appear that you creates earlier. + +Almost done. The one thing missing is that we need to adjust the build configuration to use your new bundle identifier. + +1. Open Client/Configuration/Fennec.xcconfig +2. Change MOZ_BUNDLE_ID to `YOURREVERSEDOMAIN`. +3. Navigate to each of the application targets (Client/SendTo/ShareTo/ViewLater) and your developer account. + +Before building, do *Product -> Clean Build Folder* (option-shift-command-k) + +You should now be able to build the *Fennec* scheme and run on your device. + +We would love a Pull Request for a smarter Xcode project configuration or even a shell script that makes this process simpler. + + +Random notes +------------ + +Updating SQLCipher. + +As of bug https://bugzilla.mozilla.org/show_bug.cgi?id=1182620 we do not run the SQLCipher 'amalgamation' phase anymore. Instead we have simply included generated copies of `sqlite3.c`, `sqlite3.h` and `sqlite3ext.h` in the project. This works around problems where the amalgamation phase did not work for production builds. It also speeds up things. + +To update to a newer version of SQLCipher: check out the original SQLCipher project and build it. Do not copy the project or anything in the Firefox project. Just follow their instructions. Then copy the above three `.c` and `.h` files back into the Firefox project. Also update the `README`, `VERION` and `CHANGELOG` files from the original distribution so that we know what version we have included. diff --git a/mobile/ios/Docs/Dependency Graphs/README.md b/mobile/ios/Docs/Dependency Graphs/README.md new file mode 100644 index 0000000000..21b30648e4 --- /dev/null +++ b/mobile/ios/Docs/Dependency Graphs/README.md @@ -0,0 +1,21 @@ + +Available Dependency Graphs +=============== + +* `digraph-fxios` - a graph showing dependencies between the different targets in Firefox for iOS +* `digraph-fxios-carthage` - a graph showing dependencies between Firefox for iOS targets and our Carthage Dependencies +* `digraph-carthage-fxios` - a graph showing dependencies between our Carthage Dependencies and Firefox for iOS targets + +Generating dependency graphs +----------- + +* Download [Graphviz](http://www.graphviz.org/Download_macos.php) or install via homebrew `brew install graphviz` +* Open Graphviz.app +* Import `.dot` file +* Export as `jpg` or `pdf` + + +Examples +----------- + +Here are some [examples](http://graphs.grevian.org/example) of more things you can do with Graphviz \ No newline at end of file diff --git a/mobile/ios/Docs/Dependency Graphs/digraph-carthage-fxios.jpg b/mobile/ios/Docs/Dependency Graphs/digraph-carthage-fxios.jpg new file mode 100644 index 0000000000..d70edabb43 Binary files /dev/null and b/mobile/ios/Docs/Dependency Graphs/digraph-carthage-fxios.jpg differ diff --git a/mobile/ios/Docs/Dependency Graphs/digraph-fxios-carthage.jpg b/mobile/ios/Docs/Dependency Graphs/digraph-fxios-carthage.jpg new file mode 100644 index 0000000000..eff25c7903 Binary files /dev/null and b/mobile/ios/Docs/Dependency Graphs/digraph-fxios-carthage.jpg differ diff --git a/mobile/ios/Docs/Dependency Graphs/digraph-fxios.jpg b/mobile/ios/Docs/Dependency Graphs/digraph-fxios.jpg new file mode 100644 index 0000000000..38911cb42e Binary files /dev/null and b/mobile/ios/Docs/Dependency Graphs/digraph-fxios.jpg differ diff --git a/mobile/ios/Docs/Dependency Graphs/dot-files/digraph-carthage-fxios.dot b/mobile/ios/Docs/Dependency Graphs/dot-files/digraph-carthage-fxios.dot new file mode 100644 index 0000000000..9d5f287d47 --- /dev/null +++ b/mobile/ios/Docs/Dependency Graphs/dot-files/digraph-carthage-fxios.dot @@ -0,0 +1,32 @@ +digraph CarthageFxiOS { + "Alamofire" -> "Client" + "Alamofire" -> "ShareTo" + "Alamofire" -> "Shared" + "Alamofire" -> "Account" + "Alamofire" -> "Sync" + "Alamofire" -> "ReadingList" + "Snapkit" -> "Client" + "Snapkit" -> "Today" + "SDWebImage" -> "Client" + "SDWebImage" -> "SendTo" + "SDWebImage" -> "ShareTo" + "SDWebImage" -> "ViewLater" + "GCDWebServer" -> "Client" + "Adjust" -> "Client" + "OnePasswordExtension" -> "Client" + "libPhoneNumber-iOS" -> "Client" + "web_metadata_ios" -> "Client" + "XCGLogger" -> "Client" + "XCGLogger" -> "Shared" + "XCGLogger" -> "Storage" + "XCGLogger" -> "Account" + "Fuzi" -> "Client" + "Fuzi" -> "SendTo" + "Fuzi" -> "ShareTo" + "Onyx" -> "Client" + "readability" -> "Client" + "SwiftKeychainWrapper" -> "Shared" + "Deferred" -> "Shared" + "KIF" -> "ReadingList" + "SQLite" -> "ReadingList" +} \ No newline at end of file diff --git a/mobile/ios/Docs/Dependency Graphs/dot-files/digraph-fxios-carthage.dot b/mobile/ios/Docs/Dependency Graphs/dot-files/digraph-fxios-carthage.dot new file mode 100644 index 0000000000..bf3a14e2b7 --- /dev/null +++ b/mobile/ios/Docs/Dependency Graphs/dot-files/digraph-fxios-carthage.dot @@ -0,0 +1,26 @@ +digraph FxiOSCarthage { + "Client" -> "Alamofire" + "Client" -> "Snapkit" + "Client" -> "SDWebImage" + "Client" -> "GCDWebServer" + "Client" -> "Adjust" + "Client" -> "OnePasswordExtension" + "Client" -> "libPhoneNumber-iOS" + "Client" -> "web_metadata_ios" + "Client" -> "XCGLogger" + "Client" -> "Fuzi" + "Client" -> "Onyx" + "Client" -> "readability" + "ShareTo" -> "Alamofire" + "ShareTo" -> "SDWebImage" + "ShareTo" -> "Fuzi" + "Shared" -> "Alamofire" + "Shared" -> "XCGLogger" + "Shared" -> "SwiftKeychainWrapper" + "Shared" -> "Deferred" + "Today" -> "Snapkit" + "SendTo" -> "SDWebImage" + "SendTo" -> "Fuzi" + "ViewLater" -> "SDWebImage" + "Storage" -> "XCGLogger" +} diff --git a/mobile/ios/Docs/Dependency Graphs/dot-files/digraph-fxios.dot b/mobile/ios/Docs/Dependency Graphs/dot-files/digraph-fxios.dot new file mode 100644 index 0000000000..faba438a31 --- /dev/null +++ b/mobile/ios/Docs/Dependency Graphs/dot-files/digraph-fxios.dot @@ -0,0 +1,49 @@ +digraph FxiOS { + "Shared" -> "Storage" + "Shared" -> "StorageTests" + "Shared" -> "Client" + "Shared" -> "ClientTests" + "Shared" -> "ViewLater" + "Shared" -> "ReadingList" + "Shared" -> "SendTo" + "Shared" -> "ShareTo" + "Shared" -> "Account" + "Shared" -> "AccountTests" + "Shared" -> "Sync" + "Shared" -> "SyncTests" + "Shared" -> "UITests" + "Shared" -> "Today" + "FxA" -> "Account" + "FxA" -> "AccountTests" + "FxA" -> "Sync" + "FxA" -> "SyncTests" + "Storage" -> "StorageTests" + "Storage" -> "Client" + "Storage" -> "ClientTests" + "Storage" -> "ViewLater" + "Storage" -> "SendTo" + "Storage" -> "UITests" + "Storage" -> "SyncTests" + "Storage" -> "Sync" + "Storage" -> "ShareTo" + "Sync" -> "SyncTests" + "Sync" -> "Client" + "Sync" -> "ClientTests" + "Sync" -> "ShareTo" + "Sync" -> "SendTo" + "Sync" -> "ViewLater" + "ReadingList" -> "ReadingListTests" + "ReadingList" -> "Client" + "ReadingList" -> "ClientTests" + "Account" -> "AccountTests" + "Account" -> "Sync" + "Account" -> "SyncTests" + "Account" -> "Client" + "Account" -> "ClientTests" + "Client" -> "ClientTests" + "Client" -> "UITests" + "Client" -> "ShareTo" + "Client" -> "SendTo" + "Client" -> "ViewLater" + "Client" -> "Today" +} \ No newline at end of file diff --git a/mobile/ios/Docs/Dependency Graphs/dot-files/digraph-fxios.jpg b/mobile/ios/Docs/Dependency Graphs/dot-files/digraph-fxios.jpg new file mode 100644 index 0000000000..38911cb42e Binary files /dev/null and b/mobile/ios/Docs/Dependency Graphs/dot-files/digraph-fxios.jpg differ diff --git a/mobile/ios/Docs/browserdb.md b/mobile/ios/Docs/browserdb.md new file mode 100644 index 0000000000..fbd5694f8d --- /dev/null +++ b/mobile/ios/Docs/browserdb.md @@ -0,0 +1,14 @@ +# How to get your browser DB off the device + +* Launch Firefox. +* Open Settings. +* Scroll down to "Version" and tap it several times. +* Scroll down and hit "Debug: copy databases to app container". +* Connect your device via USB. +* Open Xcode. +* Window > Devices. Choose your device. +* Find "Firefox" on the right side. +* Click the gear icon, and choose "Download Container…". Save it somewhere. +* After some time, Finder will open focused on an .xcappdata file. +* Right-click, "Show Package Contents". +* Navigate to `AppData/Documents`. Zip up `browser.*`. diff --git a/mobile/ios/Docs/sync.md b/mobile/ios/Docs/sync.md new file mode 100644 index 0000000000..c7e3b14dbb --- /dev/null +++ b/mobile/ios/Docs/sync.md @@ -0,0 +1,76 @@ +# Firefox Sync Through The Lens Of Jaded Developers +## Or, Let's Use A State Machine For Our Own Sakes + +(These docs are approximately one sentence per line to make changes more civilized in version control.) + +Sync moves through a number of states. + +Initially, we know only the Married account state: we have a valid, verified FxA. This also gives us kB. + +Sync will need to know when kB changes, but it does so implicitly -- when kB changes, we fetch a new token, and the X-Client-State header we supply will result in a new storage endpoint. + +We transition from this state, then, to one where we also have a Sync storage token and a node assignment. This is the point at which we can first talk to the Sync server. We call this "Initial Assigned". We start in this state when first signed in, and also when creating an account on the device. + +(Note that the Sync server allows us to fetch info/collections with an expired token. This is to make the process of short-circuiting a sync cheaper. We still need a server assignment, of course. That we can do this implies that the Sync client has a pull-based relationship with the token server client.) + + +The process thereafter depends on some amount of locally persisted state. + +This state falls into three categories: + +* Real storage metadata on which our syncing behavior relies. This involves timestamps of collection fetches, syncIDs for storage as a whole and for each collection, and lists of engines. (Perhaps more.) + +* Protocol-level signaling, such as Retry-After and Backoff headers. These need to persist in order for us to behave correctly. + +* Cached data to avoid repeating operations. The client can cache info/collections for the duration of a sync, and meta/global and crypto/keys until they change. + +For more on this, see "Persistence", below. + +Starting from that "initial assigned" state, we will always fetch info/collections. + +If we have local meta/global and crypto/keys caches, and nothing is indicated as changed in i/c, we can move directly to Ready. + +If either is missing, each needs to be fetched and processed. These two tasks should be independent, but typically a change to meta/global will be followed by fetching keys. + +If meta/global is missing, one must be created based on the user's datatype elections, ideally preserved from any previous configuration. For consistency's sake, the rest of storage should be wiped prior to uploading a meta/global; a missing meta/global implies that something is wrong, and it probably went wrong during a wipe or node reassignment. + +If crypto/keys is missing, there should be no other data on the server. On a non-first-sync, this is likely to be an error state, requiring recovery. If crypto/keys changed, but the server wasn't wiped and refilled, then HMAC errors will result. + +So now we have meta/global and crypto/keys, in one of three ways: cached, fetched, or computed and uploaded. If we don't get to that point, we error out; the intermediate fetching states really aren't exposed. If we had to compute and upload -- a very uncommon situation -- we re-enter the state machine from the beginning to simplify analysis. + +We call this state "Ready". + +From Ready we can perform a number of operations: + +* We can wipe the server. + +* We can safely change datatype elections or keys for collections. + +* We can perform storage operations on one or more collections. + +The first two we'll ignore for now. They're relatively straightforward. + +The last is encapsulated in a datatype-specific synchronizer and a `Sync15CollectionClient` that pushes and pulls records. + + +The process of synchronizing is driven by info/collections (indicating remote changes), by meta/global (a change to syncID implies a reset), and by local change indicators. + + +## Invalidation + +At any point we might get a 401 in response to a storage request. At this point our token is invalidated; we abort this sync, fetch a new token, and continue. That token might itself point to a new Sync server, which might result in a fresh-start sync. + + +## Persistence + +So what do we need to persist? Quite apart from the actual datatype-specific stuff: + +* We need to store the syncID and last fetch timestamp for each collection. If the syncID changes, we need to throw away the timestamp. +* We need to store the list of enabled engines, and keep it in lockstep with meta/global. The same for declined. +* We need to store our own client ID, and use that when uploading our client record. +* We need to pay attention to kB; if it changes, e.g., via a password change, then any in-memory copy of the Sync Key should be discarded. +* We ought to cache meta/global. +* We ought to cache bulk keys. If i/c changes for crypto, we should redownload and compare. If any collection key changes, then we should act as if the syncID changed. (TODO: should we change the syncID for that collection? Probably.) + (If we can't decrypt your bulk keys, probably our kB is now invalid. If it's not, the server is corrupt.) +* We ought to remember the last info/collections. +* We should store the last info/collections timestamp, so that an i/c fetch can return 304. diff --git a/mobile/ios/Extensions/Entitlements/Fennec.entitlements b/mobile/ios/Extensions/Entitlements/Fennec.entitlements new file mode 100644 index 0000000000..d3294bef8d --- /dev/null +++ b/mobile/ios/Extensions/Entitlements/Fennec.entitlements @@ -0,0 +1,14 @@ + + + + + com.apple.security.application-groups + + group.org.mozilla.ios.Fennec + + keychain-access-groups + + $(AppIdentifierPrefix)org.mozilla.ios.Fennec + + + diff --git a/mobile/ios/Extensions/Entitlements/FennecEnterprise.entitlements b/mobile/ios/Extensions/Entitlements/FennecEnterprise.entitlements new file mode 100644 index 0000000000..bc8c56f1ba --- /dev/null +++ b/mobile/ios/Extensions/Entitlements/FennecEnterprise.entitlements @@ -0,0 +1,14 @@ + + + + + com.apple.security.application-groups + + group.org.mozilla.ios.Fennec.enterprise + + keychain-access-groups + + $(AppIdentifierPrefix)org.mozilla.ios.Fennec.enterprise + + + diff --git a/mobile/ios/Extensions/Entitlements/Firefox.entitlements b/mobile/ios/Extensions/Entitlements/Firefox.entitlements new file mode 100644 index 0000000000..3e056af7ba --- /dev/null +++ b/mobile/ios/Extensions/Entitlements/Firefox.entitlements @@ -0,0 +1,14 @@ + + + + + com.apple.security.application-groups + + group.org.mozilla.ios.Firefox + + keychain-access-groups + + $(AppIdentifierPrefix)org.mozilla.ios.Firefox + + + diff --git a/mobile/ios/Extensions/Entitlements/FirefoxBeta.entitlements b/mobile/ios/Extensions/Entitlements/FirefoxBeta.entitlements new file mode 100644 index 0000000000..b743c4052c --- /dev/null +++ b/mobile/ios/Extensions/Entitlements/FirefoxBeta.entitlements @@ -0,0 +1,14 @@ + + + + + com.apple.security.application-groups + + group.org.mozilla.ios.FirefoxBeta + + keychain-access-groups + + $(AppIdentifierPrefix)org.mozilla.ios.FirefoxBeta + + + diff --git a/mobile/ios/Extensions/NotificationService/ExtensionProfile.swift b/mobile/ios/Extensions/NotificationService/ExtensionProfile.swift new file mode 100644 index 0000000000..45579f32ee --- /dev/null +++ b/mobile/ios/Extensions/NotificationService/ExtensionProfile.swift @@ -0,0 +1,50 @@ +/* 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 Deferred +import Shared +import Sync + +// This is a cut down version of the Profile. +// This will only ever be used in the NotificationService extension. +// It allows us to customize the SyncDelegate, and later the SyncManager. +class ExtensionProfile: BrowserProfile { + init(localName: String) { + super.init(localName: localName, clear: false) + self.syncManager = ExtensionSyncManager(profile: self) + } +} + +fileprivate let extensionSafeNames = Set(["clients"]) + +// Mock class required by `BrowserProfile` +open class PanelDataObservers { + init(profile: Any) {} +} + +// Mock class required by `BrowserProfile` +open class SearchEngines { + init(prefs: Any, files: Any) {} +} + +class ExtensionSyncManager: BrowserProfile.BrowserSyncManager { + init(profile: ExtensionProfile) { + super.init(profile: profile) + } + + // We don't want to send ping data at all while we're in the extension. + override func canSendUsageData() -> Bool { + return false + } + + // We should probably only want to sync client commands while we're in the extension. + override func syncNamedCollections(why: SyncReason, names: [String]) -> Success { + let names = names.filter { extensionSafeNames.contains($0) } + return super.syncNamedCollections(why: why, names: names) + } + + override func takeActionsOnEngineStateChanges(_ changes: T) -> Deferred> { + return deferMaybe(changes) + } +} diff --git a/mobile/ios/Extensions/NotificationService/Info.plist b/mobile/ios/Extensions/NotificationService/Info.plist new file mode 100644 index 0000000000..6926a90443 --- /dev/null +++ b/mobile/ios/Extensions/NotificationService/Info.plist @@ -0,0 +1,33 @@ + + + + + MozDevelopmentTeam + $(DEVELOPMENT_TEAM) + CFBundleDevelopmentRegion + en + CFBundleDisplayName + $(MOZ_BUNDLE_DISPLAY_NAME) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + XPC! + CFBundleShortVersionString + 10.6 + CFBundleVersion + 1 + NSExtension + + NSExtensionPointIdentifier + com.apple.usernotifications.service + NSExtensionPrincipalClass + $(PRODUCT_MODULE_NAME).NotificationService + + + diff --git a/mobile/ios/Extensions/NotificationService/NotificationService.swift b/mobile/ios/Extensions/NotificationService/NotificationService.swift new file mode 100644 index 0000000000..95a2db9174 --- /dev/null +++ b/mobile/ios/Extensions/NotificationService/NotificationService.swift @@ -0,0 +1,274 @@ +/* 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 Shared +import Storage +import Sync +import UserNotifications + +class NotificationService: UNNotificationServiceExtension { + var display: SyncDataDisplay? + var profile: ExtensionProfile? + + // This is run when an APNS notification with `mutable-content` is received. + // If the app is backgrounded, then the alert notification is displayed. + // If the app is foregrounded, then the notification.userInfo is passed straight to + // AppDelegate.application(_:didReceiveRemoteNotification:completionHandler:) + // Once the notification is tapped, then the same userInfo is passed to the same method in the AppDelegate. + override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) { + let userInfo = request.content.userInfo + + guard let content = (request.content.mutableCopy() as? UNMutableNotificationContent) else { + return self.didFinish(PushMessage.accountVerified) + } + + if self.profile == nil { + self.profile = ExtensionProfile(localName: "profile") + } + + guard let profile = self.profile else { + self.didFinish(with: .noProfile) + return + } + + let queue = profile.queue + let display = SyncDataDisplay(content: content, contentHandler: contentHandler, tabQueue: queue) + self.display = display + profile.syncDelegate = display + + let handler = FxAPushMessageHandler(with: profile) + + handler.handle(userInfo: userInfo).upon { res in + self.didFinish(res.successValue, with: res.failureValue as? PushMessageError) + } + } + + func didFinish(_ what: PushMessage? = nil, with error: PushMessageError? = nil) { + profile?.shutdown() + + guard let display = self.display else { + return + } + + // We cannot use tabqueue after the profile has shutdown; + // however, we can't use weak references, because TabQueue isn't a class. + // Rather than changing tabQueue, we manually nil it out here. + display.tabQueue = nil + display.messageDelivered = false + display.displayNotification(what, with: error) + if !display.messageDelivered { + display.displayUnknownMessageNotification() + } + } + + override func serviceExtensionTimeWillExpire() { + // Called just before the extension will be terminated by the system. + // Use this as an opportunity to deliver your "best attempt" at modified content, otherwise the original push payload will be used. + didFinish(with: .timeout) + } +} + +class SyncDataDisplay { + var contentHandler: ((UNNotificationContent) -> Void) + var notificationContent: UNMutableNotificationContent + var sentTabs: [SentTab] + + var tabQueue: TabQueue? + var messageDelivered: Bool = false + + init(content: UNMutableNotificationContent, contentHandler: @escaping (UNNotificationContent) -> Void, tabQueue: TabQueue) { + self.contentHandler = contentHandler + self.notificationContent = content + self.sentTabs = [] + self.tabQueue = tabQueue + } + + func displayNotification(_ message: PushMessage? = nil, with error: PushMessageError? = nil) { + guard let message = message, error == nil else { + return displayUnknownMessageNotification() + } + + switch message { + case .accountVerified: + displayAccountVerifiedNotification() + case .deviceConnected(let deviceName): + displayDeviceConnectedNotification(deviceName) + case .deviceDisconnected(let deviceName): + displayDeviceDisconnectedNotification(deviceName) + case .thisDeviceDisconnected: + displayThisDeviceDisconnectedNotification() + case .collectionChanged(let collections): + if collections.contains("clients") { + displaySentTabNotification() + } else { + displayUnknownMessageNotification() + } + default: + displayUnknownMessageNotification() + break + } + } +} + +extension SyncDataDisplay { + func displayDeviceConnectedNotification(_ deviceName: String) { + presentNotification(title: Strings.FxAPush_DeviceConnected_title, + body: Strings.FxAPush_DeviceConnected_body, + bodyArg: deviceName) + } + + func displayDeviceDisconnectedNotification(_ deviceName: String?) { + if let deviceName = deviceName { + presentNotification(title: Strings.FxAPush_DeviceDisconnected_title, + body: Strings.FxAPush_DeviceDisconnected_body, + bodyArg: deviceName) + } else { + // We should never see this branch + presentNotification(title: Strings.FxAPush_DeviceDisconnected_title, + body: Strings.FxAPush_DeviceDisconnected_UnknownDevice_body) + } + } + + func displayThisDeviceDisconnectedNotification() { + presentNotification(title: Strings.FxAPush_DeviceDisconnected_ThisDevice_title, + body: Strings.FxAPush_DeviceDisconnected_ThisDevice_body) + } + + func displayAccountVerifiedNotification() { + presentNotification(title: Strings.SentTab_NoTabArrivingNotification_title, body: Strings.SentTab_NoTabArrivingNotification_body) + } + + func displayUnknownMessageNotification() { + // if, by any change we haven't dealt with the message, then perhaps we + // can recycle it as a sent tab message. + if sentTabs.count > 0 { + displaySentTabNotification() + } else { + presentNotification(title: Strings.SentTab_NoTabArrivingNotification_title, body: Strings.SentTab_NoTabArrivingNotification_body) + } + } +} + +extension SyncDataDisplay { + func displaySentTabNotification() { + // We will need to be more precise about calling these SentTab alerts + // once we are a) detecting different types of notifications and b) adding actions. + // For now, we need to add them so we can handle zero-tab sent-tab-notifications. + notificationContent.categoryIdentifier = "org.mozilla.ios.SentTab.placeholder" + + var userInfo = notificationContent.userInfo + + // Add the tabs we've found to userInfo, so that the AppDelegate + // doesn't have to do it again. + let serializedTabs = sentTabs.flatMap { t -> NSDictionary? in + return [ + "title": t.title, + "url": t.url.absoluteString, + "displayURL": t.url.absoluteDisplayExternalString, + "deviceName": t.deviceName as Any, + ] as NSDictionary + } + + func present(_ tabs: [NSDictionary]) { + if !tabs.isEmpty { + userInfo["sentTabs"] = tabs as NSArray + } + notificationContent.userInfo = userInfo + presentSentTabsNotification(tabs) + } + + let center = UNUserNotificationCenter.current() + center.getDeliveredNotifications { notifications in + // Let's deal with sent-tab-notifications + let sentTabNotifications = notifications.filter { + $0.request.content.categoryIdentifier == self.notificationContent.categoryIdentifier + } + + // We can delete zero tab sent-tab-notifications + let emptyTabNotificationsIds = sentTabNotifications.filter { + $0.request.content.userInfo["sentTabs"] == nil + }.map { $0.request.identifier } + center.removeDeliveredNotifications(withIdentifiers: emptyTabNotificationsIds) + + // The one we've just received (but not delivered) may not have any tabs in it either + // e.g. if the previous one consumed two tabs. + if serializedTabs.count == 0 { + // In that case, we try and recycle an existing notification (one that has a tab in it). + if let firstNonEmpty = sentTabNotifications.first(where: { $0.request.content.userInfo["sentTabs"] != nil }), + let previouslyDeliveredTabs = firstNonEmpty.request.content.userInfo["sentTabs"] as? [NSDictionary] { + center.removeDeliveredNotifications(withIdentifiers: [firstNonEmpty.request.identifier]) + return present(previouslyDeliveredTabs) + } + } + + // We have tabs in this notification, or we couldn't recycle an existing one that does. + present(serializedTabs) + } + } + + func presentSentTabsNotification(_ tabs: [NSDictionary]) { + let title: String + let body: String + + if tabs.count == 0 { + title = Strings.SentTab_NoTabArrivingNotification_title + body = Strings.SentTab_NoTabArrivingNotification_body + } else { + let deviceNames = Set(tabs.flatMap { $0["deviceName"] as? String }) + if let deviceName = deviceNames.first, deviceNames.count == 1 { + title = String(format: Strings.SentTab_TabArrivingNotification_WithDevice_title, deviceName) + } else { + title = Strings.SentTab_TabArrivingNotification_NoDevice_title + } + + if tabs.count == 1 { + // We give the fallback string as the url, + // because we have only just introduced "displayURL" as a key. + body = (tabs[0]["displayURL"] as? String) ?? + (tabs[0]["url"] as! String) + } else if deviceNames.count == 0 { + body = Strings.SentTab_TabArrivingNotification_NoDevice_body + } else { + body = String(format: Strings.SentTab_TabArrivingNotification_WithDevice_body, AppInfo.displayName) + } + } + + presentNotification(title: title, body: body) + } + + func presentNotification(title: String, body: String, titleArg: String? = nil, bodyArg: String? = nil) { + func stringWithOptionalArg(_ s: String, _ a: String?) -> String { + if let a = a { + return String(format: s, a) + } + return s + } + + notificationContent.title = stringWithOptionalArg(title, titleArg) + notificationContent.body = stringWithOptionalArg(body, bodyArg) + + // This is the only place we call the contentHandler. + contentHandler(notificationContent) + // This is the only place we change messageDelivered. We can check if contentHandler hasn't be called because of + // our logic (rather than something funny with our environment, or iOS killing us). + messageDelivered = true + } +} + +extension SyncDataDisplay: SyncDelegate { + func displaySentTab(for url: URL, title: String, from deviceName: String?) { + if url.isWebPage() { + sentTabs.append(SentTab(url: url, title: title, deviceName: deviceName)) + + let item = ShareItem(url: url.absoluteString, title: title, favicon: nil) + _ = tabQueue?.addToQueue(item) + } + } +} + +struct SentTab { + let url: URL + let title: String + let deviceName: String? +} diff --git a/mobile/ios/Extensions/SendTo/ActionViewController.swift b/mobile/ios/Extensions/SendTo/ActionViewController.swift new file mode 100644 index 0000000000..fc7f337c37 --- /dev/null +++ b/mobile/ios/Extensions/SendTo/ActionViewController.swift @@ -0,0 +1,79 @@ +/* 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 UIKit +import Shared +import Storage +import SnapKit + +/// The ActionViewController is the initial viewcontroller that is presented (full screen) when the share extension +/// is activated. Depending on whether the user is logged in or not, this viewcontroller will present either the +/// InstructionsVC or the ClientPicker VC. + +@objc(ActionViewController) +class ActionViewController: UIViewController, ClientPickerViewControllerDelegate, InstructionsViewControllerDelegate { + private var sharedItem: ShareItem? + + override func viewDidLoad() { + view.backgroundColor = UIColor.white + + super.viewDidLoad() + + if !hasAccount() { + let instructionsViewController = InstructionsViewController() + instructionsViewController.delegate = self + let navigationController = UINavigationController(rootViewController: instructionsViewController) + present(navigationController, animated: false, completion: nil) + return + } + + ExtensionUtils.extractSharedItemFromExtensionContext(self.extensionContext, completionHandler: { (item, error) -> Void in + guard let item = item, error == nil, item.isShareable else { + let alert = UIAlertController(title: Strings.SendToErrorTitle, message: Strings.SendToErrorMessage, preferredStyle: .alert) + alert.addAction(UIAlertAction(title: Strings.SendToErrorOKButton, style: .default) { _ in self.finish() }) + self.present(alert, animated: true, completion: nil) + return + } + + self.sharedItem = item + let clientPickerViewController = ClientPickerViewController() + clientPickerViewController.clientPickerDelegate = self + clientPickerViewController.profile = nil // This means the picker will open and close the default profile + let navigationController = UINavigationController(rootViewController: clientPickerViewController) + self.present(navigationController, animated: false, completion: nil) + }) + } + + func finish() { + self.extensionContext!.completeRequest(returningItems: nil, completionHandler: nil) + } + + func clientPickerViewController(_ clientPickerViewController: ClientPickerViewController, didPickClients clients: [RemoteClient]) { + guard let item = sharedItem else { + return finish() + } + + let profile = BrowserProfile(localName: "profile") + profile.sendItems([item], toClients: clients).uponQueue(DispatchQueue.main) { result in + profile.shutdown() + self.finish() + } + } + + func clientPickerViewControllerDidCancel(_ clientPickerViewController: ClientPickerViewController) { + finish() + } + + func instructionsViewControllerDidClose(_ instructionsViewController: InstructionsViewController) { + finish() + } + + private func hasAccount() -> Bool { + let profile = BrowserProfile(localName: "profile") + defer { + profile.shutdown() + } + return profile.hasAccount() + } +} diff --git a/mobile/ios/Extensions/SendTo/Base.lproj/InfoPlist.strings b/mobile/ios/Extensions/SendTo/Base.lproj/InfoPlist.strings new file mode 100644 index 0000000000..529af36839 --- /dev/null +++ b/mobile/ios/Extensions/SendTo/Base.lproj/InfoPlist.strings @@ -0,0 +1,5 @@ +/* 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/. */ + +"CFBundleDisplayName" = "Send Tab"; diff --git a/mobile/ios/Extensions/SendTo/ClientPickerViewController.swift b/mobile/ios/Extensions/SendTo/ClientPickerViewController.swift new file mode 100644 index 0000000000..c64617bdb4 --- /dev/null +++ b/mobile/ios/Extensions/SendTo/ClientPickerViewController.swift @@ -0,0 +1,320 @@ +/* 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 UIKit +import Shared +import Storage +import SnapKit + +protocol ClientPickerViewControllerDelegate { + func clientPickerViewControllerDidCancel(_ clientPickerViewController: ClientPickerViewController) + func clientPickerViewController(_ clientPickerViewController: ClientPickerViewController, didPickClients clients: [RemoteClient]) +} + +struct ClientPickerViewControllerUX { + static let TableHeaderRowHeight = CGFloat(50) + static let TableHeaderTextFont = UIFont.systemFont(ofSize: 16) + static let TableHeaderTextColor = UIColor.gray + static let TableHeaderTextPaddingLeft = CGFloat(20) + + static let DeviceRowTintColor = UIColor(red: 0.427, green: 0.800, blue: 0.102, alpha: 1.0) + static let DeviceRowHeight = CGFloat(50) + static let DeviceRowTextFont = UIFont.systemFont(ofSize: 16) + static let DeviceRowTextPaddingLeft = CGFloat(72) + static let DeviceRowTextPaddingRight = CGFloat(50) +} + +/// The ClientPickerViewController displays a list of clients associated with the provided Account. +/// The user can select a number of devices and hit the Send button. +/// This viewcontroller does not implement any specific business logic that needs to happen with the selected clients. +/// That is up to it's delegate, who can listen for cancellation and success events. + +class ClientPickerViewController: UITableViewController { + var profile: Profile? + var profileNeedsShutdown = true + + var clientPickerDelegate: ClientPickerViewControllerDelegate? + + var reloading = true + var clients: [RemoteClient] = [] + var selectedClients = NSMutableSet() + + // ShareItem has been added as we are now using this class outside of the ShareTo extension to provide Share To functionality + // And in this case we need to be able to store the item we are sharing as we may not have access to the + // url later. Currently used only when sharing an item from the Tab Tray from a Preview Action. + var shareItem: ShareItem? + + override func viewDidLoad() { + super.viewDidLoad() + title = NSLocalizedString("Send Tab", tableName: "SendTo", comment: "Title of the dialog that allows you to send a tab to a different device") + refreshControl = UIRefreshControl() + refreshControl?.addTarget(self, action: #selector(ClientPickerViewController.refresh), for: UIControlEvents.valueChanged) + navigationItem.leftBarButtonItem = UIBarButtonItem( + title: Strings.SendToCancelButton, + style: .plain, + target: self, + action: #selector(ClientPickerViewController.cancel) + ) + + tableView.register(ClientPickerTableViewHeaderCell.self, forCellReuseIdentifier: ClientPickerTableViewHeaderCell.CellIdentifier) + tableView.register(ClientPickerTableViewCell.self, forCellReuseIdentifier: ClientPickerTableViewCell.CellIdentifier) + tableView.register(ClientPickerNoClientsTableViewCell.self, forCellReuseIdentifier: ClientPickerNoClientsTableViewCell.CellIdentifier) + tableView.tableFooterView = UIView(frame: CGRect.zero) + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + if let refreshControl = refreshControl { + refreshControl.beginRefreshing() + let height = -(refreshControl.bounds.size.height + (self.navigationController?.navigationBar.bounds.size.height ?? 0)) + self.tableView.contentOffset = CGPoint(x: 0, y: height) + } + reloadClients() + } + + override func numberOfSections(in tableView: UITableView) -> Int { + if clients.count == 0 { + return 1 + } else { + return 2 + } + } + + override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + if clients.count == 0 { + return 1 + } else { + if section == 0 { + return 1 + } else { + return clients.count + } + } + } + + override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let cell: UITableViewCell + + if clients.count > 0 { + if indexPath.section == 0 { + cell = tableView.dequeueReusableCell(withIdentifier: ClientPickerTableViewHeaderCell.CellIdentifier, for: indexPath) as! ClientPickerTableViewHeaderCell + } else { + let clientCell = tableView.dequeueReusableCell(withIdentifier: ClientPickerTableViewCell.CellIdentifier, for: indexPath) as! ClientPickerTableViewCell + clientCell.nameLabel.text = clients[indexPath.row].name + clientCell.clientType = clients[indexPath.row].type == "mobile" ? ClientType.Mobile : ClientType.Desktop + clientCell.checked = selectedClients.contains(indexPath) + cell = clientCell + } + } else { + if reloading == false { + cell = tableView.dequeueReusableCell(withIdentifier: ClientPickerNoClientsTableViewCell.CellIdentifier, for: indexPath) as! ClientPickerNoClientsTableViewCell + } else { + cell = UITableViewCell(style: UITableViewCellStyle.default, reuseIdentifier: "ClientCell") + } + } + + return cell + } + + override func tableView(_ tableView: UITableView, shouldHighlightRowAt indexPath: IndexPath) -> Bool { + return indexPath.section != 0 + } + + override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + if clients.count > 0 && indexPath.section == 1 { + tableView.deselectRow(at: indexPath, animated: true) + + if selectedClients.contains(indexPath) { + selectedClients.remove(indexPath) + } else { + selectedClients.add(indexPath) + } + + tableView.reloadRows(at: [indexPath], with: UITableViewRowAnimation.none) + + navigationItem.rightBarButtonItem?.isEnabled = (selectedClients.count != 0) + } + } + + override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { + if clients.count > 0 { + if indexPath.section == 0 { + return ClientPickerViewControllerUX.TableHeaderRowHeight + } else { + return ClientPickerViewControllerUX.DeviceRowHeight + } + } else { + return tableView.frame.height + } + } + + fileprivate func reloadClients() { + // If we were not given a profile, open the default profile. This happens in case we are called from an app + // extension. That also means that we need to shut down the profile, otherwise the app extension will be + // terminated when it goes into the background. + + if self.profile == nil { + self.profile = BrowserProfile(localName: "profile") + self.profileNeedsShutdown = true + } + + guard let profile = self.profile else { + return + } + + // Re-open the profile it was shutdown. This happens when we run from an app extension, where we must + // make sure that the profile is only open for brief moments of time. + + if profile.isShutdown { + profile.reopen() + } + + reloading = true + profile.getClients().upon({ result in + withExtendedLifetime(profile) { + // If we are running from an app extension then make sure we shut down the profile as soon as we are + // done with it. + + if self.profileNeedsShutdown { + profile.shutdown() + } + + self.reloading = false + guard let c = result.successValue else { + return + } + + self.clients = c + DispatchQueue.main.async { + if self.clients.count == 0 { + self.navigationItem.rightBarButtonItem = nil + } else { + self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: NSLocalizedString("Send", tableName: "SendTo", comment: "Navigation bar button to Send the current page to a device"), style: UIBarButtonItemStyle.done, target: self, action: #selector(ClientPickerViewController.send)) + self.navigationItem.rightBarButtonItem?.isEnabled = false + } + self.selectedClients.removeAllObjects() + self.tableView.reloadData() + self.refreshControl?.endRefreshing() + } + } + }) + } + + func refresh() { + reloadClients() + } + + func cancel() { + clientPickerDelegate?.clientPickerViewControllerDidCancel(self) + } + + func send() { + var clients = [RemoteClient]() + for indexPath in selectedClients { + clients.append(self.clients[(indexPath as AnyObject).row]) + } + clientPickerDelegate?.clientPickerViewController(self, didPickClients: clients) + + // Replace the Send button with a loading indicator since it takes a while to sync + // up our changes to the server. + let loadingIndicator = UIActivityIndicatorView(frame: CGRect(x: 0, y: 0, width: 25, height: 25)) + loadingIndicator.color = .darkGray + loadingIndicator.startAnimating() + let customBarButton = UIBarButtonItem(customView: loadingIndicator) + self.navigationItem.rightBarButtonItem = customBarButton + } +} + +class ClientPickerTableViewHeaderCell: UITableViewCell { + static let CellIdentifier = "ClientPickerTableViewSectionHeader" + let nameLabel = UILabel() + + override init(style: UITableViewCellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + addSubview(nameLabel) + nameLabel.font = ClientPickerViewControllerUX.TableHeaderTextFont + nameLabel.text = NSLocalizedString("Available devices:", tableName: "SendTo", comment: "Header for the list of devices table") + nameLabel.textColor = ClientPickerViewControllerUX.TableHeaderTextColor + + nameLabel.snp.makeConstraints { (make) -> Void in + make.left.equalTo(ClientPickerViewControllerUX.TableHeaderTextPaddingLeft) + make.centerY.equalTo(self) + make.right.equalTo(self) + } + + preservesSuperviewLayoutMargins = false + layoutMargins = UIEdgeInsets.zero + separatorInset = UIEdgeInsets.zero + } + + required init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +public enum ClientType: String { + case Mobile = "deviceTypeMobile" + case Desktop = "deviceTypeDesktop" +} + +class ClientPickerTableViewCell: UITableViewCell { + static let CellIdentifier = "ClientPickerTableViewCell" + + var nameLabel: UILabel + var checked: Bool = false { + didSet { + self.accessoryType = checked ? UITableViewCellAccessoryType.checkmark : UITableViewCellAccessoryType.none + } + } + + var clientType: ClientType = ClientType.Mobile { + didSet { + self.imageView?.image = UIImage(named: clientType.rawValue) + } + } + + override init(style: UITableViewCellStyle, reuseIdentifier: String?) { + nameLabel = UILabel() + super.init(style: style, reuseIdentifier: reuseIdentifier) + contentView.addSubview(nameLabel) + nameLabel.font = ClientPickerViewControllerUX.DeviceRowTextFont + nameLabel.numberOfLines = 2 + nameLabel.lineBreakMode = NSLineBreakMode.byWordWrapping + self.tintColor = ClientPickerViewControllerUX.DeviceRowTintColor + self.preservesSuperviewLayoutMargins = false + self.selectionStyle = UITableViewCellSelectionStyle.none + } + + override func layoutSubviews() { + super.layoutSubviews() + + nameLabel.snp.makeConstraints { (make) -> Void in + make.left.equalTo(ClientPickerViewControllerUX.DeviceRowTextPaddingLeft) + make.centerY.equalTo(self.snp.centerY) + make.right.equalTo(self.snp.right).offset(-ClientPickerViewControllerUX.DeviceRowTextPaddingRight) + } + } + + required init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + +} + +class ClientPickerNoClientsTableViewCell: UITableViewCell { + static let CellIdentifier = "ClientPickerNoClientsTableViewCell" + + override init(style: UITableViewCellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + setupHelpView(contentView, + introText: NSLocalizedString("You don’t have any other devices connected to this Firefox Account available to sync.", tableName: "SendTo", comment: "Error message shown in the remote tabs panel"), + showMeText: "") // TODO We used to have a 'show me how to ...' text here. But, we cannot open web pages from the extension. So this is clear for now until we decide otherwise. + // Move the separator off screen + separatorInset = UIEdgeInsets(top: 0, left: 1000, bottom: 0, right: 0) + } + + required init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} diff --git a/mobile/ios/Extensions/SendTo/Images.xcassets/AppIcon.appiconset/Contents.json b/mobile/ios/Extensions/SendTo/Images.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000000..28a919cfa2 --- /dev/null +++ b/mobile/ios/Extensions/SendTo/Images.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,138 @@ +{ + "images" : [ + { + "idiom" : "iphone", + "size" : "20x20", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "20x20", + "scale" : "3x" + }, + { + "idiom" : "iphone", + "size" : "29x29", + "scale" : "1x" + }, + { + "idiom" : "iphone", + "size" : "29x29", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "29x29", + "scale" : "3x" + }, + { + "idiom" : "iphone", + "size" : "40x40", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "40x40", + "scale" : "3x" + }, + { + "idiom" : "iphone", + "size" : "57x57", + "scale" : "1x" + }, + { + "idiom" : "iphone", + "size" : "57x57", + "scale" : "2x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "send-60@2x.png", + "scale" : "2x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "send-60@3x.png", + "scale" : "3x" + }, + { + "idiom" : "ipad", + "size" : "20x20", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "20x20", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "29x29", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "29x29", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "40x40", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "40x40", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "50x50", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "50x50", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "72x72", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "72x72", + "scale" : "2x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "send-76.png", + "scale" : "1x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "send-76@2x.png", + "scale" : "2x" + }, + { + "size" : "83.5x83.5", + "idiom" : "ipad", + "filename" : "send-83.5@2x.png", + "scale" : "2x" + }, + { + "idiom" : "ios-marketing", + "size" : "1024x1024", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/mobile/ios/Extensions/SendTo/Images.xcassets/AppIcon.appiconset/send-60@2x.png b/mobile/ios/Extensions/SendTo/Images.xcassets/AppIcon.appiconset/send-60@2x.png new file mode 100644 index 0000000000..487ed390aa Binary files /dev/null and b/mobile/ios/Extensions/SendTo/Images.xcassets/AppIcon.appiconset/send-60@2x.png differ diff --git a/mobile/ios/Extensions/SendTo/Images.xcassets/AppIcon.appiconset/send-60@3x.png b/mobile/ios/Extensions/SendTo/Images.xcassets/AppIcon.appiconset/send-60@3x.png new file mode 100644 index 0000000000..d8feffdccb Binary files /dev/null and b/mobile/ios/Extensions/SendTo/Images.xcassets/AppIcon.appiconset/send-60@3x.png differ diff --git a/mobile/ios/Extensions/SendTo/Images.xcassets/AppIcon.appiconset/send-76.png b/mobile/ios/Extensions/SendTo/Images.xcassets/AppIcon.appiconset/send-76.png new file mode 100644 index 0000000000..a2a0ab768a Binary files /dev/null and b/mobile/ios/Extensions/SendTo/Images.xcassets/AppIcon.appiconset/send-76.png differ diff --git a/mobile/ios/Extensions/SendTo/Images.xcassets/AppIcon.appiconset/send-76@2x.png b/mobile/ios/Extensions/SendTo/Images.xcassets/AppIcon.appiconset/send-76@2x.png new file mode 100644 index 0000000000..0aff7eef97 Binary files /dev/null and b/mobile/ios/Extensions/SendTo/Images.xcassets/AppIcon.appiconset/send-76@2x.png differ diff --git a/mobile/ios/Extensions/SendTo/Images.xcassets/AppIcon.appiconset/send-83.5@2x.png b/mobile/ios/Extensions/SendTo/Images.xcassets/AppIcon.appiconset/send-83.5@2x.png new file mode 100644 index 0000000000..21bffd190f Binary files /dev/null and b/mobile/ios/Extensions/SendTo/Images.xcassets/AppIcon.appiconset/send-83.5@2x.png differ diff --git a/mobile/ios/Extensions/SendTo/Images.xcassets/deviceTypeDesktop.imageset/Contents.json b/mobile/ios/Extensions/SendTo/Images.xcassets/deviceTypeDesktop.imageset/Contents.json new file mode 100644 index 0000000000..bb76c612f3 --- /dev/null +++ b/mobile/ios/Extensions/SendTo/Images.xcassets/deviceTypeDesktop.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x", + "filename" : "deviceTypeDesktop.png" + }, + { + "idiom" : "universal", + "scale" : "2x", + "filename" : "deviceTypeDesktop@2x.png" + }, + { + "idiom" : "universal", + "scale" : "3x", + "filename" : "deviceTypeDesktop@3x.png" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/mobile/ios/Extensions/SendTo/Images.xcassets/deviceTypeDesktop.imageset/deviceTypeDesktop.png b/mobile/ios/Extensions/SendTo/Images.xcassets/deviceTypeDesktop.imageset/deviceTypeDesktop.png new file mode 100644 index 0000000000..98dadc99ea Binary files /dev/null and b/mobile/ios/Extensions/SendTo/Images.xcassets/deviceTypeDesktop.imageset/deviceTypeDesktop.png differ diff --git a/mobile/ios/Extensions/SendTo/Images.xcassets/deviceTypeDesktop.imageset/deviceTypeDesktop@2x.png b/mobile/ios/Extensions/SendTo/Images.xcassets/deviceTypeDesktop.imageset/deviceTypeDesktop@2x.png new file mode 100644 index 0000000000..1d2898e243 Binary files /dev/null and b/mobile/ios/Extensions/SendTo/Images.xcassets/deviceTypeDesktop.imageset/deviceTypeDesktop@2x.png differ diff --git a/mobile/ios/Extensions/SendTo/Images.xcassets/deviceTypeDesktop.imageset/deviceTypeDesktop@3x.png b/mobile/ios/Extensions/SendTo/Images.xcassets/deviceTypeDesktop.imageset/deviceTypeDesktop@3x.png new file mode 100644 index 0000000000..5d5ab5577b Binary files /dev/null and b/mobile/ios/Extensions/SendTo/Images.xcassets/deviceTypeDesktop.imageset/deviceTypeDesktop@3x.png differ diff --git a/mobile/ios/Extensions/SendTo/Images.xcassets/deviceTypeMobile.imageset/Contents.json b/mobile/ios/Extensions/SendTo/Images.xcassets/deviceTypeMobile.imageset/Contents.json new file mode 100644 index 0000000000..e936c7ebaf --- /dev/null +++ b/mobile/ios/Extensions/SendTo/Images.xcassets/deviceTypeMobile.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x", + "filename" : "deviceTypeMobile.png" + }, + { + "idiom" : "universal", + "scale" : "2x", + "filename" : "deviceTypeMobile@2x.png" + }, + { + "idiom" : "universal", + "scale" : "3x", + "filename" : "deviceTypeMobile@3x.png" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/mobile/ios/Extensions/SendTo/Images.xcassets/deviceTypeMobile.imageset/deviceTypeMobile.png b/mobile/ios/Extensions/SendTo/Images.xcassets/deviceTypeMobile.imageset/deviceTypeMobile.png new file mode 100644 index 0000000000..f17820f821 Binary files /dev/null and b/mobile/ios/Extensions/SendTo/Images.xcassets/deviceTypeMobile.imageset/deviceTypeMobile.png differ diff --git a/mobile/ios/Extensions/SendTo/Images.xcassets/deviceTypeMobile.imageset/deviceTypeMobile@2x.png b/mobile/ios/Extensions/SendTo/Images.xcassets/deviceTypeMobile.imageset/deviceTypeMobile@2x.png new file mode 100644 index 0000000000..d8d855d7c2 Binary files /dev/null and b/mobile/ios/Extensions/SendTo/Images.xcassets/deviceTypeMobile.imageset/deviceTypeMobile@2x.png differ diff --git a/mobile/ios/Extensions/SendTo/Images.xcassets/deviceTypeMobile.imageset/deviceTypeMobile@3x.png b/mobile/ios/Extensions/SendTo/Images.xcassets/deviceTypeMobile.imageset/deviceTypeMobile@3x.png new file mode 100644 index 0000000000..19df48506f Binary files /dev/null and b/mobile/ios/Extensions/SendTo/Images.xcassets/deviceTypeMobile.imageset/deviceTypeMobile@3x.png differ diff --git a/mobile/ios/Extensions/SendTo/Images.xcassets/emptySync.imageset/Contents.json b/mobile/ios/Extensions/SendTo/Images.xcassets/emptySync.imageset/Contents.json new file mode 100644 index 0000000000..0307cc7236 --- /dev/null +++ b/mobile/ios/Extensions/SendTo/Images.xcassets/emptySync.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x", + "filename" : "emptySync.png" + }, + { + "idiom" : "universal", + "scale" : "2x", + "filename" : "emptySync@2x.png" + }, + { + "idiom" : "universal", + "scale" : "3x", + "filename" : "emptySync@3x.png" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/mobile/ios/Extensions/SendTo/Images.xcassets/emptySync.imageset/emptySync.png b/mobile/ios/Extensions/SendTo/Images.xcassets/emptySync.imageset/emptySync.png new file mode 100644 index 0000000000..c11fe82e4b Binary files /dev/null and b/mobile/ios/Extensions/SendTo/Images.xcassets/emptySync.imageset/emptySync.png differ diff --git a/mobile/ios/Extensions/SendTo/Images.xcassets/emptySync.imageset/emptySync@2x.png b/mobile/ios/Extensions/SendTo/Images.xcassets/emptySync.imageset/emptySync@2x.png new file mode 100644 index 0000000000..59db26d3d3 Binary files /dev/null and b/mobile/ios/Extensions/SendTo/Images.xcassets/emptySync.imageset/emptySync@2x.png differ diff --git a/mobile/ios/Extensions/SendTo/Images.xcassets/emptySync.imageset/emptySync@3x.png b/mobile/ios/Extensions/SendTo/Images.xcassets/emptySync.imageset/emptySync@3x.png new file mode 100644 index 0000000000..aa5f3c242c Binary files /dev/null and b/mobile/ios/Extensions/SendTo/Images.xcassets/emptySync.imageset/emptySync@3x.png differ diff --git a/mobile/ios/Extensions/SendTo/Info.plist b/mobile/ios/Extensions/SendTo/Info.plist new file mode 100644 index 0000000000..917157f93c --- /dev/null +++ b/mobile/ios/Extensions/SendTo/Info.plist @@ -0,0 +1,48 @@ + + + + + MozDevelopmentTeam + $(DEVELOPMENT_TEAM) + CFBundleDevelopmentRegion + en + CFBundleDisplayName + CFBundleDisplayName + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIcons + + CFBundleIcons~ipad + + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + XPC! + CFBundleShortVersionString + 10.6 + CFBundleSignature + ???? + CFBundleVersion + 1 + UIAppFonts + + FiraSans-Regular.ttf + + NSExtension + + NSExtensionAttributes + + NSExtensionActivationRule + SUBQUERY ( extensionItems, $extensionItem, SUBQUERY ( $extensionItem.attachments, $attachment, ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.url").@count == 1 ).@count == 1 + + NSExtensionPointIdentifier + com.apple.ui-services + NSExtensionPrincipalClass + ActionViewController + + + diff --git a/mobile/ios/Extensions/SendTo/InstructionsViewController.swift b/mobile/ios/Extensions/SendTo/InstructionsViewController.swift new file mode 100644 index 0000000000..85972240f3 --- /dev/null +++ b/mobile/ios/Extensions/SendTo/InstructionsViewController.swift @@ -0,0 +1,95 @@ +/* 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 UIKit +import SnapKit + +struct InstructionsViewControllerUX { + static let TopPadding = CGFloat(20) + static let TextFont = UIFont.systemFont(ofSize: UIFont.labelFontSize) + static let TextColor = UIColor(rgb: 0x555555) + static let LinkColor = UIColor.blue +} + +protocol InstructionsViewControllerDelegate: class { + func instructionsViewControllerDidClose(_ instructionsViewController: InstructionsViewController) +} + +private func highlightLink(_ s: NSString, withColor color: UIColor) -> NSAttributedString { + let start = s.range(of: "<") + if start.location == NSNotFound { + return NSAttributedString(string: s as String) + } + + var s: NSString = s.replacingCharacters(in: start, with: "") as NSString + let end = s.range(of: ">") + s = s.replacingCharacters(in: end, with: "") as NSString + let a = NSMutableAttributedString(string: s as String) + let r = NSRange(location: start.location, length: end.location-start.location) + a.addAttribute(NSForegroundColorAttributeName, value: color, range: r) + return a +} + +func setupHelpView(_ view: UIView, introText: String, showMeText: String) { + let imageView = UIImageView() + imageView.image = UIImage(named: "emptySync") + view.addSubview(imageView) + imageView.snp.makeConstraints { (make) -> Void in + make.top.equalTo(view).offset(InstructionsViewControllerUX.TopPadding) + make.centerX.equalTo(view) + } + + let label1 = UILabel() + view.addSubview(label1) + label1.text = introText + label1.numberOfLines = 0 + label1.lineBreakMode = NSLineBreakMode.byWordWrapping + label1.font = InstructionsViewControllerUX.TextFont + label1.textColor = InstructionsViewControllerUX.TextColor + label1.textAlignment = NSTextAlignment.center + label1.snp.makeConstraints { (make) -> Void in + make.width.equalTo(250) + make.top.equalTo(imageView.snp.bottom).offset(InstructionsViewControllerUX.TopPadding) + make.centerX.equalTo(view) + } + + let label2 = UILabel() + view.addSubview(label2) + label2.numberOfLines = 0 + label2.lineBreakMode = NSLineBreakMode.byWordWrapping + label2.font = InstructionsViewControllerUX.TextFont + label2.textColor = InstructionsViewControllerUX.TextColor + label2.textAlignment = NSTextAlignment.center + label2.attributedText = highlightLink(showMeText as NSString, withColor: InstructionsViewControllerUX.LinkColor) + label2.snp.makeConstraints { (make) -> Void in + make.width.equalTo(250) + make.top.equalTo(label1.snp.bottom).offset(InstructionsViewControllerUX.TopPadding) + make.centerX.equalTo(view) + } +} + +class InstructionsViewController: UIViewController { + weak var delegate: InstructionsViewControllerDelegate? + + override func viewDidLoad() { + super.viewDidLoad() + edgesForExtendedLayout = [] + view.backgroundColor = UIColor.white + + navigationItem.leftBarButtonItem = UIBarButtonItem(title: NSLocalizedString("Close", tableName: "SendTo", comment: "Close button in top navigation bar"), style: UIBarButtonItemStyle.done, target: self, action: #selector(InstructionsViewController.close)) + navigationItem.leftBarButtonItem?.accessibilityIdentifier = "InstructionsViewController.navigationItem.leftBarButtonItem" + + setupHelpView(view, + introText: NSLocalizedString("You are not signed in to your Firefox Account.", tableName: "SendTo", comment: "See http://mzl.la/1ISlXnU"), + showMeText: NSLocalizedString("Please open Firefox, go to Settings and sign in to continue.", tableName: "SendTo", comment: "See http://mzl.la/1ISlXnU")) + } + + func close() { + delegate?.instructionsViewControllerDidClose(self) + } + + func showMeHow() { + print("Show me how") // TODO Not sure what to do or if to keep this. Waiting for UX feedback. + } +} diff --git a/mobile/ios/Extensions/SendTo/MainInterface.storyboard b/mobile/ios/Extensions/SendTo/MainInterface.storyboard new file mode 100644 index 0000000000..a7b20d60d4 --- /dev/null +++ b/mobile/ios/Extensions/SendTo/MainInterface.storyboard @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/mobile/ios/Extensions/SendTo/en.lproj/InfoPlist.strings b/mobile/ios/Extensions/SendTo/en.lproj/InfoPlist.strings new file mode 100644 index 0000000000..529af36839 --- /dev/null +++ b/mobile/ios/Extensions/SendTo/en.lproj/InfoPlist.strings @@ -0,0 +1,5 @@ +/* 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/. */ + +"CFBundleDisplayName" = "Send Tab"; diff --git a/mobile/ios/Extensions/ShareTo/Images.xcassets/AddToBookmarks.imageset/Contents.json b/mobile/ios/Extensions/ShareTo/Images.xcassets/AddToBookmarks.imageset/Contents.json new file mode 100644 index 0000000000..e7fc60dd35 --- /dev/null +++ b/mobile/ios/Extensions/ShareTo/Images.xcassets/AddToBookmarks.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "menu-Bookmark.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "menu-Bookmark@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "menu-Bookmark@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/mobile/ios/Extensions/ShareTo/Images.xcassets/AddToBookmarks.imageset/menu-Bookmark.png b/mobile/ios/Extensions/ShareTo/Images.xcassets/AddToBookmarks.imageset/menu-Bookmark.png new file mode 100644 index 0000000000..214afcb29e Binary files /dev/null and b/mobile/ios/Extensions/ShareTo/Images.xcassets/AddToBookmarks.imageset/menu-Bookmark.png differ diff --git a/mobile/ios/Extensions/ShareTo/Images.xcassets/AddToBookmarks.imageset/menu-Bookmark@2x.png b/mobile/ios/Extensions/ShareTo/Images.xcassets/AddToBookmarks.imageset/menu-Bookmark@2x.png new file mode 100644 index 0000000000..028721ac4b Binary files /dev/null and b/mobile/ios/Extensions/ShareTo/Images.xcassets/AddToBookmarks.imageset/menu-Bookmark@2x.png differ diff --git a/mobile/ios/Extensions/ShareTo/Images.xcassets/AddToBookmarks.imageset/menu-Bookmark@3x.png b/mobile/ios/Extensions/ShareTo/Images.xcassets/AddToBookmarks.imageset/menu-Bookmark@3x.png new file mode 100644 index 0000000000..d4d18ab6c6 Binary files /dev/null and b/mobile/ios/Extensions/ShareTo/Images.xcassets/AddToBookmarks.imageset/menu-Bookmark@3x.png differ diff --git a/mobile/ios/Extensions/ShareTo/Images.xcassets/AddToReadingList.imageset/Contents.json b/mobile/ios/Extensions/ShareTo/Images.xcassets/AddToReadingList.imageset/Contents.json new file mode 100644 index 0000000000..d505445f08 --- /dev/null +++ b/mobile/ios/Extensions/ShareTo/Images.xcassets/AddToReadingList.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "reading-list-add.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "reading-list-add@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "reading-list-add@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/mobile/ios/Extensions/ShareTo/Images.xcassets/AddToReadingList.imageset/reading-list-add.png b/mobile/ios/Extensions/ShareTo/Images.xcassets/AddToReadingList.imageset/reading-list-add.png new file mode 100644 index 0000000000..c1d899a31f Binary files /dev/null and b/mobile/ios/Extensions/ShareTo/Images.xcassets/AddToReadingList.imageset/reading-list-add.png differ diff --git a/mobile/ios/Extensions/ShareTo/Images.xcassets/AddToReadingList.imageset/reading-list-add@2x.png b/mobile/ios/Extensions/ShareTo/Images.xcassets/AddToReadingList.imageset/reading-list-add@2x.png new file mode 100644 index 0000000000..66ca5a99ec Binary files /dev/null and b/mobile/ios/Extensions/ShareTo/Images.xcassets/AddToReadingList.imageset/reading-list-add@2x.png differ diff --git a/mobile/ios/Extensions/ShareTo/Images.xcassets/AddToReadingList.imageset/reading-list-add@3x.png b/mobile/ios/Extensions/ShareTo/Images.xcassets/AddToReadingList.imageset/reading-list-add@3x.png new file mode 100644 index 0000000000..998f4a164c Binary files /dev/null and b/mobile/ios/Extensions/ShareTo/Images.xcassets/AddToReadingList.imageset/reading-list-add@3x.png differ diff --git a/mobile/ios/Extensions/ShareTo/Images.xcassets/Icon-Small.imageset/Contents.json b/mobile/ios/Extensions/ShareTo/Images.xcassets/Icon-Small.imageset/Contents.json new file mode 100644 index 0000000000..37ebd6b916 --- /dev/null +++ b/mobile/ios/Extensions/ShareTo/Images.xcassets/Icon-Small.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "icon-40.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "icon-40@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "icon-40@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/mobile/ios/Extensions/ShareTo/Images.xcassets/Icon-Small.imageset/icon-40.png b/mobile/ios/Extensions/ShareTo/Images.xcassets/Icon-Small.imageset/icon-40.png new file mode 100644 index 0000000000..dc9b5a090f Binary files /dev/null and b/mobile/ios/Extensions/ShareTo/Images.xcassets/Icon-Small.imageset/icon-40.png differ diff --git a/mobile/ios/Extensions/ShareTo/Images.xcassets/Icon-Small.imageset/icon-40@2x.png b/mobile/ios/Extensions/ShareTo/Images.xcassets/Icon-Small.imageset/icon-40@2x.png new file mode 100644 index 0000000000..49db3894f1 Binary files /dev/null and b/mobile/ios/Extensions/ShareTo/Images.xcassets/Icon-Small.imageset/icon-40@2x.png differ diff --git a/mobile/ios/Extensions/ShareTo/Images.xcassets/Icon-Small.imageset/icon-40@3x.png b/mobile/ios/Extensions/ShareTo/Images.xcassets/Icon-Small.imageset/icon-40@3x.png new file mode 100644 index 0000000000..3b09cd5dbd Binary files /dev/null and b/mobile/ios/Extensions/ShareTo/Images.xcassets/Icon-Small.imageset/icon-40@3x.png differ diff --git a/mobile/ios/Extensions/ShareTo/Info.plist b/mobile/ios/Extensions/ShareTo/Info.plist new file mode 100644 index 0000000000..33f4c86a69 --- /dev/null +++ b/mobile/ios/Extensions/ShareTo/Info.plist @@ -0,0 +1,48 @@ + + + + + MozDevelopmentTeam + $(DEVELOPMENT_TEAM) + CFBundleDevelopmentRegion + en + CFBundleDisplayName + $(MOZ_BUNDLE_DISPLAY_NAME) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + XPC! + CFBundleShortVersionString + 10.6 + CFBundleSignature + ???? + CFBundleVersion + 1 + NSExtension + + NSExtensionAttributes + + NSExtensionActivationRule + SUBQUERY ( extensionItems, $extensionItem, SUBQUERY ( $extensionItem.attachments, $attachment, ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.url").@count == 1 ).@count == 1 + + NSExtensionPointIdentifier + com.apple.share-services + NSExtensionPrincipalClass + InitialViewController + + UIAppFonts + + FiraSans-Regular.ttf + FiraSans-SemiBold.ttf + FiraSans-Light.ttf + FiraSans-UltraLight.ttf + FiraSans-Medium.ttf + + + diff --git a/mobile/ios/Extensions/ShareTo/InitialViewController.swift b/mobile/ios/Extensions/ShareTo/InitialViewController.swift new file mode 100644 index 0000000000..f9c80f2061 --- /dev/null +++ b/mobile/ios/Extensions/ShareTo/InitialViewController.swift @@ -0,0 +1,136 @@ +/* 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 UIKit +import Shared +import Storage + +private let LastUsedShareDestinationsKey = "LastUsedShareDestinations" + +@objc(InitialViewController) +class InitialViewController: UIViewController, ShareControllerDelegate { + var shareDialogController: ShareDialogController! + + override func viewDidLoad() { + super.viewDidLoad() + self.view.backgroundColor = UIColor(white: 0.0, alpha: 0.66) // TODO: Is the correct color documented somewhere? + } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + ExtensionUtils.extractSharedItemFromExtensionContext(self.extensionContext, completionHandler: { (item, error) -> Void in + if let item = item, error == nil { + DispatchQueue.main.async { + guard item.isShareable else { + let alert = UIAlertController(title: Strings.SendToErrorTitle, message: Strings.SendToErrorMessage, preferredStyle: .alert) + alert.addAction(UIAlertAction(title: Strings.SendToErrorOKButton, style: .default) { _ in self.finish() }) + self.present(alert, animated: true, completion: nil) + return + } + + self.presentShareDialog(item) + } + } else { + self.extensionContext!.completeRequest(returningItems: [], completionHandler: nil) + } + }) + } + + // + + func shareControllerDidCancel(_ shareController: ShareDialogController) { + UIView.animate(withDuration: 0.25, animations: { () -> Void in + self.shareDialogController.view.alpha = 0.0 + }, completion: { (Bool) -> Void in + self.dismissShareDialog() + self.finish() + }) + } + + func finish() { + self.extensionContext!.completeRequest(returningItems: [], completionHandler: nil) + } + + func shareController(_ shareController: ShareDialogController, didShareItem item: ShareItem, toDestinations destinations: NSSet) { + setLastUsedShareDestinations(destinations) + + UIView.animate(withDuration: 0.25, animations: { () -> Void in + self.shareDialogController.view.alpha = 0.0 + }, completion: { (Bool) -> Void in + self.dismissShareDialog() + + let profile = BrowserProfile(localName: "profile") + + if destinations.contains(ShareDestinationReadingList) { + profile.readingList?.createRecordWithURL(item.url, title: item.title ?? "", addedBy: UIDevice.current.name) + } + + if destinations.contains(ShareDestinationBookmarks) { + _ = profile.bookmarks.shareItem(item).value // Blocks until database has settled + } + + profile.shutdown() + + self.finish() + }) + } + + // + + // TODO: use Set. + func getLastUsedShareDestinations() -> NSSet { + if let destinations = UserDefaults.standard.object(forKey: LastUsedShareDestinationsKey) as? NSArray { + return NSSet(array: destinations as [AnyObject]) + } + return NSSet(object: ShareDestinationBookmarks) + } + + func setLastUsedShareDestinations(_ destinations: NSSet) { + UserDefaults.standard.set(destinations.allObjects, forKey: LastUsedShareDestinationsKey) + UserDefaults.standard.synchronize() + } + + func presentShareDialog(_ item: ShareItem) { + shareDialogController = ShareDialogController() + shareDialogController.delegate = self + shareDialogController.item = item + shareDialogController.initialShareDestinations = getLastUsedShareDestinations() + + self.addChildViewController(shareDialogController) + shareDialogController.view.translatesAutoresizingMaskIntoConstraints = false + self.view.addSubview(shareDialogController.view) + shareDialogController.didMove(toParentViewController: self) + + // Setup constraints for the dialog. We keep the dialog centered with 16 points of padding on both + // sides. The dialog grows to max 380 points wide so that it does not look too big on landscape or + // iPad devices. + + let views: NSDictionary = ["dialog": shareDialogController.view] + + view.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "H:|-(16@751)-[dialog(<=380@1000)]-(16@751)-|", + options: NSLayoutFormatOptions(), metrics: nil, views: (views as? [String: AnyObject])!)) + + let cx = NSLayoutConstraint(item: shareDialogController.view, attribute: NSLayoutAttribute.centerX, + relatedBy: NSLayoutRelation.equal, toItem: view, attribute: NSLayoutAttribute.centerX, multiplier: 1.0, constant: 0) + cx.priority = 1000 // TODO: Why does UILayoutPriorityRequired give a linker error? SDK Bug? + view.addConstraint(cx) + + view.addConstraint(NSLayoutConstraint(item: shareDialogController.view, attribute: NSLayoutAttribute.centerY, + relatedBy: NSLayoutRelation.equal, toItem: view, attribute: NSLayoutAttribute.centerY, multiplier: 1.0, constant: 0)) + + // Fade the dialog in + + shareDialogController.view.alpha = 0.0 + UIView.animate(withDuration: 0.25, animations: { () -> Void in + self.shareDialogController.view.alpha = 1.0 + }, completion: nil) + } + + func dismissShareDialog() { + shareDialogController.willMove(toParentViewController: nil) + shareDialogController.view.removeFromSuperview() + shareDialogController.removeFromParentViewController() + } +} diff --git a/mobile/ios/Extensions/ShareTo/ShareViewController.swift b/mobile/ios/Extensions/ShareTo/ShareViewController.swift new file mode 100644 index 0000000000..a85279b8d8 --- /dev/null +++ b/mobile/ios/Extensions/ShareTo/ShareViewController.swift @@ -0,0 +1,245 @@ +/* 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 UIKit +import Shared +import Storage + +struct ShareDestination { + let code: String + let name: String + let image: String +} + +// TODO: See if we can do this with an Enum instead. Previous attempts failed because for example NSSet does not take (string) enum values. +let ShareDestinationBookmarks: String = "Bookmarks" +let ShareDestinationReadingList: String = "ReadingList" + +let ShareDestinations = [ + ShareDestination(code: ShareDestinationReadingList, name: NSLocalizedString("Add to Reading List", tableName: "ShareTo", comment: "On/off toggle to select adding this url to your reading list"), image: "AddToReadingList"), + ShareDestination(code: ShareDestinationBookmarks, name: NSLocalizedString("Add to Bookmarks", tableName: "ShareTo", comment: "On/off toggle to select adding this url to your bookmarks"), image: "AddToBookmarks") +] + +protocol ShareControllerDelegate { + func shareControllerDidCancel(_ shareController: ShareDialogController) + func shareController(_ shareController: ShareDialogController, didShareItem item: ShareItem, toDestinations destinations: NSSet) +} + +private struct ShareDialogControllerUX { + static let CornerRadius: CGFloat = 4 // Corner radius of the dialog + + static let NavigationBarTintColor = UIColor(rgb: 0xf37c00) // Tint color changes the text color in the navigation bar + static let NavigationBarCancelButtonFont = UIFont.systemFont(ofSize: UIFont.buttonFontSize) // System default + static let NavigationBarAddButtonFont = UIFont.boldSystemFont(ofSize: UIFont.buttonFontSize) // System default + static let NavigationBarIconSize = 40 // Width and height of the icon + static let NavigationBarBottomPadding = 12 + + static let ItemTitleFontMedium = UIFont.systemFont(ofSize: 15, weight: UIFontWeightMedium) + static let ItemTitleFont = UIFont.systemFont(ofSize: 15) + static let ItemTitleMaxNumberOfLines = 2 + static let ItemTitleLeftPadding = 44 + static let ItemTitleRightPadding = 44 + static let ItemTitleBottomPadding = 12 + + static let ItemLinkFont = UIFont.systemFont(ofSize: 12) + static let ItemLinkMaxNumberOfLines = 3 + static let ItemLinkLeftPadding = 44 + static let ItemLinkRightPadding = 44 + static let ItemLinkBottomPadding = 14 + + static let DividerColor = UIColor.lightGray // Divider between the item and the table with destinations + static let DividerHeight = 0.5 + + static let TableRowHeight: CGFloat = 44 // System default + static let TableRowFont = UIFont.systemFont(ofSize: 14) + static let TableRowFontMinScale: CGFloat = 0.8 + static let TableRowTintColor = UIColor(red: 0.427, green: 0.800, blue: 0.102, alpha: 1.0) // Green tint for the checkmark + static let TableRowTextColor = UIColor(rgb: 0x555555) + + static let TableHeight = 88 // Height of 2 standard 44px cells +} + +class ShareDialogController: UIViewController, UITableViewDataSource, UITableViewDelegate { + var delegate: ShareControllerDelegate! + var item: ShareItem! + var initialShareDestinations: NSSet = NSSet(object: ShareDestinationBookmarks) + + var selectedShareDestinations: NSMutableSet = NSMutableSet() + var navBar: UINavigationBar! + var navItem: UINavigationItem! + + override func viewDidLoad() { + super.viewDidLoad() + + selectedShareDestinations = NSMutableSet(set: initialShareDestinations) + + self.view.backgroundColor = UIColor.white + self.view.layer.cornerRadius = ShareDialogControllerUX.CornerRadius + self.view.clipsToBounds = true + + // Setup the NavigationBar + + navBar = UINavigationBar() + navBar.translatesAutoresizingMaskIntoConstraints = false + navBar.tintColor = ShareDialogControllerUX.NavigationBarTintColor + navBar.isTranslucent = false + self.view.addSubview(navBar) + + // Setup the NavigationItem + + navItem = UINavigationItem() + navItem.leftBarButtonItem = UIBarButtonItem( + title: Strings.ShareToCancelButton, + style: .plain, + target: self, + action: #selector(ShareDialogController.cancel) + ) + navItem.leftBarButtonItem?.setTitleTextAttributes([NSFontAttributeName: ShareDialogControllerUX.NavigationBarCancelButtonFont], for: UIControlState()) + navItem.leftBarButtonItem?.accessibilityIdentifier = "ShareDialogController.navigationItem.leftBarButtonItem" + + navItem.rightBarButtonItem = UIBarButtonItem(title: NSLocalizedString("Add", tableName: "ShareTo", comment: "Add button in the share dialog"), style: UIBarButtonItemStyle.done, target: self, action: #selector(ShareDialogController.add)) + navItem.rightBarButtonItem?.setTitleTextAttributes([NSFontAttributeName: ShareDialogControllerUX.NavigationBarAddButtonFont], for: UIControlState()) + + let logo = UIImageView(image: UIImage(named: "Icon-Small")) + logo.contentMode = UIViewContentMode.scaleAspectFit // TODO Can go away if icon is provided in correct size + navItem.titleView = logo + + navBar.pushItem(navItem, animated: false) + + // Setup the title view + + let titleView = UILabel() + titleView.translatesAutoresizingMaskIntoConstraints = false + titleView.numberOfLines = ShareDialogControllerUX.ItemTitleMaxNumberOfLines + titleView.lineBreakMode = NSLineBreakMode.byTruncatingTail + titleView.text = item.title + titleView.font = ShareDialogControllerUX.ItemTitleFontMedium + view.addSubview(titleView) + + // Setup the link view + + let linkView = UILabel() + linkView.translatesAutoresizingMaskIntoConstraints = false + linkView.numberOfLines = ShareDialogControllerUX.ItemLinkMaxNumberOfLines + linkView.lineBreakMode = NSLineBreakMode.byTruncatingTail + linkView.text = item.url + linkView.font = ShareDialogControllerUX.ItemLinkFont + view.addSubview(linkView) + + // Setup the icon + + let iconView = UIImageView() + iconView.translatesAutoresizingMaskIntoConstraints = false + iconView.image = UIImage(named: "defaultFavicon") + view.addSubview(iconView) + + // Setup the divider + + let dividerView = UIView() + dividerView.translatesAutoresizingMaskIntoConstraints = false + dividerView.backgroundColor = ShareDialogControllerUX.DividerColor + view.addSubview(dividerView) + + // Setup the table with destinations + + let tableView = UITableView() + tableView.translatesAutoresizingMaskIntoConstraints = false + tableView.separatorInset = UIEdgeInsets.zero + tableView.layoutMargins = UIEdgeInsets.zero + tableView.isUserInteractionEnabled = true + tableView.delegate = self + tableView.allowsSelection = true + tableView.dataSource = self + tableView.isScrollEnabled = false + view.addSubview(tableView) + + // Setup constraints + + let views = [ + "nav": navBar, + "title": titleView, + "link": linkView, + "icon": iconView, + "divider": dividerView, + "table": tableView + ] + + // TODO See Bug 1102516 - Use Snappy to define share extension layout constraints + + let constraints = [ + "H:|[nav]|", + "V:|[nav]", + + "H:|-\(ShareDialogControllerUX.ItemTitleLeftPadding)-[title]-\(ShareDialogControllerUX.ItemTitleRightPadding)-|", + "V:[nav]-\(ShareDialogControllerUX.NavigationBarBottomPadding)-[title]", + + "H:|-\(ShareDialogControllerUX.ItemLinkLeftPadding)-[link]-\(ShareDialogControllerUX.ItemLinkLeftPadding)-|", + "V:[title]-\(ShareDialogControllerUX.ItemTitleBottomPadding)-[link]", + + "H:|[divider]|", + "V:[divider(\(ShareDialogControllerUX.DividerHeight))]", + "V:[link]-\(ShareDialogControllerUX.ItemLinkBottomPadding)-[divider]", + + "H:|[table]|", + "V:[divider][table]", + "V:[table(\(ShareDialogControllerUX.TableHeight))]|" + ] + + for constraint in constraints { + view.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: constraint, options: NSLayoutFormatOptions(), metrics: nil, views: views)) + } + } + + // UITabBarItem Actions that map to our delegate methods + + func cancel() { + delegate?.shareControllerDidCancel(self) + } + + func add() { + delegate?.shareController(self, didShareItem: item, toDestinations: NSSet(set: selectedShareDestinations)) + } + + // UITableView Delegate and DataSource + + func numberOfSections(in tableView: UITableView) -> Int { + return 1 + } + + func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return ShareDestinations.count + } + + func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { + return ShareDialogControllerUX.TableRowHeight + } + + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let cell = UITableViewCell() + cell.textLabel?.textColor = UIAccessibilityDarkerSystemColorsEnabled() ? UIColor.darkGray : ShareDialogControllerUX.TableRowTextColor + cell.textLabel?.font = ShareDialogControllerUX.TableRowFont + cell.accessoryType = selectedShareDestinations.contains(ShareDestinations[indexPath.row].code) ? UITableViewCellAccessoryType.checkmark : UITableViewCellAccessoryType.none + cell.tintColor = ShareDialogControllerUX.TableRowTintColor + cell.layoutMargins = UIEdgeInsets.zero + cell.textLabel?.text = ShareDestinations[indexPath.row].name + cell.textLabel?.adjustsFontSizeToFitWidth = true + cell.textLabel?.minimumScaleFactor = ShareDialogControllerUX.TableRowFontMinScale + cell.imageView?.image = UIImage(named: ShareDestinations[indexPath.row].image) + return cell + } + + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + tableView.deselectRow(at: indexPath, animated: false) + + let code = ShareDestinations[indexPath.row].code + if selectedShareDestinations.contains(code) { + selectedShareDestinations.remove(code) + } else { + selectedShareDestinations.add(code) + } + tableView.reloadRows(at: [indexPath], with: UITableViewRowAnimation.automatic) + + navItem.rightBarButtonItem?.isEnabled = (selectedShareDestinations.count != 0) + } +} diff --git a/mobile/ios/Extensions/Today/Images.xcassets/copy_link_icon.imageset/Contents.json b/mobile/ios/Extensions/Today/Images.xcassets/copy_link_icon.imageset/Contents.json new file mode 100644 index 0000000000..2755067221 --- /dev/null +++ b/mobile/ios/Extensions/Today/Images.xcassets/copy_link_icon.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "todayCopiedLink.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "todayCopiedLink@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "todayCopiedLink@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/mobile/ios/Extensions/Today/Images.xcassets/copy_link_icon.imageset/todayCopiedLink.png b/mobile/ios/Extensions/Today/Images.xcassets/copy_link_icon.imageset/todayCopiedLink.png new file mode 100644 index 0000000000..ec961e3652 Binary files /dev/null and b/mobile/ios/Extensions/Today/Images.xcassets/copy_link_icon.imageset/todayCopiedLink.png differ diff --git a/mobile/ios/Extensions/Today/Images.xcassets/copy_link_icon.imageset/todayCopiedLink@2x.png b/mobile/ios/Extensions/Today/Images.xcassets/copy_link_icon.imageset/todayCopiedLink@2x.png new file mode 100644 index 0000000000..6aaba4e712 Binary files /dev/null and b/mobile/ios/Extensions/Today/Images.xcassets/copy_link_icon.imageset/todayCopiedLink@2x.png differ diff --git a/mobile/ios/Extensions/Today/Images.xcassets/copy_link_icon.imageset/todayCopiedLink@3x.png b/mobile/ios/Extensions/Today/Images.xcassets/copy_link_icon.imageset/todayCopiedLink@3x.png new file mode 100644 index 0000000000..2ee48ceea6 Binary files /dev/null and b/mobile/ios/Extensions/Today/Images.xcassets/copy_link_icon.imageset/todayCopiedLink@3x.png differ diff --git a/mobile/ios/Extensions/Today/Images.xcassets/new_private_tab_button_highlight.imageset/Contents.json b/mobile/ios/Extensions/Today/Images.xcassets/new_private_tab_button_highlight.imageset/Contents.json new file mode 100644 index 0000000000..1577937cd2 --- /dev/null +++ b/mobile/ios/Extensions/Today/Images.xcassets/new_private_tab_button_highlight.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "newPrivateTabHighlight.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "newPrivateTabHighlight@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "newPrivateTabHighlight@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/mobile/ios/Extensions/Today/Images.xcassets/new_private_tab_button_highlight.imageset/newPrivateTabHighlight.png b/mobile/ios/Extensions/Today/Images.xcassets/new_private_tab_button_highlight.imageset/newPrivateTabHighlight.png new file mode 100644 index 0000000000..bda053c60e Binary files /dev/null and b/mobile/ios/Extensions/Today/Images.xcassets/new_private_tab_button_highlight.imageset/newPrivateTabHighlight.png differ diff --git a/mobile/ios/Extensions/Today/Images.xcassets/new_private_tab_button_highlight.imageset/newPrivateTabHighlight@2x.png b/mobile/ios/Extensions/Today/Images.xcassets/new_private_tab_button_highlight.imageset/newPrivateTabHighlight@2x.png new file mode 100644 index 0000000000..b70eacb526 Binary files /dev/null and b/mobile/ios/Extensions/Today/Images.xcassets/new_private_tab_button_highlight.imageset/newPrivateTabHighlight@2x.png differ diff --git a/mobile/ios/Extensions/Today/Images.xcassets/new_private_tab_button_highlight.imageset/newPrivateTabHighlight@3x.png b/mobile/ios/Extensions/Today/Images.xcassets/new_private_tab_button_highlight.imageset/newPrivateTabHighlight@3x.png new file mode 100644 index 0000000000..43f6e13fe4 Binary files /dev/null and b/mobile/ios/Extensions/Today/Images.xcassets/new_private_tab_button_highlight.imageset/newPrivateTabHighlight@3x.png differ diff --git a/mobile/ios/Extensions/Today/Images.xcassets/new_private_tab_button_normal.imageset/Contents.json b/mobile/ios/Extensions/Today/Images.xcassets/new_private_tab_button_normal.imageset/Contents.json new file mode 100644 index 0000000000..3e26c70e29 --- /dev/null +++ b/mobile/ios/Extensions/Today/Images.xcassets/new_private_tab_button_normal.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "newPrivateTab.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "newPrivateTab@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "newPrivateTab@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/mobile/ios/Extensions/Today/Images.xcassets/new_private_tab_button_normal.imageset/newPrivateTab.png b/mobile/ios/Extensions/Today/Images.xcassets/new_private_tab_button_normal.imageset/newPrivateTab.png new file mode 100644 index 0000000000..319f9763b0 Binary files /dev/null and b/mobile/ios/Extensions/Today/Images.xcassets/new_private_tab_button_normal.imageset/newPrivateTab.png differ diff --git a/mobile/ios/Extensions/Today/Images.xcassets/new_private_tab_button_normal.imageset/newPrivateTab@2x.png b/mobile/ios/Extensions/Today/Images.xcassets/new_private_tab_button_normal.imageset/newPrivateTab@2x.png new file mode 100644 index 0000000000..87d4bdbe1e Binary files /dev/null and b/mobile/ios/Extensions/Today/Images.xcassets/new_private_tab_button_normal.imageset/newPrivateTab@2x.png differ diff --git a/mobile/ios/Extensions/Today/Images.xcassets/new_private_tab_button_normal.imageset/newPrivateTab@3x.png b/mobile/ios/Extensions/Today/Images.xcassets/new_private_tab_button_normal.imageset/newPrivateTab@3x.png new file mode 100644 index 0000000000..ec50af0649 Binary files /dev/null and b/mobile/ios/Extensions/Today/Images.xcassets/new_private_tab_button_normal.imageset/newPrivateTab@3x.png differ diff --git a/mobile/ios/Extensions/Today/Images.xcassets/new_tab_button_highlight.imageset/Contents.json b/mobile/ios/Extensions/Today/Images.xcassets/new_tab_button_highlight.imageset/Contents.json new file mode 100644 index 0000000000..11bff47496 --- /dev/null +++ b/mobile/ios/Extensions/Today/Images.xcassets/new_tab_button_highlight.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "newTabHighlight.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "newTabHighlight@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "newTabHighlight@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/mobile/ios/Extensions/Today/Images.xcassets/new_tab_button_highlight.imageset/newTabHighlight.png b/mobile/ios/Extensions/Today/Images.xcassets/new_tab_button_highlight.imageset/newTabHighlight.png new file mode 100644 index 0000000000..1098973f17 Binary files /dev/null and b/mobile/ios/Extensions/Today/Images.xcassets/new_tab_button_highlight.imageset/newTabHighlight.png differ diff --git a/mobile/ios/Extensions/Today/Images.xcassets/new_tab_button_highlight.imageset/newTabHighlight@2x.png b/mobile/ios/Extensions/Today/Images.xcassets/new_tab_button_highlight.imageset/newTabHighlight@2x.png new file mode 100644 index 0000000000..305fd58797 Binary files /dev/null and b/mobile/ios/Extensions/Today/Images.xcassets/new_tab_button_highlight.imageset/newTabHighlight@2x.png differ diff --git a/mobile/ios/Extensions/Today/Images.xcassets/new_tab_button_highlight.imageset/newTabHighlight@3x.png b/mobile/ios/Extensions/Today/Images.xcassets/new_tab_button_highlight.imageset/newTabHighlight@3x.png new file mode 100644 index 0000000000..3bd47318d9 Binary files /dev/null and b/mobile/ios/Extensions/Today/Images.xcassets/new_tab_button_highlight.imageset/newTabHighlight@3x.png differ diff --git a/mobile/ios/Extensions/Today/Images.xcassets/new_tab_button_normal.imageset/Contents.json b/mobile/ios/Extensions/Today/Images.xcassets/new_tab_button_normal.imageset/Contents.json new file mode 100644 index 0000000000..262e9f87a3 --- /dev/null +++ b/mobile/ios/Extensions/Today/Images.xcassets/new_tab_button_normal.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "newTab.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "newTab@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "newTab@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/mobile/ios/Extensions/Today/Images.xcassets/new_tab_button_normal.imageset/newTab.png b/mobile/ios/Extensions/Today/Images.xcassets/new_tab_button_normal.imageset/newTab.png new file mode 100644 index 0000000000..8030c40f35 Binary files /dev/null and b/mobile/ios/Extensions/Today/Images.xcassets/new_tab_button_normal.imageset/newTab.png differ diff --git a/mobile/ios/Extensions/Today/Images.xcassets/new_tab_button_normal.imageset/newTab@2x.png b/mobile/ios/Extensions/Today/Images.xcassets/new_tab_button_normal.imageset/newTab@2x.png new file mode 100644 index 0000000000..250e4351ed Binary files /dev/null and b/mobile/ios/Extensions/Today/Images.xcassets/new_tab_button_normal.imageset/newTab@2x.png differ diff --git a/mobile/ios/Extensions/Today/Images.xcassets/new_tab_button_normal.imageset/newTab@3x.png b/mobile/ios/Extensions/Today/Images.xcassets/new_tab_button_normal.imageset/newTab@3x.png new file mode 100644 index 0000000000..c2d22b0601 Binary files /dev/null and b/mobile/ios/Extensions/Today/Images.xcassets/new_tab_button_normal.imageset/newTab@3x.png differ diff --git a/mobile/ios/Extensions/Today/Info.plist b/mobile/ios/Extensions/Today/Info.plist new file mode 100644 index 0000000000..a2d6af53a6 --- /dev/null +++ b/mobile/ios/Extensions/Today/Info.plist @@ -0,0 +1,37 @@ + + + + + MozDevelopmentTeam + $(DEVELOPMENT_TEAM) + CFBundleDevelopmentRegion + en + CFBundleDisplayName + $(MOZ_BUNDLE_DISPLAY_NAME) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + XPC! + CFBundleShortVersionString + 10.6 + CFBundleSignature + ???? + CFBundleVersion + 1 + NSExtension + + NSExtensionPointIdentifier + com.apple.widget-extension + NSExtensionPrincipalClass + TodayViewController + + MozInternalURLScheme + $(MOZ_INTERNAL_URL_SCHEME) + + diff --git a/mobile/ios/Extensions/Today/TodayViewController.swift b/mobile/ios/Extensions/Today/TodayViewController.swift new file mode 100644 index 0000000000..28c596e0ff --- /dev/null +++ b/mobile/ios/Extensions/Today/TodayViewController.swift @@ -0,0 +1,298 @@ +/* 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 UIKit +import NotificationCenter +import Shared +import SnapKit +import XCGLogger + +private let log = Logger.browserLogger + +struct TodayStrings { + static let NewPrivateTabButtonLabel = NSLocalizedString("TodayWidget.NewPrivateTabButtonLabel", tableName: "Today", value: "New Private Tab", comment: "New Private Tab button label") + static let NewTabButtonLabel = NSLocalizedString("TodayWidget.NewTabButtonLabel", tableName: "Today", value: "New Tab", comment: "New Tab button label") + static let GoToCopiedLinkLabel = NSLocalizedString("TodayWidget.GoToCopiedLinkLabel", tableName: "Today", value: "Go to copied link", comment: "Go to link on clipboard") +} + +struct TodayUX { + static let privateBrowsingColor = UIColor(rgb: 0xCE6EFC) + static let backgroundHightlightColor = UIColor(white: 216.0/255.0, alpha: 44.0/255.0) + static let linkTextSize: CGFloat = 10.0 + static let labelTextSize: CGFloat = 14.0 + static let imageButtonTextSize: CGFloat = 14.0 + static let copyLinkImageWidth: CGFloat = 23 + static let margin: CGFloat = 8 + static let buttonsHorizontalMarginPercentage: CGFloat = 0.1 + static let iOS9LeftMargin: CGFloat = 40 +} + +@objc (TodayViewController) +class TodayViewController: UIViewController, NCWidgetProviding { + + var copiedURL: URL? + + fileprivate lazy var newTabButton: ImageButtonWithLabel = { + let imageButton = ImageButtonWithLabel() + imageButton.addTarget(self, action: #selector(onPressNewTab), forControlEvents: .touchUpInside) + imageButton.label.text = TodayStrings.NewTabButtonLabel + + let button = imageButton.button + + button.setImage(UIImage(named: "new_tab_button_normal")?.withRenderingMode(.alwaysTemplate), for: .normal) + button.setImage(UIImage(named: "new_tab_button_highlight")?.withRenderingMode(.alwaysTemplate), for: .highlighted) + + let label = imageButton.label + label.font = UIFont.systemFont(ofSize: TodayUX.imageButtonTextSize) + + imageButton.sizeToFit() + return imageButton + }() + + fileprivate lazy var newPrivateTabButton: ImageButtonWithLabel = { + let imageButton = ImageButtonWithLabel() + imageButton.addTarget(self, action: #selector(onPressNewPrivateTab), forControlEvents: .touchUpInside) + imageButton.label.text = TodayStrings.NewPrivateTabButtonLabel + + let button = imageButton.button + button.setImage(UIImage(named: "new_private_tab_button_normal"), for: .normal) + button.setImage(UIImage(named: "new_private_tab_button_highlight"), for: .highlighted) + + let label = imageButton.label + label.tintColor = TodayUX.privateBrowsingColor + label.textColor = TodayUX.privateBrowsingColor + label.font = UIFont.systemFont(ofSize: TodayUX.imageButtonTextSize) + imageButton.sizeToFit() + return imageButton + }() + + fileprivate lazy var openCopiedLinkButton: ButtonWithSublabel = { + let button = ButtonWithSublabel() + + button.setTitle(TodayStrings.GoToCopiedLinkLabel, for: .normal) + button.addTarget(self, action: #selector(onPressOpenClibpoard), for: .touchUpInside) + + // We need to set the background image/color for .Normal, so the whole button is tappable. + button.setBackgroundColor(UIColor.clear, forState: .normal) + button.setBackgroundColor(TodayUX.backgroundHightlightColor, forState: .highlighted) + + button.setImage(UIImage(named: "copy_link_icon")?.withRenderingMode(.alwaysTemplate), for: .normal) + + button.label.font = UIFont.systemFont(ofSize: TodayUX.labelTextSize) + button.subtitleLabel.font = UIFont.systemFont(ofSize: TodayUX.linkTextSize) + return button + }() + + fileprivate lazy var widgetStackView: UIStackView = { + let stackView = UIStackView() + stackView.axis = .vertical + stackView.alignment = .fill + stackView.spacing = TodayUX.margin / 2 + stackView.distribution = UIStackViewDistribution.fill + stackView.layoutMargins = UIEdgeInsets(top: TodayUX.margin, left: TodayUX.margin, bottom: TodayUX.margin, right: TodayUX.margin) + stackView.isLayoutMarginsRelativeArrangement = true + return stackView + }() + + fileprivate lazy var buttonStackView: UIStackView = { + let stackView = UIStackView() + stackView.axis = .horizontal + stackView.alignment = .fill + stackView.spacing = 0 + stackView.distribution = UIStackViewDistribution.fillEqually + let edge = self.view.frame.size.width * TodayUX.buttonsHorizontalMarginPercentage + stackView.layoutMargins = UIEdgeInsets(top: 0, left: edge, bottom: 0, right: edge) + stackView.isLayoutMarginsRelativeArrangement = true + return stackView + }() + + fileprivate var scheme: String { + guard let string = Bundle.main.object(forInfoDictionaryKey: "MozInternalURLScheme") as? String else { + // Something went wrong/weird, but we should fallback to the public one. + return "firefox" + } + return string + } + + override func viewDidLoad() { + super.viewDidLoad() + + let widgetView: UIView! + self.extensionContext?.widgetLargestAvailableDisplayMode = .compact + let effectView = UIVisualEffectView(effect: UIVibrancyEffect.widgetPrimary()) + self.view.addSubview(effectView) + effectView.snp.makeConstraints { make in + make.edges.equalTo(self.view) + } + widgetView = effectView.contentView + buttonStackView.addArrangedSubview(newTabButton) + buttonStackView.addArrangedSubview(newPrivateTabButton) + + widgetStackView.addArrangedSubview(buttonStackView) + widgetStackView.addArrangedSubview(openCopiedLinkButton) + + widgetView.addSubview(widgetStackView) + widgetStackView.snp.makeConstraints { make in + make.edges.equalTo(widgetView) + } + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + updateCopiedLink() + } + + override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { + let edge = size.width * TodayUX.buttonsHorizontalMarginPercentage + buttonStackView.layoutMargins = UIEdgeInsets(top: 0, left: edge, bottom: 0, right: edge) + } + + func widgetMarginInsets(forProposedMarginInsets defaultMarginInsets: UIEdgeInsets) -> UIEdgeInsets { + return UIEdgeInsets.zero + } + + func updateCopiedLink() { + UIPasteboard.general.asyncURL().uponQueue(.main) { res in + if let copiedURL: URL? = res.successValue, + let url = copiedURL { + self.openCopiedLinkButton.isHidden = false + self.openCopiedLinkButton.subtitleLabel.isHidden = SystemUtils.isDeviceLocked() + self.openCopiedLinkButton.subtitleLabel.text = url.absoluteDisplayString + self.copiedURL = url + } else { + self.openCopiedLinkButton.isHidden = true + self.copiedURL = nil + } + } + } + + // MARK: Button behaviour + @objc func onPressNewTab(_ view: UIView) { + openContainingApp() + } + + @objc func onPressNewPrivateTab(_ view: UIView) { + openContainingApp("?private=true") + } + + fileprivate func openContainingApp(_ urlSuffix: String = "") { + let urlString = "\(scheme)://open-url\(urlSuffix)" + self.extensionContext?.open(URL(string: urlString)!) { success in + log.info("Extension opened containing app: \(success)") + } + } + + @objc func onPressOpenClibpoard(_ view: UIView) { + if let url = copiedURL, + let encodedString = url.absoluteString.escape() { + openContainingApp("?url=\(encodedString)") + } + } +} + +extension UIButton { + func setBackgroundColor(_ color: UIColor, forState state: UIControlState) { + let colorView = UIView(frame: CGRect(x: 0, y: 0, width: 1, height: 1)) + colorView.backgroundColor = color + + UIGraphicsBeginImageContext(colorView.bounds.size) + if let context = UIGraphicsGetCurrentContext() { + colorView.layer.render(in: context) + } + let colorImage = UIGraphicsGetImageFromCurrentImageContext() + UIGraphicsEndImageContext() + + self.setBackgroundImage(colorImage, for: state) + } +} + +class ImageButtonWithLabel: UIView { + + lazy var button = UIButton() + lazy var label = UILabel() + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override init(frame: CGRect) { + super.init(frame: frame) + performLayout() + } + + func performLayout() { + addSubview(button) + addSubview(label) + + button.snp.makeConstraints { make in + make.top.left.centerX.equalTo(self) + } + + label.snp.makeConstraints { make in + make.top.equalTo(button.snp.bottom) + make.leading.trailing.bottom.equalTo(self) + } + + label.numberOfLines = 1 + label.lineBreakMode = .byWordWrapping + label.textAlignment = .center + label.textColor = UIColor.white + } + + func addTarget(_ target: AnyObject?, action: Selector, forControlEvents events: UIControlEvents) { + button.addTarget(target, action: action, for: events) + } +} + +class ButtonWithSublabel: UIButton { + lazy var subtitleLabel: UILabel = UILabel() + lazy var label: UILabel = UILabel() + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + convenience init() { + self.init(frame: CGRect.zero) + } + + override init(frame: CGRect) { + super.init(frame: frame) + performLayout() + } + + fileprivate func performLayout() { + let titleLabel = self.label + + self.titleLabel?.removeFromSuperview() + addSubview(titleLabel) + + let imageView = self.imageView! + let subtitleLabel = self.subtitleLabel + subtitleLabel.textColor = UIColor.lightGray + self.addSubview(subtitleLabel) + + imageView.snp.makeConstraints { make in + make.centerY.left.equalTo(self) + make.width.equalTo(TodayUX.copyLinkImageWidth) + } + + titleLabel.snp.makeConstraints { make in + make.left.equalTo(imageView.snp.right).offset(TodayUX.margin) + make.trailing.top.equalTo(self) + } + + subtitleLabel.lineBreakMode = .byTruncatingTail + subtitleLabel.snp.makeConstraints { make in + make.bottom.equalTo(self) + make.top.equalTo(titleLabel.snp.bottom) + make.leading.trailing.equalTo(titleLabel) + } + } + + override func setTitle(_ text: String?, for state: UIControlState) { + self.label.text = text + super.setTitle(text, for: state) + } +} diff --git a/mobile/ios/Extensions/ViewLater/ActionRequestHandler.swift b/mobile/ios/Extensions/ViewLater/ActionRequestHandler.swift new file mode 100644 index 0000000000..b2525a73f0 --- /dev/null +++ b/mobile/ios/Extensions/ViewLater/ActionRequestHandler.swift @@ -0,0 +1,24 @@ +/* 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 UIKit +import MobileCoreServices + +import Shared +import Storage + +class ActionRequestHandler: NSObject, NSExtensionRequestHandling { + + public func beginRequest(with context: NSExtensionContext) { + ExtensionUtils.extractSharedItemFromExtensionContext(context, completionHandler: { (item, error) -> Void in + if let item = item, error == nil && item.isShareable { + let profile = BrowserProfile(localName: "profile") + profile.queue.addToQueue(item).uponQueue(DispatchQueue.main) { _ in + profile.shutdown() + context.completeRequest(returningItems: [], completionHandler: nil) + } + } + }) + } +} diff --git a/mobile/ios/Extensions/ViewLater/Base.lproj/InfoPlist.strings b/mobile/ios/Extensions/ViewLater/Base.lproj/InfoPlist.strings new file mode 100644 index 0000000000..e9bf97e73d --- /dev/null +++ b/mobile/ios/Extensions/ViewLater/Base.lproj/InfoPlist.strings @@ -0,0 +1,5 @@ +/* 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/. */ + +"CFBundleDisplayName" = "View Later"; diff --git a/mobile/ios/Extensions/ViewLater/Info.plist b/mobile/ios/Extensions/ViewLater/Info.plist new file mode 100644 index 0000000000..9025057d0b --- /dev/null +++ b/mobile/ios/Extensions/ViewLater/Info.plist @@ -0,0 +1,44 @@ + + + + + MozDevelopmentTeam + $(DEVELOPMENT_TEAM) + CFBundleDevelopmentRegion + en + CFBundleDisplayName + CFBundleDisplayName + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIcons + + CFBundleIcons~ipad + + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + XPC! + CFBundleShortVersionString + 10.6 + CFBundleSignature + ???? + CFBundleVersion + 1 + NSExtension + + NSExtensionAttributes + + NSExtensionActivationRule + SUBQUERY ( extensionItems, $extensionItem, SUBQUERY ( $extensionItem.attachments, $attachment, ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.url").@count == 1 ).@count == 1 + + NSExtensionPointIdentifier + com.apple.services + NSExtensionPrincipalClass + $(PRODUCT_MODULE_NAME).ActionRequestHandler + + + diff --git a/mobile/ios/Extensions/ViewLater/ViewLater.xcassets/AppIcon.appiconset/Contents.json b/mobile/ios/Extensions/ViewLater/ViewLater.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000000..5d140229dc --- /dev/null +++ b/mobile/ios/Extensions/ViewLater/ViewLater.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,197 @@ +{ + "images" : [ + { + "idiom" : "iphone", + "size" : "20x20", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "20x20", + "scale" : "3x" + }, + { + "idiom" : "iphone", + "size" : "29x29", + "scale" : "1x" + }, + { + "idiom" : "iphone", + "size" : "29x29", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "29x29", + "scale" : "3x" + }, + { + "idiom" : "iphone", + "size" : "40x40", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "40x40", + "scale" : "3x" + }, + { + "idiom" : "iphone", + "size" : "57x57", + "scale" : "1x" + }, + { + "idiom" : "iphone", + "size" : "57x57", + "scale" : "2x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "view-60@2x.png", + "scale" : "2x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "view-60@3x.png", + "scale" : "3x" + }, + { + "idiom" : "ipad", + "size" : "20x20", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "20x20", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "29x29", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "29x29", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "40x40", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "40x40", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "50x50", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "50x50", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "72x72", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "72x72", + "scale" : "2x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "view-76.png", + "scale" : "1x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "view-76@2x.png", + "scale" : "2x" + }, + { + "size" : "83.5x83.5", + "idiom" : "ipad", + "filename" : "view-83.5@2x.png", + "scale" : "2x" + }, + { + "idiom" : "ios-marketing", + "size" : "1024x1024", + "scale" : "1x" + }, + { + "size" : "24x24", + "idiom" : "watch", + "scale" : "2x", + "role" : "notificationCenter", + "subtype" : "38mm" + }, + { + "size" : "27.5x27.5", + "idiom" : "watch", + "scale" : "2x", + "role" : "notificationCenter", + "subtype" : "42mm" + }, + { + "size" : "29x29", + "idiom" : "watch", + "role" : "companionSettings", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "watch", + "role" : "companionSettings", + "scale" : "3x" + }, + { + "size" : "40x40", + "idiom" : "watch", + "scale" : "2x", + "role" : "appLauncher", + "subtype" : "38mm" + }, + { + "size" : "44x44", + "idiom" : "watch", + "scale" : "2x", + "role" : "longLook", + "subtype" : "42mm" + }, + { + "size" : "86x86", + "idiom" : "watch", + "scale" : "2x", + "role" : "quickLook", + "subtype" : "38mm" + }, + { + "size" : "98x98", + "idiom" : "watch", + "scale" : "2x", + "role" : "quickLook", + "subtype" : "42mm" + }, + { + "idiom" : "watch-marketing", + "size" : "1024x1024", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/mobile/ios/Extensions/ViewLater/ViewLater.xcassets/AppIcon.appiconset/view-60@2x.png b/mobile/ios/Extensions/ViewLater/ViewLater.xcassets/AppIcon.appiconset/view-60@2x.png new file mode 100644 index 0000000000..85fe65bae9 Binary files /dev/null and b/mobile/ios/Extensions/ViewLater/ViewLater.xcassets/AppIcon.appiconset/view-60@2x.png differ diff --git a/mobile/ios/Extensions/ViewLater/ViewLater.xcassets/AppIcon.appiconset/view-60@3x.png b/mobile/ios/Extensions/ViewLater/ViewLater.xcassets/AppIcon.appiconset/view-60@3x.png new file mode 100644 index 0000000000..4fc008583f Binary files /dev/null and b/mobile/ios/Extensions/ViewLater/ViewLater.xcassets/AppIcon.appiconset/view-60@3x.png differ diff --git a/mobile/ios/Extensions/ViewLater/ViewLater.xcassets/AppIcon.appiconset/view-76.png b/mobile/ios/Extensions/ViewLater/ViewLater.xcassets/AppIcon.appiconset/view-76.png new file mode 100644 index 0000000000..e267ea80ed Binary files /dev/null and b/mobile/ios/Extensions/ViewLater/ViewLater.xcassets/AppIcon.appiconset/view-76.png differ diff --git a/mobile/ios/Extensions/ViewLater/ViewLater.xcassets/AppIcon.appiconset/view-76@2x.png b/mobile/ios/Extensions/ViewLater/ViewLater.xcassets/AppIcon.appiconset/view-76@2x.png new file mode 100644 index 0000000000..62c6303f69 Binary files /dev/null and b/mobile/ios/Extensions/ViewLater/ViewLater.xcassets/AppIcon.appiconset/view-76@2x.png differ diff --git a/mobile/ios/Extensions/ViewLater/ViewLater.xcassets/AppIcon.appiconset/view-83.5@2x.png b/mobile/ios/Extensions/ViewLater/ViewLater.xcassets/AppIcon.appiconset/view-83.5@2x.png new file mode 100644 index 0000000000..4402c23b38 Binary files /dev/null and b/mobile/ios/Extensions/ViewLater/ViewLater.xcassets/AppIcon.appiconset/view-83.5@2x.png differ diff --git a/mobile/ios/Extensions/ViewLater/en.lproj/InfoPlist.strings b/mobile/ios/Extensions/ViewLater/en.lproj/InfoPlist.strings new file mode 100644 index 0000000000..e9bf97e73d --- /dev/null +++ b/mobile/ios/Extensions/ViewLater/en.lproj/InfoPlist.strings @@ -0,0 +1,5 @@ +/* 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/. */ + +"CFBundleDisplayName" = "View Later"; diff --git a/mobile/ios/Fennec_Enterprise_XCUITests.xcscheme b/mobile/ios/Fennec_Enterprise_XCUITests.xcscheme new file mode 100644 index 0000000000..4fa2ff9562 --- /dev/null +++ b/mobile/ios/Fennec_Enterprise_XCUITests.xcscheme @@ -0,0 +1,279 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/mobile/ios/FxA/FxA.xcodeproj/project.pbxproj b/mobile/ios/FxA/FxA.xcodeproj/project.pbxproj new file mode 100644 index 0000000000..fc5307865f --- /dev/null +++ b/mobile/ios/FxA/FxA.xcodeproj/project.pbxproj @@ -0,0 +1,934 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + 0A80BCCE1EC538DD00926C6A /* base64url.c in Sources */ = {isa = PBXBuildFile; fileRef = 0A80BCC81EC538DD00926C6A /* base64url.c */; }; + 0A80BCCF1EC538DD00926C6A /* decrypt.c in Sources */ = {isa = PBXBuildFile; fileRef = 0A80BCC91EC538DD00926C6A /* decrypt.c */; }; + 0A80BCD01EC538DD00926C6A /* encrypt.c in Sources */ = {isa = PBXBuildFile; fileRef = 0A80BCCA1EC538DD00926C6A /* encrypt.c */; }; + 0A80BCD11EC538DD00926C6A /* keys.c in Sources */ = {isa = PBXBuildFile; fileRef = 0A80BCCB1EC538DD00926C6A /* keys.c */; }; + 0A80BCD21EC538DD00926C6A /* params.c in Sources */ = {isa = PBXBuildFile; fileRef = 0A80BCCC1EC538DD00926C6A /* params.c */; }; + 0A80BCD31EC538DD00926C6A /* trailer.c in Sources */ = {isa = PBXBuildFile; fileRef = 0A80BCCD1EC538DD00926C6A /* trailer.c */; }; + 0A80BCD61EC538F500926C6A /* ece in Resources */ = {isa = PBXBuildFile; fileRef = 0A80BCD41EC538F500926C6A /* ece */; }; + 0A80BCD71EC538F500926C6A /* ece.h in Headers */ = {isa = PBXBuildFile; fileRef = 0A80BCD51EC538F500926C6A /* ece.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 28F9520119D0F9FA00DCE892 /* FxA.h in Headers */ = {isa = PBXBuildFile; fileRef = 28F9520019D0F9FA00DCE892 /* FxA.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 28F9520B19D0F9FB00DCE892 /* FxATests.m in Sources */ = {isa = PBXBuildFile; fileRef = 28F9520A19D0F9FB00DCE892 /* FxATests.m */; }; + 2FA17FD219D3D32300F3D844 /* aes.h in Headers */ = {isa = PBXBuildFile; fileRef = 2FA17F8219D3D32300F3D844 /* aes.h */; }; + 2FA17FD319D3D32300F3D844 /* asn1.h in Headers */ = {isa = PBXBuildFile; fileRef = 2FA17F8319D3D32300F3D844 /* asn1.h */; }; + 2FA17FD419D3D32300F3D844 /* asn1_mac.h in Headers */ = {isa = PBXBuildFile; fileRef = 2FA17F8419D3D32300F3D844 /* asn1_mac.h */; }; + 2FA17FD519D3D32300F3D844 /* asn1t.h in Headers */ = {isa = PBXBuildFile; fileRef = 2FA17F8519D3D32300F3D844 /* asn1t.h */; }; + 2FA17FD619D3D32300F3D844 /* bio.h in Headers */ = {isa = PBXBuildFile; fileRef = 2FA17F8619D3D32300F3D844 /* bio.h */; }; + 2FA17FD719D3D32300F3D844 /* blowfish.h in Headers */ = {isa = PBXBuildFile; fileRef = 2FA17F8719D3D32300F3D844 /* blowfish.h */; }; + 2FA17FD819D3D32300F3D844 /* bn.h in Headers */ = {isa = PBXBuildFile; fileRef = 2FA17F8819D3D32300F3D844 /* bn.h */; }; + 2FA17FD919D3D32300F3D844 /* buffer.h in Headers */ = {isa = PBXBuildFile; fileRef = 2FA17F8919D3D32300F3D844 /* buffer.h */; }; + 2FA17FDA19D3D32300F3D844 /* camellia.h in Headers */ = {isa = PBXBuildFile; fileRef = 2FA17F8A19D3D32300F3D844 /* camellia.h */; }; + 2FA17FDB19D3D32300F3D844 /* cast.h in Headers */ = {isa = PBXBuildFile; fileRef = 2FA17F8B19D3D32300F3D844 /* cast.h */; }; + 2FA17FDC19D3D32300F3D844 /* cmac.h in Headers */ = {isa = PBXBuildFile; fileRef = 2FA17F8C19D3D32300F3D844 /* cmac.h */; }; + 2FA17FDD19D3D32300F3D844 /* cms.h in Headers */ = {isa = PBXBuildFile; fileRef = 2FA17F8D19D3D32300F3D844 /* cms.h */; }; + 2FA17FDE19D3D32300F3D844 /* comp.h in Headers */ = {isa = PBXBuildFile; fileRef = 2FA17F8E19D3D32300F3D844 /* comp.h */; }; + 2FA17FDF19D3D32300F3D844 /* conf.h in Headers */ = {isa = PBXBuildFile; fileRef = 2FA17F8F19D3D32300F3D844 /* conf.h */; }; + 2FA17FE019D3D32300F3D844 /* conf_api.h in Headers */ = {isa = PBXBuildFile; fileRef = 2FA17F9019D3D32300F3D844 /* conf_api.h */; }; + 2FA17FE119D3D32300F3D844 /* crypto.h in Headers */ = {isa = PBXBuildFile; fileRef = 2FA17F9119D3D32300F3D844 /* crypto.h */; }; + 2FA17FE219D3D32300F3D844 /* des.h in Headers */ = {isa = PBXBuildFile; fileRef = 2FA17F9219D3D32300F3D844 /* des.h */; }; + 2FA17FE419D3D32300F3D844 /* dh.h in Headers */ = {isa = PBXBuildFile; fileRef = 2FA17F9419D3D32300F3D844 /* dh.h */; }; + 2FA17FE519D3D32300F3D844 /* dsa.h in Headers */ = {isa = PBXBuildFile; fileRef = 2FA17F9519D3D32300F3D844 /* dsa.h */; }; + 2FA17FE719D3D32300F3D844 /* dtls1.h in Headers */ = {isa = PBXBuildFile; fileRef = 2FA17F9719D3D32300F3D844 /* dtls1.h */; }; + 2FA17FE819D3D32300F3D844 /* e_os2.h in Headers */ = {isa = PBXBuildFile; fileRef = 2FA17F9819D3D32300F3D844 /* e_os2.h */; }; + 2FA17FE919D3D32300F3D844 /* ebcdic.h in Headers */ = {isa = PBXBuildFile; fileRef = 2FA17F9919D3D32300F3D844 /* ebcdic.h */; }; + 2FA17FEA19D3D32300F3D844 /* ec.h in Headers */ = {isa = PBXBuildFile; fileRef = 2FA17F9A19D3D32300F3D844 /* ec.h */; }; + 2FA17FEB19D3D32300F3D844 /* ecdh.h in Headers */ = {isa = PBXBuildFile; fileRef = 2FA17F9B19D3D32300F3D844 /* ecdh.h */; }; + 2FA17FEC19D3D32300F3D844 /* ecdsa.h in Headers */ = {isa = PBXBuildFile; fileRef = 2FA17F9C19D3D32300F3D844 /* ecdsa.h */; }; + 2FA17FED19D3D32300F3D844 /* engine.h in Headers */ = {isa = PBXBuildFile; fileRef = 2FA17F9D19D3D32300F3D844 /* engine.h */; }; + 2FA17FEE19D3D32300F3D844 /* err.h in Headers */ = {isa = PBXBuildFile; fileRef = 2FA17F9E19D3D32300F3D844 /* err.h */; }; + 2FA17FEF19D3D32300F3D844 /* evp.h in Headers */ = {isa = PBXBuildFile; fileRef = 2FA17F9F19D3D32300F3D844 /* evp.h */; }; + 2FA17FF019D3D32300F3D844 /* hmac.h in Headers */ = {isa = PBXBuildFile; fileRef = 2FA17FA019D3D32300F3D844 /* hmac.h */; }; + 2FA17FF119D3D32300F3D844 /* idea.h in Headers */ = {isa = PBXBuildFile; fileRef = 2FA17FA119D3D32300F3D844 /* idea.h */; }; + 2FA17FF419D3D32300F3D844 /* lhash.h in Headers */ = {isa = PBXBuildFile; fileRef = 2FA17FA419D3D32300F3D844 /* lhash.h */; }; + 2FA17FF519D3D32300F3D844 /* md4.h in Headers */ = {isa = PBXBuildFile; fileRef = 2FA17FA519D3D32300F3D844 /* md4.h */; }; + 2FA17FF619D3D32300F3D844 /* md5.h in Headers */ = {isa = PBXBuildFile; fileRef = 2FA17FA619D3D32300F3D844 /* md5.h */; }; + 2FA17FF719D3D32300F3D844 /* mdc2.h in Headers */ = {isa = PBXBuildFile; fileRef = 2FA17FA719D3D32300F3D844 /* mdc2.h */; }; + 2FA17FF819D3D32300F3D844 /* modes.h in Headers */ = {isa = PBXBuildFile; fileRef = 2FA17FA819D3D32300F3D844 /* modes.h */; }; + 2FA17FF919D3D32300F3D844 /* obj_mac.h in Headers */ = {isa = PBXBuildFile; fileRef = 2FA17FA919D3D32300F3D844 /* obj_mac.h */; }; + 2FA17FFA19D3D32300F3D844 /* objects.h in Headers */ = {isa = PBXBuildFile; fileRef = 2FA17FAA19D3D32300F3D844 /* objects.h */; }; + 2FA17FFB19D3D32300F3D844 /* ocsp.h in Headers */ = {isa = PBXBuildFile; fileRef = 2FA17FAB19D3D32300F3D844 /* ocsp.h */; }; + 2FA17FFC19D3D32300F3D844 /* opensslconf.h in Headers */ = {isa = PBXBuildFile; fileRef = 2FA17FAC19D3D32300F3D844 /* opensslconf.h */; }; + 2FA17FFD19D3D32300F3D844 /* opensslv.h in Headers */ = {isa = PBXBuildFile; fileRef = 2FA17FAD19D3D32300F3D844 /* opensslv.h */; }; + 2FA17FFE19D3D32300F3D844 /* ossl_typ.h in Headers */ = {isa = PBXBuildFile; fileRef = 2FA17FAE19D3D32300F3D844 /* ossl_typ.h */; }; + 2FA17FFF19D3D32300F3D844 /* pem.h in Headers */ = {isa = PBXBuildFile; fileRef = 2FA17FAF19D3D32300F3D844 /* pem.h */; }; + 2FA1800019D3D32300F3D844 /* pem2.h in Headers */ = {isa = PBXBuildFile; fileRef = 2FA17FB019D3D32300F3D844 /* pem2.h */; }; + 2FA1800119D3D32300F3D844 /* pkcs12.h in Headers */ = {isa = PBXBuildFile; fileRef = 2FA17FB119D3D32300F3D844 /* pkcs12.h */; }; + 2FA1800219D3D32300F3D844 /* pkcs7.h in Headers */ = {isa = PBXBuildFile; fileRef = 2FA17FB219D3D32300F3D844 /* pkcs7.h */; }; + 2FA1800419D3D32300F3D844 /* rand.h in Headers */ = {isa = PBXBuildFile; fileRef = 2FA17FB419D3D32300F3D844 /* rand.h */; }; + 2FA1800519D3D32300F3D844 /* rc2.h in Headers */ = {isa = PBXBuildFile; fileRef = 2FA17FB519D3D32300F3D844 /* rc2.h */; }; + 2FA1800619D3D32300F3D844 /* rc4.h in Headers */ = {isa = PBXBuildFile; fileRef = 2FA17FB619D3D32300F3D844 /* rc4.h */; }; + 2FA1800719D3D32300F3D844 /* ripemd.h in Headers */ = {isa = PBXBuildFile; fileRef = 2FA17FB719D3D32300F3D844 /* ripemd.h */; }; + 2FA1800819D3D32300F3D844 /* rsa.h in Headers */ = {isa = PBXBuildFile; fileRef = 2FA17FB819D3D32300F3D844 /* rsa.h */; }; + 2FA1800919D3D32300F3D844 /* safestack.h in Headers */ = {isa = PBXBuildFile; fileRef = 2FA17FB919D3D32300F3D844 /* safestack.h */; }; + 2FA1800A19D3D32300F3D844 /* seed.h in Headers */ = {isa = PBXBuildFile; fileRef = 2FA17FBA19D3D32300F3D844 /* seed.h */; }; + 2FA1800B19D3D32300F3D844 /* sha.h in Headers */ = {isa = PBXBuildFile; fileRef = 2FA17FBB19D3D32300F3D844 /* sha.h */; }; + 2FA1800C19D3D32300F3D844 /* srp.h in Headers */ = {isa = PBXBuildFile; fileRef = 2FA17FBC19D3D32300F3D844 /* srp.h */; }; + 2FA1800D19D3D32300F3D844 /* srtp.h in Headers */ = {isa = PBXBuildFile; fileRef = 2FA17FBD19D3D32300F3D844 /* srtp.h */; }; + 2FA1800E19D3D32300F3D844 /* ssl.h in Headers */ = {isa = PBXBuildFile; fileRef = 2FA17FBE19D3D32300F3D844 /* ssl.h */; }; + 2FA1800F19D3D32300F3D844 /* ssl2.h in Headers */ = {isa = PBXBuildFile; fileRef = 2FA17FBF19D3D32300F3D844 /* ssl2.h */; }; + 2FA1801119D3D32300F3D844 /* ssl3.h in Headers */ = {isa = PBXBuildFile; fileRef = 2FA17FC119D3D32300F3D844 /* ssl3.h */; }; + 2FA1801219D3D32300F3D844 /* stack.h in Headers */ = {isa = PBXBuildFile; fileRef = 2FA17FC219D3D32300F3D844 /* stack.h */; }; + 2FA1801319D3D32300F3D844 /* symhacks.h in Headers */ = {isa = PBXBuildFile; fileRef = 2FA17FC319D3D32300F3D844 /* symhacks.h */; }; + 2FA1801419D3D32300F3D844 /* tls1.h in Headers */ = {isa = PBXBuildFile; fileRef = 2FA17FC419D3D32300F3D844 /* tls1.h */; }; + 2FA1801519D3D32300F3D844 /* ts.h in Headers */ = {isa = PBXBuildFile; fileRef = 2FA17FC519D3D32300F3D844 /* ts.h */; }; + 2FA1801619D3D32300F3D844 /* txt_db.h in Headers */ = {isa = PBXBuildFile; fileRef = 2FA17FC619D3D32300F3D844 /* txt_db.h */; }; + 2FA1801719D3D32300F3D844 /* ui.h in Headers */ = {isa = PBXBuildFile; fileRef = 2FA17FC719D3D32300F3D844 /* ui.h */; }; + 2FA1801919D3D32300F3D844 /* whrlpool.h in Headers */ = {isa = PBXBuildFile; fileRef = 2FA17FC919D3D32300F3D844 /* whrlpool.h */; }; + 2FA1801A19D3D32300F3D844 /* x509.h in Headers */ = {isa = PBXBuildFile; fileRef = 2FA17FCA19D3D32300F3D844 /* x509.h */; }; + 2FA1801B19D3D32300F3D844 /* x509_vfy.h in Headers */ = {isa = PBXBuildFile; fileRef = 2FA17FCB19D3D32300F3D844 /* x509_vfy.h */; }; + 2FA1801C19D3D32300F3D844 /* x509v3.h in Headers */ = {isa = PBXBuildFile; fileRef = 2FA17FCC19D3D32300F3D844 /* x509v3.h */; }; + 2FA1801D19D3D32300F3D844 /* JSONWebTokenUtils.h in Headers */ = {isa = PBXBuildFile; fileRef = 2FA17FCD19D3D32300F3D844 /* JSONWebTokenUtils.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 2FA1801E19D3D32300F3D844 /* JSONWebTokenUtils.m in Sources */ = {isa = PBXBuildFile; fileRef = 2FA17FCE19D3D32300F3D844 /* JSONWebTokenUtils.m */; }; + 2FF824E019D3C6D8003ED08E /* KeyPair.h in Headers */ = {isa = PBXBuildFile; fileRef = 2FF824D419D3C6D8003ED08E /* KeyPair.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 2FF824E119D3C6D8003ED08E /* KeyPair.m in Sources */ = {isa = PBXBuildFile; fileRef = 2FF824D519D3C6D8003ED08E /* KeyPair.m */; }; + 2FF824E219D3C6D8003ED08E /* PrivateKey.h in Headers */ = {isa = PBXBuildFile; fileRef = 2FF824D619D3C6D8003ED08E /* PrivateKey.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 2FF824E319D3C6D8003ED08E /* PrivateKey.m in Sources */ = {isa = PBXBuildFile; fileRef = 2FF824D719D3C6D8003ED08E /* PrivateKey.m */; }; + 2FF824E419D3C6D8003ED08E /* PublicKey.h in Headers */ = {isa = PBXBuildFile; fileRef = 2FF824D819D3C6D8003ED08E /* PublicKey.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 2FF824E519D3C6D8003ED08E /* PublicKey.m in Sources */ = {isa = PBXBuildFile; fileRef = 2FF824D919D3C6D8003ED08E /* PublicKey.m */; }; + 2FF824E619D3C6D8003ED08E /* RSAKeyPair.h in Headers */ = {isa = PBXBuildFile; fileRef = 2FF824DA19D3C6D8003ED08E /* RSAKeyPair.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 2FF824E719D3C6D8003ED08E /* RSAKeyPair.m in Sources */ = {isa = PBXBuildFile; fileRef = 2FF824DB19D3C6D8003ED08E /* RSAKeyPair.m */; }; + 2FF825EB19D3CAFF003ED08E /* ASNUtils.h in Headers */ = {isa = PBXBuildFile; fileRef = 2FF8258519D3CAFF003ED08E /* ASNUtils.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 2FF825EC19D3CAFF003ED08E /* ASNUtils.m in Sources */ = {isa = PBXBuildFile; fileRef = 2FF8258619D3CAFF003ED08E /* ASNUtils.m */; }; + 2FF825ED19D3CAFF003ED08E /* CHMath.h in Headers */ = {isa = PBXBuildFile; fileRef = 2FF8258719D3CAFF003ED08E /* CHMath.h */; }; + 2FF825EE19D3CAFF003ED08E /* CHMutableNumber.h in Headers */ = {isa = PBXBuildFile; fileRef = 2FF8258819D3CAFF003ED08E /* CHMutableNumber.h */; }; + 2FF825EF19D3CAFF003ED08E /* CHMutableNumber.m in Sources */ = {isa = PBXBuildFile; fileRef = 2FF8258919D3CAFF003ED08E /* CHMutableNumber.m */; }; + 2FF825F019D3CAFF003ED08E /* CHNumber_Private.h in Headers */ = {isa = PBXBuildFile; fileRef = 2FF8258A19D3CAFF003ED08E /* CHNumber_Private.h */; }; + 2FF825F119D3CAFF003ED08E /* CHNumber.h in Headers */ = {isa = PBXBuildFile; fileRef = 2FF8258B19D3CAFF003ED08E /* CHNumber.h */; }; + 2FF825F219D3CAFF003ED08E /* CHNumber.m in Sources */ = {isa = PBXBuildFile; fileRef = 2FF8258C19D3CAFF003ED08E /* CHNumber.m */; }; + 2FF8264219D3CAFF003ED08E /* NSData+Base16.h in Headers */ = {isa = PBXBuildFile; fileRef = 2FF825DF19D3CAFF003ED08E /* NSData+Base16.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 2FF8264319D3CAFF003ED08E /* NSData+Base16.m in Sources */ = {isa = PBXBuildFile; fileRef = 2FF825E019D3CAFF003ED08E /* NSData+Base16.m */; }; + 2FF8264419D3CAFF003ED08E /* NSData+Base32.h in Headers */ = {isa = PBXBuildFile; fileRef = 2FF825E119D3CAFF003ED08E /* NSData+Base32.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 2FF8264519D3CAFF003ED08E /* NSData+Base32.m in Sources */ = {isa = PBXBuildFile; fileRef = 2FF825E219D3CAFF003ED08E /* NSData+Base32.m */; }; + 2FF8264619D3CAFF003ED08E /* NSData+KeyDerivation.h in Headers */ = {isa = PBXBuildFile; fileRef = 2FF825E319D3CAFF003ED08E /* NSData+KeyDerivation.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 2FF8264719D3CAFF003ED08E /* NSData+KeyDerivation.m in Sources */ = {isa = PBXBuildFile; fileRef = 2FF825E419D3CAFF003ED08E /* NSData+KeyDerivation.m */; }; + 2FF8264819D3CAFF003ED08E /* NSData+SHA.h in Headers */ = {isa = PBXBuildFile; fileRef = 2FF825E519D3CAFF003ED08E /* NSData+SHA.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 2FF8264919D3CAFF003ED08E /* NSData+SHA.m in Sources */ = {isa = PBXBuildFile; fileRef = 2FF825E619D3CAFF003ED08E /* NSData+SHA.m */; }; + 2FF8264A19D3CAFF003ED08E /* NSData+Utils.h in Headers */ = {isa = PBXBuildFile; fileRef = 2FF825E719D3CAFF003ED08E /* NSData+Utils.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 2FF8264B19D3CAFF003ED08E /* NSData+Utils.m in Sources */ = {isa = PBXBuildFile; fileRef = 2FF825E819D3CAFF003ED08E /* NSData+Utils.m */; }; + 2FF8264C19D3CAFF003ED08E /* NSString+Utils.h in Headers */ = {isa = PBXBuildFile; fileRef = 2FF825E919D3CAFF003ED08E /* NSString+Utils.h */; }; + 2FF8264D19D3CAFF003ED08E /* NSString+Utils.m in Sources */ = {isa = PBXBuildFile; fileRef = 2FF825EA19D3CAFF003ED08E /* NSString+Utils.m */; }; + E63320451EC2052100F2BC80 /* libcrypto.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 2FA17FD019D3D32300F3D844 /* libcrypto.a */; }; + E63320461EC2052100F2BC80 /* libssl.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 2FA17FD119D3D32300F3D844 /* libssl.a */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 28F9521819D0FCA000DCE892 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 28F951F219D0F9FA00DCE892 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 28F951FA19D0F9FA00DCE892; + remoteInfo = FxA; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXFileReference section */ + 0A80BCC81EC538DD00926C6A /* base64url.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = base64url.c; path = ../ThirdParty/ecec/src/base64url.c; sourceTree = ""; }; + 0A80BCC91EC538DD00926C6A /* decrypt.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = decrypt.c; path = ../ThirdParty/ecec/src/decrypt.c; sourceTree = ""; }; + 0A80BCCA1EC538DD00926C6A /* encrypt.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = encrypt.c; path = ../ThirdParty/ecec/src/encrypt.c; sourceTree = ""; }; + 0A80BCCB1EC538DD00926C6A /* keys.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = keys.c; path = ../ThirdParty/ecec/src/keys.c; sourceTree = ""; }; + 0A80BCCC1EC538DD00926C6A /* params.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = params.c; path = ../ThirdParty/ecec/src/params.c; sourceTree = ""; }; + 0A80BCCD1EC538DD00926C6A /* trailer.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = trailer.c; path = ../ThirdParty/ecec/src/trailer.c; sourceTree = ""; }; + 0A80BCD41EC538F500926C6A /* ece */ = {isa = PBXFileReference; lastKnownFileType = folder; name = ece; path = ../ThirdParty/ecec/include/ece; sourceTree = ""; }; + 0A80BCD51EC538F500926C6A /* ece.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = ece.h; path = ../ThirdParty/ecec/include/ece.h; sourceTree = ""; }; + 28F951FB19D0F9FA00DCE892 /* FxA.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = FxA.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 28F951FF19D0F9FA00DCE892 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 28F9520019D0F9FA00DCE892 /* FxA.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FxA.h; sourceTree = ""; }; + 28F9520619D0F9FB00DCE892 /* FxATests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = FxATests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 28F9520919D0F9FB00DCE892 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 28F9520A19D0F9FB00DCE892 /* FxATests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FxATests.m; sourceTree = ""; }; + 2FA17F8219D3D32300F3D844 /* aes.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = aes.h; sourceTree = ""; }; + 2FA17F8319D3D32300F3D844 /* asn1.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = asn1.h; sourceTree = ""; }; + 2FA17F8419D3D32300F3D844 /* asn1_mac.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = asn1_mac.h; sourceTree = ""; }; + 2FA17F8519D3D32300F3D844 /* asn1t.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = asn1t.h; sourceTree = ""; }; + 2FA17F8619D3D32300F3D844 /* bio.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = bio.h; sourceTree = ""; }; + 2FA17F8719D3D32300F3D844 /* blowfish.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = blowfish.h; sourceTree = ""; }; + 2FA17F8819D3D32300F3D844 /* bn.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = bn.h; sourceTree = ""; }; + 2FA17F8919D3D32300F3D844 /* buffer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = buffer.h; sourceTree = ""; }; + 2FA17F8A19D3D32300F3D844 /* camellia.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = camellia.h; sourceTree = ""; }; + 2FA17F8B19D3D32300F3D844 /* cast.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = cast.h; sourceTree = ""; }; + 2FA17F8C19D3D32300F3D844 /* cmac.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = cmac.h; sourceTree = ""; }; + 2FA17F8D19D3D32300F3D844 /* cms.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = cms.h; sourceTree = ""; }; + 2FA17F8E19D3D32300F3D844 /* comp.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = comp.h; sourceTree = ""; }; + 2FA17F8F19D3D32300F3D844 /* conf.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = conf.h; sourceTree = ""; }; + 2FA17F9019D3D32300F3D844 /* conf_api.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = conf_api.h; sourceTree = ""; }; + 2FA17F9119D3D32300F3D844 /* crypto.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = crypto.h; sourceTree = ""; }; + 2FA17F9219D3D32300F3D844 /* des.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = des.h; sourceTree = ""; }; + 2FA17F9419D3D32300F3D844 /* dh.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = dh.h; sourceTree = ""; }; + 2FA17F9519D3D32300F3D844 /* dsa.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = dsa.h; sourceTree = ""; }; + 2FA17F9719D3D32300F3D844 /* dtls1.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = dtls1.h; sourceTree = ""; }; + 2FA17F9819D3D32300F3D844 /* e_os2.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = e_os2.h; sourceTree = ""; }; + 2FA17F9919D3D32300F3D844 /* ebcdic.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ebcdic.h; sourceTree = ""; }; + 2FA17F9A19D3D32300F3D844 /* ec.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ec.h; sourceTree = ""; }; + 2FA17F9B19D3D32300F3D844 /* ecdh.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ecdh.h; sourceTree = ""; }; + 2FA17F9C19D3D32300F3D844 /* ecdsa.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ecdsa.h; sourceTree = ""; }; + 2FA17F9D19D3D32300F3D844 /* engine.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = engine.h; sourceTree = ""; }; + 2FA17F9E19D3D32300F3D844 /* err.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = err.h; sourceTree = ""; }; + 2FA17F9F19D3D32300F3D844 /* evp.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = evp.h; sourceTree = ""; }; + 2FA17FA019D3D32300F3D844 /* hmac.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = hmac.h; sourceTree = ""; }; + 2FA17FA119D3D32300F3D844 /* idea.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = idea.h; sourceTree = ""; }; + 2FA17FA419D3D32300F3D844 /* lhash.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = lhash.h; sourceTree = ""; }; + 2FA17FA519D3D32300F3D844 /* md4.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = md4.h; sourceTree = ""; }; + 2FA17FA619D3D32300F3D844 /* md5.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = md5.h; sourceTree = ""; }; + 2FA17FA719D3D32300F3D844 /* mdc2.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = mdc2.h; sourceTree = ""; }; + 2FA17FA819D3D32300F3D844 /* modes.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = modes.h; sourceTree = ""; }; + 2FA17FA919D3D32300F3D844 /* obj_mac.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = obj_mac.h; sourceTree = ""; }; + 2FA17FAA19D3D32300F3D844 /* objects.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = objects.h; sourceTree = ""; }; + 2FA17FAB19D3D32300F3D844 /* ocsp.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ocsp.h; sourceTree = ""; }; + 2FA17FAC19D3D32300F3D844 /* opensslconf.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = opensslconf.h; sourceTree = ""; }; + 2FA17FAD19D3D32300F3D844 /* opensslv.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = opensslv.h; sourceTree = ""; }; + 2FA17FAE19D3D32300F3D844 /* ossl_typ.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ossl_typ.h; sourceTree = ""; }; + 2FA17FAF19D3D32300F3D844 /* pem.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = pem.h; sourceTree = ""; }; + 2FA17FB019D3D32300F3D844 /* pem2.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = pem2.h; sourceTree = ""; }; + 2FA17FB119D3D32300F3D844 /* pkcs12.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = pkcs12.h; sourceTree = ""; }; + 2FA17FB219D3D32300F3D844 /* pkcs7.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = pkcs7.h; sourceTree = ""; }; + 2FA17FB419D3D32300F3D844 /* rand.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = rand.h; sourceTree = ""; }; + 2FA17FB519D3D32300F3D844 /* rc2.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = rc2.h; sourceTree = ""; }; + 2FA17FB619D3D32300F3D844 /* rc4.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = rc4.h; sourceTree = ""; }; + 2FA17FB719D3D32300F3D844 /* ripemd.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ripemd.h; sourceTree = ""; }; + 2FA17FB819D3D32300F3D844 /* rsa.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = rsa.h; sourceTree = ""; }; + 2FA17FB919D3D32300F3D844 /* safestack.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = safestack.h; sourceTree = ""; }; + 2FA17FBA19D3D32300F3D844 /* seed.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = seed.h; sourceTree = ""; }; + 2FA17FBB19D3D32300F3D844 /* sha.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = sha.h; sourceTree = ""; }; + 2FA17FBC19D3D32300F3D844 /* srp.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = srp.h; sourceTree = ""; }; + 2FA17FBD19D3D32300F3D844 /* srtp.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = srtp.h; sourceTree = ""; }; + 2FA17FBE19D3D32300F3D844 /* ssl.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ssl.h; sourceTree = ""; }; + 2FA17FBF19D3D32300F3D844 /* ssl2.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ssl2.h; sourceTree = ""; }; + 2FA17FC119D3D32300F3D844 /* ssl3.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ssl3.h; sourceTree = ""; }; + 2FA17FC219D3D32300F3D844 /* stack.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = stack.h; sourceTree = ""; }; + 2FA17FC319D3D32300F3D844 /* symhacks.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = symhacks.h; sourceTree = ""; }; + 2FA17FC419D3D32300F3D844 /* tls1.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = tls1.h; sourceTree = ""; }; + 2FA17FC519D3D32300F3D844 /* ts.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ts.h; sourceTree = ""; }; + 2FA17FC619D3D32300F3D844 /* txt_db.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = txt_db.h; sourceTree = ""; }; + 2FA17FC719D3D32300F3D844 /* ui.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ui.h; sourceTree = ""; }; + 2FA17FC919D3D32300F3D844 /* whrlpool.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = whrlpool.h; sourceTree = ""; }; + 2FA17FCA19D3D32300F3D844 /* x509.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = x509.h; sourceTree = ""; }; + 2FA17FCB19D3D32300F3D844 /* x509_vfy.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = x509_vfy.h; sourceTree = ""; }; + 2FA17FCC19D3D32300F3D844 /* x509v3.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = x509v3.h; sourceTree = ""; }; + 2FA17FCD19D3D32300F3D844 /* JSONWebTokenUtils.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = JSONWebTokenUtils.h; sourceTree = ""; }; + 2FA17FCE19D3D32300F3D844 /* JSONWebTokenUtils.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = JSONWebTokenUtils.m; sourceTree = ""; }; + 2FA17FD019D3D32300F3D844 /* libcrypto.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libcrypto.a; sourceTree = ""; }; + 2FA17FD119D3D32300F3D844 /* libssl.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libssl.a; sourceTree = ""; }; + 2FF824D419D3C6D8003ED08E /* KeyPair.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = KeyPair.h; sourceTree = ""; }; + 2FF824D519D3C6D8003ED08E /* KeyPair.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = KeyPair.m; sourceTree = ""; }; + 2FF824D619D3C6D8003ED08E /* PrivateKey.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PrivateKey.h; sourceTree = ""; }; + 2FF824D719D3C6D8003ED08E /* PrivateKey.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PrivateKey.m; sourceTree = ""; }; + 2FF824D819D3C6D8003ED08E /* PublicKey.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PublicKey.h; sourceTree = ""; }; + 2FF824D919D3C6D8003ED08E /* PublicKey.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PublicKey.m; sourceTree = ""; }; + 2FF824DA19D3C6D8003ED08E /* RSAKeyPair.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RSAKeyPair.h; sourceTree = ""; }; + 2FF824DB19D3C6D8003ED08E /* RSAKeyPair.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RSAKeyPair.m; sourceTree = ""; }; + 2FF8258519D3CAFF003ED08E /* ASNUtils.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASNUtils.h; sourceTree = ""; }; + 2FF8258619D3CAFF003ED08E /* ASNUtils.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASNUtils.m; sourceTree = ""; }; + 2FF8258719D3CAFF003ED08E /* CHMath.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CHMath.h; sourceTree = ""; }; + 2FF8258819D3CAFF003ED08E /* CHMutableNumber.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CHMutableNumber.h; sourceTree = ""; }; + 2FF8258919D3CAFF003ED08E /* CHMutableNumber.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CHMutableNumber.m; sourceTree = ""; }; + 2FF8258A19D3CAFF003ED08E /* CHNumber_Private.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CHNumber_Private.h; sourceTree = ""; }; + 2FF8258B19D3CAFF003ED08E /* CHNumber.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CHNumber.h; sourceTree = ""; }; + 2FF8258C19D3CAFF003ED08E /* CHNumber.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CHNumber.m; sourceTree = ""; }; + 2FF825DF19D3CAFF003ED08E /* NSData+Base16.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSData+Base16.h"; sourceTree = ""; }; + 2FF825E019D3CAFF003ED08E /* NSData+Base16.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSData+Base16.m"; sourceTree = ""; }; + 2FF825E119D3CAFF003ED08E /* NSData+Base32.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSData+Base32.h"; sourceTree = ""; }; + 2FF825E219D3CAFF003ED08E /* NSData+Base32.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSData+Base32.m"; sourceTree = ""; }; + 2FF825E319D3CAFF003ED08E /* NSData+KeyDerivation.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSData+KeyDerivation.h"; sourceTree = ""; }; + 2FF825E419D3CAFF003ED08E /* NSData+KeyDerivation.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSData+KeyDerivation.m"; sourceTree = ""; }; + 2FF825E519D3CAFF003ED08E /* NSData+SHA.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSData+SHA.h"; sourceTree = ""; }; + 2FF825E619D3CAFF003ED08E /* NSData+SHA.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSData+SHA.m"; sourceTree = ""; }; + 2FF825E719D3CAFF003ED08E /* NSData+Utils.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSData+Utils.h"; sourceTree = ""; }; + 2FF825E819D3CAFF003ED08E /* NSData+Utils.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSData+Utils.m"; sourceTree = ""; }; + 2FF825E919D3CAFF003ED08E /* NSString+Utils.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSString+Utils.h"; sourceTree = ""; }; + 2FF825EA19D3CAFF003ED08E /* NSString+Utils.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSString+Utils.m"; sourceTree = ""; }; + E6D69D091EC241C100B80600 /* opensslconf_ios_arm64.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = opensslconf_ios_arm64.h; sourceTree = ""; }; + E6D69D0A1EC241C100B80600 /* opensslconf_ios_armv7.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = opensslconf_ios_armv7.h; sourceTree = ""; }; + E6D69D0B1EC241C100B80600 /* opensslconf_ios_armv7s.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = opensslconf_ios_armv7s.h; sourceTree = ""; }; + E6D69D0C1EC241C100B80600 /* opensslconf_ios_i386.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = opensslconf_ios_i386.h; sourceTree = ""; }; + E6D69D0D1EC241C100B80600 /* opensslconf_ios_x86_64.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = opensslconf_ios_x86_64.h; sourceTree = ""; }; + E6D69D0E1EC241C100B80600 /* opensslconf_tvos_arm64.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = opensslconf_tvos_arm64.h; sourceTree = ""; }; + E6D69D0F1EC241C100B80600 /* opensslconf_tvos_x86_64.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = opensslconf_tvos_x86_64.h; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 28F951F719D0F9FA00DCE892 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + E63320451EC2052100F2BC80 /* libcrypto.a in Frameworks */, + E63320461EC2052100F2BC80 /* libssl.a in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 28F9520319D0F9FB00DCE892 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 28F951F119D0F9FA00DCE892 = { + isa = PBXGroup; + children = ( + 0A80BCD41EC538F500926C6A /* ece */, + 0A80BCD51EC538F500926C6A /* ece.h */, + 0A80BCC81EC538DD00926C6A /* base64url.c */, + 0A80BCC91EC538DD00926C6A /* decrypt.c */, + 0A80BCCA1EC538DD00926C6A /* encrypt.c */, + 0A80BCCB1EC538DD00926C6A /* keys.c */, + 0A80BCCC1EC538DD00926C6A /* params.c */, + 0A80BCCD1EC538DD00926C6A /* trailer.c */, + 28F951FD19D0F9FA00DCE892 /* FxA */, + 28F9520719D0F9FB00DCE892 /* FxATests */, + 28F951FC19D0F9FA00DCE892 /* Products */, + ); + sourceTree = ""; + }; + 28F951FC19D0F9FA00DCE892 /* Products */ = { + isa = PBXGroup; + children = ( + 28F951FB19D0F9FA00DCE892 /* FxA.framework */, + 28F9520619D0F9FB00DCE892 /* FxATests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 28F951FD19D0F9FA00DCE892 /* FxA */ = { + isa = PBXGroup; + children = ( + 28F9520019D0F9FA00DCE892 /* FxA.h */, + 2FA17FCD19D3D32300F3D844 /* JSONWebTokenUtils.h */, + 2FA17FCE19D3D32300F3D844 /* JSONWebTokenUtils.m */, + 2FF8258519D3CAFF003ED08E /* ASNUtils.h */, + 2FF8258619D3CAFF003ED08E /* ASNUtils.m */, + 2FF8258719D3CAFF003ED08E /* CHMath.h */, + 2FF8258819D3CAFF003ED08E /* CHMutableNumber.h */, + 2FF8258919D3CAFF003ED08E /* CHMutableNumber.m */, + 2FF8258A19D3CAFF003ED08E /* CHNumber_Private.h */, + 2FF8258B19D3CAFF003ED08E /* CHNumber.h */, + 2FF8258C19D3CAFF003ED08E /* CHNumber.m */, + 2FF825DF19D3CAFF003ED08E /* NSData+Base16.h */, + 2FF825E019D3CAFF003ED08E /* NSData+Base16.m */, + 2FF825E119D3CAFF003ED08E /* NSData+Base32.h */, + 2FF825E219D3CAFF003ED08E /* NSData+Base32.m */, + 2FF825E319D3CAFF003ED08E /* NSData+KeyDerivation.h */, + 2FF825E419D3CAFF003ED08E /* NSData+KeyDerivation.m */, + 2FF825E519D3CAFF003ED08E /* NSData+SHA.h */, + 2FF825E619D3CAFF003ED08E /* NSData+SHA.m */, + 2FF825E719D3CAFF003ED08E /* NSData+Utils.h */, + 2FF825E819D3CAFF003ED08E /* NSData+Utils.m */, + 2FF825E919D3CAFF003ED08E /* NSString+Utils.h */, + 2FF825EA19D3CAFF003ED08E /* NSString+Utils.m */, + 2FF824D419D3C6D8003ED08E /* KeyPair.h */, + 2FF824D519D3C6D8003ED08E /* KeyPair.m */, + 2FF824D619D3C6D8003ED08E /* PrivateKey.h */, + 2FF824D719D3C6D8003ED08E /* PrivateKey.m */, + 2FF824D819D3C6D8003ED08E /* PublicKey.h */, + 2FF824D919D3C6D8003ED08E /* PublicKey.m */, + 2FF824DA19D3C6D8003ED08E /* RSAKeyPair.h */, + 2FF824DB19D3C6D8003ED08E /* RSAKeyPair.m */, + 2FA17F8019D3D32300F3D844 /* include */, + 2FA17FCF19D3D32300F3D844 /* lib */, + 28F951FE19D0F9FA00DCE892 /* Supporting Files */, + ); + path = FxA; + sourceTree = ""; + }; + 28F951FE19D0F9FA00DCE892 /* Supporting Files */ = { + isa = PBXGroup; + children = ( + 28F951FF19D0F9FA00DCE892 /* Info.plist */, + ); + name = "Supporting Files"; + sourceTree = ""; + }; + 28F9520719D0F9FB00DCE892 /* FxATests */ = { + isa = PBXGroup; + children = ( + 28F9520A19D0F9FB00DCE892 /* FxATests.m */, + 28F9520819D0F9FB00DCE892 /* Supporting Files */, + ); + path = FxATests; + sourceTree = ""; + }; + 28F9520819D0F9FB00DCE892 /* Supporting Files */ = { + isa = PBXGroup; + children = ( + 28F9520919D0F9FB00DCE892 /* Info.plist */, + ); + name = "Supporting Files"; + sourceTree = ""; + }; + 2FA17F8019D3D32300F3D844 /* include */ = { + isa = PBXGroup; + children = ( + 2FA17F8119D3D32300F3D844 /* openssl */, + ); + path = include; + sourceTree = ""; + }; + 2FA17F8119D3D32300F3D844 /* openssl */ = { + isa = PBXGroup; + children = ( + E6D69D091EC241C100B80600 /* opensslconf_ios_arm64.h */, + E6D69D0A1EC241C100B80600 /* opensslconf_ios_armv7.h */, + E6D69D0B1EC241C100B80600 /* opensslconf_ios_armv7s.h */, + E6D69D0C1EC241C100B80600 /* opensslconf_ios_i386.h */, + E6D69D0D1EC241C100B80600 /* opensslconf_ios_x86_64.h */, + E6D69D0E1EC241C100B80600 /* opensslconf_tvos_arm64.h */, + E6D69D0F1EC241C100B80600 /* opensslconf_tvos_x86_64.h */, + 2FA17F8219D3D32300F3D844 /* aes.h */, + 2FA17F8319D3D32300F3D844 /* asn1.h */, + 2FA17F8419D3D32300F3D844 /* asn1_mac.h */, + 2FA17F8519D3D32300F3D844 /* asn1t.h */, + 2FA17F8619D3D32300F3D844 /* bio.h */, + 2FA17F8719D3D32300F3D844 /* blowfish.h */, + 2FA17F8819D3D32300F3D844 /* bn.h */, + 2FA17F8919D3D32300F3D844 /* buffer.h */, + 2FA17F8A19D3D32300F3D844 /* camellia.h */, + 2FA17F8B19D3D32300F3D844 /* cast.h */, + 2FA17F8C19D3D32300F3D844 /* cmac.h */, + 2FA17F8D19D3D32300F3D844 /* cms.h */, + 2FA17F8E19D3D32300F3D844 /* comp.h */, + 2FA17F8F19D3D32300F3D844 /* conf.h */, + 2FA17F9019D3D32300F3D844 /* conf_api.h */, + 2FA17F9119D3D32300F3D844 /* crypto.h */, + 2FA17F9219D3D32300F3D844 /* des.h */, + 2FA17F9419D3D32300F3D844 /* dh.h */, + 2FA17F9519D3D32300F3D844 /* dsa.h */, + 2FA17F9719D3D32300F3D844 /* dtls1.h */, + 2FA17F9819D3D32300F3D844 /* e_os2.h */, + 2FA17F9919D3D32300F3D844 /* ebcdic.h */, + 2FA17F9A19D3D32300F3D844 /* ec.h */, + 2FA17F9B19D3D32300F3D844 /* ecdh.h */, + 2FA17F9C19D3D32300F3D844 /* ecdsa.h */, + 2FA17F9D19D3D32300F3D844 /* engine.h */, + 2FA17F9E19D3D32300F3D844 /* err.h */, + 2FA17F9F19D3D32300F3D844 /* evp.h */, + 2FA17FA019D3D32300F3D844 /* hmac.h */, + 2FA17FA119D3D32300F3D844 /* idea.h */, + 2FA17FA419D3D32300F3D844 /* lhash.h */, + 2FA17FA519D3D32300F3D844 /* md4.h */, + 2FA17FA619D3D32300F3D844 /* md5.h */, + 2FA17FA719D3D32300F3D844 /* mdc2.h */, + 2FA17FA819D3D32300F3D844 /* modes.h */, + 2FA17FA919D3D32300F3D844 /* obj_mac.h */, + 2FA17FAA19D3D32300F3D844 /* objects.h */, + 2FA17FAB19D3D32300F3D844 /* ocsp.h */, + 2FA17FAC19D3D32300F3D844 /* opensslconf.h */, + 2FA17FAD19D3D32300F3D844 /* opensslv.h */, + 2FA17FAE19D3D32300F3D844 /* ossl_typ.h */, + 2FA17FAF19D3D32300F3D844 /* pem.h */, + 2FA17FB019D3D32300F3D844 /* pem2.h */, + 2FA17FB119D3D32300F3D844 /* pkcs12.h */, + 2FA17FB219D3D32300F3D844 /* pkcs7.h */, + 2FA17FB419D3D32300F3D844 /* rand.h */, + 2FA17FB519D3D32300F3D844 /* rc2.h */, + 2FA17FB619D3D32300F3D844 /* rc4.h */, + 2FA17FB719D3D32300F3D844 /* ripemd.h */, + 2FA17FB819D3D32300F3D844 /* rsa.h */, + 2FA17FB919D3D32300F3D844 /* safestack.h */, + 2FA17FBA19D3D32300F3D844 /* seed.h */, + 2FA17FBB19D3D32300F3D844 /* sha.h */, + 2FA17FBC19D3D32300F3D844 /* srp.h */, + 2FA17FBD19D3D32300F3D844 /* srtp.h */, + 2FA17FBE19D3D32300F3D844 /* ssl.h */, + 2FA17FBF19D3D32300F3D844 /* ssl2.h */, + 2FA17FC119D3D32300F3D844 /* ssl3.h */, + 2FA17FC219D3D32300F3D844 /* stack.h */, + 2FA17FC319D3D32300F3D844 /* symhacks.h */, + 2FA17FC419D3D32300F3D844 /* tls1.h */, + 2FA17FC519D3D32300F3D844 /* ts.h */, + 2FA17FC619D3D32300F3D844 /* txt_db.h */, + 2FA17FC719D3D32300F3D844 /* ui.h */, + 2FA17FC919D3D32300F3D844 /* whrlpool.h */, + 2FA17FCA19D3D32300F3D844 /* x509.h */, + 2FA17FCB19D3D32300F3D844 /* x509_vfy.h */, + 2FA17FCC19D3D32300F3D844 /* x509v3.h */, + ); + path = openssl; + sourceTree = ""; + }; + 2FA17FCF19D3D32300F3D844 /* lib */ = { + isa = PBXGroup; + children = ( + 2FA17FD019D3D32300F3D844 /* libcrypto.a */, + 2FA17FD119D3D32300F3D844 /* libssl.a */, + ); + path = lib; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXHeadersBuildPhase section */ + 28F951F819D0F9FA00DCE892 /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + 2FF824E219D3C6D8003ED08E /* PrivateKey.h in Headers */, + 2FF824E419D3C6D8003ED08E /* PublicKey.h in Headers */, + 2FF824E619D3C6D8003ED08E /* RSAKeyPair.h in Headers */, + 2FF8264419D3CAFF003ED08E /* NSData+Base32.h in Headers */, + 2FA1801B19D3D32300F3D844 /* x509_vfy.h in Headers */, + 0A80BCD71EC538F500926C6A /* ece.h in Headers */, + 2FA1801419D3D32300F3D844 /* tls1.h in Headers */, + 28F9520119D0F9FA00DCE892 /* FxA.h in Headers */, + 2FA1801D19D3D32300F3D844 /* JSONWebTokenUtils.h in Headers */, + 2FA1801A19D3D32300F3D844 /* x509.h in Headers */, + 2FF8264C19D3CAFF003ED08E /* NSString+Utils.h in Headers */, + 2FA17FE519D3D32300F3D844 /* dsa.h in Headers */, + 2FA1801619D3D32300F3D844 /* txt_db.h in Headers */, + 2FA17FD319D3D32300F3D844 /* asn1.h in Headers */, + 2FA17FFC19D3D32300F3D844 /* opensslconf.h in Headers */, + 2FA17FDA19D3D32300F3D844 /* camellia.h in Headers */, + 2FF825F119D3CAFF003ED08E /* CHNumber.h in Headers */, + 2FF825EE19D3CAFF003ED08E /* CHMutableNumber.h in Headers */, + 2FF825F019D3CAFF003ED08E /* CHNumber_Private.h in Headers */, + 2FF825ED19D3CAFF003ED08E /* CHMath.h in Headers */, + 2FA17FE219D3D32300F3D844 /* des.h in Headers */, + 2FA17FDF19D3D32300F3D844 /* conf.h in Headers */, + 2FA17FED19D3D32300F3D844 /* engine.h in Headers */, + 2FA1801919D3D32300F3D844 /* whrlpool.h in Headers */, + 2FA17FD619D3D32300F3D844 /* bio.h in Headers */, + 2FA17FEA19D3D32300F3D844 /* ec.h in Headers */, + 2FA1800C19D3D32300F3D844 /* srp.h in Headers */, + 2FA17FFD19D3D32300F3D844 /* opensslv.h in Headers */, + 2FA17FEF19D3D32300F3D844 /* evp.h in Headers */, + 2FA17FD219D3D32300F3D844 /* aes.h in Headers */, + 2FA1801319D3D32300F3D844 /* symhacks.h in Headers */, + 2FA1800B19D3D32300F3D844 /* sha.h in Headers */, + 2FA17FE719D3D32300F3D844 /* dtls1.h in Headers */, + 2FA17FD819D3D32300F3D844 /* bn.h in Headers */, + 2FA17FE019D3D32300F3D844 /* conf_api.h in Headers */, + 2FA1801519D3D32300F3D844 /* ts.h in Headers */, + 2FF824E019D3C6D8003ED08E /* KeyPair.h in Headers */, + 2FA17FD419D3D32300F3D844 /* asn1_mac.h in Headers */, + 2FA17FFE19D3D32300F3D844 /* ossl_typ.h in Headers */, + 2FA17FDB19D3D32300F3D844 /* cast.h in Headers */, + 2FA1800219D3D32300F3D844 /* pkcs7.h in Headers */, + 2FA1800019D3D32300F3D844 /* pem2.h in Headers */, + 2FF8264A19D3CAFF003ED08E /* NSData+Utils.h in Headers */, + 2FA1800419D3D32300F3D844 /* rand.h in Headers */, + 2FF8264219D3CAFF003ED08E /* NSData+Base16.h in Headers */, + 2FF825EB19D3CAFF003ED08E /* ASNUtils.h in Headers */, + 2FF8264619D3CAFF003ED08E /* NSData+KeyDerivation.h in Headers */, + 2FA1801719D3D32300F3D844 /* ui.h in Headers */, + 2FA17FF019D3D32300F3D844 /* hmac.h in Headers */, + 2FA17FFB19D3D32300F3D844 /* ocsp.h in Headers */, + 2FF8264819D3CAFF003ED08E /* NSData+SHA.h in Headers */, + 2FA17FD919D3D32300F3D844 /* buffer.h in Headers */, + 2FA1801C19D3D32300F3D844 /* x509v3.h in Headers */, + 2FA17FEB19D3D32300F3D844 /* ecdh.h in Headers */, + 2FA17FDE19D3D32300F3D844 /* comp.h in Headers */, + 2FA17FEE19D3D32300F3D844 /* err.h in Headers */, + 2FA17FEC19D3D32300F3D844 /* ecdsa.h in Headers */, + 2FA1800F19D3D32300F3D844 /* ssl2.h in Headers */, + 2FA1800119D3D32300F3D844 /* pkcs12.h in Headers */, + 2FA1800519D3D32300F3D844 /* rc2.h in Headers */, + 2FA17FE119D3D32300F3D844 /* crypto.h in Headers */, + 2FA17FD719D3D32300F3D844 /* blowfish.h in Headers */, + 2FA1801119D3D32300F3D844 /* ssl3.h in Headers */, + 2FA1801219D3D32300F3D844 /* stack.h in Headers */, + 2FA17FF719D3D32300F3D844 /* mdc2.h in Headers */, + 2FA17FFA19D3D32300F3D844 /* objects.h in Headers */, + 2FA17FE819D3D32300F3D844 /* e_os2.h in Headers */, + 2FA17FE419D3D32300F3D844 /* dh.h in Headers */, + 2FA17FDD19D3D32300F3D844 /* cms.h in Headers */, + 2FA1800E19D3D32300F3D844 /* ssl.h in Headers */, + 2FA17FF919D3D32300F3D844 /* obj_mac.h in Headers */, + 2FA17FF519D3D32300F3D844 /* md4.h in Headers */, + 2FA17FF619D3D32300F3D844 /* md5.h in Headers */, + 2FA17FF819D3D32300F3D844 /* modes.h in Headers */, + 2FA17FE919D3D32300F3D844 /* ebcdic.h in Headers */, + 2FA1800719D3D32300F3D844 /* ripemd.h in Headers */, + 2FA17FF419D3D32300F3D844 /* lhash.h in Headers */, + 2FA17FF119D3D32300F3D844 /* idea.h in Headers */, + 2FA1800619D3D32300F3D844 /* rc4.h in Headers */, + 2FA1800A19D3D32300F3D844 /* seed.h in Headers */, + 2FA1800D19D3D32300F3D844 /* srtp.h in Headers */, + 2FA1800819D3D32300F3D844 /* rsa.h in Headers */, + 2FA1800919D3D32300F3D844 /* safestack.h in Headers */, + 2FA17FD519D3D32300F3D844 /* asn1t.h in Headers */, + 2FA17FDC19D3D32300F3D844 /* cmac.h in Headers */, + 2FA17FFF19D3D32300F3D844 /* pem.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXHeadersBuildPhase section */ + +/* Begin PBXNativeTarget section */ + 28F951FA19D0F9FA00DCE892 /* FxA */ = { + isa = PBXNativeTarget; + buildConfigurationList = 28F9520E19D0F9FB00DCE892 /* Build configuration list for PBXNativeTarget "FxA" */; + buildPhases = ( + 28F951F619D0F9FA00DCE892 /* Sources */, + 28F951F719D0F9FA00DCE892 /* Frameworks */, + 28F951F819D0F9FA00DCE892 /* Headers */, + 28F951F919D0F9FA00DCE892 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = FxA; + productName = FxA; + productReference = 28F951FB19D0F9FA00DCE892 /* FxA.framework */; + productType = "com.apple.product-type.framework"; + }; + 28F9520519D0F9FB00DCE892 /* FxATests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 28F9521119D0F9FB00DCE892 /* Build configuration list for PBXNativeTarget "FxATests" */; + buildPhases = ( + 28F9520219D0F9FB00DCE892 /* Sources */, + 28F9520319D0F9FB00DCE892 /* Frameworks */, + 28F9520419D0F9FB00DCE892 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 28F9521919D0FCA000DCE892 /* PBXTargetDependency */, + ); + name = FxATests; + productName = FxATests; + productReference = 28F9520619D0F9FB00DCE892 /* FxATests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 28F951F219D0F9FA00DCE892 /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftMigration = 0700; + LastUpgradeCheck = 0820; + ORGANIZATIONNAME = Mozilla; + TargetAttributes = { + 28F951FA19D0F9FA00DCE892 = { + CreatedOnToolsVersion = 6.0.1; + LastSwiftMigration = 0820; + ProvisioningStyle = Manual; + }; + 28F9520519D0F9FB00DCE892 = { + CreatedOnToolsVersion = 6.0.1; + LastSwiftMigration = 0820; + }; + }; + }; + buildConfigurationList = 28F951F519D0F9FA00DCE892 /* Build configuration list for PBXProject "FxA" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + en, + ); + mainGroup = 28F951F119D0F9FA00DCE892; + productRefGroup = 28F951FC19D0F9FA00DCE892 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 28F951FA19D0F9FA00DCE892 /* FxA */, + 28F9520519D0F9FB00DCE892 /* FxATests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 28F951F919D0F9FA00DCE892 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 0A80BCD61EC538F500926C6A /* ece in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 28F9520419D0F9FB00DCE892 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 28F951F619D0F9FA00DCE892 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 2FF825F219D3CAFF003ED08E /* CHNumber.m in Sources */, + 2FF824E119D3C6D8003ED08E /* KeyPair.m in Sources */, + 2FF8264519D3CAFF003ED08E /* NSData+Base32.m in Sources */, + 2FF824E319D3C6D8003ED08E /* PrivateKey.m in Sources */, + 2FF8264319D3CAFF003ED08E /* NSData+Base16.m in Sources */, + 0A80BCCF1EC538DD00926C6A /* decrypt.c in Sources */, + 2FA1801E19D3D32300F3D844 /* JSONWebTokenUtils.m in Sources */, + 2FF824E719D3C6D8003ED08E /* RSAKeyPair.m in Sources */, + 2FF8264D19D3CAFF003ED08E /* NSString+Utils.m in Sources */, + 0A80BCD21EC538DD00926C6A /* params.c in Sources */, + 0A80BCCE1EC538DD00926C6A /* base64url.c in Sources */, + 2FF825EC19D3CAFF003ED08E /* ASNUtils.m in Sources */, + 2FF8264719D3CAFF003ED08E /* NSData+KeyDerivation.m in Sources */, + 0A80BCD31EC538DD00926C6A /* trailer.c in Sources */, + 2FF8264B19D3CAFF003ED08E /* NSData+Utils.m in Sources */, + 0A80BCD11EC538DD00926C6A /* keys.c in Sources */, + 2FF825EF19D3CAFF003ED08E /* CHMutableNumber.m in Sources */, + 0A80BCD01EC538DD00926C6A /* encrypt.c in Sources */, + 2FF8264919D3CAFF003ED08E /* NSData+SHA.m in Sources */, + 2FF824E519D3C6D8003ED08E /* PublicKey.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 28F9520219D0F9FB00DCE892 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 28F9520B19D0F9FB00DCE892 /* FxATests.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 28F9521919D0FCA000DCE892 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 28F951FA19D0F9FA00DCE892 /* FxA */; + targetProxy = 28F9521819D0FCA000DCE892 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin XCBuildConfiguration section */ + 28F9520C19D0F9FB00DCE892 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_IDENTITY = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 8.2; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 28F9520F19D0F9FB00DCE892 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + APPLICATION_EXTENSION_API_ONLY = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = ""; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_BITCODE = NO; + GCC_WARN_INHIBIT_ALL_WARNINGS = YES; + HEADER_SEARCH_PATHS = ( + "$(inherited)", + /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, + "$(PROJECT_DIR)/FxA/include", + "$(PROJECT_DIR)/../ThirdParty/ecec/include", + ); + INFOPLIST_FILE = FxA/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/FxA/lib", + ); + PRODUCT_BUNDLE_IDENTIFIER = "org.mozilla.ios.$(PRODUCT_NAME:rfc1034identifier)"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SWIFT_VERSION = 3.0; + USER_HEADER_SEARCH_PATHS = ""; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Debug; + }; + 28F9521219D0F9FB00DCE892 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + FRAMEWORK_SEARCH_PATHS = ( + "$(SDKROOT)/Developer/Library/Frameworks", + "$(inherited)", + ); + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + HEADER_SEARCH_PATHS = ( + "$(inherited)", + /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, + "$(PROJECT_DIR)/FxA/include", + ); + INFOPLIST_FILE = FxATests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + LIBRARY_SEARCH_PATHS = "$(PROJECT_DIR)/FxA/lib"; + PRODUCT_BUNDLE_IDENTIFIER = "org.mozilla.$(PRODUCT_NAME:rfc1034identifier)"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 3.0; + }; + name = Debug; + }; + E60222DF1C6E56C10061C436 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_IDENTITY = "iPhone Distribution"; + COPY_PHASE_STRIP = YES; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 8.2; + MTL_ENABLE_DEBUG_INFO = NO; + ONLY_ACTIVE_ARCH = NO; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + E60222E01C6E56C10061C436 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + APPLICATION_EXTENSION_API_ONLY = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = ""; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_BITCODE = NO; + GCC_WARN_INHIBIT_ALL_WARNINGS = YES; + HEADER_SEARCH_PATHS = ( + "$(inherited)", + /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, + "$(PROJECT_DIR)/FxA/include", + "$(PROJECT_DIR)/../ThirdParty/ecec/include", + ); + INFOPLIST_FILE = FxA/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/FxA/lib", + ); + PRODUCT_BUNDLE_IDENTIFIER = "org.mozilla.ios.$(PRODUCT_NAME:rfc1034identifier)"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SWIFT_VERSION = 3.0; + USER_HEADER_SEARCH_PATHS = ""; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Release; + }; + E60222E11C6E56C10061C436 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + FRAMEWORK_SEARCH_PATHS = ( + "$(SDKROOT)/Developer/Library/Frameworks", + "$(inherited)", + ); + HEADER_SEARCH_PATHS = ( + "$(inherited)", + /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, + "$(PROJECT_DIR)/FxA/include", + ); + INFOPLIST_FILE = FxATests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + LIBRARY_SEARCH_PATHS = "$(PROJECT_DIR)/FxA/lib"; + PRODUCT_BUNDLE_IDENTIFIER = "org.mozilla.$(PRODUCT_NAME:rfc1034identifier)"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 3.0; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 28F951F519D0F9FA00DCE892 /* Build configuration list for PBXProject "FxA" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 28F9520C19D0F9FB00DCE892 /* Debug */, + E60222DF1C6E56C10061C436 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 28F9520E19D0F9FB00DCE892 /* Build configuration list for PBXNativeTarget "FxA" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 28F9520F19D0F9FB00DCE892 /* Debug */, + E60222E01C6E56C10061C436 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 28F9521119D0F9FB00DCE892 /* Build configuration list for PBXNativeTarget "FxATests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 28F9521219D0F9FB00DCE892 /* Debug */, + E60222E11C6E56C10061C436 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 28F951F219D0F9FA00DCE892 /* Project object */; +} diff --git a/mobile/ios/FxA/FxA.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/mobile/ios/FxA/FxA.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000000..1685920b2f --- /dev/null +++ b/mobile/ios/FxA/FxA.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,14 @@ + + + + + + + + + diff --git a/mobile/ios/FxA/FxA.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/mobile/ios/FxA/FxA.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000000..08de0be8d3 --- /dev/null +++ b/mobile/ios/FxA/FxA.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + IDEWorkspaceSharedSettings_AutocreateContextsIfNeeded + + + diff --git a/mobile/ios/FxA/FxA.xcodeproj/xcshareddata/xcschemes/FxA.xcscheme b/mobile/ios/FxA/FxA.xcodeproj/xcshareddata/xcschemes/FxA.xcscheme new file mode 100644 index 0000000000..b29ab3c15d --- /dev/null +++ b/mobile/ios/FxA/FxA.xcodeproj/xcshareddata/xcschemes/FxA.xcscheme @@ -0,0 +1,113 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/mobile/ios/FxA/FxA/ASNUtils.h b/mobile/ios/FxA/FxA/ASNUtils.h new file mode 100644 index 0000000000..935043a71a --- /dev/null +++ b/mobile/ios/FxA/FxA/ASNUtils.h @@ -0,0 +1,17 @@ +// 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 + + +@interface ASNUtils : NSObject + ++ (NSData*) encodeSequenceOfNumbers: (NSArray*) numbers; ++ (NSArray*) decodeSequenceOfNumbers: (NSData*) sequence; + ++ (NSData*) decodeDSASignature: (NSData*) signature; ++ (NSData*) encodeDSASignature: (NSData*) flattenedSignature; + +@end diff --git a/mobile/ios/FxA/FxA/ASNUtils.m b/mobile/ios/FxA/FxA/ASNUtils.m new file mode 100644 index 0000000000..48e738ea15 --- /dev/null +++ b/mobile/ios/FxA/FxA/ASNUtils.m @@ -0,0 +1,121 @@ +// 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/ + + +#include "NSData+Utils.h" +#include "CHNumber.h" +#import "ASNUtils.h" + + +@implementation ASNUtils + +// +// 0x30 +// 4 + l1 + l2 +// +// 0x02 +// l1 +// d1 +// +// 0x02 +// l2 +// d2 +// + ++ (NSData*) encodeSequenceOfNumbers: (NSArray*) numbers +{ + NSUInteger totalLength = 0; + for (CHNumber *number in numbers) { + totalLength += 2 + [[number dataValue] length]; + } + + NSMutableData *encoded = [NSMutableData data]; + unsigned char header[2]; + header[0] = 0x30; // SEQUENCE + header[1] = totalLength; + [encoded appendBytes: header length: sizeof header]; + + for (CHNumber *number in numbers) { + NSData *data = [number dataValue]; + unsigned char itemHeader[2]; + itemHeader[0] = 0x02; // NUMBER + itemHeader[1] = [data length]; + [encoded appendBytes: itemHeader length: sizeof itemHeader]; + [encoded appendData: data]; + } + + return encoded; +} + ++ (NSArray*) decodeSequenceOfNumbers: (NSData*) sequence +{ + unsigned char *p = (unsigned char*) [sequence bytes]; + unsigned char *q = p + [sequence length]; + + // TODO: These checks must be way more strict + + if (p[0] != 0x30) { + return nil; + } + + if (p[1] != ([sequence length] - 2)) { + return nil; + } + + p += 2; + + NSMutableArray *numbers = [NSMutableArray new]; + + while (p < q) + { + if (p[0] != 0x02) { + return nil; + } + + NSUInteger numberLength = p[1]; + p += 2; + + NSData *data = [NSData dataWithBytes: p length: numberLength]; + p += numberLength; + + CHNumber *number = [CHNumber numberWithData: data]; + [numbers addObject: number]; + } + + return [NSArray arrayWithArray:numbers]; +} + ++ (NSData*) decodeDSASignature: (NSData*) signature +{ + if (signature == nil || [signature length] == 0) { + return nil; + } + + NSArray *numbers = [ASNUtils decodeSequenceOfNumbers: signature]; + if (numbers == nil || [numbers count] != 2) { + return nil; + } + + return [NSData dataByAppendingDatas: @[ + [[numbers[0] dataValue] dataLeftZeroPaddedToLength: 20], + [[numbers[1] dataValue] dataLeftZeroPaddedToLength: 20], + ]]; +} + ++ (NSData*) encodeDSASignature: (NSData*) flattenedSignature +{ + if (flattenedSignature == nil || [flattenedSignature length] == 0) { + return nil; + } + + NSArray *numbers = @[ + [CHNumber numberWithData: [flattenedSignature subdataWithRange: NSMakeRange(0, [flattenedSignature length]/2)]], + [CHNumber numberWithData: [flattenedSignature subdataWithRange: NSMakeRange([flattenedSignature length]/2, [flattenedSignature length]/2)]] + ]; + + NSData *transformedSignature = [ASNUtils encodeSequenceOfNumbers: numbers]; + return transformedSignature; +} + +@end diff --git a/mobile/ios/FxA/FxA/CHMath.h b/mobile/ios/FxA/FxA/CHMath.h new file mode 100644 index 0000000000..5d33e1fdf2 --- /dev/null +++ b/mobile/ios/FxA/FxA/CHMath.h @@ -0,0 +1,25 @@ +// +// CHMath.h +// CHMath +// +// Created by Dave DeLong on 9/28/09. +// Copyright 2009 Home. All rights reserved. +// + +#import +#import "CHNumber.h" +#import "CHMutableNumber.h" +#import "CHUtils.h" + +/** + @file CHMath.h + + An umbrella header which imports all the public header files for the framework. Headers for individual classes have minimal dependencies, and they import any other header files they may require. + */ + +/** + @mainpage Overview + + CHMath.framework is an open source wrapper around OpenSSL's BigNumber functions. These functions provide support for arbitrarily long integer manipulation. + + */ \ No newline at end of file diff --git a/mobile/ios/FxA/FxA/CHMutableNumber.h b/mobile/ios/FxA/FxA/CHMutableNumber.h new file mode 100644 index 0000000000..a79db1274a --- /dev/null +++ b/mobile/ios/FxA/FxA/CHMutableNumber.h @@ -0,0 +1,227 @@ +// +// CHMutableNumber.h +// CHMath +// +// Created by Dave DeLong on 9/28/09. +// Copyright 2009 Home. All rights reserved. +// + +#import "CHNumber.h" + +/** + @file CHMutableNumber.h + An arbitrarily long number that can be directly manipulated + */ + +@class CHMutableNumber; + +@interface CHNumber (CHMutableAdditions) + +- (CHMutableNumber *) mutableCopyWithZone:(NSZone *)zone; + +@end + +/** + A mutable subclass of CHNumber. It allows users to directly manipulate the wrapped BIGNUM. + */ +@interface CHMutableNumber : CHNumber { + +} + +#pragma mark Behavior + +/** + Changes the value of the receiver + @param newValue the new integer value of the receiver + */ +- (void)setIntegerValue:(int)newValue; + +/** + Changes the value of the receiver + @note if @a newValue contains invalid characters, this method does nothing + @param newValue the new string value of the receiver + */ +- (void)setStringValue:(NSString *)newValue; + +/** + Changes the value of the receiver + @note if @a newValue contains invalid characters, this method does nothing + @param newValue the new hexadecimal string value of the receiver + */ +- (void)setHexStringValue:(NSString *)newValue; + +/** + Clears the value of the receiver and sets it to 0. + */ +- (void)clear; + +#pragma mark Mathematical Operations + +/** + Performs a modular divison on the receiver. + @param mod the NSInteger by which to divide. + */ +- (void)modByInteger:(int)mod; + +/** + Performs a modular division on the receiver. + @param mod the CHNumber by which to divide. + */ +- (void)modByNumber:(CHNumber *)mod; + +/** + Adds @a addend to the receiver + @param addend the NSInteger to add + */ +- (void)addInteger:(int)addend; + +/** + Adds @a addend to the receiver + @param addend the CHNumber to add + */ +- (void)addNumber:(CHNumber *)addend; + +/** + Adds @a addend to the receiver and then divides modulo @a mod + @param addend the CHNumber to add + @param mod the CHNumber by which to divide + */ +- (void)addNumber:(CHNumber *)addend mod:(CHNumber *)mod; + +/** + Subtracts @a subtrahend from the receiver + @param subtrahend the NSInteger to subtract + */ +- (void)subtractInteger:(int)subtrahend; + +/** + Subtracts @a subtrahend from the receiver + @param subtrahend the CHNumber to subtract + */ +- (void)subtractNumber:(CHNumber *)subtrahend; + +/** + Subtracts @a subtrahend from the receiver and then divides modulo @a mod + @param subtrahend the CHNumber to subtract + @param mod the CHNumber by which to divide + */ +- (void)subtractNumber:(CHNumber *)subtrahend mod:(CHNumber *)mod; + + +/** + Multiplies the receiver by @a multiplicand + @param multiplicand the NSInteger by which to multiply + */ +- (void)multiplyByInteger:(int)multiplicand; + +/** + Multiplies the receiver by @a multiplicand + @param multiplicand the CHNumber by which to multiply + */ +- (void)multiplyByNumber:(CHNumber *)multiplicand; + +/** + Multiplies the receiver by @a multiplicand and then divides modulo @a mod + @param multiplicand the CHNumber by which to multiply + @param mod the CHNumber by which to divide + */ +- (void)multiplyByNumber:(CHNumber *)multiplicand mod:(CHNumber *)mod; + +/** + Divides the receiver by @a divisor + @param divisor the NSInteger by which to divide + */ +- (void)divideByInteger:(int)divisor; + +/** + Divides the receiver by @a divisor + @param divisor the CHNumber by which to divide + */ +- (void)divideByNumber:(CHNumber *)divisor; + +/** + Raises the receiver to the @a exponent power + @param exponent the NSInteger by which to raise + */ +- (void)raiseToInteger:(int)exponent; + +/** + Raises the receiver to the @a exponent power + @param exponent the CHNumber by which to raise + */ +- (void)raiseToNumber:(CHNumber *)exponent; + +/** + Raises the receiver to the @a exponent power and then divides modulo @a mod + @param exponent the CHNumber by which to raise + @param mod the CHNumber by which to divide + */ +- (void)raiseToNumber:(CHNumber *)exponent mod:(CHNumber *)mod; + +/** + Squares the receiver + */ +- (void)square; + +/** + Squares the receiver and then divides modulo @a mod + @param mod the CHNumber by which to divide + */ +- (void)squareMod:(CHNumber *)mod; + +#pragma mark Bitfield Operations + +/** + Negates the receiver + */ +- (void)negate; + +/** + Sets the @a bit bit of the receiver to 1 + @note the least significant bit is in position 0 + @param bit the bit to set. + */ +- (void)setBit:(int)bit; + +/** + Sets the @a bit bit of the receiver to 0 + @note the least significant bit is in position 0 + @param bit the bit to clear. + */ +- (void)clearBit:(int)bit; + +/** + Sets the @a bit bit of the receiver to its NOT. In other words, if the bit is 1, it will be flipped to 0. If it is 0, it will be flipped to 1. + @note the least significant bit is in position 0 + @param bit the bit to flip. + */ +- (void)flipBit:(int)bit; + +/** + Perform a single left shift + */ +- (void)shiftLeftOnce; + +/** + Perform a left shift + @param leftShift the number of bits to shift the receiver left + */ +- (void)shiftLeft:(int)leftShift; + +/** + Perform a single right shift + */ +- (void)shiftRightOnce; + +/** + Perform a right shift + @param rightShift the number of bits to shift the receiver right + */ +- (void)shiftRight:(int)rightShift; + +/** + Truncates the receiver to be @a mask bits long + */ +- (void)maskWithInt:(int)mask; + +@end diff --git a/mobile/ios/FxA/FxA/CHMutableNumber.m b/mobile/ios/FxA/FxA/CHMutableNumber.m new file mode 100644 index 0000000000..8468b09c73 --- /dev/null +++ b/mobile/ios/FxA/FxA/CHMutableNumber.m @@ -0,0 +1,197 @@ +/* + CHMath.framework -- CHMutableNumber.m + + Copyright (c) 2008-2009, Dave DeLong + + Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. + + The software is provided "as is", without warranty of any kind, including all implied warranties of merchantability and fitness. In no event shall the authors or copyright holders be liable for any claim, damages, or other liability, whether in an action of contract, tort, or otherwise, arising from, out of, or in connection with the software or the use or other dealings in the software. + */ + + +#import "CHMutableNumber.h" +#import "CHNumber_Private.h" + +@implementation CHNumber (CHMutableAdditions) + +- (CHMutableNumber *) mutableCopyWithZone:(NSZone *)zone { + return [[CHMutableNumber alloc] initWithString:[self stringValue]]; +} + +@end + +@implementation CHMutableNumber + +- (void) setIntegerValue:(int)newValue { + if (newValue < 0) { + newValue *= -1; + //set the initial value to the positive value + BN_set_word([self bigNumber], newValue); + [self negate]; + } else { + BN_set_word([self bigNumber], newValue); + } +} + +- (void)setStringValue:(NSString *)newValue { + NSCharacterSet * decSet = [NSCharacterSet characterSetWithCharactersInString:@"-0123456789"]; + NSRange nonDecChar = [newValue rangeOfCharacterFromSet:[decSet invertedSet]]; + if (nonDecChar.location != NSNotFound) { return; } + BN_dec2bn(&bigNumber, [newValue cStringUsingEncoding:NSASCIIStringEncoding]); +} + +- (void)setHexStringValue:(NSString *)newValue { + NSCharacterSet * hexSet = [NSCharacterSet characterSetWithCharactersInString:@"-0123456789abcdefABCDEF"]; + NSRange nonHexChar = [newValue rangeOfCharacterFromSet:[hexSet invertedSet]]; + if (nonHexChar.location != NSNotFound) { return; } + BN_hex2bn(&bigNumber, [newValue cStringUsingEncoding:NSASCIIStringEncoding]); +} + +- (void)clear { + BN_clear([self bigNumber]); +} + +- (void)modByInteger:(int)mod { + if (mod != 0) { + if (mod < 0) { mod *= -1; } + int result = BN_mod_word([self bigNumber], mod); + [self setIntegerValue:result]; + } +} + +- (void)modByNumber:(CHNumber *)mod { + if ([mod isZero] == NO) { + BN_mod([self bigNumber], [self bigNumber], [mod bigNumber], context); + } +} + +- (void)addInteger:(int)addend { + if (addend > 0) { + BN_add_word([self bigNumber], addend); + } else { + addend *= -1; + [self subtractInteger:addend]; + } +} + +- (void)addNumber:(CHNumber *)addend { + BN_add([self bigNumber], [self bigNumber], [addend bigNumber]); +} + +- (void)addNumber:(CHNumber *)addend mod:(CHNumber *)mod { + BN_mod_add([self bigNumber], [self bigNumber], [addend bigNumber], [mod bigNumber], context); +} + +- (void)subtractInteger:(int)subtrahend { + if (subtrahend > 0) { + BN_sub_word([self bigNumber], subtrahend); + } else { + subtrahend *= -1; + [self addInteger:subtrahend]; + } +} + +- (void)subtractNumber:(CHNumber *)subtrahend { + BN_sub([self bigNumber], [self bigNumber], [subtrahend bigNumber]); +} + +- (void)subtractNumber:(CHNumber *)subtrahend mod:(CHNumber *)mod { + BN_mod_sub([self bigNumber], [self bigNumber], [subtrahend bigNumber], [mod bigNumber], context); +} + +- (void)multiplyByInteger:(int)multiplicand { + if (multiplicand > 0) { + BN_mul_word([self bigNumber], multiplicand); + } else { + multiplicand *= -1; + BN_mul_word([self bigNumber], multiplicand); + [self negate]; + } +} + +- (void)multiplyByNumber:(CHNumber *)multiplicand { + BN_mul([self bigNumber], [self bigNumber], [multiplicand bigNumber], context); +} + +- (void)multiplyByNumber:(CHNumber *)multiplicand mod:(CHNumber *)mod { + BN_mod_mul([self bigNumber], [self bigNumber], [multiplicand bigNumber], [mod bigNumber], context); +} + +- (void)divideByInteger:(int)divisor { + if (divisor == 0) { return; } + if (divisor > 0) { + BN_div_word([self bigNumber], divisor); + } else { + divisor *= -1; + BN_div_word([self bigNumber], divisor); + [self negate]; + } +} + +- (void)divideByNumber:(CHNumber *)divisor { + if ([divisor isZero]) { return; } + BN_div([self bigNumber], NULL, [self bigNumber], [divisor bigNumber], context); +} + +- (void)raiseToInteger:(int)exponent { + CHNumber * exp = [CHNumber numberWithInt:exponent]; + [self raiseToNumber:exp]; +} + +- (void)raiseToNumber:(CHNumber *)exponent { + BN_exp([self bigNumber], [self bigNumber], [exponent bigNumber], context); +} + +- (void)raiseToNumber:(CHNumber *)exponent mod:(CHNumber *)mod { + BN_mod_exp([self bigNumber], [self bigNumber], [exponent bigNumber], [mod bigNumber], context); +} + +- (void)square { + BN_sqr([self bigNumber], [self bigNumber], context); +} + +- (void)squareMod:(CHNumber *)mod { + BN_mod_sqr([self bigNumber], [self bigNumber], [mod bigNumber], context); +} + +- (void)negate { + BN_set_negative([self bigNumber], ![self isNegative]); +} + +- (void)setBit:(int)bit { + BN_set_bit([self bigNumber], bit); +} + +- (void)clearBit:(int)bit { + BN_clear_bit([self bigNumber], bit); +} + +- (void)flipBit:(int)bit { + if ([self isBitSet:bit]) { + [self clearBit:bit]; + } else { + [self setBit:bit]; + } +} + +- (void)shiftLeftOnce { + BN_lshift1([self bigNumber], [self bigNumber]); +} + +- (void)shiftLeft:(int)leftShift { + BN_lshift([self bigNumber], [self bigNumber], leftShift); +} + +- (void)shiftRightOnce { + BN_rshift1([self bigNumber], [self bigNumber]); +} + +- (void)shiftRight:(int)rightShift { + BN_rshift([self bigNumber], [self bigNumber], rightShift); +} + +- (void)maskWithInt:(int)mask { + BN_mask_bits([self bigNumber], mask); +} + +@end diff --git a/mobile/ios/FxA/FxA/CHNumber.h b/mobile/ios/FxA/FxA/CHNumber.h new file mode 100644 index 0000000000..4a2e3af769 --- /dev/null +++ b/mobile/ios/FxA/FxA/CHNumber.h @@ -0,0 +1,403 @@ +// +// CHNumber.h +// CHMath +// +// Created by Dave DeLong on 9/28/09. +// Copyright 2009 Home. All rights reserved. +// + +#import +#import "openssl/bn.h" + +/** + @file CHNumber.h + A arbitrarily long integer + */ + +/** + CHNumber is a wrapper that represents an arbitrarily long integer. It provides methods for adding, subtracting, dividing, and multiplying with other CHNumbers. + + CHNumber wraps the BIGNUM type of the OpenSSL library. + */ +@interface CHNumber : NSObject { + BIGNUM *bigNumber; + BN_CTX *context; +} + +#pragma mark Convenience Methods + +/** + Determines if an NSInteger is prime or not. + @param integer the integer to test for primality + @return @c YES if the parameter is a prime number. + */ ++ (BOOL) isIntegerPrime:(int)integer; + +#pragma mark Initializers + +/** + Creates an autoreleased CHNumber initialized to 0. + @return a new CHNumber, or @a nil if an error occurs + */ ++ (id)number; + +/** + Creates an autoreleased CHNumber initialized to @a integer. + @param integer the integer value of the new number + @return a new CHNumber, or @a nil if an error occurs + */ ++ (id)numberWithInt:(int)integer; + +/** + Creates an autoreleased CHNumber initialized to @a integer. + @param integer the unsigned integer value of the new number + @return a new CHNumber, or @a nil if an error occurs + */ ++ (id)numberWithUnsignedInt:(unsigned int)integer; + +/** + Creates an autoreleased CHNumber initialized to the value represented by @a string. + @param string an NSString of decimal characters (0 - 9) + @return a new CHNumber, or @a nil if a number cannot be extracted from @a string + */ ++ (id)numberWithString:(NSString *)string; + +/** + Creates an autoreleased CHNumber initialized to the value represented by @a string + @param string an NSString of hexadecimal characters (0 - 9, A - F) + @return a new CHNumber, or @a nil if a number cannot be extracted from @a string + */ ++ (id)numberWithHexString:(NSString *)string; + ++ (id)numberWithData:(NSData *)data; + ++ (id) numberWithOpenSSLNumber: (BIGNUM*) bn; + +/** + Creates an autoreleased CHNumber initialized to the value of @a number + @param number an NSNumber that represents an integer value + @return a new CHNumber, or @a nil if an integer cannot be extracted from @a number. + */ ++ (id)numberWithNumber:(NSNumber *)number; + + +/** + Creates a new CHNumber initialized to @a integer. + @param integer the integer value of the new number + @return a new CHNumber, or @a nil if an error occurs + */ +- (id)initWithInt:(int)integer; + +/** + Creates a new CHNumber initialized to @a integer. + @param integer the unsigned integer value of the new number + @return a new CHNumber, or @a nil if an error occurs + */ +- (id)initWithUnsignedInt:(unsigned int)integer; + +/** + Creates a new CHNumber initialized to the value represented by @a string. + @param string an NSString of decimal characters (0 - 9) + @return a new CHNumber, or @a nil if a number cannot be extracted from @a string + */ +- (id)initWithString:(NSString *)string; + +/** + Creates a new CHNumber initialized to the value represented by @a string + @param string an NSString of hexadecimal characters (0 - 9, A - F) + @return a new CHNumber, or @a nil if a number cannot be extracted from @a string + */ +- (id)initWithHexString:(NSString *)string; + +/** + Created a new CHNumber initialized of the value represented by @a data. + @param data an NSData instance + @return a new CHNumber, or @a nil if a number cannot be extraced from @a data + */ +- (id)initWithData:(NSData*)data; + +- (id)initWithOpenSSLNumber:(BIGNUM*)bn; + +/** + Creates a new CHNumber initialized to the value of @a number + @param number an NSNumber that represents an integer value + @return a new CHNumber, or @a nil if an integer cannot be extracted from @a number. + */ +- (id)initWithNumber:(NSNumber *)number; + +#pragma mark Behavior + +/** + Creates a two's complement binary representation of the integer as an NSString + @return an NSString + */ +- (NSString *)binaryStringValue; + +/** + Creates a hexadecimal representation of the integer as an NSString + @return an NSString + */ +- (NSString *)hexStringValue; + +- (NSData *)dataValue; + +/** + Creates a decimal representation of the integer as an NSString + @return an NSString + */ +- (NSString *)stringValue; + +/** + Returns the receiver's value as an NSInteger + @note if the receiver's integer value is too large to be expressed in a single NSInteger, this method returns 0xFFFFFFFFFFFFFFFF. + @return an NSInteger + */ +- (NSInteger)integerValue; + +/** + Returns the receiver's value as an NSUInteger + @note if the receiver's integer value is too large to be expressed in a single NSUInteger, this method returns 0xFFFFFFFFFFFFFFFF. + @return an NSUInteger + */ +- (NSUInteger)unsignedIntegerValue; + +- (BIGNUM*) bigNumValue; + +/** + Returns the receiver's prime factors + @note depending on the size of the receiver, this method may take a very long time to return + @return an NSArray of CHNumbers. + */ +- (NSArray *)factors; + +#pragma mark Comparisons + +/** + Returns a boolean indicating whether the receiver is equal to 0. + @return @c YES if the receiver is equal to 0, @c NO otherwise. + */ +- (BOOL)isZero; + +/** + Returns a boolean indicating whether the receiver is equal to 1. + @return @c YES if the receiver is equal to 1, @c NO otherwise. + */ +- (BOOL)isOne; + +/** + Returns a boolean indicating whether the receiver is less than 0. + @return @c YES if the receiver is less than 0, @c NO otherwise. + */ +- (BOOL)isNegative; + +/** + Returns a boolean indicating whether the receiver is greater than or equal to 0. + @return @c YES if the receiver is greater than or equal to 0, @c NO otherwise. + */ +- (BOOL)isPositive; + +/** + Returns a boolean indicating whether the receiver is a prime number. + @return @c YES if the receiver is a prime number. + */ +- (BOOL)isPrime; + +/** + Returns a boolean indicating whether the receiver is an odd number. + @return @c YES if the receiver is odd, @c NO otherwise. + */ +- (BOOL)isOdd; + +/** + Returns a boolean indicating whether the receiver is an even number. + @return @c YES if the receiver is even, @c NO otherwise. + */ +- (BOOL)isEven; + +/** + Returns a boolean indicating whether the receiver is greather than @a number. This method uses the BN_cmp to do its comparison. + @param number another CHNumber + @return @c YES if the receiver is greater than @a number, @c NO otherwise. + */ +- (BOOL)isGreaterThanNumber:(CHNumber *)number; + +/** + Returns a boolean indicating whether the receiver is greather than or equal to @a number. This method uses the BN_cmp to do its comparison. + @param number another CHNumber + @return @c YES if the receiver is greater than or equal to @a number, @c NO otherwise. + */ +- (BOOL)isGreaterThanOrEqualToNumber:(CHNumber *)number; + +/** + Returns a boolean indicating whether the receiver is equal to @a number. This method uses the BN_cmp to do its comparison. + @param number another CHNumber + @return @c YES if the receiver is equal to @a number, @c NO otherwise. + */ +- (BOOL)isEqualToNumber:(CHNumber *)number; + +/** + Returns a boolean indicating whether the receiver is less than @a number. This method uses the BN_cmp to do its comparison. + @param number another CHNumber + @return @c YES if the receiver is less than @a number, @c NO otherwise. + */ +- (BOOL)isLessThanNumber:(CHNumber *)number; + +/** + Returns a boolean indicating whether the receiver is less than or equal to @a number. This method uses the BN_cmp to do its comparison. + @param number another CHNumber + @return @c YES if the receiver is less than or equal to @a number, @c NO otherwise. + */ +- (BOOL)isLessThanOrEqualToNumber:(CHNumber *)number; + +/** + Returns an NSComparisonResult indicating how the receiver compares to @a object. This method uses the BN_cmp to do its comparison. + @param object a non-nil object + @return NSOrderedAscending if the value of @a object is greater than the receiver’s, NSOrderedSame if they’re equal, and NSOrderedDescending if the value of @a object is less than the receiver’s. + */ +- (NSComparisonResult)compare:(id)object; + +#pragma mark Mathematical Operations + +/** + Perform a modular division. Returns a number n such that @c (receiver = k * mod + n). + @param mod the number by which to divide + @return a CHNumber + */ +- (CHNumber *)numberByModding:(CHNumber *)mod; + +/** + Perform an inverse modular division. Returns a number r such that @c ((receiver * r) % mod == 1). + @param mod a non-nil CHNumber + @return a CHNumber + */ +- (CHNumber *)numberByInverseModding:(CHNumber *)mod; + +/** + Perform addition. + @param addend the number to add to the receiver + @return a new CHNumber + */ +- (CHNumber *)numberByAdding:(CHNumber *)addend; + +/** + Perform modulo addition. + @param addend the number to add to the receiver + @param mod the number to divide by + @return a new CHNumber @a r such that @c (r = (receiver + addend) % mod) + */ +- (CHNumber *)numberByAdding:(CHNumber *)addend mod:(CHNumber *)mod; + +/** + Perform subtraction. + @param subtrahend the number to subtract from the receiver + @return a new CHNumber + */ +- (CHNumber *)numberBySubtracting:(CHNumber *)subtrahend; + +/** + Perform modulo subtraction. + @param subtrahend the number to subtract from the receiver + @param mod the number to divide by + @return a new CHNumber @a r such that @c (r = (receiver - subtrahend) % mod) + */ +- (CHNumber *)numberBySubtracting:(CHNumber *)subtrahend mod:(CHNumber *)mod; + +/** + Perform multiplication. + @param multiplicand the number by which to multiply the receiver + @return a new CHNumber + */ +- (CHNumber *)numberByMultiplyingBy:(CHNumber *)multiplicand; + +/** + Perform modulo multiplication. + @param multiplicand the number by which to multiply the receiver + @param mod the number to divide by + @return a new CHNumber @a r such that @c (r = (receiver * multiplicand) % mod) + */ +- (CHNumber *)numberByMultiplyingBy:(CHNumber *)multiplicand mod:(CHNumber *)mod; + +/** + Perform division. + @param divisor the number by which to divide the receiver + @return a new CHNumber + */ +- (CHNumber *)numberByDividingBy:(CHNumber *)divisor; + +/** + Square the receiver. + @return a new CHNumber @a r such that @c (r = receiver * receiver) + */ +- (CHNumber *)squaredNumber; + +/** + Modulo square the receiver. + @param mod the number to divide by + @return a new CHNumber @a r such that @c (r = (receiver * receiver) % mod) + */ +- (CHNumber *)squaredNumberMod:(CHNumber *)mod; + +/** + Raise the receiver to a power. + @param exponent the power by which to raise the receiver + @return a new CHNumber @a r such that @c (r = receiver ^ exponent) + */ +- (CHNumber *)numberByRaisingToPower:(CHNumber *)exponent; + +/** + Modulo raise the receiver to a power. + @param exponent the power by which to raise the receiver + @param mod the number to divide by + @return a new CHNumber @a r such that @c (r = (receiver ^ exponent) % mod) + */ +- (CHNumber *)numberByRaisingToPower:(CHNumber *)exponent mod:(CHNumber *)mod; + +/** + Negate the receiver. + @return a new CHNumber @a r such that @c (r = receiver * -1) + */ +- (CHNumber *)negatedNumber; + +#pragma mark Bitfield Operations + +/** + Determine if a bit is set. + @note the least significant bit of the receiver is the zeroth (0) bit + @param bit an NSUInteger + @return @c YES if the bit is set, @c NO otherwise. + */ +- (BOOL)isBitSet:(int)bit; + +/** + Perform a single left shift + @return a new CHNumber @a r such that @c (r = receiver << 1) + */ +- (CHNumber *)numberByShiftingLeftOnce; + +/** + Perform a left shift + @param leftShift the number of bits to shift left + @return a new CHNumber @a r such that @c (r = receiver << leftShift) + */ +- (CHNumber *)numberByShiftingLeft:(int)leftShift; + +/** + Perform a single right shift + @return a new CHNumber @a r such that @c (r = receiver >> 1) + */ +- (CHNumber *)numberByShiftingRightOnce; + +/** + Perform a right shift + @param rightShift the number of bits to shift right + @return a new CHNumber @a r such that @c (r = receiver >> rightShift) + */ +- (CHNumber *)numberByShiftingRight:(int)rightShift; + +/** + Truncates the receiver to be @a mask bits long + @return a new CHNumber @a r such that @c (r = receiver && (2^(mask+1))-1) + */ +- (CHNumber *)numberByMaskingWithInt:(int)mask; + +@end diff --git a/mobile/ios/FxA/FxA/CHNumber.m b/mobile/ios/FxA/FxA/CHNumber.m new file mode 100644 index 0000000000..e54a328522 --- /dev/null +++ b/mobile/ios/FxA/FxA/CHNumber.m @@ -0,0 +1,456 @@ +/* + CHMath.framework -- CHNumber.m + + Copyright (c) 2008-2009, Dave DeLong + + Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. + + The software is provided "as is", without warranty of any kind, including all implied warranties of merchantability and fitness. In no event shall the authors or copyright holders be liable for any claim, damages, or other liability, whether in an action of contract, tort, or otherwise, arising from, out of, or in connection with the software or the use or other dealings in the software. + */ + + +#import "CHNumber.h" +#import "CHNumber_Private.h" + +#define CH_ASCII_ZERO 48 + +@interface CHNumber () + +@property (readonly) BIGNUM * bigNumber; + +@end + + +@implementation CHNumber + +@synthesize bigNumber; + ++ (BOOL) isIntegerPrime:(int)integer { + CHNumber * num = [[CHNumber alloc] initWithInt:integer]; + return [num isPrime]; +} + ++ (id)numberWithInt:(int)integer { + //in class methods, self always refers to the Class object + return [[self alloc] initWithInt:integer]; +} + ++ (id)numberWithUnsignedInt:(unsigned int)integer { + return [[self alloc] initWithUnsignedInt:integer]; +} + ++ (id)numberWithString:(NSString *)string { + return [[self alloc] initWithString:string]; +} + ++ (id)numberWithHexString:(NSString *)string { + return [[self alloc] initWithHexString:string]; +} + ++ (id)numberWithData:(NSData *)data +{ + return [[self alloc] initWithData: data]; +} + ++ (id) numberWithOpenSSLNumber: (BIGNUM*) bn +{ + return [[self alloc] initWithOpenSSLNumber: bn]; +} + ++ (id)numberWithNumber:(NSNumber *)number { + return [[self alloc] initWithNumber:number]; +} + ++ (id)number { + return [[self alloc] initWithInt:0]; +} + +- (id) init { + if (self = [super init]) { + bigNumber = BN_new(); + if (bigNumber == NULL) { + //[self release]; + return nil; + } + BN_zero(bigNumber); + context = BN_CTX_new(); + if(context == NULL) { + BN_free(bigNumber); + //[self release]; + return nil; + } + } + return self; +} + +- (id)initWithInt:(int)integer { + if(self = [self init]) { + if (integer < 0) { + integer *= -1; + //set the initial value to the positive value + BN_set_word([self bigNumber], integer); + //subtract the value twice to get the negative value + BN_sub_word([self bigNumber], integer); + BN_sub_word([self bigNumber], integer); + } else { + BN_set_word([self bigNumber], integer); + } + } + return self; +} + +- (id)initWithUnsignedInt:(unsigned int)integer { + if (self = [self init]) { + BN_set_word(bigNumber, integer); + } + return self; +} + +- (id)initWithString:(NSString *)string { + NSCharacterSet * decSet = [NSCharacterSet characterSetWithCharactersInString:@"-0123456789"]; + NSRange nonDecChar = [string rangeOfCharacterFromSet:[decSet invertedSet]]; + if (nonDecChar.location != NSNotFound) { return nil; } + if (self = [self init]) { + BN_dec2bn(&bigNumber, [string cStringUsingEncoding:NSASCIIStringEncoding]); + } + return self; +} + +- (id)initWithHexString:(NSString *)string { + NSCharacterSet * hexSet = [NSCharacterSet characterSetWithCharactersInString:@"-0123456789abcdefABCDEF"]; + NSRange nonHexChar = [string rangeOfCharacterFromSet:[hexSet invertedSet]]; + if (nonHexChar.location != NSNotFound) { return nil; } + if (self = [self init]) { + BN_hex2bn(&bigNumber, [string cStringUsingEncoding:NSASCIIStringEncoding]); + } + return self; +} + +- (id)initWithData:(NSData*)data +{ + if (self = [self init]) { + if (data == nil) { + return nil; + } + if ([data length] == 0) { + return nil; + } + (void) BN_bin2bn([data bytes], (int) [data length], bigNumber); + } + return self; +} + +- (id)initWithOpenSSLNumber:(BIGNUM*)bn +{ + if ((self = [self init]) != nil) { + BN_copy(bigNumber, bn); + } + return self; +} + +- (id)initWithNumber:(NSNumber *)number { + return [self initWithString:[number descriptionWithLocale:[NSLocale currentLocale]]]; +} + +- (void) dealloc { + BN_clear_free(bigNumber), bigNumber = NULL; + BN_CTX_free(context), context = NULL; +} + +- (void) finalize { + BN_clear_free(bigNumber), bigNumber = NULL; + BN_CTX_free(context), context = NULL; + [super finalize]; +} + +#pragma mark - +#pragma mark NSCopying compliance + +- (id) copyWithZone:(NSZone *)zone { + id copy = [[[self class] alloc] init]; + BN_copy([copy bigNumber], [self bigNumber]); + return copy; +} + +#pragma mark - +#pragma mark NSCoding compliance + +- (id) initWithCoder:(NSCoder *)decoder { + return [self initWithString:[decoder decodeObjectForKey:@"num"]]; +} + +- (void) encodeWithCoder:(NSCoder *)encoder { + [encoder encodeObject:[self stringValue] forKey:@"num"]; +} + +#pragma mark - +#pragma mark Getters and Setters + +- (NSString *)stringValue { + char *cStr = BN_bn2dec([self bigNumber]); + NSString *str = [NSString stringWithCString:cStr encoding:NSASCIIStringEncoding]; + OPENSSL_free(cStr); + return str; +} + +- (NSString *)hexStringValue { + char *cStr = BN_bn2hex([self bigNumber]); + NSString *str = [[NSString stringWithCString:cStr encoding:NSASCIIStringEncoding] lowercaseString]; + OPENSSL_free(cStr); + return str; +} + +- (NSData *)dataValue +{ + NSData* result = nil; + + void* buffer = malloc(BN_num_bytes([self bigNumber])); + if (buffer != NULL) { + BN_bn2bin([self bigNumber], buffer); + result = [NSData dataWithBytesNoCopy: buffer length: BN_num_bytes([self bigNumber]) freeWhenDone: YES]; + } + + return result; +} + +- (NSString *)binaryStringValue { + int numBits = BN_num_bits([self bigNumber]); + BOOL isNegative = [self isNegative]; + BOOL shouldFlip = NO; + int totalBufferSize = numBits + 1; + unsigned char * string = malloc(totalBufferSize * sizeof(unsigned char)); + string[totalBufferSize] = '\0'; + for (int i = 0; i < numBits; ++i) { + int idx = totalBufferSize - i - 1; + int bit = BN_is_bit_set([self bigNumber], i); + if (isNegative) { + if (shouldFlip == YES) { + bit = !bit; + } else if (bit == 1) { + shouldFlip = YES; + } + } + string[idx] = bit + CH_ASCII_ZERO; + } + string[0] = (int)isNegative + CH_ASCII_ZERO; + NSString * binaryString = [[NSString alloc] initWithBytesNoCopy:string length:totalBufferSize encoding:NSASCIIStringEncoding freeWhenDone:YES]; + return binaryString; +} + +- (NSInteger)integerValue { + NSInteger value = BN_get_word([self bigNumber]); + if ([self isNegative]) { value *= -1; } + return value; +} + +- (NSUInteger)unsignedIntegerValue { + return (NSUInteger)BN_get_word([self bigNumber]); +} + +- (BIGNUM*) bigNumValue +{ + return BN_dup([self bigNumber]); +} + +- (NSString *)description { + return [self stringValue]; +} + +- (NSString *)debugDescription { + return [NSString stringWithFormat:@"<%@ %p> - %@", NSStringFromClass([self class]), self, [self stringValue]]; +} + +#pragma mark Comparisons + +- (BOOL)isZero { + return BN_is_zero([self bigNumber]); +} + +- (BOOL)isOne { + return BN_is_one([self bigNumber]); +} + +- (BOOL)isNegative { + return [self isLessThanNumber:[CHNumber number]]; +} + +- (BOOL)isPositive { + return ![self isNegative]; +} + +- (BOOL)isPrime { + return BN_is_prime_ex([self bigNumber], BN_prime_checks, context, NULL); +} + +- (BOOL)isOdd { + return BN_is_odd([self bigNumber]); +} + +- (BOOL)isEven { + return ![self isOdd]; +} + +- (BOOL)isGreaterThanNumber:(CHNumber *)number { + return (BN_cmp([self bigNumber], [number bigNumber]) == NSOrderedDescending); +} + +- (BOOL)isGreaterThanOrEqualToNumber:(CHNumber *)number { + return ([self isGreaterThanNumber:number] || [self isEqualToNumber:number]); +} + +- (NSComparisonResult) compare:(id)object { + if ([object isKindOfClass:[CHNumber class]]) { + CHNumber * other = (CHNumber *)object; + return (NSComparisonResult)BN_cmp([self bigNumber], [other bigNumber]); + } + return NSOrderedDescending; +} + +- (BOOL)isEqualToNumber:(CHNumber *)number { + return (BN_cmp([self bigNumber], [number bigNumber]) == NSOrderedSame); +} + +- (BOOL)isLessThanNumber:(CHNumber *)number { + return (BN_cmp([self bigNumber], [number bigNumber]) == NSOrderedAscending); +} + +- (BOOL)isLessThanOrEqualToNumber:(CHNumber *)number { + return ([self isLessThanNumber:number] || [self isEqualToNumber:number]); +} + +- (BOOL) isEqualTo:(id)object { + if ([object isKindOfClass:[self class]]) { + return [self isEqualToNumber:(CHNumber *)object]; + } else { + return NO; + } +} + +- (BOOL) isEqual:(id)object { + return [self isEqualTo:object]; +} + +#pragma mark - +#pragma mark Mathematical Operations + +- (CHNumber *)numberByModding:(CHNumber *)mod { + CHNumber * result = [CHNumber number]; + BN_mod([result bigNumber], [self bigNumber], [mod bigNumber], context); + return result; +} + +- (CHNumber *)numberByInverseModding:(CHNumber *)mod { + CHNumber * result = [CHNumber number]; + BN_mod_inverse([result bigNumber], [self bigNumber], [mod bigNumber], context); + return result; +} + +- (CHNumber *)numberByAdding:(CHNumber *)addend { + CHNumber * result = [CHNumber number]; + BN_add([result bigNumber], [self bigNumber], [addend bigNumber]); + return result; +} + +- (CHNumber *)numberByAdding:(CHNumber *)addend mod:(CHNumber *)mod { + CHNumber * result = [CHNumber number]; + BN_mod_add([result bigNumber], [self bigNumber], [addend bigNumber], [mod bigNumber], context); + return result; +} + +- (CHNumber *)numberBySubtracting:(CHNumber *)subtrahend { + CHNumber * result = [CHNumber number]; + BN_sub([result bigNumber], [self bigNumber], [subtrahend bigNumber]); + return result; +} + +- (CHNumber *)numberBySubtracting:(CHNumber *)subtrahend mod:(CHNumber *)mod { + CHNumber * result = [CHNumber number]; + BN_mod_sub([result bigNumber], [self bigNumber], [subtrahend bigNumber], [mod bigNumber], context); + return result; +} + +- (CHNumber *)numberByMultiplyingBy:(CHNumber *)multiplicand { + CHNumber * result = [CHNumber number]; + BN_mul([result bigNumber], [self bigNumber], [multiplicand bigNumber], context); + return result; +} + +- (CHNumber *)numberByMultiplyingBy:(CHNumber *)multiplicand mod:(CHNumber *)mod { + CHNumber * result = [CHNumber number]; + BN_mod_mul([result bigNumber], [self bigNumber], [multiplicand bigNumber], [mod bigNumber], context); + return result; +} + +- (CHNumber *)numberByDividingBy:(CHNumber *)divisor { + CHNumber * result = [CHNumber number]; + BN_div([result bigNumber], NULL, [self bigNumber], [divisor bigNumber], context); + return result; +} + +- (CHNumber *)squaredNumber { + CHNumber * result = [CHNumber number]; + BN_sqr([result bigNumber], [self bigNumber], context); + return result; +} + +- (CHNumber *)squaredNumberMod:(CHNumber *)mod { + CHNumber * result = [CHNumber number]; + BN_mod_sqr([result bigNumber], [self bigNumber], [mod bigNumber], context); + return result; +} + +- (CHNumber *)numberByRaisingToPower:(CHNumber *)exponent { + CHNumber * result = [CHNumber number]; + BN_exp([result bigNumber], [self bigNumber], [exponent bigNumber], context); + return result; +} + +- (CHNumber *)numberByRaisingToPower:(CHNumber *)exponent mod:(CHNumber *)mod { + CHNumber * result = [CHNumber number]; + BN_mod_exp([result bigNumber], [self bigNumber], [exponent bigNumber], [mod bigNumber], context); + return result; +} + +- (CHNumber *)negatedNumber { + CHNumber * result = [self copy]; + BN_set_negative([result bigNumber], ![self isNegative]); + return result; +} + +#pragma mark Bitfield Operations + +- (BOOL)isBitSet:(int)bit { + return BN_is_bit_set([self bigNumber], bit); +} + +- (CHNumber *)numberByShiftingLeftOnce { + CHNumber * result = [CHNumber number]; + BN_lshift1([result bigNumber], [self bigNumber]); + return result; +} + +- (CHNumber *)numberByShiftingLeft:(int)leftShift { + CHNumber * result = [CHNumber number]; + BN_lshift([result bigNumber], [self bigNumber], leftShift); + return result; +} + +- (CHNumber *)numberByShiftingRightOnce { + CHNumber * result = [CHNumber number]; + BN_rshift1([result bigNumber], [self bigNumber]); + return result; +} + +- (CHNumber *)numberByShiftingRight:(int)rightShift { + CHNumber * result = [CHNumber number]; + BN_rshift([result bigNumber], [self bigNumber], rightShift); + return result; +} + +- (CHNumber *)numberByMaskingWithInt:(int)mask { + CHNumber * result = [self copy]; + BN_mask_bits([result bigNumber], mask); + return result; +} + +@end diff --git a/mobile/ios/FxA/FxA/CHNumber_Private.h b/mobile/ios/FxA/FxA/CHNumber_Private.h new file mode 100644 index 0000000000..97e6d70b20 --- /dev/null +++ b/mobile/ios/FxA/FxA/CHNumber_Private.h @@ -0,0 +1,13 @@ +// +// CHNumber_Private.h +// CHMath +// +// Created by Dave DeLong on 9/30/09. +// Copyright 2009 Home. All rights reserved. +// + +@interface CHNumber (PRIVATE) + +@property(readonly) BIGNUM * bigNumber; + +@end \ No newline at end of file diff --git a/mobile/ios/FxA/FxA/FxA.h b/mobile/ios/FxA/FxA/FxA.h new file mode 100644 index 0000000000..22375a9069 --- /dev/null +++ b/mobile/ios/FxA/FxA/FxA.h @@ -0,0 +1,31 @@ +// +// FxA.h +// FxA +// +// Created by Richard Newman on 2014-09-22. +// Copyright (c) 2014 Mozilla. All rights reserved. +// + +#import + +//! Project version number for FxA. +FOUNDATION_EXPORT double FxAVersionNumber; + +//! Project version string for FxA. +FOUNDATION_EXPORT const unsigned char FxAVersionString[]; + +// In this header, you should import all the public headers of your framework using statements like #import + +#include +#include +#include +#include +#include +#include +#include + +// These are all the ones the compiler complains are missing. +// Some are commented out because they rely on openssl/bn.h, which we can't find +// when we try the import. *shrug* +#include "ASNUtils.h" +#include diff --git a/mobile/ios/FxA/FxA/Info.plist b/mobile/ios/FxA/FxA/Info.plist new file mode 100644 index 0000000000..d3de8eefb6 --- /dev/null +++ b/mobile/ios/FxA/FxA/Info.plist @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + $(CURRENT_PROJECT_VERSION) + NSPrincipalClass + + + diff --git a/mobile/ios/FxA/FxA/JSONWebTokenUtils.h b/mobile/ios/FxA/FxA/JSONWebTokenUtils.h new file mode 100644 index 0000000000..00b891aed8 --- /dev/null +++ b/mobile/ios/FxA/FxA/JSONWebTokenUtils.h @@ -0,0 +1,40 @@ +// 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 + + +@class KeyPair; +@class PublicKey; +@class PrivateKey; +@class FXACertificate; + + +extern NSString * const JSONWebTokenUtilsDefaultAssertionIssuer; + +extern const unsigned long long JSONWebTokenUtilsDefaultCertificateDuration; +extern const unsigned long long JSONWebTokenUtilsDefaultAssertionDuration; + + +@interface JSONWebTokenUtils : NSObject + ++ (NSString*) base64EncodedJSONObject: (NSDictionary*) object; + ++ (NSString*) encodePayloadString: (NSString*) payloadString withPrivateKeyToSign: (PrivateKey*) privateKey; ++ (NSString*) decodePayloadStringFromToken: (NSString*) token withPublicKeyToVerify: (PublicKey*) publicKey; + ++ (NSDictionary*) payloadWithPayload: (NSDictionary*) payload issuer: (NSString*) issuer issuedAt: (unsigned long long) issuedAt expiresAt: (unsigned long long) expiresAt audience: (NSString*) audience; ++ (NSString*) payloadStringWithPayload: (NSDictionary*) payload issuer: (NSString*) issuer issuedAt: (unsigned long long) issuedAt expiresAt: (unsigned long long) expiresAt audience: (NSString*) audience; + ++ (NSDictionary*) certificatePayloadWithPublicKeyToSign: (PublicKey*) publicKeyToSign email: (NSString*) email; ++ (NSString*) certificatePayloadStringWithPublicKeyToSign: (PublicKey*) publicKeyToSign email: (NSString*) email; + ++ (NSString*) certificateWithPublicKeyToSign: (PublicKey*) publicKeyToSign email: (NSString*) email issuer: (NSString*) issuer issuedAt: (unsigned long long) issuedAt expiresAt: (unsigned long long) expiresAt signingPrivateKey: (PrivateKey*) signingPrivateKey; + + ++ (NSString*) createAssertionWithPrivateKeyToSignWith: (PrivateKey*) privateKeyToSignWith certificate: (NSString*) certificate audience: (NSString*) audience issuer: (NSString*) issuer issuedAt: (unsigned long long) issuedAt duration: (unsigned long long) duration; ++ (NSString*) createAssertionWithPrivateKeyToSignWith: (PrivateKey*) privateKeyToSignWith certificate: (NSString*) certificate audience: (NSString*) audience; + +@end diff --git a/mobile/ios/FxA/FxA/JSONWebTokenUtils.m b/mobile/ios/FxA/FxA/JSONWebTokenUtils.m new file mode 100644 index 0000000000..a055a4711f --- /dev/null +++ b/mobile/ios/FxA/FxA/JSONWebTokenUtils.m @@ -0,0 +1,140 @@ +// 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/ + + +#include "CHNumber.h" +#include "NSData+Utils.h" + +#import "KeyPair.h" +#import "PrivateKey.h" +#import "PublicKey.h" +#import "JSONWebTokenUtils.h" + + +NSString * const JSONWebTokenUtilsDefaultAssertionIssuer = @"127.0.0.1"; + +const unsigned long long JSONWebTokenUtilsDefaultCertificateDuration = 60 * 60 * 1000; +const unsigned long long JSONWebTokenUtilsDefaultAssertionDuration = 60 * 60 * 1000; + + +@implementation JSONWebTokenUtils + ++ (NSString*) base64EncodedJSONObject: (NSDictionary*) object +{ + NSError *encodingError = nil; + NSData *data = [NSJSONSerialization dataWithJSONObject: object options: 0 error: &encodingError]; + if (encodingError != nil) { + return nil; + } + + return [data base64URLEncodedStringWithOptions: 0]; +} + ++ (NSString*) encodePayloadString: (NSString*) payloadString withPrivateKeyToSign: (PrivateKey*) privateKey +{ + NSDictionary *header = @{ + @"alg": privateKey.algorithm, + }; + + NSString *encodedHeader = [self base64EncodedJSONObject: header]; + NSString *encodedPayload = [[payloadString dataUsingEncoding: NSUTF8StringEncoding] base64URLEncodedStringWithOptions: 0]; + + NSString *message = [NSString stringWithFormat: @"%@.%@", encodedHeader, encodedPayload]; + NSString *signature = [[privateKey signMessageString: message encoding: NSUTF8StringEncoding] base64URLEncodedStringWithOptions: 0]; + + return [NSString stringWithFormat: @"%@.%@", message, signature]; +} + ++ (NSString*) decodePayloadStringFromToken: (NSString*) token withPublicKeyToVerify: (PublicKey*) publicKey +{ + NSArray *components = [token componentsSeparatedByString: @"."]; + if ([components count] != 3) { + return nil; + } + + NSData *message = [[NSString stringWithFormat: @"%@.%@", components[0], components[1]] dataUsingEncoding: NSUTF8StringEncoding]; + NSData *signature = [[NSData alloc] initWithBase64URLEncodedString: components[2] options: 0]; + + if ([publicKey verifySignature: signature againstMessage: message] == NO) { + return nil; + } + + return [[NSString alloc] initWithData: [[NSData alloc] initWithBase64URLEncodedString: components[1] options: 0] encoding: NSUTF8StringEncoding]; +} + ++ (NSDictionary*) payloadWithPayload: (NSDictionary*) payload issuer: (NSString*) issuer issuedAt: (unsigned long long) issuedAt expiresAt: (unsigned long long) expiresAt audience: (NSString*) audience +{ + NSMutableDictionary *fullPayload = [NSMutableDictionary dictionaryWithDictionary: payload]; + fullPayload[@"iss"] = issuer; + fullPayload[@"iat"] = [NSNumber numberWithUnsignedLongLong: issuedAt]; + if (audience != nil) { + fullPayload[@"aud"] = audience; + } + fullPayload[@"exp"] = [NSNumber numberWithUnsignedLongLong: expiresAt]; + + return fullPayload; +} + ++ (NSString*) payloadStringWithPayload: (NSDictionary*) payload issuer: (NSString*) issuer issuedAt: (unsigned long long) issuedAt expiresAt: (unsigned long long) expiresAt audience: (NSString*) audience +{ + NSDictionary *fullPayload = [self payloadWithPayload: payload issuer:issuer issuedAt:issuedAt expiresAt:expiresAt audience:audience]; + + NSError *encodingError = nil; + NSData *payloadData = [NSJSONSerialization dataWithJSONObject: fullPayload options: 0 error: &encodingError]; + if (encodingError != nil) { + return nil; + } + + return [[NSString alloc] initWithData: payloadData encoding: NSUTF8StringEncoding]; +} + ++ (NSDictionary*) certificatePayloadWithPublicKeyToSign: (PublicKey*) publicKey email: (NSString*) email +{ + NSDictionary *payload = @{ + @"principal": @{ + @"email": email + }, + @"public-key": [publicKey JSONRepresentation] + }; + + return payload; +} + ++ (NSString*) certificatePayloadStringWithPublicKeyToSign: (PublicKey*) publicKeyToSign email: (NSString*) email +{ + NSDictionary *payloadObject = [self certificatePayloadWithPublicKeyToSign: publicKeyToSign email: email]; + + NSError *encodingError = nil; + NSData *payload = [NSJSONSerialization dataWithJSONObject: payloadObject options: 0 error: &encodingError]; + if (encodingError != nil) { + return nil; + } + + return [[NSString alloc] initWithData: payload encoding:NSUTF8StringEncoding]; +} + ++ (NSString*) certificateWithPublicKeyToSign: (PublicKey*) publicKeyToSign email: (NSString*) email issuer: (NSString*) issuer issuedAt: (unsigned long long) issuedAt expiresAt: (unsigned long long) expiresAt signingPrivateKey: (PrivateKey*) signingPrivateKey +{ + NSDictionary *certificatePayload = [JSONWebTokenUtils certificatePayloadWithPublicKeyToSign: publicKeyToSign email: email]; + NSString *payloadString = [JSONWebTokenUtils payloadStringWithPayload: certificatePayload issuer: issuer issuedAt: issuedAt expiresAt:expiresAt audience: nil]; + return [self encodePayloadString: payloadString withPrivateKeyToSign: signingPrivateKey]; +} + ++ (NSString*) createAssertionWithPrivateKeyToSignWith: (PrivateKey*) privateKeyToSignWith certificate: (NSString*) certificate audience: (NSString*) audience issuer: (NSString*) issuer issuedAt: (unsigned long long) issuedAt duration: (unsigned long long) duration +{ + unsigned long long expiresAt = issuedAt + duration; + NSString *payloadString = [self payloadStringWithPayload: @{} issuer:issuer issuedAt:issuedAt expiresAt:expiresAt audience:audience]; + NSString *signature = [self encodePayloadString: payloadString withPrivateKeyToSign: privateKeyToSignWith]; + return [NSString stringWithFormat: @"%@~%@", certificate, signature]; +} + ++ (NSString*) createAssertionWithPrivateKeyToSignWith: (PrivateKey*) privateKeyToSignWith certificate: (NSString*) certificate audience: (NSString*) audience +{ + unsigned long long issuedAt = [[NSDate date] timeIntervalSince1970] * 1000; + unsigned long long duration = JSONWebTokenUtilsDefaultAssertionDuration; + return [self createAssertionWithPrivateKeyToSignWith: privateKeyToSignWith certificate: certificate audience: audience + issuer: JSONWebTokenUtilsDefaultAssertionIssuer issuedAt: issuedAt duration: duration]; +} + +@end diff --git a/mobile/ios/FxA/FxA/KeyPair.h b/mobile/ios/FxA/FxA/KeyPair.h new file mode 100644 index 0000000000..bb53155af7 --- /dev/null +++ b/mobile/ios/FxA/FxA/KeyPair.h @@ -0,0 +1,23 @@ +// 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 + + +@class PublicKey; +@class PrivateKey; + + +@interface KeyPair : NSObject + +@property (readonly) PublicKey *publicKey; +@property (readonly) PrivateKey *privateKey; + +- (instancetype) initWithPublicKey: (PublicKey*) publicKey privateKey: (PrivateKey*) privateKey; +- (instancetype) initWithJSONRepresentation: (NSDictionary*) object; + +- (NSDictionary*) JSONRepresentation; + +@end diff --git a/mobile/ios/FxA/FxA/KeyPair.m b/mobile/ios/FxA/FxA/KeyPair.m new file mode 100644 index 0000000000..a3d4eb4509 --- /dev/null +++ b/mobile/ios/FxA/FxA/KeyPair.m @@ -0,0 +1,37 @@ +// 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 "PublicKey.h" +#import "PrivateKey.h" +#import "KeyPair.h" + + +@implementation KeyPair + +- (instancetype) initWithPublicKey: (PublicKey*) publicKey privateKey: (PrivateKey*) privateKey +{ + if ((self = [super init]) != nil) { + _publicKey = publicKey; + _privateKey = privateKey; + } + return self; +} + +- (instancetype) initWithJSONRepresentation: (NSDictionary*) object +{ + if ((self = [super init]) != nil) { + } + return self; +} + +- (NSDictionary*) JSONRepresentation +{ + return @{ + @"publicKey": [[self publicKey] JSONRepresentation], + @"privateKey": [[self privateKey] JSONRepresentation] + }; +} + +@end diff --git a/mobile/ios/FxA/FxA/NSData+Base16.h b/mobile/ios/FxA/FxA/NSData+Base16.h new file mode 100644 index 0000000000..929cba649f --- /dev/null +++ b/mobile/ios/FxA/FxA/NSData+Base16.h @@ -0,0 +1,24 @@ +// 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 + + +typedef NS_OPTIONS(NSUInteger, NSDataBase16DecodingOptions) { + NSDataBase16DecodingOptionsDefault = 0 +}; + +typedef NS_OPTIONS(NSUInteger, NSDataBase16EncodingOptions) { + NSDataBase16EncodingOptionsDefault = 0, + NSDataBase16EncodingOptionsLowerCase = 1 +}; + + +@interface NSData (Base16) + +- (id) initWithBase16EncodedString:(NSString *)base16String options:(NSDataBase16DecodingOptions)options; +- (NSString *)base16EncodedStringWithOptions:(NSDataBase16EncodingOptions)options; + +@end diff --git a/mobile/ios/FxA/FxA/NSData+Base16.m b/mobile/ios/FxA/FxA/NSData+Base16.m new file mode 100644 index 0000000000..6486264696 --- /dev/null +++ b/mobile/ios/FxA/FxA/NSData+Base16.m @@ -0,0 +1,80 @@ +// 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 "NSData+Base16.h" + + +static const char kBase16AlphabetUpperCase[] = "0123456789ABCDEF"; +static const char kBase16AlphabetLowerCase[] = "0123456789abcdef"; + + +@implementation NSData (Base16) + +- (id) initWithBase16EncodedString:(NSString *)base16String options:(NSDataBase16DecodingOptions)options +{ + if ([base16String length] & 1) { + return nil; + } + + if ([base16String length] == 0) { + return [self init]; + } + + unsigned char *decoded = (unsigned char*) malloc([base16String length] / 2); + unsigned char *dst = decoded; + + for (NSUInteger i = 0; i < [base16String length]; i += 2) { + unichar h = [base16String characterAtIndex: i]; + unichar l = [base16String characterAtIndex: i+1]; + + if (isxdigit(h) == 0 || isxdigit(l) == 0) { + free(decoded); + return nil; + } + + if (isdigit(h)) { + *dst = (h - '0') << 4; + } else { + if (h >= 'a') { + *dst = (h - 'a' + 10) << 4; + } else { + *dst = (h - 'A' + 10) << 4; + } + } + + if (isdigit(l)) { + *dst |= (l - '0'); + } else { + if (l >= 'a') { + *dst |= (10 + (l - 'a')) & 0x0f; + } else { + *dst |= (10 + (l - 'A')) & 0x0f; + } + } + + dst++; + } + + return [self initWithBytesNoCopy: decoded length: [base16String length] / 2 freeWhenDone: YES]; +} + +- (NSString *)base16EncodedStringWithOptions:(NSDataBase16EncodingOptions)options +{ + char *encoded = (char*) malloc([self length] * 2); + + const char *src = [self bytes]; + char *dst = encoded; + + const char *alphabet = (options & NSDataBase16EncodingOptionsLowerCase) ? kBase16AlphabetLowerCase : kBase16AlphabetUpperCase; + + for (NSUInteger i = 0; i < [self length]; i++, src++) { + *dst++ = alphabet[(*src >> 4) & 0b00001111]; + *dst++ = alphabet[(*src >> 0) & 0b00001111]; + } + + return [[NSString alloc] initWithBytesNoCopy: encoded length: [self length] * 2 encoding: NSASCIIStringEncoding freeWhenDone: YES]; +} + +@end diff --git a/mobile/ios/FxA/FxA/NSData+Base32.h b/mobile/ios/FxA/FxA/NSData+Base32.h new file mode 100644 index 0000000000..75e180664e --- /dev/null +++ b/mobile/ios/FxA/FxA/NSData+Base32.h @@ -0,0 +1,25 @@ +// 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 + + +typedef NS_OPTIONS(NSUInteger, NSDataBase32DecodingOptions) { + NSDataBase32DecodingOptionsDefault = 0, + NSDataBase32DecodingOptionsUserFriendly = 1UL << 0 +}; + +typedef NS_OPTIONS(NSUInteger, NSDataBase32EncodingOptions) { + NSDataBase32EncodingOptionsDefault = 0, + NSDataBase32EncodingOptionsUserFriendly = 1UL << 0 +}; + + +@interface NSData (Base32) + +- (id) initWithBase32EncodedString:(NSString *)base32String options:(NSDataBase32DecodingOptions)options; +- (NSString *)base32EncodedStringWithOptions:(NSDataBase32EncodingOptions)options; + +@end diff --git a/mobile/ios/FxA/FxA/NSData+Base32.m b/mobile/ios/FxA/FxA/NSData+Base32.m new file mode 100644 index 0000000000..8e6688f443 --- /dev/null +++ b/mobile/ios/FxA/FxA/NSData+Base32.m @@ -0,0 +1,207 @@ +// 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 "NSData+Base32.h" + + +static const char kBase32Alphabet[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"; +static const NSUInteger kBase32BlockSize = 5; + + +static void *base32_decode(const char *buf, unsigned int *outlen); + + +@implementation NSData (Base32) + +- (id) initWithBase32EncodedString:(NSString *)base32String options:(NSDataBase32DecodingOptions)options +{ + NSData* result = nil; + + if (options & NSDataBase32DecodingOptionsUserFriendly) { + base32String = [[[base32String uppercaseString] + stringByReplacingOccurrencesOfString: @"8" withString: @"l"] + stringByReplacingOccurrencesOfString: @"9" withString: @"o"]; + } + + unsigned int length = 0; + void* bytes = base32_decode([base32String UTF8String], &length); + if (bytes != NULL) { + result = [self initWithBytesNoCopy: bytes length: length freeWhenDone: YES]; + } + + return result; +} + +- (NSString *)base32EncodedStringWithOptions:(NSDataBase32EncodingOptions)options +{ + NSMutableString *encoded = [NSMutableString new]; + + const unsigned char *bytes = [self bytes]; + NSUInteger bytesAvailable = [self length]; + + while (bytesAvailable > 0) + { + unsigned char blockData[kBase32BlockSize] = { 0, 0, 0, 0, 0 }; + NSUInteger blockSize = MIN(bytesAvailable, kBase32BlockSize); + + memcpy(blockData, bytes, blockSize); + + bytes += blockSize; + bytesAvailable -= blockSize; + + [encoded appendFormat: @"%c", kBase32Alphabet[((blockData[0] & 0b11111000) >> 3)]]; + [encoded appendFormat: @"%c", kBase32Alphabet[((blockData[0] & 0b00000111) << 2) | ((blockData[1] & 0b11000000) >> 6)]]; + [encoded appendFormat: @"%c", kBase32Alphabet[((blockData[1] & 0b00111110) >> 1)]]; + [encoded appendFormat: @"%c", kBase32Alphabet[((blockData[1] & 0b00000001) << 4) | ((blockData[2] & 0b11110000) >> 4)]]; + [encoded appendFormat: @"%c", kBase32Alphabet[((blockData[2] & 0b00001111) << 1) | ((blockData[3] & 0b10000000) >> 7)]]; + [encoded appendFormat: @"%c", kBase32Alphabet[((blockData[3] & 0b01111100) >> 2)]]; + [encoded appendFormat: @"%c", kBase32Alphabet[((blockData[3] & 0b00000011) << 3) | ((blockData[4] & 0b11100000) >> 5)]]; + [encoded appendFormat: @"%c", kBase32Alphabet[((blockData[4] & 0b00011111))]]; + } + + NSUInteger rest = [self length] % kBase32BlockSize; + if (rest != 0) + { + switch (rest) + { + case 1: + [encoded replaceCharactersInRange: NSMakeRange([encoded length] - 6, 6) withString: @"======"]; + break; + case 2: + [encoded replaceCharactersInRange: NSMakeRange([encoded length] - 4, 4) withString: @"===="]; + break; + case 3: + [encoded replaceCharactersInRange: NSMakeRange([encoded length] - 3, 3) withString: @"==="]; + break; + case 4: + [encoded replaceCharactersInRange: NSMakeRange([encoded length] - 1, 1) withString: @"="]; + break; + } + } + + if (options & NSDataBase32EncodingOptionsUserFriendly) { + return [[encoded stringByReplacingOccurrencesOfString: @"L" withString: @"8"] stringByReplacingOccurrencesOfString: @"O" withString: @"9"]; + } else { + return encoded; + } +} + +@end + + +#pragma mark - + + +// Public domain Base32 code taken from http://bitzi.com/publicdomain + +#define BASE32_LOOKUP_MAX 43 + +static unsigned char base32Lookup[BASE32_LOOKUP_MAX][2] = +{ + { '0', 0xFF }, + { '1', 0xFF }, + { '2', 0x1A }, + { '3', 0x1B }, + { '4', 0x1C }, + { '5', 0x1D }, + { '6', 0x1E }, + { '7', 0x1F }, + { '8', 0xFF }, + { '9', 0xFF }, + { ':', 0xFF }, + { ';', 0xFF }, + { '<', 0xFF }, + { '=', 0xFF }, + { '>', 0xFF }, + { '?', 0xFF }, + { '@', 0xFF }, + { 'A', 0x00 }, + { 'B', 0x01 }, + { 'C', 0x02 }, + { 'D', 0x03 }, + { 'E', 0x04 }, + { 'F', 0x05 }, + { 'G', 0x06 }, + { 'H', 0x07 }, + { 'I', 0x08 }, + { 'J', 0x09 }, + { 'K', 0x0A }, + { 'L', 0x0B }, + { 'M', 0x0C }, + { 'N', 0x0D }, + { 'O', 0x0E }, + { 'P', 0x0F }, + { 'Q', 0x10 }, + { 'R', 0x11 }, + { 'S', 0x12 }, + { 'T', 0x13 }, + { 'U', 0x14 }, + { 'V', 0x15 }, + { 'W', 0x16 }, + { 'X', 0x17 }, + { 'Y', 0x18 }, + { 'Z', 0x19 } +}; + +static int base32_decode_length(int base32Length) +{ + return ((base32Length * 5) / 8); +} + +static int base32_decode_into(const char *base32Buffer, unsigned int base32BufLen, void *_buffer) +{ + unsigned long max; + int i, index, lookup, offset; + unsigned char word; + unsigned char *buffer = _buffer; + + memset(buffer, 0, base32_decode_length(base32BufLen)); + max = strlen(base32Buffer); + for(i = 0, index = 0, offset = 0; i < max; i++) + { + lookup = toupper(base32Buffer[i]) - '0'; + /* Check to make sure that the given word falls inside + a valid range */ + if ( lookup < 0 && lookup >= BASE32_LOOKUP_MAX) + word = 0xFF; + else + word = base32Lookup[lookup][1]; + + /* If this word is not in the table, ignore it */ + if (word == 0xFF) + continue; + + if (index <= 3) + { + index = (index + 5) % 8; + if (index == 0) + { + buffer[offset] |= word; + offset++; + } + else + buffer[offset] |= word << (8 - index); + } + else + { + index = (index + 5) % 8; + buffer[offset] |= (word >> index); + offset++; + + buffer[offset] |= word << (8 - index); + } + } + return offset; +} + +static void *base32_decode(const char *buf, unsigned int *outlen) +{ + unsigned int len = strlen(buf); + char *tmp = malloc(base32_decode_length(len)); + unsigned int x = base32_decode_into(buf, len, tmp); + if(outlen) + *outlen = x; + return tmp; +} diff --git a/mobile/ios/FxA/FxA/NSData+KeyDerivation.h b/mobile/ios/FxA/FxA/NSData+KeyDerivation.h new file mode 100644 index 0000000000..1b2cee45e4 --- /dev/null +++ b/mobile/ios/FxA/FxA/NSData+KeyDerivation.h @@ -0,0 +1,13 @@ +// 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 + +@interface NSData (KeyDerivation) + +- (NSData*) deriveHKDFSHA256KeyWithSalt: (NSData*) salt contextInfo: (NSData*) contextInfo length: (NSUInteger) length; +- (NSData*) derivePBKDF2HMACSHA256KeyWithSalt: (NSData*) salt iterations: (NSUInteger) iterations length: (NSUInteger) length; +- (NSData*) deriveSCryptKeyWithSalt: (NSData*) salt n: (uint32_t) n r: (uint32_t) r p: (uint32_t) p length: (NSUInteger) length; + +@end diff --git a/mobile/ios/FxA/FxA/NSData+KeyDerivation.m b/mobile/ios/FxA/FxA/NSData+KeyDerivation.m new file mode 100644 index 0000000000..5ead5d7ba4 --- /dev/null +++ b/mobile/ios/FxA/FxA/NSData+KeyDerivation.m @@ -0,0 +1,292 @@ +// 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 "openssl/evp.h" +#import "openssl/sha.h" + +#import "NSData+SHA.h" +#import "NSData+Utils.h" +#import "NSData+KeyDerivation.h" + +#pragma mark - Low Level SCrypt Functions + + + + +//#define WORDS_BIGENDIAN 1 +#define BLOCK_WORDS 16 + +static inline uint32_t +ROTL(uint32_t x, int n) +{ + return (x << n) | (x >> (32 - n)); +} + +#ifdef WORDS_BIGENDIAN +static inline uint32_t +ROTR(uint32_t x, int n) +{ + return (x >> n) | (x << (32 - n)); +} + +static inline uint32_t +BYTESWAP(uint32_t x) +{ + return (ROTR(x, 8) & 0xff00ff00) | + (ROTL(x, 8) & 0x00ff00ff); +} +#endif + +static void +salsa20_8_core (uint32_t out[BLOCK_WORDS], const uint32_t in[BLOCK_WORDS]) +{ + const uint32_t *x; + uint32_t *z; + int i; + + memcpy(out, in, sizeof(*out) * BLOCK_WORDS); + + z = out; + for (i = 8 - 1; i >= 0; i -= 2) { + z[ 4] ^= ROTL(z[ 0]+z[12], 7); + z[ 8] ^= ROTL(z[ 4]+z[ 0], 9); + z[12] ^= ROTL(z[ 8]+z[ 4],13); + z[ 0] ^= ROTL(z[12]+z[ 8],18); + z[ 9] ^= ROTL(z[ 5]+z[ 1], 7); + z[13] ^= ROTL(z[ 9]+z[ 5], 9); + z[ 1] ^= ROTL(z[13]+z[ 9],13); + z[ 5] ^= ROTL(z[ 1]+z[13],18); + z[14] ^= ROTL(z[10]+z[ 6], 7); + z[ 2] ^= ROTL(z[14]+z[10], 9); + z[ 6] ^= ROTL(z[ 2]+z[14],13); + z[10] ^= ROTL(z[ 6]+z[ 2],18); + z[ 3] ^= ROTL(z[15]+z[11], 7); + z[ 7] ^= ROTL(z[ 3]+z[15], 9); + z[11] ^= ROTL(z[ 7]+z[ 3],13); + z[15] ^= ROTL(z[11]+z[ 7],18); + z[ 1] ^= ROTL(z[ 0]+z[ 3], 7); + z[ 2] ^= ROTL(z[ 1]+z[ 0], 9); + z[ 3] ^= ROTL(z[ 2]+z[ 1],13); + z[ 0] ^= ROTL(z[ 3]+z[ 2],18); + z[ 6] ^= ROTL(z[ 5]+z[ 4], 7); + z[ 7] ^= ROTL(z[ 6]+z[ 5], 9); + z[ 4] ^= ROTL(z[ 7]+z[ 6],13); + z[ 5] ^= ROTL(z[ 4]+z[ 7],18); + z[11] ^= ROTL(z[10]+z[ 9], 7); + z[ 8] ^= ROTL(z[11]+z[10], 9); + z[ 9] ^= ROTL(z[ 8]+z[11],13); + z[10] ^= ROTL(z[ 9]+z[ 8],18); + z[12] ^= ROTL(z[15]+z[14], 7); + z[13] ^= ROTL(z[12]+z[15], 9); + z[14] ^= ROTL(z[13]+z[12],13); + z[15] ^= ROTL(z[14]+z[13],18); + } + + x = in; + for (i = BLOCK_WORDS - 1; i >= 0; i--) { + *(z++) += *(x++); + } +} + +static void +blockmix_salsa20_8_core (uint32_t out[/* (2 * r * BLOCK_WORDS) */], + const uint32_t in[/* (2 * r * BLOCK_WORDS) */], + unsigned int r) +{ + uint32_t *even, *odd; + uint32_t *x; + uint32_t tmp[BLOCK_WORDS]; + unsigned int i, j; + + even = out; + odd = &out[r * BLOCK_WORDS]; + + memcpy(tmp, &in[(2 * r - 1) * BLOCK_WORDS], sizeof(tmp)); + + for (i = r; i > 0; i--) { + x = tmp; + for (j = BLOCK_WORDS; j > 0; j--) { + *(x++) ^= *(in++); + } + salsa20_8_core(even, tmp); + memcpy(tmp, even, sizeof(tmp)); + even += BLOCK_WORDS; + + x = tmp; + for (j = BLOCK_WORDS; j > 0; j--) { + *(x++) ^= *(in++); + } + salsa20_8_core(odd, tmp); + memcpy(tmp, odd, sizeof(tmp)); + odd += BLOCK_WORDS; + } +} + +static void +smix (void *out /* (sizeof(uint32_t) * 2 * r * BLOCK_WORDS) */, + const void *in /* (sizeof(uint32_t) * 2 * r * BLOCK_WORDS) */, + unsigned int N, unsigned int r, + uint32_t tmp[/* (2 * r * BLOCK_WORDS * (N + 2)) */]) +{ + uint32_t *v; + uint32_t *X, *T, *x, *t; + uint32_t j; + unsigned int i, k; + + memcpy(tmp, in, sizeof(*tmp) * 2 * r * BLOCK_WORDS); + +#ifdef WORDS_BIGENDIAN + v = tmp; + for (i = 2 * r * BLOCK_WORDS; i > 0; i--) { + *v = BYTESWAP(*v); + v++; + } +#endif /* WORDS_BIGENDIAN */ + + v = tmp; + for (i = N; i > 0; i--) { + blockmix_salsa20_8_core(v + 2 * r * BLOCK_WORDS, v, r); + v += 2 * r * BLOCK_WORDS; + } + X = v; + T = &X[2 * r * BLOCK_WORDS]; + + for (i = N; i > 0; i--) { + j = X[(2 * r - 1) * BLOCK_WORDS] % N; + + x = X; + v = &tmp[2 * r * BLOCK_WORDS * j]; + for (k = 2 * r * BLOCK_WORDS; k > 0; k--) { + *(x++) ^= *(v++); + } + + blockmix_salsa20_8_core(T, X, r); + + /* swap X & T */ + t = T; + T = X; + X = t; + } + +#ifdef WORDS_BIGENDIAN + x = X; + for (i = 2 * r * BLOCK_WORDS; i > 0; i--) { + *x = BYTESWAP(*x); + x++; + } +#endif /* WORDS_BIGENDIAN */ + + memcpy (out, X, sizeof(*X) * 2 * r * BLOCK_WORDS); +} + +int +scrypt (const void *password, size_t passwordLen, + const void *salt, size_t saltLen, + unsigned int N, unsigned int r, unsigned int p, + uint8_t *derivedKey, size_t dkLen) +{ + size_t MFLen = sizeof(uint32_t) * 2 * r * BLOCK_WORDS; + uint8_t *B, *b; + uint32_t *tmp; + unsigned int i; + + if ((B = malloc(p * MFLen)) == NULL) + return -1; + + if (PKCS5_PBKDF2_HMAC(password, passwordLen, salt, saltLen, 1, EVP_sha256(), p * MFLen, B)) { + //return -1; +} + + if ((tmp = malloc(sizeof(*tmp) * 2 * r * BLOCK_WORDS * (N + 2))) == NULL) { + free(B); + return -1; + } + + b = B; + for (i = p; i > 0; i--) { + smix(b, b, N, r, tmp); + b += MFLen; + } + + if (PKCS5_PBKDF2_HMAC(password, passwordLen, B, p * MFLen, 1, EVP_sha256(), dkLen, derivedKey)) { + //return -1; + } + + free(tmp); + free(B); + return 0; +} + + + + + + +@implementation NSData (KeyDerivation) + +- (NSData*) deriveHKDFSHA256KeyWithSalt: (NSData*) salt contextInfo: (NSData*) contextInfo length: (NSUInteger) length +{ + // If salt is not specified then we use a zero salt + + if (salt == nil) { + salt = [NSMutableData dataWithLength: SHA256_DIGEST_LENGTH]; + } + + // Extract + + NSData *prk = [self HMACSHA256WithKey: salt]; + + // Expand + + NSUInteger iterations = (length + SHA256_DIGEST_LENGTH - 1) / SHA256_DIGEST_LENGTH; + + NSMutableData* tr = [NSMutableData dataWithCapacity: (iterations * SHA256_DIGEST_LENGTH)]; + + NSData* tn = [NSData data]; + for (NSUInteger i = 1; i <= iterations; i++) { + unsigned char n = i; + tn = [[NSData dataByAppendingDatas: @[tn, contextInfo, [NSData dataWithBytes: &n length: 1]]] HMACSHA256WithKey: prk]; + [tr appendData: tn]; + } + + return [tr subdataWithRange: NSMakeRange(0, length)]; +} + +- (NSData*) derivePBKDF2HMACSHA256KeyWithSalt: (NSData*) salt iterations: (NSUInteger) iterations length: (NSUInteger) length +{ + unsigned char *key = malloc(length); + if (key == NULL) { + return nil; + } + + PKCS5_PBKDF2_HMAC( + [self bytes], + [self length], + [salt bytes], + [salt length], + iterations, + EVP_sha256(), + length, + key + ); + + return [NSData dataWithBytesNoCopy: key length: length freeWhenDone: YES]; +} + +- (NSData*) deriveSCryptKeyWithSalt: (NSData*) salt n: (uint32_t) n r: (uint32_t) r p: (uint32_t) p length: (NSUInteger) length +{ + uint8_t *buffer = (uint8_t*) malloc(length); + if (buffer == NULL) { + return nil; + } + + if (scrypt([self bytes], [self length], [salt bytes], [salt length], n, r, p, buffer, length) != 0) { + free(buffer); + return nil; + } + + return [NSData dataWithBytesNoCopy: buffer length:length freeWhenDone:YES]; +} + +@end diff --git a/mobile/ios/FxA/FxA/NSData+SHA.h b/mobile/ios/FxA/FxA/NSData+SHA.h new file mode 100644 index 0000000000..13d359afda --- /dev/null +++ b/mobile/ios/FxA/FxA/NSData+SHA.h @@ -0,0 +1,15 @@ +// 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 + + +@interface NSData (SHA) + +- (NSData*) SHA1Hash; +- (NSData*) SHA256Hash; +- (NSData*) HMACSHA256WithKey: (NSData*) key; + +@end \ No newline at end of file diff --git a/mobile/ios/FxA/FxA/NSData+SHA.m b/mobile/ios/FxA/FxA/NSData+SHA.m new file mode 100644 index 0000000000..389292fc6a --- /dev/null +++ b/mobile/ios/FxA/FxA/NSData+SHA.m @@ -0,0 +1,41 @@ +// 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 +#import +#import "NSData+SHA.h" + + +@implementation NSData (SHA) + +// TODO: Needs a test +- (NSData*) SHA1Hash +{ + unsigned char md[CC_SHA1_DIGEST_LENGTH]; + CC_SHA1([self bytes], [self length], md); + return [NSData dataWithBytes: md length: CC_SHA1_DIGEST_LENGTH]; +} + +- (NSData*) SHA256Hash +{ + unsigned char md[CC_SHA256_DIGEST_LENGTH]; + CC_SHA256([self bytes], [self length], md); + return [NSData dataWithBytes: md length: CC_SHA256_DIGEST_LENGTH]; +} + +- (NSData*) HMACSHA256WithKey: (NSData*) key +{ + // This is kind of odd, but we do this because [NSData dataWithBytes "" length: 0] results in an object that is invalid. + + if ([key length] == 0) { + key = [NSMutableData data]; + } + + unsigned char mac[CC_SHA256_DIGEST_LENGTH]; + CCHmac(kCCHmacAlgSHA256, [key bytes], [key length], [self bytes], [self length], mac); + return [NSData dataWithBytes: mac length: CC_SHA256_DIGEST_LENGTH]; +} + +@end diff --git a/mobile/ios/FxA/FxA/NSData+Utils.h b/mobile/ios/FxA/FxA/NSData+Utils.h new file mode 100644 index 0000000000..e6d960ff5d --- /dev/null +++ b/mobile/ios/FxA/FxA/NSData+Utils.h @@ -0,0 +1,23 @@ +// 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 + + +@interface NSData (Utils) + ++ (id) dataByAppendingDatas: (NSArray*) datas; + +- (id) dataLeftZeroPaddedToLength: (NSUInteger) length; +- (id) dataRightZeroPaddedToLength: (NSUInteger) length; + +- (id) exclusiveOrWithKey: (NSData*) key; + ++ (id) randomDataWithLength: (NSUInteger) length; + +- (NSString*) base64URLEncodedStringWithOptions: (NSDataBase64EncodingOptions) options; +- (instancetype) initWithBase64URLEncodedString:(NSString *)base64String options:(NSDataBase64DecodingOptions)options; + +@end diff --git a/mobile/ios/FxA/FxA/NSData+Utils.m b/mobile/ios/FxA/FxA/NSData+Utils.m new file mode 100644 index 0000000000..96733515f6 --- /dev/null +++ b/mobile/ios/FxA/FxA/NSData+Utils.m @@ -0,0 +1,112 @@ +// 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 +#import "NSData+Utils.h" + + +@implementation NSData (Utils) + ++ (id) dataByAppendingDatas: (NSArray*) datas +{ + NSMutableData *result = [NSMutableData data]; + for (NSData *data in datas) { + [result appendData: data]; + } + return result; +} + +- (id) dataLeftZeroPaddedToLength: (NSUInteger) length +{ + if ([self length] < length) { + return [NSData dataByAppendingDatas: @[[NSMutableData dataWithLength: (length - [self length])], self]]; + } else { + return self; + } +} + +- (id) dataRightZeroPaddedToLength: (NSUInteger) length +{ + if ([self length] < length) { + return [NSData dataByAppendingDatas: @[self, [NSMutableData dataWithLength: (length - [self length])]]]; + } else { + return self; + } +} + +- (id) exclusiveOrWithKey: (NSData*) key +{ + if ([self length] != [key length]) { + return nil; + } + + unsigned char *o = (unsigned char*) malloc([self length]); + unsigned char *p = (unsigned char*) [self bytes]; + unsigned char *q = (unsigned char*) [key bytes]; + + for (NSUInteger i = 0; i < [self length]; i++) { + o[i] = p[i] ^ q[i]; + } + + return [NSData dataWithBytesNoCopy: o length: [self length] freeWhenDone: YES]; +} + ++ (id) randomDataWithLength: (NSUInteger) length +{ + void *bytes = malloc(length); + if (bytes == NULL) { + return nil; + } + + if (SecRandomCopyBytes(kSecRandomDefault, length, bytes) != 0) { + free(bytes); + return nil; + } + + return [NSData dataWithBytesNoCopy: bytes length:length freeWhenDone: YES]; +} + +- (NSString*) base64URLEncodedStringWithOptions: (NSDataBase64EncodingOptions) options +{ + // This is not awesome but it works. + NSMutableString *encodedString = [NSMutableString stringWithString: [self base64EncodedStringWithOptions: options]]; + if (encodedString != nil) + { + [encodedString replaceOccurrencesOfString: @"+" withString: @"-" options:NSLiteralSearch range: NSMakeRange(0, [encodedString length])]; + [encodedString replaceOccurrencesOfString: @"/" withString: @"_" options:NSLiteralSearch range: NSMakeRange(0, [encodedString length])]; + + if ([encodedString hasSuffix: @"=="]) { + return [encodedString substringToIndex: [encodedString length] - 2]; + } + + if ([encodedString hasSuffix: @"="]) { + return [encodedString substringToIndex: [encodedString length] - 1]; + } + } + return encodedString; +} + +- (instancetype) initWithBase64URLEncodedString:(NSString *)base64String options:(NSDataBase64DecodingOptions)options +{ + NSMutableString *encodedString = [NSMutableString stringWithString: base64String]; + if (encodedString != nil) + { + [encodedString replaceOccurrencesOfString: @"-" withString: @"+" options:NSLiteralSearch range: NSMakeRange(0, [encodedString length])]; + [encodedString replaceOccurrencesOfString: @"_" withString: @"/" options:NSLiteralSearch range: NSMakeRange(0, [encodedString length])]; + + switch ([encodedString length] % 4) { + case 2: + [encodedString appendString: @"=="]; + break; + case 3: + [encodedString appendString: @"="]; + break; + } + } + + return [self initWithBase64EncodedString: encodedString options: options]; +} + +@end diff --git a/mobile/ios/FxA/FxA/NSString+Utils.h b/mobile/ios/FxA/FxA/NSString+Utils.h new file mode 100644 index 0000000000..e858dd90de --- /dev/null +++ b/mobile/ios/FxA/FxA/NSString+Utils.h @@ -0,0 +1,11 @@ +// 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 + +@interface NSString (Utils) + ++ (NSString*) randomAlphanumericStringWithLength: (NSUInteger) length; + +@end diff --git a/mobile/ios/FxA/FxA/NSString+Utils.m b/mobile/ios/FxA/FxA/NSString+Utils.m new file mode 100644 index 0000000000..a6831ca370 --- /dev/null +++ b/mobile/ios/FxA/FxA/NSString+Utils.m @@ -0,0 +1,26 @@ +// 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 "NSString+Utils.h" + + +@implementation NSString (Utils) + ++ (NSString*) randomAlphanumericStringWithLength: (NSUInteger) length +{ + sranddev(); + + static char alphanumeric[] = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; + + NSMutableString *s = [NSMutableString string]; + + for (NSUInteger i = 0; i < length; i++) { + [s appendString: [NSString stringWithFormat: @"%c", alphanumeric[rand() % (sizeof alphanumeric - 1)]]]; + } + + return s; +} + +@end diff --git a/mobile/ios/FxA/FxA/PrivateKey.h b/mobile/ios/FxA/FxA/PrivateKey.h new file mode 100644 index 0000000000..460047bb79 --- /dev/null +++ b/mobile/ios/FxA/FxA/PrivateKey.h @@ -0,0 +1,20 @@ +// 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 + + +@interface PrivateKey : NSObject + +- (id) initWithJSONRepresentation: (NSDictionary*) object; + +- (NSDictionary*) JSONRepresentation; + +- (NSString*) algorithm; + +- (NSData*) signMessageString: (NSString*) string encoding: (NSStringEncoding) encoding; +- (NSData*) signMessage: (NSData*) data; + +@end diff --git a/mobile/ios/FxA/FxA/PrivateKey.m b/mobile/ios/FxA/FxA/PrivateKey.m new file mode 100644 index 0000000000..522a8bd373 --- /dev/null +++ b/mobile/ios/FxA/FxA/PrivateKey.m @@ -0,0 +1,41 @@ +// 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 "PrivateKey.h" + + +@implementation PrivateKey + +- (id) initWithJSONRepresentation: (NSDictionary*) object +{ + [self doesNotRecognizeSelector: _cmd]; + return nil; +} + +- (NSDictionary*) JSONRepresentation +{ + [self doesNotRecognizeSelector: _cmd]; + return nil; +} + +- (NSString*) algorithm +{ + [self doesNotRecognizeSelector: _cmd]; + return nil; +} + +- (NSData*) signMessageString: (NSString*) string encoding: (NSStringEncoding) encoding +{ + [self doesNotRecognizeSelector: _cmd]; + return nil; +} + +- (NSData*) signMessage: (NSData*) data +{ + [self doesNotRecognizeSelector: _cmd]; + return nil; +} + +@end diff --git a/mobile/ios/FxA/FxA/PublicKey.h b/mobile/ios/FxA/FxA/PublicKey.h new file mode 100644 index 0000000000..5cc23d8305 --- /dev/null +++ b/mobile/ios/FxA/FxA/PublicKey.h @@ -0,0 +1,20 @@ +// 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 + + +@interface PublicKey : NSObject + +- (id) initWithJSONRepresentation: (NSDictionary*) object; + +- (NSDictionary*) JSONRepresentation; + +- (NSString*) algorithm; + +- (BOOL) verifySignature: (NSData*) signature againstMessage: (NSData*) message; +- (BOOL) verifySignature: (NSData*) signature againstMessageString: (NSString*) message encoding: (NSStringEncoding) encoding; + +@end diff --git a/mobile/ios/FxA/FxA/PublicKey.m b/mobile/ios/FxA/FxA/PublicKey.m new file mode 100644 index 0000000000..b7eb7132da --- /dev/null +++ b/mobile/ios/FxA/FxA/PublicKey.m @@ -0,0 +1,41 @@ +// 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 "PublicKey.h" + + +@implementation PublicKey + +- (id) initWithJSONRepresentation: (NSDictionary*) object +{ + [self doesNotRecognizeSelector: _cmd]; + return nil; +} + +- (NSDictionary*) JSONRepresentation +{ + [self doesNotRecognizeSelector: _cmd]; + return nil; +} + +- (NSString*) algorithm +{ + [self doesNotRecognizeSelector: _cmd]; + return nil; +} + +- (BOOL) verifySignature: (NSData*) signature againstMessage: (NSData*) message +{ + [self doesNotRecognizeSelector: _cmd]; + return FALSE; +} + +- (BOOL) verifySignature: (NSData*) signature againstMessageString: (NSString*) message encoding: (NSStringEncoding) encoding +{ + [self doesNotRecognizeSelector: _cmd]; + return FALSE; +} + +@end diff --git a/mobile/ios/FxA/FxA/RSAKeyPair.h b/mobile/ios/FxA/FxA/RSAKeyPair.h new file mode 100644 index 0000000000..5abcf6d365 --- /dev/null +++ b/mobile/ios/FxA/FxA/RSAKeyPair.h @@ -0,0 +1,34 @@ +// 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 "KeyPair.h" +#import "PrivateKey.h" +#import "PublicKey.h" +#import + + +@class CHNumber; +@class RSAKeyPair; + + +@interface RSAPrivateKey : PrivateKey +- (id) initWithModulus: (CHNumber*) n privateExponent: (CHNumber*) d; +@end + + +@interface RSAPublicKey : PublicKey +- (id) initWithModulus: (CHNumber*) n publicExponent: (CHNumber*) e; +@end + + +@interface RSAKeyPair : KeyPair + ++ (instancetype) generateKeyPairWithModulusSize: (int) size; + +- (instancetype) initWithModulus: (CHNumber*) n privateExponent: (CHNumber*) d publicExponent: (CHNumber*) e; + +@property (readonly) RSAPublicKey *publicKey; +@property (readonly) RSAPrivateKey *privateKey; + +@end diff --git a/mobile/ios/FxA/FxA/RSAKeyPair.m b/mobile/ios/FxA/FxA/RSAKeyPair.m new file mode 100644 index 0000000000..9e9831b6e8 --- /dev/null +++ b/mobile/ios/FxA/FxA/RSAKeyPair.m @@ -0,0 +1,297 @@ +// 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/ + + +#include +#include + +#include "CHNumber.h" +#import "RSAKeyPair.h" + + +@implementation RSAPrivateKey { + RSA *_rsa; +} + +- (id) initWithJSONRepresentation: (NSDictionary*) object +{ + CHNumber *n = [CHNumber numberWithString: object[@"n"]]; + CHNumber *d = [CHNumber numberWithString: object[@"d"]]; + if (n == nil || d == nil) { + return nil; + } + + return [self initWithModulus: n privateExponent: d]; +} + +- (id) initWithModulus: (CHNumber*) n privateExponent: (CHNumber*) d +{ + if ((self = [super init]) != nil) { + _rsa = RSA_new(); + BIGNUM* _e = BN_new(); + BIGNUM* _n = BN_dup([n bigNumValue]); + BIGNUM* _d = BN_dup([d bigNumValue]); + + if (_n == NULL || _e == NULL || _d == NULL) { + BN_free(_e); + BN_free(_n); + BN_free(_d); + return nil; + } + + if (BN_set_word(_e, RSA_F4) != 1) { + BN_free(_e); + BN_free(_n); + BN_free(_d); + return nil; + } + + if (RSA_set0_key(_rsa, _n, _e, _d) != 1) { + BN_free(_e); + BN_free(_n); + BN_free(_d); + return nil; + } + // Maybe there is some secret communication between a Java KeyPair where the Private Key can discover e from the associated Public Key? +// _rsa->p = BN_dup([p bigNumValue]); +// _rsa->q = BN_dup([q bigNumValue]); + } + return self; +} + +- (void) dealloc +{ + RSA_free(_rsa); + _rsa = NULL; +} + +- (NSDictionary*) JSONRepresentation +{ + BIGNUM *n, *d; + RSA_get0_key(_rsa, &n, NULL, &d); + if (n == NULL || d == NULL) { + return nil; + } + return @{ + @"algorithm": @"RS", + @"n": [[CHNumber numberWithOpenSSLNumber: n] stringValue], + @"d": [[CHNumber numberWithOpenSSLNumber: d] stringValue] + }; +} + +- (NSString*) algorithm +{ + BIGNUM *n; + RSA_get0_key(_rsa, &n, NULL, NULL); + if (n == NULL) { + return nil; + } + int sizeInBytes = (BN_num_bits(n) + 7)/8; + return [NSString stringWithFormat: @"RS%d", sizeInBytes]; +} + +- (NSData*) signMessageString: (NSString*) string encoding: (NSStringEncoding) encoding +{ + return [self signMessage: [string dataUsingEncoding: encoding]]; +} + +- (NSData*) signMessage: (NSData*) data +{ + NSData *signature = nil; + + EVP_PKEY *pkey = EVP_PKEY_new(); + if (pkey != NULL) { + if (EVP_PKEY_set1_RSA(pkey, _rsa)) { + EVP_MD_CTX *ctx = EVP_MD_CTX_create(); + if (ctx != NULL) { + if (EVP_SignInit_ex(ctx, EVP_sha256(), NULL)) { + if (EVP_SignUpdate(ctx, [data bytes], [data length])) { + unsigned int sig_size; + unsigned char *sig_data = malloc(EVP_PKEY_size(pkey)); + if (sig_data != NULL) { + if (EVP_SignFinal(ctx, sig_data, &sig_size, pkey)) { + signature = [NSData dataWithBytesNoCopy: sig_data length: sig_size freeWhenDone: YES]; + } + } + } + } + EVP_MD_CTX_destroy(ctx); + } + } + EVP_PKEY_free(pkey); + } + + return signature; +} + +@end + + +@implementation RSAPublicKey { + RSA *_rsa; +} + +- (id) initWithJSONRepresentation: (NSDictionary*) object +{ + CHNumber *n = [CHNumber numberWithString: object[@"n"]]; + CHNumber *e = [CHNumber numberWithString: object[@"e"]]; + if (n == nil || e == nil) { + return nil; + } + + return [self initWithModulus: n publicExponent: e]; +} + + +- (id) initWithModulus: (CHNumber*) n publicExponent: (CHNumber*) e; +{ + if ((self = [super init]) != nil) { + BIGNUM *_n = BN_dup([n bigNumValue]); + BIGNUM *_e = BN_dup([e bigNumValue]); + + if (_n == NULL || _e == NULL) { + BN_free(_n); + BN_free(_e); + return nil; + } + _rsa = RSA_new(); + if (RSA_set0_key(_rsa, _n, _e, NULL) != 1) { + BN_free(_n); + BN_free(_e); + return nil; + } + } + return self; +} + +- (void) dealloc +{ + RSA_free(_rsa); + _rsa = NULL; +} + +- (NSDictionary*) JSONRepresentation +{ + BIGNUM *n, *e; + RSA_get0_key(_rsa, &n, &e, NULL); + if (n == NULL || e == NULL) { + return nil; + } + return @{ + @"algorithm": @"RS", + @"n": [[CHNumber numberWithOpenSSLNumber: n] stringValue], + @"e": [[CHNumber numberWithOpenSSLNumber: e] stringValue] + }; +} + +- (NSString*) algorithm +{ + BIGNUM *n; + RSA_get0_key(_rsa, &n, NULL, NULL); + if (n == NULL) { + return nil; + } + int sizeInBytes = (BN_num_bits(n) + 7)/8; + return [NSString stringWithFormat: @"RS%d", sizeInBytes]; +} + +- (BOOL) verifySignature: (NSData*) signature againstMessage: (NSData*) message +{ + BOOL verified = NO; + + EVP_PKEY *pkey = EVP_PKEY_new(); + if (pkey != NULL) { + if (EVP_PKEY_set1_RSA(pkey, _rsa)) { + EVP_MD_CTX *ctx = EVP_MD_CTX_create(); + if (ctx != NULL) { + if (EVP_VerifyInit_ex(ctx, EVP_sha256(), NULL)) { + if (EVP_VerifyUpdate(ctx, [message bytes], [message length])) { + if (EVP_VerifyFinal(ctx, [signature bytes], [signature length], pkey)) { + verified = YES; + } + } + } + EVP_MD_CTX_destroy(ctx); + } + } + EVP_PKEY_free(pkey); + } + + return verified; +} + +- (BOOL) verifySignature: (NSData*) signature againstMessageString: (NSString*) message encoding: (NSStringEncoding) encoding +{ + return [self verifySignature: signature againstMessage: [message dataUsingEncoding: encoding]]; +} + +@end + + +@implementation RSAKeyPair + ++ (instancetype) generateKeyPairWithModulusSize: (int) modulusSize +{ + RSA* rsa = RSA_new(); + if (rsa == NULL) { + return nil; + } + + BIGNUM* exp = BN_new(); + BN_set_word(exp, RSA_F4); + if (!RSA_generate_key_ex(rsa, modulusSize, exp, NULL)) { + RSA_free(rsa); + BN_free(exp); + return nil; + } + BIGNUM *n, *e, *d; + RSA_get0_key(rsa, &n, &e, &d); + + if (n == NULL || e == NULL || d == NULL) { + RSA_free(rsa); + BN_free(exp); + return nil; + } + + RSAPublicKey *publicKey = [[RSAPublicKey alloc] initWithModulus: [CHNumber numberWithOpenSSLNumber: n] + publicExponent: [CHNumber numberWithOpenSSLNumber: e]]; + RSAPrivateKey *privateKey = [[RSAPrivateKey alloc] initWithModulus: [CHNumber numberWithOpenSSLNumber: n] + privateExponent: [CHNumber numberWithOpenSSLNumber: d]]; + + RSA_free(rsa); + + return [[self alloc] initWithPublicKey: publicKey privateKey: privateKey]; +} + +- (instancetype) initWithModulus: (CHNumber*) n privateExponent: (CHNumber*) d publicExponent: (CHNumber*) e +{ + RSAPrivateKey *privateKey = [[RSAPrivateKey alloc] initWithModulus: n privateExponent: d]; + if (privateKey == nil) { + return nil; + } + + RSAPublicKey *publicKey = [[RSAPublicKey alloc] initWithModulus: n publicExponent: e]; + if (privateKey == nil) { + return nil; + } + + return [self initWithPublicKey: publicKey privateKey: privateKey]; +} + +- (instancetype) initWithJSONRepresentation: (NSDictionary*) object +{ + RSAPublicKey *publicKey = [[RSAPublicKey alloc] initWithJSONRepresentation: object[@"publicKey"]]; + if (publicKey == nil) { + return nil; + } + + RSAPrivateKey *privateKey = [[RSAPrivateKey alloc] initWithJSONRepresentation: object[@"privateKey"]]; + if (privateKey == nil) { + return nil; + } + + return [self initWithPublicKey: publicKey privateKey: privateKey]; +} + +@end diff --git a/mobile/ios/FxA/FxA/include/openssl/aes.h b/mobile/ios/FxA/FxA/include/openssl/aes.h new file mode 100644 index 0000000000..245c552abd --- /dev/null +++ b/mobile/ios/FxA/FxA/include/openssl/aes.h @@ -0,0 +1,92 @@ +/* + * Copyright 2002-2016 The OpenSSL Project Authors. All Rights Reserved. + * + * Licensed under the OpenSSL license (the "License"). You may not use + * this file except in compliance with the License. You can obtain a copy + * in the file LICENSE in the source distribution or at + * https://www.openssl.org/source/license.html + */ + +#ifndef HEADER_AES_H +# define HEADER_AES_H + +# include + +# include +# ifdef __cplusplus +extern "C" { +# endif + +# define AES_ENCRYPT 1 +# define AES_DECRYPT 0 + +/* + * Because array size can't be a const in C, the following two are macros. + * Both sizes are in bytes. + */ +# define AES_MAXNR 14 +# define AES_BLOCK_SIZE 16 + +/* This should be a hidden type, but EVP requires that the size be known */ +struct aes_key_st { +# ifdef AES_LONG + unsigned long rd_key[4 * (AES_MAXNR + 1)]; +# else + unsigned int rd_key[4 * (AES_MAXNR + 1)]; +# endif + int rounds; +}; +typedef struct aes_key_st AES_KEY; + +const char *AES_options(void); + +int AES_set_encrypt_key(const unsigned char *userKey, const int bits, + AES_KEY *key); +int AES_set_decrypt_key(const unsigned char *userKey, const int bits, + AES_KEY *key); + +void AES_encrypt(const unsigned char *in, unsigned char *out, + const AES_KEY *key); +void AES_decrypt(const unsigned char *in, unsigned char *out, + const AES_KEY *key); + +void AES_ecb_encrypt(const unsigned char *in, unsigned char *out, + const AES_KEY *key, const int enc); +void AES_cbc_encrypt(const unsigned char *in, unsigned char *out, + size_t length, const AES_KEY *key, + unsigned char *ivec, const int enc); +void AES_cfb128_encrypt(const unsigned char *in, unsigned char *out, + size_t length, const AES_KEY *key, + unsigned char *ivec, int *num, const int enc); +void AES_cfb1_encrypt(const unsigned char *in, unsigned char *out, + size_t length, const AES_KEY *key, + unsigned char *ivec, int *num, const int enc); +void AES_cfb8_encrypt(const unsigned char *in, unsigned char *out, + size_t length, const AES_KEY *key, + unsigned char *ivec, int *num, const int enc); +void AES_ofb128_encrypt(const unsigned char *in, unsigned char *out, + size_t length, const AES_KEY *key, + unsigned char *ivec, int *num); +/* NB: the IV is _two_ blocks long */ +void AES_ige_encrypt(const unsigned char *in, unsigned char *out, + size_t length, const AES_KEY *key, + unsigned char *ivec, const int enc); +/* NB: the IV is _four_ blocks long */ +void AES_bi_ige_encrypt(const unsigned char *in, unsigned char *out, + size_t length, const AES_KEY *key, + const AES_KEY *key2, const unsigned char *ivec, + const int enc); + +int AES_wrap_key(AES_KEY *key, const unsigned char *iv, + unsigned char *out, + const unsigned char *in, unsigned int inlen); +int AES_unwrap_key(AES_KEY *key, const unsigned char *iv, + unsigned char *out, + const unsigned char *in, unsigned int inlen); + + +# ifdef __cplusplus +} +# endif + +#endif diff --git a/mobile/ios/FxA/FxA/include/openssl/asn1.h b/mobile/ios/FxA/FxA/include/openssl/asn1.h new file mode 100644 index 0000000000..7cf6116120 --- /dev/null +++ b/mobile/ios/FxA/FxA/include/openssl/asn1.h @@ -0,0 +1,1096 @@ +/* + * Copyright 1995-2016 The OpenSSL Project Authors. All Rights Reserved. + * + * Licensed under the OpenSSL license (the "License"). You may not use + * this file except in compliance with the License. You can obtain a copy + * in the file LICENSE in the source distribution or at + * https://www.openssl.org/source/license.html + */ + +#ifndef HEADER_ASN1_H +# define HEADER_ASN1_H + +# include +# include +# include +# include +# include +# include + +# include + +# include +# if OPENSSL_API_COMPAT < 0x10100000L +# include +# endif + +# ifdef OPENSSL_BUILD_SHLIBCRYPTO +# undef OPENSSL_EXTERN +# define OPENSSL_EXTERN OPENSSL_EXPORT +# endif + +#ifdef __cplusplus +extern "C" { +#endif + +# define V_ASN1_UNIVERSAL 0x00 +# define V_ASN1_APPLICATION 0x40 +# define V_ASN1_CONTEXT_SPECIFIC 0x80 +# define V_ASN1_PRIVATE 0xc0 + +# define V_ASN1_CONSTRUCTED 0x20 +# define V_ASN1_PRIMITIVE_TAG 0x1f +# define V_ASN1_PRIMATIVE_TAG 0x1f + +# define V_ASN1_APP_CHOOSE -2/* let the recipient choose */ +# define V_ASN1_OTHER -3/* used in ASN1_TYPE */ +# define V_ASN1_ANY -4/* used in ASN1 template code */ + +# define V_ASN1_UNDEF -1 +/* ASN.1 tag values */ +# define V_ASN1_EOC 0 +# define V_ASN1_BOOLEAN 1 /**/ +# define V_ASN1_INTEGER 2 +# define V_ASN1_BIT_STRING 3 +# define V_ASN1_OCTET_STRING 4 +# define V_ASN1_NULL 5 +# define V_ASN1_OBJECT 6 +# define V_ASN1_OBJECT_DESCRIPTOR 7 +# define V_ASN1_EXTERNAL 8 +# define V_ASN1_REAL 9 +# define V_ASN1_ENUMERATED 10 +# define V_ASN1_UTF8STRING 12 +# define V_ASN1_SEQUENCE 16 +# define V_ASN1_SET 17 +# define V_ASN1_NUMERICSTRING 18 /**/ +# define V_ASN1_PRINTABLESTRING 19 +# define V_ASN1_T61STRING 20 +# define V_ASN1_TELETEXSTRING 20/* alias */ +# define V_ASN1_VIDEOTEXSTRING 21 /**/ +# define V_ASN1_IA5STRING 22 +# define V_ASN1_UTCTIME 23 +# define V_ASN1_GENERALIZEDTIME 24 /**/ +# define V_ASN1_GRAPHICSTRING 25 /**/ +# define V_ASN1_ISO64STRING 26 /**/ +# define V_ASN1_VISIBLESTRING 26/* alias */ +# define V_ASN1_GENERALSTRING 27 /**/ +# define V_ASN1_UNIVERSALSTRING 28 /**/ +# define V_ASN1_BMPSTRING 30 + +/* + * NB the constants below are used internally by ASN1_INTEGER + * and ASN1_ENUMERATED to indicate the sign. They are *not* on + * the wire tag values. + */ + +# define V_ASN1_NEG 0x100 +# define V_ASN1_NEG_INTEGER (2 | V_ASN1_NEG) +# define V_ASN1_NEG_ENUMERATED (10 | V_ASN1_NEG) + +/* For use with d2i_ASN1_type_bytes() */ +# define B_ASN1_NUMERICSTRING 0x0001 +# define B_ASN1_PRINTABLESTRING 0x0002 +# define B_ASN1_T61STRING 0x0004 +# define B_ASN1_TELETEXSTRING 0x0004 +# define B_ASN1_VIDEOTEXSTRING 0x0008 +# define B_ASN1_IA5STRING 0x0010 +# define B_ASN1_GRAPHICSTRING 0x0020 +# define B_ASN1_ISO64STRING 0x0040 +# define B_ASN1_VISIBLESTRING 0x0040 +# define B_ASN1_GENERALSTRING 0x0080 +# define B_ASN1_UNIVERSALSTRING 0x0100 +# define B_ASN1_OCTET_STRING 0x0200 +# define B_ASN1_BIT_STRING 0x0400 +# define B_ASN1_BMPSTRING 0x0800 +# define B_ASN1_UNKNOWN 0x1000 +# define B_ASN1_UTF8STRING 0x2000 +# define B_ASN1_UTCTIME 0x4000 +# define B_ASN1_GENERALIZEDTIME 0x8000 +# define B_ASN1_SEQUENCE 0x10000 +/* For use with ASN1_mbstring_copy() */ +# define MBSTRING_FLAG 0x1000 +# define MBSTRING_UTF8 (MBSTRING_FLAG) +# define MBSTRING_ASC (MBSTRING_FLAG|1) +# define MBSTRING_BMP (MBSTRING_FLAG|2) +# define MBSTRING_UNIV (MBSTRING_FLAG|4) +# define SMIME_OLDMIME 0x400 +# define SMIME_CRLFEOL 0x800 +# define SMIME_STREAM 0x1000 + struct X509_algor_st; +DEFINE_STACK_OF(X509_ALGOR) + +# define ASN1_STRING_FLAG_BITS_LEFT 0x08/* Set if 0x07 has bits left value */ +/* + * This indicates that the ASN1_STRING is not a real value but just a place + * holder for the location where indefinite length constructed data should be + * inserted in the memory buffer + */ +# define ASN1_STRING_FLAG_NDEF 0x010 + +/* + * This flag is used by the CMS code to indicate that a string is not + * complete and is a place holder for content when it had all been accessed. + * The flag will be reset when content has been written to it. + */ + +# define ASN1_STRING_FLAG_CONT 0x020 +/* + * This flag is used by ASN1 code to indicate an ASN1_STRING is an MSTRING + * type. + */ +# define ASN1_STRING_FLAG_MSTRING 0x040 +/* String is embedded and only content should be freed */ +# define ASN1_STRING_FLAG_EMBED 0x080 +/* This is the base type that holds just about everything :-) */ +struct asn1_string_st { + int length; + int type; + unsigned char *data; + /* + * The value of the following field depends on the type being held. It + * is mostly being used for BIT_STRING so if the input data has a + * non-zero 'unused bits' value, it will be handled correctly + */ + long flags; +}; + +/* + * ASN1_ENCODING structure: this is used to save the received encoding of an + * ASN1 type. This is useful to get round problems with invalid encodings + * which can break signatures. + */ + +typedef struct ASN1_ENCODING_st { + unsigned char *enc; /* DER encoding */ + long len; /* Length of encoding */ + int modified; /* set to 1 if 'enc' is invalid */ +} ASN1_ENCODING; + +/* Used with ASN1 LONG type: if a long is set to this it is omitted */ +# define ASN1_LONG_UNDEF 0x7fffffffL + +# define STABLE_FLAGS_MALLOC 0x01 +/* + * A zero passed to ASN1_STRING_TABLE_new_add for the flags is interpreted + * as "don't change" and STABLE_FLAGS_MALLOC is always set. By setting + * STABLE_FLAGS_MALLOC only we can clear the existing value. Use the alias + * STABLE_FLAGS_CLEAR to reflect this. + */ +# define STABLE_FLAGS_CLEAR STABLE_FLAGS_MALLOC +# define STABLE_NO_MASK 0x02 +# define DIRSTRING_TYPE \ + (B_ASN1_PRINTABLESTRING|B_ASN1_T61STRING|B_ASN1_BMPSTRING|B_ASN1_UTF8STRING) +# define PKCS9STRING_TYPE (DIRSTRING_TYPE|B_ASN1_IA5STRING) + +typedef struct asn1_string_table_st { + int nid; + long minsize; + long maxsize; + unsigned long mask; + unsigned long flags; +} ASN1_STRING_TABLE; + +DEFINE_STACK_OF(ASN1_STRING_TABLE) + +/* size limits: this stuff is taken straight from RFC2459 */ + +# define ub_name 32768 +# define ub_common_name 64 +# define ub_locality_name 128 +# define ub_state_name 128 +# define ub_organization_name 64 +# define ub_organization_unit_name 64 +# define ub_title 64 +# define ub_email_address 128 + +/* + * Declarations for template structures: for full definitions see asn1t.h + */ +typedef struct ASN1_TEMPLATE_st ASN1_TEMPLATE; +typedef struct ASN1_TLC_st ASN1_TLC; +/* This is just an opaque pointer */ +typedef struct ASN1_VALUE_st ASN1_VALUE; + +/* Declare ASN1 functions: the implement macro in in asn1t.h */ + +# define DECLARE_ASN1_FUNCTIONS(type) DECLARE_ASN1_FUNCTIONS_name(type, type) + +# define DECLARE_ASN1_ALLOC_FUNCTIONS(type) \ + DECLARE_ASN1_ALLOC_FUNCTIONS_name(type, type) + +# define DECLARE_ASN1_FUNCTIONS_name(type, name) \ + DECLARE_ASN1_ALLOC_FUNCTIONS_name(type, name) \ + DECLARE_ASN1_ENCODE_FUNCTIONS(type, name, name) + +# define DECLARE_ASN1_FUNCTIONS_fname(type, itname, name) \ + DECLARE_ASN1_ALLOC_FUNCTIONS_name(type, name) \ + DECLARE_ASN1_ENCODE_FUNCTIONS(type, itname, name) + +# define DECLARE_ASN1_ENCODE_FUNCTIONS(type, itname, name) \ + type *d2i_##name(type **a, const unsigned char **in, long len); \ + int i2d_##name(type *a, unsigned char **out); \ + DECLARE_ASN1_ITEM(itname) + +# define DECLARE_ASN1_ENCODE_FUNCTIONS_const(type, name) \ + type *d2i_##name(type **a, const unsigned char **in, long len); \ + int i2d_##name(const type *a, unsigned char **out); \ + DECLARE_ASN1_ITEM(name) + +# define DECLARE_ASN1_NDEF_FUNCTION(name) \ + int i2d_##name##_NDEF(name *a, unsigned char **out); + +# define DECLARE_ASN1_FUNCTIONS_const(name) \ + DECLARE_ASN1_ALLOC_FUNCTIONS(name) \ + DECLARE_ASN1_ENCODE_FUNCTIONS_const(name, name) + +# define DECLARE_ASN1_ALLOC_FUNCTIONS_name(type, name) \ + type *name##_new(void); \ + void name##_free(type *a); + +# define DECLARE_ASN1_PRINT_FUNCTION(stname) \ + DECLARE_ASN1_PRINT_FUNCTION_fname(stname, stname) + +# define DECLARE_ASN1_PRINT_FUNCTION_fname(stname, fname) \ + int fname##_print_ctx(BIO *out, stname *x, int indent, \ + const ASN1_PCTX *pctx); + +# define D2I_OF(type) type *(*)(type **,const unsigned char **,long) +# define I2D_OF(type) int (*)(type *,unsigned char **) +# define I2D_OF_const(type) int (*)(const type *,unsigned char **) + +# define CHECKED_D2I_OF(type, d2i) \ + ((d2i_of_void*) (1 ? d2i : ((D2I_OF(type))0))) +# define CHECKED_I2D_OF(type, i2d) \ + ((i2d_of_void*) (1 ? i2d : ((I2D_OF(type))0))) +# define CHECKED_NEW_OF(type, xnew) \ + ((void *(*)(void)) (1 ? xnew : ((type *(*)(void))0))) +# define CHECKED_PTR_OF(type, p) \ + ((void*) (1 ? p : (type*)0)) +# define CHECKED_PPTR_OF(type, p) \ + ((void**) (1 ? p : (type**)0)) + +# define TYPEDEF_D2I_OF(type) typedef type *d2i_of_##type(type **,const unsigned char **,long) +# define TYPEDEF_I2D_OF(type) typedef int i2d_of_##type(type *,unsigned char **) +# define TYPEDEF_D2I2D_OF(type) TYPEDEF_D2I_OF(type); TYPEDEF_I2D_OF(type) + +TYPEDEF_D2I2D_OF(void); + +/*- + * The following macros and typedefs allow an ASN1_ITEM + * to be embedded in a structure and referenced. Since + * the ASN1_ITEM pointers need to be globally accessible + * (possibly from shared libraries) they may exist in + * different forms. On platforms that support it the + * ASN1_ITEM structure itself will be globally exported. + * Other platforms will export a function that returns + * an ASN1_ITEM pointer. + * + * To handle both cases transparently the macros below + * should be used instead of hard coding an ASN1_ITEM + * pointer in a structure. + * + * The structure will look like this: + * + * typedef struct SOMETHING_st { + * ... + * ASN1_ITEM_EXP *iptr; + * ... + * } SOMETHING; + * + * It would be initialised as e.g.: + * + * SOMETHING somevar = {...,ASN1_ITEM_ref(X509),...}; + * + * and the actual pointer extracted with: + * + * const ASN1_ITEM *it = ASN1_ITEM_ptr(somevar.iptr); + * + * Finally an ASN1_ITEM pointer can be extracted from an + * appropriate reference with: ASN1_ITEM_rptr(X509). This + * would be used when a function takes an ASN1_ITEM * argument. + * + */ + +# ifndef OPENSSL_EXPORT_VAR_AS_FUNCTION + +/* ASN1_ITEM pointer exported type */ +typedef const ASN1_ITEM ASN1_ITEM_EXP; + +/* Macro to obtain ASN1_ITEM pointer from exported type */ +# define ASN1_ITEM_ptr(iptr) (iptr) + +/* Macro to include ASN1_ITEM pointer from base type */ +# define ASN1_ITEM_ref(iptr) (&(iptr##_it)) + +# define ASN1_ITEM_rptr(ref) (&(ref##_it)) + +# define DECLARE_ASN1_ITEM(name) \ + OPENSSL_EXTERN const ASN1_ITEM name##_it; + +# else + +/* + * Platforms that can't easily handle shared global variables are declared as + * functions returning ASN1_ITEM pointers. + */ + +/* ASN1_ITEM pointer exported type */ +typedef const ASN1_ITEM *ASN1_ITEM_EXP (void); + +/* Macro to obtain ASN1_ITEM pointer from exported type */ +# define ASN1_ITEM_ptr(iptr) (iptr()) + +/* Macro to include ASN1_ITEM pointer from base type */ +# define ASN1_ITEM_ref(iptr) (iptr##_it) + +# define ASN1_ITEM_rptr(ref) (ref##_it()) + +# define DECLARE_ASN1_ITEM(name) \ + const ASN1_ITEM * name##_it(void); + +# endif + +/* Parameters used by ASN1_STRING_print_ex() */ + +/* + * These determine which characters to escape: RFC2253 special characters, + * control characters and MSB set characters + */ + +# define ASN1_STRFLGS_ESC_2253 1 +# define ASN1_STRFLGS_ESC_CTRL 2 +# define ASN1_STRFLGS_ESC_MSB 4 + +/* + * This flag determines how we do escaping: normally RC2253 backslash only, + * set this to use backslash and quote. + */ + +# define ASN1_STRFLGS_ESC_QUOTE 8 + +/* These three flags are internal use only. */ + +/* Character is a valid PrintableString character */ +# define CHARTYPE_PRINTABLESTRING 0x10 +/* Character needs escaping if it is the first character */ +# define CHARTYPE_FIRST_ESC_2253 0x20 +/* Character needs escaping if it is the last character */ +# define CHARTYPE_LAST_ESC_2253 0x40 + +/* + * NB the internal flags are safely reused below by flags handled at the top + * level. + */ + +/* + * If this is set we convert all character strings to UTF8 first + */ + +# define ASN1_STRFLGS_UTF8_CONVERT 0x10 + +/* + * If this is set we don't attempt to interpret content: just assume all + * strings are 1 byte per character. This will produce some pretty odd + * looking output! + */ + +# define ASN1_STRFLGS_IGNORE_TYPE 0x20 + +/* If this is set we include the string type in the output */ +# define ASN1_STRFLGS_SHOW_TYPE 0x40 + +/* + * This determines which strings to display and which to 'dump' (hex dump of + * content octets or DER encoding). We can only dump non character strings or + * everything. If we don't dump 'unknown' they are interpreted as character + * strings with 1 octet per character and are subject to the usual escaping + * options. + */ + +# define ASN1_STRFLGS_DUMP_ALL 0x80 +# define ASN1_STRFLGS_DUMP_UNKNOWN 0x100 + +/* + * These determine what 'dumping' does, we can dump the content octets or the + * DER encoding: both use the RFC2253 #XXXXX notation. + */ + +# define ASN1_STRFLGS_DUMP_DER 0x200 + +/* + * This flag specifies that RC2254 escaping shall be performed. + */ +#define ASN1_STRFLGS_ESC_2254 0x400 + +/* + * All the string flags consistent with RFC2253, escaping control characters + * isn't essential in RFC2253 but it is advisable anyway. + */ + +# define ASN1_STRFLGS_RFC2253 (ASN1_STRFLGS_ESC_2253 | \ + ASN1_STRFLGS_ESC_CTRL | \ + ASN1_STRFLGS_ESC_MSB | \ + ASN1_STRFLGS_UTF8_CONVERT | \ + ASN1_STRFLGS_DUMP_UNKNOWN | \ + ASN1_STRFLGS_DUMP_DER) + +DEFINE_STACK_OF(ASN1_INTEGER) + +DEFINE_STACK_OF(ASN1_GENERALSTRING) + +DEFINE_STACK_OF(ASN1_UTF8STRING) + +typedef struct asn1_type_st { + int type; + union { + char *ptr; + ASN1_BOOLEAN boolean; + ASN1_STRING *asn1_string; + ASN1_OBJECT *object; + ASN1_INTEGER *integer; + ASN1_ENUMERATED *enumerated; + ASN1_BIT_STRING *bit_string; + ASN1_OCTET_STRING *octet_string; + ASN1_PRINTABLESTRING *printablestring; + ASN1_T61STRING *t61string; + ASN1_IA5STRING *ia5string; + ASN1_GENERALSTRING *generalstring; + ASN1_BMPSTRING *bmpstring; + ASN1_UNIVERSALSTRING *universalstring; + ASN1_UTCTIME *utctime; + ASN1_GENERALIZEDTIME *generalizedtime; + ASN1_VISIBLESTRING *visiblestring; + ASN1_UTF8STRING *utf8string; + /* + * set and sequence are left complete and still contain the set or + * sequence bytes + */ + ASN1_STRING *set; + ASN1_STRING *sequence; + ASN1_VALUE *asn1_value; + } value; +} ASN1_TYPE; + +DEFINE_STACK_OF(ASN1_TYPE) + +typedef STACK_OF(ASN1_TYPE) ASN1_SEQUENCE_ANY; + +DECLARE_ASN1_ENCODE_FUNCTIONS_const(ASN1_SEQUENCE_ANY, ASN1_SEQUENCE_ANY) +DECLARE_ASN1_ENCODE_FUNCTIONS_const(ASN1_SEQUENCE_ANY, ASN1_SET_ANY) + +/* This is used to contain a list of bit names */ +typedef struct BIT_STRING_BITNAME_st { + int bitnum; + const char *lname; + const char *sname; +} BIT_STRING_BITNAME; + +# define B_ASN1_TIME \ + B_ASN1_UTCTIME | \ + B_ASN1_GENERALIZEDTIME + +# define B_ASN1_PRINTABLE \ + B_ASN1_NUMERICSTRING| \ + B_ASN1_PRINTABLESTRING| \ + B_ASN1_T61STRING| \ + B_ASN1_IA5STRING| \ + B_ASN1_BIT_STRING| \ + B_ASN1_UNIVERSALSTRING|\ + B_ASN1_BMPSTRING|\ + B_ASN1_UTF8STRING|\ + B_ASN1_SEQUENCE|\ + B_ASN1_UNKNOWN + +# define B_ASN1_DIRECTORYSTRING \ + B_ASN1_PRINTABLESTRING| \ + B_ASN1_TELETEXSTRING|\ + B_ASN1_BMPSTRING|\ + B_ASN1_UNIVERSALSTRING|\ + B_ASN1_UTF8STRING + +# define B_ASN1_DISPLAYTEXT \ + B_ASN1_IA5STRING| \ + B_ASN1_VISIBLESTRING| \ + B_ASN1_BMPSTRING|\ + B_ASN1_UTF8STRING + +DECLARE_ASN1_FUNCTIONS_fname(ASN1_TYPE, ASN1_ANY, ASN1_TYPE) + +int ASN1_TYPE_get(const ASN1_TYPE *a); +void ASN1_TYPE_set(ASN1_TYPE *a, int type, void *value); +int ASN1_TYPE_set1(ASN1_TYPE *a, int type, const void *value); +int ASN1_TYPE_cmp(const ASN1_TYPE *a, const ASN1_TYPE *b); + +ASN1_TYPE *ASN1_TYPE_pack_sequence(const ASN1_ITEM *it, void *s, ASN1_TYPE **t); +void *ASN1_TYPE_unpack_sequence(const ASN1_ITEM *it, const ASN1_TYPE *t); + +ASN1_OBJECT *ASN1_OBJECT_new(void); +void ASN1_OBJECT_free(ASN1_OBJECT *a); +int i2d_ASN1_OBJECT(const ASN1_OBJECT *a, unsigned char **pp); +ASN1_OBJECT *d2i_ASN1_OBJECT(ASN1_OBJECT **a, const unsigned char **pp, + long length); + +DECLARE_ASN1_ITEM(ASN1_OBJECT) + +DEFINE_STACK_OF(ASN1_OBJECT) + +ASN1_STRING *ASN1_STRING_new(void); +void ASN1_STRING_free(ASN1_STRING *a); +void ASN1_STRING_clear_free(ASN1_STRING *a); +int ASN1_STRING_copy(ASN1_STRING *dst, const ASN1_STRING *str); +ASN1_STRING *ASN1_STRING_dup(const ASN1_STRING *a); +ASN1_STRING *ASN1_STRING_type_new(int type); +int ASN1_STRING_cmp(const ASN1_STRING *a, const ASN1_STRING *b); + /* + * Since this is used to store all sorts of things, via macros, for now, + * make its data void * + */ +int ASN1_STRING_set(ASN1_STRING *str, const void *data, int len); +void ASN1_STRING_set0(ASN1_STRING *str, void *data, int len); +int ASN1_STRING_length(const ASN1_STRING *x); +void ASN1_STRING_length_set(ASN1_STRING *x, int n); +int ASN1_STRING_type(const ASN1_STRING *x); +DEPRECATEDIN_1_1_0(unsigned char *ASN1_STRING_data(ASN1_STRING *x)) +const unsigned char *ASN1_STRING_get0_data(const ASN1_STRING *x); + +DECLARE_ASN1_FUNCTIONS(ASN1_BIT_STRING) +int ASN1_BIT_STRING_set(ASN1_BIT_STRING *a, unsigned char *d, int length); +int ASN1_BIT_STRING_set_bit(ASN1_BIT_STRING *a, int n, int value); +int ASN1_BIT_STRING_get_bit(const ASN1_BIT_STRING *a, int n); +int ASN1_BIT_STRING_check(const ASN1_BIT_STRING *a, + const unsigned char *flags, int flags_len); + +int ASN1_BIT_STRING_name_print(BIO *out, ASN1_BIT_STRING *bs, + BIT_STRING_BITNAME *tbl, int indent); +int ASN1_BIT_STRING_num_asc(const char *name, BIT_STRING_BITNAME *tbl); +int ASN1_BIT_STRING_set_asc(ASN1_BIT_STRING *bs, const char *name, int value, + BIT_STRING_BITNAME *tbl); + +DECLARE_ASN1_FUNCTIONS(ASN1_INTEGER) +ASN1_INTEGER *d2i_ASN1_UINTEGER(ASN1_INTEGER **a, const unsigned char **pp, + long length); +ASN1_INTEGER *ASN1_INTEGER_dup(const ASN1_INTEGER *x); +int ASN1_INTEGER_cmp(const ASN1_INTEGER *x, const ASN1_INTEGER *y); + +DECLARE_ASN1_FUNCTIONS(ASN1_ENUMERATED) + +int ASN1_UTCTIME_check(const ASN1_UTCTIME *a); +ASN1_UTCTIME *ASN1_UTCTIME_set(ASN1_UTCTIME *s, time_t t); +ASN1_UTCTIME *ASN1_UTCTIME_adj(ASN1_UTCTIME *s, time_t t, + int offset_day, long offset_sec); +int ASN1_UTCTIME_set_string(ASN1_UTCTIME *s, const char *str); +int ASN1_UTCTIME_cmp_time_t(const ASN1_UTCTIME *s, time_t t); + +int ASN1_GENERALIZEDTIME_check(const ASN1_GENERALIZEDTIME *a); +ASN1_GENERALIZEDTIME *ASN1_GENERALIZEDTIME_set(ASN1_GENERALIZEDTIME *s, + time_t t); +ASN1_GENERALIZEDTIME *ASN1_GENERALIZEDTIME_adj(ASN1_GENERALIZEDTIME *s, + time_t t, int offset_day, + long offset_sec); +int ASN1_GENERALIZEDTIME_set_string(ASN1_GENERALIZEDTIME *s, const char *str); +int ASN1_TIME_diff(int *pday, int *psec, + const ASN1_TIME *from, const ASN1_TIME *to); + +DECLARE_ASN1_FUNCTIONS(ASN1_OCTET_STRING) +ASN1_OCTET_STRING *ASN1_OCTET_STRING_dup(const ASN1_OCTET_STRING *a); +int ASN1_OCTET_STRING_cmp(const ASN1_OCTET_STRING *a, + const ASN1_OCTET_STRING *b); +int ASN1_OCTET_STRING_set(ASN1_OCTET_STRING *str, const unsigned char *data, + int len); + +DECLARE_ASN1_FUNCTIONS(ASN1_VISIBLESTRING) +DECLARE_ASN1_FUNCTIONS(ASN1_UNIVERSALSTRING) +DECLARE_ASN1_FUNCTIONS(ASN1_UTF8STRING) +DECLARE_ASN1_FUNCTIONS(ASN1_NULL) +DECLARE_ASN1_FUNCTIONS(ASN1_BMPSTRING) + +int UTF8_getc(const unsigned char *str, int len, unsigned long *val); +int UTF8_putc(unsigned char *str, int len, unsigned long value); + +DECLARE_ASN1_FUNCTIONS_name(ASN1_STRING, ASN1_PRINTABLE) + +DECLARE_ASN1_FUNCTIONS_name(ASN1_STRING, DIRECTORYSTRING) +DECLARE_ASN1_FUNCTIONS_name(ASN1_STRING, DISPLAYTEXT) +DECLARE_ASN1_FUNCTIONS(ASN1_PRINTABLESTRING) +DECLARE_ASN1_FUNCTIONS(ASN1_T61STRING) +DECLARE_ASN1_FUNCTIONS(ASN1_IA5STRING) +DECLARE_ASN1_FUNCTIONS(ASN1_GENERALSTRING) +DECLARE_ASN1_FUNCTIONS(ASN1_UTCTIME) +DECLARE_ASN1_FUNCTIONS(ASN1_GENERALIZEDTIME) +DECLARE_ASN1_FUNCTIONS(ASN1_TIME) + +DECLARE_ASN1_ITEM(ASN1_OCTET_STRING_NDEF) + +ASN1_TIME *ASN1_TIME_set(ASN1_TIME *s, time_t t); +ASN1_TIME *ASN1_TIME_adj(ASN1_TIME *s, time_t t, + int offset_day, long offset_sec); +int ASN1_TIME_check(const ASN1_TIME *t); +ASN1_GENERALIZEDTIME *ASN1_TIME_to_generalizedtime(ASN1_TIME *t, ASN1_GENERALIZEDTIME + **out); +int ASN1_TIME_set_string(ASN1_TIME *s, const char *str); + +int i2a_ASN1_INTEGER(BIO *bp, const ASN1_INTEGER *a); +int a2i_ASN1_INTEGER(BIO *bp, ASN1_INTEGER *bs, char *buf, int size); +int i2a_ASN1_ENUMERATED(BIO *bp, const ASN1_ENUMERATED *a); +int a2i_ASN1_ENUMERATED(BIO *bp, ASN1_ENUMERATED *bs, char *buf, int size); +int i2a_ASN1_OBJECT(BIO *bp, const ASN1_OBJECT *a); +int a2i_ASN1_STRING(BIO *bp, ASN1_STRING *bs, char *buf, int size); +int i2a_ASN1_STRING(BIO *bp, const ASN1_STRING *a, int type); +int i2t_ASN1_OBJECT(char *buf, int buf_len, const ASN1_OBJECT *a); + +int a2d_ASN1_OBJECT(unsigned char *out, int olen, const char *buf, int num); +ASN1_OBJECT *ASN1_OBJECT_create(int nid, unsigned char *data, int len, + const char *sn, const char *ln); + +int ASN1_INTEGER_get_int64(int64_t *pr, const ASN1_INTEGER *a); +int ASN1_INTEGER_set_int64(ASN1_INTEGER *a, int64_t r); +int ASN1_INTEGER_get_uint64(uint64_t *pr, const ASN1_INTEGER *a); +int ASN1_INTEGER_set_uint64(ASN1_INTEGER *a, uint64_t r); + +int ASN1_INTEGER_set(ASN1_INTEGER *a, long v); +long ASN1_INTEGER_get(const ASN1_INTEGER *a); +ASN1_INTEGER *BN_to_ASN1_INTEGER(const BIGNUM *bn, ASN1_INTEGER *ai); +BIGNUM *ASN1_INTEGER_to_BN(const ASN1_INTEGER *ai, BIGNUM *bn); + +int ASN1_ENUMERATED_get_int64(int64_t *pr, const ASN1_ENUMERATED *a); +int ASN1_ENUMERATED_set_int64(ASN1_ENUMERATED *a, int64_t r); + + +int ASN1_ENUMERATED_set(ASN1_ENUMERATED *a, long v); +long ASN1_ENUMERATED_get(const ASN1_ENUMERATED *a); +ASN1_ENUMERATED *BN_to_ASN1_ENUMERATED(const BIGNUM *bn, ASN1_ENUMERATED *ai); +BIGNUM *ASN1_ENUMERATED_to_BN(const ASN1_ENUMERATED *ai, BIGNUM *bn); + +/* General */ +/* given a string, return the correct type, max is the maximum length */ +int ASN1_PRINTABLE_type(const unsigned char *s, int max); + +unsigned long ASN1_tag2bit(int tag); + +/* SPECIALS */ +int ASN1_get_object(const unsigned char **pp, long *plength, int *ptag, + int *pclass, long omax); +int ASN1_check_infinite_end(unsigned char **p, long len); +int ASN1_const_check_infinite_end(const unsigned char **p, long len); +void ASN1_put_object(unsigned char **pp, int constructed, int length, + int tag, int xclass); +int ASN1_put_eoc(unsigned char **pp); +int ASN1_object_size(int constructed, int length, int tag); + +/* Used to implement other functions */ +void *ASN1_dup(i2d_of_void *i2d, d2i_of_void *d2i, void *x); + +# define ASN1_dup_of(type,i2d,d2i,x) \ + ((type*)ASN1_dup(CHECKED_I2D_OF(type, i2d), \ + CHECKED_D2I_OF(type, d2i), \ + CHECKED_PTR_OF(type, x))) + +# define ASN1_dup_of_const(type,i2d,d2i,x) \ + ((type*)ASN1_dup(CHECKED_I2D_OF(const type, i2d), \ + CHECKED_D2I_OF(type, d2i), \ + CHECKED_PTR_OF(const type, x))) + +void *ASN1_item_dup(const ASN1_ITEM *it, void *x); + +/* ASN1 alloc/free macros for when a type is only used internally */ + +# define M_ASN1_new_of(type) (type *)ASN1_item_new(ASN1_ITEM_rptr(type)) +# define M_ASN1_free_of(x, type) \ + ASN1_item_free(CHECKED_PTR_OF(type, x), ASN1_ITEM_rptr(type)) + +# ifndef OPENSSL_NO_STDIO +void *ASN1_d2i_fp(void *(*xnew) (void), d2i_of_void *d2i, FILE *in, void **x); + +# define ASN1_d2i_fp_of(type,xnew,d2i,in,x) \ + ((type*)ASN1_d2i_fp(CHECKED_NEW_OF(type, xnew), \ + CHECKED_D2I_OF(type, d2i), \ + in, \ + CHECKED_PPTR_OF(type, x))) + +void *ASN1_item_d2i_fp(const ASN1_ITEM *it, FILE *in, void *x); +int ASN1_i2d_fp(i2d_of_void *i2d, FILE *out, void *x); + +# define ASN1_i2d_fp_of(type,i2d,out,x) \ + (ASN1_i2d_fp(CHECKED_I2D_OF(type, i2d), \ + out, \ + CHECKED_PTR_OF(type, x))) + +# define ASN1_i2d_fp_of_const(type,i2d,out,x) \ + (ASN1_i2d_fp(CHECKED_I2D_OF(const type, i2d), \ + out, \ + CHECKED_PTR_OF(const type, x))) + +int ASN1_item_i2d_fp(const ASN1_ITEM *it, FILE *out, void *x); +int ASN1_STRING_print_ex_fp(FILE *fp, const ASN1_STRING *str, unsigned long flags); +# endif + +int ASN1_STRING_to_UTF8(unsigned char **out, const ASN1_STRING *in); + +void *ASN1_d2i_bio(void *(*xnew) (void), d2i_of_void *d2i, BIO *in, void **x); + +# define ASN1_d2i_bio_of(type,xnew,d2i,in,x) \ + ((type*)ASN1_d2i_bio( CHECKED_NEW_OF(type, xnew), \ + CHECKED_D2I_OF(type, d2i), \ + in, \ + CHECKED_PPTR_OF(type, x))) + +void *ASN1_item_d2i_bio(const ASN1_ITEM *it, BIO *in, void *x); +int ASN1_i2d_bio(i2d_of_void *i2d, BIO *out, unsigned char *x); + +# define ASN1_i2d_bio_of(type,i2d,out,x) \ + (ASN1_i2d_bio(CHECKED_I2D_OF(type, i2d), \ + out, \ + CHECKED_PTR_OF(type, x))) + +# define ASN1_i2d_bio_of_const(type,i2d,out,x) \ + (ASN1_i2d_bio(CHECKED_I2D_OF(const type, i2d), \ + out, \ + CHECKED_PTR_OF(const type, x))) + +int ASN1_item_i2d_bio(const ASN1_ITEM *it, BIO *out, void *x); +int ASN1_UTCTIME_print(BIO *fp, const ASN1_UTCTIME *a); +int ASN1_GENERALIZEDTIME_print(BIO *fp, const ASN1_GENERALIZEDTIME *a); +int ASN1_TIME_print(BIO *fp, const ASN1_TIME *a); +int ASN1_STRING_print(BIO *bp, const ASN1_STRING *v); +int ASN1_STRING_print_ex(BIO *out, const ASN1_STRING *str, unsigned long flags); +int ASN1_buf_print(BIO *bp, const unsigned char *buf, size_t buflen, int off); +int ASN1_bn_print(BIO *bp, const char *number, const BIGNUM *num, + unsigned char *buf, int off); +int ASN1_parse(BIO *bp, const unsigned char *pp, long len, int indent); +int ASN1_parse_dump(BIO *bp, const unsigned char *pp, long len, int indent, + int dump); +const char *ASN1_tag2str(int tag); + +/* Used to load and write Netscape format cert */ + +int ASN1_UNIVERSALSTRING_to_string(ASN1_UNIVERSALSTRING *s); + +int ASN1_TYPE_set_octetstring(ASN1_TYPE *a, unsigned char *data, int len); +int ASN1_TYPE_get_octetstring(const ASN1_TYPE *a, unsigned char *data, int max_len); +int ASN1_TYPE_set_int_octetstring(ASN1_TYPE *a, long num, + unsigned char *data, int len); +int ASN1_TYPE_get_int_octetstring(const ASN1_TYPE *a, long *num, + unsigned char *data, int max_len); + +void *ASN1_item_unpack(const ASN1_STRING *oct, const ASN1_ITEM *it); + +ASN1_STRING *ASN1_item_pack(void *obj, const ASN1_ITEM *it, + ASN1_OCTET_STRING **oct); + +void ASN1_STRING_set_default_mask(unsigned long mask); +int ASN1_STRING_set_default_mask_asc(const char *p); +unsigned long ASN1_STRING_get_default_mask(void); +int ASN1_mbstring_copy(ASN1_STRING **out, const unsigned char *in, int len, + int inform, unsigned long mask); +int ASN1_mbstring_ncopy(ASN1_STRING **out, const unsigned char *in, int len, + int inform, unsigned long mask, + long minsize, long maxsize); + +ASN1_STRING *ASN1_STRING_set_by_NID(ASN1_STRING **out, + const unsigned char *in, int inlen, + int inform, int nid); +ASN1_STRING_TABLE *ASN1_STRING_TABLE_get(int nid); +int ASN1_STRING_TABLE_add(int, long, long, unsigned long, unsigned long); +void ASN1_STRING_TABLE_cleanup(void); + +/* ASN1 template functions */ + +/* Old API compatible functions */ +ASN1_VALUE *ASN1_item_new(const ASN1_ITEM *it); +void ASN1_item_free(ASN1_VALUE *val, const ASN1_ITEM *it); +ASN1_VALUE *ASN1_item_d2i(ASN1_VALUE **val, const unsigned char **in, + long len, const ASN1_ITEM *it); +int ASN1_item_i2d(ASN1_VALUE *val, unsigned char **out, const ASN1_ITEM *it); +int ASN1_item_ndef_i2d(ASN1_VALUE *val, unsigned char **out, + const ASN1_ITEM *it); + +void ASN1_add_oid_module(void); +void ASN1_add_stable_module(void); + +ASN1_TYPE *ASN1_generate_nconf(const char *str, CONF *nconf); +ASN1_TYPE *ASN1_generate_v3(const char *str, X509V3_CTX *cnf); +int ASN1_str2mask(const char *str, unsigned long *pmask); + +/* ASN1 Print flags */ + +/* Indicate missing OPTIONAL fields */ +# define ASN1_PCTX_FLAGS_SHOW_ABSENT 0x001 +/* Mark start and end of SEQUENCE */ +# define ASN1_PCTX_FLAGS_SHOW_SEQUENCE 0x002 +/* Mark start and end of SEQUENCE/SET OF */ +# define ASN1_PCTX_FLAGS_SHOW_SSOF 0x004 +/* Show the ASN1 type of primitives */ +# define ASN1_PCTX_FLAGS_SHOW_TYPE 0x008 +/* Don't show ASN1 type of ANY */ +# define ASN1_PCTX_FLAGS_NO_ANY_TYPE 0x010 +/* Don't show ASN1 type of MSTRINGs */ +# define ASN1_PCTX_FLAGS_NO_MSTRING_TYPE 0x020 +/* Don't show field names in SEQUENCE */ +# define ASN1_PCTX_FLAGS_NO_FIELD_NAME 0x040 +/* Show structure names of each SEQUENCE field */ +# define ASN1_PCTX_FLAGS_SHOW_FIELD_STRUCT_NAME 0x080 +/* Don't show structure name even at top level */ +# define ASN1_PCTX_FLAGS_NO_STRUCT_NAME 0x100 + +int ASN1_item_print(BIO *out, ASN1_VALUE *ifld, int indent, + const ASN1_ITEM *it, const ASN1_PCTX *pctx); +ASN1_PCTX *ASN1_PCTX_new(void); +void ASN1_PCTX_free(ASN1_PCTX *p); +unsigned long ASN1_PCTX_get_flags(const ASN1_PCTX *p); +void ASN1_PCTX_set_flags(ASN1_PCTX *p, unsigned long flags); +unsigned long ASN1_PCTX_get_nm_flags(const ASN1_PCTX *p); +void ASN1_PCTX_set_nm_flags(ASN1_PCTX *p, unsigned long flags); +unsigned long ASN1_PCTX_get_cert_flags(const ASN1_PCTX *p); +void ASN1_PCTX_set_cert_flags(ASN1_PCTX *p, unsigned long flags); +unsigned long ASN1_PCTX_get_oid_flags(const ASN1_PCTX *p); +void ASN1_PCTX_set_oid_flags(ASN1_PCTX *p, unsigned long flags); +unsigned long ASN1_PCTX_get_str_flags(const ASN1_PCTX *p); +void ASN1_PCTX_set_str_flags(ASN1_PCTX *p, unsigned long flags); + +ASN1_SCTX *ASN1_SCTX_new(int (*scan_cb) (ASN1_SCTX *ctx)); +void ASN1_SCTX_free(ASN1_SCTX *p); +const ASN1_ITEM *ASN1_SCTX_get_item(ASN1_SCTX *p); +const ASN1_TEMPLATE *ASN1_SCTX_get_template(ASN1_SCTX *p); +unsigned long ASN1_SCTX_get_flags(ASN1_SCTX *p); +void ASN1_SCTX_set_app_data(ASN1_SCTX *p, void *data); +void *ASN1_SCTX_get_app_data(ASN1_SCTX *p); + +const BIO_METHOD *BIO_f_asn1(void); + +BIO *BIO_new_NDEF(BIO *out, ASN1_VALUE *val, const ASN1_ITEM *it); + +int i2d_ASN1_bio_stream(BIO *out, ASN1_VALUE *val, BIO *in, int flags, + const ASN1_ITEM *it); +int PEM_write_bio_ASN1_stream(BIO *out, ASN1_VALUE *val, BIO *in, int flags, + const char *hdr, const ASN1_ITEM *it); +int SMIME_write_ASN1(BIO *bio, ASN1_VALUE *val, BIO *data, int flags, + int ctype_nid, int econt_nid, + STACK_OF(X509_ALGOR) *mdalgs, const ASN1_ITEM *it); +ASN1_VALUE *SMIME_read_ASN1(BIO *bio, BIO **bcont, const ASN1_ITEM *it); +int SMIME_crlf_copy(BIO *in, BIO *out, int flags); +int SMIME_text(BIO *in, BIO *out); + +/* BEGIN ERROR CODES */ +/* + * The following lines are auto generated by the script mkerr.pl. Any changes + * made after this point may be overwritten when the script is next run. + */ + +int ERR_load_ASN1_strings(void); + +/* Error codes for the ASN1 functions. */ + +/* Function codes. */ +# define ASN1_F_A2D_ASN1_OBJECT 100 +# define ASN1_F_A2I_ASN1_INTEGER 102 +# define ASN1_F_A2I_ASN1_STRING 103 +# define ASN1_F_APPEND_EXP 176 +# define ASN1_F_ASN1_BIT_STRING_SET_BIT 183 +# define ASN1_F_ASN1_CB 177 +# define ASN1_F_ASN1_CHECK_TLEN 104 +# define ASN1_F_ASN1_COLLECT 106 +# define ASN1_F_ASN1_D2I_EX_PRIMITIVE 108 +# define ASN1_F_ASN1_D2I_FP 109 +# define ASN1_F_ASN1_D2I_READ_BIO 107 +# define ASN1_F_ASN1_DIGEST 184 +# define ASN1_F_ASN1_DO_ADB 110 +# define ASN1_F_ASN1_DO_LOCK 233 +# define ASN1_F_ASN1_DUP 111 +# define ASN1_F_ASN1_EX_C2I 204 +# define ASN1_F_ASN1_FIND_END 190 +# define ASN1_F_ASN1_GENERALIZEDTIME_ADJ 216 +# define ASN1_F_ASN1_GENERATE_V3 178 +# define ASN1_F_ASN1_GET_INT64 224 +# define ASN1_F_ASN1_GET_OBJECT 114 +# define ASN1_F_ASN1_GET_UINT64 225 +# define ASN1_F_ASN1_I2D_BIO 116 +# define ASN1_F_ASN1_I2D_FP 117 +# define ASN1_F_ASN1_ITEM_D2I_FP 206 +# define ASN1_F_ASN1_ITEM_DUP 191 +# define ASN1_F_ASN1_ITEM_EMBED_D2I 120 +# define ASN1_F_ASN1_ITEM_EMBED_NEW 121 +# define ASN1_F_ASN1_ITEM_I2D_BIO 192 +# define ASN1_F_ASN1_ITEM_I2D_FP 193 +# define ASN1_F_ASN1_ITEM_PACK 198 +# define ASN1_F_ASN1_ITEM_SIGN 195 +# define ASN1_F_ASN1_ITEM_SIGN_CTX 220 +# define ASN1_F_ASN1_ITEM_UNPACK 199 +# define ASN1_F_ASN1_ITEM_VERIFY 197 +# define ASN1_F_ASN1_MBSTRING_NCOPY 122 +# define ASN1_F_ASN1_OBJECT_NEW 123 +# define ASN1_F_ASN1_OUTPUT_DATA 214 +# define ASN1_F_ASN1_PCTX_NEW 205 +# define ASN1_F_ASN1_SCTX_NEW 221 +# define ASN1_F_ASN1_SIGN 128 +# define ASN1_F_ASN1_STR2TYPE 179 +# define ASN1_F_ASN1_STRING_GET_INT64 227 +# define ASN1_F_ASN1_STRING_GET_UINT64 230 +# define ASN1_F_ASN1_STRING_SET 186 +# define ASN1_F_ASN1_STRING_TABLE_ADD 129 +# define ASN1_F_ASN1_STRING_TO_BN 228 +# define ASN1_F_ASN1_STRING_TYPE_NEW 130 +# define ASN1_F_ASN1_TEMPLATE_EX_D2I 132 +# define ASN1_F_ASN1_TEMPLATE_NEW 133 +# define ASN1_F_ASN1_TEMPLATE_NOEXP_D2I 131 +# define ASN1_F_ASN1_TIME_ADJ 217 +# define ASN1_F_ASN1_TYPE_GET_INT_OCTETSTRING 134 +# define ASN1_F_ASN1_TYPE_GET_OCTETSTRING 135 +# define ASN1_F_ASN1_UTCTIME_ADJ 218 +# define ASN1_F_ASN1_VERIFY 137 +# define ASN1_F_B64_READ_ASN1 209 +# define ASN1_F_B64_WRITE_ASN1 210 +# define ASN1_F_BIO_NEW_NDEF 208 +# define ASN1_F_BITSTR_CB 180 +# define ASN1_F_BN_TO_ASN1_STRING 229 +# define ASN1_F_C2I_ASN1_BIT_STRING 189 +# define ASN1_F_C2I_ASN1_INTEGER 194 +# define ASN1_F_C2I_ASN1_OBJECT 196 +# define ASN1_F_C2I_IBUF 226 +# define ASN1_F_COLLECT_DATA 140 +# define ASN1_F_D2I_ASN1_OBJECT 147 +# define ASN1_F_D2I_ASN1_UINTEGER 150 +# define ASN1_F_D2I_AUTOPRIVATEKEY 207 +# define ASN1_F_D2I_PRIVATEKEY 154 +# define ASN1_F_D2I_PUBLICKEY 155 +# define ASN1_F_DO_TCREATE 222 +# define ASN1_F_I2D_ASN1_BIO_STREAM 211 +# define ASN1_F_I2D_DSA_PUBKEY 161 +# define ASN1_F_I2D_EC_PUBKEY 181 +# define ASN1_F_I2D_PRIVATEKEY 163 +# define ASN1_F_I2D_PUBLICKEY 164 +# define ASN1_F_I2D_RSA_PUBKEY 165 +# define ASN1_F_LONG_C2I 166 +# define ASN1_F_OID_MODULE_INIT 174 +# define ASN1_F_PARSE_TAGGING 182 +# define ASN1_F_PKCS5_PBE2_SET_IV 167 +# define ASN1_F_PKCS5_PBE2_SET_SCRYPT 231 +# define ASN1_F_PKCS5_PBE_SET 202 +# define ASN1_F_PKCS5_PBE_SET0_ALGOR 215 +# define ASN1_F_PKCS5_PBKDF2_SET 219 +# define ASN1_F_PKCS5_SCRYPT_SET 232 +# define ASN1_F_SMIME_READ_ASN1 212 +# define ASN1_F_SMIME_TEXT 213 +# define ASN1_F_STBL_MODULE_INIT 223 +# define ASN1_F_X509_CRL_ADD0_REVOKED 169 +# define ASN1_F_X509_INFO_NEW 170 +# define ASN1_F_X509_NAME_ENCODE 203 +# define ASN1_F_X509_NAME_EX_D2I 158 +# define ASN1_F_X509_NAME_EX_NEW 171 +# define ASN1_F_X509_PKEY_NEW 173 + +/* Reason codes. */ +# define ASN1_R_ADDING_OBJECT 171 +# define ASN1_R_ASN1_PARSE_ERROR 203 +# define ASN1_R_ASN1_SIG_PARSE_ERROR 204 +# define ASN1_R_AUX_ERROR 100 +# define ASN1_R_BAD_OBJECT_HEADER 102 +# define ASN1_R_BMPSTRING_IS_WRONG_LENGTH 214 +# define ASN1_R_BN_LIB 105 +# define ASN1_R_BOOLEAN_IS_WRONG_LENGTH 106 +# define ASN1_R_BUFFER_TOO_SMALL 107 +# define ASN1_R_CIPHER_HAS_NO_OBJECT_IDENTIFIER 108 +# define ASN1_R_CONTEXT_NOT_INITIALISED 217 +# define ASN1_R_DATA_IS_WRONG 109 +# define ASN1_R_DECODE_ERROR 110 +# define ASN1_R_DEPTH_EXCEEDED 174 +# define ASN1_R_DIGEST_AND_KEY_TYPE_NOT_SUPPORTED 198 +# define ASN1_R_ENCODE_ERROR 112 +# define ASN1_R_ERROR_GETTING_TIME 173 +# define ASN1_R_ERROR_LOADING_SECTION 172 +# define ASN1_R_ERROR_SETTING_CIPHER_PARAMS 114 +# define ASN1_R_EXPECTING_AN_INTEGER 115 +# define ASN1_R_EXPECTING_AN_OBJECT 116 +# define ASN1_R_EXPLICIT_LENGTH_MISMATCH 119 +# define ASN1_R_EXPLICIT_TAG_NOT_CONSTRUCTED 120 +# define ASN1_R_FIELD_MISSING 121 +# define ASN1_R_FIRST_NUM_TOO_LARGE 122 +# define ASN1_R_HEADER_TOO_LONG 123 +# define ASN1_R_ILLEGAL_BITSTRING_FORMAT 175 +# define ASN1_R_ILLEGAL_BOOLEAN 176 +# define ASN1_R_ILLEGAL_CHARACTERS 124 +# define ASN1_R_ILLEGAL_FORMAT 177 +# define ASN1_R_ILLEGAL_HEX 178 +# define ASN1_R_ILLEGAL_IMPLICIT_TAG 179 +# define ASN1_R_ILLEGAL_INTEGER 180 +# define ASN1_R_ILLEGAL_NEGATIVE_VALUE 226 +# define ASN1_R_ILLEGAL_NESTED_TAGGING 181 +# define ASN1_R_ILLEGAL_NULL 125 +# define ASN1_R_ILLEGAL_NULL_VALUE 182 +# define ASN1_R_ILLEGAL_OBJECT 183 +# define ASN1_R_ILLEGAL_OPTIONAL_ANY 126 +# define ASN1_R_ILLEGAL_OPTIONS_ON_ITEM_TEMPLATE 170 +# define ASN1_R_ILLEGAL_PADDING 221 +# define ASN1_R_ILLEGAL_TAGGED_ANY 127 +# define ASN1_R_ILLEGAL_TIME_VALUE 184 +# define ASN1_R_ILLEGAL_ZERO_CONTENT 222 +# define ASN1_R_INTEGER_NOT_ASCII_FORMAT 185 +# define ASN1_R_INTEGER_TOO_LARGE_FOR_LONG 128 +# define ASN1_R_INVALID_BIT_STRING_BITS_LEFT 220 +# define ASN1_R_INVALID_BMPSTRING_LENGTH 129 +# define ASN1_R_INVALID_DIGIT 130 +# define ASN1_R_INVALID_MIME_TYPE 205 +# define ASN1_R_INVALID_MODIFIER 186 +# define ASN1_R_INVALID_NUMBER 187 +# define ASN1_R_INVALID_OBJECT_ENCODING 216 +# define ASN1_R_INVALID_SCRYPT_PARAMETERS 227 +# define ASN1_R_INVALID_SEPARATOR 131 +# define ASN1_R_INVALID_STRING_TABLE_VALUE 218 +# define ASN1_R_INVALID_UNIVERSALSTRING_LENGTH 133 +# define ASN1_R_INVALID_UTF8STRING 134 +# define ASN1_R_INVALID_VALUE 219 +# define ASN1_R_LIST_ERROR 188 +# define ASN1_R_MIME_NO_CONTENT_TYPE 206 +# define ASN1_R_MIME_PARSE_ERROR 207 +# define ASN1_R_MIME_SIG_PARSE_ERROR 208 +# define ASN1_R_MISSING_EOC 137 +# define ASN1_R_MISSING_SECOND_NUMBER 138 +# define ASN1_R_MISSING_VALUE 189 +# define ASN1_R_MSTRING_NOT_UNIVERSAL 139 +# define ASN1_R_MSTRING_WRONG_TAG 140 +# define ASN1_R_NESTED_ASN1_STRING 197 +# define ASN1_R_NON_HEX_CHARACTERS 141 +# define ASN1_R_NOT_ASCII_FORMAT 190 +# define ASN1_R_NOT_ENOUGH_DATA 142 +# define ASN1_R_NO_CONTENT_TYPE 209 +# define ASN1_R_NO_MATCHING_CHOICE_TYPE 143 +# define ASN1_R_NO_MULTIPART_BODY_FAILURE 210 +# define ASN1_R_NO_MULTIPART_BOUNDARY 211 +# define ASN1_R_NO_SIG_CONTENT_TYPE 212 +# define ASN1_R_NULL_IS_WRONG_LENGTH 144 +# define ASN1_R_OBJECT_NOT_ASCII_FORMAT 191 +# define ASN1_R_ODD_NUMBER_OF_CHARS 145 +# define ASN1_R_SECOND_NUMBER_TOO_LARGE 147 +# define ASN1_R_SEQUENCE_LENGTH_MISMATCH 148 +# define ASN1_R_SEQUENCE_NOT_CONSTRUCTED 149 +# define ASN1_R_SEQUENCE_OR_SET_NEEDS_CONFIG 192 +# define ASN1_R_SHORT_LINE 150 +# define ASN1_R_SIG_INVALID_MIME_TYPE 213 +# define ASN1_R_STREAMING_NOT_SUPPORTED 202 +# define ASN1_R_STRING_TOO_LONG 151 +# define ASN1_R_STRING_TOO_SHORT 152 +# define ASN1_R_THE_ASN1_OBJECT_IDENTIFIER_IS_NOT_KNOWN_FOR_THIS_MD 154 +# define ASN1_R_TIME_NOT_ASCII_FORMAT 193 +# define ASN1_R_TOO_LARGE 223 +# define ASN1_R_TOO_LONG 155 +# define ASN1_R_TOO_SMALL 224 +# define ASN1_R_TYPE_NOT_CONSTRUCTED 156 +# define ASN1_R_TYPE_NOT_PRIMITIVE 195 +# define ASN1_R_UNEXPECTED_EOC 159 +# define ASN1_R_UNIVERSALSTRING_IS_WRONG_LENGTH 215 +# define ASN1_R_UNKNOWN_FORMAT 160 +# define ASN1_R_UNKNOWN_MESSAGE_DIGEST_ALGORITHM 161 +# define ASN1_R_UNKNOWN_OBJECT_TYPE 162 +# define ASN1_R_UNKNOWN_PUBLIC_KEY_TYPE 163 +# define ASN1_R_UNKNOWN_SIGNATURE_ALGORITHM 199 +# define ASN1_R_UNKNOWN_TAG 194 +# define ASN1_R_UNSUPPORTED_ANY_DEFINED_BY_TYPE 164 +# define ASN1_R_UNSUPPORTED_PUBLIC_KEY_TYPE 167 +# define ASN1_R_UNSUPPORTED_TYPE 196 +# define ASN1_R_WRONG_INTEGER_TYPE 225 +# define ASN1_R_WRONG_PUBLIC_KEY_TYPE 200 +# define ASN1_R_WRONG_TAG 168 + +# ifdef __cplusplus +} +# endif +#endif diff --git a/mobile/ios/FxA/FxA/include/openssl/asn1_mac.h b/mobile/ios/FxA/FxA/include/openssl/asn1_mac.h new file mode 100644 index 0000000000..7ac1782a3f --- /dev/null +++ b/mobile/ios/FxA/FxA/include/openssl/asn1_mac.h @@ -0,0 +1,10 @@ +/* + * Copyright 2015-2016 The OpenSSL Project Authors. All Rights Reserved. + * + * Licensed under the OpenSSL license (the "License"). You may not use + * this file except in compliance with the License. You can obtain a copy + * in the file LICENSE in the source distribution or at + * https://www.openssl.org/source/license.html + */ + +#error "This file is obsolete; please update your software." diff --git a/mobile/ios/FxA/FxA/include/openssl/asn1t.h b/mobile/ios/FxA/FxA/include/openssl/asn1t.h new file mode 100644 index 0000000000..8eedfb3f43 --- /dev/null +++ b/mobile/ios/FxA/FxA/include/openssl/asn1t.h @@ -0,0 +1,924 @@ +/* + * Copyright 2000-2016 The OpenSSL Project Authors. All Rights Reserved. + * + * Licensed under the OpenSSL license (the "License"). You may not use + * this file except in compliance with the License. You can obtain a copy + * in the file LICENSE in the source distribution or at + * https://www.openssl.org/source/license.html + */ + +#ifndef HEADER_ASN1T_H +# define HEADER_ASN1T_H + +# include +# include +# include + +# ifdef OPENSSL_BUILD_SHLIBCRYPTO +# undef OPENSSL_EXTERN +# define OPENSSL_EXTERN OPENSSL_EXPORT +# endif + +/* ASN1 template defines, structures and functions */ + +#ifdef __cplusplus +extern "C" { +#endif + +# ifndef OPENSSL_EXPORT_VAR_AS_FUNCTION + +/* Macro to obtain ASN1_ADB pointer from a type (only used internally) */ +# define ASN1_ADB_ptr(iptr) ((const ASN1_ADB *)(iptr)) + +/* Macros for start and end of ASN1_ITEM definition */ + +# define ASN1_ITEM_start(itname) \ + OPENSSL_GLOBAL const ASN1_ITEM itname##_it = { + +# define static_ASN1_ITEM_start(itname) \ + static const ASN1_ITEM itname##_it = { + +# define ASN1_ITEM_end(itname) \ + }; + +# else + +/* Macro to obtain ASN1_ADB pointer from a type (only used internally) */ +# define ASN1_ADB_ptr(iptr) ((const ASN1_ADB *)(iptr())) + +/* Macros for start and end of ASN1_ITEM definition */ + +# define ASN1_ITEM_start(itname) \ + const ASN1_ITEM * itname##_it(void) \ + { \ + static const ASN1_ITEM local_it = { + +# define static_ASN1_ITEM_start(itname) \ + static ASN1_ITEM_start(itname) + +# define ASN1_ITEM_end(itname) \ + }; \ + return &local_it; \ + } + +# endif + +/* Macros to aid ASN1 template writing */ + +# define ASN1_ITEM_TEMPLATE(tname) \ + static const ASN1_TEMPLATE tname##_item_tt + +# define ASN1_ITEM_TEMPLATE_END(tname) \ + ;\ + ASN1_ITEM_start(tname) \ + ASN1_ITYPE_PRIMITIVE,\ + -1,\ + &tname##_item_tt,\ + 0,\ + NULL,\ + 0,\ + #tname \ + ASN1_ITEM_end(tname) +# define static_ASN1_ITEM_TEMPLATE_END(tname) \ + ;\ + static_ASN1_ITEM_start(tname) \ + ASN1_ITYPE_PRIMITIVE,\ + -1,\ + &tname##_item_tt,\ + 0,\ + NULL,\ + 0,\ + #tname \ + ASN1_ITEM_end(tname) + +/* This is a ASN1 type which just embeds a template */ + +/*- + * This pair helps declare a SEQUENCE. We can do: + * + * ASN1_SEQUENCE(stname) = { + * ... SEQUENCE components ... + * } ASN1_SEQUENCE_END(stname) + * + * This will produce an ASN1_ITEM called stname_it + * for a structure called stname. + * + * If you want the same structure but a different + * name then use: + * + * ASN1_SEQUENCE(itname) = { + * ... SEQUENCE components ... + * } ASN1_SEQUENCE_END_name(stname, itname) + * + * This will create an item called itname_it using + * a structure called stname. + */ + +# define ASN1_SEQUENCE(tname) \ + static const ASN1_TEMPLATE tname##_seq_tt[] + +# define ASN1_SEQUENCE_END(stname) ASN1_SEQUENCE_END_name(stname, stname) + +# define static_ASN1_SEQUENCE_END(stname) static_ASN1_SEQUENCE_END_name(stname, stname) + +# define ASN1_SEQUENCE_END_name(stname, tname) \ + ;\ + ASN1_ITEM_start(tname) \ + ASN1_ITYPE_SEQUENCE,\ + V_ASN1_SEQUENCE,\ + tname##_seq_tt,\ + sizeof(tname##_seq_tt) / sizeof(ASN1_TEMPLATE),\ + NULL,\ + sizeof(stname),\ + #stname \ + ASN1_ITEM_end(tname) + +# define static_ASN1_SEQUENCE_END_name(stname, tname) \ + ;\ + static_ASN1_ITEM_start(tname) \ + ASN1_ITYPE_SEQUENCE,\ + V_ASN1_SEQUENCE,\ + tname##_seq_tt,\ + sizeof(tname##_seq_tt) / sizeof(ASN1_TEMPLATE),\ + NULL,\ + sizeof(stname),\ + #stname \ + ASN1_ITEM_end(tname) + +# define ASN1_NDEF_SEQUENCE(tname) \ + ASN1_SEQUENCE(tname) + +# define ASN1_NDEF_SEQUENCE_cb(tname, cb) \ + ASN1_SEQUENCE_cb(tname, cb) + +# define ASN1_SEQUENCE_cb(tname, cb) \ + static const ASN1_AUX tname##_aux = {NULL, 0, 0, 0, cb, 0}; \ + ASN1_SEQUENCE(tname) + +# define ASN1_BROKEN_SEQUENCE(tname) \ + static const ASN1_AUX tname##_aux = {NULL, ASN1_AFLG_BROKEN, 0, 0, 0, 0}; \ + ASN1_SEQUENCE(tname) + +# define ASN1_SEQUENCE_ref(tname, cb) \ + static const ASN1_AUX tname##_aux = {NULL, ASN1_AFLG_REFCOUNT, offsetof(tname, references), offsetof(tname, lock), cb, 0}; \ + ASN1_SEQUENCE(tname) + +# define ASN1_SEQUENCE_enc(tname, enc, cb) \ + static const ASN1_AUX tname##_aux = {NULL, ASN1_AFLG_ENCODING, 0, 0, cb, offsetof(tname, enc)}; \ + ASN1_SEQUENCE(tname) + +# define ASN1_NDEF_SEQUENCE_END(tname) \ + ;\ + ASN1_ITEM_start(tname) \ + ASN1_ITYPE_NDEF_SEQUENCE,\ + V_ASN1_SEQUENCE,\ + tname##_seq_tt,\ + sizeof(tname##_seq_tt) / sizeof(ASN1_TEMPLATE),\ + NULL,\ + sizeof(tname),\ + #tname \ + ASN1_ITEM_end(tname) +# define static_ASN1_NDEF_SEQUENCE_END(tname) \ + ;\ + static_ASN1_ITEM_start(tname) \ + ASN1_ITYPE_NDEF_SEQUENCE,\ + V_ASN1_SEQUENCE,\ + tname##_seq_tt,\ + sizeof(tname##_seq_tt) / sizeof(ASN1_TEMPLATE),\ + NULL,\ + sizeof(tname),\ + #tname \ + ASN1_ITEM_end(tname) + +# define ASN1_BROKEN_SEQUENCE_END(stname) ASN1_SEQUENCE_END_ref(stname, stname) +# define static_ASN1_BROKEN_SEQUENCE_END(stname) \ + static_ASN1_SEQUENCE_END_ref(stname, stname) + +# define ASN1_SEQUENCE_END_enc(stname, tname) ASN1_SEQUENCE_END_ref(stname, tname) + +# define ASN1_SEQUENCE_END_cb(stname, tname) ASN1_SEQUENCE_END_ref(stname, tname) +# define static_ASN1_SEQUENCE_END_cb(stname, tname) static_ASN1_SEQUENCE_END_ref(stname, tname) + +# define ASN1_SEQUENCE_END_ref(stname, tname) \ + ;\ + ASN1_ITEM_start(tname) \ + ASN1_ITYPE_SEQUENCE,\ + V_ASN1_SEQUENCE,\ + tname##_seq_tt,\ + sizeof(tname##_seq_tt) / sizeof(ASN1_TEMPLATE),\ + &tname##_aux,\ + sizeof(stname),\ + #stname \ + ASN1_ITEM_end(tname) +# define static_ASN1_SEQUENCE_END_ref(stname, tname) \ + ;\ + static_ASN1_ITEM_start(tname) \ + ASN1_ITYPE_SEQUENCE,\ + V_ASN1_SEQUENCE,\ + tname##_seq_tt,\ + sizeof(tname##_seq_tt) / sizeof(ASN1_TEMPLATE),\ + &tname##_aux,\ + sizeof(stname),\ + #stname \ + ASN1_ITEM_end(tname) + +# define ASN1_NDEF_SEQUENCE_END_cb(stname, tname) \ + ;\ + ASN1_ITEM_start(tname) \ + ASN1_ITYPE_NDEF_SEQUENCE,\ + V_ASN1_SEQUENCE,\ + tname##_seq_tt,\ + sizeof(tname##_seq_tt) / sizeof(ASN1_TEMPLATE),\ + &tname##_aux,\ + sizeof(stname),\ + #stname \ + ASN1_ITEM_end(tname) + +/*- + * This pair helps declare a CHOICE type. We can do: + * + * ASN1_CHOICE(chname) = { + * ... CHOICE options ... + * ASN1_CHOICE_END(chname) + * + * This will produce an ASN1_ITEM called chname_it + * for a structure called chname. The structure + * definition must look like this: + * typedef struct { + * int type; + * union { + * ASN1_SOMETHING *opt1; + * ASN1_SOMEOTHER *opt2; + * } value; + * } chname; + * + * the name of the selector must be 'type'. + * to use an alternative selector name use the + * ASN1_CHOICE_END_selector() version. + */ + +# define ASN1_CHOICE(tname) \ + static const ASN1_TEMPLATE tname##_ch_tt[] + +# define ASN1_CHOICE_cb(tname, cb) \ + static const ASN1_AUX tname##_aux = {NULL, 0, 0, 0, cb, 0}; \ + ASN1_CHOICE(tname) + +# define ASN1_CHOICE_END(stname) ASN1_CHOICE_END_name(stname, stname) + +# define static_ASN1_CHOICE_END(stname) static_ASN1_CHOICE_END_name(stname, stname) + +# define ASN1_CHOICE_END_name(stname, tname) ASN1_CHOICE_END_selector(stname, tname, type) + +# define static_ASN1_CHOICE_END_name(stname, tname) static_ASN1_CHOICE_END_selector(stname, tname, type) + +# define ASN1_CHOICE_END_selector(stname, tname, selname) \ + ;\ + ASN1_ITEM_start(tname) \ + ASN1_ITYPE_CHOICE,\ + offsetof(stname,selname) ,\ + tname##_ch_tt,\ + sizeof(tname##_ch_tt) / sizeof(ASN1_TEMPLATE),\ + NULL,\ + sizeof(stname),\ + #stname \ + ASN1_ITEM_end(tname) + +# define static_ASN1_CHOICE_END_selector(stname, tname, selname) \ + ;\ + static_ASN1_ITEM_start(tname) \ + ASN1_ITYPE_CHOICE,\ + offsetof(stname,selname) ,\ + tname##_ch_tt,\ + sizeof(tname##_ch_tt) / sizeof(ASN1_TEMPLATE),\ + NULL,\ + sizeof(stname),\ + #stname \ + ASN1_ITEM_end(tname) + +# define ASN1_CHOICE_END_cb(stname, tname, selname) \ + ;\ + ASN1_ITEM_start(tname) \ + ASN1_ITYPE_CHOICE,\ + offsetof(stname,selname) ,\ + tname##_ch_tt,\ + sizeof(tname##_ch_tt) / sizeof(ASN1_TEMPLATE),\ + &tname##_aux,\ + sizeof(stname),\ + #stname \ + ASN1_ITEM_end(tname) + +/* This helps with the template wrapper form of ASN1_ITEM */ + +# define ASN1_EX_TEMPLATE_TYPE(flags, tag, name, type) { \ + (flags), (tag), 0,\ + #name, ASN1_ITEM_ref(type) } + +/* These help with SEQUENCE or CHOICE components */ + +/* used to declare other types */ + +# define ASN1_EX_TYPE(flags, tag, stname, field, type) { \ + (flags), (tag), offsetof(stname, field),\ + #field, ASN1_ITEM_ref(type) } + +/* implicit and explicit helper macros */ + +# define ASN1_IMP_EX(stname, field, type, tag, ex) \ + ASN1_EX_TYPE(ASN1_TFLG_IMPLICIT | ex, tag, stname, field, type) + +# define ASN1_EXP_EX(stname, field, type, tag, ex) \ + ASN1_EX_TYPE(ASN1_TFLG_EXPLICIT | ex, tag, stname, field, type) + +/* Any defined by macros: the field used is in the table itself */ + +# ifndef OPENSSL_EXPORT_VAR_AS_FUNCTION +# define ASN1_ADB_OBJECT(tblname) { ASN1_TFLG_ADB_OID, -1, 0, #tblname, (const ASN1_ITEM *)&(tblname##_adb) } +# define ASN1_ADB_INTEGER(tblname) { ASN1_TFLG_ADB_INT, -1, 0, #tblname, (const ASN1_ITEM *)&(tblname##_adb) } +# else +# define ASN1_ADB_OBJECT(tblname) { ASN1_TFLG_ADB_OID, -1, 0, #tblname, tblname##_adb } +# define ASN1_ADB_INTEGER(tblname) { ASN1_TFLG_ADB_INT, -1, 0, #tblname, tblname##_adb } +# endif +/* Plain simple type */ +# define ASN1_SIMPLE(stname, field, type) ASN1_EX_TYPE(0,0, stname, field, type) +/* Embedded simple type */ +# define ASN1_EMBED(stname, field, type) ASN1_EX_TYPE(ASN1_TFLG_EMBED,0, stname, field, type) + +/* OPTIONAL simple type */ +# define ASN1_OPT(stname, field, type) ASN1_EX_TYPE(ASN1_TFLG_OPTIONAL, 0, stname, field, type) + +/* IMPLICIT tagged simple type */ +# define ASN1_IMP(stname, field, type, tag) ASN1_IMP_EX(stname, field, type, tag, 0) + +/* IMPLICIT tagged OPTIONAL simple type */ +# define ASN1_IMP_OPT(stname, field, type, tag) ASN1_IMP_EX(stname, field, type, tag, ASN1_TFLG_OPTIONAL) + +/* Same as above but EXPLICIT */ + +# define ASN1_EXP(stname, field, type, tag) ASN1_EXP_EX(stname, field, type, tag, 0) +# define ASN1_EXP_OPT(stname, field, type, tag) ASN1_EXP_EX(stname, field, type, tag, ASN1_TFLG_OPTIONAL) + +/* SEQUENCE OF type */ +# define ASN1_SEQUENCE_OF(stname, field, type) \ + ASN1_EX_TYPE(ASN1_TFLG_SEQUENCE_OF, 0, stname, field, type) + +/* OPTIONAL SEQUENCE OF */ +# define ASN1_SEQUENCE_OF_OPT(stname, field, type) \ + ASN1_EX_TYPE(ASN1_TFLG_SEQUENCE_OF|ASN1_TFLG_OPTIONAL, 0, stname, field, type) + +/* Same as above but for SET OF */ + +# define ASN1_SET_OF(stname, field, type) \ + ASN1_EX_TYPE(ASN1_TFLG_SET_OF, 0, stname, field, type) + +# define ASN1_SET_OF_OPT(stname, field, type) \ + ASN1_EX_TYPE(ASN1_TFLG_SET_OF|ASN1_TFLG_OPTIONAL, 0, stname, field, type) + +/* Finally compound types of SEQUENCE, SET, IMPLICIT, EXPLICIT and OPTIONAL */ + +# define ASN1_IMP_SET_OF(stname, field, type, tag) \ + ASN1_IMP_EX(stname, field, type, tag, ASN1_TFLG_SET_OF) + +# define ASN1_EXP_SET_OF(stname, field, type, tag) \ + ASN1_EXP_EX(stname, field, type, tag, ASN1_TFLG_SET_OF) + +# define ASN1_IMP_SET_OF_OPT(stname, field, type, tag) \ + ASN1_IMP_EX(stname, field, type, tag, ASN1_TFLG_SET_OF|ASN1_TFLG_OPTIONAL) + +# define ASN1_EXP_SET_OF_OPT(stname, field, type, tag) \ + ASN1_EXP_EX(stname, field, type, tag, ASN1_TFLG_SET_OF|ASN1_TFLG_OPTIONAL) + +# define ASN1_IMP_SEQUENCE_OF(stname, field, type, tag) \ + ASN1_IMP_EX(stname, field, type, tag, ASN1_TFLG_SEQUENCE_OF) + +# define ASN1_IMP_SEQUENCE_OF_OPT(stname, field, type, tag) \ + ASN1_IMP_EX(stname, field, type, tag, ASN1_TFLG_SEQUENCE_OF|ASN1_TFLG_OPTIONAL) + +# define ASN1_EXP_SEQUENCE_OF(stname, field, type, tag) \ + ASN1_EXP_EX(stname, field, type, tag, ASN1_TFLG_SEQUENCE_OF) + +# define ASN1_EXP_SEQUENCE_OF_OPT(stname, field, type, tag) \ + ASN1_EXP_EX(stname, field, type, tag, ASN1_TFLG_SEQUENCE_OF|ASN1_TFLG_OPTIONAL) + +/* EXPLICIT using indefinite length constructed form */ +# define ASN1_NDEF_EXP(stname, field, type, tag) \ + ASN1_EXP_EX(stname, field, type, tag, ASN1_TFLG_NDEF) + +/* EXPLICIT OPTIONAL using indefinite length constructed form */ +# define ASN1_NDEF_EXP_OPT(stname, field, type, tag) \ + ASN1_EXP_EX(stname, field, type, tag, ASN1_TFLG_OPTIONAL|ASN1_TFLG_NDEF) + +/* Macros for the ASN1_ADB structure */ + +# define ASN1_ADB(name) \ + static const ASN1_ADB_TABLE name##_adbtbl[] + +# ifndef OPENSSL_EXPORT_VAR_AS_FUNCTION + +# define ASN1_ADB_END(name, flags, field, adb_cb, def, none) \ + ;\ + static const ASN1_ADB name##_adb = {\ + flags,\ + offsetof(name, field),\ + adb_cb,\ + name##_adbtbl,\ + sizeof(name##_adbtbl) / sizeof(ASN1_ADB_TABLE),\ + def,\ + none\ + } + +# else + +# define ASN1_ADB_END(name, flags, field, adb_cb, def, none) \ + ;\ + static const ASN1_ITEM *name##_adb(void) \ + { \ + static const ASN1_ADB internal_adb = \ + {\ + flags,\ + offsetof(name, field),\ + adb_cb,\ + name##_adbtbl,\ + sizeof(name##_adbtbl) / sizeof(ASN1_ADB_TABLE),\ + def,\ + none\ + }; \ + return (const ASN1_ITEM *) &internal_adb; \ + } \ + void dummy_function(void) + +# endif + +# define ADB_ENTRY(val, template) {val, template} + +# define ASN1_ADB_TEMPLATE(name) \ + static const ASN1_TEMPLATE name##_tt + +/* + * This is the ASN1 template structure that defines a wrapper round the + * actual type. It determines the actual position of the field in the value + * structure, various flags such as OPTIONAL and the field name. + */ + +struct ASN1_TEMPLATE_st { + unsigned long flags; /* Various flags */ + long tag; /* tag, not used if no tagging */ + unsigned long offset; /* Offset of this field in structure */ + const char *field_name; /* Field name */ + ASN1_ITEM_EXP *item; /* Relevant ASN1_ITEM or ASN1_ADB */ +}; + +/* Macro to extract ASN1_ITEM and ASN1_ADB pointer from ASN1_TEMPLATE */ + +# define ASN1_TEMPLATE_item(t) (t->item_ptr) +# define ASN1_TEMPLATE_adb(t) (t->item_ptr) + +typedef struct ASN1_ADB_TABLE_st ASN1_ADB_TABLE; +typedef struct ASN1_ADB_st ASN1_ADB; + +struct ASN1_ADB_st { + unsigned long flags; /* Various flags */ + unsigned long offset; /* Offset of selector field */ + int (*adb_cb)(long *psel); /* Application callback */ + const ASN1_ADB_TABLE *tbl; /* Table of possible types */ + long tblcount; /* Number of entries in tbl */ + const ASN1_TEMPLATE *default_tt; /* Type to use if no match */ + const ASN1_TEMPLATE *null_tt; /* Type to use if selector is NULL */ +}; + +struct ASN1_ADB_TABLE_st { + long value; /* NID for an object or value for an int */ + const ASN1_TEMPLATE tt; /* item for this value */ +}; + +/* template flags */ + +/* Field is optional */ +# define ASN1_TFLG_OPTIONAL (0x1) + +/* Field is a SET OF */ +# define ASN1_TFLG_SET_OF (0x1 << 1) + +/* Field is a SEQUENCE OF */ +# define ASN1_TFLG_SEQUENCE_OF (0x2 << 1) + +/* + * Special case: this refers to a SET OF that will be sorted into DER order + * when encoded *and* the corresponding STACK will be modified to match the + * new order. + */ +# define ASN1_TFLG_SET_ORDER (0x3 << 1) + +/* Mask for SET OF or SEQUENCE OF */ +# define ASN1_TFLG_SK_MASK (0x3 << 1) + +/* + * These flags mean the tag should be taken from the tag field. If EXPLICIT + * then the underlying type is used for the inner tag. + */ + +/* IMPLICIT tagging */ +# define ASN1_TFLG_IMPTAG (0x1 << 3) + +/* EXPLICIT tagging, inner tag from underlying type */ +# define ASN1_TFLG_EXPTAG (0x2 << 3) + +# define ASN1_TFLG_TAG_MASK (0x3 << 3) + +/* context specific IMPLICIT */ +# define ASN1_TFLG_IMPLICIT ASN1_TFLG_IMPTAG|ASN1_TFLG_CONTEXT + +/* context specific EXPLICIT */ +# define ASN1_TFLG_EXPLICIT ASN1_TFLG_EXPTAG|ASN1_TFLG_CONTEXT + +/* + * If tagging is in force these determine the type of tag to use. Otherwise + * the tag is determined by the underlying type. These values reflect the + * actual octet format. + */ + +/* Universal tag */ +# define ASN1_TFLG_UNIVERSAL (0x0<<6) +/* Application tag */ +# define ASN1_TFLG_APPLICATION (0x1<<6) +/* Context specific tag */ +# define ASN1_TFLG_CONTEXT (0x2<<6) +/* Private tag */ +# define ASN1_TFLG_PRIVATE (0x3<<6) + +# define ASN1_TFLG_TAG_CLASS (0x3<<6) + +/* + * These are for ANY DEFINED BY type. In this case the 'item' field points to + * an ASN1_ADB structure which contains a table of values to decode the + * relevant type + */ + +# define ASN1_TFLG_ADB_MASK (0x3<<8) + +# define ASN1_TFLG_ADB_OID (0x1<<8) + +# define ASN1_TFLG_ADB_INT (0x1<<9) + +/* + * This flag when present in a SEQUENCE OF, SET OF or EXPLICIT causes + * indefinite length constructed encoding to be used if required. + */ + +# define ASN1_TFLG_NDEF (0x1<<11) + +/* Field is embedded and not a pointer */ +# define ASN1_TFLG_EMBED (0x1 << 12) + +/* This is the actual ASN1 item itself */ + +struct ASN1_ITEM_st { + char itype; /* The item type, primitive, SEQUENCE, CHOICE + * or extern */ + long utype; /* underlying type */ + const ASN1_TEMPLATE *templates; /* If SEQUENCE or CHOICE this contains + * the contents */ + long tcount; /* Number of templates if SEQUENCE or CHOICE */ + const void *funcs; /* functions that handle this type */ + long size; /* Structure size (usually) */ + const char *sname; /* Structure name */ +}; + +/*- + * These are values for the itype field and + * determine how the type is interpreted. + * + * For PRIMITIVE types the underlying type + * determines the behaviour if items is NULL. + * + * Otherwise templates must contain a single + * template and the type is treated in the + * same way as the type specified in the template. + * + * For SEQUENCE types the templates field points + * to the members, the size field is the + * structure size. + * + * For CHOICE types the templates field points + * to each possible member (typically a union) + * and the 'size' field is the offset of the + * selector. + * + * The 'funcs' field is used for application + * specific functions. + * + * The EXTERN type uses a new style d2i/i2d. + * The new style should be used where possible + * because it avoids things like the d2i IMPLICIT + * hack. + * + * MSTRING is a multiple string type, it is used + * for a CHOICE of character strings where the + * actual strings all occupy an ASN1_STRING + * structure. In this case the 'utype' field + * has a special meaning, it is used as a mask + * of acceptable types using the B_ASN1 constants. + * + * NDEF_SEQUENCE is the same as SEQUENCE except + * that it will use indefinite length constructed + * encoding if requested. + * + */ + +# define ASN1_ITYPE_PRIMITIVE 0x0 + +# define ASN1_ITYPE_SEQUENCE 0x1 + +# define ASN1_ITYPE_CHOICE 0x2 + +# define ASN1_ITYPE_EXTERN 0x4 + +# define ASN1_ITYPE_MSTRING 0x5 + +# define ASN1_ITYPE_NDEF_SEQUENCE 0x6 + +/* + * Cache for ASN1 tag and length, so we don't keep re-reading it for things + * like CHOICE + */ + +struct ASN1_TLC_st { + char valid; /* Values below are valid */ + int ret; /* return value */ + long plen; /* length */ + int ptag; /* class value */ + int pclass; /* class value */ + int hdrlen; /* header length */ +}; + +/* Typedefs for ASN1 function pointers */ +typedef int ASN1_ex_d2i(ASN1_VALUE **pval, const unsigned char **in, long len, + const ASN1_ITEM *it, int tag, int aclass, char opt, + ASN1_TLC *ctx); + +typedef int ASN1_ex_i2d(ASN1_VALUE **pval, unsigned char **out, + const ASN1_ITEM *it, int tag, int aclass); +typedef int ASN1_ex_new_func(ASN1_VALUE **pval, const ASN1_ITEM *it); +typedef void ASN1_ex_free_func(ASN1_VALUE **pval, const ASN1_ITEM *it); + +typedef int ASN1_ex_print_func(BIO *out, ASN1_VALUE **pval, + int indent, const char *fname, + const ASN1_PCTX *pctx); + +typedef int ASN1_primitive_i2c(ASN1_VALUE **pval, unsigned char *cont, + int *putype, const ASN1_ITEM *it); +typedef int ASN1_primitive_c2i(ASN1_VALUE **pval, const unsigned char *cont, + int len, int utype, char *free_cont, + const ASN1_ITEM *it); +typedef int ASN1_primitive_print(BIO *out, ASN1_VALUE **pval, + const ASN1_ITEM *it, int indent, + const ASN1_PCTX *pctx); + +typedef struct ASN1_EXTERN_FUNCS_st { + void *app_data; + ASN1_ex_new_func *asn1_ex_new; + ASN1_ex_free_func *asn1_ex_free; + ASN1_ex_free_func *asn1_ex_clear; + ASN1_ex_d2i *asn1_ex_d2i; + ASN1_ex_i2d *asn1_ex_i2d; + ASN1_ex_print_func *asn1_ex_print; +} ASN1_EXTERN_FUNCS; + +typedef struct ASN1_PRIMITIVE_FUNCS_st { + void *app_data; + unsigned long flags; + ASN1_ex_new_func *prim_new; + ASN1_ex_free_func *prim_free; + ASN1_ex_free_func *prim_clear; + ASN1_primitive_c2i *prim_c2i; + ASN1_primitive_i2c *prim_i2c; + ASN1_primitive_print *prim_print; +} ASN1_PRIMITIVE_FUNCS; + +/* + * This is the ASN1_AUX structure: it handles various miscellaneous + * requirements. For example the use of reference counts and an informational + * callback. The "informational callback" is called at various points during + * the ASN1 encoding and decoding. It can be used to provide minor + * customisation of the structures used. This is most useful where the + * supplied routines *almost* do the right thing but need some extra help at + * a few points. If the callback returns zero then it is assumed a fatal + * error has occurred and the main operation should be abandoned. If major + * changes in the default behaviour are required then an external type is + * more appropriate. + */ + +typedef int ASN1_aux_cb(int operation, ASN1_VALUE **in, const ASN1_ITEM *it, + void *exarg); + +typedef struct ASN1_AUX_st { + void *app_data; + int flags; + int ref_offset; /* Offset of reference value */ + int ref_lock; /* Lock type to use */ + ASN1_aux_cb *asn1_cb; + int enc_offset; /* Offset of ASN1_ENCODING structure */ +} ASN1_AUX; + +/* For print related callbacks exarg points to this structure */ +typedef struct ASN1_PRINT_ARG_st { + BIO *out; + int indent; + const ASN1_PCTX *pctx; +} ASN1_PRINT_ARG; + +/* For streaming related callbacks exarg points to this structure */ +typedef struct ASN1_STREAM_ARG_st { + /* BIO to stream through */ + BIO *out; + /* BIO with filters appended */ + BIO *ndef_bio; + /* Streaming I/O boundary */ + unsigned char **boundary; +} ASN1_STREAM_ARG; + +/* Flags in ASN1_AUX */ + +/* Use a reference count */ +# define ASN1_AFLG_REFCOUNT 1 +/* Save the encoding of structure (useful for signatures) */ +# define ASN1_AFLG_ENCODING 2 +/* The Sequence length is invalid */ +# define ASN1_AFLG_BROKEN 4 + +/* operation values for asn1_cb */ + +# define ASN1_OP_NEW_PRE 0 +# define ASN1_OP_NEW_POST 1 +# define ASN1_OP_FREE_PRE 2 +# define ASN1_OP_FREE_POST 3 +# define ASN1_OP_D2I_PRE 4 +# define ASN1_OP_D2I_POST 5 +# define ASN1_OP_I2D_PRE 6 +# define ASN1_OP_I2D_POST 7 +# define ASN1_OP_PRINT_PRE 8 +# define ASN1_OP_PRINT_POST 9 +# define ASN1_OP_STREAM_PRE 10 +# define ASN1_OP_STREAM_POST 11 +# define ASN1_OP_DETACHED_PRE 12 +# define ASN1_OP_DETACHED_POST 13 + +/* Macro to implement a primitive type */ +# define IMPLEMENT_ASN1_TYPE(stname) IMPLEMENT_ASN1_TYPE_ex(stname, stname, 0) +# define IMPLEMENT_ASN1_TYPE_ex(itname, vname, ex) \ + ASN1_ITEM_start(itname) \ + ASN1_ITYPE_PRIMITIVE, V_##vname, NULL, 0, NULL, ex, #itname \ + ASN1_ITEM_end(itname) + +/* Macro to implement a multi string type */ +# define IMPLEMENT_ASN1_MSTRING(itname, mask) \ + ASN1_ITEM_start(itname) \ + ASN1_ITYPE_MSTRING, mask, NULL, 0, NULL, sizeof(ASN1_STRING), #itname \ + ASN1_ITEM_end(itname) + +# define IMPLEMENT_EXTERN_ASN1(sname, tag, fptrs) \ + ASN1_ITEM_start(sname) \ + ASN1_ITYPE_EXTERN, \ + tag, \ + NULL, \ + 0, \ + &fptrs, \ + 0, \ + #sname \ + ASN1_ITEM_end(sname) + +/* Macro to implement standard functions in terms of ASN1_ITEM structures */ + +# define IMPLEMENT_ASN1_FUNCTIONS(stname) IMPLEMENT_ASN1_FUNCTIONS_fname(stname, stname, stname) + +# define IMPLEMENT_ASN1_FUNCTIONS_name(stname, itname) IMPLEMENT_ASN1_FUNCTIONS_fname(stname, itname, itname) + +# define IMPLEMENT_ASN1_FUNCTIONS_ENCODE_name(stname, itname) \ + IMPLEMENT_ASN1_FUNCTIONS_ENCODE_fname(stname, itname, itname) + +# define IMPLEMENT_STATIC_ASN1_ALLOC_FUNCTIONS(stname) \ + IMPLEMENT_ASN1_ALLOC_FUNCTIONS_pfname(static, stname, stname, stname) + +# define IMPLEMENT_ASN1_ALLOC_FUNCTIONS(stname) \ + IMPLEMENT_ASN1_ALLOC_FUNCTIONS_fname(stname, stname, stname) + +# define IMPLEMENT_ASN1_ALLOC_FUNCTIONS_pfname(pre, stname, itname, fname) \ + pre stname *fname##_new(void) \ + { \ + return (stname *)ASN1_item_new(ASN1_ITEM_rptr(itname)); \ + } \ + pre void fname##_free(stname *a) \ + { \ + ASN1_item_free((ASN1_VALUE *)a, ASN1_ITEM_rptr(itname)); \ + } + +# define IMPLEMENT_ASN1_ALLOC_FUNCTIONS_fname(stname, itname, fname) \ + stname *fname##_new(void) \ + { \ + return (stname *)ASN1_item_new(ASN1_ITEM_rptr(itname)); \ + } \ + void fname##_free(stname *a) \ + { \ + ASN1_item_free((ASN1_VALUE *)a, ASN1_ITEM_rptr(itname)); \ + } + +# define IMPLEMENT_ASN1_FUNCTIONS_fname(stname, itname, fname) \ + IMPLEMENT_ASN1_ENCODE_FUNCTIONS_fname(stname, itname, fname) \ + IMPLEMENT_ASN1_ALLOC_FUNCTIONS_fname(stname, itname, fname) + +# define IMPLEMENT_ASN1_ENCODE_FUNCTIONS_fname(stname, itname, fname) \ + stname *d2i_##fname(stname **a, const unsigned char **in, long len) \ + { \ + return (stname *)ASN1_item_d2i((ASN1_VALUE **)a, in, len, ASN1_ITEM_rptr(itname));\ + } \ + int i2d_##fname(stname *a, unsigned char **out) \ + { \ + return ASN1_item_i2d((ASN1_VALUE *)a, out, ASN1_ITEM_rptr(itname));\ + } + +# define IMPLEMENT_ASN1_NDEF_FUNCTION(stname) \ + int i2d_##stname##_NDEF(stname *a, unsigned char **out) \ + { \ + return ASN1_item_ndef_i2d((ASN1_VALUE *)a, out, ASN1_ITEM_rptr(stname));\ + } + +# define IMPLEMENT_STATIC_ASN1_ENCODE_FUNCTIONS(stname) \ + static stname *d2i_##stname(stname **a, \ + const unsigned char **in, long len) \ + { \ + return (stname *)ASN1_item_d2i((ASN1_VALUE **)a, in, len, \ + ASN1_ITEM_rptr(stname)); \ + } \ + static int i2d_##stname(stname *a, unsigned char **out) \ + { \ + return ASN1_item_i2d((ASN1_VALUE *)a, out, \ + ASN1_ITEM_rptr(stname)); \ + } + +/* + * This includes evil casts to remove const: they will go away when full ASN1 + * constification is done. + */ +# define IMPLEMENT_ASN1_ENCODE_FUNCTIONS_const_fname(stname, itname, fname) \ + stname *d2i_##fname(stname **a, const unsigned char **in, long len) \ + { \ + return (stname *)ASN1_item_d2i((ASN1_VALUE **)a, in, len, ASN1_ITEM_rptr(itname));\ + } \ + int i2d_##fname(const stname *a, unsigned char **out) \ + { \ + return ASN1_item_i2d((ASN1_VALUE *)a, out, ASN1_ITEM_rptr(itname));\ + } + +# define IMPLEMENT_ASN1_DUP_FUNCTION(stname) \ + stname * stname##_dup(stname *x) \ + { \ + return ASN1_item_dup(ASN1_ITEM_rptr(stname), x); \ + } + +# define IMPLEMENT_ASN1_PRINT_FUNCTION(stname) \ + IMPLEMENT_ASN1_PRINT_FUNCTION_fname(stname, stname, stname) + +# define IMPLEMENT_ASN1_PRINT_FUNCTION_fname(stname, itname, fname) \ + int fname##_print_ctx(BIO *out, stname *x, int indent, \ + const ASN1_PCTX *pctx) \ + { \ + return ASN1_item_print(out, (ASN1_VALUE *)x, indent, \ + ASN1_ITEM_rptr(itname), pctx); \ + } + +# define IMPLEMENT_ASN1_FUNCTIONS_const(name) \ + IMPLEMENT_ASN1_FUNCTIONS_const_fname(name, name, name) + +# define IMPLEMENT_ASN1_FUNCTIONS_const_fname(stname, itname, fname) \ + IMPLEMENT_ASN1_ENCODE_FUNCTIONS_const_fname(stname, itname, fname) \ + IMPLEMENT_ASN1_ALLOC_FUNCTIONS_fname(stname, itname, fname) + +/* external definitions for primitive types */ + +DECLARE_ASN1_ITEM(ASN1_BOOLEAN) +DECLARE_ASN1_ITEM(ASN1_TBOOLEAN) +DECLARE_ASN1_ITEM(ASN1_FBOOLEAN) +DECLARE_ASN1_ITEM(ASN1_SEQUENCE) +DECLARE_ASN1_ITEM(CBIGNUM) +DECLARE_ASN1_ITEM(BIGNUM) +DECLARE_ASN1_ITEM(LONG) +DECLARE_ASN1_ITEM(ZLONG) + +DEFINE_STACK_OF(ASN1_VALUE) + +/* Functions used internally by the ASN1 code */ + +int ASN1_item_ex_new(ASN1_VALUE **pval, const ASN1_ITEM *it); +void ASN1_item_ex_free(ASN1_VALUE **pval, const ASN1_ITEM *it); + +int ASN1_item_ex_d2i(ASN1_VALUE **pval, const unsigned char **in, long len, + const ASN1_ITEM *it, int tag, int aclass, char opt, + ASN1_TLC *ctx); + +int ASN1_item_ex_i2d(ASN1_VALUE **pval, unsigned char **out, + const ASN1_ITEM *it, int tag, int aclass); + +#ifdef __cplusplus +} +#endif +#endif diff --git a/mobile/ios/FxA/FxA/include/openssl/async.h b/mobile/ios/FxA/FxA/include/openssl/async.h new file mode 100644 index 0000000000..5b2e496dbd --- /dev/null +++ b/mobile/ios/FxA/FxA/include/openssl/async.h @@ -0,0 +1,98 @@ +/* + * Copyright 2015-2016 The OpenSSL Project Authors. All Rights Reserved. + * + * Licensed under the OpenSSL license (the "License"). You may not use + * this file except in compliance with the License. You can obtain a copy + * in the file LICENSE in the source distribution or at + * https://www.openssl.org/source/license.html + */ + +#include + +#ifndef HEADER_ASYNC_H +# define HEADER_ASYNC_H + +#if defined(_WIN32) +# if defined(BASETYPES) || defined(_WINDEF_H) +/* application has to include to use this */ +#define OSSL_ASYNC_FD HANDLE +#define OSSL_BAD_ASYNC_FD INVALID_HANDLE_VALUE +# endif +#else +#define OSSL_ASYNC_FD int +#define OSSL_BAD_ASYNC_FD -1 +#endif + + +# ifdef __cplusplus +extern "C" { +# endif + +typedef struct async_job_st ASYNC_JOB; +typedef struct async_wait_ctx_st ASYNC_WAIT_CTX; + +#define ASYNC_ERR 0 +#define ASYNC_NO_JOBS 1 +#define ASYNC_PAUSE 2 +#define ASYNC_FINISH 3 + +int ASYNC_init_thread(size_t max_size, size_t init_size); +void ASYNC_cleanup_thread(void); + +#ifdef OSSL_ASYNC_FD +ASYNC_WAIT_CTX *ASYNC_WAIT_CTX_new(void); +void ASYNC_WAIT_CTX_free(ASYNC_WAIT_CTX *ctx); +int ASYNC_WAIT_CTX_set_wait_fd(ASYNC_WAIT_CTX *ctx, const void *key, + OSSL_ASYNC_FD fd, + void *custom_data, + void (*cleanup)(ASYNC_WAIT_CTX *, const void *, + OSSL_ASYNC_FD, void *)); +int ASYNC_WAIT_CTX_get_fd(ASYNC_WAIT_CTX *ctx, const void *key, + OSSL_ASYNC_FD *fd, void **custom_data); +int ASYNC_WAIT_CTX_get_all_fds(ASYNC_WAIT_CTX *ctx, OSSL_ASYNC_FD *fd, + size_t *numfds); +int ASYNC_WAIT_CTX_get_changed_fds(ASYNC_WAIT_CTX *ctx, OSSL_ASYNC_FD *addfd, + size_t *numaddfds, OSSL_ASYNC_FD *delfd, + size_t *numdelfds); +int ASYNC_WAIT_CTX_clear_fd(ASYNC_WAIT_CTX *ctx, const void *key); +#endif + +int ASYNC_is_capable(void); + +int ASYNC_start_job(ASYNC_JOB **job, ASYNC_WAIT_CTX *ctx, int *ret, + int (*func)(void *), void *args, size_t size); +int ASYNC_pause_job(void); + +ASYNC_JOB *ASYNC_get_current_job(void); +ASYNC_WAIT_CTX *ASYNC_get_wait_ctx(ASYNC_JOB *job); +void ASYNC_block_pause(void); +void ASYNC_unblock_pause(void); + +/* BEGIN ERROR CODES */ +/* + * The following lines are auto generated by the script mkerr.pl. Any changes + * made after this point may be overwritten when the script is next run. + */ + +int ERR_load_ASYNC_strings(void); + +/* Error codes for the ASYNC functions. */ + +/* Function codes. */ +# define ASYNC_F_ASYNC_CTX_NEW 100 +# define ASYNC_F_ASYNC_INIT_THREAD 101 +# define ASYNC_F_ASYNC_JOB_NEW 102 +# define ASYNC_F_ASYNC_PAUSE_JOB 103 +# define ASYNC_F_ASYNC_START_FUNC 104 +# define ASYNC_F_ASYNC_START_JOB 105 + +/* Reason codes. */ +# define ASYNC_R_FAILED_TO_SET_POOL 101 +# define ASYNC_R_FAILED_TO_SWAP_CONTEXT 102 +# define ASYNC_R_INIT_FAILED 105 +# define ASYNC_R_INVALID_POOL_SIZE 103 + +# ifdef __cplusplus +} +# endif +#endif diff --git a/mobile/ios/FxA/FxA/include/openssl/bio.h b/mobile/ios/FxA/FxA/include/openssl/bio.h new file mode 100644 index 0000000000..9bc941b25f --- /dev/null +++ b/mobile/ios/FxA/FxA/include/openssl/bio.h @@ -0,0 +1,854 @@ +/* + * Copyright 1995-2016 The OpenSSL Project Authors. All Rights Reserved. + * + * Licensed under the OpenSSL license (the "License"). You may not use + * this file except in compliance with the License. You can obtain a copy + * in the file LICENSE in the source distribution or at + * https://www.openssl.org/source/license.html + */ + +#ifndef HEADER_BIO_H +# define HEADER_BIO_H + +# include + +# ifndef OPENSSL_NO_STDIO +# include +# endif +# include + +# include + +# ifndef OPENSSL_NO_SCTP +# ifndef OPENSSL_SYS_VMS +# include +# else +# include +# endif +# endif + +#ifdef __cplusplus +extern "C" { +#endif + +/* There are the classes of BIOs */ +# define BIO_TYPE_DESCRIPTOR 0x0100 /* socket, fd, connect or accept */ +# define BIO_TYPE_FILTER 0x0200 +# define BIO_TYPE_SOURCE_SINK 0x0400 + +/* These are the 'types' of BIOs */ +# define BIO_TYPE_NONE 0 +# define BIO_TYPE_MEM ( 1|BIO_TYPE_SOURCE_SINK) +# define BIO_TYPE_FILE ( 2|BIO_TYPE_SOURCE_SINK) + +# define BIO_TYPE_FD ( 4|BIO_TYPE_SOURCE_SINK|BIO_TYPE_DESCRIPTOR) +# define BIO_TYPE_SOCKET ( 5|BIO_TYPE_SOURCE_SINK|BIO_TYPE_DESCRIPTOR) +# define BIO_TYPE_NULL ( 6|BIO_TYPE_SOURCE_SINK) +# define BIO_TYPE_SSL ( 7|BIO_TYPE_FILTER) +# define BIO_TYPE_MD ( 8|BIO_TYPE_FILTER) +# define BIO_TYPE_BUFFER ( 9|BIO_TYPE_FILTER) +# define BIO_TYPE_CIPHER (10|BIO_TYPE_FILTER) +# define BIO_TYPE_BASE64 (11|BIO_TYPE_FILTER) +# define BIO_TYPE_CONNECT (12|BIO_TYPE_SOURCE_SINK|BIO_TYPE_DESCRIPTOR) +# define BIO_TYPE_ACCEPT (13|BIO_TYPE_SOURCE_SINK|BIO_TYPE_DESCRIPTOR) + +# define BIO_TYPE_NBIO_TEST (16|BIO_TYPE_FILTER)/* server proxy BIO */ +# define BIO_TYPE_NULL_FILTER (17|BIO_TYPE_FILTER) +# define BIO_TYPE_BIO (19|BIO_TYPE_SOURCE_SINK)/* half a BIO pair */ +# define BIO_TYPE_LINEBUFFER (20|BIO_TYPE_FILTER) +# define BIO_TYPE_DGRAM (21|BIO_TYPE_SOURCE_SINK|BIO_TYPE_DESCRIPTOR) +# define BIO_TYPE_ASN1 (22|BIO_TYPE_FILTER) +# define BIO_TYPE_COMP (23|BIO_TYPE_FILTER) +# ifndef OPENSSL_NO_SCTP +# define BIO_TYPE_DGRAM_SCTP (24|BIO_TYPE_SOURCE_SINK|BIO_TYPE_DESCRIPTOR) +# endif + +#define BIO_TYPE_START 128 + +/* + * BIO_FILENAME_READ|BIO_CLOSE to open or close on free. + * BIO_set_fp(in,stdin,BIO_NOCLOSE); + */ +# define BIO_NOCLOSE 0x00 +# define BIO_CLOSE 0x01 + +/* + * These are used in the following macros and are passed to BIO_ctrl() + */ +# define BIO_CTRL_RESET 1/* opt - rewind/zero etc */ +# define BIO_CTRL_EOF 2/* opt - are we at the eof */ +# define BIO_CTRL_INFO 3/* opt - extra tit-bits */ +# define BIO_CTRL_SET 4/* man - set the 'IO' type */ +# define BIO_CTRL_GET 5/* man - get the 'IO' type */ +# define BIO_CTRL_PUSH 6/* opt - internal, used to signify change */ +# define BIO_CTRL_POP 7/* opt - internal, used to signify change */ +# define BIO_CTRL_GET_CLOSE 8/* man - set the 'close' on free */ +# define BIO_CTRL_SET_CLOSE 9/* man - set the 'close' on free */ +# define BIO_CTRL_PENDING 10/* opt - is their more data buffered */ +# define BIO_CTRL_FLUSH 11/* opt - 'flush' buffered output */ +# define BIO_CTRL_DUP 12/* man - extra stuff for 'duped' BIO */ +# define BIO_CTRL_WPENDING 13/* opt - number of bytes still to write */ +# define BIO_CTRL_SET_CALLBACK 14/* opt - set callback function */ +# define BIO_CTRL_GET_CALLBACK 15/* opt - set callback function */ + +# define BIO_CTRL_SET_FILENAME 30/* BIO_s_file special */ + +/* dgram BIO stuff */ +# define BIO_CTRL_DGRAM_CONNECT 31/* BIO dgram special */ +# define BIO_CTRL_DGRAM_SET_CONNECTED 32/* allow for an externally connected + * socket to be passed in */ +# define BIO_CTRL_DGRAM_SET_RECV_TIMEOUT 33/* setsockopt, essentially */ +# define BIO_CTRL_DGRAM_GET_RECV_TIMEOUT 34/* getsockopt, essentially */ +# define BIO_CTRL_DGRAM_SET_SEND_TIMEOUT 35/* setsockopt, essentially */ +# define BIO_CTRL_DGRAM_GET_SEND_TIMEOUT 36/* getsockopt, essentially */ + +# define BIO_CTRL_DGRAM_GET_RECV_TIMER_EXP 37/* flag whether the last */ +# define BIO_CTRL_DGRAM_GET_SEND_TIMER_EXP 38/* I/O operation tiemd out */ + +/* #ifdef IP_MTU_DISCOVER */ +# define BIO_CTRL_DGRAM_MTU_DISCOVER 39/* set DF bit on egress packets */ +/* #endif */ + +# define BIO_CTRL_DGRAM_QUERY_MTU 40/* as kernel for current MTU */ +# define BIO_CTRL_DGRAM_GET_FALLBACK_MTU 47 +# define BIO_CTRL_DGRAM_GET_MTU 41/* get cached value for MTU */ +# define BIO_CTRL_DGRAM_SET_MTU 42/* set cached value for MTU. + * want to use this if asking + * the kernel fails */ + +# define BIO_CTRL_DGRAM_MTU_EXCEEDED 43/* check whether the MTU was + * exceed in the previous write + * operation */ + +# define BIO_CTRL_DGRAM_GET_PEER 46 +# define BIO_CTRL_DGRAM_SET_PEER 44/* Destination for the data */ + +# define BIO_CTRL_DGRAM_SET_NEXT_TIMEOUT 45/* Next DTLS handshake timeout + * to adjust socket timeouts */ +# define BIO_CTRL_DGRAM_SET_DONT_FRAG 48 + +# define BIO_CTRL_DGRAM_GET_MTU_OVERHEAD 49 + +# define BIO_CTRL_DGRAM_SET_PEEK_MODE 50 + +# ifndef OPENSSL_NO_SCTP +/* SCTP stuff */ +# define BIO_CTRL_DGRAM_SCTP_SET_IN_HANDSHAKE 50 +# define BIO_CTRL_DGRAM_SCTP_ADD_AUTH_KEY 51 +# define BIO_CTRL_DGRAM_SCTP_NEXT_AUTH_KEY 52 +# define BIO_CTRL_DGRAM_SCTP_AUTH_CCS_RCVD 53 +# define BIO_CTRL_DGRAM_SCTP_GET_SNDINFO 60 +# define BIO_CTRL_DGRAM_SCTP_SET_SNDINFO 61 +# define BIO_CTRL_DGRAM_SCTP_GET_RCVINFO 62 +# define BIO_CTRL_DGRAM_SCTP_SET_RCVINFO 63 +# define BIO_CTRL_DGRAM_SCTP_GET_PRINFO 64 +# define BIO_CTRL_DGRAM_SCTP_SET_PRINFO 65 +# define BIO_CTRL_DGRAM_SCTP_SAVE_SHUTDOWN 70 +# endif + +/* modifiers */ +# define BIO_FP_READ 0x02 +# define BIO_FP_WRITE 0x04 +# define BIO_FP_APPEND 0x08 +# define BIO_FP_TEXT 0x10 + +# define BIO_FLAGS_READ 0x01 +# define BIO_FLAGS_WRITE 0x02 +# define BIO_FLAGS_IO_SPECIAL 0x04 +# define BIO_FLAGS_RWS (BIO_FLAGS_READ|BIO_FLAGS_WRITE|BIO_FLAGS_IO_SPECIAL) +# define BIO_FLAGS_SHOULD_RETRY 0x08 +# ifndef BIO_FLAGS_UPLINK +/* + * "UPLINK" flag denotes file descriptors provided by application. It + * defaults to 0, as most platforms don't require UPLINK interface. + */ +# define BIO_FLAGS_UPLINK 0 +# endif + +# define BIO_FLAGS_BASE64_NO_NL 0x100 + +/* + * This is used with memory BIOs: + * BIO_FLAGS_MEM_RDONLY means we shouldn't free up or change the data in any way; + * BIO_FLAGS_NONCLEAR_RST means we should't clear data on reset. + */ +# define BIO_FLAGS_MEM_RDONLY 0x200 +# define BIO_FLAGS_NONCLEAR_RST 0x400 + +typedef union bio_addr_st BIO_ADDR; +typedef struct bio_addrinfo_st BIO_ADDRINFO; + +int BIO_get_new_index(void); +void BIO_set_flags(BIO *b, int flags); +int BIO_test_flags(const BIO *b, int flags); +void BIO_clear_flags(BIO *b, int flags); + +# define BIO_get_flags(b) BIO_test_flags(b, ~(0x0)) +# define BIO_set_retry_special(b) \ + BIO_set_flags(b, (BIO_FLAGS_IO_SPECIAL|BIO_FLAGS_SHOULD_RETRY)) +# define BIO_set_retry_read(b) \ + BIO_set_flags(b, (BIO_FLAGS_READ|BIO_FLAGS_SHOULD_RETRY)) +# define BIO_set_retry_write(b) \ + BIO_set_flags(b, (BIO_FLAGS_WRITE|BIO_FLAGS_SHOULD_RETRY)) + +/* These are normally used internally in BIOs */ +# define BIO_clear_retry_flags(b) \ + BIO_clear_flags(b, (BIO_FLAGS_RWS|BIO_FLAGS_SHOULD_RETRY)) +# define BIO_get_retry_flags(b) \ + BIO_test_flags(b, (BIO_FLAGS_RWS|BIO_FLAGS_SHOULD_RETRY)) + +/* These should be used by the application to tell why we should retry */ +# define BIO_should_read(a) BIO_test_flags(a, BIO_FLAGS_READ) +# define BIO_should_write(a) BIO_test_flags(a, BIO_FLAGS_WRITE) +# define BIO_should_io_special(a) BIO_test_flags(a, BIO_FLAGS_IO_SPECIAL) +# define BIO_retry_type(a) BIO_test_flags(a, BIO_FLAGS_RWS) +# define BIO_should_retry(a) BIO_test_flags(a, BIO_FLAGS_SHOULD_RETRY) + +/* + * The next three are used in conjunction with the BIO_should_io_special() + * condition. After this returns true, BIO *BIO_get_retry_BIO(BIO *bio, int + * *reason); will walk the BIO stack and return the 'reason' for the special + * and the offending BIO. Given a BIO, BIO_get_retry_reason(bio) will return + * the code. + */ +/* + * Returned from the SSL bio when the certificate retrieval code had an error + */ +# define BIO_RR_SSL_X509_LOOKUP 0x01 +/* Returned from the connect BIO when a connect would have blocked */ +# define BIO_RR_CONNECT 0x02 +/* Returned from the accept BIO when an accept would have blocked */ +# define BIO_RR_ACCEPT 0x03 + +/* These are passed by the BIO callback */ +# define BIO_CB_FREE 0x01 +# define BIO_CB_READ 0x02 +# define BIO_CB_WRITE 0x03 +# define BIO_CB_PUTS 0x04 +# define BIO_CB_GETS 0x05 +# define BIO_CB_CTRL 0x06 + +/* + * The callback is called before and after the underling operation, The + * BIO_CB_RETURN flag indicates if it is after the call + */ +# define BIO_CB_RETURN 0x80 +# define BIO_CB_return(a) ((a)|BIO_CB_RETURN) +# define BIO_cb_pre(a) (!((a)&BIO_CB_RETURN)) +# define BIO_cb_post(a) ((a)&BIO_CB_RETURN) + +typedef long (*BIO_callback_fn)(BIO *b, int oper, const char *argp, int argi, + long argl, long ret); +BIO_callback_fn BIO_get_callback(const BIO *b); +void BIO_set_callback(BIO *b, BIO_callback_fn callback); +char *BIO_get_callback_arg(const BIO *b); +void BIO_set_callback_arg(BIO *b, char *arg); + +typedef struct bio_method_st BIO_METHOD; + +const char *BIO_method_name(const BIO *b); +int BIO_method_type(const BIO *b); + +typedef void bio_info_cb(BIO *, int, const char *, int, long, long); + +DEFINE_STACK_OF(BIO) + +/* Prefix and suffix callback in ASN1 BIO */ +typedef int asn1_ps_func (BIO *b, unsigned char **pbuf, int *plen, + void *parg); + +# ifndef OPENSSL_NO_SCTP +/* SCTP parameter structs */ +struct bio_dgram_sctp_sndinfo { + uint16_t snd_sid; + uint16_t snd_flags; + uint32_t snd_ppid; + uint32_t snd_context; +}; + +struct bio_dgram_sctp_rcvinfo { + uint16_t rcv_sid; + uint16_t rcv_ssn; + uint16_t rcv_flags; + uint32_t rcv_ppid; + uint32_t rcv_tsn; + uint32_t rcv_cumtsn; + uint32_t rcv_context; +}; + +struct bio_dgram_sctp_prinfo { + uint16_t pr_policy; + uint32_t pr_value; +}; +# endif + +/* + * #define BIO_CONN_get_param_hostname BIO_ctrl + */ + +# define BIO_C_SET_CONNECT 100 +# define BIO_C_DO_STATE_MACHINE 101 +# define BIO_C_SET_NBIO 102 +/* # define BIO_C_SET_PROXY_PARAM 103 */ +# define BIO_C_SET_FD 104 +# define BIO_C_GET_FD 105 +# define BIO_C_SET_FILE_PTR 106 +# define BIO_C_GET_FILE_PTR 107 +# define BIO_C_SET_FILENAME 108 +# define BIO_C_SET_SSL 109 +# define BIO_C_GET_SSL 110 +# define BIO_C_SET_MD 111 +# define BIO_C_GET_MD 112 +# define BIO_C_GET_CIPHER_STATUS 113 +# define BIO_C_SET_BUF_MEM 114 +# define BIO_C_GET_BUF_MEM_PTR 115 +# define BIO_C_GET_BUFF_NUM_LINES 116 +# define BIO_C_SET_BUFF_SIZE 117 +# define BIO_C_SET_ACCEPT 118 +# define BIO_C_SSL_MODE 119 +# define BIO_C_GET_MD_CTX 120 +/* # define BIO_C_GET_PROXY_PARAM 121 */ +# define BIO_C_SET_BUFF_READ_DATA 122/* data to read first */ +# define BIO_C_GET_CONNECT 123 +# define BIO_C_GET_ACCEPT 124 +# define BIO_C_SET_SSL_RENEGOTIATE_BYTES 125 +# define BIO_C_GET_SSL_NUM_RENEGOTIATES 126 +# define BIO_C_SET_SSL_RENEGOTIATE_TIMEOUT 127 +# define BIO_C_FILE_SEEK 128 +# define BIO_C_GET_CIPHER_CTX 129 +# define BIO_C_SET_BUF_MEM_EOF_RETURN 130/* return end of input + * value */ +# define BIO_C_SET_BIND_MODE 131 +# define BIO_C_GET_BIND_MODE 132 +# define BIO_C_FILE_TELL 133 +# define BIO_C_GET_SOCKS 134 +# define BIO_C_SET_SOCKS 135 + +# define BIO_C_SET_WRITE_BUF_SIZE 136/* for BIO_s_bio */ +# define BIO_C_GET_WRITE_BUF_SIZE 137 +# define BIO_C_MAKE_BIO_PAIR 138 +# define BIO_C_DESTROY_BIO_PAIR 139 +# define BIO_C_GET_WRITE_GUARANTEE 140 +# define BIO_C_GET_READ_REQUEST 141 +# define BIO_C_SHUTDOWN_WR 142 +# define BIO_C_NREAD0 143 +# define BIO_C_NREAD 144 +# define BIO_C_NWRITE0 145 +# define BIO_C_NWRITE 146 +# define BIO_C_RESET_READ_REQUEST 147 +# define BIO_C_SET_MD_CTX 148 + +# define BIO_C_SET_PREFIX 149 +# define BIO_C_GET_PREFIX 150 +# define BIO_C_SET_SUFFIX 151 +# define BIO_C_GET_SUFFIX 152 + +# define BIO_C_SET_EX_ARG 153 +# define BIO_C_GET_EX_ARG 154 + +# define BIO_C_SET_CONNECT_MODE 155 + +# define BIO_set_app_data(s,arg) BIO_set_ex_data(s,0,arg) +# define BIO_get_app_data(s) BIO_get_ex_data(s,0) + +# define BIO_set_nbio(b,n) BIO_ctrl(b,BIO_C_SET_NBIO,(n),NULL) + +# ifndef OPENSSL_NO_SOCK +/* IP families we support, for BIO_s_connect() and BIO_s_accept() */ +/* Note: the underlying operating system may not support some of them */ +# define BIO_FAMILY_IPV4 4 +# define BIO_FAMILY_IPV6 6 +# define BIO_FAMILY_IPANY 256 + +/* BIO_s_connect() */ +# define BIO_set_conn_hostname(b,name) BIO_ctrl(b,BIO_C_SET_CONNECT,0,(char *)name) +# define BIO_set_conn_port(b,port) BIO_ctrl(b,BIO_C_SET_CONNECT,1,(char *)port) +# define BIO_set_conn_address(b,addr) BIO_ctrl(b,BIO_C_SET_CONNECT,2,(char *)addr) +# define BIO_set_conn_ip_family(b,f) BIO_int_ctrl(b,BIO_C_SET_CONNECT,3,f) +# define BIO_get_conn_hostname(b) ((const char *)BIO_ptr_ctrl(b,BIO_C_GET_CONNECT,0)) +# define BIO_get_conn_port(b) ((const char *)BIO_ptr_ctrl(b,BIO_C_GET_CONNECT,1)) +# define BIO_get_conn_address(b) ((const BIO_ADDR *)BIO_ptr_ctrl(b,BIO_C_GET_CONNECT,2)) +# define BIO_get_conn_ip_family(b) BIO_ctrl(b,BIO_C_GET_CONNECT,3,NULL) +# define BIO_set_conn_mode(b,n) BIO_ctrl(b,BIO_C_SET_CONNECT_MODE,(n),NULL) + +/* BIO_s_accept() */ +# define BIO_set_accept_name(b,name) BIO_ctrl(b,BIO_C_SET_ACCEPT,0,(char *)name) +# define BIO_set_accept_port(b,port) BIO_ctrl(b,BIO_C_SET_ACCEPT,1,(char *)port) +# define BIO_get_accept_name(b) ((const char *)BIO_ptr_ctrl(b,BIO_C_GET_ACCEPT,0)) +# define BIO_get_accept_port(b) ((const char *)BIO_ptr_ctrl(b,BIO_C_GET_ACCEPT,1)) +# define BIO_get_peer_name(b) ((const char *)BIO_ptr_ctrl(b,BIO_C_GET_ACCEPT,2)) +# define BIO_get_peer_port(b) ((const char *)BIO_ptr_ctrl(b,BIO_C_GET_ACCEPT,3)) +/* #define BIO_set_nbio(b,n) BIO_ctrl(b,BIO_C_SET_NBIO,(n),NULL) */ +# define BIO_set_nbio_accept(b,n) BIO_ctrl(b,BIO_C_SET_ACCEPT,2,(n)?(void *)"a":NULL) +# define BIO_set_accept_bios(b,bio) BIO_ctrl(b,BIO_C_SET_ACCEPT,3,(char *)bio) +# define BIO_set_accept_ip_family(b,f) BIO_int_ctrl(b,BIO_C_SET_ACCEPT,4,f) +# define BIO_get_accept_ip_family(b) BIO_ctrl(b,BIO_C_GET_ACCEPT,4,NULL) + +/* Aliases kept for backward compatibility */ +# define BIO_BIND_NORMAL 0 +# define BIO_BIND_REUSEADDR BIO_SOCK_REUSEADDR +# define BIO_BIND_REUSEADDR_IF_UNUSED BIO_SOCK_REUSEADDR +# define BIO_set_bind_mode(b,mode) BIO_ctrl(b,BIO_C_SET_BIND_MODE,mode,NULL) +# define BIO_get_bind_mode(b) BIO_ctrl(b,BIO_C_GET_BIND_MODE,0,NULL) + +/* BIO_s_accept() and BIO_s_connect() */ +# define BIO_do_connect(b) BIO_do_handshake(b) +# define BIO_do_accept(b) BIO_do_handshake(b) +# endif /* OPENSSL_NO_SOCK */ + +# define BIO_do_handshake(b) BIO_ctrl(b,BIO_C_DO_STATE_MACHINE,0,NULL) + +/* BIO_s_datagram(), BIO_s_fd(), BIO_s_socket(), BIO_s_accept() and BIO_s_connect() */ +# define BIO_set_fd(b,fd,c) BIO_int_ctrl(b,BIO_C_SET_FD,c,fd) +# define BIO_get_fd(b,c) BIO_ctrl(b,BIO_C_GET_FD,0,(char *)c) + +/* BIO_s_file() */ +# define BIO_set_fp(b,fp,c) BIO_ctrl(b,BIO_C_SET_FILE_PTR,c,(char *)fp) +# define BIO_get_fp(b,fpp) BIO_ctrl(b,BIO_C_GET_FILE_PTR,0,(char *)fpp) + +/* BIO_s_fd() and BIO_s_file() */ +# define BIO_seek(b,ofs) (int)BIO_ctrl(b,BIO_C_FILE_SEEK,ofs,NULL) +# define BIO_tell(b) (int)BIO_ctrl(b,BIO_C_FILE_TELL,0,NULL) + +/* + * name is cast to lose const, but might be better to route through a + * function so we can do it safely + */ +# ifdef CONST_STRICT +/* + * If you are wondering why this isn't defined, its because CONST_STRICT is + * purely a compile-time kludge to allow const to be checked. + */ +int BIO_read_filename(BIO *b, const char *name); +# else +# define BIO_read_filename(b,name) (int)BIO_ctrl(b,BIO_C_SET_FILENAME, \ + BIO_CLOSE|BIO_FP_READ,(char *)name) +# endif +# define BIO_write_filename(b,name) (int)BIO_ctrl(b,BIO_C_SET_FILENAME, \ + BIO_CLOSE|BIO_FP_WRITE,name) +# define BIO_append_filename(b,name) (int)BIO_ctrl(b,BIO_C_SET_FILENAME, \ + BIO_CLOSE|BIO_FP_APPEND,name) +# define BIO_rw_filename(b,name) (int)BIO_ctrl(b,BIO_C_SET_FILENAME, \ + BIO_CLOSE|BIO_FP_READ|BIO_FP_WRITE,name) + +/* + * WARNING WARNING, this ups the reference count on the read bio of the SSL + * structure. This is because the ssl read BIO is now pointed to by the + * next_bio field in the bio. So when you free the BIO, make sure you are + * doing a BIO_free_all() to catch the underlying BIO. + */ +# define BIO_set_ssl(b,ssl,c) BIO_ctrl(b,BIO_C_SET_SSL,c,(char *)ssl) +# define BIO_get_ssl(b,sslp) BIO_ctrl(b,BIO_C_GET_SSL,0,(char *)sslp) +# define BIO_set_ssl_mode(b,client) BIO_ctrl(b,BIO_C_SSL_MODE,client,NULL) +# define BIO_set_ssl_renegotiate_bytes(b,num) \ + BIO_ctrl(b,BIO_C_SET_SSL_RENEGOTIATE_BYTES,num,NULL) +# define BIO_get_num_renegotiates(b) \ + BIO_ctrl(b,BIO_C_GET_SSL_NUM_RENEGOTIATES,0,NULL) +# define BIO_set_ssl_renegotiate_timeout(b,seconds) \ + BIO_ctrl(b,BIO_C_SET_SSL_RENEGOTIATE_TIMEOUT,seconds,NULL) + +/* defined in evp.h */ +/* #define BIO_set_md(b,md) BIO_ctrl(b,BIO_C_SET_MD,1,(char *)md) */ + +# define BIO_get_mem_data(b,pp) BIO_ctrl(b,BIO_CTRL_INFO,0,(char *)pp) +# define BIO_set_mem_buf(b,bm,c) BIO_ctrl(b,BIO_C_SET_BUF_MEM,c,(char *)bm) +# define BIO_get_mem_ptr(b,pp) BIO_ctrl(b,BIO_C_GET_BUF_MEM_PTR,0,(char *)pp) +# define BIO_set_mem_eof_return(b,v) \ + BIO_ctrl(b,BIO_C_SET_BUF_MEM_EOF_RETURN,v,NULL) + +/* For the BIO_f_buffer() type */ +# define BIO_get_buffer_num_lines(b) BIO_ctrl(b,BIO_C_GET_BUFF_NUM_LINES,0,NULL) +# define BIO_set_buffer_size(b,size) BIO_ctrl(b,BIO_C_SET_BUFF_SIZE,size,NULL) +# define BIO_set_read_buffer_size(b,size) BIO_int_ctrl(b,BIO_C_SET_BUFF_SIZE,size,0) +# define BIO_set_write_buffer_size(b,size) BIO_int_ctrl(b,BIO_C_SET_BUFF_SIZE,size,1) +# define BIO_set_buffer_read_data(b,buf,num) BIO_ctrl(b,BIO_C_SET_BUFF_READ_DATA,num,buf) + +/* Don't use the next one unless you know what you are doing :-) */ +# define BIO_dup_state(b,ret) BIO_ctrl(b,BIO_CTRL_DUP,0,(char *)(ret)) + +# define BIO_reset(b) (int)BIO_ctrl(b,BIO_CTRL_RESET,0,NULL) +# define BIO_eof(b) (int)BIO_ctrl(b,BIO_CTRL_EOF,0,NULL) +# define BIO_set_close(b,c) (int)BIO_ctrl(b,BIO_CTRL_SET_CLOSE,(c),NULL) +# define BIO_get_close(b) (int)BIO_ctrl(b,BIO_CTRL_GET_CLOSE,0,NULL) +# define BIO_pending(b) (int)BIO_ctrl(b,BIO_CTRL_PENDING,0,NULL) +# define BIO_wpending(b) (int)BIO_ctrl(b,BIO_CTRL_WPENDING,0,NULL) +/* ...pending macros have inappropriate return type */ +size_t BIO_ctrl_pending(BIO *b); +size_t BIO_ctrl_wpending(BIO *b); +# define BIO_flush(b) (int)BIO_ctrl(b,BIO_CTRL_FLUSH,0,NULL) +# define BIO_get_info_callback(b,cbp) (int)BIO_ctrl(b,BIO_CTRL_GET_CALLBACK,0, \ + cbp) +# define BIO_set_info_callback(b,cb) (int)BIO_callback_ctrl(b,BIO_CTRL_SET_CALLBACK,cb) + +/* For the BIO_f_buffer() type */ +# define BIO_buffer_get_num_lines(b) BIO_ctrl(b,BIO_CTRL_GET,0,NULL) + +/* For BIO_s_bio() */ +# define BIO_set_write_buf_size(b,size) (int)BIO_ctrl(b,BIO_C_SET_WRITE_BUF_SIZE,size,NULL) +# define BIO_get_write_buf_size(b,size) (size_t)BIO_ctrl(b,BIO_C_GET_WRITE_BUF_SIZE,size,NULL) +# define BIO_make_bio_pair(b1,b2) (int)BIO_ctrl(b1,BIO_C_MAKE_BIO_PAIR,0,b2) +# define BIO_destroy_bio_pair(b) (int)BIO_ctrl(b,BIO_C_DESTROY_BIO_PAIR,0,NULL) +# define BIO_shutdown_wr(b) (int)BIO_ctrl(b, BIO_C_SHUTDOWN_WR, 0, NULL) +/* macros with inappropriate type -- but ...pending macros use int too: */ +# define BIO_get_write_guarantee(b) (int)BIO_ctrl(b,BIO_C_GET_WRITE_GUARANTEE,0,NULL) +# define BIO_get_read_request(b) (int)BIO_ctrl(b,BIO_C_GET_READ_REQUEST,0,NULL) +size_t BIO_ctrl_get_write_guarantee(BIO *b); +size_t BIO_ctrl_get_read_request(BIO *b); +int BIO_ctrl_reset_read_request(BIO *b); + +/* ctrl macros for dgram */ +# define BIO_ctrl_dgram_connect(b,peer) \ + (int)BIO_ctrl(b,BIO_CTRL_DGRAM_CONNECT,0, (char *)peer) +# define BIO_ctrl_set_connected(b,peer) \ + (int)BIO_ctrl(b, BIO_CTRL_DGRAM_SET_CONNECTED, 0, (char *)peer) +# define BIO_dgram_recv_timedout(b) \ + (int)BIO_ctrl(b, BIO_CTRL_DGRAM_GET_RECV_TIMER_EXP, 0, NULL) +# define BIO_dgram_send_timedout(b) \ + (int)BIO_ctrl(b, BIO_CTRL_DGRAM_GET_SEND_TIMER_EXP, 0, NULL) +# define BIO_dgram_get_peer(b,peer) \ + (int)BIO_ctrl(b, BIO_CTRL_DGRAM_GET_PEER, 0, (char *)peer) +# define BIO_dgram_set_peer(b,peer) \ + (int)BIO_ctrl(b, BIO_CTRL_DGRAM_SET_PEER, 0, (char *)peer) +# define BIO_dgram_get_mtu_overhead(b) \ + (unsigned int)BIO_ctrl((b), BIO_CTRL_DGRAM_GET_MTU_OVERHEAD, 0, NULL) + +#define BIO_get_ex_new_index(l, p, newf, dupf, freef) \ + CRYPTO_get_ex_new_index(CRYPTO_EX_INDEX_BIO, l, p, newf, dupf, freef) +int BIO_set_ex_data(BIO *bio, int idx, void *data); +void *BIO_get_ex_data(BIO *bio, int idx); +uint64_t BIO_number_read(BIO *bio); +uint64_t BIO_number_written(BIO *bio); + +/* For BIO_f_asn1() */ +int BIO_asn1_set_prefix(BIO *b, asn1_ps_func *prefix, + asn1_ps_func *prefix_free); +int BIO_asn1_get_prefix(BIO *b, asn1_ps_func **pprefix, + asn1_ps_func **pprefix_free); +int BIO_asn1_set_suffix(BIO *b, asn1_ps_func *suffix, + asn1_ps_func *suffix_free); +int BIO_asn1_get_suffix(BIO *b, asn1_ps_func **psuffix, + asn1_ps_func **psuffix_free); + +const BIO_METHOD *BIO_s_file(void); +BIO *BIO_new_file(const char *filename, const char *mode); +# ifndef OPENSSL_NO_STDIO +BIO *BIO_new_fp(FILE *stream, int close_flag); +# endif +BIO *BIO_new(const BIO_METHOD *type); +int BIO_free(BIO *a); +void BIO_set_data(BIO *a, void *ptr); +void *BIO_get_data(BIO *a); +void BIO_set_init(BIO *a, int init); +int BIO_get_init(BIO *a); +void BIO_set_shutdown(BIO *a, int shut); +int BIO_get_shutdown(BIO *a); +void BIO_vfree(BIO *a); +int BIO_up_ref(BIO *a); +int BIO_read(BIO *b, void *data, int len); +int BIO_gets(BIO *bp, char *buf, int size); +int BIO_write(BIO *b, const void *data, int len); +int BIO_puts(BIO *bp, const char *buf); +int BIO_indent(BIO *b, int indent, int max); +long BIO_ctrl(BIO *bp, int cmd, long larg, void *parg); +long BIO_callback_ctrl(BIO *b, int cmd, + void (*fp) (BIO *, int, const char *, int, long, long)); +void *BIO_ptr_ctrl(BIO *bp, int cmd, long larg); +long BIO_int_ctrl(BIO *bp, int cmd, long larg, int iarg); +BIO *BIO_push(BIO *b, BIO *append); +BIO *BIO_pop(BIO *b); +void BIO_free_all(BIO *a); +BIO *BIO_find_type(BIO *b, int bio_type); +BIO *BIO_next(BIO *b); +void BIO_set_next(BIO *b, BIO *next); +BIO *BIO_get_retry_BIO(BIO *bio, int *reason); +int BIO_get_retry_reason(BIO *bio); +void BIO_set_retry_reason(BIO *bio, int reason); +BIO *BIO_dup_chain(BIO *in); + +int BIO_nread0(BIO *bio, char **buf); +int BIO_nread(BIO *bio, char **buf, int num); +int BIO_nwrite0(BIO *bio, char **buf); +int BIO_nwrite(BIO *bio, char **buf, int num); + +long BIO_debug_callback(BIO *bio, int cmd, const char *argp, int argi, + long argl, long ret); + +const BIO_METHOD *BIO_s_mem(void); +const BIO_METHOD *BIO_s_secmem(void); +BIO *BIO_new_mem_buf(const void *buf, int len); +# ifndef OPENSSL_NO_SOCK +const BIO_METHOD *BIO_s_socket(void); +const BIO_METHOD *BIO_s_connect(void); +const BIO_METHOD *BIO_s_accept(void); +# endif +const BIO_METHOD *BIO_s_fd(void); +const BIO_METHOD *BIO_s_log(void); +const BIO_METHOD *BIO_s_bio(void); +const BIO_METHOD *BIO_s_null(void); +const BIO_METHOD *BIO_f_null(void); +const BIO_METHOD *BIO_f_buffer(void); +const BIO_METHOD *BIO_f_linebuffer(void); +const BIO_METHOD *BIO_f_nbio_test(void); +# ifndef OPENSSL_NO_DGRAM +const BIO_METHOD *BIO_s_datagram(void); +int BIO_dgram_non_fatal_error(int error); +BIO *BIO_new_dgram(int fd, int close_flag); +# ifndef OPENSSL_NO_SCTP +const BIO_METHOD *BIO_s_datagram_sctp(void); +BIO *BIO_new_dgram_sctp(int fd, int close_flag); +int BIO_dgram_is_sctp(BIO *bio); +int BIO_dgram_sctp_notification_cb(BIO *b, + void (*handle_notifications) (BIO *bio, + void *context, + void *buf), + void *context); +int BIO_dgram_sctp_wait_for_dry(BIO *b); +int BIO_dgram_sctp_msg_waiting(BIO *b); +# endif +# endif + +# ifndef OPENSSL_NO_SOCK +int BIO_sock_should_retry(int i); +int BIO_sock_non_fatal_error(int error); +# endif + +int BIO_fd_should_retry(int i); +int BIO_fd_non_fatal_error(int error); +int BIO_dump_cb(int (*cb) (const void *data, size_t len, void *u), + void *u, const char *s, int len); +int BIO_dump_indent_cb(int (*cb) (const void *data, size_t len, void *u), + void *u, const char *s, int len, int indent); +int BIO_dump(BIO *b, const char *bytes, int len); +int BIO_dump_indent(BIO *b, const char *bytes, int len, int indent); +# ifndef OPENSSL_NO_STDIO +int BIO_dump_fp(FILE *fp, const char *s, int len); +int BIO_dump_indent_fp(FILE *fp, const char *s, int len, int indent); +# endif +int BIO_hex_string(BIO *out, int indent, int width, unsigned char *data, + int datalen); + +# ifndef OPENSSL_NO_SOCK +BIO_ADDR *BIO_ADDR_new(void); +int BIO_ADDR_rawmake(BIO_ADDR *ap, int family, + const void *where, size_t wherelen, unsigned short port); +void BIO_ADDR_free(BIO_ADDR *); +void BIO_ADDR_clear(BIO_ADDR *ap); +int BIO_ADDR_family(const BIO_ADDR *ap); +int BIO_ADDR_rawaddress(const BIO_ADDR *ap, void *p, size_t *l); +unsigned short BIO_ADDR_rawport(const BIO_ADDR *ap); +char *BIO_ADDR_hostname_string(const BIO_ADDR *ap, int numeric); +char *BIO_ADDR_service_string(const BIO_ADDR *ap, int numeric); +char *BIO_ADDR_path_string(const BIO_ADDR *ap); + +const BIO_ADDRINFO *BIO_ADDRINFO_next(const BIO_ADDRINFO *bai); +int BIO_ADDRINFO_family(const BIO_ADDRINFO *bai); +int BIO_ADDRINFO_socktype(const BIO_ADDRINFO *bai); +int BIO_ADDRINFO_protocol(const BIO_ADDRINFO *bai); +const BIO_ADDR *BIO_ADDRINFO_address(const BIO_ADDRINFO *bai); +void BIO_ADDRINFO_free(BIO_ADDRINFO *bai); + +enum BIO_hostserv_priorities { + BIO_PARSE_PRIO_HOST, BIO_PARSE_PRIO_SERV +}; +int BIO_parse_hostserv(const char *hostserv, char **host, char **service, + enum BIO_hostserv_priorities hostserv_prio); +enum BIO_lookup_type { + BIO_LOOKUP_CLIENT, BIO_LOOKUP_SERVER +}; +int BIO_lookup(const char *host, const char *service, + enum BIO_lookup_type lookup_type, + int family, int socktype, BIO_ADDRINFO **res); +int BIO_sock_error(int sock); +int BIO_socket_ioctl(int fd, long type, void *arg); +int BIO_socket_nbio(int fd, int mode); +int BIO_sock_init(void); +# if OPENSSL_API_COMPAT < 0x10100000L +# define BIO_sock_cleanup() while(0) continue +# endif +int BIO_set_tcp_ndelay(int sock, int turn_on); + +DEPRECATEDIN_1_1_0(struct hostent *BIO_gethostbyname(const char *name)) +DEPRECATEDIN_1_1_0(int BIO_get_port(const char *str, unsigned short *port_ptr)) +DEPRECATEDIN_1_1_0(int BIO_get_host_ip(const char *str, unsigned char *ip)) +DEPRECATEDIN_1_1_0(int BIO_get_accept_socket(char *host_port, int mode)) +DEPRECATEDIN_1_1_0(int BIO_accept(int sock, char **ip_port)) + +union BIO_sock_info_u { + BIO_ADDR *addr; +}; +enum BIO_sock_info_type { + BIO_SOCK_INFO_ADDRESS +}; +int BIO_sock_info(int sock, + enum BIO_sock_info_type type, union BIO_sock_info_u *info); + +# define BIO_SOCK_REUSEADDR 0x01 +# define BIO_SOCK_V6_ONLY 0x02 +# define BIO_SOCK_KEEPALIVE 0x04 +# define BIO_SOCK_NONBLOCK 0x08 +# define BIO_SOCK_NODELAY 0x10 + +int BIO_socket(int domain, int socktype, int protocol, int options); +int BIO_connect(int sock, const BIO_ADDR *addr, int options); +int BIO_listen(int sock, const BIO_ADDR *addr, int options); +int BIO_accept_ex(int accept_sock, BIO_ADDR *addr, int options); +int BIO_closesocket(int sock); + +BIO *BIO_new_socket(int sock, int close_flag); +BIO *BIO_new_connect(const char *host_port); +BIO *BIO_new_accept(const char *host_port); +# endif /* OPENSSL_NO_SOCK*/ + +BIO *BIO_new_fd(int fd, int close_flag); + +int BIO_new_bio_pair(BIO **bio1, size_t writebuf1, + BIO **bio2, size_t writebuf2); +/* + * If successful, returns 1 and in *bio1, *bio2 two BIO pair endpoints. + * Otherwise returns 0 and sets *bio1 and *bio2 to NULL. Size 0 uses default + * value. + */ + +void BIO_copy_next_retry(BIO *b); + +/* + * long BIO_ghbn_ctrl(int cmd,int iarg,char *parg); + */ + +# ifdef __GNUC__ +# define __bio_h__attr__ __attribute__ +# else +# define __bio_h__attr__(x) +# endif +int BIO_printf(BIO *bio, const char *format, ...) +__bio_h__attr__((__format__(__printf__, 2, 3))); +int BIO_vprintf(BIO *bio, const char *format, va_list args) +__bio_h__attr__((__format__(__printf__, 2, 0))); +int BIO_snprintf(char *buf, size_t n, const char *format, ...) +__bio_h__attr__((__format__(__printf__, 3, 4))); +int BIO_vsnprintf(char *buf, size_t n, const char *format, va_list args) +__bio_h__attr__((__format__(__printf__, 3, 0))); +# undef __bio_h__attr__ + + +BIO_METHOD *BIO_meth_new(int type, const char *name); +void BIO_meth_free(BIO_METHOD *biom); +int (*BIO_meth_get_write(BIO_METHOD *biom)) (BIO *, const char *, int); +int BIO_meth_set_write(BIO_METHOD *biom, + int (*write) (BIO *, const char *, int)); +int (*BIO_meth_get_read(BIO_METHOD *biom)) (BIO *, char *, int); +int BIO_meth_set_read(BIO_METHOD *biom, + int (*read) (BIO *, char *, int)); +int (*BIO_meth_get_puts(BIO_METHOD *biom)) (BIO *, const char *); +int BIO_meth_set_puts(BIO_METHOD *biom, + int (*puts) (BIO *, const char *)); +int (*BIO_meth_get_gets(BIO_METHOD *biom)) (BIO *, char *, int); +int BIO_meth_set_gets(BIO_METHOD *biom, + int (*gets) (BIO *, char *, int)); +long (*BIO_meth_get_ctrl(BIO_METHOD *biom)) (BIO *, int, long, void *); +int BIO_meth_set_ctrl(BIO_METHOD *biom, + long (*ctrl) (BIO *, int, long, void *)); +int (*BIO_meth_get_create(BIO_METHOD *bion)) (BIO *); +int BIO_meth_set_create(BIO_METHOD *biom, int (*create) (BIO *)); +int (*BIO_meth_get_destroy(BIO_METHOD *biom)) (BIO *); +int BIO_meth_set_destroy(BIO_METHOD *biom, int (*destroy) (BIO *)); +long (*BIO_meth_get_callback_ctrl(BIO_METHOD *biom)) + (BIO *, int, bio_info_cb *); +int BIO_meth_set_callback_ctrl(BIO_METHOD *biom, + long (*callback_ctrl) (BIO *, int, + bio_info_cb *)); + +/* BEGIN ERROR CODES */ +/* + * The following lines are auto generated by the script mkerr.pl. Any changes + * made after this point may be overwritten when the script is next run. + */ + +int ERR_load_BIO_strings(void); + +/* Error codes for the BIO functions. */ + +/* Function codes. */ +# define BIO_F_ACPT_STATE 100 +# define BIO_F_ADDR_STRINGS 134 +# define BIO_F_BIO_ACCEPT 101 +# define BIO_F_BIO_ACCEPT_EX 137 +# define BIO_F_BIO_ADDR_NEW 144 +# define BIO_F_BIO_CALLBACK_CTRL 131 +# define BIO_F_BIO_CONNECT 138 +# define BIO_F_BIO_CTRL 103 +# define BIO_F_BIO_GETS 104 +# define BIO_F_BIO_GET_HOST_IP 106 +# define BIO_F_BIO_GET_NEW_INDEX 102 +# define BIO_F_BIO_GET_PORT 107 +# define BIO_F_BIO_LISTEN 139 +# define BIO_F_BIO_LOOKUP 135 +# define BIO_F_BIO_MAKE_PAIR 121 +# define BIO_F_BIO_NEW 108 +# define BIO_F_BIO_NEW_FILE 109 +# define BIO_F_BIO_NEW_MEM_BUF 126 +# define BIO_F_BIO_NREAD 123 +# define BIO_F_BIO_NREAD0 124 +# define BIO_F_BIO_NWRITE 125 +# define BIO_F_BIO_NWRITE0 122 +# define BIO_F_BIO_PARSE_HOSTSERV 136 +# define BIO_F_BIO_PUTS 110 +# define BIO_F_BIO_READ 111 +# define BIO_F_BIO_SOCKET 140 +# define BIO_F_BIO_SOCKET_NBIO 142 +# define BIO_F_BIO_SOCK_INFO 141 +# define BIO_F_BIO_SOCK_INIT 112 +# define BIO_F_BIO_WRITE 113 +# define BIO_F_BUFFER_CTRL 114 +# define BIO_F_CONN_CTRL 127 +# define BIO_F_CONN_STATE 115 +# define BIO_F_DGRAM_SCTP_READ 132 +# define BIO_F_DGRAM_SCTP_WRITE 133 +# define BIO_F_FILE_CTRL 116 +# define BIO_F_FILE_READ 130 +# define BIO_F_LINEBUFFER_CTRL 129 +# define BIO_F_MEM_WRITE 117 +# define BIO_F_SSL_NEW 118 + +/* Reason codes. */ +# define BIO_R_ACCEPT_ERROR 100 +# define BIO_R_ADDRINFO_ADDR_IS_NOT_AF_INET 141 +# define BIO_R_AMBIGUOUS_HOST_OR_SERVICE 129 +# define BIO_R_BAD_FOPEN_MODE 101 +# define BIO_R_BROKEN_PIPE 124 +# define BIO_R_CONNECT_ERROR 103 +# define BIO_R_GETHOSTBYNAME_ADDR_IS_NOT_AF_INET 107 +# define BIO_R_GETSOCKNAME_ERROR 132 +# define BIO_R_GETSOCKNAME_TRUNCATED_ADDRESS 133 +# define BIO_R_GETTING_SOCKTYPE 134 +# define BIO_R_INVALID_ARGUMENT 125 +# define BIO_R_INVALID_SOCKET 135 +# define BIO_R_IN_USE 123 +# define BIO_R_LISTEN_V6_ONLY 136 +# define BIO_R_LOOKUP_RETURNED_NOTHING 142 +# define BIO_R_MALFORMED_HOST_OR_SERVICE 130 +# define BIO_R_NBIO_CONNECT_ERROR 110 +# define BIO_R_NO_ACCEPT_ADDR_OR_SERVICE_SPECIFIED 143 +# define BIO_R_NO_HOSTNAME_OR_SERVICE_SPECIFIED 144 +# define BIO_R_NO_PORT_DEFINED 113 +# define BIO_R_NO_SUCH_FILE 128 +# define BIO_R_NULL_PARAMETER 115 +# define BIO_R_UNABLE_TO_BIND_SOCKET 117 +# define BIO_R_UNABLE_TO_CREATE_SOCKET 118 +# define BIO_R_UNABLE_TO_KEEPALIVE 137 +# define BIO_R_UNABLE_TO_LISTEN_SOCKET 119 +# define BIO_R_UNABLE_TO_NODELAY 138 +# define BIO_R_UNABLE_TO_REUSEADDR 139 +# define BIO_R_UNAVAILABLE_IP_FAMILY 145 +# define BIO_R_UNINITIALIZED 120 +# define BIO_R_UNKNOWN_INFO_TYPE 140 +# define BIO_R_UNSUPPORTED_IP_FAMILY 146 +# define BIO_R_UNSUPPORTED_METHOD 121 +# define BIO_R_UNSUPPORTED_PROTOCOL_FAMILY 131 +# define BIO_R_WRITE_TO_READ_ONLY_BIO 126 +# define BIO_R_WSASTARTUP 122 + +# ifdef __cplusplus +} +# endif +#endif diff --git a/mobile/ios/FxA/FxA/include/openssl/blowfish.h b/mobile/ios/FxA/FxA/include/openssl/blowfish.h new file mode 100644 index 0000000000..cd3e460e98 --- /dev/null +++ b/mobile/ios/FxA/FxA/include/openssl/blowfish.h @@ -0,0 +1,61 @@ +/* + * Copyright 1995-2016 The OpenSSL Project Authors. All Rights Reserved. + * + * Licensed under the OpenSSL license (the "License"). You may not use + * this file except in compliance with the License. You can obtain a copy + * in the file LICENSE in the source distribution or at + * https://www.openssl.org/source/license.html + */ + +#ifndef HEADER_BLOWFISH_H +# define HEADER_BLOWFISH_H + +# include + +# ifndef OPENSSL_NO_BF +# include +# ifdef __cplusplus +extern "C" { +# endif + +# define BF_ENCRYPT 1 +# define BF_DECRYPT 0 + +/*- + * !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + * ! BF_LONG has to be at least 32 bits wide. ! + * !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + */ +# define BF_LONG unsigned int + +# define BF_ROUNDS 16 +# define BF_BLOCK 8 + +typedef struct bf_key_st { + BF_LONG P[BF_ROUNDS + 2]; + BF_LONG S[4 * 256]; +} BF_KEY; + +void BF_set_key(BF_KEY *key, int len, const unsigned char *data); + +void BF_encrypt(BF_LONG *data, const BF_KEY *key); +void BF_decrypt(BF_LONG *data, const BF_KEY *key); + +void BF_ecb_encrypt(const unsigned char *in, unsigned char *out, + const BF_KEY *key, int enc); +void BF_cbc_encrypt(const unsigned char *in, unsigned char *out, long length, + const BF_KEY *schedule, unsigned char *ivec, int enc); +void BF_cfb64_encrypt(const unsigned char *in, unsigned char *out, + long length, const BF_KEY *schedule, + unsigned char *ivec, int *num, int enc); +void BF_ofb64_encrypt(const unsigned char *in, unsigned char *out, + long length, const BF_KEY *schedule, + unsigned char *ivec, int *num); +const char *BF_options(void); + +# ifdef __cplusplus +} +# endif +# endif + +#endif diff --git a/mobile/ios/FxA/FxA/include/openssl/bn.h b/mobile/ios/FxA/FxA/include/openssl/bn.h new file mode 100644 index 0000000000..17bd52136c --- /dev/null +++ b/mobile/ios/FxA/FxA/include/openssl/bn.h @@ -0,0 +1,575 @@ +/* + * Copyright 1995-2016 The OpenSSL Project Authors. All Rights Reserved. + * + * Licensed under the OpenSSL license (the "License"). You may not use + * this file except in compliance with the License. You can obtain a copy + * in the file LICENSE in the source distribution or at + * https://www.openssl.org/source/license.html + */ + +/* ==================================================================== + * Copyright 2002 Sun Microsystems, Inc. ALL RIGHTS RESERVED. + * + * Portions of the attached software ("Contribution") are developed by + * SUN MICROSYSTEMS, INC., and are contributed to the OpenSSL project. + * + * The Contribution is licensed pursuant to the Eric Young open source + * license provided above. + * + * The binary polynomial arithmetic software is originally written by + * Sheueling Chang Shantz and Douglas Stebila of Sun Microsystems Laboratories. + * + */ + +#ifndef HEADER_BN_H +# define HEADER_BN_H + +# include +# ifndef OPENSSL_NO_STDIO +# include +# endif +# include +# include +# include + +#ifdef __cplusplus +extern "C" { +#endif + +/* + * 64-bit processor with LP64 ABI + */ +# ifdef SIXTY_FOUR_BIT_LONG +# define BN_ULONG unsigned long +# define BN_BYTES 8 +# endif + +/* + * 64-bit processor other than LP64 ABI + */ +# ifdef SIXTY_FOUR_BIT +# define BN_ULONG unsigned long long +# define BN_BYTES 8 +# endif + +# ifdef THIRTY_TWO_BIT +# define BN_ULONG unsigned int +# define BN_BYTES 4 +# endif + +# define BN_BITS2 (BN_BYTES * 8) +# define BN_BITS (BN_BITS2 * 2) +# define BN_TBIT ((BN_ULONG)1 << (BN_BITS2 - 1)) + +# define BN_FLG_MALLOCED 0x01 +# define BN_FLG_STATIC_DATA 0x02 + +/* + * avoid leaking exponent information through timing, + * BN_mod_exp_mont() will call BN_mod_exp_mont_consttime, + * BN_div() will call BN_div_no_branch, + * BN_mod_inverse() will call BN_mod_inverse_no_branch. + */ +# define BN_FLG_CONSTTIME 0x04 +# define BN_FLG_SECURE 0x08 + +# if OPENSSL_API_COMPAT < 0x00908000L +/* deprecated name for the flag */ +# define BN_FLG_EXP_CONSTTIME BN_FLG_CONSTTIME +# define BN_FLG_FREE 0x8000 /* used for debugging */ +# endif + +void BN_set_flags(BIGNUM *b, int n); +int BN_get_flags(const BIGNUM *b, int n); + +/* Values for |top| in BN_rand() */ +#define BN_RAND_TOP_ANY -1 +#define BN_RAND_TOP_ONE 0 +#define BN_RAND_TOP_TWO 1 + +/* Values for |bottom| in BN_rand() */ +#define BN_RAND_BOTTOM_ANY 0 +#define BN_RAND_BOTTOM_ODD 1 + +/* + * get a clone of a BIGNUM with changed flags, for *temporary* use only (the + * two BIGNUMs cannot be used in parallel!). Also only for *read only* use. The + * value |dest| should be a newly allocated BIGNUM obtained via BN_new() that + * has not been otherwise initialised or used. + */ +void BN_with_flags(BIGNUM *dest, const BIGNUM *b, int flags); + +/* Wrapper function to make using BN_GENCB easier */ +int BN_GENCB_call(BN_GENCB *cb, int a, int b); + +BN_GENCB *BN_GENCB_new(void); +void BN_GENCB_free(BN_GENCB *cb); + +/* Populate a BN_GENCB structure with an "old"-style callback */ +void BN_GENCB_set_old(BN_GENCB *gencb, void (*callback) (int, int, void *), + void *cb_arg); + +/* Populate a BN_GENCB structure with a "new"-style callback */ +void BN_GENCB_set(BN_GENCB *gencb, int (*callback) (int, int, BN_GENCB *), + void *cb_arg); + +void *BN_GENCB_get_arg(BN_GENCB *cb); + +# define BN_prime_checks 0 /* default: select number of iterations based + * on the size of the number */ + +/* + * number of Miller-Rabin iterations for an error rate of less than 2^-80 for + * random 'b'-bit input, b >= 100 (taken from table 4.4 in the Handbook of + * Applied Cryptography [Menezes, van Oorschot, Vanstone; CRC Press 1996]; + * original paper: Damgaard, Landrock, Pomerance: Average case error + * estimates for the strong probable prime test. -- Math. Comp. 61 (1993) + * 177-194) + */ +# define BN_prime_checks_for_size(b) ((b) >= 1300 ? 2 : \ + (b) >= 850 ? 3 : \ + (b) >= 650 ? 4 : \ + (b) >= 550 ? 5 : \ + (b) >= 450 ? 6 : \ + (b) >= 400 ? 7 : \ + (b) >= 350 ? 8 : \ + (b) >= 300 ? 9 : \ + (b) >= 250 ? 12 : \ + (b) >= 200 ? 15 : \ + (b) >= 150 ? 18 : \ + /* b >= 100 */ 27) + +# define BN_num_bytes(a) ((BN_num_bits(a)+7)/8) + +int BN_abs_is_word(const BIGNUM *a, const BN_ULONG w); +int BN_is_zero(const BIGNUM *a); +int BN_is_one(const BIGNUM *a); +int BN_is_word(const BIGNUM *a, const BN_ULONG w); +int BN_is_odd(const BIGNUM *a); + +# define BN_one(a) (BN_set_word((a),1)) + +void BN_zero_ex(BIGNUM *a); + +# if OPENSSL_API_COMPAT >= 0x00908000L +# define BN_zero(a) BN_zero_ex(a) +# else +# define BN_zero(a) (BN_set_word((a),0)) +# endif + +const BIGNUM *BN_value_one(void); +char *BN_options(void); +BN_CTX *BN_CTX_new(void); +BN_CTX *BN_CTX_secure_new(void); +void BN_CTX_free(BN_CTX *c); +void BN_CTX_start(BN_CTX *ctx); +BIGNUM *BN_CTX_get(BN_CTX *ctx); +void BN_CTX_end(BN_CTX *ctx); +int BN_rand(BIGNUM *rnd, int bits, int top, int bottom); +int BN_pseudo_rand(BIGNUM *rnd, int bits, int top, int bottom); +int BN_rand_range(BIGNUM *rnd, const BIGNUM *range); +int BN_pseudo_rand_range(BIGNUM *rnd, const BIGNUM *range); +int BN_num_bits(const BIGNUM *a); +int BN_num_bits_word(BN_ULONG l); +int BN_security_bits(int L, int N); +BIGNUM *BN_new(void); +BIGNUM *BN_secure_new(void); +void BN_clear_free(BIGNUM *a); +BIGNUM *BN_copy(BIGNUM *a, const BIGNUM *b); +void BN_swap(BIGNUM *a, BIGNUM *b); +BIGNUM *BN_bin2bn(const unsigned char *s, int len, BIGNUM *ret); +int BN_bn2bin(const BIGNUM *a, unsigned char *to); +int BN_bn2binpad(const BIGNUM *a, unsigned char *to, int tolen); +BIGNUM *BN_lebin2bn(const unsigned char *s, int len, BIGNUM *ret); +int BN_bn2lebinpad(const BIGNUM *a, unsigned char *to, int tolen); +BIGNUM *BN_mpi2bn(const unsigned char *s, int len, BIGNUM *ret); +int BN_bn2mpi(const BIGNUM *a, unsigned char *to); +int BN_sub(BIGNUM *r, const BIGNUM *a, const BIGNUM *b); +int BN_usub(BIGNUM *r, const BIGNUM *a, const BIGNUM *b); +int BN_uadd(BIGNUM *r, const BIGNUM *a, const BIGNUM *b); +int BN_add(BIGNUM *r, const BIGNUM *a, const BIGNUM *b); +int BN_mul(BIGNUM *r, const BIGNUM *a, const BIGNUM *b, BN_CTX *ctx); +int BN_sqr(BIGNUM *r, const BIGNUM *a, BN_CTX *ctx); +/** BN_set_negative sets sign of a BIGNUM + * \param b pointer to the BIGNUM object + * \param n 0 if the BIGNUM b should be positive and a value != 0 otherwise + */ +void BN_set_negative(BIGNUM *b, int n); +/** BN_is_negative returns 1 if the BIGNUM is negative + * \param a pointer to the BIGNUM object + * \return 1 if a < 0 and 0 otherwise + */ +int BN_is_negative(const BIGNUM *b); + +int BN_div(BIGNUM *dv, BIGNUM *rem, const BIGNUM *m, const BIGNUM *d, + BN_CTX *ctx); +# define BN_mod(rem,m,d,ctx) BN_div(NULL,(rem),(m),(d),(ctx)) +int BN_nnmod(BIGNUM *r, const BIGNUM *m, const BIGNUM *d, BN_CTX *ctx); +int BN_mod_add(BIGNUM *r, const BIGNUM *a, const BIGNUM *b, const BIGNUM *m, + BN_CTX *ctx); +int BN_mod_add_quick(BIGNUM *r, const BIGNUM *a, const BIGNUM *b, + const BIGNUM *m); +int BN_mod_sub(BIGNUM *r, const BIGNUM *a, const BIGNUM *b, const BIGNUM *m, + BN_CTX *ctx); +int BN_mod_sub_quick(BIGNUM *r, const BIGNUM *a, const BIGNUM *b, + const BIGNUM *m); +int BN_mod_mul(BIGNUM *r, const BIGNUM *a, const BIGNUM *b, const BIGNUM *m, + BN_CTX *ctx); +int BN_mod_sqr(BIGNUM *r, const BIGNUM *a, const BIGNUM *m, BN_CTX *ctx); +int BN_mod_lshift1(BIGNUM *r, const BIGNUM *a, const BIGNUM *m, BN_CTX *ctx); +int BN_mod_lshift1_quick(BIGNUM *r, const BIGNUM *a, const BIGNUM *m); +int BN_mod_lshift(BIGNUM *r, const BIGNUM *a, int n, const BIGNUM *m, + BN_CTX *ctx); +int BN_mod_lshift_quick(BIGNUM *r, const BIGNUM *a, int n, const BIGNUM *m); + +BN_ULONG BN_mod_word(const BIGNUM *a, BN_ULONG w); +BN_ULONG BN_div_word(BIGNUM *a, BN_ULONG w); +int BN_mul_word(BIGNUM *a, BN_ULONG w); +int BN_add_word(BIGNUM *a, BN_ULONG w); +int BN_sub_word(BIGNUM *a, BN_ULONG w); +int BN_set_word(BIGNUM *a, BN_ULONG w); +BN_ULONG BN_get_word(const BIGNUM *a); + +int BN_cmp(const BIGNUM *a, const BIGNUM *b); +void BN_free(BIGNUM *a); +int BN_is_bit_set(const BIGNUM *a, int n); +int BN_lshift(BIGNUM *r, const BIGNUM *a, int n); +int BN_lshift1(BIGNUM *r, const BIGNUM *a); +int BN_exp(BIGNUM *r, const BIGNUM *a, const BIGNUM *p, BN_CTX *ctx); + +int BN_mod_exp(BIGNUM *r, const BIGNUM *a, const BIGNUM *p, + const BIGNUM *m, BN_CTX *ctx); +int BN_mod_exp_mont(BIGNUM *r, const BIGNUM *a, const BIGNUM *p, + const BIGNUM *m, BN_CTX *ctx, BN_MONT_CTX *m_ctx); +int BN_mod_exp_mont_consttime(BIGNUM *rr, const BIGNUM *a, const BIGNUM *p, + const BIGNUM *m, BN_CTX *ctx, + BN_MONT_CTX *in_mont); +int BN_mod_exp_mont_word(BIGNUM *r, BN_ULONG a, const BIGNUM *p, + const BIGNUM *m, BN_CTX *ctx, BN_MONT_CTX *m_ctx); +int BN_mod_exp2_mont(BIGNUM *r, const BIGNUM *a1, const BIGNUM *p1, + const BIGNUM *a2, const BIGNUM *p2, const BIGNUM *m, + BN_CTX *ctx, BN_MONT_CTX *m_ctx); +int BN_mod_exp_simple(BIGNUM *r, const BIGNUM *a, const BIGNUM *p, + const BIGNUM *m, BN_CTX *ctx); + +int BN_mask_bits(BIGNUM *a, int n); +# ifndef OPENSSL_NO_STDIO +int BN_print_fp(FILE *fp, const BIGNUM *a); +# endif +int BN_print(BIO *bio, const BIGNUM *a); +int BN_reciprocal(BIGNUM *r, const BIGNUM *m, int len, BN_CTX *ctx); +int BN_rshift(BIGNUM *r, const BIGNUM *a, int n); +int BN_rshift1(BIGNUM *r, const BIGNUM *a); +void BN_clear(BIGNUM *a); +BIGNUM *BN_dup(const BIGNUM *a); +int BN_ucmp(const BIGNUM *a, const BIGNUM *b); +int BN_set_bit(BIGNUM *a, int n); +int BN_clear_bit(BIGNUM *a, int n); +char *BN_bn2hex(const BIGNUM *a); +char *BN_bn2dec(const BIGNUM *a); +int BN_hex2bn(BIGNUM **a, const char *str); +int BN_dec2bn(BIGNUM **a, const char *str); +int BN_asc2bn(BIGNUM **a, const char *str); +int BN_gcd(BIGNUM *r, const BIGNUM *a, const BIGNUM *b, BN_CTX *ctx); +int BN_kronecker(const BIGNUM *a, const BIGNUM *b, BN_CTX *ctx); /* returns + * -2 for + * error */ +BIGNUM *BN_mod_inverse(BIGNUM *ret, + const BIGNUM *a, const BIGNUM *n, BN_CTX *ctx); +BIGNUM *BN_mod_sqrt(BIGNUM *ret, + const BIGNUM *a, const BIGNUM *n, BN_CTX *ctx); + +void BN_consttime_swap(BN_ULONG swap, BIGNUM *a, BIGNUM *b, int nwords); + +/* Deprecated versions */ +DEPRECATEDIN_0_9_8(BIGNUM *BN_generate_prime(BIGNUM *ret, int bits, int safe, + const BIGNUM *add, + const BIGNUM *rem, + void (*callback) (int, int, + void *), + void *cb_arg)) +DEPRECATEDIN_0_9_8(int + BN_is_prime(const BIGNUM *p, int nchecks, + void (*callback) (int, int, void *), + BN_CTX *ctx, void *cb_arg)) +DEPRECATEDIN_0_9_8(int + BN_is_prime_fasttest(const BIGNUM *p, int nchecks, + void (*callback) (int, int, void *), + BN_CTX *ctx, void *cb_arg, + int do_trial_division)) + +/* Newer versions */ +int BN_generate_prime_ex(BIGNUM *ret, int bits, int safe, const BIGNUM *add, + const BIGNUM *rem, BN_GENCB *cb); +int BN_is_prime_ex(const BIGNUM *p, int nchecks, BN_CTX *ctx, BN_GENCB *cb); +int BN_is_prime_fasttest_ex(const BIGNUM *p, int nchecks, BN_CTX *ctx, + int do_trial_division, BN_GENCB *cb); + +int BN_X931_generate_Xpq(BIGNUM *Xp, BIGNUM *Xq, int nbits, BN_CTX *ctx); + +int BN_X931_derive_prime_ex(BIGNUM *p, BIGNUM *p1, BIGNUM *p2, + const BIGNUM *Xp, const BIGNUM *Xp1, + const BIGNUM *Xp2, const BIGNUM *e, BN_CTX *ctx, + BN_GENCB *cb); +int BN_X931_generate_prime_ex(BIGNUM *p, BIGNUM *p1, BIGNUM *p2, BIGNUM *Xp1, + BIGNUM *Xp2, const BIGNUM *Xp, const BIGNUM *e, + BN_CTX *ctx, BN_GENCB *cb); + +BN_MONT_CTX *BN_MONT_CTX_new(void); +int BN_mod_mul_montgomery(BIGNUM *r, const BIGNUM *a, const BIGNUM *b, + BN_MONT_CTX *mont, BN_CTX *ctx); +int BN_to_montgomery(BIGNUM *r, const BIGNUM *a, BN_MONT_CTX *mont, + BN_CTX *ctx); +int BN_from_montgomery(BIGNUM *r, const BIGNUM *a, BN_MONT_CTX *mont, + BN_CTX *ctx); +void BN_MONT_CTX_free(BN_MONT_CTX *mont); +int BN_MONT_CTX_set(BN_MONT_CTX *mont, const BIGNUM *mod, BN_CTX *ctx); +BN_MONT_CTX *BN_MONT_CTX_copy(BN_MONT_CTX *to, BN_MONT_CTX *from); +BN_MONT_CTX *BN_MONT_CTX_set_locked(BN_MONT_CTX **pmont, CRYPTO_RWLOCK *lock, + const BIGNUM *mod, BN_CTX *ctx); + +/* BN_BLINDING flags */ +# define BN_BLINDING_NO_UPDATE 0x00000001 +# define BN_BLINDING_NO_RECREATE 0x00000002 + +BN_BLINDING *BN_BLINDING_new(const BIGNUM *A, const BIGNUM *Ai, BIGNUM *mod); +void BN_BLINDING_free(BN_BLINDING *b); +int BN_BLINDING_update(BN_BLINDING *b, BN_CTX *ctx); +int BN_BLINDING_convert(BIGNUM *n, BN_BLINDING *b, BN_CTX *ctx); +int BN_BLINDING_invert(BIGNUM *n, BN_BLINDING *b, BN_CTX *ctx); +int BN_BLINDING_convert_ex(BIGNUM *n, BIGNUM *r, BN_BLINDING *b, BN_CTX *); +int BN_BLINDING_invert_ex(BIGNUM *n, const BIGNUM *r, BN_BLINDING *b, + BN_CTX *); + +int BN_BLINDING_is_current_thread(BN_BLINDING *b); +void BN_BLINDING_set_current_thread(BN_BLINDING *b); +int BN_BLINDING_lock(BN_BLINDING *b); +int BN_BLINDING_unlock(BN_BLINDING *b); + +unsigned long BN_BLINDING_get_flags(const BN_BLINDING *); +void BN_BLINDING_set_flags(BN_BLINDING *, unsigned long); +BN_BLINDING *BN_BLINDING_create_param(BN_BLINDING *b, + const BIGNUM *e, BIGNUM *m, BN_CTX *ctx, + int (*bn_mod_exp) (BIGNUM *r, + const BIGNUM *a, + const BIGNUM *p, + const BIGNUM *m, + BN_CTX *ctx, + BN_MONT_CTX *m_ctx), + BN_MONT_CTX *m_ctx); + +DEPRECATEDIN_0_9_8(void BN_set_params(int mul, int high, int low, int mont)) +DEPRECATEDIN_0_9_8(int BN_get_params(int which)) /* 0, mul, 1 high, 2 low, 3 + * mont */ + +BN_RECP_CTX *BN_RECP_CTX_new(void); +void BN_RECP_CTX_free(BN_RECP_CTX *recp); +int BN_RECP_CTX_set(BN_RECP_CTX *recp, const BIGNUM *rdiv, BN_CTX *ctx); +int BN_mod_mul_reciprocal(BIGNUM *r, const BIGNUM *x, const BIGNUM *y, + BN_RECP_CTX *recp, BN_CTX *ctx); +int BN_mod_exp_recp(BIGNUM *r, const BIGNUM *a, const BIGNUM *p, + const BIGNUM *m, BN_CTX *ctx); +int BN_div_recp(BIGNUM *dv, BIGNUM *rem, const BIGNUM *m, + BN_RECP_CTX *recp, BN_CTX *ctx); + +# ifndef OPENSSL_NO_EC2M + +/* + * Functions for arithmetic over binary polynomials represented by BIGNUMs. + * The BIGNUM::neg property of BIGNUMs representing binary polynomials is + * ignored. Note that input arguments are not const so that their bit arrays + * can be expanded to the appropriate size if needed. + */ + +/* + * r = a + b + */ +int BN_GF2m_add(BIGNUM *r, const BIGNUM *a, const BIGNUM *b); +# define BN_GF2m_sub(r, a, b) BN_GF2m_add(r, a, b) +/* + * r=a mod p + */ +int BN_GF2m_mod(BIGNUM *r, const BIGNUM *a, const BIGNUM *p); +/* r = (a * b) mod p */ +int BN_GF2m_mod_mul(BIGNUM *r, const BIGNUM *a, const BIGNUM *b, + const BIGNUM *p, BN_CTX *ctx); +/* r = (a * a) mod p */ +int BN_GF2m_mod_sqr(BIGNUM *r, const BIGNUM *a, const BIGNUM *p, BN_CTX *ctx); +/* r = (1 / b) mod p */ +int BN_GF2m_mod_inv(BIGNUM *r, const BIGNUM *b, const BIGNUM *p, BN_CTX *ctx); +/* r = (a / b) mod p */ +int BN_GF2m_mod_div(BIGNUM *r, const BIGNUM *a, const BIGNUM *b, + const BIGNUM *p, BN_CTX *ctx); +/* r = (a ^ b) mod p */ +int BN_GF2m_mod_exp(BIGNUM *r, const BIGNUM *a, const BIGNUM *b, + const BIGNUM *p, BN_CTX *ctx); +/* r = sqrt(a) mod p */ +int BN_GF2m_mod_sqrt(BIGNUM *r, const BIGNUM *a, const BIGNUM *p, + BN_CTX *ctx); +/* r^2 + r = a mod p */ +int BN_GF2m_mod_solve_quad(BIGNUM *r, const BIGNUM *a, const BIGNUM *p, + BN_CTX *ctx); +# define BN_GF2m_cmp(a, b) BN_ucmp((a), (b)) +/*- + * Some functions allow for representation of the irreducible polynomials + * as an unsigned int[], say p. The irreducible f(t) is then of the form: + * t^p[0] + t^p[1] + ... + t^p[k] + * where m = p[0] > p[1] > ... > p[k] = 0. + */ +/* r = a mod p */ +int BN_GF2m_mod_arr(BIGNUM *r, const BIGNUM *a, const int p[]); +/* r = (a * b) mod p */ +int BN_GF2m_mod_mul_arr(BIGNUM *r, const BIGNUM *a, const BIGNUM *b, + const int p[], BN_CTX *ctx); +/* r = (a * a) mod p */ +int BN_GF2m_mod_sqr_arr(BIGNUM *r, const BIGNUM *a, const int p[], + BN_CTX *ctx); +/* r = (1 / b) mod p */ +int BN_GF2m_mod_inv_arr(BIGNUM *r, const BIGNUM *b, const int p[], + BN_CTX *ctx); +/* r = (a / b) mod p */ +int BN_GF2m_mod_div_arr(BIGNUM *r, const BIGNUM *a, const BIGNUM *b, + const int p[], BN_CTX *ctx); +/* r = (a ^ b) mod p */ +int BN_GF2m_mod_exp_arr(BIGNUM *r, const BIGNUM *a, const BIGNUM *b, + const int p[], BN_CTX *ctx); +/* r = sqrt(a) mod p */ +int BN_GF2m_mod_sqrt_arr(BIGNUM *r, const BIGNUM *a, + const int p[], BN_CTX *ctx); +/* r^2 + r = a mod p */ +int BN_GF2m_mod_solve_quad_arr(BIGNUM *r, const BIGNUM *a, + const int p[], BN_CTX *ctx); +int BN_GF2m_poly2arr(const BIGNUM *a, int p[], int max); +int BN_GF2m_arr2poly(const int p[], BIGNUM *a); + +# endif + +/* + * faster mod functions for the 'NIST primes' 0 <= a < p^2 + */ +int BN_nist_mod_192(BIGNUM *r, const BIGNUM *a, const BIGNUM *p, BN_CTX *ctx); +int BN_nist_mod_224(BIGNUM *r, const BIGNUM *a, const BIGNUM *p, BN_CTX *ctx); +int BN_nist_mod_256(BIGNUM *r, const BIGNUM *a, const BIGNUM *p, BN_CTX *ctx); +int BN_nist_mod_384(BIGNUM *r, const BIGNUM *a, const BIGNUM *p, BN_CTX *ctx); +int BN_nist_mod_521(BIGNUM *r, const BIGNUM *a, const BIGNUM *p, BN_CTX *ctx); + +const BIGNUM *BN_get0_nist_prime_192(void); +const BIGNUM *BN_get0_nist_prime_224(void); +const BIGNUM *BN_get0_nist_prime_256(void); +const BIGNUM *BN_get0_nist_prime_384(void); +const BIGNUM *BN_get0_nist_prime_521(void); + +int (*BN_nist_mod_func(const BIGNUM *p)) (BIGNUM *r, const BIGNUM *a, + const BIGNUM *field, BN_CTX *ctx); + +int BN_generate_dsa_nonce(BIGNUM *out, const BIGNUM *range, + const BIGNUM *priv, const unsigned char *message, + size_t message_len, BN_CTX *ctx); + +/* Primes from RFC 2409 */ +BIGNUM *BN_get_rfc2409_prime_768(BIGNUM *bn); +BIGNUM *BN_get_rfc2409_prime_1024(BIGNUM *bn); + +/* Primes from RFC 3526 */ +BIGNUM *BN_get_rfc3526_prime_1536(BIGNUM *bn); +BIGNUM *BN_get_rfc3526_prime_2048(BIGNUM *bn); +BIGNUM *BN_get_rfc3526_prime_3072(BIGNUM *bn); +BIGNUM *BN_get_rfc3526_prime_4096(BIGNUM *bn); +BIGNUM *BN_get_rfc3526_prime_6144(BIGNUM *bn); +BIGNUM *BN_get_rfc3526_prime_8192(BIGNUM *bn); + +# if OPENSSL_API_COMPAT < 0x10100000L +# define get_rfc2409_prime_768 BN_get_rfc2409_prime_768 +# define get_rfc2409_prime_1024 BN_get_rfc2409_prime_1024 +# define get_rfc3526_prime_1536 BN_get_rfc3526_prime_1536 +# define get_rfc3526_prime_2048 BN_get_rfc3526_prime_2048 +# define get_rfc3526_prime_3072 BN_get_rfc3526_prime_3072 +# define get_rfc3526_prime_4096 BN_get_rfc3526_prime_4096 +# define get_rfc3526_prime_6144 BN_get_rfc3526_prime_6144 +# define get_rfc3526_prime_8192 BN_get_rfc3526_prime_8192 +# endif + +int BN_bntest_rand(BIGNUM *rnd, int bits, int top, int bottom); + +/* BEGIN ERROR CODES */ +/* + * The following lines are auto generated by the script mkerr.pl. Any changes + * made after this point may be overwritten when the script is next run. + */ + +int ERR_load_BN_strings(void); + +/* Error codes for the BN functions. */ + +/* Function codes. */ +# define BN_F_BNRAND 127 +# define BN_F_BN_BLINDING_CONVERT_EX 100 +# define BN_F_BN_BLINDING_CREATE_PARAM 128 +# define BN_F_BN_BLINDING_INVERT_EX 101 +# define BN_F_BN_BLINDING_NEW 102 +# define BN_F_BN_BLINDING_UPDATE 103 +# define BN_F_BN_BN2DEC 104 +# define BN_F_BN_BN2HEX 105 +# define BN_F_BN_COMPUTE_WNAF 142 +# define BN_F_BN_CTX_GET 116 +# define BN_F_BN_CTX_NEW 106 +# define BN_F_BN_CTX_START 129 +# define BN_F_BN_DIV 107 +# define BN_F_BN_DIV_RECP 130 +# define BN_F_BN_EXP 123 +# define BN_F_BN_EXPAND_INTERNAL 120 +# define BN_F_BN_GENCB_NEW 143 +# define BN_F_BN_GENERATE_DSA_NONCE 140 +# define BN_F_BN_GENERATE_PRIME_EX 141 +# define BN_F_BN_GF2M_MOD 131 +# define BN_F_BN_GF2M_MOD_EXP 132 +# define BN_F_BN_GF2M_MOD_MUL 133 +# define BN_F_BN_GF2M_MOD_SOLVE_QUAD 134 +# define BN_F_BN_GF2M_MOD_SOLVE_QUAD_ARR 135 +# define BN_F_BN_GF2M_MOD_SQR 136 +# define BN_F_BN_GF2M_MOD_SQRT 137 +# define BN_F_BN_LSHIFT 145 +# define BN_F_BN_MOD_EXP2_MONT 118 +# define BN_F_BN_MOD_EXP_MONT 109 +# define BN_F_BN_MOD_EXP_MONT_CONSTTIME 124 +# define BN_F_BN_MOD_EXP_MONT_WORD 117 +# define BN_F_BN_MOD_EXP_RECP 125 +# define BN_F_BN_MOD_EXP_SIMPLE 126 +# define BN_F_BN_MOD_INVERSE 110 +# define BN_F_BN_MOD_INVERSE_NO_BRANCH 139 +# define BN_F_BN_MOD_LSHIFT_QUICK 119 +# define BN_F_BN_MOD_SQRT 121 +# define BN_F_BN_MPI2BN 112 +# define BN_F_BN_NEW 113 +# define BN_F_BN_RAND 114 +# define BN_F_BN_RAND_RANGE 122 +# define BN_F_BN_RSHIFT 146 +# define BN_F_BN_SET_WORDS 144 +# define BN_F_BN_USUB 115 + +/* Reason codes. */ +# define BN_R_ARG2_LT_ARG3 100 +# define BN_R_BAD_RECIPROCAL 101 +# define BN_R_BIGNUM_TOO_LONG 114 +# define BN_R_BITS_TOO_SMALL 118 +# define BN_R_CALLED_WITH_EVEN_MODULUS 102 +# define BN_R_DIV_BY_ZERO 103 +# define BN_R_ENCODING_ERROR 104 +# define BN_R_EXPAND_ON_STATIC_BIGNUM_DATA 105 +# define BN_R_INPUT_NOT_REDUCED 110 +# define BN_R_INVALID_LENGTH 106 +# define BN_R_INVALID_RANGE 115 +# define BN_R_INVALID_SHIFT 119 +# define BN_R_NOT_A_SQUARE 111 +# define BN_R_NOT_INITIALIZED 107 +# define BN_R_NO_INVERSE 108 +# define BN_R_NO_SOLUTION 116 +# define BN_R_PRIVATE_KEY_TOO_LARGE 117 +# define BN_R_P_IS_NOT_PRIME 112 +# define BN_R_TOO_MANY_ITERATIONS 113 +# define BN_R_TOO_MANY_TEMPORARY_VARIABLES 109 + +# ifdef __cplusplus +} +# endif +#endif diff --git a/mobile/ios/FxA/FxA/include/openssl/buffer.h b/mobile/ios/FxA/FxA/include/openssl/buffer.h new file mode 100644 index 0000000000..91f0e07ff9 --- /dev/null +++ b/mobile/ios/FxA/FxA/include/openssl/buffer.h @@ -0,0 +1,76 @@ +/* + * Copyright 1995-2016 The OpenSSL Project Authors. All Rights Reserved. + * + * Licensed under the OpenSSL license (the "License"). You may not use + * this file except in compliance with the License. You can obtain a copy + * in the file LICENSE in the source distribution or at + * https://www.openssl.org/source/license.html + */ + +#ifndef HEADER_BUFFER_H +# define HEADER_BUFFER_H + +# include +# ifndef HEADER_CRYPTO_H +# include +# endif + + +#ifdef __cplusplus +extern "C" { +#endif + +# include + +# if !defined(NO_SYS_TYPES_H) +# include +# endif + +/* + * These names are outdated as of OpenSSL 1.1; a future release + * will move them to be deprecated. + */ +# define BUF_strdup(s) OPENSSL_strdup(s) +# define BUF_strndup(s, size) OPENSSL_strndup(s, size) +# define BUF_memdup(data, size) OPENSSL_memdup(data, size) +# define BUF_strlcpy(dst, src, size) OPENSSL_strlcpy(dst, src, size) +# define BUF_strlcat(dst, src, size) OPENSSL_strlcat(dst, src, size) +# define BUF_strnlen(str, maxlen) OPENSSL_strnlen(str, maxlen) + +struct buf_mem_st { + size_t length; /* current number of bytes */ + char *data; + size_t max; /* size of buffer */ + unsigned long flags; +}; + +# define BUF_MEM_FLAG_SECURE 0x01 + +BUF_MEM *BUF_MEM_new(void); +BUF_MEM *BUF_MEM_new_ex(unsigned long flags); +void BUF_MEM_free(BUF_MEM *a); +size_t BUF_MEM_grow(BUF_MEM *str, size_t len); +size_t BUF_MEM_grow_clean(BUF_MEM *str, size_t len); +void BUF_reverse(unsigned char *out, const unsigned char *in, size_t siz); + +/* BEGIN ERROR CODES */ +/* + * The following lines are auto generated by the script mkerr.pl. Any changes + * made after this point may be overwritten when the script is next run. + */ + +int ERR_load_BUF_strings(void); + +/* Error codes for the BUF functions. */ + +/* Function codes. */ +# define BUF_F_BUF_MEM_GROW 100 +# define BUF_F_BUF_MEM_GROW_CLEAN 105 +# define BUF_F_BUF_MEM_NEW 101 + +/* Reason codes. */ + +# ifdef __cplusplus +} +# endif +#endif diff --git a/mobile/ios/FxA/FxA/include/openssl/camellia.h b/mobile/ios/FxA/FxA/include/openssl/camellia.h new file mode 100644 index 0000000000..151f3c1349 --- /dev/null +++ b/mobile/ios/FxA/FxA/include/openssl/camellia.h @@ -0,0 +1,83 @@ +/* + * Copyright 2006-2016 The OpenSSL Project Authors. All Rights Reserved. + * + * Licensed under the OpenSSL license (the "License"). You may not use + * this file except in compliance with the License. You can obtain a copy + * in the file LICENSE in the source distribution or at + * https://www.openssl.org/source/license.html + */ + +#ifndef HEADER_CAMELLIA_H +# define HEADER_CAMELLIA_H + +# include + +# ifndef OPENSSL_NO_CAMELLIA +# include +#ifdef __cplusplus +extern "C" { +#endif + +# define CAMELLIA_ENCRYPT 1 +# define CAMELLIA_DECRYPT 0 + +/* + * Because array size can't be a const in C, the following two are macros. + * Both sizes are in bytes. + */ + +/* This should be a hidden type, but EVP requires that the size be known */ + +# define CAMELLIA_BLOCK_SIZE 16 +# define CAMELLIA_TABLE_BYTE_LEN 272 +# define CAMELLIA_TABLE_WORD_LEN (CAMELLIA_TABLE_BYTE_LEN / 4) + +typedef unsigned int KEY_TABLE_TYPE[CAMELLIA_TABLE_WORD_LEN]; /* to match + * with WORD */ + +struct camellia_key_st { + union { + double d; /* ensures 64-bit align */ + KEY_TABLE_TYPE rd_key; + } u; + int grand_rounds; +}; +typedef struct camellia_key_st CAMELLIA_KEY; + +int Camellia_set_key(const unsigned char *userKey, const int bits, + CAMELLIA_KEY *key); + +void Camellia_encrypt(const unsigned char *in, unsigned char *out, + const CAMELLIA_KEY *key); +void Camellia_decrypt(const unsigned char *in, unsigned char *out, + const CAMELLIA_KEY *key); + +void Camellia_ecb_encrypt(const unsigned char *in, unsigned char *out, + const CAMELLIA_KEY *key, const int enc); +void Camellia_cbc_encrypt(const unsigned char *in, unsigned char *out, + size_t length, const CAMELLIA_KEY *key, + unsigned char *ivec, const int enc); +void Camellia_cfb128_encrypt(const unsigned char *in, unsigned char *out, + size_t length, const CAMELLIA_KEY *key, + unsigned char *ivec, int *num, const int enc); +void Camellia_cfb1_encrypt(const unsigned char *in, unsigned char *out, + size_t length, const CAMELLIA_KEY *key, + unsigned char *ivec, int *num, const int enc); +void Camellia_cfb8_encrypt(const unsigned char *in, unsigned char *out, + size_t length, const CAMELLIA_KEY *key, + unsigned char *ivec, int *num, const int enc); +void Camellia_ofb128_encrypt(const unsigned char *in, unsigned char *out, + size_t length, const CAMELLIA_KEY *key, + unsigned char *ivec, int *num); +void Camellia_ctr128_encrypt(const unsigned char *in, unsigned char *out, + size_t length, const CAMELLIA_KEY *key, + unsigned char ivec[CAMELLIA_BLOCK_SIZE], + unsigned char ecount_buf[CAMELLIA_BLOCK_SIZE], + unsigned int *num); + +# ifdef __cplusplus +} +# endif +# endif + +#endif diff --git a/mobile/ios/FxA/FxA/include/openssl/cast.h b/mobile/ios/FxA/FxA/include/openssl/cast.h new file mode 100644 index 0000000000..2cc89ae013 --- /dev/null +++ b/mobile/ios/FxA/FxA/include/openssl/cast.h @@ -0,0 +1,53 @@ +/* + * Copyright 1995-2016 The OpenSSL Project Authors. All Rights Reserved. + * + * Licensed under the OpenSSL license (the "License"). You may not use + * this file except in compliance with the License. You can obtain a copy + * in the file LICENSE in the source distribution or at + * https://www.openssl.org/source/license.html + */ + +#ifndef HEADER_CAST_H +# define HEADER_CAST_H + +# include + +# ifndef OPENSSL_NO_CAST +# ifdef __cplusplus +extern "C" { +# endif + +# define CAST_ENCRYPT 1 +# define CAST_DECRYPT 0 + +# define CAST_LONG unsigned int + +# define CAST_BLOCK 8 +# define CAST_KEY_LENGTH 16 + +typedef struct cast_key_st { + CAST_LONG data[32]; + int short_key; /* Use reduced rounds for short key */ +} CAST_KEY; + +void CAST_set_key(CAST_KEY *key, int len, const unsigned char *data); +void CAST_ecb_encrypt(const unsigned char *in, unsigned char *out, + const CAST_KEY *key, int enc); +void CAST_encrypt(CAST_LONG *data, const CAST_KEY *key); +void CAST_decrypt(CAST_LONG *data, const CAST_KEY *key); +void CAST_cbc_encrypt(const unsigned char *in, unsigned char *out, + long length, const CAST_KEY *ks, unsigned char *iv, + int enc); +void CAST_cfb64_encrypt(const unsigned char *in, unsigned char *out, + long length, const CAST_KEY *schedule, + unsigned char *ivec, int *num, int enc); +void CAST_ofb64_encrypt(const unsigned char *in, unsigned char *out, + long length, const CAST_KEY *schedule, + unsigned char *ivec, int *num); + +# ifdef __cplusplus +} +# endif +# endif + +#endif diff --git a/mobile/ios/FxA/FxA/include/openssl/cmac.h b/mobile/ios/FxA/FxA/include/openssl/cmac.h new file mode 100644 index 0000000000..3535a9abf7 --- /dev/null +++ b/mobile/ios/FxA/FxA/include/openssl/cmac.h @@ -0,0 +1,41 @@ +/* + * Copyright 2010-2016 The OpenSSL Project Authors. All Rights Reserved. + * + * Licensed under the OpenSSL license (the "License"). You may not use + * this file except in compliance with the License. You can obtain a copy + * in the file LICENSE in the source distribution or at + * https://www.openssl.org/source/license.html + */ + +#ifndef HEADER_CMAC_H +# define HEADER_CMAC_H + +# ifndef OPENSSL_NO_CMAC + +#ifdef __cplusplus +extern "C" { +#endif + +# include + +/* Opaque */ +typedef struct CMAC_CTX_st CMAC_CTX; + +CMAC_CTX *CMAC_CTX_new(void); +void CMAC_CTX_cleanup(CMAC_CTX *ctx); +void CMAC_CTX_free(CMAC_CTX *ctx); +EVP_CIPHER_CTX *CMAC_CTX_get0_cipher_ctx(CMAC_CTX *ctx); +int CMAC_CTX_copy(CMAC_CTX *out, const CMAC_CTX *in); + +int CMAC_Init(CMAC_CTX *ctx, const void *key, size_t keylen, + const EVP_CIPHER *cipher, ENGINE *impl); +int CMAC_Update(CMAC_CTX *ctx, const void *data, size_t dlen); +int CMAC_Final(CMAC_CTX *ctx, unsigned char *out, size_t *poutlen); +int CMAC_resume(CMAC_CTX *ctx); + +#ifdef __cplusplus +} +#endif + +# endif +#endif diff --git a/mobile/ios/FxA/FxA/include/openssl/cms.h b/mobile/ios/FxA/FxA/include/openssl/cms.h new file mode 100644 index 0000000000..7e534e0dd6 --- /dev/null +++ b/mobile/ios/FxA/FxA/include/openssl/cms.h @@ -0,0 +1,512 @@ +/* + * Copyright 2008-2016 The OpenSSL Project Authors. All Rights Reserved. + * + * Licensed under the OpenSSL license (the "License"). You may not use + * this file except in compliance with the License. You can obtain a copy + * in the file LICENSE in the source distribution or at + * https://www.openssl.org/source/license.html + */ + +#ifndef HEADER_CMS_H +# define HEADER_CMS_H + +# include + +# ifndef OPENSSL_NO_CMS +# include +# include +# ifdef __cplusplus +extern "C" { +# endif + +typedef struct CMS_ContentInfo_st CMS_ContentInfo; +typedef struct CMS_SignerInfo_st CMS_SignerInfo; +typedef struct CMS_CertificateChoices CMS_CertificateChoices; +typedef struct CMS_RevocationInfoChoice_st CMS_RevocationInfoChoice; +typedef struct CMS_RecipientInfo_st CMS_RecipientInfo; +typedef struct CMS_ReceiptRequest_st CMS_ReceiptRequest; +typedef struct CMS_Receipt_st CMS_Receipt; +typedef struct CMS_RecipientEncryptedKey_st CMS_RecipientEncryptedKey; +typedef struct CMS_OtherKeyAttribute_st CMS_OtherKeyAttribute; + +DEFINE_STACK_OF(CMS_SignerInfo) +DEFINE_STACK_OF(CMS_RecipientEncryptedKey) +DEFINE_STACK_OF(CMS_RecipientInfo) +DEFINE_STACK_OF(CMS_RevocationInfoChoice) +DECLARE_ASN1_FUNCTIONS(CMS_ContentInfo) +DECLARE_ASN1_FUNCTIONS(CMS_ReceiptRequest) +DECLARE_ASN1_PRINT_FUNCTION(CMS_ContentInfo) + +# define CMS_SIGNERINFO_ISSUER_SERIAL 0 +# define CMS_SIGNERINFO_KEYIDENTIFIER 1 + +# define CMS_RECIPINFO_NONE -1 +# define CMS_RECIPINFO_TRANS 0 +# define CMS_RECIPINFO_AGREE 1 +# define CMS_RECIPINFO_KEK 2 +# define CMS_RECIPINFO_PASS 3 +# define CMS_RECIPINFO_OTHER 4 + +/* S/MIME related flags */ + +# define CMS_TEXT 0x1 +# define CMS_NOCERTS 0x2 +# define CMS_NO_CONTENT_VERIFY 0x4 +# define CMS_NO_ATTR_VERIFY 0x8 +# define CMS_NOSIGS \ + (CMS_NO_CONTENT_VERIFY|CMS_NO_ATTR_VERIFY) +# define CMS_NOINTERN 0x10 +# define CMS_NO_SIGNER_CERT_VERIFY 0x20 +# define CMS_NOVERIFY 0x20 +# define CMS_DETACHED 0x40 +# define CMS_BINARY 0x80 +# define CMS_NOATTR 0x100 +# define CMS_NOSMIMECAP 0x200 +# define CMS_NOOLDMIMETYPE 0x400 +# define CMS_CRLFEOL 0x800 +# define CMS_STREAM 0x1000 +# define CMS_NOCRL 0x2000 +# define CMS_PARTIAL 0x4000 +# define CMS_REUSE_DIGEST 0x8000 +# define CMS_USE_KEYID 0x10000 +# define CMS_DEBUG_DECRYPT 0x20000 +# define CMS_KEY_PARAM 0x40000 +# define CMS_ASCIICRLF 0x80000 + +const ASN1_OBJECT *CMS_get0_type(const CMS_ContentInfo *cms); + +BIO *CMS_dataInit(CMS_ContentInfo *cms, BIO *icont); +int CMS_dataFinal(CMS_ContentInfo *cms, BIO *bio); + +ASN1_OCTET_STRING **CMS_get0_content(CMS_ContentInfo *cms); +int CMS_is_detached(CMS_ContentInfo *cms); +int CMS_set_detached(CMS_ContentInfo *cms, int detached); + +# ifdef HEADER_PEM_H +DECLARE_PEM_rw_const(CMS, CMS_ContentInfo) +# endif +int CMS_stream(unsigned char ***boundary, CMS_ContentInfo *cms); +CMS_ContentInfo *d2i_CMS_bio(BIO *bp, CMS_ContentInfo **cms); +int i2d_CMS_bio(BIO *bp, CMS_ContentInfo *cms); + +BIO *BIO_new_CMS(BIO *out, CMS_ContentInfo *cms); +int i2d_CMS_bio_stream(BIO *out, CMS_ContentInfo *cms, BIO *in, int flags); +int PEM_write_bio_CMS_stream(BIO *out, CMS_ContentInfo *cms, BIO *in, + int flags); +CMS_ContentInfo *SMIME_read_CMS(BIO *bio, BIO **bcont); +int SMIME_write_CMS(BIO *bio, CMS_ContentInfo *cms, BIO *data, int flags); + +int CMS_final(CMS_ContentInfo *cms, BIO *data, BIO *dcont, + unsigned int flags); + +CMS_ContentInfo *CMS_sign(X509 *signcert, EVP_PKEY *pkey, + STACK_OF(X509) *certs, BIO *data, + unsigned int flags); + +CMS_ContentInfo *CMS_sign_receipt(CMS_SignerInfo *si, + X509 *signcert, EVP_PKEY *pkey, + STACK_OF(X509) *certs, unsigned int flags); + +int CMS_data(CMS_ContentInfo *cms, BIO *out, unsigned int flags); +CMS_ContentInfo *CMS_data_create(BIO *in, unsigned int flags); + +int CMS_digest_verify(CMS_ContentInfo *cms, BIO *dcont, BIO *out, + unsigned int flags); +CMS_ContentInfo *CMS_digest_create(BIO *in, const EVP_MD *md, + unsigned int flags); + +int CMS_EncryptedData_decrypt(CMS_ContentInfo *cms, + const unsigned char *key, size_t keylen, + BIO *dcont, BIO *out, unsigned int flags); + +CMS_ContentInfo *CMS_EncryptedData_encrypt(BIO *in, const EVP_CIPHER *cipher, + const unsigned char *key, + size_t keylen, unsigned int flags); + +int CMS_EncryptedData_set1_key(CMS_ContentInfo *cms, const EVP_CIPHER *ciph, + const unsigned char *key, size_t keylen); + +int CMS_verify(CMS_ContentInfo *cms, STACK_OF(X509) *certs, + X509_STORE *store, BIO *dcont, BIO *out, unsigned int flags); + +int CMS_verify_receipt(CMS_ContentInfo *rcms, CMS_ContentInfo *ocms, + STACK_OF(X509) *certs, + X509_STORE *store, unsigned int flags); + +STACK_OF(X509) *CMS_get0_signers(CMS_ContentInfo *cms); + +CMS_ContentInfo *CMS_encrypt(STACK_OF(X509) *certs, BIO *in, + const EVP_CIPHER *cipher, unsigned int flags); + +int CMS_decrypt(CMS_ContentInfo *cms, EVP_PKEY *pkey, X509 *cert, + BIO *dcont, BIO *out, unsigned int flags); + +int CMS_decrypt_set1_pkey(CMS_ContentInfo *cms, EVP_PKEY *pk, X509 *cert); +int CMS_decrypt_set1_key(CMS_ContentInfo *cms, + unsigned char *key, size_t keylen, + const unsigned char *id, size_t idlen); +int CMS_decrypt_set1_password(CMS_ContentInfo *cms, + unsigned char *pass, ossl_ssize_t passlen); + +STACK_OF(CMS_RecipientInfo) *CMS_get0_RecipientInfos(CMS_ContentInfo *cms); +int CMS_RecipientInfo_type(CMS_RecipientInfo *ri); +EVP_PKEY_CTX *CMS_RecipientInfo_get0_pkey_ctx(CMS_RecipientInfo *ri); +CMS_ContentInfo *CMS_EnvelopedData_create(const EVP_CIPHER *cipher); +CMS_RecipientInfo *CMS_add1_recipient_cert(CMS_ContentInfo *cms, + X509 *recip, unsigned int flags); +int CMS_RecipientInfo_set0_pkey(CMS_RecipientInfo *ri, EVP_PKEY *pkey); +int CMS_RecipientInfo_ktri_cert_cmp(CMS_RecipientInfo *ri, X509 *cert); +int CMS_RecipientInfo_ktri_get0_algs(CMS_RecipientInfo *ri, + EVP_PKEY **pk, X509 **recip, + X509_ALGOR **palg); +int CMS_RecipientInfo_ktri_get0_signer_id(CMS_RecipientInfo *ri, + ASN1_OCTET_STRING **keyid, + X509_NAME **issuer, + ASN1_INTEGER **sno); + +CMS_RecipientInfo *CMS_add0_recipient_key(CMS_ContentInfo *cms, int nid, + unsigned char *key, size_t keylen, + unsigned char *id, size_t idlen, + ASN1_GENERALIZEDTIME *date, + ASN1_OBJECT *otherTypeId, + ASN1_TYPE *otherType); + +int CMS_RecipientInfo_kekri_get0_id(CMS_RecipientInfo *ri, + X509_ALGOR **palg, + ASN1_OCTET_STRING **pid, + ASN1_GENERALIZEDTIME **pdate, + ASN1_OBJECT **potherid, + ASN1_TYPE **pothertype); + +int CMS_RecipientInfo_set0_key(CMS_RecipientInfo *ri, + unsigned char *key, size_t keylen); + +int CMS_RecipientInfo_kekri_id_cmp(CMS_RecipientInfo *ri, + const unsigned char *id, size_t idlen); + +int CMS_RecipientInfo_set0_password(CMS_RecipientInfo *ri, + unsigned char *pass, + ossl_ssize_t passlen); + +CMS_RecipientInfo *CMS_add0_recipient_password(CMS_ContentInfo *cms, + int iter, int wrap_nid, + int pbe_nid, + unsigned char *pass, + ossl_ssize_t passlen, + const EVP_CIPHER *kekciph); + +int CMS_RecipientInfo_decrypt(CMS_ContentInfo *cms, CMS_RecipientInfo *ri); +int CMS_RecipientInfo_encrypt(CMS_ContentInfo *cms, CMS_RecipientInfo *ri); + +int CMS_uncompress(CMS_ContentInfo *cms, BIO *dcont, BIO *out, + unsigned int flags); +CMS_ContentInfo *CMS_compress(BIO *in, int comp_nid, unsigned int flags); + +int CMS_set1_eContentType(CMS_ContentInfo *cms, const ASN1_OBJECT *oid); +const ASN1_OBJECT *CMS_get0_eContentType(CMS_ContentInfo *cms); + +CMS_CertificateChoices *CMS_add0_CertificateChoices(CMS_ContentInfo *cms); +int CMS_add0_cert(CMS_ContentInfo *cms, X509 *cert); +int CMS_add1_cert(CMS_ContentInfo *cms, X509 *cert); +STACK_OF(X509) *CMS_get1_certs(CMS_ContentInfo *cms); + +CMS_RevocationInfoChoice *CMS_add0_RevocationInfoChoice(CMS_ContentInfo *cms); +int CMS_add0_crl(CMS_ContentInfo *cms, X509_CRL *crl); +int CMS_add1_crl(CMS_ContentInfo *cms, X509_CRL *crl); +STACK_OF(X509_CRL) *CMS_get1_crls(CMS_ContentInfo *cms); + +int CMS_SignedData_init(CMS_ContentInfo *cms); +CMS_SignerInfo *CMS_add1_signer(CMS_ContentInfo *cms, + X509 *signer, EVP_PKEY *pk, const EVP_MD *md, + unsigned int flags); +EVP_PKEY_CTX *CMS_SignerInfo_get0_pkey_ctx(CMS_SignerInfo *si); +EVP_MD_CTX *CMS_SignerInfo_get0_md_ctx(CMS_SignerInfo *si); +STACK_OF(CMS_SignerInfo) *CMS_get0_SignerInfos(CMS_ContentInfo *cms); + +void CMS_SignerInfo_set1_signer_cert(CMS_SignerInfo *si, X509 *signer); +int CMS_SignerInfo_get0_signer_id(CMS_SignerInfo *si, + ASN1_OCTET_STRING **keyid, + X509_NAME **issuer, ASN1_INTEGER **sno); +int CMS_SignerInfo_cert_cmp(CMS_SignerInfo *si, X509 *cert); +int CMS_set1_signers_certs(CMS_ContentInfo *cms, STACK_OF(X509) *certs, + unsigned int flags); +void CMS_SignerInfo_get0_algs(CMS_SignerInfo *si, EVP_PKEY **pk, + X509 **signer, X509_ALGOR **pdig, + X509_ALGOR **psig); +ASN1_OCTET_STRING *CMS_SignerInfo_get0_signature(CMS_SignerInfo *si); +int CMS_SignerInfo_sign(CMS_SignerInfo *si); +int CMS_SignerInfo_verify(CMS_SignerInfo *si); +int CMS_SignerInfo_verify_content(CMS_SignerInfo *si, BIO *chain); + +int CMS_add_smimecap(CMS_SignerInfo *si, STACK_OF(X509_ALGOR) *algs); +int CMS_add_simple_smimecap(STACK_OF(X509_ALGOR) **algs, + int algnid, int keysize); +int CMS_add_standard_smimecap(STACK_OF(X509_ALGOR) **smcap); + +int CMS_signed_get_attr_count(const CMS_SignerInfo *si); +int CMS_signed_get_attr_by_NID(const CMS_SignerInfo *si, int nid, + int lastpos); +int CMS_signed_get_attr_by_OBJ(const CMS_SignerInfo *si, const ASN1_OBJECT *obj, + int lastpos); +X509_ATTRIBUTE *CMS_signed_get_attr(const CMS_SignerInfo *si, int loc); +X509_ATTRIBUTE *CMS_signed_delete_attr(CMS_SignerInfo *si, int loc); +int CMS_signed_add1_attr(CMS_SignerInfo *si, X509_ATTRIBUTE *attr); +int CMS_signed_add1_attr_by_OBJ(CMS_SignerInfo *si, + const ASN1_OBJECT *obj, int type, + const void *bytes, int len); +int CMS_signed_add1_attr_by_NID(CMS_SignerInfo *si, + int nid, int type, + const void *bytes, int len); +int CMS_signed_add1_attr_by_txt(CMS_SignerInfo *si, + const char *attrname, int type, + const void *bytes, int len); +void *CMS_signed_get0_data_by_OBJ(CMS_SignerInfo *si, const ASN1_OBJECT *oid, + int lastpos, int type); + +int CMS_unsigned_get_attr_count(const CMS_SignerInfo *si); +int CMS_unsigned_get_attr_by_NID(const CMS_SignerInfo *si, int nid, + int lastpos); +int CMS_unsigned_get_attr_by_OBJ(const CMS_SignerInfo *si, + const ASN1_OBJECT *obj, int lastpos); +X509_ATTRIBUTE *CMS_unsigned_get_attr(const CMS_SignerInfo *si, int loc); +X509_ATTRIBUTE *CMS_unsigned_delete_attr(CMS_SignerInfo *si, int loc); +int CMS_unsigned_add1_attr(CMS_SignerInfo *si, X509_ATTRIBUTE *attr); +int CMS_unsigned_add1_attr_by_OBJ(CMS_SignerInfo *si, + const ASN1_OBJECT *obj, int type, + const void *bytes, int len); +int CMS_unsigned_add1_attr_by_NID(CMS_SignerInfo *si, + int nid, int type, + const void *bytes, int len); +int CMS_unsigned_add1_attr_by_txt(CMS_SignerInfo *si, + const char *attrname, int type, + const void *bytes, int len); +void *CMS_unsigned_get0_data_by_OBJ(CMS_SignerInfo *si, ASN1_OBJECT *oid, + int lastpos, int type); + +# ifdef HEADER_X509V3_H + +int CMS_get1_ReceiptRequest(CMS_SignerInfo *si, CMS_ReceiptRequest **prr); +CMS_ReceiptRequest *CMS_ReceiptRequest_create0(unsigned char *id, int idlen, + int allorfirst, + STACK_OF(GENERAL_NAMES) + *receiptList, STACK_OF(GENERAL_NAMES) + *receiptsTo); +int CMS_add1_ReceiptRequest(CMS_SignerInfo *si, CMS_ReceiptRequest *rr); +void CMS_ReceiptRequest_get0_values(CMS_ReceiptRequest *rr, + ASN1_STRING **pcid, + int *pallorfirst, + STACK_OF(GENERAL_NAMES) **plist, + STACK_OF(GENERAL_NAMES) **prto); +# endif +int CMS_RecipientInfo_kari_get0_alg(CMS_RecipientInfo *ri, + X509_ALGOR **palg, + ASN1_OCTET_STRING **pukm); +STACK_OF(CMS_RecipientEncryptedKey) +*CMS_RecipientInfo_kari_get0_reks(CMS_RecipientInfo *ri); + +int CMS_RecipientInfo_kari_get0_orig_id(CMS_RecipientInfo *ri, + X509_ALGOR **pubalg, + ASN1_BIT_STRING **pubkey, + ASN1_OCTET_STRING **keyid, + X509_NAME **issuer, + ASN1_INTEGER **sno); + +int CMS_RecipientInfo_kari_orig_id_cmp(CMS_RecipientInfo *ri, X509 *cert); + +int CMS_RecipientEncryptedKey_get0_id(CMS_RecipientEncryptedKey *rek, + ASN1_OCTET_STRING **keyid, + ASN1_GENERALIZEDTIME **tm, + CMS_OtherKeyAttribute **other, + X509_NAME **issuer, ASN1_INTEGER **sno); +int CMS_RecipientEncryptedKey_cert_cmp(CMS_RecipientEncryptedKey *rek, + X509 *cert); +int CMS_RecipientInfo_kari_set0_pkey(CMS_RecipientInfo *ri, EVP_PKEY *pk); +EVP_CIPHER_CTX *CMS_RecipientInfo_kari_get0_ctx(CMS_RecipientInfo *ri); +int CMS_RecipientInfo_kari_decrypt(CMS_ContentInfo *cms, + CMS_RecipientInfo *ri, + CMS_RecipientEncryptedKey *rek); + +int CMS_SharedInfo_encode(unsigned char **pder, X509_ALGOR *kekalg, + ASN1_OCTET_STRING *ukm, int keylen); + +/* BEGIN ERROR CODES */ +/* + * The following lines are auto generated by the script mkerr.pl. Any changes + * made after this point may be overwritten when the script is next run. + */ + +int ERR_load_CMS_strings(void); + +/* Error codes for the CMS functions. */ + +/* Function codes. */ +# define CMS_F_CHECK_CONTENT 99 +# define CMS_F_CMS_ADD0_CERT 164 +# define CMS_F_CMS_ADD0_RECIPIENT_KEY 100 +# define CMS_F_CMS_ADD0_RECIPIENT_PASSWORD 165 +# define CMS_F_CMS_ADD1_RECEIPTREQUEST 158 +# define CMS_F_CMS_ADD1_RECIPIENT_CERT 101 +# define CMS_F_CMS_ADD1_SIGNER 102 +# define CMS_F_CMS_ADD1_SIGNINGTIME 103 +# define CMS_F_CMS_COMPRESS 104 +# define CMS_F_CMS_COMPRESSEDDATA_CREATE 105 +# define CMS_F_CMS_COMPRESSEDDATA_INIT_BIO 106 +# define CMS_F_CMS_COPY_CONTENT 107 +# define CMS_F_CMS_COPY_MESSAGEDIGEST 108 +# define CMS_F_CMS_DATA 109 +# define CMS_F_CMS_DATAFINAL 110 +# define CMS_F_CMS_DATAINIT 111 +# define CMS_F_CMS_DECRYPT 112 +# define CMS_F_CMS_DECRYPT_SET1_KEY 113 +# define CMS_F_CMS_DECRYPT_SET1_PASSWORD 166 +# define CMS_F_CMS_DECRYPT_SET1_PKEY 114 +# define CMS_F_CMS_DIGESTALGORITHM_FIND_CTX 115 +# define CMS_F_CMS_DIGESTALGORITHM_INIT_BIO 116 +# define CMS_F_CMS_DIGESTEDDATA_DO_FINAL 117 +# define CMS_F_CMS_DIGEST_VERIFY 118 +# define CMS_F_CMS_ENCODE_RECEIPT 161 +# define CMS_F_CMS_ENCRYPT 119 +# define CMS_F_CMS_ENCRYPTEDCONTENT_INIT_BIO 120 +# define CMS_F_CMS_ENCRYPTEDDATA_DECRYPT 121 +# define CMS_F_CMS_ENCRYPTEDDATA_ENCRYPT 122 +# define CMS_F_CMS_ENCRYPTEDDATA_SET1_KEY 123 +# define CMS_F_CMS_ENVELOPEDDATA_CREATE 124 +# define CMS_F_CMS_ENVELOPEDDATA_INIT_BIO 125 +# define CMS_F_CMS_ENVELOPED_DATA_INIT 126 +# define CMS_F_CMS_ENV_ASN1_CTRL 171 +# define CMS_F_CMS_FINAL 127 +# define CMS_F_CMS_GET0_CERTIFICATE_CHOICES 128 +# define CMS_F_CMS_GET0_CONTENT 129 +# define CMS_F_CMS_GET0_ECONTENT_TYPE 130 +# define CMS_F_CMS_GET0_ENVELOPED 131 +# define CMS_F_CMS_GET0_REVOCATION_CHOICES 132 +# define CMS_F_CMS_GET0_SIGNED 133 +# define CMS_F_CMS_MSGSIGDIGEST_ADD1 162 +# define CMS_F_CMS_RECEIPTREQUEST_CREATE0 159 +# define CMS_F_CMS_RECEIPT_VERIFY 160 +# define CMS_F_CMS_RECIPIENTINFO_DECRYPT 134 +# define CMS_F_CMS_RECIPIENTINFO_ENCRYPT 169 +# define CMS_F_CMS_RECIPIENTINFO_KARI_ENCRYPT 178 +# define CMS_F_CMS_RECIPIENTINFO_KARI_GET0_ALG 175 +# define CMS_F_CMS_RECIPIENTINFO_KARI_GET0_ORIG_ID 173 +# define CMS_F_CMS_RECIPIENTINFO_KARI_GET0_REKS 172 +# define CMS_F_CMS_RECIPIENTINFO_KARI_ORIG_ID_CMP 174 +# define CMS_F_CMS_RECIPIENTINFO_KEKRI_DECRYPT 135 +# define CMS_F_CMS_RECIPIENTINFO_KEKRI_ENCRYPT 136 +# define CMS_F_CMS_RECIPIENTINFO_KEKRI_GET0_ID 137 +# define CMS_F_CMS_RECIPIENTINFO_KEKRI_ID_CMP 138 +# define CMS_F_CMS_RECIPIENTINFO_KTRI_CERT_CMP 139 +# define CMS_F_CMS_RECIPIENTINFO_KTRI_DECRYPT 140 +# define CMS_F_CMS_RECIPIENTINFO_KTRI_ENCRYPT 141 +# define CMS_F_CMS_RECIPIENTINFO_KTRI_GET0_ALGS 142 +# define CMS_F_CMS_RECIPIENTINFO_KTRI_GET0_SIGNER_ID 143 +# define CMS_F_CMS_RECIPIENTINFO_PWRI_CRYPT 167 +# define CMS_F_CMS_RECIPIENTINFO_SET0_KEY 144 +# define CMS_F_CMS_RECIPIENTINFO_SET0_PASSWORD 168 +# define CMS_F_CMS_RECIPIENTINFO_SET0_PKEY 145 +# define CMS_F_CMS_SD_ASN1_CTRL 170 +# define CMS_F_CMS_SET1_IAS 176 +# define CMS_F_CMS_SET1_KEYID 177 +# define CMS_F_CMS_SET1_SIGNERIDENTIFIER 146 +# define CMS_F_CMS_SET_DETACHED 147 +# define CMS_F_CMS_SIGN 148 +# define CMS_F_CMS_SIGNED_DATA_INIT 149 +# define CMS_F_CMS_SIGNERINFO_CONTENT_SIGN 150 +# define CMS_F_CMS_SIGNERINFO_SIGN 151 +# define CMS_F_CMS_SIGNERINFO_VERIFY 152 +# define CMS_F_CMS_SIGNERINFO_VERIFY_CERT 153 +# define CMS_F_CMS_SIGNERINFO_VERIFY_CONTENT 154 +# define CMS_F_CMS_SIGN_RECEIPT 163 +# define CMS_F_CMS_STREAM 155 +# define CMS_F_CMS_UNCOMPRESS 156 +# define CMS_F_CMS_VERIFY 157 + +/* Reason codes. */ +# define CMS_R_ADD_SIGNER_ERROR 99 +# define CMS_R_CERTIFICATE_ALREADY_PRESENT 175 +# define CMS_R_CERTIFICATE_HAS_NO_KEYID 160 +# define CMS_R_CERTIFICATE_VERIFY_ERROR 100 +# define CMS_R_CIPHER_INITIALISATION_ERROR 101 +# define CMS_R_CIPHER_PARAMETER_INITIALISATION_ERROR 102 +# define CMS_R_CMS_DATAFINAL_ERROR 103 +# define CMS_R_CMS_LIB 104 +# define CMS_R_CONTENTIDENTIFIER_MISMATCH 170 +# define CMS_R_CONTENT_NOT_FOUND 105 +# define CMS_R_CONTENT_TYPE_MISMATCH 171 +# define CMS_R_CONTENT_TYPE_NOT_COMPRESSED_DATA 106 +# define CMS_R_CONTENT_TYPE_NOT_ENVELOPED_DATA 107 +# define CMS_R_CONTENT_TYPE_NOT_SIGNED_DATA 108 +# define CMS_R_CONTENT_VERIFY_ERROR 109 +# define CMS_R_CTRL_ERROR 110 +# define CMS_R_CTRL_FAILURE 111 +# define CMS_R_DECRYPT_ERROR 112 +# define CMS_R_ERROR_GETTING_PUBLIC_KEY 113 +# define CMS_R_ERROR_READING_MESSAGEDIGEST_ATTRIBUTE 114 +# define CMS_R_ERROR_SETTING_KEY 115 +# define CMS_R_ERROR_SETTING_RECIPIENTINFO 116 +# define CMS_R_INVALID_ENCRYPTED_KEY_LENGTH 117 +# define CMS_R_INVALID_KEY_ENCRYPTION_PARAMETER 176 +# define CMS_R_INVALID_KEY_LENGTH 118 +# define CMS_R_MD_BIO_INIT_ERROR 119 +# define CMS_R_MESSAGEDIGEST_ATTRIBUTE_WRONG_LENGTH 120 +# define CMS_R_MESSAGEDIGEST_WRONG_LENGTH 121 +# define CMS_R_MSGSIGDIGEST_ERROR 172 +# define CMS_R_MSGSIGDIGEST_VERIFICATION_FAILURE 162 +# define CMS_R_MSGSIGDIGEST_WRONG_LENGTH 163 +# define CMS_R_NEED_ONE_SIGNER 164 +# define CMS_R_NOT_A_SIGNED_RECEIPT 165 +# define CMS_R_NOT_ENCRYPTED_DATA 122 +# define CMS_R_NOT_KEK 123 +# define CMS_R_NOT_KEY_AGREEMENT 181 +# define CMS_R_NOT_KEY_TRANSPORT 124 +# define CMS_R_NOT_PWRI 177 +# define CMS_R_NOT_SUPPORTED_FOR_THIS_KEY_TYPE 125 +# define CMS_R_NO_CIPHER 126 +# define CMS_R_NO_CONTENT 127 +# define CMS_R_NO_CONTENT_TYPE 173 +# define CMS_R_NO_DEFAULT_DIGEST 128 +# define CMS_R_NO_DIGEST_SET 129 +# define CMS_R_NO_KEY 130 +# define CMS_R_NO_KEY_OR_CERT 174 +# define CMS_R_NO_MATCHING_DIGEST 131 +# define CMS_R_NO_MATCHING_RECIPIENT 132 +# define CMS_R_NO_MATCHING_SIGNATURE 166 +# define CMS_R_NO_MSGSIGDIGEST 167 +# define CMS_R_NO_PASSWORD 178 +# define CMS_R_NO_PRIVATE_KEY 133 +# define CMS_R_NO_PUBLIC_KEY 134 +# define CMS_R_NO_RECEIPT_REQUEST 168 +# define CMS_R_NO_SIGNERS 135 +# define CMS_R_PRIVATE_KEY_DOES_NOT_MATCH_CERTIFICATE 136 +# define CMS_R_RECEIPT_DECODE_ERROR 169 +# define CMS_R_RECIPIENT_ERROR 137 +# define CMS_R_SIGNER_CERTIFICATE_NOT_FOUND 138 +# define CMS_R_SIGNFINAL_ERROR 139 +# define CMS_R_SMIME_TEXT_ERROR 140 +# define CMS_R_STORE_INIT_ERROR 141 +# define CMS_R_TYPE_NOT_COMPRESSED_DATA 142 +# define CMS_R_TYPE_NOT_DATA 143 +# define CMS_R_TYPE_NOT_DIGESTED_DATA 144 +# define CMS_R_TYPE_NOT_ENCRYPTED_DATA 145 +# define CMS_R_TYPE_NOT_ENVELOPED_DATA 146 +# define CMS_R_UNABLE_TO_FINALIZE_CONTEXT 147 +# define CMS_R_UNKNOWN_CIPHER 148 +# define CMS_R_UNKNOWN_DIGEST_ALGORIHM 149 +# define CMS_R_UNKNOWN_ID 150 +# define CMS_R_UNSUPPORTED_COMPRESSION_ALGORITHM 151 +# define CMS_R_UNSUPPORTED_CONTENT_TYPE 152 +# define CMS_R_UNSUPPORTED_KEK_ALGORITHM 153 +# define CMS_R_UNSUPPORTED_KEY_ENCRYPTION_ALGORITHM 179 +# define CMS_R_UNSUPPORTED_RECIPIENT_TYPE 154 +# define CMS_R_UNSUPPORTED_RECPIENTINFO_TYPE 155 +# define CMS_R_UNSUPPORTED_TYPE 156 +# define CMS_R_UNWRAP_ERROR 157 +# define CMS_R_UNWRAP_FAILURE 180 +# define CMS_R_VERIFICATION_FAILURE 158 +# define CMS_R_WRAP_ERROR 159 + +# ifdef __cplusplus +} +# endif +# endif +#endif diff --git a/mobile/ios/FxA/FxA/include/openssl/comp.h b/mobile/ios/FxA/FxA/include/openssl/comp.h new file mode 100644 index 0000000000..260ff1e0ac --- /dev/null +++ b/mobile/ios/FxA/FxA/include/openssl/comp.h @@ -0,0 +1,72 @@ +/* + * Copyright 2015-2016 The OpenSSL Project Authors. All Rights Reserved. + * + * Licensed under the OpenSSL license (the "License"). You may not use + * this file except in compliance with the License. You can obtain a copy + * in the file LICENSE in the source distribution or at + * https://www.openssl.org/source/license.html + */ + +#ifndef HEADER_COMP_H +# define HEADER_COMP_H + +# include + +# ifndef OPENSSL_NO_COMP +# include +# ifdef __cplusplus +extern "C" { +# endif + + + +COMP_CTX *COMP_CTX_new(COMP_METHOD *meth); +const COMP_METHOD *COMP_CTX_get_method(const COMP_CTX *ctx); +int COMP_CTX_get_type(const COMP_CTX* comp); +int COMP_get_type(const COMP_METHOD *meth); +const char *COMP_get_name(const COMP_METHOD *meth); +void COMP_CTX_free(COMP_CTX *ctx); + +int COMP_compress_block(COMP_CTX *ctx, unsigned char *out, int olen, + unsigned char *in, int ilen); +int COMP_expand_block(COMP_CTX *ctx, unsigned char *out, int olen, + unsigned char *in, int ilen); + +COMP_METHOD *COMP_zlib(void); + +#if OPENSSL_API_COMPAT < 0x10100000L +#define COMP_zlib_cleanup() while(0) continue +#endif + +# ifdef HEADER_BIO_H +# ifdef ZLIB +const BIO_METHOD *BIO_f_zlib(void); +# endif +# endif + +/* BEGIN ERROR CODES */ +/* + * The following lines are auto generated by the script mkerr.pl. Any changes + * made after this point may be overwritten when the script is next run. + */ + +int ERR_load_COMP_strings(void); + +/* Error codes for the COMP functions. */ + +/* Function codes. */ +# define COMP_F_BIO_ZLIB_FLUSH 99 +# define COMP_F_BIO_ZLIB_NEW 100 +# define COMP_F_BIO_ZLIB_READ 101 +# define COMP_F_BIO_ZLIB_WRITE 102 + +/* Reason codes. */ +# define COMP_R_ZLIB_DEFLATE_ERROR 99 +# define COMP_R_ZLIB_INFLATE_ERROR 100 +# define COMP_R_ZLIB_NOT_SUPPORTED 101 + +# ifdef __cplusplus +} +# endif +# endif +#endif diff --git a/mobile/ios/FxA/FxA/include/openssl/conf.h b/mobile/ios/FxA/FxA/include/openssl/conf.h new file mode 100644 index 0000000000..462e3c9d39 --- /dev/null +++ b/mobile/ios/FxA/FxA/include/openssl/conf.h @@ -0,0 +1,216 @@ +/* + * Copyright 1995-2016 The OpenSSL Project Authors. All Rights Reserved. + * + * Licensed under the OpenSSL license (the "License"). You may not use + * this file except in compliance with the License. You can obtain a copy + * in the file LICENSE in the source distribution or at + * https://www.openssl.org/source/license.html + */ + +#ifndef HEADER_CONF_H +# define HEADER_CONF_H + +# include +# include +# include +# include +# include + +# include + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct { + char *section; + char *name; + char *value; +} CONF_VALUE; + +DEFINE_STACK_OF(CONF_VALUE) +DEFINE_LHASH_OF(CONF_VALUE); + +struct conf_st; +struct conf_method_st; +typedef struct conf_method_st CONF_METHOD; + +struct conf_method_st { + const char *name; + CONF *(*create) (CONF_METHOD *meth); + int (*init) (CONF *conf); + int (*destroy) (CONF *conf); + int (*destroy_data) (CONF *conf); + int (*load_bio) (CONF *conf, BIO *bp, long *eline); + int (*dump) (const CONF *conf, BIO *bp); + int (*is_number) (const CONF *conf, char c); + int (*to_int) (const CONF *conf, char c); + int (*load) (CONF *conf, const char *name, long *eline); +}; + +/* Module definitions */ + +typedef struct conf_imodule_st CONF_IMODULE; +typedef struct conf_module_st CONF_MODULE; + +DEFINE_STACK_OF(CONF_MODULE) +DEFINE_STACK_OF(CONF_IMODULE) + +/* DSO module function typedefs */ +typedef int conf_init_func (CONF_IMODULE *md, const CONF *cnf); +typedef void conf_finish_func (CONF_IMODULE *md); + +# define CONF_MFLAGS_IGNORE_ERRORS 0x1 +# define CONF_MFLAGS_IGNORE_RETURN_CODES 0x2 +# define CONF_MFLAGS_SILENT 0x4 +# define CONF_MFLAGS_NO_DSO 0x8 +# define CONF_MFLAGS_IGNORE_MISSING_FILE 0x10 +# define CONF_MFLAGS_DEFAULT_SECTION 0x20 + +int CONF_set_default_method(CONF_METHOD *meth); +void CONF_set_nconf(CONF *conf, LHASH_OF(CONF_VALUE) *hash); +LHASH_OF(CONF_VALUE) *CONF_load(LHASH_OF(CONF_VALUE) *conf, const char *file, + long *eline); +# ifndef OPENSSL_NO_STDIO +LHASH_OF(CONF_VALUE) *CONF_load_fp(LHASH_OF(CONF_VALUE) *conf, FILE *fp, + long *eline); +# endif +LHASH_OF(CONF_VALUE) *CONF_load_bio(LHASH_OF(CONF_VALUE) *conf, BIO *bp, + long *eline); +STACK_OF(CONF_VALUE) *CONF_get_section(LHASH_OF(CONF_VALUE) *conf, + const char *section); +char *CONF_get_string(LHASH_OF(CONF_VALUE) *conf, const char *group, + const char *name); +long CONF_get_number(LHASH_OF(CONF_VALUE) *conf, const char *group, + const char *name); +void CONF_free(LHASH_OF(CONF_VALUE) *conf); +#ifndef OPENSSL_NO_STDIO +int CONF_dump_fp(LHASH_OF(CONF_VALUE) *conf, FILE *out); +#endif +int CONF_dump_bio(LHASH_OF(CONF_VALUE) *conf, BIO *out); + +DEPRECATEDIN_1_1_0(void OPENSSL_config(const char *config_name)) + +#if OPENSSL_API_COMPAT < 0x10100000L +# define OPENSSL_no_config() \ + OPENSSL_init_crypto(OPENSSL_INIT_NO_LOAD_CONFIG, NULL) +#endif + +/* + * New conf code. The semantics are different from the functions above. If + * that wasn't the case, the above functions would have been replaced + */ + +struct conf_st { + CONF_METHOD *meth; + void *meth_data; + LHASH_OF(CONF_VALUE) *data; +}; + +CONF *NCONF_new(CONF_METHOD *meth); +CONF_METHOD *NCONF_default(void); +CONF_METHOD *NCONF_WIN32(void); +void NCONF_free(CONF *conf); +void NCONF_free_data(CONF *conf); + +int NCONF_load(CONF *conf, const char *file, long *eline); +# ifndef OPENSSL_NO_STDIO +int NCONF_load_fp(CONF *conf, FILE *fp, long *eline); +# endif +int NCONF_load_bio(CONF *conf, BIO *bp, long *eline); +STACK_OF(CONF_VALUE) *NCONF_get_section(const CONF *conf, + const char *section); +char *NCONF_get_string(const CONF *conf, const char *group, const char *name); +int NCONF_get_number_e(const CONF *conf, const char *group, const char *name, + long *result); +#ifndef OPENSSL_NO_STDIO +int NCONF_dump_fp(const CONF *conf, FILE *out); +#endif +int NCONF_dump_bio(const CONF *conf, BIO *out); + +#define NCONF_get_number(c,g,n,r) NCONF_get_number_e(c,g,n,r) + +/* Module functions */ + +int CONF_modules_load(const CONF *cnf, const char *appname, + unsigned long flags); +int CONF_modules_load_file(const char *filename, const char *appname, + unsigned long flags); +void CONF_modules_unload(int all); +void CONF_modules_finish(void); +#if OPENSSL_API_COMPAT < 0x10100000L +# define CONF_modules_free() while(0) continue +#endif +int CONF_module_add(const char *name, conf_init_func *ifunc, + conf_finish_func *ffunc); + +const char *CONF_imodule_get_name(const CONF_IMODULE *md); +const char *CONF_imodule_get_value(const CONF_IMODULE *md); +void *CONF_imodule_get_usr_data(const CONF_IMODULE *md); +void CONF_imodule_set_usr_data(CONF_IMODULE *md, void *usr_data); +CONF_MODULE *CONF_imodule_get_module(const CONF_IMODULE *md); +unsigned long CONF_imodule_get_flags(const CONF_IMODULE *md); +void CONF_imodule_set_flags(CONF_IMODULE *md, unsigned long flags); +void *CONF_module_get_usr_data(CONF_MODULE *pmod); +void CONF_module_set_usr_data(CONF_MODULE *pmod, void *usr_data); + +char *CONF_get1_default_config_file(void); + +int CONF_parse_list(const char *list, int sep, int nospc, + int (*list_cb) (const char *elem, int len, void *usr), + void *arg); + +void OPENSSL_load_builtin_modules(void); + +/* BEGIN ERROR CODES */ +/* + * The following lines are auto generated by the script mkerr.pl. Any changes + * made after this point may be overwritten when the script is next run. + */ + +int ERR_load_CONF_strings(void); + +/* Error codes for the CONF functions. */ + +/* Function codes. */ +# define CONF_F_CONF_DUMP_FP 104 +# define CONF_F_CONF_LOAD 100 +# define CONF_F_CONF_LOAD_FP 103 +# define CONF_F_CONF_PARSE_LIST 119 +# define CONF_F_DEF_LOAD 120 +# define CONF_F_DEF_LOAD_BIO 121 +# define CONF_F_MODULE_INIT 115 +# define CONF_F_MODULE_LOAD_DSO 117 +# define CONF_F_MODULE_RUN 118 +# define CONF_F_NCONF_DUMP_BIO 105 +# define CONF_F_NCONF_DUMP_FP 106 +# define CONF_F_NCONF_GET_NUMBER_E 112 +# define CONF_F_NCONF_GET_SECTION 108 +# define CONF_F_NCONF_GET_STRING 109 +# define CONF_F_NCONF_LOAD 113 +# define CONF_F_NCONF_LOAD_BIO 110 +# define CONF_F_NCONF_LOAD_FP 114 +# define CONF_F_NCONF_NEW 111 +# define CONF_F_STR_COPY 101 + +/* Reason codes. */ +# define CONF_R_ERROR_LOADING_DSO 110 +# define CONF_R_LIST_CANNOT_BE_NULL 115 +# define CONF_R_MISSING_CLOSE_SQUARE_BRACKET 100 +# define CONF_R_MISSING_EQUAL_SIGN 101 +# define CONF_R_MISSING_INIT_FUNCTION 112 +# define CONF_R_MODULE_INITIALIZATION_ERROR 109 +# define CONF_R_NO_CLOSE_BRACE 102 +# define CONF_R_NO_CONF 105 +# define CONF_R_NO_CONF_OR_ENVIRONMENT_VARIABLE 106 +# define CONF_R_NO_SECTION 107 +# define CONF_R_NO_SUCH_FILE 114 +# define CONF_R_NO_VALUE 108 +# define CONF_R_UNABLE_TO_CREATE_NEW_SECTION 103 +# define CONF_R_UNKNOWN_MODULE_NAME 113 +# define CONF_R_VARIABLE_HAS_NO_VALUE 104 + +# ifdef __cplusplus +} +# endif +#endif diff --git a/mobile/ios/FxA/FxA/include/openssl/conf_api.h b/mobile/ios/FxA/FxA/include/openssl/conf_api.h new file mode 100644 index 0000000000..a0275ad79b --- /dev/null +++ b/mobile/ios/FxA/FxA/include/openssl/conf_api.h @@ -0,0 +1,40 @@ +/* + * Copyright 1995-2016 The OpenSSL Project Authors. All Rights Reserved. + * + * Licensed under the OpenSSL license (the "License"). You may not use + * this file except in compliance with the License. You can obtain a copy + * in the file LICENSE in the source distribution or at + * https://www.openssl.org/source/license.html + */ + +#ifndef HEADER_CONF_API_H +# define HEADER_CONF_API_H + +# include +# include + +#ifdef __cplusplus +extern "C" { +#endif + +/* Up until OpenSSL 0.9.5a, this was new_section */ +CONF_VALUE *_CONF_new_section(CONF *conf, const char *section); +/* Up until OpenSSL 0.9.5a, this was get_section */ +CONF_VALUE *_CONF_get_section(const CONF *conf, const char *section); +/* Up until OpenSSL 0.9.5a, this was CONF_get_section */ +STACK_OF(CONF_VALUE) *_CONF_get_section_values(const CONF *conf, + const char *section); + +int _CONF_add_string(CONF *conf, CONF_VALUE *section, CONF_VALUE *value); +char *_CONF_get_string(const CONF *conf, const char *section, + const char *name); +long _CONF_get_number(const CONF *conf, const char *section, + const char *name); + +int _CONF_new_data(CONF *conf); +void _CONF_free_data(CONF *conf); + +#ifdef __cplusplus +} +#endif +#endif diff --git a/mobile/ios/FxA/FxA/include/openssl/crypto.h b/mobile/ios/FxA/FxA/include/openssl/crypto.h new file mode 100644 index 0000000000..bd0b140827 --- /dev/null +++ b/mobile/ios/FxA/FxA/include/openssl/crypto.h @@ -0,0 +1,463 @@ +/* + * Copyright 1995-2016 The OpenSSL Project Authors. All Rights Reserved. + * + * Licensed under the OpenSSL license (the "License"). You may not use + * this file except in compliance with the License. You can obtain a copy + * in the file LICENSE in the source distribution or at + * https://www.openssl.org/source/license.html + */ + +/* ==================================================================== + * Copyright 2002 Sun Microsystems, Inc. ALL RIGHTS RESERVED. + * ECDH support in OpenSSL originally developed by + * SUN MICROSYSTEMS, INC., and contributed to the OpenSSL project. + */ + +#ifndef HEADER_CRYPTO_H +# define HEADER_CRYPTO_H + +# include +# include + +# include + +# ifndef OPENSSL_NO_STDIO +# include +# endif + +# include +# include +# include +# include +# include + +# ifdef CHARSET_EBCDIC +# include +# endif + +/* + * Resolve problems on some operating systems with symbol names that clash + * one way or another + */ +# include + +# if OPENSSL_API_COMPAT < 0x10100000L +# include +# endif + +#ifdef __cplusplus +extern "C" { +#endif + +# if OPENSSL_API_COMPAT < 0x10100000L +# define SSLeay OpenSSL_version_num +# define SSLeay_version OpenSSL_version +# define SSLEAY_VERSION_NUMBER OPENSSL_VERSION_NUMBER +# define SSLEAY_VERSION OPENSSL_VERSION +# define SSLEAY_CFLAGS OPENSSL_CFLAGS +# define SSLEAY_BUILT_ON OPENSSL_BUILT_ON +# define SSLEAY_PLATFORM OPENSSL_PLATFORM +# define SSLEAY_DIR OPENSSL_DIR + +/* + * Old type for allocating dynamic locks. No longer used. Use the new thread + * API instead. + */ +typedef struct { + int dummy; +} CRYPTO_dynlock; + +# endif /* OPENSSL_API_COMPAT */ + +typedef void CRYPTO_RWLOCK; + +CRYPTO_RWLOCK *CRYPTO_THREAD_lock_new(void); +int CRYPTO_THREAD_read_lock(CRYPTO_RWLOCK *lock); +int CRYPTO_THREAD_write_lock(CRYPTO_RWLOCK *lock); +int CRYPTO_THREAD_unlock(CRYPTO_RWLOCK *lock); +void CRYPTO_THREAD_lock_free(CRYPTO_RWLOCK *lock); + +int CRYPTO_atomic_add(int *val, int amount, int *ret, CRYPTO_RWLOCK *lock); + +/* + * The following can be used to detect memory leaks in the library. If + * used, it turns on malloc checking + */ +# define CRYPTO_MEM_CHECK_OFF 0x0 /* Control only */ +# define CRYPTO_MEM_CHECK_ON 0x1 /* Control and mode bit */ +# define CRYPTO_MEM_CHECK_ENABLE 0x2 /* Control and mode bit */ +# define CRYPTO_MEM_CHECK_DISABLE 0x3 /* Control only */ + +struct crypto_ex_data_st { + STACK_OF(void) *sk; +}; +DEFINE_STACK_OF(void) + +/* + * Per class, we have a STACK of function pointers. + */ +# define CRYPTO_EX_INDEX_SSL 0 +# define CRYPTO_EX_INDEX_SSL_CTX 1 +# define CRYPTO_EX_INDEX_SSL_SESSION 2 +# define CRYPTO_EX_INDEX_X509 3 +# define CRYPTO_EX_INDEX_X509_STORE 4 +# define CRYPTO_EX_INDEX_X509_STORE_CTX 5 +# define CRYPTO_EX_INDEX_DH 6 +# define CRYPTO_EX_INDEX_DSA 7 +# define CRYPTO_EX_INDEX_EC_KEY 8 +# define CRYPTO_EX_INDEX_RSA 9 +# define CRYPTO_EX_INDEX_ENGINE 10 +# define CRYPTO_EX_INDEX_UI 11 +# define CRYPTO_EX_INDEX_BIO 12 +# define CRYPTO_EX_INDEX_APP 13 +# define CRYPTO_EX_INDEX__COUNT 14 + +/* + * This is the default callbacks, but we can have others as well: this is + * needed in Win32 where the application malloc and the library malloc may + * not be the same. + */ +#define OPENSSL_malloc_init() \ + CRYPTO_set_mem_functions(CRYPTO_malloc, CRYPTO_realloc, CRYPTO_free) + +int CRYPTO_mem_ctrl(int mode); + +# define OPENSSL_malloc(num) \ + CRYPTO_malloc(num, OPENSSL_FILE, OPENSSL_LINE) +# define OPENSSL_zalloc(num) \ + CRYPTO_zalloc(num, OPENSSL_FILE, OPENSSL_LINE) +# define OPENSSL_realloc(addr, num) \ + CRYPTO_realloc(addr, num, OPENSSL_FILE, OPENSSL_LINE) +# define OPENSSL_clear_realloc(addr, old_num, num) \ + CRYPTO_clear_realloc(addr, old_num, num, OPENSSL_FILE, OPENSSL_LINE) +# define OPENSSL_clear_free(addr, num) \ + CRYPTO_clear_free(addr, num, OPENSSL_FILE, OPENSSL_LINE) +# define OPENSSL_free(addr) \ + CRYPTO_free(addr, OPENSSL_FILE, OPENSSL_LINE) +# define OPENSSL_memdup(str, s) \ + CRYPTO_memdup((str), s, OPENSSL_FILE, OPENSSL_LINE) +# define OPENSSL_strdup(str) \ + CRYPTO_strdup(str, OPENSSL_FILE, OPENSSL_LINE) +# define OPENSSL_strndup(str, n) \ + CRYPTO_strndup(str, n, OPENSSL_FILE, OPENSSL_LINE) +# define OPENSSL_secure_malloc(num) \ + CRYPTO_secure_malloc(num, OPENSSL_FILE, OPENSSL_LINE) +# define OPENSSL_secure_zalloc(num) \ + CRYPTO_secure_zalloc(num, OPENSSL_FILE, OPENSSL_LINE) +# define OPENSSL_secure_free(addr) \ + CRYPTO_secure_free(addr, OPENSSL_FILE, OPENSSL_LINE) +# define OPENSSL_secure_actual_size(ptr) \ + CRYPTO_secure_actual_size(ptr) + +size_t OPENSSL_strlcpy(char *dst, const char *src, size_t siz); +size_t OPENSSL_strlcat(char *dst, const char *src, size_t siz); +size_t OPENSSL_strnlen(const char *str, size_t maxlen); +char *OPENSSL_buf2hexstr(const unsigned char *buffer, long len); +unsigned char *OPENSSL_hexstr2buf(const char *str, long *len); +int OPENSSL_hexchar2int(unsigned char c); + +# define OPENSSL_MALLOC_MAX_NELEMS(type) (((1U<<(sizeof(int)*8-1))-1)/sizeof(type)) + +unsigned long OpenSSL_version_num(void); +const char *OpenSSL_version(int type); +# define OPENSSL_VERSION 0 +# define OPENSSL_CFLAGS 1 +# define OPENSSL_BUILT_ON 2 +# define OPENSSL_PLATFORM 3 +# define OPENSSL_DIR 4 +# define OPENSSL_ENGINES_DIR 5 + +int OPENSSL_issetugid(void); + +typedef void CRYPTO_EX_new (void *parent, void *ptr, CRYPTO_EX_DATA *ad, + int idx, long argl, void *argp); +typedef void CRYPTO_EX_free (void *parent, void *ptr, CRYPTO_EX_DATA *ad, + int idx, long argl, void *argp); +typedef int CRYPTO_EX_dup (CRYPTO_EX_DATA *to, const CRYPTO_EX_DATA *from, + void *srcp, int idx, long argl, void *argp); +__owur int CRYPTO_get_ex_new_index(int class_index, long argl, void *argp, + CRYPTO_EX_new *new_func, CRYPTO_EX_dup *dup_func, + CRYPTO_EX_free *free_func); +/* No longer use an index. */ +int CRYPTO_free_ex_index(int class_index, int idx); + +/* + * Initialise/duplicate/free CRYPTO_EX_DATA variables corresponding to a + * given class (invokes whatever per-class callbacks are applicable) + */ +int CRYPTO_new_ex_data(int class_index, void *obj, CRYPTO_EX_DATA *ad); +int CRYPTO_dup_ex_data(int class_index, CRYPTO_EX_DATA *to, + const CRYPTO_EX_DATA *from); + +void CRYPTO_free_ex_data(int class_index, void *obj, CRYPTO_EX_DATA *ad); + +/* + * Get/set data in a CRYPTO_EX_DATA variable corresponding to a particular + * index (relative to the class type involved) + */ +int CRYPTO_set_ex_data(CRYPTO_EX_DATA *ad, int idx, void *val); +void *CRYPTO_get_ex_data(const CRYPTO_EX_DATA *ad, int idx); + +# if OPENSSL_API_COMPAT < 0x10100000L +/* + * This function cleans up all "ex_data" state. It mustn't be called under + * potential race-conditions. + */ +# define CRYPTO_cleanup_all_ex_data() while(0) continue + +/* + * The old locking functions have been removed completely without compatibility + * macros. This is because the old functions either could not properly report + * errors, or the returned error values were not clearly documented. + * Replacing the locking functions with with no-ops would cause race condition + * issues in the affected applications. It is far better for them to fail at + * compile time. + * On the other hand, the locking callbacks are no longer used. Consequently, + * the callback management functions can be safely replaced with no-op macros. + */ +# define CRYPTO_num_locks() (1) +# define CRYPTO_set_locking_callback(func) +# define CRYPTO_get_locking_callback() (NULL) +# define CRYPTO_set_add_lock_callback(func) +# define CRYPTO_get_add_lock_callback() (NULL) + +/* + * These defines where used in combination with the old locking callbacks, + * they are not called anymore, but old code that's not called might still + * use them. + */ +# define CRYPTO_LOCK 1 +# define CRYPTO_UNLOCK 2 +# define CRYPTO_READ 4 +# define CRYPTO_WRITE 8 + +/* This structure is no longer used */ +typedef struct crypto_threadid_st { + int dummy; +} CRYPTO_THREADID; +/* Only use CRYPTO_THREADID_set_[numeric|pointer]() within callbacks */ +# define CRYPTO_THREADID_set_numeric(id, val) +# define CRYPTO_THREADID_set_pointer(id, ptr) +# define CRYPTO_THREADID_set_callback(threadid_func) (0) +# define CRYPTO_THREADID_get_callback() (NULL) +# define CRYPTO_THREADID_current(id) +# define CRYPTO_THREADID_cmp(a, b) (-1) +# define CRYPTO_THREADID_cpy(dest, src) +# define CRYPTO_THREADID_hash(id) (0UL) + +# if OPENSSL_API_COMPAT < 0x10000000L +# define CRYPTO_set_id_callback(func) +# define CRYPTO_get_id_callback() (NULL) +# define CRYPTO_thread_id() (0UL) +# endif /* OPENSSL_API_COMPAT < 0x10000000L */ + +# define CRYPTO_set_dynlock_create_callback(dyn_create_function) +# define CRYPTO_set_dynlock_lock_callback(dyn_lock_function) +# define CRYPTO_set_dynlock_destroy_callback(dyn_destroy_function) +# define CRYPTO_get_dynlock_create_callback() (NULL) +# define CRYPTO_get_dynlock_lock_callback() (NULL) +# define CRYPTO_get_dynlock_destroy_callback() (NULL) +# endif /* OPENSSL_API_COMPAT < 0x10100000L */ + +int CRYPTO_set_mem_functions( + void *(*m) (size_t, const char *, int), + void *(*r) (void *, size_t, const char *, int), + void (*f) (void *, const char *, int)); +int CRYPTO_set_mem_debug(int flag); +void CRYPTO_get_mem_functions( + void *(**m) (size_t, const char *, int), + void *(**r) (void *, size_t, const char *, int), + void (**f) (void *, const char *, int)); + +void *CRYPTO_malloc(size_t num, const char *file, int line); +void *CRYPTO_zalloc(size_t num, const char *file, int line); +void *CRYPTO_memdup(const void *str, size_t siz, const char *file, int line); +char *CRYPTO_strdup(const char *str, const char *file, int line); +char *CRYPTO_strndup(const char *str, size_t s, const char *file, int line); +void CRYPTO_free(void *ptr, const char *file, int line); +void CRYPTO_clear_free(void *ptr, size_t num, const char *file, int line); +void *CRYPTO_realloc(void *addr, size_t num, const char *file, int line); +void *CRYPTO_clear_realloc(void *addr, size_t old_num, size_t num, + const char *file, int line); + +int CRYPTO_secure_malloc_init(size_t sz, int minsize); +int CRYPTO_secure_malloc_done(void); +void *CRYPTO_secure_malloc(size_t num, const char *file, int line); +void *CRYPTO_secure_zalloc(size_t num, const char *file, int line); +void CRYPTO_secure_free(void *ptr, const char *file, int line); +int CRYPTO_secure_allocated(const void *ptr); +int CRYPTO_secure_malloc_initialized(void); +size_t CRYPTO_secure_actual_size(void *ptr); +size_t CRYPTO_secure_used(void); + +void OPENSSL_cleanse(void *ptr, size_t len); + +# ifndef OPENSSL_NO_CRYPTO_MDEBUG +# define OPENSSL_mem_debug_push(info) \ + CRYPTO_mem_debug_push(info, OPENSSL_FILE, OPENSSL_LINE) +# define OPENSSL_mem_debug_pop() \ + CRYPTO_mem_debug_pop() +int CRYPTO_mem_debug_push(const char *info, const char *file, int line); +int CRYPTO_mem_debug_pop(void); + +/*- + * Debugging functions (enabled by CRYPTO_set_mem_debug(1)) + * The flag argument has the following significance: + * 0: called before the actual memory allocation has taken place + * 1: called after the actual memory allocation has taken place + */ +void CRYPTO_mem_debug_malloc(void *addr, size_t num, int flag, + const char *file, int line); +void CRYPTO_mem_debug_realloc(void *addr1, void *addr2, size_t num, int flag, + const char *file, int line); +void CRYPTO_mem_debug_free(void *addr, int flag, + const char *file, int line); + +# ifndef OPENSSL_NO_STDIO +int CRYPTO_mem_leaks_fp(FILE *); +# endif +int CRYPTO_mem_leaks(BIO *bio); +# endif + +/* die if we have to */ +ossl_noreturn void OPENSSL_die(const char *assertion, const char *file, int line); +# if OPENSSL_API_COMPAT < 0x10100000L +# define OpenSSLDie(f,l,a) OPENSSL_die((a),(f),(l)) +# endif +# define OPENSSL_assert(e) \ + (void)((e) ? 0 : (OPENSSL_die("assertion failed: " #e, OPENSSL_FILE, OPENSSL_LINE), 1)) + +int OPENSSL_isservice(void); + +int FIPS_mode(void); +int FIPS_mode_set(int r); + +void OPENSSL_init(void); + +struct tm *OPENSSL_gmtime(const time_t *timer, struct tm *result); +int OPENSSL_gmtime_adj(struct tm *tm, int offset_day, long offset_sec); +int OPENSSL_gmtime_diff(int *pday, int *psec, + const struct tm *from, const struct tm *to); + +/* + * CRYPTO_memcmp returns zero iff the |len| bytes at |a| and |b| are equal. + * It takes an amount of time dependent on |len|, but independent of the + * contents of |a| and |b|. Unlike memcmp, it cannot be used to put elements + * into a defined order as the return value when a != b is undefined, other + * than to be non-zero. + */ +int CRYPTO_memcmp(const volatile void * volatile in_a, + const volatile void * volatile in_b, + size_t len); + +/* Standard initialisation options */ +# define OPENSSL_INIT_NO_LOAD_CRYPTO_STRINGS 0x00000001L +# define OPENSSL_INIT_LOAD_CRYPTO_STRINGS 0x00000002L +# define OPENSSL_INIT_ADD_ALL_CIPHERS 0x00000004L +# define OPENSSL_INIT_ADD_ALL_DIGESTS 0x00000008L +# define OPENSSL_INIT_NO_ADD_ALL_CIPHERS 0x00000010L +# define OPENSSL_INIT_NO_ADD_ALL_DIGESTS 0x00000020L +# define OPENSSL_INIT_LOAD_CONFIG 0x00000040L +# define OPENSSL_INIT_NO_LOAD_CONFIG 0x00000080L +# define OPENSSL_INIT_ASYNC 0x00000100L +# define OPENSSL_INIT_ENGINE_RDRAND 0x00000200L +# define OPENSSL_INIT_ENGINE_DYNAMIC 0x00000400L +# define OPENSSL_INIT_ENGINE_OPENSSL 0x00000800L +# define OPENSSL_INIT_ENGINE_CRYPTODEV 0x00001000L +# define OPENSSL_INIT_ENGINE_CAPI 0x00002000L +# define OPENSSL_INIT_ENGINE_PADLOCK 0x00004000L +# define OPENSSL_INIT_ENGINE_AFALG 0x00008000L +/* OPENSSL_INIT flag 0x00010000 reserved for internal use */ +/* OPENSSL_INIT flag range 0xfff00000 reserved for OPENSSL_init_ssl() */ +/* Max OPENSSL_INIT flag value is 0x80000000 */ + +/* openssl and dasync not counted as builtin */ +# define OPENSSL_INIT_ENGINE_ALL_BUILTIN \ + (OPENSSL_INIT_ENGINE_RDRAND | OPENSSL_INIT_ENGINE_DYNAMIC \ + | OPENSSL_INIT_ENGINE_CRYPTODEV | OPENSSL_INIT_ENGINE_CAPI | \ + OPENSSL_INIT_ENGINE_PADLOCK) + + +/* Library initialisation functions */ +void OPENSSL_cleanup(void); +int OPENSSL_init_crypto(uint64_t opts, const OPENSSL_INIT_SETTINGS *settings); +int OPENSSL_atexit(void (*handler)(void)); +void OPENSSL_thread_stop(void); + +/* Low-level control of initialization */ +OPENSSL_INIT_SETTINGS *OPENSSL_INIT_new(void); +# ifndef OPENSSL_NO_STDIO +int OPENSSL_INIT_set_config_appname(OPENSSL_INIT_SETTINGS *settings, + const char *config_file); +# endif +void OPENSSL_INIT_free(OPENSSL_INIT_SETTINGS *settings); + +# if defined(OPENSSL_THREADS) && !defined(CRYPTO_TDEBUG) +# if defined(_WIN32) +# if defined(BASETYPES) || defined(_WINDEF_H) +/* application has to include in order to use this */ +typedef DWORD CRYPTO_THREAD_LOCAL; +typedef DWORD CRYPTO_THREAD_ID; + +typedef LONG CRYPTO_ONCE; +# define CRYPTO_ONCE_STATIC_INIT 0 +# endif +# else +# include +typedef pthread_once_t CRYPTO_ONCE; +typedef pthread_key_t CRYPTO_THREAD_LOCAL; +typedef pthread_t CRYPTO_THREAD_ID; + +# define CRYPTO_ONCE_STATIC_INIT PTHREAD_ONCE_INIT +# endif +# endif + +# if !defined(CRYPTO_ONCE_STATIC_INIT) +typedef unsigned int CRYPTO_ONCE; +typedef unsigned int CRYPTO_THREAD_LOCAL; +typedef unsigned int CRYPTO_THREAD_ID; +# define CRYPTO_ONCE_STATIC_INIT 0 +# endif + +int CRYPTO_THREAD_run_once(CRYPTO_ONCE *once, void (*init)(void)); + +int CRYPTO_THREAD_init_local(CRYPTO_THREAD_LOCAL *key, void (*cleanup)(void *)); +void *CRYPTO_THREAD_get_local(CRYPTO_THREAD_LOCAL *key); +int CRYPTO_THREAD_set_local(CRYPTO_THREAD_LOCAL *key, void *val); +int CRYPTO_THREAD_cleanup_local(CRYPTO_THREAD_LOCAL *key); + +CRYPTO_THREAD_ID CRYPTO_THREAD_get_current_id(void); +int CRYPTO_THREAD_compare_id(CRYPTO_THREAD_ID a, CRYPTO_THREAD_ID b); + +/* BEGIN ERROR CODES */ +/* + * The following lines are auto generated by the script mkerr.pl. Any changes + * made after this point may be overwritten when the script is next run. + */ + +int ERR_load_CRYPTO_strings(void); + +/* Error codes for the CRYPTO functions. */ + +/* Function codes. */ +# define CRYPTO_F_CRYPTO_DUP_EX_DATA 110 +# define CRYPTO_F_CRYPTO_FREE_EX_DATA 111 +# define CRYPTO_F_CRYPTO_GET_EX_NEW_INDEX 100 +# define CRYPTO_F_CRYPTO_MEMDUP 115 +# define CRYPTO_F_CRYPTO_NEW_EX_DATA 112 +# define CRYPTO_F_CRYPTO_SET_EX_DATA 102 +# define CRYPTO_F_FIPS_MODE_SET 109 +# define CRYPTO_F_GET_AND_LOCK 113 +# define CRYPTO_F_OPENSSL_BUF2HEXSTR 117 +# define CRYPTO_F_OPENSSL_HEXSTR2BUF 118 +# define CRYPTO_F_OPENSSL_INIT_CRYPTO 116 + +/* Reason codes. */ +# define CRYPTO_R_FIPS_MODE_NOT_SUPPORTED 101 +# define CRYPTO_R_ILLEGAL_HEX_DIGIT 102 +# define CRYPTO_R_ODD_NUMBER_OF_DIGITS 103 + +# ifdef __cplusplus +} +# endif +#endif diff --git a/mobile/ios/FxA/FxA/include/openssl/ct.h b/mobile/ios/FxA/FxA/include/openssl/ct.h new file mode 100644 index 0000000000..bf29fbabe0 --- /dev/null +++ b/mobile/ios/FxA/FxA/include/openssl/ct.h @@ -0,0 +1,533 @@ +/* + * Copyright 2016 The OpenSSL Project Authors. All Rights Reserved. + * + * Licensed under the OpenSSL license (the "License"). You may not use + * this file except in compliance with the License. You can obtain a copy + * in the file LICENSE in the source distribution or at + * https://www.openssl.org/source/license.html + */ + +#ifndef HEADER_CT_H +# define HEADER_CT_H + +# include + +# ifndef OPENSSL_NO_CT +# include +# include +# include +# ifdef __cplusplus +extern "C" { +# endif + + +/* Minimum RSA key size, from RFC6962 */ +# define SCT_MIN_RSA_BITS 2048 + +/* All hashes are SHA256 in v1 of Certificate Transparency */ +# define CT_V1_HASHLEN SHA256_DIGEST_LENGTH + +typedef enum { + CT_LOG_ENTRY_TYPE_NOT_SET = -1, + CT_LOG_ENTRY_TYPE_X509 = 0, + CT_LOG_ENTRY_TYPE_PRECERT = 1 +} ct_log_entry_type_t; + +typedef enum { + SCT_VERSION_NOT_SET = -1, + SCT_VERSION_V1 = 0 +} sct_version_t; + +typedef enum { + SCT_SOURCE_UNKNOWN, + SCT_SOURCE_TLS_EXTENSION, + SCT_SOURCE_X509V3_EXTENSION, + SCT_SOURCE_OCSP_STAPLED_RESPONSE +} sct_source_t; + +typedef enum { + SCT_VALIDATION_STATUS_NOT_SET, + SCT_VALIDATION_STATUS_UNKNOWN_LOG, + SCT_VALIDATION_STATUS_VALID, + SCT_VALIDATION_STATUS_INVALID, + SCT_VALIDATION_STATUS_UNVERIFIED, + SCT_VALIDATION_STATUS_UNKNOWN_VERSION +} sct_validation_status_t; + +DEFINE_STACK_OF(SCT) +DEFINE_STACK_OF(CTLOG) + +/****************************************** + * CT policy evaluation context functions * + ******************************************/ + +/* + * Creates a new, empty policy evaluation context. + * The caller is responsible for calling CT_POLICY_EVAL_CTX_free when finished + * with the CT_POLICY_EVAL_CTX. + */ +CT_POLICY_EVAL_CTX *CT_POLICY_EVAL_CTX_new(void); + +/* Deletes a policy evaluation context and anything it owns. */ +void CT_POLICY_EVAL_CTX_free(CT_POLICY_EVAL_CTX *ctx); + +/* Gets the peer certificate that the SCTs are for */ +X509* CT_POLICY_EVAL_CTX_get0_cert(const CT_POLICY_EVAL_CTX *ctx); + +/* + * Sets the certificate associated with the received SCTs. + * Increments the reference count of cert. + * Returns 1 on success, 0 otherwise. + */ +int CT_POLICY_EVAL_CTX_set1_cert(CT_POLICY_EVAL_CTX *ctx, X509 *cert); + +/* Gets the issuer of the aforementioned certificate */ +X509* CT_POLICY_EVAL_CTX_get0_issuer(const CT_POLICY_EVAL_CTX *ctx); + +/* + * Sets the issuer of the certificate associated with the received SCTs. + * Increments the reference count of issuer. + * Returns 1 on success, 0 otherwise. + */ +int CT_POLICY_EVAL_CTX_set1_issuer(CT_POLICY_EVAL_CTX *ctx, X509 *issuer); + +/* Gets the CT logs that are trusted sources of SCTs */ +const CTLOG_STORE *CT_POLICY_EVAL_CTX_get0_log_store(const CT_POLICY_EVAL_CTX *ctx); + +/* Sets the log store that is in use. It must outlive the CT_POLICY_EVAL_CTX. */ +void CT_POLICY_EVAL_CTX_set_shared_CTLOG_STORE(CT_POLICY_EVAL_CTX *ctx, + CTLOG_STORE *log_store); + +/* + * Gets the time, in milliseconds since the Unix epoch, that will be used as the + * current time when checking whether an SCT was issued in the future. + * Such SCTs will fail validation, as required by RFC6962. + */ +uint64_t CT_POLICY_EVAL_CTX_get_time(const CT_POLICY_EVAL_CTX *ctx); + +/* + * Sets the time to evaluate SCTs against, in milliseconds since the Unix epoch. + * If an SCT's timestamp is after this time, it will be interpreted as having + * been issued in the future. RFC6962 states that "TLS clients MUST reject SCTs + * whose timestamp is in the future", so an SCT will not validate in this case. + */ +void CT_POLICY_EVAL_CTX_set_time(CT_POLICY_EVAL_CTX *ctx, uint64_t time_in_ms); + +/***************** + * SCT functions * + *****************/ + +/* + * Creates a new, blank SCT. + * The caller is responsible for calling SCT_free when finished with the SCT. + */ +SCT *SCT_new(void); + +/* + * Creates a new SCT from some base64-encoded strings. + * The caller is responsible for calling SCT_free when finished with the SCT. + */ +SCT *SCT_new_from_base64(unsigned char version, + const char *logid_base64, + ct_log_entry_type_t entry_type, + uint64_t timestamp, + const char *extensions_base64, + const char *signature_base64); + +/* + * Frees the SCT and the underlying data structures. + */ +void SCT_free(SCT *sct); + +/* + * Free a stack of SCTs, and the underlying SCTs themselves. + * Intended to be compatible with X509V3_EXT_FREE. + */ +void SCT_LIST_free(STACK_OF(SCT) *a); + +/* + * Returns the version of the SCT. + */ +sct_version_t SCT_get_version(const SCT *sct); + +/* + * Set the version of an SCT. + * Returns 1 on success, 0 if the version is unrecognized. + */ +__owur int SCT_set_version(SCT *sct, sct_version_t version); + +/* + * Returns the log entry type of the SCT. + */ +ct_log_entry_type_t SCT_get_log_entry_type(const SCT *sct); + +/* + * Set the log entry type of an SCT. + * Returns 1 on success, 0 otherwise. + */ +__owur int SCT_set_log_entry_type(SCT *sct, ct_log_entry_type_t entry_type); + +/* + * Gets the ID of the log that an SCT came from. + * Ownership of the log ID remains with the SCT. + * Returns the length of the log ID. + */ +size_t SCT_get0_log_id(const SCT *sct, unsigned char **log_id); + +/* + * Set the log ID of an SCT to point directly to the *log_id specified. + * The SCT takes ownership of the specified pointer. + * Returns 1 on success, 0 otherwise. + */ +__owur int SCT_set0_log_id(SCT *sct, unsigned char *log_id, size_t log_id_len); + +/* + * Set the log ID of an SCT. + * This makes a copy of the log_id. + * Returns 1 on success, 0 otherwise. + */ +__owur int SCT_set1_log_id(SCT *sct, const unsigned char *log_id, + size_t log_id_len); + +/* + * Returns the timestamp for the SCT (epoch time in milliseconds). + */ +uint64_t SCT_get_timestamp(const SCT *sct); + +/* + * Set the timestamp of an SCT (epoch time in milliseconds). + */ +void SCT_set_timestamp(SCT *sct, uint64_t timestamp); + +/* + * Return the NID for the signature used by the SCT. + * For CT v1, this will be either NID_sha256WithRSAEncryption or + * NID_ecdsa_with_SHA256 (or NID_undef if incorrect/unset). + */ +int SCT_get_signature_nid(const SCT *sct); + +/* + * Set the signature type of an SCT + * For CT v1, this should be either NID_sha256WithRSAEncryption or + * NID_ecdsa_with_SHA256. + * Returns 1 on success, 0 otherwise. + */ +__owur int SCT_set_signature_nid(SCT *sct, int nid); + +/* + * Set *ext to point to the extension data for the SCT. ext must not be NULL. + * The SCT retains ownership of this pointer. + * Returns length of the data pointed to. + */ +size_t SCT_get0_extensions(const SCT *sct, unsigned char **ext); + +/* + * Set the extensions of an SCT to point directly to the *ext specified. + * The SCT takes ownership of the specified pointer. + */ +void SCT_set0_extensions(SCT *sct, unsigned char *ext, size_t ext_len); + +/* + * Set the extensions of an SCT. + * This takes a copy of the ext. + * Returns 1 on success, 0 otherwise. + */ +__owur int SCT_set1_extensions(SCT *sct, const unsigned char *ext, + size_t ext_len); + +/* + * Set *sig to point to the signature for the SCT. sig must not be NULL. + * The SCT retains ownership of this pointer. + * Returns length of the data pointed to. + */ +size_t SCT_get0_signature(const SCT *sct, unsigned char **sig); + +/* + * Set the signature of an SCT to point directly to the *sig specified. + * The SCT takes ownership of the specified pointer. + */ +void SCT_set0_signature(SCT *sct, unsigned char *sig, size_t sig_len); + +/* + * Set the signature of an SCT to be a copy of the *sig specified. + * Returns 1 on success, 0 otherwise. + */ +__owur int SCT_set1_signature(SCT *sct, const unsigned char *sig, + size_t sig_len); + +/* + * The origin of this SCT, e.g. TLS extension, OCSP response, etc. + */ +sct_source_t SCT_get_source(const SCT *sct); + +/* + * Set the origin of this SCT, e.g. TLS extension, OCSP response, etc. + * Returns 1 on success, 0 otherwise. + */ +__owur int SCT_set_source(SCT *sct, sct_source_t source); + +/* + * Returns a text string describing the validation status of |sct|. + */ +const char *SCT_validation_status_string(const SCT *sct); + +/* + * Pretty-prints an |sct| to |out|. + * It will be indented by the number of spaces specified by |indent|. + * If |logs| is not NULL, it will be used to lookup the CT log that the SCT came + * from, so that the log name can be printed. + */ +void SCT_print(const SCT *sct, BIO *out, int indent, const CTLOG_STORE *logs); + +/* + * Pretty-prints an |sct_list| to |out|. + * It will be indented by the number of spaces specified by |indent|. + * SCTs will be delimited by |separator|. + * If |logs| is not NULL, it will be used to lookup the CT log that each SCT + * came from, so that the log names can be printed. + */ +void SCT_LIST_print(const STACK_OF(SCT) *sct_list, BIO *out, int indent, + const char *separator, const CTLOG_STORE *logs); + +/* + * Gets the last result of validating this SCT. + * If it has not been validated yet, returns SCT_VALIDATION_STATUS_NOT_SET. + */ +sct_validation_status_t SCT_get_validation_status(const SCT *sct); + +/* + * Validates the given SCT with the provided context. + * Sets the "validation_status" field of the SCT. + * Returns 1 if the SCT is valid and the signature verifies. + * Returns 0 if the SCT is invalid or could not be verified. + * Returns -1 if an error occurs. + */ +__owur int SCT_validate(SCT *sct, const CT_POLICY_EVAL_CTX *ctx); + +/* + * Validates the given list of SCTs with the provided context. + * Sets the "validation_status" field of each SCT. + * Returns 1 if there are no invalid SCTs and all signatures verify. + * Returns 0 if at least one SCT is invalid or could not be verified. + * Returns a negative integer if an error occurs. + */ +__owur int SCT_LIST_validate(const STACK_OF(SCT) *scts, + CT_POLICY_EVAL_CTX *ctx); + + +/********************************* + * SCT parsing and serialisation * + *********************************/ + +/* + * Serialize (to TLS format) a stack of SCTs and return the length. + * "a" must not be NULL. + * If "pp" is NULL, just return the length of what would have been serialized. + * If "pp" is not NULL and "*pp" is null, function will allocate a new pointer + * for data that caller is responsible for freeing (only if function returns + * successfully). + * If "pp" is NULL and "*pp" is not NULL, caller is responsible for ensuring + * that "*pp" is large enough to accept all of the serialized data. + * Returns < 0 on error, >= 0 indicating bytes written (or would have been) + * on success. + */ +__owur int i2o_SCT_LIST(const STACK_OF(SCT) *a, unsigned char **pp); + +/* + * Convert TLS format SCT list to a stack of SCTs. + * If "a" or "*a" is NULL, a new stack will be created that the caller is + * responsible for freeing (by calling SCT_LIST_free). + * "**pp" and "*pp" must not be NULL. + * Upon success, "*pp" will point to after the last bytes read, and a stack + * will be returned. + * Upon failure, a NULL pointer will be returned, and the position of "*pp" is + * not defined. + */ +STACK_OF(SCT) *o2i_SCT_LIST(STACK_OF(SCT) **a, const unsigned char **pp, + size_t len); + +/* + * Serialize (to DER format) a stack of SCTs and return the length. + * "a" must not be NULL. + * If "pp" is NULL, just returns the length of what would have been serialized. + * If "pp" is not NULL and "*pp" is null, function will allocate a new pointer + * for data that caller is responsible for freeing (only if function returns + * successfully). + * If "pp" is NULL and "*pp" is not NULL, caller is responsible for ensuring + * that "*pp" is large enough to accept all of the serialized data. + * Returns < 0 on error, >= 0 indicating bytes written (or would have been) + * on success. + */ +__owur int i2d_SCT_LIST(const STACK_OF(SCT) *a, unsigned char **pp); + +/* + * Parses an SCT list in DER format and returns it. + * If "a" or "*a" is NULL, a new stack will be created that the caller is + * responsible for freeing (by calling SCT_LIST_free). + * "**pp" and "*pp" must not be NULL. + * Upon success, "*pp" will point to after the last bytes read, and a stack + * will be returned. + * Upon failure, a NULL pointer will be returned, and the position of "*pp" is + * not defined. + */ +STACK_OF(SCT) *d2i_SCT_LIST(STACK_OF(SCT) **a, const unsigned char **pp, + long len); + +/* + * Serialize (to TLS format) an |sct| and write it to |out|. + * If |out| is null, no SCT will be output but the length will still be returned. + * If |out| points to a null pointer, a string will be allocated to hold the + * TLS-format SCT. It is the responsibility of the caller to free it. + * If |out| points to an allocated string, the TLS-format SCT will be written + * to it. + * The length of the SCT in TLS format will be returned. + */ +__owur int i2o_SCT(const SCT *sct, unsigned char **out); + +/* + * Parses an SCT in TLS format and returns it. + * If |psct| is not null, it will end up pointing to the parsed SCT. If it + * already points to a non-null pointer, the pointer will be free'd. + * |in| should be a pointer to a string containing the TLS-format SCT. + * |in| will be advanced to the end of the SCT if parsing succeeds. + * |len| should be the length of the SCT in |in|. + * Returns NULL if an error occurs. + * If the SCT is an unsupported version, only the SCT's 'sct' and 'sct_len' + * fields will be populated (with |in| and |len| respectively). + */ +SCT *o2i_SCT(SCT **psct, const unsigned char **in, size_t len); + +/******************** + * CT log functions * + ********************/ + +/* + * Creates a new CT log instance with the given |public_key| and |name|. + * Takes ownership of |public_key| but copies |name|. + * Returns NULL if malloc fails or if |public_key| cannot be converted to DER. + * Should be deleted by the caller using CTLOG_free when no longer needed. + */ +CTLOG *CTLOG_new(EVP_PKEY *public_key, const char *name); + +/* + * Creates a new CTLOG instance with the base64-encoded SubjectPublicKeyInfo DER + * in |pkey_base64|. The |name| is a string to help users identify this log. + * Returns 1 on success, 0 on failure. + * Should be deleted by the caller using CTLOG_free when no longer needed. + */ +int CTLOG_new_from_base64(CTLOG ** ct_log, + const char *pkey_base64, const char *name); + +/* + * Deletes a CT log instance and its fields. + */ +void CTLOG_free(CTLOG *log); + +/* Gets the name of the CT log */ +const char *CTLOG_get0_name(const CTLOG *log); +/* Gets the ID of the CT log */ +void CTLOG_get0_log_id(const CTLOG *log, const uint8_t **log_id, + size_t *log_id_len); +/* Gets the public key of the CT log */ +EVP_PKEY *CTLOG_get0_public_key(const CTLOG *log); + +/************************** + * CT log store functions * + **************************/ + +/* + * Creates a new CT log store. + * Should be deleted by the caller using CTLOG_STORE_free when no longer needed. + */ +CTLOG_STORE *CTLOG_STORE_new(void); + +/* + * Deletes a CT log store and all of the CT log instances held within. + */ +void CTLOG_STORE_free(CTLOG_STORE *store); + +/* + * Finds a CT log in the store based on its log ID. + * Returns the CT log, or NULL if no match is found. + */ +const CTLOG *CTLOG_STORE_get0_log_by_id(const CTLOG_STORE *store, + const uint8_t *log_id, + size_t log_id_len); + +/* + * Loads a CT log list into a |store| from a |file|. + * Returns 1 if loading is successful, or 0 otherwise. + */ +__owur int CTLOG_STORE_load_file(CTLOG_STORE *store, const char *file); + +/* + * Loads the default CT log list into a |store|. + * See internal/cryptlib.h for the environment variable and file path that are + * consulted to find the default file. + * Returns 1 if loading is successful, or 0 otherwise. + */ +__owur int CTLOG_STORE_load_default_file(CTLOG_STORE *store); + +/* BEGIN ERROR CODES */ +/* + * The following lines are auto generated by the script mkerr.pl. Any changes + * made after this point may be overwritten when the script is next run. + */ + +int ERR_load_CT_strings(void); + +/* Error codes for the CT functions. */ + +/* Function codes. */ +# define CT_F_CTLOG_NEW 117 +# define CT_F_CTLOG_NEW_FROM_BASE64 118 +# define CT_F_CTLOG_NEW_FROM_CONF 119 +# define CT_F_CTLOG_STORE_LOAD_CTX_NEW 122 +# define CT_F_CTLOG_STORE_LOAD_FILE 123 +# define CT_F_CTLOG_STORE_LOAD_LOG 130 +# define CT_F_CTLOG_STORE_NEW 131 +# define CT_F_CT_BASE64_DECODE 124 +# define CT_F_CT_POLICY_EVAL_CTX_NEW 133 +# define CT_F_CT_V1_LOG_ID_FROM_PKEY 125 +# define CT_F_I2O_SCT 107 +# define CT_F_I2O_SCT_LIST 108 +# define CT_F_I2O_SCT_SIGNATURE 109 +# define CT_F_O2I_SCT 110 +# define CT_F_O2I_SCT_LIST 111 +# define CT_F_O2I_SCT_SIGNATURE 112 +# define CT_F_SCT_CTX_NEW 126 +# define CT_F_SCT_CTX_VERIFY 128 +# define CT_F_SCT_NEW 100 +# define CT_F_SCT_NEW_FROM_BASE64 127 +# define CT_F_SCT_SET0_LOG_ID 101 +# define CT_F_SCT_SET1_EXTENSIONS 114 +# define CT_F_SCT_SET1_LOG_ID 115 +# define CT_F_SCT_SET1_SIGNATURE 116 +# define CT_F_SCT_SET_LOG_ENTRY_TYPE 102 +# define CT_F_SCT_SET_SIGNATURE_NID 103 +# define CT_F_SCT_SET_VERSION 104 + +/* Reason codes. */ +# define CT_R_BASE64_DECODE_ERROR 108 +# define CT_R_INVALID_LOG_ID_LENGTH 100 +# define CT_R_LOG_CONF_INVALID 109 +# define CT_R_LOG_CONF_INVALID_KEY 110 +# define CT_R_LOG_CONF_MISSING_DESCRIPTION 111 +# define CT_R_LOG_CONF_MISSING_KEY 112 +# define CT_R_LOG_KEY_INVALID 113 +# define CT_R_SCT_FUTURE_TIMESTAMP 116 +# define CT_R_SCT_INVALID 104 +# define CT_R_SCT_INVALID_SIGNATURE 107 +# define CT_R_SCT_LIST_INVALID 105 +# define CT_R_SCT_LOG_ID_MISMATCH 114 +# define CT_R_SCT_NOT_SET 106 +# define CT_R_SCT_UNSUPPORTED_VERSION 115 +# define CT_R_UNRECOGNIZED_SIGNATURE_NID 101 +# define CT_R_UNSUPPORTED_ENTRY_TYPE 102 +# define CT_R_UNSUPPORTED_VERSION 103 + +# ifdef __cplusplus +} +# endif +# endif +#endif diff --git a/mobile/ios/FxA/FxA/include/openssl/des.h b/mobile/ios/FxA/FxA/include/openssl/des.h new file mode 100644 index 0000000000..be4abbdfd0 --- /dev/null +++ b/mobile/ios/FxA/FxA/include/openssl/des.h @@ -0,0 +1,174 @@ +/* + * Copyright 1995-2016 The OpenSSL Project Authors. All Rights Reserved. + * + * Licensed under the OpenSSL license (the "License"). You may not use + * this file except in compliance with the License. You can obtain a copy + * in the file LICENSE in the source distribution or at + * https://www.openssl.org/source/license.html + */ + +#ifndef HEADER_DES_H +# define HEADER_DES_H + +# include + +# ifndef OPENSSL_NO_DES +# ifdef __cplusplus +extern "C" { +# endif +# include + +typedef unsigned int DES_LONG; + +# ifdef OPENSSL_BUILD_SHLIBCRYPTO +# undef OPENSSL_EXTERN +# define OPENSSL_EXTERN OPENSSL_EXPORT +# endif + +typedef unsigned char DES_cblock[8]; +typedef /* const */ unsigned char const_DES_cblock[8]; +/* + * With "const", gcc 2.8.1 on Solaris thinks that DES_cblock * and + * const_DES_cblock * are incompatible pointer types. + */ + +typedef struct DES_ks { + union { + DES_cblock cblock; + /* + * make sure things are correct size on machines with 8 byte longs + */ + DES_LONG deslong[2]; + } ks[16]; +} DES_key_schedule; + +# define DES_KEY_SZ (sizeof(DES_cblock)) +# define DES_SCHEDULE_SZ (sizeof(DES_key_schedule)) + +# define DES_ENCRYPT 1 +# define DES_DECRYPT 0 + +# define DES_CBC_MODE 0 +# define DES_PCBC_MODE 1 + +# define DES_ecb2_encrypt(i,o,k1,k2,e) \ + DES_ecb3_encrypt((i),(o),(k1),(k2),(k1),(e)) + +# define DES_ede2_cbc_encrypt(i,o,l,k1,k2,iv,e) \ + DES_ede3_cbc_encrypt((i),(o),(l),(k1),(k2),(k1),(iv),(e)) + +# define DES_ede2_cfb64_encrypt(i,o,l,k1,k2,iv,n,e) \ + DES_ede3_cfb64_encrypt((i),(o),(l),(k1),(k2),(k1),(iv),(n),(e)) + +# define DES_ede2_ofb64_encrypt(i,o,l,k1,k2,iv,n) \ + DES_ede3_ofb64_encrypt((i),(o),(l),(k1),(k2),(k1),(iv),(n)) + +OPENSSL_DECLARE_GLOBAL(int, DES_check_key); /* defaults to false */ +# define DES_check_key OPENSSL_GLOBAL_REF(DES_check_key) + +const char *DES_options(void); +void DES_ecb3_encrypt(const_DES_cblock *input, DES_cblock *output, + DES_key_schedule *ks1, DES_key_schedule *ks2, + DES_key_schedule *ks3, int enc); +DES_LONG DES_cbc_cksum(const unsigned char *input, DES_cblock *output, + long length, DES_key_schedule *schedule, + const_DES_cblock *ivec); +/* DES_cbc_encrypt does not update the IV! Use DES_ncbc_encrypt instead. */ +void DES_cbc_encrypt(const unsigned char *input, unsigned char *output, + long length, DES_key_schedule *schedule, + DES_cblock *ivec, int enc); +void DES_ncbc_encrypt(const unsigned char *input, unsigned char *output, + long length, DES_key_schedule *schedule, + DES_cblock *ivec, int enc); +void DES_xcbc_encrypt(const unsigned char *input, unsigned char *output, + long length, DES_key_schedule *schedule, + DES_cblock *ivec, const_DES_cblock *inw, + const_DES_cblock *outw, int enc); +void DES_cfb_encrypt(const unsigned char *in, unsigned char *out, int numbits, + long length, DES_key_schedule *schedule, + DES_cblock *ivec, int enc); +void DES_ecb_encrypt(const_DES_cblock *input, DES_cblock *output, + DES_key_schedule *ks, int enc); + +/* + * This is the DES encryption function that gets called by just about every + * other DES routine in the library. You should not use this function except + * to implement 'modes' of DES. I say this because the functions that call + * this routine do the conversion from 'char *' to long, and this needs to be + * done to make sure 'non-aligned' memory access do not occur. The + * characters are loaded 'little endian'. Data is a pointer to 2 unsigned + * long's and ks is the DES_key_schedule to use. enc, is non zero specifies + * encryption, zero if decryption. + */ +void DES_encrypt1(DES_LONG *data, DES_key_schedule *ks, int enc); + +/* + * This functions is the same as DES_encrypt1() except that the DES initial + * permutation (IP) and final permutation (FP) have been left out. As for + * DES_encrypt1(), you should not use this function. It is used by the + * routines in the library that implement triple DES. IP() DES_encrypt2() + * DES_encrypt2() DES_encrypt2() FP() is the same as DES_encrypt1() + * DES_encrypt1() DES_encrypt1() except faster :-). + */ +void DES_encrypt2(DES_LONG *data, DES_key_schedule *ks, int enc); + +void DES_encrypt3(DES_LONG *data, DES_key_schedule *ks1, + DES_key_schedule *ks2, DES_key_schedule *ks3); +void DES_decrypt3(DES_LONG *data, DES_key_schedule *ks1, + DES_key_schedule *ks2, DES_key_schedule *ks3); +void DES_ede3_cbc_encrypt(const unsigned char *input, unsigned char *output, + long length, + DES_key_schedule *ks1, DES_key_schedule *ks2, + DES_key_schedule *ks3, DES_cblock *ivec, int enc); +void DES_ede3_cfb64_encrypt(const unsigned char *in, unsigned char *out, + long length, DES_key_schedule *ks1, + DES_key_schedule *ks2, DES_key_schedule *ks3, + DES_cblock *ivec, int *num, int enc); +void DES_ede3_cfb_encrypt(const unsigned char *in, unsigned char *out, + int numbits, long length, DES_key_schedule *ks1, + DES_key_schedule *ks2, DES_key_schedule *ks3, + DES_cblock *ivec, int enc); +void DES_ede3_ofb64_encrypt(const unsigned char *in, unsigned char *out, + long length, DES_key_schedule *ks1, + DES_key_schedule *ks2, DES_key_schedule *ks3, + DES_cblock *ivec, int *num); +char *DES_fcrypt(const char *buf, const char *salt, char *ret); +char *DES_crypt(const char *buf, const char *salt); +void DES_ofb_encrypt(const unsigned char *in, unsigned char *out, int numbits, + long length, DES_key_schedule *schedule, + DES_cblock *ivec); +void DES_pcbc_encrypt(const unsigned char *input, unsigned char *output, + long length, DES_key_schedule *schedule, + DES_cblock *ivec, int enc); +DES_LONG DES_quad_cksum(const unsigned char *input, DES_cblock output[], + long length, int out_count, DES_cblock *seed); +int DES_random_key(DES_cblock *ret); +void DES_set_odd_parity(DES_cblock *key); +int DES_check_key_parity(const_DES_cblock *key); +int DES_is_weak_key(const_DES_cblock *key); +/* + * DES_set_key (= set_key = DES_key_sched = key_sched) calls + * DES_set_key_checked if global variable DES_check_key is set, + * DES_set_key_unchecked otherwise. + */ +int DES_set_key(const_DES_cblock *key, DES_key_schedule *schedule); +int DES_key_sched(const_DES_cblock *key, DES_key_schedule *schedule); +int DES_set_key_checked(const_DES_cblock *key, DES_key_schedule *schedule); +void DES_set_key_unchecked(const_DES_cblock *key, DES_key_schedule *schedule); +void DES_string_to_key(const char *str, DES_cblock *key); +void DES_string_to_2keys(const char *str, DES_cblock *key1, DES_cblock *key2); +void DES_cfb64_encrypt(const unsigned char *in, unsigned char *out, + long length, DES_key_schedule *schedule, + DES_cblock *ivec, int *num, int enc); +void DES_ofb64_encrypt(const unsigned char *in, unsigned char *out, + long length, DES_key_schedule *schedule, + DES_cblock *ivec, int *num); + +# define DES_fixup_key_parity DES_set_odd_parity + +# ifdef __cplusplus +} +# endif +# endif + +#endif diff --git a/mobile/ios/FxA/FxA/include/openssl/dh.h b/mobile/ios/FxA/FxA/include/openssl/dh.h new file mode 100644 index 0000000000..6d149bc932 --- /dev/null +++ b/mobile/ios/FxA/FxA/include/openssl/dh.h @@ -0,0 +1,344 @@ +/* + * Copyright 1995-2016 The OpenSSL Project Authors. All Rights Reserved. + * + * Licensed under the OpenSSL license (the "License"). You may not use + * this file except in compliance with the License. You can obtain a copy + * in the file LICENSE in the source distribution or at + * https://www.openssl.org/source/license.html + */ + +#ifndef HEADER_DH_H +# define HEADER_DH_H + +# include + +# ifndef OPENSSL_NO_DH +# include +# include +# include +# include +# if OPENSSL_API_COMPAT < 0x10100000L +# include +# endif + +# ifdef __cplusplus +extern "C" { +# endif + +# ifndef OPENSSL_DH_MAX_MODULUS_BITS +# define OPENSSL_DH_MAX_MODULUS_BITS 10000 +# endif + +# define OPENSSL_DH_FIPS_MIN_MODULUS_BITS 1024 + +# define DH_FLAG_CACHE_MONT_P 0x01 + +# if OPENSSL_API_COMPAT < 0x10100000L +/* + * Does nothing. Previously this switched off constant time behaviour. + */ +# define DH_FLAG_NO_EXP_CONSTTIME 0x00 +# endif + +/* + * If this flag is set the DH method is FIPS compliant and can be used in + * FIPS mode. This is set in the validated module method. If an application + * sets this flag in its own methods it is its responsibility to ensure the + * result is compliant. + */ + +# define DH_FLAG_FIPS_METHOD 0x0400 + +/* + * If this flag is set the operations normally disabled in FIPS mode are + * permitted it is then the applications responsibility to ensure that the + * usage is compliant. + */ + +# define DH_FLAG_NON_FIPS_ALLOW 0x0400 + +/* Already defined in ossl_typ.h */ +/* typedef struct dh_st DH; */ +/* typedef struct dh_method DH_METHOD; */ + +DECLARE_ASN1_ITEM(DHparams) + +# define DH_GENERATOR_2 2 +/* #define DH_GENERATOR_3 3 */ +# define DH_GENERATOR_5 5 + +/* DH_check error codes */ +# define DH_CHECK_P_NOT_PRIME 0x01 +# define DH_CHECK_P_NOT_SAFE_PRIME 0x02 +# define DH_UNABLE_TO_CHECK_GENERATOR 0x04 +# define DH_NOT_SUITABLE_GENERATOR 0x08 +# define DH_CHECK_Q_NOT_PRIME 0x10 +# define DH_CHECK_INVALID_Q_VALUE 0x20 +# define DH_CHECK_INVALID_J_VALUE 0x40 + +/* DH_check_pub_key error codes */ +# define DH_CHECK_PUBKEY_TOO_SMALL 0x01 +# define DH_CHECK_PUBKEY_TOO_LARGE 0x02 +# define DH_CHECK_PUBKEY_INVALID 0x04 + +/* + * primes p where (p-1)/2 is prime too are called "safe"; we define this for + * backward compatibility: + */ +# define DH_CHECK_P_NOT_STRONG_PRIME DH_CHECK_P_NOT_SAFE_PRIME + +# define d2i_DHparams_fp(fp,x) (DH *)ASN1_d2i_fp((char *(*)())DH_new, \ + (char *(*)())d2i_DHparams,(fp),(unsigned char **)(x)) +# define i2d_DHparams_fp(fp,x) ASN1_i2d_fp(i2d_DHparams,(fp), \ + (unsigned char *)(x)) +# define d2i_DHparams_bio(bp,x) ASN1_d2i_bio_of(DH,DH_new,d2i_DHparams,bp,x) +# define i2d_DHparams_bio(bp,x) ASN1_i2d_bio_of_const(DH,i2d_DHparams,bp,x) + +DH *DHparams_dup(DH *); + +const DH_METHOD *DH_OpenSSL(void); + +void DH_set_default_method(const DH_METHOD *meth); +const DH_METHOD *DH_get_default_method(void); +int DH_set_method(DH *dh, const DH_METHOD *meth); +DH *DH_new_method(ENGINE *engine); + +DH *DH_new(void); +void DH_free(DH *dh); +int DH_up_ref(DH *dh); +int DH_bits(const DH *dh); +int DH_size(const DH *dh); +int DH_security_bits(const DH *dh); +#define DH_get_ex_new_index(l, p, newf, dupf, freef) \ + CRYPTO_get_ex_new_index(CRYPTO_EX_INDEX_DH, l, p, newf, dupf, freef) +int DH_set_ex_data(DH *d, int idx, void *arg); +void *DH_get_ex_data(DH *d, int idx); + +/* Deprecated version */ +DEPRECATEDIN_0_9_8(DH *DH_generate_parameters(int prime_len, int generator, + void (*callback) (int, int, + void *), + void *cb_arg)) + +/* New version */ +int DH_generate_parameters_ex(DH *dh, int prime_len, int generator, + BN_GENCB *cb); + +int DH_check_params(const DH *dh, int *ret); +int DH_check(const DH *dh, int *codes); +int DH_check_pub_key(const DH *dh, const BIGNUM *pub_key, int *codes); +int DH_generate_key(DH *dh); +int DH_compute_key(unsigned char *key, const BIGNUM *pub_key, DH *dh); +int DH_compute_key_padded(unsigned char *key, const BIGNUM *pub_key, DH *dh); +DH *d2i_DHparams(DH **a, const unsigned char **pp, long length); +int i2d_DHparams(const DH *a, unsigned char **pp); +DH *d2i_DHxparams(DH **a, const unsigned char **pp, long length); +int i2d_DHxparams(const DH *a, unsigned char **pp); +# ifndef OPENSSL_NO_STDIO +int DHparams_print_fp(FILE *fp, const DH *x); +# endif +int DHparams_print(BIO *bp, const DH *x); + +/* RFC 5114 parameters */ +DH *DH_get_1024_160(void); +DH *DH_get_2048_224(void); +DH *DH_get_2048_256(void); + +# ifndef OPENSSL_NO_CMS +/* RFC2631 KDF */ +int DH_KDF_X9_42(unsigned char *out, size_t outlen, + const unsigned char *Z, size_t Zlen, + ASN1_OBJECT *key_oid, + const unsigned char *ukm, size_t ukmlen, const EVP_MD *md); +# endif + +void DH_get0_pqg(const DH *dh, + const BIGNUM **p, const BIGNUM **q, const BIGNUM **g); +int DH_set0_pqg(DH *dh, BIGNUM *p, BIGNUM *q, BIGNUM *g); +void DH_get0_key(const DH *dh, + const BIGNUM **pub_key, const BIGNUM **priv_key); +int DH_set0_key(DH *dh, BIGNUM *pub_key, BIGNUM *priv_key); +void DH_clear_flags(DH *dh, int flags); +int DH_test_flags(const DH *dh, int flags); +void DH_set_flags(DH *dh, int flags); +ENGINE *DH_get0_engine(DH *d); +long DH_get_length(const DH *dh); +int DH_set_length(DH *dh, long length); + +DH_METHOD *DH_meth_new(const char *name, int flags); +void DH_meth_free(DH_METHOD *dhm); +DH_METHOD *DH_meth_dup(const DH_METHOD *dhm); +const char *DH_meth_get0_name(const DH_METHOD *dhm); +int DH_meth_set1_name(DH_METHOD *dhm, const char *name); +int DH_meth_get_flags(DH_METHOD *dhm); +int DH_meth_set_flags(DH_METHOD *dhm, int flags); +void *DH_meth_get0_app_data(const DH_METHOD *dhm); +int DH_meth_set0_app_data(DH_METHOD *dhm, void *app_data); +int (*DH_meth_get_generate_key(const DH_METHOD *dhm)) (DH *); +int DH_meth_set_generate_key(DH_METHOD *dhm, int (*generate_key) (DH *)); +int (*DH_meth_get_compute_key(const DH_METHOD *dhm)) + (unsigned char *key, const BIGNUM *pub_key, DH *dh); +int DH_meth_set_compute_key(DH_METHOD *dhm, + int (*compute_key) (unsigned char *key, const BIGNUM *pub_key, DH *dh)); +int (*DH_meth_get_bn_mod_exp(const DH_METHOD *dhm)) + (const DH *, BIGNUM *, const BIGNUM *, const BIGNUM *, const BIGNUM *, + BN_CTX *, BN_MONT_CTX *); +int DH_meth_set_bn_mod_exp(DH_METHOD *dhm, + int (*bn_mod_exp) (const DH *, BIGNUM *, const BIGNUM *, const BIGNUM *, + const BIGNUM *, BN_CTX *, BN_MONT_CTX *)); +int (*DH_meth_get_init(const DH_METHOD *dhm))(DH *); +int DH_meth_set_init(DH_METHOD *dhm, int (*init)(DH *)); +int (*DH_meth_get_finish(const DH_METHOD *dhm)) (DH *); +int DH_meth_set_finish(DH_METHOD *dhm, int (*finish) (DH *)); +int (*DH_meth_get_generate_params(const DH_METHOD *dhm)) + (DH *, int, int, BN_GENCB *); +int DH_meth_set_generate_params(DH_METHOD *dhm, + int (*generate_params) (DH *, int, int, BN_GENCB *)); + + +# define EVP_PKEY_CTX_set_dh_paramgen_prime_len(ctx, len) \ + EVP_PKEY_CTX_ctrl(ctx, EVP_PKEY_DH, EVP_PKEY_OP_PARAMGEN, \ + EVP_PKEY_CTRL_DH_PARAMGEN_PRIME_LEN, len, NULL) + +# define EVP_PKEY_CTX_set_dh_paramgen_subprime_len(ctx, len) \ + EVP_PKEY_CTX_ctrl(ctx, EVP_PKEY_DH, EVP_PKEY_OP_PARAMGEN, \ + EVP_PKEY_CTRL_DH_PARAMGEN_SUBPRIME_LEN, len, NULL) + +# define EVP_PKEY_CTX_set_dh_paramgen_type(ctx, typ) \ + EVP_PKEY_CTX_ctrl(ctx, EVP_PKEY_DH, EVP_PKEY_OP_PARAMGEN, \ + EVP_PKEY_CTRL_DH_PARAMGEN_TYPE, typ, NULL) + +# define EVP_PKEY_CTX_set_dh_paramgen_generator(ctx, gen) \ + EVP_PKEY_CTX_ctrl(ctx, EVP_PKEY_DH, EVP_PKEY_OP_PARAMGEN, \ + EVP_PKEY_CTRL_DH_PARAMGEN_GENERATOR, gen, NULL) + +# define EVP_PKEY_CTX_set_dh_rfc5114(ctx, gen) \ + EVP_PKEY_CTX_ctrl(ctx, EVP_PKEY_DHX, EVP_PKEY_OP_PARAMGEN, \ + EVP_PKEY_CTRL_DH_RFC5114, gen, NULL) + +# define EVP_PKEY_CTX_set_dhx_rfc5114(ctx, gen) \ + EVP_PKEY_CTX_ctrl(ctx, EVP_PKEY_DHX, EVP_PKEY_OP_PARAMGEN, \ + EVP_PKEY_CTRL_DH_RFC5114, gen, NULL) + +# define EVP_PKEY_CTX_set_dh_kdf_type(ctx, kdf) \ + EVP_PKEY_CTX_ctrl(ctx, EVP_PKEY_DHX, \ + EVP_PKEY_OP_DERIVE, \ + EVP_PKEY_CTRL_DH_KDF_TYPE, kdf, NULL) + +# define EVP_PKEY_CTX_get_dh_kdf_type(ctx) \ + EVP_PKEY_CTX_ctrl(ctx, EVP_PKEY_DHX, \ + EVP_PKEY_OP_DERIVE, \ + EVP_PKEY_CTRL_DH_KDF_TYPE, -2, NULL) + +# define EVP_PKEY_CTX_set0_dh_kdf_oid(ctx, oid) \ + EVP_PKEY_CTX_ctrl(ctx, EVP_PKEY_DHX, \ + EVP_PKEY_OP_DERIVE, \ + EVP_PKEY_CTRL_DH_KDF_OID, 0, (void *)oid) + +# define EVP_PKEY_CTX_get0_dh_kdf_oid(ctx, poid) \ + EVP_PKEY_CTX_ctrl(ctx, EVP_PKEY_DHX, \ + EVP_PKEY_OP_DERIVE, \ + EVP_PKEY_CTRL_GET_DH_KDF_OID, 0, (void *)poid) + +# define EVP_PKEY_CTX_set_dh_kdf_md(ctx, md) \ + EVP_PKEY_CTX_ctrl(ctx, EVP_PKEY_DHX, \ + EVP_PKEY_OP_DERIVE, \ + EVP_PKEY_CTRL_DH_KDF_MD, 0, (void *)md) + +# define EVP_PKEY_CTX_get_dh_kdf_md(ctx, pmd) \ + EVP_PKEY_CTX_ctrl(ctx, EVP_PKEY_DHX, \ + EVP_PKEY_OP_DERIVE, \ + EVP_PKEY_CTRL_GET_DH_KDF_MD, 0, (void *)pmd) + +# define EVP_PKEY_CTX_set_dh_kdf_outlen(ctx, len) \ + EVP_PKEY_CTX_ctrl(ctx, EVP_PKEY_DHX, \ + EVP_PKEY_OP_DERIVE, \ + EVP_PKEY_CTRL_DH_KDF_OUTLEN, len, NULL) + +# define EVP_PKEY_CTX_get_dh_kdf_outlen(ctx, plen) \ + EVP_PKEY_CTX_ctrl(ctx, EVP_PKEY_DHX, \ + EVP_PKEY_OP_DERIVE, \ + EVP_PKEY_CTRL_GET_DH_KDF_OUTLEN, 0, (void *)plen) + +# define EVP_PKEY_CTX_set0_dh_kdf_ukm(ctx, p, plen) \ + EVP_PKEY_CTX_ctrl(ctx, EVP_PKEY_DHX, \ + EVP_PKEY_OP_DERIVE, \ + EVP_PKEY_CTRL_DH_KDF_UKM, plen, (void *)p) + +# define EVP_PKEY_CTX_get0_dh_kdf_ukm(ctx, p) \ + EVP_PKEY_CTX_ctrl(ctx, EVP_PKEY_DHX, \ + EVP_PKEY_OP_DERIVE, \ + EVP_PKEY_CTRL_GET_DH_KDF_UKM, 0, (void *)p) + +# define EVP_PKEY_CTRL_DH_PARAMGEN_PRIME_LEN (EVP_PKEY_ALG_CTRL + 1) +# define EVP_PKEY_CTRL_DH_PARAMGEN_GENERATOR (EVP_PKEY_ALG_CTRL + 2) +# define EVP_PKEY_CTRL_DH_RFC5114 (EVP_PKEY_ALG_CTRL + 3) +# define EVP_PKEY_CTRL_DH_PARAMGEN_SUBPRIME_LEN (EVP_PKEY_ALG_CTRL + 4) +# define EVP_PKEY_CTRL_DH_PARAMGEN_TYPE (EVP_PKEY_ALG_CTRL + 5) +# define EVP_PKEY_CTRL_DH_KDF_TYPE (EVP_PKEY_ALG_CTRL + 6) +# define EVP_PKEY_CTRL_DH_KDF_MD (EVP_PKEY_ALG_CTRL + 7) +# define EVP_PKEY_CTRL_GET_DH_KDF_MD (EVP_PKEY_ALG_CTRL + 8) +# define EVP_PKEY_CTRL_DH_KDF_OUTLEN (EVP_PKEY_ALG_CTRL + 9) +# define EVP_PKEY_CTRL_GET_DH_KDF_OUTLEN (EVP_PKEY_ALG_CTRL + 10) +# define EVP_PKEY_CTRL_DH_KDF_UKM (EVP_PKEY_ALG_CTRL + 11) +# define EVP_PKEY_CTRL_GET_DH_KDF_UKM (EVP_PKEY_ALG_CTRL + 12) +# define EVP_PKEY_CTRL_DH_KDF_OID (EVP_PKEY_ALG_CTRL + 13) +# define EVP_PKEY_CTRL_GET_DH_KDF_OID (EVP_PKEY_ALG_CTRL + 14) + +/* KDF types */ +# define EVP_PKEY_DH_KDF_NONE 1 +# ifndef OPENSSL_NO_CMS +# define EVP_PKEY_DH_KDF_X9_42 2 +# endif + +/* BEGIN ERROR CODES */ +/* + * The following lines are auto generated by the script mkerr.pl. Any changes + * made after this point may be overwritten when the script is next run. + */ + +int ERR_load_DH_strings(void); + +/* Error codes for the DH functions. */ + +/* Function codes. */ +# define DH_F_COMPUTE_KEY 102 +# define DH_F_DHPARAMS_PRINT_FP 101 +# define DH_F_DH_BUILTIN_GENPARAMS 106 +# define DH_F_DH_CMS_DECRYPT 114 +# define DH_F_DH_CMS_SET_PEERKEY 115 +# define DH_F_DH_CMS_SET_SHARED_INFO 116 +# define DH_F_DH_METH_DUP 117 +# define DH_F_DH_METH_NEW 118 +# define DH_F_DH_METH_SET1_NAME 119 +# define DH_F_DH_NEW_METHOD 105 +# define DH_F_DH_PARAM_DECODE 107 +# define DH_F_DH_PRIV_DECODE 110 +# define DH_F_DH_PRIV_ENCODE 111 +# define DH_F_DH_PUB_DECODE 108 +# define DH_F_DH_PUB_ENCODE 109 +# define DH_F_DO_DH_PRINT 100 +# define DH_F_GENERATE_KEY 103 +# define DH_F_PKEY_DH_DERIVE 112 +# define DH_F_PKEY_DH_KEYGEN 113 + +/* Reason codes. */ +# define DH_R_BAD_GENERATOR 101 +# define DH_R_BN_DECODE_ERROR 109 +# define DH_R_BN_ERROR 106 +# define DH_R_DECODE_ERROR 104 +# define DH_R_INVALID_PUBKEY 102 +# define DH_R_KDF_PARAMETER_ERROR 112 +# define DH_R_KEYS_NOT_SET 108 +# define DH_R_MODULUS_TOO_LARGE 103 +# define DH_R_NO_PARAMETERS_SET 107 +# define DH_R_NO_PRIVATE_VALUE 100 +# define DH_R_PARAMETER_ENCODING_ERROR 105 +# define DH_R_PEER_KEY_ERROR 111 +# define DH_R_SHARED_INFO_ERROR 113 + +# ifdef __cplusplus +} +# endif +# endif +#endif diff --git a/mobile/ios/FxA/FxA/include/openssl/dsa.h b/mobile/ios/FxA/FxA/include/openssl/dsa.h new file mode 100644 index 0000000000..139718edb9 --- /dev/null +++ b/mobile/ios/FxA/FxA/include/openssl/dsa.h @@ -0,0 +1,283 @@ +/* + * Copyright 1995-2016 The OpenSSL Project Authors. All Rights Reserved. + * + * Licensed under the OpenSSL license (the "License"). You may not use + * this file except in compliance with the License. You can obtain a copy + * in the file LICENSE in the source distribution or at + * https://www.openssl.org/source/license.html + */ + +/* + * The DSS routines are based on patches supplied by + * Steven Schoch . + */ + +#ifndef HEADER_DSA_H +# define HEADER_DSA_H + +# include + +# ifndef OPENSSL_NO_DSA +# ifdef __cplusplus +extern "C" { +# endif +# include +# include +# include +# include +# include +# include +# if OPENSSL_API_COMPAT < 0x10100000L +# include +# endif + +# ifndef OPENSSL_DSA_MAX_MODULUS_BITS +# define OPENSSL_DSA_MAX_MODULUS_BITS 10000 +# endif + +# define OPENSSL_DSA_FIPS_MIN_MODULUS_BITS 1024 + +# define DSA_FLAG_CACHE_MONT_P 0x01 +# if OPENSSL_API_COMPAT < 0x10100000L +/* + * Does nothing. Previously this switched off constant time behaviour. + */ +# define DSA_FLAG_NO_EXP_CONSTTIME 0x00 +# endif + +/* + * If this flag is set the DSA method is FIPS compliant and can be used in + * FIPS mode. This is set in the validated module method. If an application + * sets this flag in its own methods it is its responsibility to ensure the + * result is compliant. + */ + +# define DSA_FLAG_FIPS_METHOD 0x0400 + +/* + * If this flag is set the operations normally disabled in FIPS mode are + * permitted it is then the applications responsibility to ensure that the + * usage is compliant. + */ + +# define DSA_FLAG_NON_FIPS_ALLOW 0x0400 +# define DSA_FLAG_FIPS_CHECKED 0x0800 + +/* Already defined in ossl_typ.h */ +/* typedef struct dsa_st DSA; */ +/* typedef struct dsa_method DSA_METHOD; */ + +typedef struct DSA_SIG_st DSA_SIG; + +# define d2i_DSAparams_fp(fp,x) (DSA *)ASN1_d2i_fp((char *(*)())DSA_new, \ + (char *(*)())d2i_DSAparams,(fp),(unsigned char **)(x)) +# define i2d_DSAparams_fp(fp,x) ASN1_i2d_fp(i2d_DSAparams,(fp), \ + (unsigned char *)(x)) +# define d2i_DSAparams_bio(bp,x) ASN1_d2i_bio_of(DSA,DSA_new,d2i_DSAparams,bp,x) +# define i2d_DSAparams_bio(bp,x) ASN1_i2d_bio_of_const(DSA,i2d_DSAparams,bp,x) + +DSA *DSAparams_dup(DSA *x); +DSA_SIG *DSA_SIG_new(void); +void DSA_SIG_free(DSA_SIG *a); +int i2d_DSA_SIG(const DSA_SIG *a, unsigned char **pp); +DSA_SIG *d2i_DSA_SIG(DSA_SIG **v, const unsigned char **pp, long length); +void DSA_SIG_get0(const DSA_SIG *sig, const BIGNUM **pr, const BIGNUM **ps); +int DSA_SIG_set0(DSA_SIG *sig, BIGNUM *r, BIGNUM *s); + +DSA_SIG *DSA_do_sign(const unsigned char *dgst, int dlen, DSA *dsa); +int DSA_do_verify(const unsigned char *dgst, int dgst_len, + DSA_SIG *sig, DSA *dsa); + +const DSA_METHOD *DSA_OpenSSL(void); + +void DSA_set_default_method(const DSA_METHOD *); +const DSA_METHOD *DSA_get_default_method(void); +int DSA_set_method(DSA *dsa, const DSA_METHOD *); +const DSA_METHOD *DSA_get_method(DSA *d); + +DSA *DSA_new(void); +DSA *DSA_new_method(ENGINE *engine); +void DSA_free(DSA *r); +/* "up" the DSA object's reference count */ +int DSA_up_ref(DSA *r); +int DSA_size(const DSA *); +int DSA_bits(const DSA *d); +int DSA_security_bits(const DSA *d); + /* next 4 return -1 on error */ +int DSA_sign_setup(DSA *dsa, BN_CTX *ctx_in, BIGNUM **kinvp, BIGNUM **rp); +int DSA_sign(int type, const unsigned char *dgst, int dlen, + unsigned char *sig, unsigned int *siglen, DSA *dsa); +int DSA_verify(int type, const unsigned char *dgst, int dgst_len, + const unsigned char *sigbuf, int siglen, DSA *dsa); +#define DSA_get_ex_new_index(l, p, newf, dupf, freef) \ + CRYPTO_get_ex_new_index(CRYPTO_EX_INDEX_DSA, l, p, newf, dupf, freef) +int DSA_set_ex_data(DSA *d, int idx, void *arg); +void *DSA_get_ex_data(DSA *d, int idx); + +DSA *d2i_DSAPublicKey(DSA **a, const unsigned char **pp, long length); +DSA *d2i_DSAPrivateKey(DSA **a, const unsigned char **pp, long length); +DSA *d2i_DSAparams(DSA **a, const unsigned char **pp, long length); + +/* Deprecated version */ +DEPRECATEDIN_0_9_8(DSA *DSA_generate_parameters(int bits, + unsigned char *seed, + int seed_len, + int *counter_ret, + unsigned long *h_ret, void + (*callback) (int, int, + void *), + void *cb_arg)) + +/* New version */ +int DSA_generate_parameters_ex(DSA *dsa, int bits, + const unsigned char *seed, int seed_len, + int *counter_ret, unsigned long *h_ret, + BN_GENCB *cb); + +int DSA_generate_key(DSA *a); +int i2d_DSAPublicKey(const DSA *a, unsigned char **pp); +int i2d_DSAPrivateKey(const DSA *a, unsigned char **pp); +int i2d_DSAparams(const DSA *a, unsigned char **pp); + +int DSAparams_print(BIO *bp, const DSA *x); +int DSA_print(BIO *bp, const DSA *x, int off); +# ifndef OPENSSL_NO_STDIO +int DSAparams_print_fp(FILE *fp, const DSA *x); +int DSA_print_fp(FILE *bp, const DSA *x, int off); +# endif + +# define DSS_prime_checks 50 +/* + * Primality test according to FIPS PUB 186[-1], Appendix 2.1: 50 rounds of + * Rabin-Miller + */ +# define DSA_is_prime(n, callback, cb_arg) \ + BN_is_prime(n, DSS_prime_checks, callback, NULL, cb_arg) + +# ifndef OPENSSL_NO_DH +/* + * Convert DSA structure (key or just parameters) into DH structure (be + * careful to avoid small subgroup attacks when using this!) + */ +DH *DSA_dup_DH(const DSA *r); +# endif + +# define EVP_PKEY_CTX_set_dsa_paramgen_bits(ctx, nbits) \ + EVP_PKEY_CTX_ctrl(ctx, EVP_PKEY_DSA, EVP_PKEY_OP_PARAMGEN, \ + EVP_PKEY_CTRL_DSA_PARAMGEN_BITS, nbits, NULL) + +# define EVP_PKEY_CTRL_DSA_PARAMGEN_BITS (EVP_PKEY_ALG_CTRL + 1) +# define EVP_PKEY_CTRL_DSA_PARAMGEN_Q_BITS (EVP_PKEY_ALG_CTRL + 2) +# define EVP_PKEY_CTRL_DSA_PARAMGEN_MD (EVP_PKEY_ALG_CTRL + 3) + +void DSA_get0_pqg(const DSA *d, + const BIGNUM **p, const BIGNUM **q, const BIGNUM **g); +int DSA_set0_pqg(DSA *d, BIGNUM *p, BIGNUM *q, BIGNUM *g); +void DSA_get0_key(const DSA *d, + const BIGNUM **pub_key, const BIGNUM **priv_key); +int DSA_set0_key(DSA *d, BIGNUM *pub_key, BIGNUM *priv_key); +void DSA_clear_flags(DSA *d, int flags); +int DSA_test_flags(const DSA *d, int flags); +void DSA_set_flags(DSA *d, int flags); +ENGINE *DSA_get0_engine(DSA *d); + +DSA_METHOD *DSA_meth_new(const char *name, int flags); +void DSA_meth_free(DSA_METHOD *dsam); +DSA_METHOD *DSA_meth_dup(const DSA_METHOD *dsam); +const char *DSA_meth_get0_name(const DSA_METHOD *dsam); +int DSA_meth_set1_name(DSA_METHOD *dsam, const char *name); +int DSA_meth_get_flags(DSA_METHOD *dsam); +int DSA_meth_set_flags(DSA_METHOD *dsam, int flags); +void *DSA_meth_get0_app_data(const DSA_METHOD *dsam); +int DSA_meth_set0_app_data(DSA_METHOD *dsam, void *app_data); +DSA_SIG *(*DSA_meth_get_sign(const DSA_METHOD *dsam)) + (const unsigned char *, int, DSA *); +int DSA_meth_set_sign(DSA_METHOD *dsam, + DSA_SIG *(*sign) (const unsigned char *, int, DSA *)); +int (*DSA_meth_get_sign_setup(const DSA_METHOD *dsam)) + (DSA *, BN_CTX *, BIGNUM **, BIGNUM **); +int DSA_meth_set_sign_setup(DSA_METHOD *dsam, + int (*sign_setup) (DSA *, BN_CTX *, BIGNUM **, BIGNUM **)); +int (*DSA_meth_get_verify(const DSA_METHOD *dsam)) + (const unsigned char *, int , DSA_SIG *, DSA *); +int DSA_meth_set_verify(DSA_METHOD *dsam, + int (*verify) (const unsigned char *, int, DSA_SIG *, DSA *)); +int (*DSA_meth_get_mod_exp(const DSA_METHOD *dsam)) + (DSA *, BIGNUM *, const BIGNUM *, const BIGNUM *, const BIGNUM *, + const BIGNUM *, const BIGNUM *, BN_CTX *, BN_MONT_CTX *); +int DSA_meth_set_mod_exp(DSA_METHOD *dsam, + int (*mod_exp) (DSA *, BIGNUM *, const BIGNUM *, const BIGNUM *, + const BIGNUM *, const BIGNUM *, const BIGNUM *, BN_CTX *, + BN_MONT_CTX *)); +int (*DSA_meth_get_bn_mod_exp(const DSA_METHOD *dsam)) + (DSA *, BIGNUM *, const BIGNUM *, const BIGNUM *, const BIGNUM *, + BN_CTX *, BN_MONT_CTX *); +int DSA_meth_set_bn_mod_exp(DSA_METHOD *dsam, + int (*bn_mod_exp) (DSA *, BIGNUM *, const BIGNUM *, const BIGNUM *, + const BIGNUM *, BN_CTX *, BN_MONT_CTX *)); +int (*DSA_meth_get_init(const DSA_METHOD *dsam))(DSA *); +int DSA_meth_set_init(DSA_METHOD *dsam, int (*init)(DSA *)); +int (*DSA_meth_get_finish(const DSA_METHOD *dsam)) (DSA *); +int DSA_meth_set_finish(DSA_METHOD *dsam, int (*finish) (DSA *)); +int (*DSA_meth_get_paramgen(const DSA_METHOD *dsam)) + (DSA *, int, const unsigned char *, int, int *, unsigned long *, + BN_GENCB *); +int DSA_meth_set_paramgen(DSA_METHOD *dsam, + int (*paramgen) (DSA *, int, const unsigned char *, int, int *, + unsigned long *, BN_GENCB *)); +int (*DSA_meth_get_keygen(const DSA_METHOD *dsam)) (DSA *); +int DSA_meth_set_keygen(DSA_METHOD *dsam, int (*keygen) (DSA *)); + +/* BEGIN ERROR CODES */ +/* + * The following lines are auto generated by the script mkerr.pl. Any changes + * made after this point may be overwritten when the script is next run. + */ + +int ERR_load_DSA_strings(void); + +/* Error codes for the DSA functions. */ + +/* Function codes. */ +# define DSA_F_DSAPARAMS_PRINT 100 +# define DSA_F_DSAPARAMS_PRINT_FP 101 +# define DSA_F_DSA_BUILTIN_PARAMGEN 125 +# define DSA_F_DSA_BUILTIN_PARAMGEN2 126 +# define DSA_F_DSA_DO_SIGN 112 +# define DSA_F_DSA_DO_VERIFY 113 +# define DSA_F_DSA_METH_DUP 127 +# define DSA_F_DSA_METH_NEW 128 +# define DSA_F_DSA_METH_SET1_NAME 129 +# define DSA_F_DSA_NEW_METHOD 103 +# define DSA_F_DSA_PARAM_DECODE 119 +# define DSA_F_DSA_PRINT_FP 105 +# define DSA_F_DSA_PRIV_DECODE 115 +# define DSA_F_DSA_PRIV_ENCODE 116 +# define DSA_F_DSA_PUB_DECODE 117 +# define DSA_F_DSA_PUB_ENCODE 118 +# define DSA_F_DSA_SIGN 106 +# define DSA_F_DSA_SIGN_SETUP 107 +# define DSA_F_DSA_SIG_NEW 102 +# define DSA_F_OLD_DSA_PRIV_DECODE 122 +# define DSA_F_PKEY_DSA_CTRL 120 +# define DSA_F_PKEY_DSA_KEYGEN 121 + +/* Reason codes. */ +# define DSA_R_BAD_Q_VALUE 102 +# define DSA_R_BN_DECODE_ERROR 108 +# define DSA_R_BN_ERROR 109 +# define DSA_R_DECODE_ERROR 104 +# define DSA_R_INVALID_DIGEST_TYPE 106 +# define DSA_R_INVALID_PARAMETERS 112 +# define DSA_R_MISSING_PARAMETERS 101 +# define DSA_R_MODULUS_TOO_LARGE 103 +# define DSA_R_NO_PARAMETERS_SET 107 +# define DSA_R_PARAMETER_ENCODING_ERROR 105 +# define DSA_R_Q_NOT_PRIME 113 +# define DSA_R_SEED_LEN_SMALL 110 + +# ifdef __cplusplus +} +# endif +# endif +#endif diff --git a/mobile/ios/FxA/FxA/include/openssl/dtls1.h b/mobile/ios/FxA/FxA/include/openssl/dtls1.h new file mode 100644 index 0000000000..f4769f83fe --- /dev/null +++ b/mobile/ios/FxA/FxA/include/openssl/dtls1.h @@ -0,0 +1,56 @@ +/* + * Copyright 2005-2016 The OpenSSL Project Authors. All Rights Reserved. + * + * Licensed under the OpenSSL license (the "License"). You may not use + * this file except in compliance with the License. You can obtain a copy + * in the file LICENSE in the source distribution or at + * https://www.openssl.org/source/license.html + */ + +#ifndef HEADER_DTLS1_H +# define HEADER_DTLS1_H + +#ifdef __cplusplus +extern "C" { +#endif + +# define DTLS1_VERSION 0xFEFF +# define DTLS1_2_VERSION 0xFEFD +# define DTLS_MIN_VERSION DTLS1_VERSION +# define DTLS_MAX_VERSION DTLS1_2_VERSION +# define DTLS1_VERSION_MAJOR 0xFE + +# define DTLS1_BAD_VER 0x0100 + +/* Special value for method supporting multiple versions */ +# define DTLS_ANY_VERSION 0x1FFFF + +/* lengths of messages */ +# define DTLS1_COOKIE_LENGTH 256 + +# define DTLS1_RT_HEADER_LENGTH 13 + +# define DTLS1_HM_HEADER_LENGTH 12 + +# define DTLS1_HM_BAD_FRAGMENT -2 +# define DTLS1_HM_FRAGMENT_RETRY -3 + +# define DTLS1_CCS_HEADER_LENGTH 1 + +# ifdef DTLS1_AD_MISSING_HANDSHAKE_MESSAGE +# define DTLS1_AL_HEADER_LENGTH 7 +# else +# define DTLS1_AL_HEADER_LENGTH 2 +# endif + + +/* Timeout multipliers (timeout slice is defined in apps/timeouts.h */ +# define DTLS1_TMO_READ_COUNT 2 +# define DTLS1_TMO_WRITE_COUNT 2 + +# define DTLS1_TMO_ALERT_COUNT 12 + +#ifdef __cplusplus +} +#endif +#endif diff --git a/mobile/ios/FxA/FxA/include/openssl/e_os2.h b/mobile/ios/FxA/FxA/include/openssl/e_os2.h new file mode 100644 index 0000000000..99ea3477d7 --- /dev/null +++ b/mobile/ios/FxA/FxA/include/openssl/e_os2.h @@ -0,0 +1,311 @@ +/* + * Copyright 1995-2016 The OpenSSL Project Authors. All Rights Reserved. + * + * Licensed under the OpenSSL license (the "License"). You may not use + * this file except in compliance with the License. You can obtain a copy + * in the file LICENSE in the source distribution or at + * https://www.openssl.org/source/license.html + */ + +#ifndef HEADER_E_OS2_H +# define HEADER_E_OS2_H + +# include + +#ifdef __cplusplus +extern "C" { +#endif + +/****************************************************************************** + * Detect operating systems. This probably needs completing. + * The result is that at least one OPENSSL_SYS_os macro should be defined. + * However, if none is defined, Unix is assumed. + **/ + +# define OPENSSL_SYS_UNIX + +/* --------------------- Microsoft operating systems ---------------------- */ + +/* + * Note that MSDOS actually denotes 32-bit environments running on top of + * MS-DOS, such as DJGPP one. + */ +# if defined(OPENSSL_SYS_MSDOS) +# undef OPENSSL_SYS_UNIX +# endif + +/* + * For 32 bit environment, there seems to be the CygWin environment and then + * all the others that try to do the same thing Microsoft does... + */ +/* + * UEFI lives here because it might be built with a Microsoft toolchain and + * we need to avoid the false positive match on Windows. + */ +# if defined(OPENSSL_SYS_UEFI) +# undef OPENSSL_SYS_UNIX +# elif defined(OPENSSL_SYS_UWIN) +# undef OPENSSL_SYS_UNIX +# define OPENSSL_SYS_WIN32_UWIN +# else +# if defined(__CYGWIN__) || defined(OPENSSL_SYS_CYGWIN) +# undef OPENSSL_SYS_UNIX +# define OPENSSL_SYS_WIN32_CYGWIN +# else +# if defined(_WIN32) || defined(OPENSSL_SYS_WIN32) +# undef OPENSSL_SYS_UNIX +# if !defined(OPENSSL_SYS_WIN32) +# define OPENSSL_SYS_WIN32 +# endif +# endif +# if defined(_WIN64) || defined(OPENSSL_SYS_WIN64) +# undef OPENSSL_SYS_UNIX +# if !defined(OPENSSL_SYS_WIN64) +# define OPENSSL_SYS_WIN64 +# endif +# endif +# if defined(OPENSSL_SYS_WINNT) +# undef OPENSSL_SYS_UNIX +# endif +# if defined(OPENSSL_SYS_WINCE) +# undef OPENSSL_SYS_UNIX +# endif +# endif +# endif + +/* Anything that tries to look like Microsoft is "Windows" */ +# if defined(OPENSSL_SYS_WIN32) || defined(OPENSSL_SYS_WIN64) || defined(OPENSSL_SYS_WINNT) || defined(OPENSSL_SYS_WINCE) +# undef OPENSSL_SYS_UNIX +# define OPENSSL_SYS_WINDOWS +# ifndef OPENSSL_SYS_MSDOS +# define OPENSSL_SYS_MSDOS +# endif +# endif + +/* + * DLL settings. This part is a bit tough, because it's up to the + * application implementor how he or she will link the application, so it + * requires some macro to be used. + */ +# ifdef OPENSSL_SYS_WINDOWS +# ifndef OPENSSL_OPT_WINDLL +# if defined(_WINDLL) /* This is used when building OpenSSL to + * indicate that DLL linkage should be used */ +# define OPENSSL_OPT_WINDLL +# endif +# endif +# endif + +/* ------------------------------- OpenVMS -------------------------------- */ +# if defined(__VMS) || defined(VMS) || defined(OPENSSL_SYS_VMS) +# if !defined(OPENSSL_SYS_VMS) +# undef OPENSSL_SYS_UNIX +# endif +# define OPENSSL_SYS_VMS +# if defined(__DECC) +# define OPENSSL_SYS_VMS_DECC +# elif defined(__DECCXX) +# define OPENSSL_SYS_VMS_DECC +# define OPENSSL_SYS_VMS_DECCXX +# else +# define OPENSSL_SYS_VMS_NODECC +# endif +# endif + +/* -------------------------------- Unix ---------------------------------- */ +# ifdef OPENSSL_SYS_UNIX +# if defined(linux) || defined(__linux__) && !defined(OPENSSL_SYS_LINUX) +# define OPENSSL_SYS_LINUX +# endif +# if defined(_AIX) && !defined(OPENSSL_SYS_AIX) +# define OPENSSL_SYS_AIX +# endif +# endif + +/* -------------------------------- VOS ----------------------------------- */ +# if defined(__VOS__) && !defined(OPENSSL_SYS_VOS) +# define OPENSSL_SYS_VOS +# ifdef __HPPA__ +# define OPENSSL_SYS_VOS_HPPA +# endif +# ifdef __IA32__ +# define OPENSSL_SYS_VOS_IA32 +# endif +# endif + +/** + * That's it for OS-specific stuff + *****************************************************************************/ + +/* Specials for I/O an exit */ +# ifdef OPENSSL_SYS_MSDOS +# define OPENSSL_UNISTD_IO +# define OPENSSL_DECLARE_EXIT extern void exit(int); +# else +# define OPENSSL_UNISTD_IO OPENSSL_UNISTD +# define OPENSSL_DECLARE_EXIT /* declared in unistd.h */ +# endif + +/*- + * Definitions of OPENSSL_GLOBAL and OPENSSL_EXTERN, to define and declare + * certain global symbols that, with some compilers under VMS, have to be + * defined and declared explicitly with globaldef and globalref. + * Definitions of OPENSSL_EXPORT and OPENSSL_IMPORT, to define and declare + * DLL exports and imports for compilers under Win32. These are a little + * more complicated to use. Basically, for any library that exports some + * global variables, the following code must be present in the header file + * that declares them, before OPENSSL_EXTERN is used: + * + * #ifdef SOME_BUILD_FLAG_MACRO + * # undef OPENSSL_EXTERN + * # define OPENSSL_EXTERN OPENSSL_EXPORT + * #endif + * + * The default is to have OPENSSL_EXPORT, OPENSSL_EXTERN and OPENSSL_GLOBAL + * have some generally sensible values. + */ + +# if defined(OPENSSL_SYS_VMS_NODECC) +# define OPENSSL_EXPORT globalref +# define OPENSSL_EXTERN globalref +# define OPENSSL_GLOBAL globaldef +# elif defined(OPENSSL_SYS_WINDOWS) && defined(OPENSSL_OPT_WINDLL) +# define OPENSSL_EXPORT extern __declspec(dllexport) +# define OPENSSL_EXTERN extern __declspec(dllimport) +# define OPENSSL_GLOBAL +# else +# define OPENSSL_EXPORT extern +# define OPENSSL_EXTERN extern +# define OPENSSL_GLOBAL +# endif + +/*- + * Macros to allow global variables to be reached through function calls when + * required (if a shared library version requires it, for example. + * The way it's done allows definitions like this: + * + * // in foobar.c + * OPENSSL_IMPLEMENT_GLOBAL(int,foobar,0) + * // in foobar.h + * OPENSSL_DECLARE_GLOBAL(int,foobar); + * #define foobar OPENSSL_GLOBAL_REF(foobar) + */ +# ifdef OPENSSL_EXPORT_VAR_AS_FUNCTION +# define OPENSSL_IMPLEMENT_GLOBAL(type,name,value) \ + type *_shadow_##name(void) \ + { static type _hide_##name=value; return &_hide_##name; } +# define OPENSSL_DECLARE_GLOBAL(type,name) type *_shadow_##name(void) +# define OPENSSL_GLOBAL_REF(name) (*(_shadow_##name())) +# else +# define OPENSSL_IMPLEMENT_GLOBAL(type,name,value) OPENSSL_GLOBAL type _shadow_##name=value; +# define OPENSSL_DECLARE_GLOBAL(type,name) OPENSSL_EXPORT type _shadow_##name +# define OPENSSL_GLOBAL_REF(name) _shadow_##name +# endif + +# ifdef _WIN32 +# ifdef _WIN64 +# define ossl_ssize_t __int64 +# define OSSL_SSIZE_MAX _I64_MAX +# else +# define ossl_ssize_t int +# define OSSL_SSIZE_MAX INT_MAX +# endif +# endif + +# if defined(OPENSSL_SYS_UEFI) && !defined(ssize_t) +# define ossl_ssize_t int +# define OSSL_SSIZE_MAX INT_MAX +# endif + +# ifndef ossl_ssize_t +# define ossl_ssize_t ssize_t +# if defined(SSIZE_MAX) +# define OSSL_SSIZE_MAX SSIZE_MAX +# elif defined(_POSIX_SSIZE_MAX) +# define OSSL_SSIZE_MAX _POSIX_SSIZE_MAX +# endif +# endif + +# ifdef DEBUG_UNUSED +# define __owur __attribute__((__warn_unused_result__)) +# else +# define __owur +# endif + +/* Standard integer types */ +# if defined(OPENSSL_SYS_UEFI) +typedef INT8 int8_t; +typedef UINT8 uint8_t; +typedef INT16 int16_t; +typedef UINT16 uint16_t; +typedef INT32 int32_t; +typedef UINT32 uint32_t; +typedef INT64 int64_t; +typedef UINT64 uint64_t; +# define PRIu64 "%Lu" +# elif (defined(__STDC_VERSION__) && __STDC_VERSION__ >= 199901L) || \ + defined(__osf__) || defined(__sgi) || defined(__hpux) || \ + defined(OPENSSL_SYS_VMS) || defined (__OpenBSD__) +# include +# elif defined(_MSC_VER) && _MSC_VER<=1500 +/* + * minimally required typdefs for systems not supporting inttypes.h or + * stdint.h: currently just older VC++ + */ +typedef signed char int8_t; +typedef unsigned char uint8_t; +typedef short int16_t; +typedef unsigned short uint16_t; +typedef int int32_t; +typedef unsigned int uint32_t; +typedef __int64 int64_t; +typedef unsigned __int64 uint64_t; +# else +# include +# endif + +/* + * We need a format operator for some client tools for uint64_t. If inttypes.h + * isn't available or did not define it, just go with hard-coded. + */ +# ifndef PRIu64 +# ifdef SIXTY_FOUR_BIT_LONG +# define PRIu64 "lu" +# else +# define PRIu64 "llu" +# endif +# endif + +/* ossl_inline: portable inline definition usable in public headers */ +# if !defined(inline) && !defined(__cplusplus) +# if defined(__STDC_VERSION__) && __STDC_VERSION__>=199901L + /* just use inline */ +# define ossl_inline inline +# elif defined(__GNUC__) && __GNUC__>=2 +# define ossl_inline __inline__ +# elif defined(_MSC_VER) + /* + * Visual Studio: inline is available in C++ only, however + * __inline is available for C, see + * http://msdn.microsoft.com/en-us/library/z8y1yy88.aspx + */ +# define ossl_inline __inline +# else +# define ossl_inline +# endif +# else +# define ossl_inline inline +# endif + +# if defined(__STDC_VERSION__) && __STDC_VERSION__ >= 201112L +# define ossl_noreturn _Noreturn +# elif defined(__GNUC__) && __GNUC__ >= 2 +# define ossl_noreturn __attribute__((noreturn)) +# else +# define ossl_noreturn +# endif + +#ifdef __cplusplus +} +#endif +#endif diff --git a/mobile/ios/FxA/FxA/include/openssl/ebcdic.h b/mobile/ios/FxA/FxA/include/openssl/ebcdic.h new file mode 100644 index 0000000000..aa01285599 --- /dev/null +++ b/mobile/ios/FxA/FxA/include/openssl/ebcdic.h @@ -0,0 +1,33 @@ +/* + * Copyright 1999-2016 The OpenSSL Project Authors. All Rights Reserved. + * + * Licensed under the OpenSSL license (the "License"). You may not use + * this file except in compliance with the License. You can obtain a copy + * in the file LICENSE in the source distribution or at + * https://www.openssl.org/source/license.html + */ + +#ifndef HEADER_EBCDIC_H +# define HEADER_EBCDIC_H + +# include + +#ifdef __cplusplus +extern "C" { +#endif + +/* Avoid name clashes with other applications */ +# define os_toascii _openssl_os_toascii +# define os_toebcdic _openssl_os_toebcdic +# define ebcdic2ascii _openssl_ebcdic2ascii +# define ascii2ebcdic _openssl_ascii2ebcdic + +extern const unsigned char os_toascii[256]; +extern const unsigned char os_toebcdic[256]; +void *ebcdic2ascii(void *dest, const void *srce, size_t count); +void *ascii2ebcdic(void *dest, const void *srce, size_t count); + +#ifdef __cplusplus +} +#endif +#endif diff --git a/mobile/ios/FxA/FxA/include/openssl/ec.h b/mobile/ios/FxA/FxA/include/openssl/ec.h new file mode 100644 index 0000000000..656cb410e6 --- /dev/null +++ b/mobile/ios/FxA/FxA/include/openssl/ec.h @@ -0,0 +1,1581 @@ +/* + * Copyright 2002-2016 The OpenSSL Project Authors. All Rights Reserved. + * + * Licensed under the OpenSSL license (the "License"). You may not use + * this file except in compliance with the License. You can obtain a copy + * in the file LICENSE in the source distribution or at + * https://www.openssl.org/source/license.html + */ + +/* ==================================================================== + * Copyright 2002 Sun Microsystems, Inc. ALL RIGHTS RESERVED. + * + * Portions of the attached software ("Contribution") are developed by + * SUN MICROSYSTEMS, INC., and are contributed to the OpenSSL project. + * + * The Contribution is licensed pursuant to the OpenSSL open source + * license provided above. + * + * The elliptic curve binary polynomial software is originally written by + * Sheueling Chang Shantz and Douglas Stebila of Sun Microsystems Laboratories. + * + */ + +#ifndef HEADER_EC_H +# define HEADER_EC_H + +# include + +# ifndef OPENSSL_NO_EC +# include +# include +# if OPENSSL_API_COMPAT < 0x10100000L +# include +# endif +# ifdef __cplusplus +extern "C" { +# endif + +# ifndef OPENSSL_ECC_MAX_FIELD_BITS +# define OPENSSL_ECC_MAX_FIELD_BITS 661 +# endif + +/** Enum for the point conversion form as defined in X9.62 (ECDSA) + * for the encoding of a elliptic curve point (x,y) */ +typedef enum { + /** the point is encoded as z||x, where the octet z specifies + * which solution of the quadratic equation y is */ + POINT_CONVERSION_COMPRESSED = 2, + /** the point is encoded as z||x||y, where z is the octet 0x04 */ + POINT_CONVERSION_UNCOMPRESSED = 4, + /** the point is encoded as z||x||y, where the octet z specifies + * which solution of the quadratic equation y is */ + POINT_CONVERSION_HYBRID = 6 +} point_conversion_form_t; + +typedef struct ec_method_st EC_METHOD; +typedef struct ec_group_st EC_GROUP; +typedef struct ec_point_st EC_POINT; +typedef struct ecpk_parameters_st ECPKPARAMETERS; +typedef struct ec_parameters_st ECPARAMETERS; + +/********************************************************************/ +/* EC_METHODs for curves over GF(p) */ +/********************************************************************/ + +/** Returns the basic GFp ec methods which provides the basis for the + * optimized methods. + * \return EC_METHOD object + */ +const EC_METHOD *EC_GFp_simple_method(void); + +/** Returns GFp methods using montgomery multiplication. + * \return EC_METHOD object + */ +const EC_METHOD *EC_GFp_mont_method(void); + +/** Returns GFp methods using optimized methods for NIST recommended curves + * \return EC_METHOD object + */ +const EC_METHOD *EC_GFp_nist_method(void); + +# ifndef OPENSSL_NO_EC_NISTP_64_GCC_128 +/** Returns 64-bit optimized methods for nistp224 + * \return EC_METHOD object + */ +const EC_METHOD *EC_GFp_nistp224_method(void); + +/** Returns 64-bit optimized methods for nistp256 + * \return EC_METHOD object + */ +const EC_METHOD *EC_GFp_nistp256_method(void); + +/** Returns 64-bit optimized methods for nistp521 + * \return EC_METHOD object + */ +const EC_METHOD *EC_GFp_nistp521_method(void); +# endif + +# ifndef OPENSSL_NO_EC2M +/********************************************************************/ +/* EC_METHOD for curves over GF(2^m) */ +/********************************************************************/ + +/** Returns the basic GF2m ec method + * \return EC_METHOD object + */ +const EC_METHOD *EC_GF2m_simple_method(void); + +# endif + +/********************************************************************/ +/* EC_GROUP functions */ +/********************************************************************/ + +/** Creates a new EC_GROUP object + * \param meth EC_METHOD to use + * \return newly created EC_GROUP object or NULL in case of an error. + */ +EC_GROUP *EC_GROUP_new(const EC_METHOD *meth); + +/** Frees a EC_GROUP object + * \param group EC_GROUP object to be freed. + */ +void EC_GROUP_free(EC_GROUP *group); + +/** Clears and frees a EC_GROUP object + * \param group EC_GROUP object to be cleared and freed. + */ +void EC_GROUP_clear_free(EC_GROUP *group); + +/** Copies EC_GROUP objects. Note: both EC_GROUPs must use the same EC_METHOD. + * \param dst destination EC_GROUP object + * \param src source EC_GROUP object + * \return 1 on success and 0 if an error occurred. + */ +int EC_GROUP_copy(EC_GROUP *dst, const EC_GROUP *src); + +/** Creates a new EC_GROUP object and copies the copies the content + * form src to the newly created EC_KEY object + * \param src source EC_GROUP object + * \return newly created EC_GROUP object or NULL in case of an error. + */ +EC_GROUP *EC_GROUP_dup(const EC_GROUP *src); + +/** Returns the EC_METHOD of the EC_GROUP object. + * \param group EC_GROUP object + * \return EC_METHOD used in this EC_GROUP object. + */ +const EC_METHOD *EC_GROUP_method_of(const EC_GROUP *group); + +/** Returns the field type of the EC_METHOD. + * \param meth EC_METHOD object + * \return NID of the underlying field type OID. + */ +int EC_METHOD_get_field_type(const EC_METHOD *meth); + +/** Sets the generator and it's order/cofactor of a EC_GROUP object. + * \param group EC_GROUP object + * \param generator EC_POINT object with the generator. + * \param order the order of the group generated by the generator. + * \param cofactor the index of the sub-group generated by the generator + * in the group of all points on the elliptic curve. + * \return 1 on success and 0 if an error occurred + */ +int EC_GROUP_set_generator(EC_GROUP *group, const EC_POINT *generator, + const BIGNUM *order, const BIGNUM *cofactor); + +/** Returns the generator of a EC_GROUP object. + * \param group EC_GROUP object + * \return the currently used generator (possibly NULL). + */ +const EC_POINT *EC_GROUP_get0_generator(const EC_GROUP *group); + +/** Returns the montgomery data for order(Generator) + * \param group EC_GROUP object + * \return the currently used montgomery data (possibly NULL). +*/ +BN_MONT_CTX *EC_GROUP_get_mont_data(const EC_GROUP *group); + +/** Gets the order of a EC_GROUP + * \param group EC_GROUP object + * \param order BIGNUM to which the order is copied + * \param ctx unused + * \return 1 on success and 0 if an error occurred + */ +int EC_GROUP_get_order(const EC_GROUP *group, BIGNUM *order, BN_CTX *ctx); + +/** Gets the order of an EC_GROUP + * \param group EC_GROUP object + * \return the group order + */ +const BIGNUM *EC_GROUP_get0_order(const EC_GROUP *group); + +/** Gets the number of bits of the order of an EC_GROUP + * \param group EC_GROUP object + * \return number of bits of group order. + */ +int EC_GROUP_order_bits(const EC_GROUP *group); + +/** Gets the cofactor of a EC_GROUP + * \param group EC_GROUP object + * \param cofactor BIGNUM to which the cofactor is copied + * \param ctx unused + * \return 1 on success and 0 if an error occurred + */ +int EC_GROUP_get_cofactor(const EC_GROUP *group, BIGNUM *cofactor, + BN_CTX *ctx); + +/** Gets the cofactor of an EC_GROUP + * \param group EC_GROUP object + * \return the group cofactor + */ +const BIGNUM *EC_GROUP_get0_cofactor(const EC_GROUP *group); + +/** Sets the name of a EC_GROUP object + * \param group EC_GROUP object + * \param nid NID of the curve name OID + */ +void EC_GROUP_set_curve_name(EC_GROUP *group, int nid); + +/** Returns the curve name of a EC_GROUP object + * \param group EC_GROUP object + * \return NID of the curve name OID or 0 if not set. + */ +int EC_GROUP_get_curve_name(const EC_GROUP *group); + +void EC_GROUP_set_asn1_flag(EC_GROUP *group, int flag); +int EC_GROUP_get_asn1_flag(const EC_GROUP *group); + +void EC_GROUP_set_point_conversion_form(EC_GROUP *group, + point_conversion_form_t form); +point_conversion_form_t EC_GROUP_get_point_conversion_form(const EC_GROUP *); + +unsigned char *EC_GROUP_get0_seed(const EC_GROUP *x); +size_t EC_GROUP_get_seed_len(const EC_GROUP *); +size_t EC_GROUP_set_seed(EC_GROUP *, const unsigned char *, size_t len); + +/** Sets the parameter of a ec over GFp defined by y^2 = x^3 + a*x + b + * \param group EC_GROUP object + * \param p BIGNUM with the prime number + * \param a BIGNUM with parameter a of the equation + * \param b BIGNUM with parameter b of the equation + * \param ctx BN_CTX object (optional) + * \return 1 on success and 0 if an error occurred + */ +int EC_GROUP_set_curve_GFp(EC_GROUP *group, const BIGNUM *p, const BIGNUM *a, + const BIGNUM *b, BN_CTX *ctx); + +/** Gets the parameter of the ec over GFp defined by y^2 = x^3 + a*x + b + * \param group EC_GROUP object + * \param p BIGNUM for the prime number + * \param a BIGNUM for parameter a of the equation + * \param b BIGNUM for parameter b of the equation + * \param ctx BN_CTX object (optional) + * \return 1 on success and 0 if an error occurred + */ +int EC_GROUP_get_curve_GFp(const EC_GROUP *group, BIGNUM *p, BIGNUM *a, + BIGNUM *b, BN_CTX *ctx); + +# ifndef OPENSSL_NO_EC2M +/** Sets the parameter of a ec over GF2m defined by y^2 + x*y = x^3 + a*x^2 + b + * \param group EC_GROUP object + * \param p BIGNUM with the polynomial defining the underlying field + * \param a BIGNUM with parameter a of the equation + * \param b BIGNUM with parameter b of the equation + * \param ctx BN_CTX object (optional) + * \return 1 on success and 0 if an error occurred + */ +int EC_GROUP_set_curve_GF2m(EC_GROUP *group, const BIGNUM *p, const BIGNUM *a, + const BIGNUM *b, BN_CTX *ctx); + +/** Gets the parameter of the ec over GF2m defined by y^2 + x*y = x^3 + a*x^2 + b + * \param group EC_GROUP object + * \param p BIGNUM for the polynomial defining the underlying field + * \param a BIGNUM for parameter a of the equation + * \param b BIGNUM for parameter b of the equation + * \param ctx BN_CTX object (optional) + * \return 1 on success and 0 if an error occurred + */ +int EC_GROUP_get_curve_GF2m(const EC_GROUP *group, BIGNUM *p, BIGNUM *a, + BIGNUM *b, BN_CTX *ctx); +# endif +/** Returns the number of bits needed to represent a field element + * \param group EC_GROUP object + * \return number of bits needed to represent a field element + */ +int EC_GROUP_get_degree(const EC_GROUP *group); + +/** Checks whether the parameter in the EC_GROUP define a valid ec group + * \param group EC_GROUP object + * \param ctx BN_CTX object (optional) + * \return 1 if group is a valid ec group and 0 otherwise + */ +int EC_GROUP_check(const EC_GROUP *group, BN_CTX *ctx); + +/** Checks whether the discriminant of the elliptic curve is zero or not + * \param group EC_GROUP object + * \param ctx BN_CTX object (optional) + * \return 1 if the discriminant is not zero and 0 otherwise + */ +int EC_GROUP_check_discriminant(const EC_GROUP *group, BN_CTX *ctx); + +/** Compares two EC_GROUP objects + * \param a first EC_GROUP object + * \param b second EC_GROUP object + * \param ctx BN_CTX object (optional) + * \return 0 if the groups are equal, 1 if not, or -1 on error + */ +int EC_GROUP_cmp(const EC_GROUP *a, const EC_GROUP *b, BN_CTX *ctx); + +/* + * EC_GROUP_new_GF*() calls EC_GROUP_new() and EC_GROUP_set_GF*() after + * choosing an appropriate EC_METHOD + */ + +/** Creates a new EC_GROUP object with the specified parameters defined + * over GFp (defined by the equation y^2 = x^3 + a*x + b) + * \param p BIGNUM with the prime number + * \param a BIGNUM with the parameter a of the equation + * \param b BIGNUM with the parameter b of the equation + * \param ctx BN_CTX object (optional) + * \return newly created EC_GROUP object with the specified parameters + */ +EC_GROUP *EC_GROUP_new_curve_GFp(const BIGNUM *p, const BIGNUM *a, + const BIGNUM *b, BN_CTX *ctx); +# ifndef OPENSSL_NO_EC2M +/** Creates a new EC_GROUP object with the specified parameters defined + * over GF2m (defined by the equation y^2 + x*y = x^3 + a*x^2 + b) + * \param p BIGNUM with the polynomial defining the underlying field + * \param a BIGNUM with the parameter a of the equation + * \param b BIGNUM with the parameter b of the equation + * \param ctx BN_CTX object (optional) + * \return newly created EC_GROUP object with the specified parameters + */ +EC_GROUP *EC_GROUP_new_curve_GF2m(const BIGNUM *p, const BIGNUM *a, + const BIGNUM *b, BN_CTX *ctx); +# endif + +/** Creates a EC_GROUP object with a curve specified by a NID + * \param nid NID of the OID of the curve name + * \return newly created EC_GROUP object with specified curve or NULL + * if an error occurred + */ +EC_GROUP *EC_GROUP_new_by_curve_name(int nid); + +/** Creates a new EC_GROUP object from an ECPARAMETERS object + * \param params pointer to the ECPARAMETERS object + * \return newly created EC_GROUP object with specified curve or NULL + * if an error occurred + */ +EC_GROUP *EC_GROUP_new_from_ecparameters(const ECPARAMETERS *params); + +/** Creates an ECPARAMETERS object for the the given EC_GROUP object. + * \param group pointer to the EC_GROUP object + * \param params pointer to an existing ECPARAMETERS object or NULL + * \return pointer to the new ECPARAMETERS object or NULL + * if an error occurred. + */ +ECPARAMETERS *EC_GROUP_get_ecparameters(const EC_GROUP *group, + ECPARAMETERS *params); + +/** Creates a new EC_GROUP object from an ECPKPARAMETERS object + * \param params pointer to an existing ECPKPARAMETERS object, or NULL + * \return newly created EC_GROUP object with specified curve, or NULL + * if an error occurred + */ +EC_GROUP *EC_GROUP_new_from_ecpkparameters(const ECPKPARAMETERS *params); + +/** Creates an ECPKPARAMETERS object for the the given EC_GROUP object. + * \param group pointer to the EC_GROUP object + * \param params pointer to an existing ECPKPARAMETERS object or NULL + * \return pointer to the new ECPKPARAMETERS object or NULL + * if an error occurred. + */ +ECPKPARAMETERS *EC_GROUP_get_ecpkparameters(const EC_GROUP *group, + ECPKPARAMETERS *params); + +/********************************************************************/ +/* handling of internal curves */ +/********************************************************************/ + +typedef struct { + int nid; + const char *comment; +} EC_builtin_curve; + +/* + * EC_builtin_curves(EC_builtin_curve *r, size_t size) returns number of all + * available curves or zero if a error occurred. In case r is not zero, + * nitems EC_builtin_curve structures are filled with the data of the first + * nitems internal groups + */ +size_t EC_get_builtin_curves(EC_builtin_curve *r, size_t nitems); + +const char *EC_curve_nid2nist(int nid); +int EC_curve_nist2nid(const char *name); + +/********************************************************************/ +/* EC_POINT functions */ +/********************************************************************/ + +/** Creates a new EC_POINT object for the specified EC_GROUP + * \param group EC_GROUP the underlying EC_GROUP object + * \return newly created EC_POINT object or NULL if an error occurred + */ +EC_POINT *EC_POINT_new(const EC_GROUP *group); + +/** Frees a EC_POINT object + * \param point EC_POINT object to be freed + */ +void EC_POINT_free(EC_POINT *point); + +/** Clears and frees a EC_POINT object + * \param point EC_POINT object to be cleared and freed + */ +void EC_POINT_clear_free(EC_POINT *point); + +/** Copies EC_POINT object + * \param dst destination EC_POINT object + * \param src source EC_POINT object + * \return 1 on success and 0 if an error occurred + */ +int EC_POINT_copy(EC_POINT *dst, const EC_POINT *src); + +/** Creates a new EC_POINT object and copies the content of the supplied + * EC_POINT + * \param src source EC_POINT object + * \param group underlying the EC_GROUP object + * \return newly created EC_POINT object or NULL if an error occurred + */ +EC_POINT *EC_POINT_dup(const EC_POINT *src, const EC_GROUP *group); + +/** Returns the EC_METHOD used in EC_POINT object + * \param point EC_POINT object + * \return the EC_METHOD used + */ +const EC_METHOD *EC_POINT_method_of(const EC_POINT *point); + +/** Sets a point to infinity (neutral element) + * \param group underlying EC_GROUP object + * \param point EC_POINT to set to infinity + * \return 1 on success and 0 if an error occurred + */ +int EC_POINT_set_to_infinity(const EC_GROUP *group, EC_POINT *point); + +/** Sets the jacobian projective coordinates of a EC_POINT over GFp + * \param group underlying EC_GROUP object + * \param p EC_POINT object + * \param x BIGNUM with the x-coordinate + * \param y BIGNUM with the y-coordinate + * \param z BIGNUM with the z-coordinate + * \param ctx BN_CTX object (optional) + * \return 1 on success and 0 if an error occurred + */ +int EC_POINT_set_Jprojective_coordinates_GFp(const EC_GROUP *group, + EC_POINT *p, const BIGNUM *x, + const BIGNUM *y, const BIGNUM *z, + BN_CTX *ctx); + +/** Gets the jacobian projective coordinates of a EC_POINT over GFp + * \param group underlying EC_GROUP object + * \param p EC_POINT object + * \param x BIGNUM for the x-coordinate + * \param y BIGNUM for the y-coordinate + * \param z BIGNUM for the z-coordinate + * \param ctx BN_CTX object (optional) + * \return 1 on success and 0 if an error occurred + */ +int EC_POINT_get_Jprojective_coordinates_GFp(const EC_GROUP *group, + const EC_POINT *p, BIGNUM *x, + BIGNUM *y, BIGNUM *z, + BN_CTX *ctx); + +/** Sets the affine coordinates of a EC_POINT over GFp + * \param group underlying EC_GROUP object + * \param p EC_POINT object + * \param x BIGNUM with the x-coordinate + * \param y BIGNUM with the y-coordinate + * \param ctx BN_CTX object (optional) + * \return 1 on success and 0 if an error occurred + */ +int EC_POINT_set_affine_coordinates_GFp(const EC_GROUP *group, EC_POINT *p, + const BIGNUM *x, const BIGNUM *y, + BN_CTX *ctx); + +/** Gets the affine coordinates of a EC_POINT over GFp + * \param group underlying EC_GROUP object + * \param p EC_POINT object + * \param x BIGNUM for the x-coordinate + * \param y BIGNUM for the y-coordinate + * \param ctx BN_CTX object (optional) + * \return 1 on success and 0 if an error occurred + */ +int EC_POINT_get_affine_coordinates_GFp(const EC_GROUP *group, + const EC_POINT *p, BIGNUM *x, + BIGNUM *y, BN_CTX *ctx); + +/** Sets the x9.62 compressed coordinates of a EC_POINT over GFp + * \param group underlying EC_GROUP object + * \param p EC_POINT object + * \param x BIGNUM with x-coordinate + * \param y_bit integer with the y-Bit (either 0 or 1) + * \param ctx BN_CTX object (optional) + * \return 1 on success and 0 if an error occurred + */ +int EC_POINT_set_compressed_coordinates_GFp(const EC_GROUP *group, + EC_POINT *p, const BIGNUM *x, + int y_bit, BN_CTX *ctx); +# ifndef OPENSSL_NO_EC2M +/** Sets the affine coordinates of a EC_POINT over GF2m + * \param group underlying EC_GROUP object + * \param p EC_POINT object + * \param x BIGNUM with the x-coordinate + * \param y BIGNUM with the y-coordinate + * \param ctx BN_CTX object (optional) + * \return 1 on success and 0 if an error occurred + */ +int EC_POINT_set_affine_coordinates_GF2m(const EC_GROUP *group, EC_POINT *p, + const BIGNUM *x, const BIGNUM *y, + BN_CTX *ctx); + +/** Gets the affine coordinates of a EC_POINT over GF2m + * \param group underlying EC_GROUP object + * \param p EC_POINT object + * \param x BIGNUM for the x-coordinate + * \param y BIGNUM for the y-coordinate + * \param ctx BN_CTX object (optional) + * \return 1 on success and 0 if an error occurred + */ +int EC_POINT_get_affine_coordinates_GF2m(const EC_GROUP *group, + const EC_POINT *p, BIGNUM *x, + BIGNUM *y, BN_CTX *ctx); + +/** Sets the x9.62 compressed coordinates of a EC_POINT over GF2m + * \param group underlying EC_GROUP object + * \param p EC_POINT object + * \param x BIGNUM with x-coordinate + * \param y_bit integer with the y-Bit (either 0 or 1) + * \param ctx BN_CTX object (optional) + * \return 1 on success and 0 if an error occurred + */ +int EC_POINT_set_compressed_coordinates_GF2m(const EC_GROUP *group, + EC_POINT *p, const BIGNUM *x, + int y_bit, BN_CTX *ctx); +# endif +/** Encodes a EC_POINT object to a octet string + * \param group underlying EC_GROUP object + * \param p EC_POINT object + * \param form point conversion form + * \param buf memory buffer for the result. If NULL the function returns + * required buffer size. + * \param len length of the memory buffer + * \param ctx BN_CTX object (optional) + * \return the length of the encoded octet string or 0 if an error occurred + */ +size_t EC_POINT_point2oct(const EC_GROUP *group, const EC_POINT *p, + point_conversion_form_t form, + unsigned char *buf, size_t len, BN_CTX *ctx); + +/** Decodes a EC_POINT from a octet string + * \param group underlying EC_GROUP object + * \param p EC_POINT object + * \param buf memory buffer with the encoded ec point + * \param len length of the encoded ec point + * \param ctx BN_CTX object (optional) + * \return 1 on success and 0 if an error occurred + */ +int EC_POINT_oct2point(const EC_GROUP *group, EC_POINT *p, + const unsigned char *buf, size_t len, BN_CTX *ctx); + +/** Encodes an EC_POINT object to an allocated octet string + * \param group underlying EC_GROUP object + * \param point EC_POINT object + * \param form point conversion form + * \param pbuf returns pointer to allocated buffer + * \param len length of the memory buffer + * \param ctx BN_CTX object (optional) + * \return the length of the encoded octet string or 0 if an error occurred + */ + +size_t EC_POINT_point2buf(const EC_GROUP *group, const EC_POINT *point, + point_conversion_form_t form, + unsigned char **pbuf, BN_CTX *ctx); + +/* other interfaces to point2oct/oct2point: */ +BIGNUM *EC_POINT_point2bn(const EC_GROUP *, const EC_POINT *, + point_conversion_form_t form, BIGNUM *, BN_CTX *); +EC_POINT *EC_POINT_bn2point(const EC_GROUP *, const BIGNUM *, + EC_POINT *, BN_CTX *); +char *EC_POINT_point2hex(const EC_GROUP *, const EC_POINT *, + point_conversion_form_t form, BN_CTX *); +EC_POINT *EC_POINT_hex2point(const EC_GROUP *, const char *, + EC_POINT *, BN_CTX *); + +/********************************************************************/ +/* functions for doing EC_POINT arithmetic */ +/********************************************************************/ + +/** Computes the sum of two EC_POINT + * \param group underlying EC_GROUP object + * \param r EC_POINT object for the result (r = a + b) + * \param a EC_POINT object with the first summand + * \param b EC_POINT object with the second summand + * \param ctx BN_CTX object (optional) + * \return 1 on success and 0 if an error occurred + */ +int EC_POINT_add(const EC_GROUP *group, EC_POINT *r, const EC_POINT *a, + const EC_POINT *b, BN_CTX *ctx); + +/** Computes the double of a EC_POINT + * \param group underlying EC_GROUP object + * \param r EC_POINT object for the result (r = 2 * a) + * \param a EC_POINT object + * \param ctx BN_CTX object (optional) + * \return 1 on success and 0 if an error occurred + */ +int EC_POINT_dbl(const EC_GROUP *group, EC_POINT *r, const EC_POINT *a, + BN_CTX *ctx); + +/** Computes the inverse of a EC_POINT + * \param group underlying EC_GROUP object + * \param a EC_POINT object to be inverted (it's used for the result as well) + * \param ctx BN_CTX object (optional) + * \return 1 on success and 0 if an error occurred + */ +int EC_POINT_invert(const EC_GROUP *group, EC_POINT *a, BN_CTX *ctx); + +/** Checks whether the point is the neutral element of the group + * \param group the underlying EC_GROUP object + * \param p EC_POINT object + * \return 1 if the point is the neutral element and 0 otherwise + */ +int EC_POINT_is_at_infinity(const EC_GROUP *group, const EC_POINT *p); + +/** Checks whether the point is on the curve + * \param group underlying EC_GROUP object + * \param point EC_POINT object to check + * \param ctx BN_CTX object (optional) + * \return 1 if the point is on the curve, 0 if not, or -1 on error + */ +int EC_POINT_is_on_curve(const EC_GROUP *group, const EC_POINT *point, + BN_CTX *ctx); + +/** Compares two EC_POINTs + * \param group underlying EC_GROUP object + * \param a first EC_POINT object + * \param b second EC_POINT object + * \param ctx BN_CTX object (optional) + * \return 1 if the points are not equal, 0 if they are, or -1 on error + */ +int EC_POINT_cmp(const EC_GROUP *group, const EC_POINT *a, const EC_POINT *b, + BN_CTX *ctx); + +int EC_POINT_make_affine(const EC_GROUP *group, EC_POINT *point, BN_CTX *ctx); +int EC_POINTs_make_affine(const EC_GROUP *group, size_t num, + EC_POINT *points[], BN_CTX *ctx); + +/** Computes r = generator * n + sum_{i=0}^{num-1} p[i] * m[i] + * \param group underlying EC_GROUP object + * \param r EC_POINT object for the result + * \param n BIGNUM with the multiplier for the group generator (optional) + * \param num number further summands + * \param p array of size num of EC_POINT objects + * \param m array of size num of BIGNUM objects + * \param ctx BN_CTX object (optional) + * \return 1 on success and 0 if an error occurred + */ +int EC_POINTs_mul(const EC_GROUP *group, EC_POINT *r, const BIGNUM *n, + size_t num, const EC_POINT *p[], const BIGNUM *m[], + BN_CTX *ctx); + +/** Computes r = generator * n + q * m + * \param group underlying EC_GROUP object + * \param r EC_POINT object for the result + * \param n BIGNUM with the multiplier for the group generator (optional) + * \param q EC_POINT object with the first factor of the second summand + * \param m BIGNUM with the second factor of the second summand + * \param ctx BN_CTX object (optional) + * \return 1 on success and 0 if an error occurred + */ +int EC_POINT_mul(const EC_GROUP *group, EC_POINT *r, const BIGNUM *n, + const EC_POINT *q, const BIGNUM *m, BN_CTX *ctx); + +/** Stores multiples of generator for faster point multiplication + * \param group EC_GROUP object + * \param ctx BN_CTX object (optional) + * \return 1 on success and 0 if an error occurred + */ +int EC_GROUP_precompute_mult(EC_GROUP *group, BN_CTX *ctx); + +/** Reports whether a precomputation has been done + * \param group EC_GROUP object + * \return 1 if a pre-computation has been done and 0 otherwise + */ +int EC_GROUP_have_precompute_mult(const EC_GROUP *group); + +/********************************************************************/ +/* ASN1 stuff */ +/********************************************************************/ + +DECLARE_ASN1_ITEM(ECPKPARAMETERS) +DECLARE_ASN1_ALLOC_FUNCTIONS(ECPKPARAMETERS) +DECLARE_ASN1_ITEM(ECPARAMETERS) +DECLARE_ASN1_ALLOC_FUNCTIONS(ECPARAMETERS) + +/* + * EC_GROUP_get_basis_type() returns the NID of the basis type used to + * represent the field elements + */ +int EC_GROUP_get_basis_type(const EC_GROUP *); +# ifndef OPENSSL_NO_EC2M +int EC_GROUP_get_trinomial_basis(const EC_GROUP *, unsigned int *k); +int EC_GROUP_get_pentanomial_basis(const EC_GROUP *, unsigned int *k1, + unsigned int *k2, unsigned int *k3); +# endif + +# define OPENSSL_EC_EXPLICIT_CURVE 0x000 +# define OPENSSL_EC_NAMED_CURVE 0x001 + +EC_GROUP *d2i_ECPKParameters(EC_GROUP **, const unsigned char **in, long len); +int i2d_ECPKParameters(const EC_GROUP *, unsigned char **out); + +# define d2i_ECPKParameters_bio(bp,x) ASN1_d2i_bio_of(EC_GROUP,NULL,d2i_ECPKParameters,bp,x) +# define i2d_ECPKParameters_bio(bp,x) ASN1_i2d_bio_of_const(EC_GROUP,i2d_ECPKParameters,bp,x) +# define d2i_ECPKParameters_fp(fp,x) (EC_GROUP *)ASN1_d2i_fp(NULL, \ + (char *(*)())d2i_ECPKParameters,(fp),(unsigned char **)(x)) +# define i2d_ECPKParameters_fp(fp,x) ASN1_i2d_fp(i2d_ECPKParameters,(fp), \ + (unsigned char *)(x)) + +int ECPKParameters_print(BIO *bp, const EC_GROUP *x, int off); +# ifndef OPENSSL_NO_STDIO +int ECPKParameters_print_fp(FILE *fp, const EC_GROUP *x, int off); +# endif + +/********************************************************************/ +/* EC_KEY functions */ +/********************************************************************/ + +/* some values for the encoding_flag */ +# define EC_PKEY_NO_PARAMETERS 0x001 +# define EC_PKEY_NO_PUBKEY 0x002 + +/* some values for the flags field */ +# define EC_FLAG_NON_FIPS_ALLOW 0x1 +# define EC_FLAG_FIPS_CHECKED 0x2 +# define EC_FLAG_COFACTOR_ECDH 0x1000 + +/** Creates a new EC_KEY object. + * \return EC_KEY object or NULL if an error occurred. + */ +EC_KEY *EC_KEY_new(void); + +int EC_KEY_get_flags(const EC_KEY *key); + +void EC_KEY_set_flags(EC_KEY *key, int flags); + +void EC_KEY_clear_flags(EC_KEY *key, int flags); + +/** Creates a new EC_KEY object using a named curve as underlying + * EC_GROUP object. + * \param nid NID of the named curve. + * \return EC_KEY object or NULL if an error occurred. + */ +EC_KEY *EC_KEY_new_by_curve_name(int nid); + +/** Frees a EC_KEY object. + * \param key EC_KEY object to be freed. + */ +void EC_KEY_free(EC_KEY *key); + +/** Copies a EC_KEY object. + * \param dst destination EC_KEY object + * \param src src EC_KEY object + * \return dst or NULL if an error occurred. + */ +EC_KEY *EC_KEY_copy(EC_KEY *dst, const EC_KEY *src); + +/** Creates a new EC_KEY object and copies the content from src to it. + * \param src the source EC_KEY object + * \return newly created EC_KEY object or NULL if an error occurred. + */ +EC_KEY *EC_KEY_dup(const EC_KEY *src); + +/** Increases the internal reference count of a EC_KEY object. + * \param key EC_KEY object + * \return 1 on success and 0 if an error occurred. + */ +int EC_KEY_up_ref(EC_KEY *key); + +/** Returns the EC_GROUP object of a EC_KEY object + * \param key EC_KEY object + * \return the EC_GROUP object (possibly NULL). + */ +const EC_GROUP *EC_KEY_get0_group(const EC_KEY *key); + +/** Sets the EC_GROUP of a EC_KEY object. + * \param key EC_KEY object + * \param group EC_GROUP to use in the EC_KEY object (note: the EC_KEY + * object will use an own copy of the EC_GROUP). + * \return 1 on success and 0 if an error occurred. + */ +int EC_KEY_set_group(EC_KEY *key, const EC_GROUP *group); + +/** Returns the private key of a EC_KEY object. + * \param key EC_KEY object + * \return a BIGNUM with the private key (possibly NULL). + */ +const BIGNUM *EC_KEY_get0_private_key(const EC_KEY *key); + +/** Sets the private key of a EC_KEY object. + * \param key EC_KEY object + * \param prv BIGNUM with the private key (note: the EC_KEY object + * will use an own copy of the BIGNUM). + * \return 1 on success and 0 if an error occurred. + */ +int EC_KEY_set_private_key(EC_KEY *key, const BIGNUM *prv); + +/** Returns the public key of a EC_KEY object. + * \param key the EC_KEY object + * \return a EC_POINT object with the public key (possibly NULL) + */ +const EC_POINT *EC_KEY_get0_public_key(const EC_KEY *key); + +/** Sets the public key of a EC_KEY object. + * \param key EC_KEY object + * \param pub EC_POINT object with the public key (note: the EC_KEY object + * will use an own copy of the EC_POINT object). + * \return 1 on success and 0 if an error occurred. + */ +int EC_KEY_set_public_key(EC_KEY *key, const EC_POINT *pub); + +unsigned EC_KEY_get_enc_flags(const EC_KEY *key); +void EC_KEY_set_enc_flags(EC_KEY *eckey, unsigned int flags); +point_conversion_form_t EC_KEY_get_conv_form(const EC_KEY *key); +void EC_KEY_set_conv_form(EC_KEY *eckey, point_conversion_form_t cform); + +#define EC_KEY_get_ex_new_index(l, p, newf, dupf, freef) \ + CRYPTO_get_ex_new_index(CRYPTO_EX_INDEX_EC_KEY, l, p, newf, dupf, freef) +int EC_KEY_set_ex_data(EC_KEY *key, int idx, void *arg); +void *EC_KEY_get_ex_data(const EC_KEY *key, int idx); + +/* wrapper functions for the underlying EC_GROUP object */ +void EC_KEY_set_asn1_flag(EC_KEY *eckey, int asn1_flag); + +/** Creates a table of pre-computed multiples of the generator to + * accelerate further EC_KEY operations. + * \param key EC_KEY object + * \param ctx BN_CTX object (optional) + * \return 1 on success and 0 if an error occurred. + */ +int EC_KEY_precompute_mult(EC_KEY *key, BN_CTX *ctx); + +/** Creates a new ec private (and optional a new public) key. + * \param key EC_KEY object + * \return 1 on success and 0 if an error occurred. + */ +int EC_KEY_generate_key(EC_KEY *key); + +/** Verifies that a private and/or public key is valid. + * \param key the EC_KEY object + * \return 1 on success and 0 otherwise. + */ +int EC_KEY_check_key(const EC_KEY *key); + +/** Indicates if an EC_KEY can be used for signing. + * \param key the EC_KEY object + * \return 1 if can can sign and 0 otherwise. + */ +int EC_KEY_can_sign(const EC_KEY *eckey); + +/** Sets a public key from affine coordinates performing + * necessary NIST PKV tests. + * \param key the EC_KEY object + * \param x public key x coordinate + * \param y public key y coordinate + * \return 1 on success and 0 otherwise. + */ +int EC_KEY_set_public_key_affine_coordinates(EC_KEY *key, BIGNUM *x, + BIGNUM *y); + +/** Encodes an EC_KEY public key to an allocated octet string + * \param key key to encode + * \param form point conversion form + * \param pbuf returns pointer to allocated buffer + * \param len length of the memory buffer + * \param ctx BN_CTX object (optional) + * \return the length of the encoded octet string or 0 if an error occurred + */ + +size_t EC_KEY_key2buf(const EC_KEY *key, point_conversion_form_t form, + unsigned char **pbuf, BN_CTX *ctx); + +/** Decodes a EC_KEY public key from a octet string + * \param key key to decode + * \param buf memory buffer with the encoded ec point + * \param len length of the encoded ec point + * \param ctx BN_CTX object (optional) + * \return 1 on success and 0 if an error occurred + */ + +int EC_KEY_oct2key(EC_KEY *key, const unsigned char *buf, size_t len, + BN_CTX *ctx); + +/** Decodes an EC_KEY private key from an octet string + * \param key key to decode + * \param buf memory buffer with the encoded private key + * \param len length of the encoded key + * \return 1 on success and 0 if an error occurred + */ + +int EC_KEY_oct2priv(EC_KEY *key, const unsigned char *buf, size_t len); + +/** Encodes a EC_KEY private key to an octet string + * \param key key to encode + * \param buf memory buffer for the result. If NULL the function returns + * required buffer size. + * \param len length of the memory buffer + * \return the length of the encoded octet string or 0 if an error occurred + */ + +size_t EC_KEY_priv2oct(const EC_KEY *key, unsigned char *buf, size_t len); + +/** Encodes an EC_KEY private key to an allocated octet string + * \param key key to encode + * \param pbuf returns pointer to allocated buffer + * \return the length of the encoded octet string or 0 if an error occurred + */ + +size_t EC_KEY_priv2buf(const EC_KEY *eckey, unsigned char **pbuf); + +/********************************************************************/ +/* de- and encoding functions for SEC1 ECPrivateKey */ +/********************************************************************/ + +/** Decodes a private key from a memory buffer. + * \param key a pointer to a EC_KEY object which should be used (or NULL) + * \param in pointer to memory with the DER encoded private key + * \param len length of the DER encoded private key + * \return the decoded private key or NULL if an error occurred. + */ +EC_KEY *d2i_ECPrivateKey(EC_KEY **key, const unsigned char **in, long len); + +/** Encodes a private key object and stores the result in a buffer. + * \param key the EC_KEY object to encode + * \param out the buffer for the result (if NULL the function returns number + * of bytes needed). + * \return 1 on success and 0 if an error occurred. + */ +int i2d_ECPrivateKey(EC_KEY *key, unsigned char **out); + +/********************************************************************/ +/* de- and encoding functions for EC parameters */ +/********************************************************************/ + +/** Decodes ec parameter from a memory buffer. + * \param key a pointer to a EC_KEY object which should be used (or NULL) + * \param in pointer to memory with the DER encoded ec parameters + * \param len length of the DER encoded ec parameters + * \return a EC_KEY object with the decoded parameters or NULL if an error + * occurred. + */ +EC_KEY *d2i_ECParameters(EC_KEY **key, const unsigned char **in, long len); + +/** Encodes ec parameter and stores the result in a buffer. + * \param key the EC_KEY object with ec parameters to encode + * \param out the buffer for the result (if NULL the function returns number + * of bytes needed). + * \return 1 on success and 0 if an error occurred. + */ +int i2d_ECParameters(EC_KEY *key, unsigned char **out); + +/********************************************************************/ +/* de- and encoding functions for EC public key */ +/* (octet string, not DER -- hence 'o2i' and 'i2o') */ +/********************************************************************/ + +/** Decodes a ec public key from a octet string. + * \param key a pointer to a EC_KEY object which should be used + * \param in memory buffer with the encoded public key + * \param len length of the encoded public key + * \return EC_KEY object with decoded public key or NULL if an error + * occurred. + */ +EC_KEY *o2i_ECPublicKey(EC_KEY **key, const unsigned char **in, long len); + +/** Encodes a ec public key in an octet string. + * \param key the EC_KEY object with the public key + * \param out the buffer for the result (if NULL the function returns number + * of bytes needed). + * \return 1 on success and 0 if an error occurred + */ +int i2o_ECPublicKey(const EC_KEY *key, unsigned char **out); + +/** Prints out the ec parameters on human readable form. + * \param bp BIO object to which the information is printed + * \param key EC_KEY object + * \return 1 on success and 0 if an error occurred + */ +int ECParameters_print(BIO *bp, const EC_KEY *key); + +/** Prints out the contents of a EC_KEY object + * \param bp BIO object to which the information is printed + * \param key EC_KEY object + * \param off line offset + * \return 1 on success and 0 if an error occurred + */ +int EC_KEY_print(BIO *bp, const EC_KEY *key, int off); + +# ifndef OPENSSL_NO_STDIO +/** Prints out the ec parameters on human readable form. + * \param fp file descriptor to which the information is printed + * \param key EC_KEY object + * \return 1 on success and 0 if an error occurred + */ +int ECParameters_print_fp(FILE *fp, const EC_KEY *key); + +/** Prints out the contents of a EC_KEY object + * \param fp file descriptor to which the information is printed + * \param key EC_KEY object + * \param off line offset + * \return 1 on success and 0 if an error occurred + */ +int EC_KEY_print_fp(FILE *fp, const EC_KEY *key, int off); + +# endif + +const EC_KEY_METHOD *EC_KEY_OpenSSL(void); +const EC_KEY_METHOD *EC_KEY_get_default_method(void); +void EC_KEY_set_default_method(const EC_KEY_METHOD *meth); +const EC_KEY_METHOD *EC_KEY_get_method(const EC_KEY *key); +int EC_KEY_set_method(EC_KEY *key, const EC_KEY_METHOD *meth); +EC_KEY *EC_KEY_new_method(ENGINE *engine); + +int ECDH_KDF_X9_62(unsigned char *out, size_t outlen, + const unsigned char *Z, size_t Zlen, + const unsigned char *sinfo, size_t sinfolen, + const EVP_MD *md); + +int ECDH_compute_key(void *out, size_t outlen, const EC_POINT *pub_key, + const EC_KEY *ecdh, + void *(*KDF) (const void *in, size_t inlen, + void *out, size_t *outlen)); + +typedef struct ECDSA_SIG_st ECDSA_SIG; + +/** Allocates and initialize a ECDSA_SIG structure + * \return pointer to a ECDSA_SIG structure or NULL if an error occurred + */ +ECDSA_SIG *ECDSA_SIG_new(void); + +/** frees a ECDSA_SIG structure + * \param sig pointer to the ECDSA_SIG structure + */ +void ECDSA_SIG_free(ECDSA_SIG *sig); + +/** DER encode content of ECDSA_SIG object (note: this function modifies *pp + * (*pp += length of the DER encoded signature)). + * \param sig pointer to the ECDSA_SIG object + * \param pp pointer to a unsigned char pointer for the output or NULL + * \return the length of the DER encoded ECDSA_SIG object or 0 + */ +int i2d_ECDSA_SIG(const ECDSA_SIG *sig, unsigned char **pp); + +/** Decodes a DER encoded ECDSA signature (note: this function changes *pp + * (*pp += len)). + * \param sig pointer to ECDSA_SIG pointer (may be NULL) + * \param pp memory buffer with the DER encoded signature + * \param len length of the buffer + * \return pointer to the decoded ECDSA_SIG structure (or NULL) + */ +ECDSA_SIG *d2i_ECDSA_SIG(ECDSA_SIG **sig, const unsigned char **pp, long len); + +/** Accessor for r and s fields of ECDSA_SIG + * \param sig pointer to ECDSA_SIG pointer + * \param pr pointer to BIGNUM pointer for r (may be NULL) + * \param ps pointer to BIGNUM pointer for s (may be NULL) + */ +void ECDSA_SIG_get0(const ECDSA_SIG *sig, const BIGNUM **pr, const BIGNUM **ps); + +/** Setter for r and s fields of ECDSA_SIG + * \param sig pointer to ECDSA_SIG pointer + * \param r pointer to BIGNUM for r (may be NULL) + * \param s pointer to BIGNUM for s (may be NULL) + */ +int ECDSA_SIG_set0(ECDSA_SIG *sig, BIGNUM *r, BIGNUM *s); + +/** Computes the ECDSA signature of the given hash value using + * the supplied private key and returns the created signature. + * \param dgst pointer to the hash value + * \param dgst_len length of the hash value + * \param eckey EC_KEY object containing a private EC key + * \return pointer to a ECDSA_SIG structure or NULL if an error occurred + */ +ECDSA_SIG *ECDSA_do_sign(const unsigned char *dgst, int dgst_len, + EC_KEY *eckey); + +/** Computes ECDSA signature of a given hash value using the supplied + * private key (note: sig must point to ECDSA_size(eckey) bytes of memory). + * \param dgst pointer to the hash value to sign + * \param dgstlen length of the hash value + * \param kinv BIGNUM with a pre-computed inverse k (optional) + * \param rp BIGNUM with a pre-computed rp value (optional), + * see ECDSA_sign_setup + * \param eckey EC_KEY object containing a private EC key + * \return pointer to a ECDSA_SIG structure or NULL if an error occurred + */ +ECDSA_SIG *ECDSA_do_sign_ex(const unsigned char *dgst, int dgstlen, + const BIGNUM *kinv, const BIGNUM *rp, + EC_KEY *eckey); + +/** Verifies that the supplied signature is a valid ECDSA + * signature of the supplied hash value using the supplied public key. + * \param dgst pointer to the hash value + * \param dgst_len length of the hash value + * \param sig ECDSA_SIG structure + * \param eckey EC_KEY object containing a public EC key + * \return 1 if the signature is valid, 0 if the signature is invalid + * and -1 on error + */ +int ECDSA_do_verify(const unsigned char *dgst, int dgst_len, + const ECDSA_SIG *sig, EC_KEY *eckey); + +/** Precompute parts of the signing operation + * \param eckey EC_KEY object containing a private EC key + * \param ctx BN_CTX object (optional) + * \param kinv BIGNUM pointer for the inverse of k + * \param rp BIGNUM pointer for x coordinate of k * generator + * \return 1 on success and 0 otherwise + */ +int ECDSA_sign_setup(EC_KEY *eckey, BN_CTX *ctx, BIGNUM **kinv, BIGNUM **rp); + +/** Computes ECDSA signature of a given hash value using the supplied + * private key (note: sig must point to ECDSA_size(eckey) bytes of memory). + * \param type this parameter is ignored + * \param dgst pointer to the hash value to sign + * \param dgstlen length of the hash value + * \param sig memory for the DER encoded created signature + * \param siglen pointer to the length of the returned signature + * \param eckey EC_KEY object containing a private EC key + * \return 1 on success and 0 otherwise + */ +int ECDSA_sign(int type, const unsigned char *dgst, int dgstlen, + unsigned char *sig, unsigned int *siglen, EC_KEY *eckey); + +/** Computes ECDSA signature of a given hash value using the supplied + * private key (note: sig must point to ECDSA_size(eckey) bytes of memory). + * \param type this parameter is ignored + * \param dgst pointer to the hash value to sign + * \param dgstlen length of the hash value + * \param sig buffer to hold the DER encoded signature + * \param siglen pointer to the length of the returned signature + * \param kinv BIGNUM with a pre-computed inverse k (optional) + * \param rp BIGNUM with a pre-computed rp value (optional), + * see ECDSA_sign_setup + * \param eckey EC_KEY object containing a private EC key + * \return 1 on success and 0 otherwise + */ +int ECDSA_sign_ex(int type, const unsigned char *dgst, int dgstlen, + unsigned char *sig, unsigned int *siglen, + const BIGNUM *kinv, const BIGNUM *rp, EC_KEY *eckey); + +/** Verifies that the given signature is valid ECDSA signature + * of the supplied hash value using the specified public key. + * \param type this parameter is ignored + * \param dgst pointer to the hash value + * \param dgstlen length of the hash value + * \param sig pointer to the DER encoded signature + * \param siglen length of the DER encoded signature + * \param eckey EC_KEY object containing a public EC key + * \return 1 if the signature is valid, 0 if the signature is invalid + * and -1 on error + */ +int ECDSA_verify(int type, const unsigned char *dgst, int dgstlen, + const unsigned char *sig, int siglen, EC_KEY *eckey); + +/** Returns the maximum length of the DER encoded signature + * \param eckey EC_KEY object + * \return numbers of bytes required for the DER encoded signature + */ +int ECDSA_size(const EC_KEY *eckey); + +/********************************************************************/ +/* EC_KEY_METHOD constructors, destructors, writers and accessors */ +/********************************************************************/ + +EC_KEY_METHOD *EC_KEY_METHOD_new(const EC_KEY_METHOD *meth); +void EC_KEY_METHOD_free(EC_KEY_METHOD *meth); +void EC_KEY_METHOD_set_init(EC_KEY_METHOD *meth, + int (*init)(EC_KEY *key), + void (*finish)(EC_KEY *key), + int (*copy)(EC_KEY *dest, const EC_KEY *src), + int (*set_group)(EC_KEY *key, const EC_GROUP *grp), + int (*set_private)(EC_KEY *key, + const BIGNUM *priv_key), + int (*set_public)(EC_KEY *key, + const EC_POINT *pub_key)); + +void EC_KEY_METHOD_set_keygen(EC_KEY_METHOD *meth, + int (*keygen)(EC_KEY *key)); + +void EC_KEY_METHOD_set_compute_key(EC_KEY_METHOD *meth, + int (*ckey)(unsigned char **psec, + size_t *pseclen, + const EC_POINT *pub_key, + const EC_KEY *ecdh)); + +void EC_KEY_METHOD_set_sign(EC_KEY_METHOD *meth, + int (*sign)(int type, const unsigned char *dgst, + int dlen, unsigned char *sig, + unsigned int *siglen, + const BIGNUM *kinv, const BIGNUM *r, + EC_KEY *eckey), + int (*sign_setup)(EC_KEY *eckey, BN_CTX *ctx_in, + BIGNUM **kinvp, BIGNUM **rp), + ECDSA_SIG *(*sign_sig)(const unsigned char *dgst, + int dgst_len, + const BIGNUM *in_kinv, + const BIGNUM *in_r, + EC_KEY *eckey)); + +void EC_KEY_METHOD_set_verify(EC_KEY_METHOD *meth, + int (*verify)(int type, const unsigned + char *dgst, int dgst_len, + const unsigned char *sigbuf, + int sig_len, EC_KEY *eckey), + int (*verify_sig)(const unsigned char *dgst, + int dgst_len, + const ECDSA_SIG *sig, + EC_KEY *eckey)); + +void EC_KEY_METHOD_get_init(EC_KEY_METHOD *meth, + int (**pinit)(EC_KEY *key), + void (**pfinish)(EC_KEY *key), + int (**pcopy)(EC_KEY *dest, const EC_KEY *src), + int (**pset_group)(EC_KEY *key, + const EC_GROUP *grp), + int (**pset_private)(EC_KEY *key, + const BIGNUM *priv_key), + int (**pset_public)(EC_KEY *key, + const EC_POINT *pub_key)); + +void EC_KEY_METHOD_get_keygen(EC_KEY_METHOD *meth, + int (**pkeygen)(EC_KEY *key)); + +void EC_KEY_METHOD_get_compute_key(EC_KEY_METHOD *meth, + int (**pck)(unsigned char **psec, + size_t *pseclen, + const EC_POINT *pub_key, + const EC_KEY *ecdh)); + +void EC_KEY_METHOD_get_sign(EC_KEY_METHOD *meth, + int (**psign)(int type, const unsigned char *dgst, + int dlen, unsigned char *sig, + unsigned int *siglen, + const BIGNUM *kinv, const BIGNUM *r, + EC_KEY *eckey), + int (**psign_setup)(EC_KEY *eckey, BN_CTX *ctx_in, + BIGNUM **kinvp, BIGNUM **rp), + ECDSA_SIG *(**psign_sig)(const unsigned char *dgst, + int dgst_len, + const BIGNUM *in_kinv, + const BIGNUM *in_r, + EC_KEY *eckey)); + +void EC_KEY_METHOD_get_verify(EC_KEY_METHOD *meth, + int (**pverify)(int type, const unsigned + char *dgst, int dgst_len, + const unsigned char *sigbuf, + int sig_len, EC_KEY *eckey), + int (**pverify_sig)(const unsigned char *dgst, + int dgst_len, + const ECDSA_SIG *sig, + EC_KEY *eckey)); + +# define ECParameters_dup(x) ASN1_dup_of(EC_KEY,i2d_ECParameters,d2i_ECParameters,x) + +# ifndef __cplusplus +# if defined(__SUNPRO_C) +# if __SUNPRO_C >= 0x520 +# pragma error_messages (default,E_ARRAY_OF_INCOMPLETE_NONAME,E_ARRAY_OF_INCOMPLETE) +# endif +# endif +# endif + +# define EVP_PKEY_CTX_set_ec_paramgen_curve_nid(ctx, nid) \ + EVP_PKEY_CTX_ctrl(ctx, EVP_PKEY_EC, \ + EVP_PKEY_OP_PARAMGEN|EVP_PKEY_OP_KEYGEN, \ + EVP_PKEY_CTRL_EC_PARAMGEN_CURVE_NID, nid, NULL) + +# define EVP_PKEY_CTX_set_ec_param_enc(ctx, flag) \ + EVP_PKEY_CTX_ctrl(ctx, EVP_PKEY_EC, \ + EVP_PKEY_OP_PARAMGEN|EVP_PKEY_OP_KEYGEN, \ + EVP_PKEY_CTRL_EC_PARAM_ENC, flag, NULL) + +# define EVP_PKEY_CTX_set_ecdh_cofactor_mode(ctx, flag) \ + EVP_PKEY_CTX_ctrl(ctx, EVP_PKEY_EC, \ + EVP_PKEY_OP_DERIVE, \ + EVP_PKEY_CTRL_EC_ECDH_COFACTOR, flag, NULL) + +# define EVP_PKEY_CTX_get_ecdh_cofactor_mode(ctx) \ + EVP_PKEY_CTX_ctrl(ctx, EVP_PKEY_EC, \ + EVP_PKEY_OP_DERIVE, \ + EVP_PKEY_CTRL_EC_ECDH_COFACTOR, -2, NULL) + +# define EVP_PKEY_CTX_set_ecdh_kdf_type(ctx, kdf) \ + EVP_PKEY_CTX_ctrl(ctx, EVP_PKEY_EC, \ + EVP_PKEY_OP_DERIVE, \ + EVP_PKEY_CTRL_EC_KDF_TYPE, kdf, NULL) + +# define EVP_PKEY_CTX_get_ecdh_kdf_type(ctx) \ + EVP_PKEY_CTX_ctrl(ctx, EVP_PKEY_EC, \ + EVP_PKEY_OP_DERIVE, \ + EVP_PKEY_CTRL_EC_KDF_TYPE, -2, NULL) + +# define EVP_PKEY_CTX_set_ecdh_kdf_md(ctx, md) \ + EVP_PKEY_CTX_ctrl(ctx, EVP_PKEY_EC, \ + EVP_PKEY_OP_DERIVE, \ + EVP_PKEY_CTRL_EC_KDF_MD, 0, (void *)md) + +# define EVP_PKEY_CTX_get_ecdh_kdf_md(ctx, pmd) \ + EVP_PKEY_CTX_ctrl(ctx, EVP_PKEY_EC, \ + EVP_PKEY_OP_DERIVE, \ + EVP_PKEY_CTRL_GET_EC_KDF_MD, 0, (void *)pmd) + +# define EVP_PKEY_CTX_set_ecdh_kdf_outlen(ctx, len) \ + EVP_PKEY_CTX_ctrl(ctx, EVP_PKEY_EC, \ + EVP_PKEY_OP_DERIVE, \ + EVP_PKEY_CTRL_EC_KDF_OUTLEN, len, NULL) + +# define EVP_PKEY_CTX_get_ecdh_kdf_outlen(ctx, plen) \ + EVP_PKEY_CTX_ctrl(ctx, EVP_PKEY_EC, \ + EVP_PKEY_OP_DERIVE, \ + EVP_PKEY_CTRL_GET_EC_KDF_OUTLEN, 0, (void *)plen) + +# define EVP_PKEY_CTX_set0_ecdh_kdf_ukm(ctx, p, plen) \ + EVP_PKEY_CTX_ctrl(ctx, EVP_PKEY_EC, \ + EVP_PKEY_OP_DERIVE, \ + EVP_PKEY_CTRL_EC_KDF_UKM, plen, (void *)p) + +# define EVP_PKEY_CTX_get0_ecdh_kdf_ukm(ctx, p) \ + EVP_PKEY_CTX_ctrl(ctx, EVP_PKEY_EC, \ + EVP_PKEY_OP_DERIVE, \ + EVP_PKEY_CTRL_GET_EC_KDF_UKM, 0, (void *)p) + +# define EVP_PKEY_CTRL_EC_PARAMGEN_CURVE_NID (EVP_PKEY_ALG_CTRL + 1) +# define EVP_PKEY_CTRL_EC_PARAM_ENC (EVP_PKEY_ALG_CTRL + 2) +# define EVP_PKEY_CTRL_EC_ECDH_COFACTOR (EVP_PKEY_ALG_CTRL + 3) +# define EVP_PKEY_CTRL_EC_KDF_TYPE (EVP_PKEY_ALG_CTRL + 4) +# define EVP_PKEY_CTRL_EC_KDF_MD (EVP_PKEY_ALG_CTRL + 5) +# define EVP_PKEY_CTRL_GET_EC_KDF_MD (EVP_PKEY_ALG_CTRL + 6) +# define EVP_PKEY_CTRL_EC_KDF_OUTLEN (EVP_PKEY_ALG_CTRL + 7) +# define EVP_PKEY_CTRL_GET_EC_KDF_OUTLEN (EVP_PKEY_ALG_CTRL + 8) +# define EVP_PKEY_CTRL_EC_KDF_UKM (EVP_PKEY_ALG_CTRL + 9) +# define EVP_PKEY_CTRL_GET_EC_KDF_UKM (EVP_PKEY_ALG_CTRL + 10) +/* KDF types */ +# define EVP_PKEY_ECDH_KDF_NONE 1 +# define EVP_PKEY_ECDH_KDF_X9_62 2 + +/* BEGIN ERROR CODES */ +/* + * The following lines are auto generated by the script mkerr.pl. Any changes + * made after this point may be overwritten when the script is next run. + */ + +int ERR_load_EC_strings(void); + +/* Error codes for the EC functions. */ + +/* Function codes. */ +# define EC_F_BN_TO_FELEM 224 +# define EC_F_D2I_ECPARAMETERS 144 +# define EC_F_D2I_ECPKPARAMETERS 145 +# define EC_F_D2I_ECPRIVATEKEY 146 +# define EC_F_DO_EC_KEY_PRINT 221 +# define EC_F_ECDH_CMS_DECRYPT 238 +# define EC_F_ECDH_CMS_SET_SHARED_INFO 239 +# define EC_F_ECDH_COMPUTE_KEY 246 +# define EC_F_ECDH_SIMPLE_COMPUTE_KEY 257 +# define EC_F_ECDSA_DO_SIGN_EX 251 +# define EC_F_ECDSA_DO_VERIFY 252 +# define EC_F_ECDSA_SIGN_EX 254 +# define EC_F_ECDSA_SIGN_SETUP 248 +# define EC_F_ECDSA_SIG_NEW 265 +# define EC_F_ECDSA_VERIFY 253 +# define EC_F_ECKEY_PARAM2TYPE 223 +# define EC_F_ECKEY_PARAM_DECODE 212 +# define EC_F_ECKEY_PRIV_DECODE 213 +# define EC_F_ECKEY_PRIV_ENCODE 214 +# define EC_F_ECKEY_PUB_DECODE 215 +# define EC_F_ECKEY_PUB_ENCODE 216 +# define EC_F_ECKEY_TYPE2PARAM 220 +# define EC_F_ECPARAMETERS_PRINT 147 +# define EC_F_ECPARAMETERS_PRINT_FP 148 +# define EC_F_ECPKPARAMETERS_PRINT 149 +# define EC_F_ECPKPARAMETERS_PRINT_FP 150 +# define EC_F_ECP_NISTZ256_GET_AFFINE 240 +# define EC_F_ECP_NISTZ256_MULT_PRECOMPUTE 243 +# define EC_F_ECP_NISTZ256_POINTS_MUL 241 +# define EC_F_ECP_NISTZ256_PRE_COMP_NEW 244 +# define EC_F_ECP_NISTZ256_WINDOWED_MUL 242 +# define EC_F_ECX_KEY_OP 266 +# define EC_F_ECX_PRIV_ENCODE 267 +# define EC_F_ECX_PUB_ENCODE 268 +# define EC_F_EC_ASN1_GROUP2CURVE 153 +# define EC_F_EC_ASN1_GROUP2FIELDID 154 +# define EC_F_EC_GF2M_MONTGOMERY_POINT_MULTIPLY 208 +# define EC_F_EC_GF2M_SIMPLE_GROUP_CHECK_DISCRIMINANT 159 +# define EC_F_EC_GF2M_SIMPLE_GROUP_SET_CURVE 195 +# define EC_F_EC_GF2M_SIMPLE_OCT2POINT 160 +# define EC_F_EC_GF2M_SIMPLE_POINT2OCT 161 +# define EC_F_EC_GF2M_SIMPLE_POINT_GET_AFFINE_COORDINATES 162 +# define EC_F_EC_GF2M_SIMPLE_POINT_SET_AFFINE_COORDINATES 163 +# define EC_F_EC_GF2M_SIMPLE_SET_COMPRESSED_COORDINATES 164 +# define EC_F_EC_GFP_MONT_FIELD_DECODE 133 +# define EC_F_EC_GFP_MONT_FIELD_ENCODE 134 +# define EC_F_EC_GFP_MONT_FIELD_MUL 131 +# define EC_F_EC_GFP_MONT_FIELD_SET_TO_ONE 209 +# define EC_F_EC_GFP_MONT_FIELD_SQR 132 +# define EC_F_EC_GFP_MONT_GROUP_SET_CURVE 189 +# define EC_F_EC_GFP_NISTP224_GROUP_SET_CURVE 225 +# define EC_F_EC_GFP_NISTP224_POINTS_MUL 228 +# define EC_F_EC_GFP_NISTP224_POINT_GET_AFFINE_COORDINATES 226 +# define EC_F_EC_GFP_NISTP256_GROUP_SET_CURVE 230 +# define EC_F_EC_GFP_NISTP256_POINTS_MUL 231 +# define EC_F_EC_GFP_NISTP256_POINT_GET_AFFINE_COORDINATES 232 +# define EC_F_EC_GFP_NISTP521_GROUP_SET_CURVE 233 +# define EC_F_EC_GFP_NISTP521_POINTS_MUL 234 +# define EC_F_EC_GFP_NISTP521_POINT_GET_AFFINE_COORDINATES 235 +# define EC_F_EC_GFP_NIST_FIELD_MUL 200 +# define EC_F_EC_GFP_NIST_FIELD_SQR 201 +# define EC_F_EC_GFP_NIST_GROUP_SET_CURVE 202 +# define EC_F_EC_GFP_SIMPLE_GROUP_CHECK_DISCRIMINANT 165 +# define EC_F_EC_GFP_SIMPLE_GROUP_SET_CURVE 166 +# define EC_F_EC_GFP_SIMPLE_MAKE_AFFINE 102 +# define EC_F_EC_GFP_SIMPLE_OCT2POINT 103 +# define EC_F_EC_GFP_SIMPLE_POINT2OCT 104 +# define EC_F_EC_GFP_SIMPLE_POINTS_MAKE_AFFINE 137 +# define EC_F_EC_GFP_SIMPLE_POINT_GET_AFFINE_COORDINATES 167 +# define EC_F_EC_GFP_SIMPLE_POINT_SET_AFFINE_COORDINATES 168 +# define EC_F_EC_GFP_SIMPLE_SET_COMPRESSED_COORDINATES 169 +# define EC_F_EC_GROUP_CHECK 170 +# define EC_F_EC_GROUP_CHECK_DISCRIMINANT 171 +# define EC_F_EC_GROUP_COPY 106 +# define EC_F_EC_GROUP_GET_CURVE_GF2M 172 +# define EC_F_EC_GROUP_GET_CURVE_GFP 130 +# define EC_F_EC_GROUP_GET_DEGREE 173 +# define EC_F_EC_GROUP_GET_ECPARAMETERS 261 +# define EC_F_EC_GROUP_GET_ECPKPARAMETERS 262 +# define EC_F_EC_GROUP_GET_PENTANOMIAL_BASIS 193 +# define EC_F_EC_GROUP_GET_TRINOMIAL_BASIS 194 +# define EC_F_EC_GROUP_NEW 108 +# define EC_F_EC_GROUP_NEW_BY_CURVE_NAME 174 +# define EC_F_EC_GROUP_NEW_FROM_DATA 175 +# define EC_F_EC_GROUP_NEW_FROM_ECPARAMETERS 263 +# define EC_F_EC_GROUP_NEW_FROM_ECPKPARAMETERS 264 +# define EC_F_EC_GROUP_SET_CURVE_GF2M 176 +# define EC_F_EC_GROUP_SET_CURVE_GFP 109 +# define EC_F_EC_GROUP_SET_GENERATOR 111 +# define EC_F_EC_KEY_CHECK_KEY 177 +# define EC_F_EC_KEY_COPY 178 +# define EC_F_EC_KEY_GENERATE_KEY 179 +# define EC_F_EC_KEY_NEW 182 +# define EC_F_EC_KEY_NEW_METHOD 245 +# define EC_F_EC_KEY_OCT2PRIV 255 +# define EC_F_EC_KEY_PRINT 180 +# define EC_F_EC_KEY_PRINT_FP 181 +# define EC_F_EC_KEY_PRIV2OCT 256 +# define EC_F_EC_KEY_SET_PUBLIC_KEY_AFFINE_COORDINATES 229 +# define EC_F_EC_KEY_SIMPLE_CHECK_KEY 258 +# define EC_F_EC_KEY_SIMPLE_OCT2PRIV 259 +# define EC_F_EC_KEY_SIMPLE_PRIV2OCT 260 +# define EC_F_EC_POINTS_MAKE_AFFINE 136 +# define EC_F_EC_POINT_ADD 112 +# define EC_F_EC_POINT_CMP 113 +# define EC_F_EC_POINT_COPY 114 +# define EC_F_EC_POINT_DBL 115 +# define EC_F_EC_POINT_GET_AFFINE_COORDINATES_GF2M 183 +# define EC_F_EC_POINT_GET_AFFINE_COORDINATES_GFP 116 +# define EC_F_EC_POINT_GET_JPROJECTIVE_COORDINATES_GFP 117 +# define EC_F_EC_POINT_INVERT 210 +# define EC_F_EC_POINT_IS_AT_INFINITY 118 +# define EC_F_EC_POINT_IS_ON_CURVE 119 +# define EC_F_EC_POINT_MAKE_AFFINE 120 +# define EC_F_EC_POINT_NEW 121 +# define EC_F_EC_POINT_OCT2POINT 122 +# define EC_F_EC_POINT_POINT2OCT 123 +# define EC_F_EC_POINT_SET_AFFINE_COORDINATES_GF2M 185 +# define EC_F_EC_POINT_SET_AFFINE_COORDINATES_GFP 124 +# define EC_F_EC_POINT_SET_COMPRESSED_COORDINATES_GF2M 186 +# define EC_F_EC_POINT_SET_COMPRESSED_COORDINATES_GFP 125 +# define EC_F_EC_POINT_SET_JPROJECTIVE_COORDINATES_GFP 126 +# define EC_F_EC_POINT_SET_TO_INFINITY 127 +# define EC_F_EC_PRE_COMP_NEW 196 +# define EC_F_EC_WNAF_MUL 187 +# define EC_F_EC_WNAF_PRECOMPUTE_MULT 188 +# define EC_F_I2D_ECPARAMETERS 190 +# define EC_F_I2D_ECPKPARAMETERS 191 +# define EC_F_I2D_ECPRIVATEKEY 192 +# define EC_F_I2O_ECPUBLICKEY 151 +# define EC_F_NISTP224_PRE_COMP_NEW 227 +# define EC_F_NISTP256_PRE_COMP_NEW 236 +# define EC_F_NISTP521_PRE_COMP_NEW 237 +# define EC_F_O2I_ECPUBLICKEY 152 +# define EC_F_OLD_EC_PRIV_DECODE 222 +# define EC_F_OSSL_ECDH_COMPUTE_KEY 247 +# define EC_F_OSSL_ECDSA_SIGN_SIG 249 +# define EC_F_OSSL_ECDSA_VERIFY_SIG 250 +# define EC_F_PKEY_ECX_DERIVE 269 +# define EC_F_PKEY_EC_CTRL 197 +# define EC_F_PKEY_EC_CTRL_STR 198 +# define EC_F_PKEY_EC_DERIVE 217 +# define EC_F_PKEY_EC_KEYGEN 199 +# define EC_F_PKEY_EC_PARAMGEN 219 +# define EC_F_PKEY_EC_SIGN 218 + +/* Reason codes. */ +# define EC_R_ASN1_ERROR 115 +# define EC_R_BAD_SIGNATURE 156 +# define EC_R_BIGNUM_OUT_OF_RANGE 144 +# define EC_R_BUFFER_TOO_SMALL 100 +# define EC_R_COORDINATES_OUT_OF_RANGE 146 +# define EC_R_CURVE_DOES_NOT_SUPPORT_ECDH 160 +# define EC_R_CURVE_DOES_NOT_SUPPORT_SIGNING 159 +# define EC_R_D2I_ECPKPARAMETERS_FAILURE 117 +# define EC_R_DECODE_ERROR 142 +# define EC_R_DISCRIMINANT_IS_ZERO 118 +# define EC_R_EC_GROUP_NEW_BY_NAME_FAILURE 119 +# define EC_R_FIELD_TOO_LARGE 143 +# define EC_R_GF2M_NOT_SUPPORTED 147 +# define EC_R_GROUP2PKPARAMETERS_FAILURE 120 +# define EC_R_I2D_ECPKPARAMETERS_FAILURE 121 +# define EC_R_INCOMPATIBLE_OBJECTS 101 +# define EC_R_INVALID_ARGUMENT 112 +# define EC_R_INVALID_COMPRESSED_POINT 110 +# define EC_R_INVALID_COMPRESSION_BIT 109 +# define EC_R_INVALID_CURVE 141 +# define EC_R_INVALID_DIGEST 151 +# define EC_R_INVALID_DIGEST_TYPE 138 +# define EC_R_INVALID_ENCODING 102 +# define EC_R_INVALID_FIELD 103 +# define EC_R_INVALID_FORM 104 +# define EC_R_INVALID_GROUP_ORDER 122 +# define EC_R_INVALID_KEY 116 +# define EC_R_INVALID_OUTPUT_LENGTH 161 +# define EC_R_INVALID_PEER_KEY 133 +# define EC_R_INVALID_PENTANOMIAL_BASIS 132 +# define EC_R_INVALID_PRIVATE_KEY 123 +# define EC_R_INVALID_TRINOMIAL_BASIS 137 +# define EC_R_KDF_PARAMETER_ERROR 148 +# define EC_R_KEYS_NOT_SET 140 +# define EC_R_MISSING_PARAMETERS 124 +# define EC_R_MISSING_PRIVATE_KEY 125 +# define EC_R_NEED_NEW_SETUP_VALUES 157 +# define EC_R_NOT_A_NIST_PRIME 135 +# define EC_R_NOT_IMPLEMENTED 126 +# define EC_R_NOT_INITIALIZED 111 +# define EC_R_NO_PARAMETERS_SET 139 +# define EC_R_NO_PRIVATE_VALUE 154 +# define EC_R_OPERATION_NOT_SUPPORTED 152 +# define EC_R_PASSED_NULL_PARAMETER 134 +# define EC_R_PEER_KEY_ERROR 149 +# define EC_R_PKPARAMETERS2GROUP_FAILURE 127 +# define EC_R_POINT_ARITHMETIC_FAILURE 155 +# define EC_R_POINT_AT_INFINITY 106 +# define EC_R_POINT_IS_NOT_ON_CURVE 107 +# define EC_R_RANDOM_NUMBER_GENERATION_FAILED 158 +# define EC_R_SHARED_INFO_ERROR 150 +# define EC_R_SLOT_FULL 108 +# define EC_R_UNDEFINED_GENERATOR 113 +# define EC_R_UNDEFINED_ORDER 128 +# define EC_R_UNKNOWN_GROUP 129 +# define EC_R_UNKNOWN_ORDER 114 +# define EC_R_UNSUPPORTED_FIELD 131 +# define EC_R_WRONG_CURVE_PARAMETERS 145 +# define EC_R_WRONG_ORDER 130 + +# ifdef __cplusplus +} +# endif +# endif +#endif diff --git a/mobile/ios/FxA/FxA/include/openssl/ecdh.h b/mobile/ios/FxA/FxA/include/openssl/ecdh.h new file mode 100644 index 0000000000..681f3d5e55 --- /dev/null +++ b/mobile/ios/FxA/FxA/include/openssl/ecdh.h @@ -0,0 +1,10 @@ +/* + * Copyright 2002-2016 The OpenSSL Project Authors. All Rights Reserved. + * + * Licensed under the OpenSSL license (the "License"). You may not use + * this file except in compliance with the License. You can obtain a copy + * in the file LICENSE in the source distribution or at + * https://www.openssl.org/source/license.html + */ + +#include diff --git a/mobile/ios/FxA/FxA/include/openssl/ecdsa.h b/mobile/ios/FxA/FxA/include/openssl/ecdsa.h new file mode 100644 index 0000000000..681f3d5e55 --- /dev/null +++ b/mobile/ios/FxA/FxA/include/openssl/ecdsa.h @@ -0,0 +1,10 @@ +/* + * Copyright 2002-2016 The OpenSSL Project Authors. All Rights Reserved. + * + * Licensed under the OpenSSL license (the "License"). You may not use + * this file except in compliance with the License. You can obtain a copy + * in the file LICENSE in the source distribution or at + * https://www.openssl.org/source/license.html + */ + +#include diff --git a/mobile/ios/FxA/FxA/include/openssl/engine.h b/mobile/ios/FxA/FxA/include/openssl/engine.h new file mode 100644 index 0000000000..26cf714843 --- /dev/null +++ b/mobile/ios/FxA/FxA/include/openssl/engine.h @@ -0,0 +1,840 @@ +/* + * Copyright 2000-2016 The OpenSSL Project Authors. All Rights Reserved. + * + * Licensed under the OpenSSL license (the "License"). You may not use + * this file except in compliance with the License. You can obtain a copy + * in the file LICENSE in the source distribution or at + * https://www.openssl.org/source/license.html + */ + +/* ==================================================================== + * Copyright 2002 Sun Microsystems, Inc. ALL RIGHTS RESERVED. + * ECDH support in OpenSSL originally developed by + * SUN MICROSYSTEMS, INC., and contributed to the OpenSSL project. + */ + +#ifndef HEADER_ENGINE_H +# define HEADER_ENGINE_H + +# include + +# ifndef OPENSSL_NO_ENGINE +# if OPENSSL_API_COMPAT < 0x10100000L +# include +# include +# include +# include +# include +# include +# include +# include +# endif +# include +# include +# include +# ifdef __cplusplus +extern "C" { +# endif + +/* + * These flags are used to control combinations of algorithm (methods) by + * bitwise "OR"ing. + */ +# define ENGINE_METHOD_RSA (unsigned int)0x0001 +# define ENGINE_METHOD_DSA (unsigned int)0x0002 +# define ENGINE_METHOD_DH (unsigned int)0x0004 +# define ENGINE_METHOD_RAND (unsigned int)0x0008 +# define ENGINE_METHOD_CIPHERS (unsigned int)0x0040 +# define ENGINE_METHOD_DIGESTS (unsigned int)0x0080 +# define ENGINE_METHOD_PKEY_METHS (unsigned int)0x0200 +# define ENGINE_METHOD_PKEY_ASN1_METHS (unsigned int)0x0400 +# define ENGINE_METHOD_EC (unsigned int)0x0800 +/* Obvious all-or-nothing cases. */ +# define ENGINE_METHOD_ALL (unsigned int)0xFFFF +# define ENGINE_METHOD_NONE (unsigned int)0x0000 + +/* + * This(ese) flag(s) controls behaviour of the ENGINE_TABLE mechanism used + * internally to control registration of ENGINE implementations, and can be + * set by ENGINE_set_table_flags(). The "NOINIT" flag prevents attempts to + * initialise registered ENGINEs if they are not already initialised. + */ +# define ENGINE_TABLE_FLAG_NOINIT (unsigned int)0x0001 + +/* ENGINE flags that can be set by ENGINE_set_flags(). */ +/* Not used */ +/* #define ENGINE_FLAGS_MALLOCED 0x0001 */ + +/* + * This flag is for ENGINEs that wish to handle the various 'CMD'-related + * control commands on their own. Without this flag, ENGINE_ctrl() handles + * these control commands on behalf of the ENGINE using their "cmd_defns" + * data. + */ +# define ENGINE_FLAGS_MANUAL_CMD_CTRL (int)0x0002 + +/* + * This flag is for ENGINEs who return new duplicate structures when found + * via "ENGINE_by_id()". When an ENGINE must store state (eg. if + * ENGINE_ctrl() commands are called in sequence as part of some stateful + * process like key-generation setup and execution), it can set this flag - + * then each attempt to obtain the ENGINE will result in it being copied into + * a new structure. Normally, ENGINEs don't declare this flag so + * ENGINE_by_id() just increments the existing ENGINE's structural reference + * count. + */ +# define ENGINE_FLAGS_BY_ID_COPY (int)0x0004 + +/* + * This flag if for an ENGINE that does not want its methods registered as + * part of ENGINE_register_all_complete() for example if the methods are not + * usable as default methods. + */ + +# define ENGINE_FLAGS_NO_REGISTER_ALL (int)0x0008 + +/* + * ENGINEs can support their own command types, and these flags are used in + * ENGINE_CTRL_GET_CMD_FLAGS to indicate to the caller what kind of input + * each command expects. Currently only numeric and string input is + * supported. If a control command supports none of the _NUMERIC, _STRING, or + * _NO_INPUT options, then it is regarded as an "internal" control command - + * and not for use in config setting situations. As such, they're not + * available to the ENGINE_ctrl_cmd_string() function, only raw ENGINE_ctrl() + * access. Changes to this list of 'command types' should be reflected + * carefully in ENGINE_cmd_is_executable() and ENGINE_ctrl_cmd_string(). + */ + +/* accepts a 'long' input value (3rd parameter to ENGINE_ctrl) */ +# define ENGINE_CMD_FLAG_NUMERIC (unsigned int)0x0001 +/* + * accepts string input (cast from 'void*' to 'const char *', 4th parameter + * to ENGINE_ctrl) + */ +# define ENGINE_CMD_FLAG_STRING (unsigned int)0x0002 +/* + * Indicates that the control command takes *no* input. Ie. the control + * command is unparameterised. + */ +# define ENGINE_CMD_FLAG_NO_INPUT (unsigned int)0x0004 +/* + * Indicates that the control command is internal. This control command won't + * be shown in any output, and is only usable through the ENGINE_ctrl_cmd() + * function. + */ +# define ENGINE_CMD_FLAG_INTERNAL (unsigned int)0x0008 + +/* + * NB: These 3 control commands are deprecated and should not be used. + * ENGINEs relying on these commands should compile conditional support for + * compatibility (eg. if these symbols are defined) but should also migrate + * the same functionality to their own ENGINE-specific control functions that + * can be "discovered" by calling applications. The fact these control + * commands wouldn't be "executable" (ie. usable by text-based config) + * doesn't change the fact that application code can find and use them + * without requiring per-ENGINE hacking. + */ + +/* + * These flags are used to tell the ctrl function what should be done. All + * command numbers are shared between all engines, even if some don't make + * sense to some engines. In such a case, they do nothing but return the + * error ENGINE_R_CTRL_COMMAND_NOT_IMPLEMENTED. + */ +# define ENGINE_CTRL_SET_LOGSTREAM 1 +# define ENGINE_CTRL_SET_PASSWORD_CALLBACK 2 +# define ENGINE_CTRL_HUP 3/* Close and reinitialise + * any handles/connections + * etc. */ +# define ENGINE_CTRL_SET_USER_INTERFACE 4/* Alternative to callback */ +# define ENGINE_CTRL_SET_CALLBACK_DATA 5/* User-specific data, used + * when calling the password + * callback and the user + * interface */ +# define ENGINE_CTRL_LOAD_CONFIGURATION 6/* Load a configuration, + * given a string that + * represents a file name + * or so */ +# define ENGINE_CTRL_LOAD_SECTION 7/* Load data from a given + * section in the already + * loaded configuration */ + +/* + * These control commands allow an application to deal with an arbitrary + * engine in a dynamic way. Warn: Negative return values indicate errors FOR + * THESE COMMANDS because zero is used to indicate 'end-of-list'. Other + * commands, including ENGINE-specific command types, return zero for an + * error. An ENGINE can choose to implement these ctrl functions, and can + * internally manage things however it chooses - it does so by setting the + * ENGINE_FLAGS_MANUAL_CMD_CTRL flag (using ENGINE_set_flags()). Otherwise + * the ENGINE_ctrl() code handles this on the ENGINE's behalf using the + * cmd_defns data (set using ENGINE_set_cmd_defns()). This means an ENGINE's + * ctrl() handler need only implement its own commands - the above "meta" + * commands will be taken care of. + */ + +/* + * Returns non-zero if the supplied ENGINE has a ctrl() handler. If "not", + * then all the remaining control commands will return failure, so it is + * worth checking this first if the caller is trying to "discover" the + * engine's capabilities and doesn't want errors generated unnecessarily. + */ +# define ENGINE_CTRL_HAS_CTRL_FUNCTION 10 +/* + * Returns a positive command number for the first command supported by the + * engine. Returns zero if no ctrl commands are supported. + */ +# define ENGINE_CTRL_GET_FIRST_CMD_TYPE 11 +/* + * The 'long' argument specifies a command implemented by the engine, and the + * return value is the next command supported, or zero if there are no more. + */ +# define ENGINE_CTRL_GET_NEXT_CMD_TYPE 12 +/* + * The 'void*' argument is a command name (cast from 'const char *'), and the + * return value is the command that corresponds to it. + */ +# define ENGINE_CTRL_GET_CMD_FROM_NAME 13 +/* + * The next two allow a command to be converted into its corresponding string + * form. In each case, the 'long' argument supplies the command. In the + * NAME_LEN case, the return value is the length of the command name (not + * counting a trailing EOL). In the NAME case, the 'void*' argument must be a + * string buffer large enough, and it will be populated with the name of the + * command (WITH a trailing EOL). + */ +# define ENGINE_CTRL_GET_NAME_LEN_FROM_CMD 14 +# define ENGINE_CTRL_GET_NAME_FROM_CMD 15 +/* The next two are similar but give a "short description" of a command. */ +# define ENGINE_CTRL_GET_DESC_LEN_FROM_CMD 16 +# define ENGINE_CTRL_GET_DESC_FROM_CMD 17 +/* + * With this command, the return value is the OR'd combination of + * ENGINE_CMD_FLAG_*** values that indicate what kind of input a given + * engine-specific ctrl command expects. + */ +# define ENGINE_CTRL_GET_CMD_FLAGS 18 + +/* + * ENGINE implementations should start the numbering of their own control + * commands from this value. (ie. ENGINE_CMD_BASE, ENGINE_CMD_BASE + 1, etc). + */ +# define ENGINE_CMD_BASE 200 + +/* + * NB: These 2 nCipher "chil" control commands are deprecated, and their + * functionality is now available through ENGINE-specific control commands + * (exposed through the above-mentioned 'CMD'-handling). Code using these 2 + * commands should be migrated to the more general command handling before + * these are removed. + */ + +/* Flags specific to the nCipher "chil" engine */ +# define ENGINE_CTRL_CHIL_SET_FORKCHECK 100 + /* + * Depending on the value of the (long)i argument, this sets or + * unsets the SimpleForkCheck flag in the CHIL API to enable or + * disable checking and workarounds for applications that fork(). + */ +# define ENGINE_CTRL_CHIL_NO_LOCKING 101 + /* + * This prevents the initialisation function from providing mutex + * callbacks to the nCipher library. + */ + +/* + * If an ENGINE supports its own specific control commands and wishes the + * framework to handle the above 'ENGINE_CMD_***'-manipulation commands on + * its behalf, it should supply a null-terminated array of ENGINE_CMD_DEFN + * entries to ENGINE_set_cmd_defns(). It should also implement a ctrl() + * handler that supports the stated commands (ie. the "cmd_num" entries as + * described by the array). NB: The array must be ordered in increasing order + * of cmd_num. "null-terminated" means that the last ENGINE_CMD_DEFN element + * has cmd_num set to zero and/or cmd_name set to NULL. + */ +typedef struct ENGINE_CMD_DEFN_st { + unsigned int cmd_num; /* The command number */ + const char *cmd_name; /* The command name itself */ + const char *cmd_desc; /* A short description of the command */ + unsigned int cmd_flags; /* The input the command expects */ +} ENGINE_CMD_DEFN; + +/* Generic function pointer */ +typedef int (*ENGINE_GEN_FUNC_PTR) (void); +/* Generic function pointer taking no arguments */ +typedef int (*ENGINE_GEN_INT_FUNC_PTR) (ENGINE *); +/* Specific control function pointer */ +typedef int (*ENGINE_CTRL_FUNC_PTR) (ENGINE *, int, long, void *, + void (*f) (void)); +/* Generic load_key function pointer */ +typedef EVP_PKEY *(*ENGINE_LOAD_KEY_PTR)(ENGINE *, const char *, + UI_METHOD *ui_method, + void *callback_data); +typedef int (*ENGINE_SSL_CLIENT_CERT_PTR) (ENGINE *, SSL *ssl, + STACK_OF(X509_NAME) *ca_dn, + X509 **pcert, EVP_PKEY **pkey, + STACK_OF(X509) **pother, + UI_METHOD *ui_method, + void *callback_data); +/*- + * These callback types are for an ENGINE's handler for cipher and digest logic. + * These handlers have these prototypes; + * int foo(ENGINE *e, const EVP_CIPHER **cipher, const int **nids, int nid); + * int foo(ENGINE *e, const EVP_MD **digest, const int **nids, int nid); + * Looking at how to implement these handlers in the case of cipher support, if + * the framework wants the EVP_CIPHER for 'nid', it will call; + * foo(e, &p_evp_cipher, NULL, nid); (return zero for failure) + * If the framework wants a list of supported 'nid's, it will call; + * foo(e, NULL, &p_nids, 0); (returns number of 'nids' or -1 for error) + */ +/* + * Returns to a pointer to the array of supported cipher 'nid's. If the + * second parameter is non-NULL it is set to the size of the returned array. + */ +typedef int (*ENGINE_CIPHERS_PTR) (ENGINE *, const EVP_CIPHER **, + const int **, int); +typedef int (*ENGINE_DIGESTS_PTR) (ENGINE *, const EVP_MD **, const int **, + int); +typedef int (*ENGINE_PKEY_METHS_PTR) (ENGINE *, EVP_PKEY_METHOD **, + const int **, int); +typedef int (*ENGINE_PKEY_ASN1_METHS_PTR) (ENGINE *, EVP_PKEY_ASN1_METHOD **, + const int **, int); +/* + * STRUCTURE functions ... all of these functions deal with pointers to + * ENGINE structures where the pointers have a "structural reference". This + * means that their reference is to allowed access to the structure but it + * does not imply that the structure is functional. To simply increment or + * decrement the structural reference count, use ENGINE_by_id and + * ENGINE_free. NB: This is not required when iterating using ENGINE_get_next + * as it will automatically decrement the structural reference count of the + * "current" ENGINE and increment the structural reference count of the + * ENGINE it returns (unless it is NULL). + */ + +/* Get the first/last "ENGINE" type available. */ +ENGINE *ENGINE_get_first(void); +ENGINE *ENGINE_get_last(void); +/* Iterate to the next/previous "ENGINE" type (NULL = end of the list). */ +ENGINE *ENGINE_get_next(ENGINE *e); +ENGINE *ENGINE_get_prev(ENGINE *e); +/* Add another "ENGINE" type into the array. */ +int ENGINE_add(ENGINE *e); +/* Remove an existing "ENGINE" type from the array. */ +int ENGINE_remove(ENGINE *e); +/* Retrieve an engine from the list by its unique "id" value. */ +ENGINE *ENGINE_by_id(const char *id); + +#if OPENSSL_API_COMPAT < 0x10100000L +# define ENGINE_load_openssl() \ + OPENSSL_init_crypto(OPENSSL_INIT_ENGINE_OPENSSL, NULL) +# define ENGINE_load_dynamic() \ + OPENSSL_init_crypto(OPENSSL_INIT_ENGINE_DYNAMIC, NULL) +# ifndef OPENSSL_NO_STATIC_ENGINE +# define ENGINE_load_padlock() \ + OPENSSL_init_crypto(OPENSSL_INIT_ENGINE_PADLOCK, NULL) +# define ENGINE_load_capi() \ + OPENSSL_init_crypto(OPENSSL_INIT_ENGINE_CAPI, NULL) +# define ENGINE_load_afalg() \ + OPENSSL_init_crypto(OPENSSL_INIT_ENGINE_AFALG, NULL) +# endif +# define ENGINE_load_cryptodev() \ + OPENSSL_init_crypto(OPENSSL_INIT_ENGINE_CRYPTODEV, NULL) +# define ENGINE_load_rdrand() \ + OPENSSL_init_crypto(OPENSSL_INIT_ENGINE_RDRAND, NULL) +#endif +void ENGINE_load_builtin_engines(void); + +/* + * Get and set global flags (ENGINE_TABLE_FLAG_***) for the implementation + * "registry" handling. + */ +unsigned int ENGINE_get_table_flags(void); +void ENGINE_set_table_flags(unsigned int flags); + +/*- Manage registration of ENGINEs per "table". For each type, there are 3 + * functions; + * ENGINE_register_***(e) - registers the implementation from 'e' (if it has one) + * ENGINE_unregister_***(e) - unregister the implementation from 'e' + * ENGINE_register_all_***() - call ENGINE_register_***() for each 'e' in the list + * Cleanup is automatically registered from each table when required. + */ + +int ENGINE_register_RSA(ENGINE *e); +void ENGINE_unregister_RSA(ENGINE *e); +void ENGINE_register_all_RSA(void); + +int ENGINE_register_DSA(ENGINE *e); +void ENGINE_unregister_DSA(ENGINE *e); +void ENGINE_register_all_DSA(void); + +int ENGINE_register_EC(ENGINE *e); +void ENGINE_unregister_EC(ENGINE *e); +void ENGINE_register_all_EC(void); + +int ENGINE_register_DH(ENGINE *e); +void ENGINE_unregister_DH(ENGINE *e); +void ENGINE_register_all_DH(void); + +int ENGINE_register_RAND(ENGINE *e); +void ENGINE_unregister_RAND(ENGINE *e); +void ENGINE_register_all_RAND(void); + +int ENGINE_register_ciphers(ENGINE *e); +void ENGINE_unregister_ciphers(ENGINE *e); +void ENGINE_register_all_ciphers(void); + +int ENGINE_register_digests(ENGINE *e); +void ENGINE_unregister_digests(ENGINE *e); +void ENGINE_register_all_digests(void); + +int ENGINE_register_pkey_meths(ENGINE *e); +void ENGINE_unregister_pkey_meths(ENGINE *e); +void ENGINE_register_all_pkey_meths(void); + +int ENGINE_register_pkey_asn1_meths(ENGINE *e); +void ENGINE_unregister_pkey_asn1_meths(ENGINE *e); +void ENGINE_register_all_pkey_asn1_meths(void); + +/* + * These functions register all support from the above categories. Note, use + * of these functions can result in static linkage of code your application + * may not need. If you only need a subset of functionality, consider using + * more selective initialisation. + */ +int ENGINE_register_complete(ENGINE *e); +int ENGINE_register_all_complete(void); + +/* + * Send parametrised control commands to the engine. The possibilities to + * send down an integer, a pointer to data or a function pointer are + * provided. Any of the parameters may or may not be NULL, depending on the + * command number. In actuality, this function only requires a structural + * (rather than functional) reference to an engine, but many control commands + * may require the engine be functional. The caller should be aware of trying + * commands that require an operational ENGINE, and only use functional + * references in such situations. + */ +int ENGINE_ctrl(ENGINE *e, int cmd, long i, void *p, void (*f) (void)); + +/* + * This function tests if an ENGINE-specific command is usable as a + * "setting". Eg. in an application's config file that gets processed through + * ENGINE_ctrl_cmd_string(). If this returns zero, it is not available to + * ENGINE_ctrl_cmd_string(), only ENGINE_ctrl(). + */ +int ENGINE_cmd_is_executable(ENGINE *e, int cmd); + +/* + * This function works like ENGINE_ctrl() with the exception of taking a + * command name instead of a command number, and can handle optional + * commands. See the comment on ENGINE_ctrl_cmd_string() for an explanation + * on how to use the cmd_name and cmd_optional. + */ +int ENGINE_ctrl_cmd(ENGINE *e, const char *cmd_name, + long i, void *p, void (*f) (void), int cmd_optional); + +/* + * This function passes a command-name and argument to an ENGINE. The + * cmd_name is converted to a command number and the control command is + * called using 'arg' as an argument (unless the ENGINE doesn't support such + * a command, in which case no control command is called). The command is + * checked for input flags, and if necessary the argument will be converted + * to a numeric value. If cmd_optional is non-zero, then if the ENGINE + * doesn't support the given cmd_name the return value will be success + * anyway. This function is intended for applications to use so that users + * (or config files) can supply engine-specific config data to the ENGINE at + * run-time to control behaviour of specific engines. As such, it shouldn't + * be used for calling ENGINE_ctrl() functions that return data, deal with + * binary data, or that are otherwise supposed to be used directly through + * ENGINE_ctrl() in application code. Any "return" data from an ENGINE_ctrl() + * operation in this function will be lost - the return value is interpreted + * as failure if the return value is zero, success otherwise, and this + * function returns a boolean value as a result. In other words, vendors of + * 'ENGINE'-enabled devices should write ENGINE implementations with + * parameterisations that work in this scheme, so that compliant ENGINE-based + * applications can work consistently with the same configuration for the + * same ENGINE-enabled devices, across applications. + */ +int ENGINE_ctrl_cmd_string(ENGINE *e, const char *cmd_name, const char *arg, + int cmd_optional); + +/* + * These functions are useful for manufacturing new ENGINE structures. They + * don't address reference counting at all - one uses them to populate an + * ENGINE structure with personalised implementations of things prior to + * using it directly or adding it to the builtin ENGINE list in OpenSSL. + * These are also here so that the ENGINE structure doesn't have to be + * exposed and break binary compatibility! + */ +ENGINE *ENGINE_new(void); +int ENGINE_free(ENGINE *e); +int ENGINE_up_ref(ENGINE *e); +int ENGINE_set_id(ENGINE *e, const char *id); +int ENGINE_set_name(ENGINE *e, const char *name); +int ENGINE_set_RSA(ENGINE *e, const RSA_METHOD *rsa_meth); +int ENGINE_set_DSA(ENGINE *e, const DSA_METHOD *dsa_meth); +int ENGINE_set_EC(ENGINE *e, const EC_KEY_METHOD *ecdsa_meth); +int ENGINE_set_DH(ENGINE *e, const DH_METHOD *dh_meth); +int ENGINE_set_RAND(ENGINE *e, const RAND_METHOD *rand_meth); +int ENGINE_set_destroy_function(ENGINE *e, ENGINE_GEN_INT_FUNC_PTR destroy_f); +int ENGINE_set_init_function(ENGINE *e, ENGINE_GEN_INT_FUNC_PTR init_f); +int ENGINE_set_finish_function(ENGINE *e, ENGINE_GEN_INT_FUNC_PTR finish_f); +int ENGINE_set_ctrl_function(ENGINE *e, ENGINE_CTRL_FUNC_PTR ctrl_f); +int ENGINE_set_load_privkey_function(ENGINE *e, + ENGINE_LOAD_KEY_PTR loadpriv_f); +int ENGINE_set_load_pubkey_function(ENGINE *e, ENGINE_LOAD_KEY_PTR loadpub_f); +int ENGINE_set_load_ssl_client_cert_function(ENGINE *e, + ENGINE_SSL_CLIENT_CERT_PTR + loadssl_f); +int ENGINE_set_ciphers(ENGINE *e, ENGINE_CIPHERS_PTR f); +int ENGINE_set_digests(ENGINE *e, ENGINE_DIGESTS_PTR f); +int ENGINE_set_pkey_meths(ENGINE *e, ENGINE_PKEY_METHS_PTR f); +int ENGINE_set_pkey_asn1_meths(ENGINE *e, ENGINE_PKEY_ASN1_METHS_PTR f); +int ENGINE_set_flags(ENGINE *e, int flags); +int ENGINE_set_cmd_defns(ENGINE *e, const ENGINE_CMD_DEFN *defns); +/* These functions allow control over any per-structure ENGINE data. */ +#define ENGINE_get_ex_new_index(l, p, newf, dupf, freef) \ + CRYPTO_get_ex_new_index(CRYPTO_EX_INDEX_ENGINE, l, p, newf, dupf, freef) +int ENGINE_set_ex_data(ENGINE *e, int idx, void *arg); +void *ENGINE_get_ex_data(const ENGINE *e, int idx); + +#if OPENSSL_API_COMPAT < 0x10100000L +/* + * This function previously cleaned up anything that needs it. Auto-deinit will + * now take care of it so it is no longer required to call this function. + */ +# define ENGINE_cleanup() while(0) continue +#endif + +/* + * These return values from within the ENGINE structure. These can be useful + * with functional references as well as structural references - it depends + * which you obtained. Using the result for functional purposes if you only + * obtained a structural reference may be problematic! + */ +const char *ENGINE_get_id(const ENGINE *e); +const char *ENGINE_get_name(const ENGINE *e); +const RSA_METHOD *ENGINE_get_RSA(const ENGINE *e); +const DSA_METHOD *ENGINE_get_DSA(const ENGINE *e); +const EC_KEY_METHOD *ENGINE_get_EC(const ENGINE *e); +const DH_METHOD *ENGINE_get_DH(const ENGINE *e); +const RAND_METHOD *ENGINE_get_RAND(const ENGINE *e); +ENGINE_GEN_INT_FUNC_PTR ENGINE_get_destroy_function(const ENGINE *e); +ENGINE_GEN_INT_FUNC_PTR ENGINE_get_init_function(const ENGINE *e); +ENGINE_GEN_INT_FUNC_PTR ENGINE_get_finish_function(const ENGINE *e); +ENGINE_CTRL_FUNC_PTR ENGINE_get_ctrl_function(const ENGINE *e); +ENGINE_LOAD_KEY_PTR ENGINE_get_load_privkey_function(const ENGINE *e); +ENGINE_LOAD_KEY_PTR ENGINE_get_load_pubkey_function(const ENGINE *e); +ENGINE_SSL_CLIENT_CERT_PTR ENGINE_get_ssl_client_cert_function(const ENGINE + *e); +ENGINE_CIPHERS_PTR ENGINE_get_ciphers(const ENGINE *e); +ENGINE_DIGESTS_PTR ENGINE_get_digests(const ENGINE *e); +ENGINE_PKEY_METHS_PTR ENGINE_get_pkey_meths(const ENGINE *e); +ENGINE_PKEY_ASN1_METHS_PTR ENGINE_get_pkey_asn1_meths(const ENGINE *e); +const EVP_CIPHER *ENGINE_get_cipher(ENGINE *e, int nid); +const EVP_MD *ENGINE_get_digest(ENGINE *e, int nid); +const EVP_PKEY_METHOD *ENGINE_get_pkey_meth(ENGINE *e, int nid); +const EVP_PKEY_ASN1_METHOD *ENGINE_get_pkey_asn1_meth(ENGINE *e, int nid); +const EVP_PKEY_ASN1_METHOD *ENGINE_get_pkey_asn1_meth_str(ENGINE *e, + const char *str, + int len); +const EVP_PKEY_ASN1_METHOD *ENGINE_pkey_asn1_find_str(ENGINE **pe, + const char *str, + int len); +const ENGINE_CMD_DEFN *ENGINE_get_cmd_defns(const ENGINE *e); +int ENGINE_get_flags(const ENGINE *e); + +/* + * FUNCTIONAL functions. These functions deal with ENGINE structures that + * have (or will) be initialised for use. Broadly speaking, the structural + * functions are useful for iterating the list of available engine types, + * creating new engine types, and other "list" operations. These functions + * actually deal with ENGINEs that are to be used. As such these functions + * can fail (if applicable) when particular engines are unavailable - eg. if + * a hardware accelerator is not attached or not functioning correctly. Each + * ENGINE has 2 reference counts; structural and functional. Every time a + * functional reference is obtained or released, a corresponding structural + * reference is automatically obtained or released too. + */ + +/* + * Initialise a engine type for use (or up its reference count if it's + * already in use). This will fail if the engine is not currently operational + * and cannot initialise. + */ +int ENGINE_init(ENGINE *e); +/* + * Free a functional reference to a engine type. This does not require a + * corresponding call to ENGINE_free as it also releases a structural + * reference. + */ +int ENGINE_finish(ENGINE *e); + +/* + * The following functions handle keys that are stored in some secondary + * location, handled by the engine. The storage may be on a card or + * whatever. + */ +EVP_PKEY *ENGINE_load_private_key(ENGINE *e, const char *key_id, + UI_METHOD *ui_method, void *callback_data); +EVP_PKEY *ENGINE_load_public_key(ENGINE *e, const char *key_id, + UI_METHOD *ui_method, void *callback_data); +int ENGINE_load_ssl_client_cert(ENGINE *e, SSL *s, + STACK_OF(X509_NAME) *ca_dn, X509 **pcert, + EVP_PKEY **ppkey, STACK_OF(X509) **pother, + UI_METHOD *ui_method, void *callback_data); + +/* + * This returns a pointer for the current ENGINE structure that is (by + * default) performing any RSA operations. The value returned is an + * incremented reference, so it should be free'd (ENGINE_finish) before it is + * discarded. + */ +ENGINE *ENGINE_get_default_RSA(void); +/* Same for the other "methods" */ +ENGINE *ENGINE_get_default_DSA(void); +ENGINE *ENGINE_get_default_EC(void); +ENGINE *ENGINE_get_default_DH(void); +ENGINE *ENGINE_get_default_RAND(void); +/* + * These functions can be used to get a functional reference to perform + * ciphering or digesting corresponding to "nid". + */ +ENGINE *ENGINE_get_cipher_engine(int nid); +ENGINE *ENGINE_get_digest_engine(int nid); +ENGINE *ENGINE_get_pkey_meth_engine(int nid); +ENGINE *ENGINE_get_pkey_asn1_meth_engine(int nid); + +/* + * This sets a new default ENGINE structure for performing RSA operations. If + * the result is non-zero (success) then the ENGINE structure will have had + * its reference count up'd so the caller should still free their own + * reference 'e'. + */ +int ENGINE_set_default_RSA(ENGINE *e); +int ENGINE_set_default_string(ENGINE *e, const char *def_list); +/* Same for the other "methods" */ +int ENGINE_set_default_DSA(ENGINE *e); +int ENGINE_set_default_EC(ENGINE *e); +int ENGINE_set_default_DH(ENGINE *e); +int ENGINE_set_default_RAND(ENGINE *e); +int ENGINE_set_default_ciphers(ENGINE *e); +int ENGINE_set_default_digests(ENGINE *e); +int ENGINE_set_default_pkey_meths(ENGINE *e); +int ENGINE_set_default_pkey_asn1_meths(ENGINE *e); + +/* + * The combination "set" - the flags are bitwise "OR"d from the + * ENGINE_METHOD_*** defines above. As with the "ENGINE_register_complete()" + * function, this function can result in unnecessary static linkage. If your + * application requires only specific functionality, consider using more + * selective functions. + */ +int ENGINE_set_default(ENGINE *e, unsigned int flags); + +void ENGINE_add_conf_module(void); + +/* Deprecated functions ... */ +/* int ENGINE_clear_defaults(void); */ + +/**************************/ +/* DYNAMIC ENGINE SUPPORT */ +/**************************/ + +/* Binary/behaviour compatibility levels */ +# define OSSL_DYNAMIC_VERSION (unsigned long)0x00030000 +/* + * Binary versions older than this are too old for us (whether we're a loader + * or a loadee) + */ +# define OSSL_DYNAMIC_OLDEST (unsigned long)0x00030000 + +/* + * When compiling an ENGINE entirely as an external shared library, loadable + * by the "dynamic" ENGINE, these types are needed. The 'dynamic_fns' + * structure type provides the calling application's (or library's) error + * functionality and memory management function pointers to the loaded + * library. These should be used/set in the loaded library code so that the + * loading application's 'state' will be used/changed in all operations. The + * 'static_state' pointer allows the loaded library to know if it shares the + * same static data as the calling application (or library), and thus whether + * these callbacks need to be set or not. + */ +typedef void *(*dyn_MEM_malloc_fn) (size_t, const char *, int); +typedef void *(*dyn_MEM_realloc_fn) (void *, size_t, const char *, int); +typedef void (*dyn_MEM_free_fn) (void *, const char *, int); +typedef struct st_dynamic_MEM_fns { + dyn_MEM_malloc_fn malloc_fn; + dyn_MEM_realloc_fn realloc_fn; + dyn_MEM_free_fn free_fn; +} dynamic_MEM_fns; +/* + * FIXME: Perhaps the memory and locking code (crypto.h) should declare and + * use these types so we (and any other dependent code) can simplify a bit?? + */ +/* The top-level structure */ +typedef struct st_dynamic_fns { + void *static_state; + dynamic_MEM_fns mem_fns; +} dynamic_fns; + +/* + * The version checking function should be of this prototype. NB: The + * ossl_version value passed in is the OSSL_DYNAMIC_VERSION of the loading + * code. If this function returns zero, it indicates a (potential) version + * incompatibility and the loaded library doesn't believe it can proceed. + * Otherwise, the returned value is the (latest) version supported by the + * loading library. The loader may still decide that the loaded code's + * version is unsatisfactory and could veto the load. The function is + * expected to be implemented with the symbol name "v_check", and a default + * implementation can be fully instantiated with + * IMPLEMENT_DYNAMIC_CHECK_FN(). + */ +typedef unsigned long (*dynamic_v_check_fn) (unsigned long ossl_version); +# define IMPLEMENT_DYNAMIC_CHECK_FN() \ + OPENSSL_EXPORT unsigned long v_check(unsigned long v); \ + OPENSSL_EXPORT unsigned long v_check(unsigned long v) { \ + if (v >= OSSL_DYNAMIC_OLDEST) return OSSL_DYNAMIC_VERSION; \ + return 0; } + +/* + * This function is passed the ENGINE structure to initialise with its own + * function and command settings. It should not adjust the structural or + * functional reference counts. If this function returns zero, (a) the load + * will be aborted, (b) the previous ENGINE state will be memcpy'd back onto + * the structure, and (c) the shared library will be unloaded. So + * implementations should do their own internal cleanup in failure + * circumstances otherwise they could leak. The 'id' parameter, if non-NULL, + * represents the ENGINE id that the loader is looking for. If this is NULL, + * the shared library can choose to return failure or to initialise a + * 'default' ENGINE. If non-NULL, the shared library must initialise only an + * ENGINE matching the passed 'id'. The function is expected to be + * implemented with the symbol name "bind_engine". A standard implementation + * can be instantiated with IMPLEMENT_DYNAMIC_BIND_FN(fn) where the parameter + * 'fn' is a callback function that populates the ENGINE structure and + * returns an int value (zero for failure). 'fn' should have prototype; + * [static] int fn(ENGINE *e, const char *id); + */ +typedef int (*dynamic_bind_engine) (ENGINE *e, const char *id, + const dynamic_fns *fns); +# define IMPLEMENT_DYNAMIC_BIND_FN(fn) \ + OPENSSL_EXPORT \ + int bind_engine(ENGINE *e, const char *id, const dynamic_fns *fns); \ + OPENSSL_EXPORT \ + int bind_engine(ENGINE *e, const char *id, const dynamic_fns *fns) { \ + if (ENGINE_get_static_state() == fns->static_state) goto skip_cbs; \ + CRYPTO_set_mem_functions(fns->mem_fns.malloc_fn, \ + fns->mem_fns.realloc_fn, \ + fns->mem_fns.free_fn); \ + skip_cbs: \ + if (!fn(e, id)) return 0; \ + return 1; } + +/* + * If the loading application (or library) and the loaded ENGINE library + * share the same static data (eg. they're both dynamically linked to the + * same libcrypto.so) we need a way to avoid trying to set system callbacks - + * this would fail, and for the same reason that it's unnecessary to try. If + * the loaded ENGINE has (or gets from through the loader) its own copy of + * the libcrypto static data, we will need to set the callbacks. The easiest + * way to detect this is to have a function that returns a pointer to some + * static data and let the loading application and loaded ENGINE compare + * their respective values. + */ +void *ENGINE_get_static_state(void); + +# if defined(__OpenBSD__) || defined(__FreeBSD__) || defined(HAVE_CRYPTODEV) +DEPRECATEDIN_1_1_0(void ENGINE_setup_bsd_cryptodev(void)) +# endif + +/* BEGIN ERROR CODES */ +/* + * The following lines are auto generated by the script mkerr.pl. Any changes + * made after this point may be overwritten when the script is next run. + */ + +int ERR_load_ENGINE_strings(void); + +/* Error codes for the ENGINE functions. */ + +/* Function codes. */ +# define ENGINE_F_DYNAMIC_CTRL 180 +# define ENGINE_F_DYNAMIC_GET_DATA_CTX 181 +# define ENGINE_F_DYNAMIC_LOAD 182 +# define ENGINE_F_DYNAMIC_SET_DATA_CTX 183 +# define ENGINE_F_ENGINE_ADD 105 +# define ENGINE_F_ENGINE_BY_ID 106 +# define ENGINE_F_ENGINE_CMD_IS_EXECUTABLE 170 +# define ENGINE_F_ENGINE_CTRL 142 +# define ENGINE_F_ENGINE_CTRL_CMD 178 +# define ENGINE_F_ENGINE_CTRL_CMD_STRING 171 +# define ENGINE_F_ENGINE_FINISH 107 +# define ENGINE_F_ENGINE_GET_CIPHER 185 +# define ENGINE_F_ENGINE_GET_DIGEST 186 +# define ENGINE_F_ENGINE_GET_FIRST 195 +# define ENGINE_F_ENGINE_GET_LAST 196 +# define ENGINE_F_ENGINE_GET_NEXT 115 +# define ENGINE_F_ENGINE_GET_PKEY_ASN1_METH 193 +# define ENGINE_F_ENGINE_GET_PKEY_METH 192 +# define ENGINE_F_ENGINE_GET_PREV 116 +# define ENGINE_F_ENGINE_INIT 119 +# define ENGINE_F_ENGINE_LIST_ADD 120 +# define ENGINE_F_ENGINE_LIST_REMOVE 121 +# define ENGINE_F_ENGINE_LOAD_PRIVATE_KEY 150 +# define ENGINE_F_ENGINE_LOAD_PUBLIC_KEY 151 +# define ENGINE_F_ENGINE_LOAD_SSL_CLIENT_CERT 194 +# define ENGINE_F_ENGINE_NEW 122 +# define ENGINE_F_ENGINE_PKEY_ASN1_FIND_STR 197 +# define ENGINE_F_ENGINE_REMOVE 123 +# define ENGINE_F_ENGINE_SET_DEFAULT_STRING 189 +# define ENGINE_F_ENGINE_SET_ID 129 +# define ENGINE_F_ENGINE_SET_NAME 130 +# define ENGINE_F_ENGINE_TABLE_REGISTER 184 +# define ENGINE_F_ENGINE_UNLOCKED_FINISH 191 +# define ENGINE_F_ENGINE_UP_REF 190 +# define ENGINE_F_INT_CTRL_HELPER 172 +# define ENGINE_F_INT_ENGINE_CONFIGURE 188 +# define ENGINE_F_INT_ENGINE_MODULE_INIT 187 + +/* Reason codes. */ +# define ENGINE_R_ALREADY_LOADED 100 +# define ENGINE_R_ARGUMENT_IS_NOT_A_NUMBER 133 +# define ENGINE_R_CMD_NOT_EXECUTABLE 134 +# define ENGINE_R_COMMAND_TAKES_INPUT 135 +# define ENGINE_R_COMMAND_TAKES_NO_INPUT 136 +# define ENGINE_R_CONFLICTING_ENGINE_ID 103 +# define ENGINE_R_CTRL_COMMAND_NOT_IMPLEMENTED 119 +# define ENGINE_R_DSO_FAILURE 104 +# define ENGINE_R_DSO_NOT_FOUND 132 +# define ENGINE_R_ENGINES_SECTION_ERROR 148 +# define ENGINE_R_ENGINE_CONFIGURATION_ERROR 102 +# define ENGINE_R_ENGINE_IS_NOT_IN_LIST 105 +# define ENGINE_R_ENGINE_SECTION_ERROR 149 +# define ENGINE_R_FAILED_LOADING_PRIVATE_KEY 128 +# define ENGINE_R_FAILED_LOADING_PUBLIC_KEY 129 +# define ENGINE_R_FINISH_FAILED 106 +# define ENGINE_R_ID_OR_NAME_MISSING 108 +# define ENGINE_R_INIT_FAILED 109 +# define ENGINE_R_INTERNAL_LIST_ERROR 110 +# define ENGINE_R_INVALID_ARGUMENT 143 +# define ENGINE_R_INVALID_CMD_NAME 137 +# define ENGINE_R_INVALID_CMD_NUMBER 138 +# define ENGINE_R_INVALID_INIT_VALUE 151 +# define ENGINE_R_INVALID_STRING 150 +# define ENGINE_R_NOT_INITIALISED 117 +# define ENGINE_R_NOT_LOADED 112 +# define ENGINE_R_NO_CONTROL_FUNCTION 120 +# define ENGINE_R_NO_INDEX 144 +# define ENGINE_R_NO_LOAD_FUNCTION 125 +# define ENGINE_R_NO_REFERENCE 130 +# define ENGINE_R_NO_SUCH_ENGINE 116 +# define ENGINE_R_UNIMPLEMENTED_CIPHER 146 +# define ENGINE_R_UNIMPLEMENTED_DIGEST 147 +# define ENGINE_R_UNIMPLEMENTED_PUBLIC_KEY_METHOD 101 +# define ENGINE_R_VERSION_INCOMPATIBILITY 145 + +# ifdef __cplusplus +} +# endif +# endif +#endif diff --git a/mobile/ios/FxA/FxA/include/openssl/err.h b/mobile/ios/FxA/FxA/include/openssl/err.h new file mode 100644 index 0000000000..9bbe9e1091 --- /dev/null +++ b/mobile/ios/FxA/FxA/include/openssl/err.h @@ -0,0 +1,259 @@ +/* + * Copyright 1995-2016 The OpenSSL Project Authors. All Rights Reserved. + * + * Licensed under the OpenSSL license (the "License"). You may not use + * this file except in compliance with the License. You can obtain a copy + * in the file LICENSE in the source distribution or at + * https://www.openssl.org/source/license.html + */ + +#ifndef HEADER_ERR_H +# define HEADER_ERR_H + +# include + +# ifndef OPENSSL_NO_STDIO +# include +# include +# endif + +# include +# include +# include + +#ifdef __cplusplus +extern "C" { +#endif + +# ifndef OPENSSL_NO_ERR +# define ERR_PUT_error(a,b,c,d,e) ERR_put_error(a,b,c,d,e) +# else +# define ERR_PUT_error(a,b,c,d,e) ERR_put_error(a,b,c,NULL,0) +# endif + +# include + +# define ERR_TXT_MALLOCED 0x01 +# define ERR_TXT_STRING 0x02 + +# define ERR_FLAG_MARK 0x01 + +# define ERR_NUM_ERRORS 16 +typedef struct err_state_st { + int err_flags[ERR_NUM_ERRORS]; + unsigned long err_buffer[ERR_NUM_ERRORS]; + char *err_data[ERR_NUM_ERRORS]; + int err_data_flags[ERR_NUM_ERRORS]; + const char *err_file[ERR_NUM_ERRORS]; + int err_line[ERR_NUM_ERRORS]; + int top, bottom; +} ERR_STATE; + +/* library */ +# define ERR_LIB_NONE 1 +# define ERR_LIB_SYS 2 +# define ERR_LIB_BN 3 +# define ERR_LIB_RSA 4 +# define ERR_LIB_DH 5 +# define ERR_LIB_EVP 6 +# define ERR_LIB_BUF 7 +# define ERR_LIB_OBJ 8 +# define ERR_LIB_PEM 9 +# define ERR_LIB_DSA 10 +# define ERR_LIB_X509 11 +/* #define ERR_LIB_METH 12 */ +# define ERR_LIB_ASN1 13 +# define ERR_LIB_CONF 14 +# define ERR_LIB_CRYPTO 15 +# define ERR_LIB_EC 16 +# define ERR_LIB_SSL 20 +/* #define ERR_LIB_SSL23 21 */ +/* #define ERR_LIB_SSL2 22 */ +/* #define ERR_LIB_SSL3 23 */ +/* #define ERR_LIB_RSAREF 30 */ +/* #define ERR_LIB_PROXY 31 */ +# define ERR_LIB_BIO 32 +# define ERR_LIB_PKCS7 33 +# define ERR_LIB_X509V3 34 +# define ERR_LIB_PKCS12 35 +# define ERR_LIB_RAND 36 +# define ERR_LIB_DSO 37 +# define ERR_LIB_ENGINE 38 +# define ERR_LIB_OCSP 39 +# define ERR_LIB_UI 40 +# define ERR_LIB_COMP 41 +# define ERR_LIB_ECDSA 42 +# define ERR_LIB_ECDH 43 +# define ERR_LIB_STORE 44 +# define ERR_LIB_FIPS 45 +# define ERR_LIB_CMS 46 +# define ERR_LIB_TS 47 +# define ERR_LIB_HMAC 48 +/* # define ERR_LIB_JPAKE 49 */ +# define ERR_LIB_CT 50 +# define ERR_LIB_ASYNC 51 +# define ERR_LIB_KDF 52 + +# define ERR_LIB_USER 128 + +# define SYSerr(f,r) ERR_PUT_error(ERR_LIB_SYS,(f),(r),OPENSSL_FILE,OPENSSL_LINE) +# define BNerr(f,r) ERR_PUT_error(ERR_LIB_BN,(f),(r),OPENSSL_FILE,OPENSSL_LINE) +# define RSAerr(f,r) ERR_PUT_error(ERR_LIB_RSA,(f),(r),OPENSSL_FILE,OPENSSL_LINE) +# define DHerr(f,r) ERR_PUT_error(ERR_LIB_DH,(f),(r),OPENSSL_FILE,OPENSSL_LINE) +# define EVPerr(f,r) ERR_PUT_error(ERR_LIB_EVP,(f),(r),OPENSSL_FILE,OPENSSL_LINE) +# define BUFerr(f,r) ERR_PUT_error(ERR_LIB_BUF,(f),(r),OPENSSL_FILE,OPENSSL_LINE) +# define OBJerr(f,r) ERR_PUT_error(ERR_LIB_OBJ,(f),(r),OPENSSL_FILE,OPENSSL_LINE) +# define PEMerr(f,r) ERR_PUT_error(ERR_LIB_PEM,(f),(r),OPENSSL_FILE,OPENSSL_LINE) +# define DSAerr(f,r) ERR_PUT_error(ERR_LIB_DSA,(f),(r),OPENSSL_FILE,OPENSSL_LINE) +# define X509err(f,r) ERR_PUT_error(ERR_LIB_X509,(f),(r),OPENSSL_FILE,OPENSSL_LINE) +# define ASN1err(f,r) ERR_PUT_error(ERR_LIB_ASN1,(f),(r),OPENSSL_FILE,OPENSSL_LINE) +# define CONFerr(f,r) ERR_PUT_error(ERR_LIB_CONF,(f),(r),OPENSSL_FILE,OPENSSL_LINE) +# define CRYPTOerr(f,r) ERR_PUT_error(ERR_LIB_CRYPTO,(f),(r),OPENSSL_FILE,OPENSSL_LINE) +# define ECerr(f,r) ERR_PUT_error(ERR_LIB_EC,(f),(r),OPENSSL_FILE,OPENSSL_LINE) +# define SSLerr(f,r) ERR_PUT_error(ERR_LIB_SSL,(f),(r),OPENSSL_FILE,OPENSSL_LINE) +# define BIOerr(f,r) ERR_PUT_error(ERR_LIB_BIO,(f),(r),OPENSSL_FILE,OPENSSL_LINE) +# define PKCS7err(f,r) ERR_PUT_error(ERR_LIB_PKCS7,(f),(r),OPENSSL_FILE,OPENSSL_LINE) +# define X509V3err(f,r) ERR_PUT_error(ERR_LIB_X509V3,(f),(r),OPENSSL_FILE,OPENSSL_LINE) +# define PKCS12err(f,r) ERR_PUT_error(ERR_LIB_PKCS12,(f),(r),OPENSSL_FILE,OPENSSL_LINE) +# define RANDerr(f,r) ERR_PUT_error(ERR_LIB_RAND,(f),(r),OPENSSL_FILE,OPENSSL_LINE) +# define DSOerr(f,r) ERR_PUT_error(ERR_LIB_DSO,(f),(r),OPENSSL_FILE,OPENSSL_LINE) +# define ENGINEerr(f,r) ERR_PUT_error(ERR_LIB_ENGINE,(f),(r),OPENSSL_FILE,OPENSSL_LINE) +# define OCSPerr(f,r) ERR_PUT_error(ERR_LIB_OCSP,(f),(r),OPENSSL_FILE,OPENSSL_LINE) +# define UIerr(f,r) ERR_PUT_error(ERR_LIB_UI,(f),(r),OPENSSL_FILE,OPENSSL_LINE) +# define COMPerr(f,r) ERR_PUT_error(ERR_LIB_COMP,(f),(r),OPENSSL_FILE,OPENSSL_LINE) +# define ECDSAerr(f,r) ERR_PUT_error(ERR_LIB_ECDSA,(f),(r),OPENSSL_FILE,OPENSSL_LINE) +# define ECDHerr(f,r) ERR_PUT_error(ERR_LIB_ECDH,(f),(r),OPENSSL_FILE,OPENSSL_LINE) +# define STOREerr(f,r) ERR_PUT_error(ERR_LIB_STORE,(f),(r),OPENSSL_FILE,OPENSSL_LINE) +# define FIPSerr(f,r) ERR_PUT_error(ERR_LIB_FIPS,(f),(r),OPENSSL_FILE,OPENSSL_LINE) +# define CMSerr(f,r) ERR_PUT_error(ERR_LIB_CMS,(f),(r),OPENSSL_FILE,OPENSSL_LINE) +# define TSerr(f,r) ERR_PUT_error(ERR_LIB_TS,(f),(r),OPENSSL_FILE,OPENSSL_LINE) +# define HMACerr(f,r) ERR_PUT_error(ERR_LIB_HMAC,(f),(r),OPENSSL_FILE,OPENSSL_LINE) +# define CTerr(f,r) ERR_PUT_error(ERR_LIB_CT,(f),(r),OPENSSL_FILE,OPENSSL_LINE) +# define ASYNCerr(f,r) ERR_PUT_error(ERR_LIB_ASYNC,(f),(r),OPENSSL_FILE,OPENSSL_LINE) +# define KDFerr(f,r) ERR_PUT_error(ERR_LIB_KDF,(f),(r),OPENSSL_FILE,OPENSSL_LINE) + +# define ERR_PACK(l,f,r) ( \ + (((unsigned int)(l) & 0x0FF) << 24L) | \ + (((unsigned int)(f) & 0xFFF) << 12L) | \ + (((unsigned int)(r) & 0xFFF) ) ) +# define ERR_GET_LIB(l) (int)(((l) >> 24L) & 0x0FFL) +# define ERR_GET_FUNC(l) (int)(((l) >> 12L) & 0xFFFL) +# define ERR_GET_REASON(l) (int)( (l) & 0xFFFL) +# define ERR_FATAL_ERROR(l) (int)( (l) & ERR_R_FATAL) + +/* OS functions */ +# define SYS_F_FOPEN 1 +# define SYS_F_CONNECT 2 +# define SYS_F_GETSERVBYNAME 3 +# define SYS_F_SOCKET 4 +# define SYS_F_IOCTLSOCKET 5 +# define SYS_F_BIND 6 +# define SYS_F_LISTEN 7 +# define SYS_F_ACCEPT 8 +# define SYS_F_WSASTARTUP 9/* Winsock stuff */ +# define SYS_F_OPENDIR 10 +# define SYS_F_FREAD 11 +# define SYS_F_GETADDRINFO 12 +# define SYS_F_GETNAMEINFO 13 +# define SYS_F_SETSOCKOPT 14 +# define SYS_F_GETSOCKOPT 15 +# define SYS_F_GETSOCKNAME 16 +# define SYS_F_GETHOSTBYNAME 17 + +/* reasons */ +# define ERR_R_SYS_LIB ERR_LIB_SYS/* 2 */ +# define ERR_R_BN_LIB ERR_LIB_BN/* 3 */ +# define ERR_R_RSA_LIB ERR_LIB_RSA/* 4 */ +# define ERR_R_DH_LIB ERR_LIB_DH/* 5 */ +# define ERR_R_EVP_LIB ERR_LIB_EVP/* 6 */ +# define ERR_R_BUF_LIB ERR_LIB_BUF/* 7 */ +# define ERR_R_OBJ_LIB ERR_LIB_OBJ/* 8 */ +# define ERR_R_PEM_LIB ERR_LIB_PEM/* 9 */ +# define ERR_R_DSA_LIB ERR_LIB_DSA/* 10 */ +# define ERR_R_X509_LIB ERR_LIB_X509/* 11 */ +# define ERR_R_ASN1_LIB ERR_LIB_ASN1/* 13 */ +# define ERR_R_EC_LIB ERR_LIB_EC/* 16 */ +# define ERR_R_BIO_LIB ERR_LIB_BIO/* 32 */ +# define ERR_R_PKCS7_LIB ERR_LIB_PKCS7/* 33 */ +# define ERR_R_X509V3_LIB ERR_LIB_X509V3/* 34 */ +# define ERR_R_ENGINE_LIB ERR_LIB_ENGINE/* 38 */ +# define ERR_R_ECDSA_LIB ERR_LIB_ECDSA/* 42 */ + +# define ERR_R_NESTED_ASN1_ERROR 58 +# define ERR_R_MISSING_ASN1_EOS 63 + +/* fatal error */ +# define ERR_R_FATAL 64 +# define ERR_R_MALLOC_FAILURE (1|ERR_R_FATAL) +# define ERR_R_SHOULD_NOT_HAVE_BEEN_CALLED (2|ERR_R_FATAL) +# define ERR_R_PASSED_NULL_PARAMETER (3|ERR_R_FATAL) +# define ERR_R_INTERNAL_ERROR (4|ERR_R_FATAL) +# define ERR_R_DISABLED (5|ERR_R_FATAL) +# define ERR_R_INIT_FAIL (6|ERR_R_FATAL) +# define ERR_R_PASSED_INVALID_ARGUMENT (7) + +/* + * 99 is the maximum possible ERR_R_... code, higher values are reserved for + * the individual libraries + */ + +typedef struct ERR_string_data_st { + unsigned long error; + const char *string; +} ERR_STRING_DATA; + +DEFINE_LHASH_OF(ERR_STRING_DATA); + +void ERR_put_error(int lib, int func, int reason, const char *file, int line); +void ERR_set_error_data(char *data, int flags); + +unsigned long ERR_get_error(void); +unsigned long ERR_get_error_line(const char **file, int *line); +unsigned long ERR_get_error_line_data(const char **file, int *line, + const char **data, int *flags); +unsigned long ERR_peek_error(void); +unsigned long ERR_peek_error_line(const char **file, int *line); +unsigned long ERR_peek_error_line_data(const char **file, int *line, + const char **data, int *flags); +unsigned long ERR_peek_last_error(void); +unsigned long ERR_peek_last_error_line(const char **file, int *line); +unsigned long ERR_peek_last_error_line_data(const char **file, int *line, + const char **data, int *flags); +void ERR_clear_error(void); +char *ERR_error_string(unsigned long e, char *buf); +void ERR_error_string_n(unsigned long e, char *buf, size_t len); +const char *ERR_lib_error_string(unsigned long e); +const char *ERR_func_error_string(unsigned long e); +const char *ERR_reason_error_string(unsigned long e); +void ERR_print_errors_cb(int (*cb) (const char *str, size_t len, void *u), + void *u); +# ifndef OPENSSL_NO_STDIO +void ERR_print_errors_fp(FILE *fp); +# endif +void ERR_print_errors(BIO *bp); +void ERR_add_error_data(int num, ...); +void ERR_add_error_vdata(int num, va_list args); +int ERR_load_strings(int lib, ERR_STRING_DATA str[]); +int ERR_unload_strings(int lib, ERR_STRING_DATA str[]); +int ERR_load_ERR_strings(void); + +#if OPENSSL_API_COMPAT < 0x10100000L +# define ERR_load_crypto_strings() \ + OPENSSL_init_crypto(OPENSSL_INIT_LOAD_CRYPTO_STRINGS, NULL) +# define ERR_free_strings() while(0) continue +#endif + +DEPRECATEDIN_1_1_0(void ERR_remove_thread_state(void *)) +DEPRECATEDIN_1_0_0(void ERR_remove_state(unsigned long pid)) +ERR_STATE *ERR_get_state(void); + +int ERR_get_next_error_library(void); + +int ERR_set_mark(void); +int ERR_pop_to_mark(void); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/mobile/ios/FxA/FxA/include/openssl/evp.h b/mobile/ios/FxA/FxA/include/openssl/evp.h new file mode 100644 index 0000000000..01f51b7be3 --- /dev/null +++ b/mobile/ios/FxA/FxA/include/openssl/evp.h @@ -0,0 +1,1590 @@ +/* + * Copyright 1995-2016 The OpenSSL Project Authors. All Rights Reserved. + * + * Licensed under the OpenSSL license (the "License"). You may not use + * this file except in compliance with the License. You can obtain a copy + * in the file LICENSE in the source distribution or at + * https://www.openssl.org/source/license.html + */ + +#ifndef HEADER_ENVELOPE_H +# define HEADER_ENVELOPE_H + +# include +# include +# include +# include + +# define EVP_MAX_MD_SIZE 64/* longest known is SHA512 */ +# define EVP_MAX_KEY_LENGTH 64 +# define EVP_MAX_IV_LENGTH 16 +# define EVP_MAX_BLOCK_LENGTH 32 + +# define PKCS5_SALT_LEN 8 +/* Default PKCS#5 iteration count */ +# define PKCS5_DEFAULT_ITER 2048 + +# include + +# define EVP_PK_RSA 0x0001 +# define EVP_PK_DSA 0x0002 +# define EVP_PK_DH 0x0004 +# define EVP_PK_EC 0x0008 +# define EVP_PKT_SIGN 0x0010 +# define EVP_PKT_ENC 0x0020 +# define EVP_PKT_EXCH 0x0040 +# define EVP_PKS_RSA 0x0100 +# define EVP_PKS_DSA 0x0200 +# define EVP_PKS_EC 0x0400 + +# define EVP_PKEY_NONE NID_undef +# define EVP_PKEY_RSA NID_rsaEncryption +# define EVP_PKEY_RSA2 NID_rsa +# define EVP_PKEY_DSA NID_dsa +# define EVP_PKEY_DSA1 NID_dsa_2 +# define EVP_PKEY_DSA2 NID_dsaWithSHA +# define EVP_PKEY_DSA3 NID_dsaWithSHA1 +# define EVP_PKEY_DSA4 NID_dsaWithSHA1_2 +# define EVP_PKEY_DH NID_dhKeyAgreement +# define EVP_PKEY_DHX NID_dhpublicnumber +# define EVP_PKEY_EC NID_X9_62_id_ecPublicKey +# define EVP_PKEY_HMAC NID_hmac +# define EVP_PKEY_CMAC NID_cmac +# define EVP_PKEY_TLS1_PRF NID_tls1_prf +# define EVP_PKEY_HKDF NID_hkdf + +#ifdef __cplusplus +extern "C" { +#endif + +# define EVP_PKEY_MO_SIGN 0x0001 +# define EVP_PKEY_MO_VERIFY 0x0002 +# define EVP_PKEY_MO_ENCRYPT 0x0004 +# define EVP_PKEY_MO_DECRYPT 0x0008 + +# ifndef EVP_MD +EVP_MD *EVP_MD_meth_new(int md_type, int pkey_type); +EVP_MD *EVP_MD_meth_dup(const EVP_MD *md); +void EVP_MD_meth_free(EVP_MD *md); + +int EVP_MD_meth_set_input_blocksize(EVP_MD *md, int blocksize); +int EVP_MD_meth_set_result_size(EVP_MD *md, int resultsize); +int EVP_MD_meth_set_app_datasize(EVP_MD *md, int datasize); +int EVP_MD_meth_set_flags(EVP_MD *md, unsigned long flags); +int EVP_MD_meth_set_init(EVP_MD *md, int (*init)(EVP_MD_CTX *ctx)); +int EVP_MD_meth_set_update(EVP_MD *md, int (*update)(EVP_MD_CTX *ctx, + const void *data, + size_t count)); +int EVP_MD_meth_set_final(EVP_MD *md, int (*final)(EVP_MD_CTX *ctx, + unsigned char *md)); +int EVP_MD_meth_set_copy(EVP_MD *md, int (*copy)(EVP_MD_CTX *to, + const EVP_MD_CTX *from)); +int EVP_MD_meth_set_cleanup(EVP_MD *md, int (*cleanup)(EVP_MD_CTX *ctx)); +int EVP_MD_meth_set_ctrl(EVP_MD *md, int (*ctrl)(EVP_MD_CTX *ctx, int cmd, + int p1, void *p2)); + +int EVP_MD_meth_get_input_blocksize(const EVP_MD *md); +int EVP_MD_meth_get_result_size(const EVP_MD *md); +int EVP_MD_meth_get_app_datasize(const EVP_MD *md); +unsigned long EVP_MD_meth_get_flags(const EVP_MD *md); +int (*EVP_MD_meth_get_init(const EVP_MD *md))(EVP_MD_CTX *ctx); +int (*EVP_MD_meth_get_update(const EVP_MD *md))(EVP_MD_CTX *ctx, + const void *data, + size_t count); +int (*EVP_MD_meth_get_final(const EVP_MD *md))(EVP_MD_CTX *ctx, + unsigned char *md); +int (*EVP_MD_meth_get_copy(const EVP_MD *md))(EVP_MD_CTX *to, + const EVP_MD_CTX *from); +int (*EVP_MD_meth_get_cleanup(const EVP_MD *md))(EVP_MD_CTX *ctx); +int (*EVP_MD_meth_get_ctrl(const EVP_MD *md))(EVP_MD_CTX *ctx, int cmd, + int p1, void *p2); + +/* digest can only handle a single block */ +# define EVP_MD_FLAG_ONESHOT 0x0001 + +/* DigestAlgorithmIdentifier flags... */ + +# define EVP_MD_FLAG_DIGALGID_MASK 0x0018 + +/* NULL or absent parameter accepted. Use NULL */ + +# define EVP_MD_FLAG_DIGALGID_NULL 0x0000 + +/* NULL or absent parameter accepted. Use NULL for PKCS#1 otherwise absent */ + +# define EVP_MD_FLAG_DIGALGID_ABSENT 0x0008 + +/* Custom handling via ctrl */ + +# define EVP_MD_FLAG_DIGALGID_CUSTOM 0x0018 + +/* Note if suitable for use in FIPS mode */ +# define EVP_MD_FLAG_FIPS 0x0400 + +/* Digest ctrls */ + +# define EVP_MD_CTRL_DIGALGID 0x1 +# define EVP_MD_CTRL_MICALG 0x2 + +/* Minimum Algorithm specific ctrl value */ + +# define EVP_MD_CTRL_ALG_CTRL 0x1000 + +# endif /* !EVP_MD */ + +/* values for EVP_MD_CTX flags */ + +# define EVP_MD_CTX_FLAG_ONESHOT 0x0001/* digest update will be + * called once only */ +# define EVP_MD_CTX_FLAG_CLEANED 0x0002/* context has already been + * cleaned */ +# define EVP_MD_CTX_FLAG_REUSE 0x0004/* Don't free up ctx->md_data + * in EVP_MD_CTX_reset */ +/* + * FIPS and pad options are ignored in 1.0.0, definitions are here so we + * don't accidentally reuse the values for other purposes. + */ + +# define EVP_MD_CTX_FLAG_NON_FIPS_ALLOW 0x0008/* Allow use of non FIPS + * digest in FIPS mode */ + +/* + * The following PAD options are also currently ignored in 1.0.0, digest + * parameters are handled through EVP_DigestSign*() and EVP_DigestVerify*() + * instead. + */ +# define EVP_MD_CTX_FLAG_PAD_MASK 0xF0/* RSA mode to use */ +# define EVP_MD_CTX_FLAG_PAD_PKCS1 0x00/* PKCS#1 v1.5 mode */ +# define EVP_MD_CTX_FLAG_PAD_X931 0x10/* X9.31 mode */ +# define EVP_MD_CTX_FLAG_PAD_PSS 0x20/* PSS mode */ + +# define EVP_MD_CTX_FLAG_NO_INIT 0x0100/* Don't initialize md_data */ +/* + * Some functions such as EVP_DigestSign only finalise copies of internal + * contexts so additional data can be included after the finalisation call. + * This is inefficient if this functionality is not required: it is disabled + * if the following flag is set. + */ +# define EVP_MD_CTX_FLAG_FINALISE 0x0200 + +EVP_CIPHER *EVP_CIPHER_meth_new(int cipher_type, int block_size, int key_len); +EVP_CIPHER *EVP_CIPHER_meth_dup(const EVP_CIPHER *cipher); +void EVP_CIPHER_meth_free(EVP_CIPHER *cipher); + +int EVP_CIPHER_meth_set_iv_length(EVP_CIPHER *cipher, int iv_len); +int EVP_CIPHER_meth_set_flags(EVP_CIPHER *cipher, unsigned long flags); +int EVP_CIPHER_meth_set_impl_ctx_size(EVP_CIPHER *cipher, int ctx_size); +int EVP_CIPHER_meth_set_init(EVP_CIPHER *cipher, + int (*init) (EVP_CIPHER_CTX *ctx, + const unsigned char *key, + const unsigned char *iv, + int enc)); +int EVP_CIPHER_meth_set_do_cipher(EVP_CIPHER *cipher, + int (*do_cipher) (EVP_CIPHER_CTX *ctx, + unsigned char *out, + const unsigned char *in, + size_t inl)); +int EVP_CIPHER_meth_set_cleanup(EVP_CIPHER *cipher, + int (*cleanup) (EVP_CIPHER_CTX *)); +int EVP_CIPHER_meth_set_set_asn1_params(EVP_CIPHER *cipher, + int (*set_asn1_parameters) (EVP_CIPHER_CTX *, + ASN1_TYPE *)); +int EVP_CIPHER_meth_set_get_asn1_params(EVP_CIPHER *cipher, + int (*get_asn1_parameters) (EVP_CIPHER_CTX *, + ASN1_TYPE *)); +int EVP_CIPHER_meth_set_ctrl(EVP_CIPHER *cipher, + int (*ctrl) (EVP_CIPHER_CTX *, int type, + int arg, void *ptr)); + +int (*EVP_CIPHER_meth_get_init(const EVP_CIPHER *cipher))(EVP_CIPHER_CTX *ctx, + const unsigned char *key, + const unsigned char *iv, + int enc); +int (*EVP_CIPHER_meth_get_do_cipher(const EVP_CIPHER *cipher))(EVP_CIPHER_CTX *ctx, + unsigned char *out, + const unsigned char *in, + size_t inl); +int (*EVP_CIPHER_meth_get_cleanup(const EVP_CIPHER *cipher))(EVP_CIPHER_CTX *); +int (*EVP_CIPHER_meth_get_set_asn1_params(const EVP_CIPHER *cipher))(EVP_CIPHER_CTX *, + ASN1_TYPE *); +int (*EVP_CIPHER_meth_get_get_asn1_params(const EVP_CIPHER *cipher))(EVP_CIPHER_CTX *, + ASN1_TYPE *); +int (*EVP_CIPHER_meth_get_ctrl(const EVP_CIPHER *cipher))(EVP_CIPHER_CTX *, + int type, int arg, + void *ptr); + +/* Values for cipher flags */ + +/* Modes for ciphers */ + +# define EVP_CIPH_STREAM_CIPHER 0x0 +# define EVP_CIPH_ECB_MODE 0x1 +# define EVP_CIPH_CBC_MODE 0x2 +# define EVP_CIPH_CFB_MODE 0x3 +# define EVP_CIPH_OFB_MODE 0x4 +# define EVP_CIPH_CTR_MODE 0x5 +# define EVP_CIPH_GCM_MODE 0x6 +# define EVP_CIPH_CCM_MODE 0x7 +# define EVP_CIPH_XTS_MODE 0x10001 +# define EVP_CIPH_WRAP_MODE 0x10002 +# define EVP_CIPH_OCB_MODE 0x10003 +# define EVP_CIPH_MODE 0xF0007 +/* Set if variable length cipher */ +# define EVP_CIPH_VARIABLE_LENGTH 0x8 +/* Set if the iv handling should be done by the cipher itself */ +# define EVP_CIPH_CUSTOM_IV 0x10 +/* Set if the cipher's init() function should be called if key is NULL */ +# define EVP_CIPH_ALWAYS_CALL_INIT 0x20 +/* Call ctrl() to init cipher parameters */ +# define EVP_CIPH_CTRL_INIT 0x40 +/* Don't use standard key length function */ +# define EVP_CIPH_CUSTOM_KEY_LENGTH 0x80 +/* Don't use standard block padding */ +# define EVP_CIPH_NO_PADDING 0x100 +/* cipher handles random key generation */ +# define EVP_CIPH_RAND_KEY 0x200 +/* cipher has its own additional copying logic */ +# define EVP_CIPH_CUSTOM_COPY 0x400 +/* Allow use default ASN1 get/set iv */ +# define EVP_CIPH_FLAG_DEFAULT_ASN1 0x1000 +/* Buffer length in bits not bytes: CFB1 mode only */ +# define EVP_CIPH_FLAG_LENGTH_BITS 0x2000 +/* Note if suitable for use in FIPS mode */ +# define EVP_CIPH_FLAG_FIPS 0x4000 +/* Allow non FIPS cipher in FIPS mode */ +# define EVP_CIPH_FLAG_NON_FIPS_ALLOW 0x8000 +/* + * Cipher handles any and all padding logic as well as finalisation. + */ +# define EVP_CIPH_FLAG_CUSTOM_CIPHER 0x100000 +# define EVP_CIPH_FLAG_AEAD_CIPHER 0x200000 +# define EVP_CIPH_FLAG_TLS1_1_MULTIBLOCK 0x400000 +/* Cipher can handle pipeline operations */ +# define EVP_CIPH_FLAG_PIPELINE 0X800000 + +/* + * Cipher context flag to indicate we can handle wrap mode: if allowed in + * older applications it could overflow buffers. + */ + +# define EVP_CIPHER_CTX_FLAG_WRAP_ALLOW 0x1 + +/* ctrl() values */ + +# define EVP_CTRL_INIT 0x0 +# define EVP_CTRL_SET_KEY_LENGTH 0x1 +# define EVP_CTRL_GET_RC2_KEY_BITS 0x2 +# define EVP_CTRL_SET_RC2_KEY_BITS 0x3 +# define EVP_CTRL_GET_RC5_ROUNDS 0x4 +# define EVP_CTRL_SET_RC5_ROUNDS 0x5 +# define EVP_CTRL_RAND_KEY 0x6 +# define EVP_CTRL_PBE_PRF_NID 0x7 +# define EVP_CTRL_COPY 0x8 +# define EVP_CTRL_AEAD_SET_IVLEN 0x9 +# define EVP_CTRL_AEAD_GET_TAG 0x10 +# define EVP_CTRL_AEAD_SET_TAG 0x11 +# define EVP_CTRL_AEAD_SET_IV_FIXED 0x12 +# define EVP_CTRL_GCM_SET_IVLEN EVP_CTRL_AEAD_SET_IVLEN +# define EVP_CTRL_GCM_GET_TAG EVP_CTRL_AEAD_GET_TAG +# define EVP_CTRL_GCM_SET_TAG EVP_CTRL_AEAD_SET_TAG +# define EVP_CTRL_GCM_SET_IV_FIXED EVP_CTRL_AEAD_SET_IV_FIXED +# define EVP_CTRL_GCM_IV_GEN 0x13 +# define EVP_CTRL_CCM_SET_IVLEN EVP_CTRL_AEAD_SET_IVLEN +# define EVP_CTRL_CCM_GET_TAG EVP_CTRL_AEAD_GET_TAG +# define EVP_CTRL_CCM_SET_TAG EVP_CTRL_AEAD_SET_TAG +# define EVP_CTRL_CCM_SET_IV_FIXED EVP_CTRL_AEAD_SET_IV_FIXED +# define EVP_CTRL_CCM_SET_L 0x14 +# define EVP_CTRL_CCM_SET_MSGLEN 0x15 +/* + * AEAD cipher deduces payload length and returns number of bytes required to + * store MAC and eventual padding. Subsequent call to EVP_Cipher even + * appends/verifies MAC. + */ +# define EVP_CTRL_AEAD_TLS1_AAD 0x16 +/* Used by composite AEAD ciphers, no-op in GCM, CCM... */ +# define EVP_CTRL_AEAD_SET_MAC_KEY 0x17 +/* Set the GCM invocation field, decrypt only */ +# define EVP_CTRL_GCM_SET_IV_INV 0x18 + +# define EVP_CTRL_TLS1_1_MULTIBLOCK_AAD 0x19 +# define EVP_CTRL_TLS1_1_MULTIBLOCK_ENCRYPT 0x1a +# define EVP_CTRL_TLS1_1_MULTIBLOCK_DECRYPT 0x1b +# define EVP_CTRL_TLS1_1_MULTIBLOCK_MAX_BUFSIZE 0x1c + +# define EVP_CTRL_SSL3_MASTER_SECRET 0x1d + +/* EVP_CTRL_SET_SBOX takes the char * specifying S-boxes */ +# define EVP_CTRL_SET_SBOX 0x1e +/* + * EVP_CTRL_SBOX_USED takes a 'size_t' and 'char *', pointing at a + * pre-allocated buffer with specified size + */ +# define EVP_CTRL_SBOX_USED 0x1f +/* EVP_CTRL_KEY_MESH takes 'size_t' number of bytes to mesh the key after, + * 0 switches meshing off + */ +# define EVP_CTRL_KEY_MESH 0x20 +/* EVP_CTRL_BLOCK_PADDING_MODE takes the padding mode */ +# define EVP_CTRL_BLOCK_PADDING_MODE 0x21 + +/* Set the output buffers to use for a pipelined operation */ +# define EVP_CTRL_SET_PIPELINE_OUTPUT_BUFS 0x22 +/* Set the input buffers to use for a pipelined operation */ +# define EVP_CTRL_SET_PIPELINE_INPUT_BUFS 0x23 +/* Set the input buffer lengths to use for a pipelined operation */ +# define EVP_CTRL_SET_PIPELINE_INPUT_LENS 0x24 + +/* Padding modes */ +#define EVP_PADDING_PKCS7 1 +#define EVP_PADDING_ISO7816_4 2 +#define EVP_PADDING_ANSI923 3 +#define EVP_PADDING_ISO10126 4 +#define EVP_PADDING_ZERO 5 + +/* RFC 5246 defines additional data to be 13 bytes in length */ +# define EVP_AEAD_TLS1_AAD_LEN 13 + +typedef struct { + unsigned char *out; + const unsigned char *inp; + size_t len; + unsigned int interleave; +} EVP_CTRL_TLS1_1_MULTIBLOCK_PARAM; + +/* GCM TLS constants */ +/* Length of fixed part of IV derived from PRF */ +# define EVP_GCM_TLS_FIXED_IV_LEN 4 +/* Length of explicit part of IV part of TLS records */ +# define EVP_GCM_TLS_EXPLICIT_IV_LEN 8 +/* Length of tag for TLS */ +# define EVP_GCM_TLS_TAG_LEN 16 + +/* CCM TLS constants */ +/* Length of fixed part of IV derived from PRF */ +# define EVP_CCM_TLS_FIXED_IV_LEN 4 +/* Length of explicit part of IV part of TLS records */ +# define EVP_CCM_TLS_EXPLICIT_IV_LEN 8 + +typedef struct evp_cipher_info_st { + const EVP_CIPHER *cipher; + unsigned char iv[EVP_MAX_IV_LENGTH]; +} EVP_CIPHER_INFO; + + +/* Password based encryption function */ +typedef int (EVP_PBE_KEYGEN) (EVP_CIPHER_CTX *ctx, const char *pass, + int passlen, ASN1_TYPE *param, + const EVP_CIPHER *cipher, const EVP_MD *md, + int en_de); + +# ifndef OPENSSL_NO_RSA +# define EVP_PKEY_assign_RSA(pkey,rsa) EVP_PKEY_assign((pkey),EVP_PKEY_RSA,\ + (char *)(rsa)) +# endif + +# ifndef OPENSSL_NO_DSA +# define EVP_PKEY_assign_DSA(pkey,dsa) EVP_PKEY_assign((pkey),EVP_PKEY_DSA,\ + (char *)(dsa)) +# endif + +# ifndef OPENSSL_NO_DH +# define EVP_PKEY_assign_DH(pkey,dh) EVP_PKEY_assign((pkey),EVP_PKEY_DH,\ + (char *)(dh)) +# endif + +# ifndef OPENSSL_NO_EC +# define EVP_PKEY_assign_EC_KEY(pkey,eckey) EVP_PKEY_assign((pkey),EVP_PKEY_EC,\ + (char *)(eckey)) +# endif + +/* Add some extra combinations */ +# define EVP_get_digestbynid(a) EVP_get_digestbyname(OBJ_nid2sn(a)) +# define EVP_get_digestbyobj(a) EVP_get_digestbynid(OBJ_obj2nid(a)) +# define EVP_get_cipherbynid(a) EVP_get_cipherbyname(OBJ_nid2sn(a)) +# define EVP_get_cipherbyobj(a) EVP_get_cipherbynid(OBJ_obj2nid(a)) + +int EVP_MD_type(const EVP_MD *md); +# define EVP_MD_nid(e) EVP_MD_type(e) +# define EVP_MD_name(e) OBJ_nid2sn(EVP_MD_nid(e)) +int EVP_MD_pkey_type(const EVP_MD *md); +int EVP_MD_size(const EVP_MD *md); +int EVP_MD_block_size(const EVP_MD *md); +unsigned long EVP_MD_flags(const EVP_MD *md); + +const EVP_MD *EVP_MD_CTX_md(const EVP_MD_CTX *ctx); +int (*EVP_MD_CTX_update_fn(EVP_MD_CTX *ctx))(EVP_MD_CTX *ctx, + const void *data, size_t count); +void EVP_MD_CTX_set_update_fn(EVP_MD_CTX *ctx, + int (*update) (EVP_MD_CTX *ctx, + const void *data, size_t count)); +# define EVP_MD_CTX_size(e) EVP_MD_size(EVP_MD_CTX_md(e)) +# define EVP_MD_CTX_block_size(e) EVP_MD_block_size(EVP_MD_CTX_md(e)) +# define EVP_MD_CTX_type(e) EVP_MD_type(EVP_MD_CTX_md(e)) +EVP_PKEY_CTX *EVP_MD_CTX_pkey_ctx(const EVP_MD_CTX *ctx); +void *EVP_MD_CTX_md_data(const EVP_MD_CTX *ctx); + +int EVP_CIPHER_nid(const EVP_CIPHER *cipher); +# define EVP_CIPHER_name(e) OBJ_nid2sn(EVP_CIPHER_nid(e)) +int EVP_CIPHER_block_size(const EVP_CIPHER *cipher); +int EVP_CIPHER_impl_ctx_size(const EVP_CIPHER *cipher); +int EVP_CIPHER_key_length(const EVP_CIPHER *cipher); +int EVP_CIPHER_iv_length(const EVP_CIPHER *cipher); +unsigned long EVP_CIPHER_flags(const EVP_CIPHER *cipher); +# define EVP_CIPHER_mode(e) (EVP_CIPHER_flags(e) & EVP_CIPH_MODE) + +const EVP_CIPHER *EVP_CIPHER_CTX_cipher(const EVP_CIPHER_CTX *ctx); +int EVP_CIPHER_CTX_encrypting(const EVP_CIPHER_CTX *ctx); +int EVP_CIPHER_CTX_nid(const EVP_CIPHER_CTX *ctx); +int EVP_CIPHER_CTX_block_size(const EVP_CIPHER_CTX *ctx); +int EVP_CIPHER_CTX_key_length(const EVP_CIPHER_CTX *ctx); +int EVP_CIPHER_CTX_iv_length(const EVP_CIPHER_CTX *ctx); +const unsigned char *EVP_CIPHER_CTX_iv(const EVP_CIPHER_CTX *ctx); +const unsigned char *EVP_CIPHER_CTX_original_iv(const EVP_CIPHER_CTX *ctx); +unsigned char *EVP_CIPHER_CTX_iv_noconst(EVP_CIPHER_CTX *ctx); +unsigned char *EVP_CIPHER_CTX_buf_noconst(EVP_CIPHER_CTX *ctx); +int EVP_CIPHER_CTX_num(const EVP_CIPHER_CTX *ctx); +void EVP_CIPHER_CTX_set_num(EVP_CIPHER_CTX *ctx, int num); +int EVP_CIPHER_CTX_copy(EVP_CIPHER_CTX *out, const EVP_CIPHER_CTX *in); +void *EVP_CIPHER_CTX_get_app_data(const EVP_CIPHER_CTX *ctx); +void EVP_CIPHER_CTX_set_app_data(EVP_CIPHER_CTX *ctx, void *data); +void *EVP_CIPHER_CTX_get_cipher_data(const EVP_CIPHER_CTX *ctx); +void *EVP_CIPHER_CTX_set_cipher_data(EVP_CIPHER_CTX *ctx, void *cipher_data); +# define EVP_CIPHER_CTX_type(c) EVP_CIPHER_type(EVP_CIPHER_CTX_cipher(c)) +# if OPENSSL_API_COMPAT < 0x10100000L +# define EVP_CIPHER_CTX_flags(c) EVP_CIPHER_flags(EVP_CIPHER_CTX_cipher(c)) +# endif +# define EVP_CIPHER_CTX_mode(c) EVP_CIPHER_mode(EVP_CIPHER_CTX_cipher(c)) + +# define EVP_ENCODE_LENGTH(l) (((l+2)/3*4)+(l/48+1)*2+80) +# define EVP_DECODE_LENGTH(l) ((l+3)/4*3+80) + +# define EVP_SignInit_ex(a,b,c) EVP_DigestInit_ex(a,b,c) +# define EVP_SignInit(a,b) EVP_DigestInit(a,b) +# define EVP_SignUpdate(a,b,c) EVP_DigestUpdate(a,b,c) +# define EVP_VerifyInit_ex(a,b,c) EVP_DigestInit_ex(a,b,c) +# define EVP_VerifyInit(a,b) EVP_DigestInit(a,b) +# define EVP_VerifyUpdate(a,b,c) EVP_DigestUpdate(a,b,c) +# define EVP_OpenUpdate(a,b,c,d,e) EVP_DecryptUpdate(a,b,c,d,e) +# define EVP_SealUpdate(a,b,c,d,e) EVP_EncryptUpdate(a,b,c,d,e) +# define EVP_DigestSignUpdate(a,b,c) EVP_DigestUpdate(a,b,c) +# define EVP_DigestVerifyUpdate(a,b,c) EVP_DigestUpdate(a,b,c) + +# ifdef CONST_STRICT +void BIO_set_md(BIO *, const EVP_MD *md); +# else +# define BIO_set_md(b,md) BIO_ctrl(b,BIO_C_SET_MD,0,(char *)md) +# endif +# define BIO_get_md(b,mdp) BIO_ctrl(b,BIO_C_GET_MD,0,(char *)mdp) +# define BIO_get_md_ctx(b,mdcp) BIO_ctrl(b,BIO_C_GET_MD_CTX,0,(char *)mdcp) +# define BIO_set_md_ctx(b,mdcp) BIO_ctrl(b,BIO_C_SET_MD_CTX,0,(char *)mdcp) +# define BIO_get_cipher_status(b) BIO_ctrl(b,BIO_C_GET_CIPHER_STATUS,0,NULL) +# define BIO_get_cipher_ctx(b,c_pp) BIO_ctrl(b,BIO_C_GET_CIPHER_CTX,0,(char *)c_pp) + +/*__owur*/ int EVP_Cipher(EVP_CIPHER_CTX *c, + unsigned char *out, + const unsigned char *in, unsigned int inl); + +# define EVP_add_cipher_alias(n,alias) \ + OBJ_NAME_add((alias),OBJ_NAME_TYPE_CIPHER_METH|OBJ_NAME_ALIAS,(n)) +# define EVP_add_digest_alias(n,alias) \ + OBJ_NAME_add((alias),OBJ_NAME_TYPE_MD_METH|OBJ_NAME_ALIAS,(n)) +# define EVP_delete_cipher_alias(alias) \ + OBJ_NAME_remove(alias,OBJ_NAME_TYPE_CIPHER_METH|OBJ_NAME_ALIAS); +# define EVP_delete_digest_alias(alias) \ + OBJ_NAME_remove(alias,OBJ_NAME_TYPE_MD_METH|OBJ_NAME_ALIAS); + +int EVP_MD_CTX_ctrl(EVP_MD_CTX *ctx, int cmd, int p1, void *p2); +EVP_MD_CTX *EVP_MD_CTX_new(void); +int EVP_MD_CTX_reset(EVP_MD_CTX *ctx); +void EVP_MD_CTX_free(EVP_MD_CTX *ctx); +# define EVP_MD_CTX_create() EVP_MD_CTX_new() +# define EVP_MD_CTX_init(ctx) EVP_MD_CTX_reset((ctx)) +# define EVP_MD_CTX_destroy(ctx) EVP_MD_CTX_free((ctx)) +__owur int EVP_MD_CTX_copy_ex(EVP_MD_CTX *out, const EVP_MD_CTX *in); +void EVP_MD_CTX_set_flags(EVP_MD_CTX *ctx, int flags); +void EVP_MD_CTX_clear_flags(EVP_MD_CTX *ctx, int flags); +int EVP_MD_CTX_test_flags(const EVP_MD_CTX *ctx, int flags); +__owur int EVP_DigestInit_ex(EVP_MD_CTX *ctx, const EVP_MD *type, + ENGINE *impl); +__owur int EVP_DigestUpdate(EVP_MD_CTX *ctx, const void *d, + size_t cnt); +__owur int EVP_DigestFinal_ex(EVP_MD_CTX *ctx, unsigned char *md, + unsigned int *s); +__owur int EVP_Digest(const void *data, size_t count, + unsigned char *md, unsigned int *size, + const EVP_MD *type, ENGINE *impl); + +__owur int EVP_MD_CTX_copy(EVP_MD_CTX *out, const EVP_MD_CTX *in); +__owur int EVP_DigestInit(EVP_MD_CTX *ctx, const EVP_MD *type); +__owur int EVP_DigestFinal(EVP_MD_CTX *ctx, unsigned char *md, + unsigned int *s); + +#ifndef OPENSSL_NO_UI +int EVP_read_pw_string(char *buf, int length, const char *prompt, int verify); +int EVP_read_pw_string_min(char *buf, int minlen, int maxlen, + const char *prompt, int verify); +void EVP_set_pw_prompt(const char *prompt); +char *EVP_get_pw_prompt(void); +#endif + +__owur int EVP_BytesToKey(const EVP_CIPHER *type, const EVP_MD *md, + const unsigned char *salt, + const unsigned char *data, int datal, int count, + unsigned char *key, unsigned char *iv); + +void EVP_CIPHER_CTX_set_flags(EVP_CIPHER_CTX *ctx, int flags); +void EVP_CIPHER_CTX_clear_flags(EVP_CIPHER_CTX *ctx, int flags); +int EVP_CIPHER_CTX_test_flags(const EVP_CIPHER_CTX *ctx, int flags); + +__owur int EVP_EncryptInit(EVP_CIPHER_CTX *ctx, const EVP_CIPHER *cipher, + const unsigned char *key, const unsigned char *iv); +/*__owur*/ int EVP_EncryptInit_ex(EVP_CIPHER_CTX *ctx, + const EVP_CIPHER *cipher, ENGINE *impl, + const unsigned char *key, + const unsigned char *iv); +/*__owur*/ int EVP_EncryptUpdate(EVP_CIPHER_CTX *ctx, unsigned char *out, + int *outl, const unsigned char *in, int inl); +/*__owur*/ int EVP_EncryptFinal_ex(EVP_CIPHER_CTX *ctx, unsigned char *out, + int *outl); +/*__owur*/ int EVP_EncryptFinal(EVP_CIPHER_CTX *ctx, unsigned char *out, + int *outl); + +__owur int EVP_DecryptInit(EVP_CIPHER_CTX *ctx, const EVP_CIPHER *cipher, + const unsigned char *key, const unsigned char *iv); +/*__owur*/ int EVP_DecryptInit_ex(EVP_CIPHER_CTX *ctx, + const EVP_CIPHER *cipher, ENGINE *impl, + const unsigned char *key, + const unsigned char *iv); +/*__owur*/ int EVP_DecryptUpdate(EVP_CIPHER_CTX *ctx, unsigned char *out, + int *outl, const unsigned char *in, int inl); +__owur int EVP_DecryptFinal(EVP_CIPHER_CTX *ctx, unsigned char *outm, + int *outl); +/*__owur*/ int EVP_DecryptFinal_ex(EVP_CIPHER_CTX *ctx, unsigned char *outm, + int *outl); + +__owur int EVP_CipherInit(EVP_CIPHER_CTX *ctx, const EVP_CIPHER *cipher, + const unsigned char *key, const unsigned char *iv, + int enc); +/*__owur*/ int EVP_CipherInit_ex(EVP_CIPHER_CTX *ctx, + const EVP_CIPHER *cipher, ENGINE *impl, + const unsigned char *key, + const unsigned char *iv, int enc); +__owur int EVP_CipherUpdate(EVP_CIPHER_CTX *ctx, unsigned char *out, + int *outl, const unsigned char *in, int inl); +__owur int EVP_CipherFinal(EVP_CIPHER_CTX *ctx, unsigned char *outm, + int *outl); +__owur int EVP_CipherFinal_ex(EVP_CIPHER_CTX *ctx, unsigned char *outm, + int *outl); + +__owur int EVP_SignFinal(EVP_MD_CTX *ctx, unsigned char *md, unsigned int *s, + EVP_PKEY *pkey); + +__owur int EVP_VerifyFinal(EVP_MD_CTX *ctx, const unsigned char *sigbuf, + unsigned int siglen, EVP_PKEY *pkey); + +/*__owur*/ int EVP_DigestSignInit(EVP_MD_CTX *ctx, EVP_PKEY_CTX **pctx, + const EVP_MD *type, ENGINE *e, + EVP_PKEY *pkey); +__owur int EVP_DigestSignFinal(EVP_MD_CTX *ctx, unsigned char *sigret, + size_t *siglen); + +__owur int EVP_DigestVerifyInit(EVP_MD_CTX *ctx, EVP_PKEY_CTX **pctx, + const EVP_MD *type, ENGINE *e, + EVP_PKEY *pkey); +__owur int EVP_DigestVerifyFinal(EVP_MD_CTX *ctx, const unsigned char *sig, + size_t siglen); + +# ifndef OPENSSL_NO_RSA +__owur int EVP_OpenInit(EVP_CIPHER_CTX *ctx, const EVP_CIPHER *type, + const unsigned char *ek, int ekl, + const unsigned char *iv, EVP_PKEY *priv); +__owur int EVP_OpenFinal(EVP_CIPHER_CTX *ctx, unsigned char *out, int *outl); + +__owur int EVP_SealInit(EVP_CIPHER_CTX *ctx, const EVP_CIPHER *type, + unsigned char **ek, int *ekl, unsigned char *iv, + EVP_PKEY **pubk, int npubk); +__owur int EVP_SealFinal(EVP_CIPHER_CTX *ctx, unsigned char *out, int *outl); +# endif + +EVP_ENCODE_CTX *EVP_ENCODE_CTX_new(void); +void EVP_ENCODE_CTX_free(EVP_ENCODE_CTX *ctx); +int EVP_ENCODE_CTX_copy(EVP_ENCODE_CTX *dctx, EVP_ENCODE_CTX *sctx); +int EVP_ENCODE_CTX_num(EVP_ENCODE_CTX *ctx); +void EVP_EncodeInit(EVP_ENCODE_CTX *ctx); +int EVP_EncodeUpdate(EVP_ENCODE_CTX *ctx, unsigned char *out, int *outl, + const unsigned char *in, int inl); +void EVP_EncodeFinal(EVP_ENCODE_CTX *ctx, unsigned char *out, int *outl); +int EVP_EncodeBlock(unsigned char *t, const unsigned char *f, int n); + +void EVP_DecodeInit(EVP_ENCODE_CTX *ctx); +int EVP_DecodeUpdate(EVP_ENCODE_CTX *ctx, unsigned char *out, int *outl, + const unsigned char *in, int inl); +int EVP_DecodeFinal(EVP_ENCODE_CTX *ctx, unsigned + char *out, int *outl); +int EVP_DecodeBlock(unsigned char *t, const unsigned char *f, int n); + +# if OPENSSL_API_COMPAT < 0x10100000L +# define EVP_CIPHER_CTX_init(c) EVP_CIPHER_CTX_reset(c) +# define EVP_CIPHER_CTX_cleanup(c) EVP_CIPHER_CTX_reset(c) +# endif +EVP_CIPHER_CTX *EVP_CIPHER_CTX_new(void); +int EVP_CIPHER_CTX_reset(EVP_CIPHER_CTX *c); +void EVP_CIPHER_CTX_free(EVP_CIPHER_CTX *c); +int EVP_CIPHER_CTX_set_key_length(EVP_CIPHER_CTX *x, int keylen); +int EVP_CIPHER_CTX_set_padding(EVP_CIPHER_CTX *c, int pad); +int EVP_CIPHER_CTX_ctrl(EVP_CIPHER_CTX *ctx, int type, int arg, void *ptr); +int EVP_CIPHER_CTX_rand_key(EVP_CIPHER_CTX *ctx, unsigned char *key); + +const BIO_METHOD *BIO_f_md(void); +const BIO_METHOD *BIO_f_base64(void); +const BIO_METHOD *BIO_f_cipher(void); +const BIO_METHOD *BIO_f_reliable(void); +__owur int BIO_set_cipher(BIO *b, const EVP_CIPHER *c, const unsigned char *k, + const unsigned char *i, int enc); + +const EVP_MD *EVP_md_null(void); +# ifndef OPENSSL_NO_MD2 +const EVP_MD *EVP_md2(void); +# endif +# ifndef OPENSSL_NO_MD4 +const EVP_MD *EVP_md4(void); +# endif +# ifndef OPENSSL_NO_MD5 +const EVP_MD *EVP_md5(void); +const EVP_MD *EVP_md5_sha1(void); +# endif +# ifndef OPENSSL_NO_BLAKE2 +const EVP_MD *EVP_blake2b512(void); +const EVP_MD *EVP_blake2s256(void); +# endif +const EVP_MD *EVP_sha1(void); +const EVP_MD *EVP_sha224(void); +const EVP_MD *EVP_sha256(void); +const EVP_MD *EVP_sha384(void); +const EVP_MD *EVP_sha512(void); +# ifndef OPENSSL_NO_MDC2 +const EVP_MD *EVP_mdc2(void); +# endif +# ifndef OPENSSL_NO_RMD160 +const EVP_MD *EVP_ripemd160(void); +# endif +# ifndef OPENSSL_NO_WHIRLPOOL +const EVP_MD *EVP_whirlpool(void); +# endif +const EVP_CIPHER *EVP_enc_null(void); /* does nothing :-) */ +# ifndef OPENSSL_NO_DES +const EVP_CIPHER *EVP_des_ecb(void); +const EVP_CIPHER *EVP_des_ede(void); +const EVP_CIPHER *EVP_des_ede3(void); +const EVP_CIPHER *EVP_des_ede_ecb(void); +const EVP_CIPHER *EVP_des_ede3_ecb(void); +const EVP_CIPHER *EVP_des_cfb64(void); +# define EVP_des_cfb EVP_des_cfb64 +const EVP_CIPHER *EVP_des_cfb1(void); +const EVP_CIPHER *EVP_des_cfb8(void); +const EVP_CIPHER *EVP_des_ede_cfb64(void); +# define EVP_des_ede_cfb EVP_des_ede_cfb64 +const EVP_CIPHER *EVP_des_ede3_cfb64(void); +# define EVP_des_ede3_cfb EVP_des_ede3_cfb64 +const EVP_CIPHER *EVP_des_ede3_cfb1(void); +const EVP_CIPHER *EVP_des_ede3_cfb8(void); +const EVP_CIPHER *EVP_des_ofb(void); +const EVP_CIPHER *EVP_des_ede_ofb(void); +const EVP_CIPHER *EVP_des_ede3_ofb(void); +const EVP_CIPHER *EVP_des_cbc(void); +const EVP_CIPHER *EVP_des_ede_cbc(void); +const EVP_CIPHER *EVP_des_ede3_cbc(void); +const EVP_CIPHER *EVP_desx_cbc(void); +const EVP_CIPHER *EVP_des_ede3_wrap(void); +/* + * This should now be supported through the dev_crypto ENGINE. But also, why + * are rc4 and md5 declarations made here inside a "NO_DES" precompiler + * branch? + */ +# endif +# ifndef OPENSSL_NO_RC4 +const EVP_CIPHER *EVP_rc4(void); +const EVP_CIPHER *EVP_rc4_40(void); +# ifndef OPENSSL_NO_MD5 +const EVP_CIPHER *EVP_rc4_hmac_md5(void); +# endif +# endif +# ifndef OPENSSL_NO_IDEA +const EVP_CIPHER *EVP_idea_ecb(void); +const EVP_CIPHER *EVP_idea_cfb64(void); +# define EVP_idea_cfb EVP_idea_cfb64 +const EVP_CIPHER *EVP_idea_ofb(void); +const EVP_CIPHER *EVP_idea_cbc(void); +# endif +# ifndef OPENSSL_NO_RC2 +const EVP_CIPHER *EVP_rc2_ecb(void); +const EVP_CIPHER *EVP_rc2_cbc(void); +const EVP_CIPHER *EVP_rc2_40_cbc(void); +const EVP_CIPHER *EVP_rc2_64_cbc(void); +const EVP_CIPHER *EVP_rc2_cfb64(void); +# define EVP_rc2_cfb EVP_rc2_cfb64 +const EVP_CIPHER *EVP_rc2_ofb(void); +# endif +# ifndef OPENSSL_NO_BF +const EVP_CIPHER *EVP_bf_ecb(void); +const EVP_CIPHER *EVP_bf_cbc(void); +const EVP_CIPHER *EVP_bf_cfb64(void); +# define EVP_bf_cfb EVP_bf_cfb64 +const EVP_CIPHER *EVP_bf_ofb(void); +# endif +# ifndef OPENSSL_NO_CAST +const EVP_CIPHER *EVP_cast5_ecb(void); +const EVP_CIPHER *EVP_cast5_cbc(void); +const EVP_CIPHER *EVP_cast5_cfb64(void); +# define EVP_cast5_cfb EVP_cast5_cfb64 +const EVP_CIPHER *EVP_cast5_ofb(void); +# endif +# ifndef OPENSSL_NO_RC5 +const EVP_CIPHER *EVP_rc5_32_12_16_cbc(void); +const EVP_CIPHER *EVP_rc5_32_12_16_ecb(void); +const EVP_CIPHER *EVP_rc5_32_12_16_cfb64(void); +# define EVP_rc5_32_12_16_cfb EVP_rc5_32_12_16_cfb64 +const EVP_CIPHER *EVP_rc5_32_12_16_ofb(void); +# endif +const EVP_CIPHER *EVP_aes_128_ecb(void); +const EVP_CIPHER *EVP_aes_128_cbc(void); +const EVP_CIPHER *EVP_aes_128_cfb1(void); +const EVP_CIPHER *EVP_aes_128_cfb8(void); +const EVP_CIPHER *EVP_aes_128_cfb128(void); +# define EVP_aes_128_cfb EVP_aes_128_cfb128 +const EVP_CIPHER *EVP_aes_128_ofb(void); +const EVP_CIPHER *EVP_aes_128_ctr(void); +const EVP_CIPHER *EVP_aes_128_ccm(void); +const EVP_CIPHER *EVP_aes_128_gcm(void); +const EVP_CIPHER *EVP_aes_128_xts(void); +const EVP_CIPHER *EVP_aes_128_wrap(void); +const EVP_CIPHER *EVP_aes_128_wrap_pad(void); +# ifndef OPENSSL_NO_OCB +const EVP_CIPHER *EVP_aes_128_ocb(void); +# endif +const EVP_CIPHER *EVP_aes_192_ecb(void); +const EVP_CIPHER *EVP_aes_192_cbc(void); +const EVP_CIPHER *EVP_aes_192_cfb1(void); +const EVP_CIPHER *EVP_aes_192_cfb8(void); +const EVP_CIPHER *EVP_aes_192_cfb128(void); +# define EVP_aes_192_cfb EVP_aes_192_cfb128 +const EVP_CIPHER *EVP_aes_192_ofb(void); +const EVP_CIPHER *EVP_aes_192_ctr(void); +const EVP_CIPHER *EVP_aes_192_ccm(void); +const EVP_CIPHER *EVP_aes_192_gcm(void); +const EVP_CIPHER *EVP_aes_192_wrap(void); +const EVP_CIPHER *EVP_aes_192_wrap_pad(void); +# ifndef OPENSSL_NO_OCB +const EVP_CIPHER *EVP_aes_192_ocb(void); +# endif +const EVP_CIPHER *EVP_aes_256_ecb(void); +const EVP_CIPHER *EVP_aes_256_cbc(void); +const EVP_CIPHER *EVP_aes_256_cfb1(void); +const EVP_CIPHER *EVP_aes_256_cfb8(void); +const EVP_CIPHER *EVP_aes_256_cfb128(void); +# define EVP_aes_256_cfb EVP_aes_256_cfb128 +const EVP_CIPHER *EVP_aes_256_ofb(void); +const EVP_CIPHER *EVP_aes_256_ctr(void); +const EVP_CIPHER *EVP_aes_256_ccm(void); +const EVP_CIPHER *EVP_aes_256_gcm(void); +const EVP_CIPHER *EVP_aes_256_xts(void); +const EVP_CIPHER *EVP_aes_256_wrap(void); +const EVP_CIPHER *EVP_aes_256_wrap_pad(void); +# ifndef OPENSSL_NO_OCB +const EVP_CIPHER *EVP_aes_256_ocb(void); +# endif +const EVP_CIPHER *EVP_aes_128_cbc_hmac_sha1(void); +const EVP_CIPHER *EVP_aes_256_cbc_hmac_sha1(void); +const EVP_CIPHER *EVP_aes_128_cbc_hmac_sha256(void); +const EVP_CIPHER *EVP_aes_256_cbc_hmac_sha256(void); +# ifndef OPENSSL_NO_CAMELLIA +const EVP_CIPHER *EVP_camellia_128_ecb(void); +const EVP_CIPHER *EVP_camellia_128_cbc(void); +const EVP_CIPHER *EVP_camellia_128_cfb1(void); +const EVP_CIPHER *EVP_camellia_128_cfb8(void); +const EVP_CIPHER *EVP_camellia_128_cfb128(void); +# define EVP_camellia_128_cfb EVP_camellia_128_cfb128 +const EVP_CIPHER *EVP_camellia_128_ofb(void); +const EVP_CIPHER *EVP_camellia_128_ctr(void); +const EVP_CIPHER *EVP_camellia_192_ecb(void); +const EVP_CIPHER *EVP_camellia_192_cbc(void); +const EVP_CIPHER *EVP_camellia_192_cfb1(void); +const EVP_CIPHER *EVP_camellia_192_cfb8(void); +const EVP_CIPHER *EVP_camellia_192_cfb128(void); +# define EVP_camellia_192_cfb EVP_camellia_192_cfb128 +const EVP_CIPHER *EVP_camellia_192_ofb(void); +const EVP_CIPHER *EVP_camellia_192_ctr(void); +const EVP_CIPHER *EVP_camellia_256_ecb(void); +const EVP_CIPHER *EVP_camellia_256_cbc(void); +const EVP_CIPHER *EVP_camellia_256_cfb1(void); +const EVP_CIPHER *EVP_camellia_256_cfb8(void); +const EVP_CIPHER *EVP_camellia_256_cfb128(void); +# define EVP_camellia_256_cfb EVP_camellia_256_cfb128 +const EVP_CIPHER *EVP_camellia_256_ofb(void); +const EVP_CIPHER *EVP_camellia_256_ctr(void); +# endif +# ifndef OPENSSL_NO_CHACHA +const EVP_CIPHER *EVP_chacha20(void); +# ifndef OPENSSL_NO_POLY1305 +const EVP_CIPHER *EVP_chacha20_poly1305(void); +# endif +# endif + +# ifndef OPENSSL_NO_SEED +const EVP_CIPHER *EVP_seed_ecb(void); +const EVP_CIPHER *EVP_seed_cbc(void); +const EVP_CIPHER *EVP_seed_cfb128(void); +# define EVP_seed_cfb EVP_seed_cfb128 +const EVP_CIPHER *EVP_seed_ofb(void); +# endif + +# if OPENSSL_API_COMPAT < 0x10100000L +# define OPENSSL_add_all_algorithms_conf() \ + OPENSSL_init_crypto(OPENSSL_INIT_ADD_ALL_CIPHERS \ + | OPENSSL_INIT_ADD_ALL_DIGESTS \ + | OPENSSL_INIT_LOAD_CONFIG, NULL) +# define OPENSSL_add_all_algorithms_noconf() \ + OPENSSL_init_crypto(OPENSSL_INIT_ADD_ALL_CIPHERS \ + | OPENSSL_INIT_ADD_ALL_DIGESTS, NULL) + +# ifdef OPENSSL_LOAD_CONF +# define OpenSSL_add_all_algorithms() \ + OPENSSL_init_crypto(OPENSSL_INIT_ADD_ALL_CIPHERS \ + | OPENSSL_INIT_ADD_ALL_DIGESTS \ + | OPENSSL_INIT_LOAD_CONFIG, NULL) +# else +# define OpenSSL_add_all_algorithms() \ + OPENSSL_init_crypto(OPENSSL_INIT_ADD_ALL_CIPHERS \ + | OPENSSL_INIT_ADD_ALL_DIGESTS, NULL) +# endif + +# define OpenSSL_add_all_ciphers() \ + OPENSSL_init_crypto(OPENSSL_INIT_ADD_ALL_CIPHERS, NULL) +# define OpenSSL_add_all_digests() \ + OPENSSL_init_crypto(OPENSSL_INIT_ADD_ALL_DIGESTS, NULL) + +# define EVP_cleanup() while(0) continue +# endif + +int EVP_add_cipher(const EVP_CIPHER *cipher); +int EVP_add_digest(const EVP_MD *digest); + +const EVP_CIPHER *EVP_get_cipherbyname(const char *name); +const EVP_MD *EVP_get_digestbyname(const char *name); + +void EVP_CIPHER_do_all(void (*fn) (const EVP_CIPHER *ciph, + const char *from, const char *to, void *x), + void *arg); +void EVP_CIPHER_do_all_sorted(void (*fn) + (const EVP_CIPHER *ciph, const char *from, + const char *to, void *x), void *arg); + +void EVP_MD_do_all(void (*fn) (const EVP_MD *ciph, + const char *from, const char *to, void *x), + void *arg); +void EVP_MD_do_all_sorted(void (*fn) + (const EVP_MD *ciph, const char *from, + const char *to, void *x), void *arg); + +int EVP_PKEY_decrypt_old(unsigned char *dec_key, + const unsigned char *enc_key, int enc_key_len, + EVP_PKEY *private_key); +int EVP_PKEY_encrypt_old(unsigned char *enc_key, + const unsigned char *key, int key_len, + EVP_PKEY *pub_key); +int EVP_PKEY_type(int type); +int EVP_PKEY_id(const EVP_PKEY *pkey); +int EVP_PKEY_base_id(const EVP_PKEY *pkey); +int EVP_PKEY_bits(const EVP_PKEY *pkey); +int EVP_PKEY_security_bits(const EVP_PKEY *pkey); +int EVP_PKEY_size(EVP_PKEY *pkey); +int EVP_PKEY_set_type(EVP_PKEY *pkey, int type); +int EVP_PKEY_set_type_str(EVP_PKEY *pkey, const char *str, int len); +int EVP_PKEY_assign(EVP_PKEY *pkey, int type, void *key); +void *EVP_PKEY_get0(const EVP_PKEY *pkey); +const unsigned char *EVP_PKEY_get0_hmac(const EVP_PKEY *pkey, size_t *len); + +# ifndef OPENSSL_NO_RSA +struct rsa_st; +int EVP_PKEY_set1_RSA(EVP_PKEY *pkey, struct rsa_st *key); +struct rsa_st *EVP_PKEY_get0_RSA(EVP_PKEY *pkey); +struct rsa_st *EVP_PKEY_get1_RSA(EVP_PKEY *pkey); +# endif +# ifndef OPENSSL_NO_DSA +struct dsa_st; +int EVP_PKEY_set1_DSA(EVP_PKEY *pkey, struct dsa_st *key); +struct dsa_st *EVP_PKEY_get0_DSA(EVP_PKEY *pkey); +struct dsa_st *EVP_PKEY_get1_DSA(EVP_PKEY *pkey); +# endif +# ifndef OPENSSL_NO_DH +struct dh_st; +int EVP_PKEY_set1_DH(EVP_PKEY *pkey, struct dh_st *key); +struct dh_st *EVP_PKEY_get0_DH(EVP_PKEY *pkey); +struct dh_st *EVP_PKEY_get1_DH(EVP_PKEY *pkey); +# endif +# ifndef OPENSSL_NO_EC +struct ec_key_st; +int EVP_PKEY_set1_EC_KEY(EVP_PKEY *pkey, struct ec_key_st *key); +struct ec_key_st *EVP_PKEY_get0_EC_KEY(EVP_PKEY *pkey); +struct ec_key_st *EVP_PKEY_get1_EC_KEY(EVP_PKEY *pkey); +# endif + +EVP_PKEY *EVP_PKEY_new(void); +int EVP_PKEY_up_ref(EVP_PKEY *pkey); +void EVP_PKEY_free(EVP_PKEY *pkey); + +EVP_PKEY *d2i_PublicKey(int type, EVP_PKEY **a, const unsigned char **pp, + long length); +int i2d_PublicKey(EVP_PKEY *a, unsigned char **pp); + +EVP_PKEY *d2i_PrivateKey(int type, EVP_PKEY **a, const unsigned char **pp, + long length); +EVP_PKEY *d2i_AutoPrivateKey(EVP_PKEY **a, const unsigned char **pp, + long length); +int i2d_PrivateKey(EVP_PKEY *a, unsigned char **pp); + +int EVP_PKEY_copy_parameters(EVP_PKEY *to, const EVP_PKEY *from); +int EVP_PKEY_missing_parameters(const EVP_PKEY *pkey); +int EVP_PKEY_save_parameters(EVP_PKEY *pkey, int mode); +int EVP_PKEY_cmp_parameters(const EVP_PKEY *a, const EVP_PKEY *b); + +int EVP_PKEY_cmp(const EVP_PKEY *a, const EVP_PKEY *b); + +int EVP_PKEY_print_public(BIO *out, const EVP_PKEY *pkey, + int indent, ASN1_PCTX *pctx); +int EVP_PKEY_print_private(BIO *out, const EVP_PKEY *pkey, + int indent, ASN1_PCTX *pctx); +int EVP_PKEY_print_params(BIO *out, const EVP_PKEY *pkey, + int indent, ASN1_PCTX *pctx); + +int EVP_PKEY_get_default_digest_nid(EVP_PKEY *pkey, int *pnid); + +int EVP_PKEY_set1_tls_encodedpoint(EVP_PKEY *pkey, + const unsigned char *pt, size_t ptlen); +size_t EVP_PKEY_get1_tls_encodedpoint(EVP_PKEY *pkey, unsigned char **ppt); + +int EVP_CIPHER_type(const EVP_CIPHER *ctx); + +/* calls methods */ +int EVP_CIPHER_param_to_asn1(EVP_CIPHER_CTX *c, ASN1_TYPE *type); +int EVP_CIPHER_asn1_to_param(EVP_CIPHER_CTX *c, ASN1_TYPE *type); + +/* These are used by EVP_CIPHER methods */ +int EVP_CIPHER_set_asn1_iv(EVP_CIPHER_CTX *c, ASN1_TYPE *type); +int EVP_CIPHER_get_asn1_iv(EVP_CIPHER_CTX *c, ASN1_TYPE *type); + +/* PKCS5 password based encryption */ +int PKCS5_PBE_keyivgen(EVP_CIPHER_CTX *ctx, const char *pass, int passlen, + ASN1_TYPE *param, const EVP_CIPHER *cipher, + const EVP_MD *md, int en_de); +int PKCS5_PBKDF2_HMAC_SHA1(const char *pass, int passlen, + const unsigned char *salt, int saltlen, int iter, + int keylen, unsigned char *out); +int PKCS5_PBKDF2_HMAC(const char *pass, int passlen, + const unsigned char *salt, int saltlen, int iter, + const EVP_MD *digest, int keylen, unsigned char *out); +int PKCS5_v2_PBE_keyivgen(EVP_CIPHER_CTX *ctx, const char *pass, int passlen, + ASN1_TYPE *param, const EVP_CIPHER *cipher, + const EVP_MD *md, int en_de); + +#ifndef OPENSSL_NO_SCRYPT +int EVP_PBE_scrypt(const char *pass, size_t passlen, + const unsigned char *salt, size_t saltlen, + uint64_t N, uint64_t r, uint64_t p, uint64_t maxmem, + unsigned char *key, size_t keylen); + +int PKCS5_v2_scrypt_keyivgen(EVP_CIPHER_CTX *ctx, const char *pass, + int passlen, ASN1_TYPE *param, + const EVP_CIPHER *c, const EVP_MD *md, int en_de); +#endif + +void PKCS5_PBE_add(void); + +int EVP_PBE_CipherInit(ASN1_OBJECT *pbe_obj, const char *pass, int passlen, + ASN1_TYPE *param, EVP_CIPHER_CTX *ctx, int en_de); + +/* PBE type */ + +/* Can appear as the outermost AlgorithmIdentifier */ +# define EVP_PBE_TYPE_OUTER 0x0 +/* Is an PRF type OID */ +# define EVP_PBE_TYPE_PRF 0x1 +/* Is a PKCS#5 v2.0 KDF */ +# define EVP_PBE_TYPE_KDF 0x2 + +int EVP_PBE_alg_add_type(int pbe_type, int pbe_nid, int cipher_nid, + int md_nid, EVP_PBE_KEYGEN *keygen); +int EVP_PBE_alg_add(int nid, const EVP_CIPHER *cipher, const EVP_MD *md, + EVP_PBE_KEYGEN *keygen); +int EVP_PBE_find(int type, int pbe_nid, int *pcnid, int *pmnid, + EVP_PBE_KEYGEN **pkeygen); +void EVP_PBE_cleanup(void); +int EVP_PBE_get(int *ptype, int *ppbe_nid, size_t num); + +# define ASN1_PKEY_ALIAS 0x1 +# define ASN1_PKEY_DYNAMIC 0x2 +# define ASN1_PKEY_SIGPARAM_NULL 0x4 + +# define ASN1_PKEY_CTRL_PKCS7_SIGN 0x1 +# define ASN1_PKEY_CTRL_PKCS7_ENCRYPT 0x2 +# define ASN1_PKEY_CTRL_DEFAULT_MD_NID 0x3 +# define ASN1_PKEY_CTRL_CMS_SIGN 0x5 +# define ASN1_PKEY_CTRL_CMS_ENVELOPE 0x7 +# define ASN1_PKEY_CTRL_CMS_RI_TYPE 0x8 + +# define ASN1_PKEY_CTRL_SET1_TLS_ENCPT 0x9 +# define ASN1_PKEY_CTRL_GET1_TLS_ENCPT 0xa + +int EVP_PKEY_asn1_get_count(void); +const EVP_PKEY_ASN1_METHOD *EVP_PKEY_asn1_get0(int idx); +const EVP_PKEY_ASN1_METHOD *EVP_PKEY_asn1_find(ENGINE **pe, int type); +const EVP_PKEY_ASN1_METHOD *EVP_PKEY_asn1_find_str(ENGINE **pe, + const char *str, int len); +int EVP_PKEY_asn1_add0(const EVP_PKEY_ASN1_METHOD *ameth); +int EVP_PKEY_asn1_add_alias(int to, int from); +int EVP_PKEY_asn1_get0_info(int *ppkey_id, int *pkey_base_id, + int *ppkey_flags, const char **pinfo, + const char **ppem_str, + const EVP_PKEY_ASN1_METHOD *ameth); + +const EVP_PKEY_ASN1_METHOD *EVP_PKEY_get0_asn1(const EVP_PKEY *pkey); +EVP_PKEY_ASN1_METHOD *EVP_PKEY_asn1_new(int id, int flags, + const char *pem_str, + const char *info); +void EVP_PKEY_asn1_copy(EVP_PKEY_ASN1_METHOD *dst, + const EVP_PKEY_ASN1_METHOD *src); +void EVP_PKEY_asn1_free(EVP_PKEY_ASN1_METHOD *ameth); +void EVP_PKEY_asn1_set_public(EVP_PKEY_ASN1_METHOD *ameth, + int (*pub_decode) (EVP_PKEY *pk, + X509_PUBKEY *pub), + int (*pub_encode) (X509_PUBKEY *pub, + const EVP_PKEY *pk), + int (*pub_cmp) (const EVP_PKEY *a, + const EVP_PKEY *b), + int (*pub_print) (BIO *out, + const EVP_PKEY *pkey, + int indent, ASN1_PCTX *pctx), + int (*pkey_size) (const EVP_PKEY *pk), + int (*pkey_bits) (const EVP_PKEY *pk)); +void EVP_PKEY_asn1_set_private(EVP_PKEY_ASN1_METHOD *ameth, + int (*priv_decode) (EVP_PKEY *pk, + const PKCS8_PRIV_KEY_INFO + *p8inf), + int (*priv_encode) (PKCS8_PRIV_KEY_INFO *p8, + const EVP_PKEY *pk), + int (*priv_print) (BIO *out, + const EVP_PKEY *pkey, + int indent, + ASN1_PCTX *pctx)); +void EVP_PKEY_asn1_set_param(EVP_PKEY_ASN1_METHOD *ameth, + int (*param_decode) (EVP_PKEY *pkey, + const unsigned char **pder, + int derlen), + int (*param_encode) (const EVP_PKEY *pkey, + unsigned char **pder), + int (*param_missing) (const EVP_PKEY *pk), + int (*param_copy) (EVP_PKEY *to, + const EVP_PKEY *from), + int (*param_cmp) (const EVP_PKEY *a, + const EVP_PKEY *b), + int (*param_print) (BIO *out, + const EVP_PKEY *pkey, + int indent, + ASN1_PCTX *pctx)); + +void EVP_PKEY_asn1_set_free(EVP_PKEY_ASN1_METHOD *ameth, + void (*pkey_free) (EVP_PKEY *pkey)); +void EVP_PKEY_asn1_set_ctrl(EVP_PKEY_ASN1_METHOD *ameth, + int (*pkey_ctrl) (EVP_PKEY *pkey, int op, + long arg1, void *arg2)); +void EVP_PKEY_asn1_set_item(EVP_PKEY_ASN1_METHOD *ameth, + int (*item_verify) (EVP_MD_CTX *ctx, + const ASN1_ITEM *it, + void *asn, + X509_ALGOR *a, + ASN1_BIT_STRING *sig, + EVP_PKEY *pkey), + int (*item_sign) (EVP_MD_CTX *ctx, + const ASN1_ITEM *it, + void *asn, + X509_ALGOR *alg1, + X509_ALGOR *alg2, + ASN1_BIT_STRING *sig)); + +void EVP_PKEY_asn1_set_security_bits(EVP_PKEY_ASN1_METHOD *ameth, + int (*pkey_security_bits) (const EVP_PKEY + *pk)); + +# define EVP_PKEY_OP_UNDEFINED 0 +# define EVP_PKEY_OP_PARAMGEN (1<<1) +# define EVP_PKEY_OP_KEYGEN (1<<2) +# define EVP_PKEY_OP_SIGN (1<<3) +# define EVP_PKEY_OP_VERIFY (1<<4) +# define EVP_PKEY_OP_VERIFYRECOVER (1<<5) +# define EVP_PKEY_OP_SIGNCTX (1<<6) +# define EVP_PKEY_OP_VERIFYCTX (1<<7) +# define EVP_PKEY_OP_ENCRYPT (1<<8) +# define EVP_PKEY_OP_DECRYPT (1<<9) +# define EVP_PKEY_OP_DERIVE (1<<10) + +# define EVP_PKEY_OP_TYPE_SIG \ + (EVP_PKEY_OP_SIGN | EVP_PKEY_OP_VERIFY | EVP_PKEY_OP_VERIFYRECOVER \ + | EVP_PKEY_OP_SIGNCTX | EVP_PKEY_OP_VERIFYCTX) + +# define EVP_PKEY_OP_TYPE_CRYPT \ + (EVP_PKEY_OP_ENCRYPT | EVP_PKEY_OP_DECRYPT) + +# define EVP_PKEY_OP_TYPE_NOGEN \ + (EVP_PKEY_OP_TYPE_SIG | EVP_PKEY_OP_TYPE_CRYPT | EVP_PKEY_OP_DERIVE) + +# define EVP_PKEY_OP_TYPE_GEN \ + (EVP_PKEY_OP_PARAMGEN | EVP_PKEY_OP_KEYGEN) + +# define EVP_PKEY_CTX_set_signature_md(ctx, md) \ + EVP_PKEY_CTX_ctrl(ctx, -1, EVP_PKEY_OP_TYPE_SIG, \ + EVP_PKEY_CTRL_MD, 0, (void *)md) + +# define EVP_PKEY_CTX_get_signature_md(ctx, pmd) \ + EVP_PKEY_CTX_ctrl(ctx, -1, EVP_PKEY_OP_TYPE_SIG, \ + EVP_PKEY_CTRL_GET_MD, 0, (void *)pmd) + +# define EVP_PKEY_CTX_set_mac_key(ctx, key, len) \ + EVP_PKEY_CTX_ctrl(ctx, -1, EVP_PKEY_OP_KEYGEN, \ + EVP_PKEY_CTRL_SET_MAC_KEY, len, (void *)key) + +# define EVP_PKEY_CTRL_MD 1 +# define EVP_PKEY_CTRL_PEER_KEY 2 + +# define EVP_PKEY_CTRL_PKCS7_ENCRYPT 3 +# define EVP_PKEY_CTRL_PKCS7_DECRYPT 4 + +# define EVP_PKEY_CTRL_PKCS7_SIGN 5 + +# define EVP_PKEY_CTRL_SET_MAC_KEY 6 + +# define EVP_PKEY_CTRL_DIGESTINIT 7 + +/* Used by GOST key encryption in TLS */ +# define EVP_PKEY_CTRL_SET_IV 8 + +# define EVP_PKEY_CTRL_CMS_ENCRYPT 9 +# define EVP_PKEY_CTRL_CMS_DECRYPT 10 +# define EVP_PKEY_CTRL_CMS_SIGN 11 + +# define EVP_PKEY_CTRL_CIPHER 12 + +# define EVP_PKEY_CTRL_GET_MD 13 + +# define EVP_PKEY_ALG_CTRL 0x1000 + +# define EVP_PKEY_FLAG_AUTOARGLEN 2 +/* + * Method handles all operations: don't assume any digest related defaults. + */ +# define EVP_PKEY_FLAG_SIGCTX_CUSTOM 4 + +const EVP_PKEY_METHOD *EVP_PKEY_meth_find(int type); +EVP_PKEY_METHOD *EVP_PKEY_meth_new(int id, int flags); +void EVP_PKEY_meth_get0_info(int *ppkey_id, int *pflags, + const EVP_PKEY_METHOD *meth); +void EVP_PKEY_meth_copy(EVP_PKEY_METHOD *dst, const EVP_PKEY_METHOD *src); +void EVP_PKEY_meth_free(EVP_PKEY_METHOD *pmeth); +int EVP_PKEY_meth_add0(const EVP_PKEY_METHOD *pmeth); + +EVP_PKEY_CTX *EVP_PKEY_CTX_new(EVP_PKEY *pkey, ENGINE *e); +EVP_PKEY_CTX *EVP_PKEY_CTX_new_id(int id, ENGINE *e); +EVP_PKEY_CTX *EVP_PKEY_CTX_dup(EVP_PKEY_CTX *ctx); +void EVP_PKEY_CTX_free(EVP_PKEY_CTX *ctx); + +int EVP_PKEY_CTX_ctrl(EVP_PKEY_CTX *ctx, int keytype, int optype, + int cmd, int p1, void *p2); +int EVP_PKEY_CTX_ctrl_str(EVP_PKEY_CTX *ctx, const char *type, + const char *value); + +int EVP_PKEY_CTX_str2ctrl(EVP_PKEY_CTX *ctx, int cmd, const char *str); +int EVP_PKEY_CTX_hex2ctrl(EVP_PKEY_CTX *ctx, int cmd, const char *hex); + +int EVP_PKEY_CTX_get_operation(EVP_PKEY_CTX *ctx); +void EVP_PKEY_CTX_set0_keygen_info(EVP_PKEY_CTX *ctx, int *dat, int datlen); + +EVP_PKEY *EVP_PKEY_new_mac_key(int type, ENGINE *e, + const unsigned char *key, int keylen); + +void EVP_PKEY_CTX_set_data(EVP_PKEY_CTX *ctx, void *data); +void *EVP_PKEY_CTX_get_data(EVP_PKEY_CTX *ctx); +EVP_PKEY *EVP_PKEY_CTX_get0_pkey(EVP_PKEY_CTX *ctx); + +EVP_PKEY *EVP_PKEY_CTX_get0_peerkey(EVP_PKEY_CTX *ctx); + +void EVP_PKEY_CTX_set_app_data(EVP_PKEY_CTX *ctx, void *data); +void *EVP_PKEY_CTX_get_app_data(EVP_PKEY_CTX *ctx); + +int EVP_PKEY_sign_init(EVP_PKEY_CTX *ctx); +int EVP_PKEY_sign(EVP_PKEY_CTX *ctx, + unsigned char *sig, size_t *siglen, + const unsigned char *tbs, size_t tbslen); +int EVP_PKEY_verify_init(EVP_PKEY_CTX *ctx); +int EVP_PKEY_verify(EVP_PKEY_CTX *ctx, + const unsigned char *sig, size_t siglen, + const unsigned char *tbs, size_t tbslen); +int EVP_PKEY_verify_recover_init(EVP_PKEY_CTX *ctx); +int EVP_PKEY_verify_recover(EVP_PKEY_CTX *ctx, + unsigned char *rout, size_t *routlen, + const unsigned char *sig, size_t siglen); +int EVP_PKEY_encrypt_init(EVP_PKEY_CTX *ctx); +int EVP_PKEY_encrypt(EVP_PKEY_CTX *ctx, + unsigned char *out, size_t *outlen, + const unsigned char *in, size_t inlen); +int EVP_PKEY_decrypt_init(EVP_PKEY_CTX *ctx); +int EVP_PKEY_decrypt(EVP_PKEY_CTX *ctx, + unsigned char *out, size_t *outlen, + const unsigned char *in, size_t inlen); + +int EVP_PKEY_derive_init(EVP_PKEY_CTX *ctx); +int EVP_PKEY_derive_set_peer(EVP_PKEY_CTX *ctx, EVP_PKEY *peer); +int EVP_PKEY_derive(EVP_PKEY_CTX *ctx, unsigned char *key, size_t *keylen); + +typedef int EVP_PKEY_gen_cb(EVP_PKEY_CTX *ctx); + +int EVP_PKEY_paramgen_init(EVP_PKEY_CTX *ctx); +int EVP_PKEY_paramgen(EVP_PKEY_CTX *ctx, EVP_PKEY **ppkey); +int EVP_PKEY_keygen_init(EVP_PKEY_CTX *ctx); +int EVP_PKEY_keygen(EVP_PKEY_CTX *ctx, EVP_PKEY **ppkey); + +void EVP_PKEY_CTX_set_cb(EVP_PKEY_CTX *ctx, EVP_PKEY_gen_cb *cb); +EVP_PKEY_gen_cb *EVP_PKEY_CTX_get_cb(EVP_PKEY_CTX *ctx); + +int EVP_PKEY_CTX_get_keygen_info(EVP_PKEY_CTX *ctx, int idx); + +void EVP_PKEY_meth_set_init(EVP_PKEY_METHOD *pmeth, + int (*init) (EVP_PKEY_CTX *ctx)); + +void EVP_PKEY_meth_set_copy(EVP_PKEY_METHOD *pmeth, + int (*copy) (EVP_PKEY_CTX *dst, + EVP_PKEY_CTX *src)); + +void EVP_PKEY_meth_set_cleanup(EVP_PKEY_METHOD *pmeth, + void (*cleanup) (EVP_PKEY_CTX *ctx)); + +void EVP_PKEY_meth_set_paramgen(EVP_PKEY_METHOD *pmeth, + int (*paramgen_init) (EVP_PKEY_CTX *ctx), + int (*paramgen) (EVP_PKEY_CTX *ctx, + EVP_PKEY *pkey)); + +void EVP_PKEY_meth_set_keygen(EVP_PKEY_METHOD *pmeth, + int (*keygen_init) (EVP_PKEY_CTX *ctx), + int (*keygen) (EVP_PKEY_CTX *ctx, + EVP_PKEY *pkey)); + +void EVP_PKEY_meth_set_sign(EVP_PKEY_METHOD *pmeth, + int (*sign_init) (EVP_PKEY_CTX *ctx), + int (*sign) (EVP_PKEY_CTX *ctx, + unsigned char *sig, size_t *siglen, + const unsigned char *tbs, + size_t tbslen)); + +void EVP_PKEY_meth_set_verify(EVP_PKEY_METHOD *pmeth, + int (*verify_init) (EVP_PKEY_CTX *ctx), + int (*verify) (EVP_PKEY_CTX *ctx, + const unsigned char *sig, + size_t siglen, + const unsigned char *tbs, + size_t tbslen)); + +void EVP_PKEY_meth_set_verify_recover(EVP_PKEY_METHOD *pmeth, + int (*verify_recover_init) (EVP_PKEY_CTX + *ctx), + int (*verify_recover) (EVP_PKEY_CTX + *ctx, + unsigned char + *sig, + size_t *siglen, + const unsigned + char *tbs, + size_t tbslen)); + +void EVP_PKEY_meth_set_signctx(EVP_PKEY_METHOD *pmeth, + int (*signctx_init) (EVP_PKEY_CTX *ctx, + EVP_MD_CTX *mctx), + int (*signctx) (EVP_PKEY_CTX *ctx, + unsigned char *sig, + size_t *siglen, + EVP_MD_CTX *mctx)); + +void EVP_PKEY_meth_set_verifyctx(EVP_PKEY_METHOD *pmeth, + int (*verifyctx_init) (EVP_PKEY_CTX *ctx, + EVP_MD_CTX *mctx), + int (*verifyctx) (EVP_PKEY_CTX *ctx, + const unsigned char *sig, + int siglen, + EVP_MD_CTX *mctx)); + +void EVP_PKEY_meth_set_encrypt(EVP_PKEY_METHOD *pmeth, + int (*encrypt_init) (EVP_PKEY_CTX *ctx), + int (*encryptfn) (EVP_PKEY_CTX *ctx, + unsigned char *out, + size_t *outlen, + const unsigned char *in, + size_t inlen)); + +void EVP_PKEY_meth_set_decrypt(EVP_PKEY_METHOD *pmeth, + int (*decrypt_init) (EVP_PKEY_CTX *ctx), + int (*decrypt) (EVP_PKEY_CTX *ctx, + unsigned char *out, + size_t *outlen, + const unsigned char *in, + size_t inlen)); + +void EVP_PKEY_meth_set_derive(EVP_PKEY_METHOD *pmeth, + int (*derive_init) (EVP_PKEY_CTX *ctx), + int (*derive) (EVP_PKEY_CTX *ctx, + unsigned char *key, + size_t *keylen)); + +void EVP_PKEY_meth_set_ctrl(EVP_PKEY_METHOD *pmeth, + int (*ctrl) (EVP_PKEY_CTX *ctx, int type, int p1, + void *p2), + int (*ctrl_str) (EVP_PKEY_CTX *ctx, + const char *type, + const char *value)); + +void EVP_PKEY_meth_get_init(EVP_PKEY_METHOD *pmeth, + int (**pinit) (EVP_PKEY_CTX *ctx)); + +void EVP_PKEY_meth_get_copy(EVP_PKEY_METHOD *pmeth, + int (**pcopy) (EVP_PKEY_CTX *dst, + EVP_PKEY_CTX *src)); + +void EVP_PKEY_meth_get_cleanup(EVP_PKEY_METHOD *pmeth, + void (**pcleanup) (EVP_PKEY_CTX *ctx)); + +void EVP_PKEY_meth_get_paramgen(EVP_PKEY_METHOD *pmeth, + int (**pparamgen_init) (EVP_PKEY_CTX *ctx), + int (**pparamgen) (EVP_PKEY_CTX *ctx, + EVP_PKEY *pkey)); + +void EVP_PKEY_meth_get_keygen(EVP_PKEY_METHOD *pmeth, + int (**pkeygen_init) (EVP_PKEY_CTX *ctx), + int (**pkeygen) (EVP_PKEY_CTX *ctx, + EVP_PKEY *pkey)); + +void EVP_PKEY_meth_get_sign(EVP_PKEY_METHOD *pmeth, + int (**psign_init) (EVP_PKEY_CTX *ctx), + int (**psign) (EVP_PKEY_CTX *ctx, + unsigned char *sig, size_t *siglen, + const unsigned char *tbs, + size_t tbslen)); + +void EVP_PKEY_meth_get_verify(EVP_PKEY_METHOD *pmeth, + int (**pverify_init) (EVP_PKEY_CTX *ctx), + int (**pverify) (EVP_PKEY_CTX *ctx, + const unsigned char *sig, + size_t siglen, + const unsigned char *tbs, + size_t tbslen)); + +void EVP_PKEY_meth_get_verify_recover(EVP_PKEY_METHOD *pmeth, + int (**pverify_recover_init) (EVP_PKEY_CTX + *ctx), + int (**pverify_recover) (EVP_PKEY_CTX + *ctx, + unsigned char + *sig, + size_t *siglen, + const unsigned + char *tbs, + size_t tbslen)); + +void EVP_PKEY_meth_get_signctx(EVP_PKEY_METHOD *pmeth, + int (**psignctx_init) (EVP_PKEY_CTX *ctx, + EVP_MD_CTX *mctx), + int (**psignctx) (EVP_PKEY_CTX *ctx, + unsigned char *sig, + size_t *siglen, + EVP_MD_CTX *mctx)); + +void EVP_PKEY_meth_get_verifyctx(EVP_PKEY_METHOD *pmeth, + int (**pverifyctx_init) (EVP_PKEY_CTX *ctx, + EVP_MD_CTX *mctx), + int (**pverifyctx) (EVP_PKEY_CTX *ctx, + const unsigned char *sig, + int siglen, + EVP_MD_CTX *mctx)); + +void EVP_PKEY_meth_get_encrypt(EVP_PKEY_METHOD *pmeth, + int (**pencrypt_init) (EVP_PKEY_CTX *ctx), + int (**pencryptfn) (EVP_PKEY_CTX *ctx, + unsigned char *out, + size_t *outlen, + const unsigned char *in, + size_t inlen)); + +void EVP_PKEY_meth_get_decrypt(EVP_PKEY_METHOD *pmeth, + int (**pdecrypt_init) (EVP_PKEY_CTX *ctx), + int (**pdecrypt) (EVP_PKEY_CTX *ctx, + unsigned char *out, + size_t *outlen, + const unsigned char *in, + size_t inlen)); + +void EVP_PKEY_meth_get_derive(EVP_PKEY_METHOD *pmeth, + int (**pderive_init) (EVP_PKEY_CTX *ctx), + int (**pderive) (EVP_PKEY_CTX *ctx, + unsigned char *key, + size_t *keylen)); + +void EVP_PKEY_meth_get_ctrl(EVP_PKEY_METHOD *pmeth, + int (**pctrl) (EVP_PKEY_CTX *ctx, int type, int p1, + void *p2), + int (**pctrl_str) (EVP_PKEY_CTX *ctx, + const char *type, + const char *value)); + +void EVP_add_alg_module(void); + +/* BEGIN ERROR CODES */ +/* + * The following lines are auto generated by the script mkerr.pl. Any changes + * made after this point may be overwritten when the script is next run. + */ + +int ERR_load_EVP_strings(void); + +/* Error codes for the EVP functions. */ + +/* Function codes. */ +# define EVP_F_AESNI_INIT_KEY 165 +# define EVP_F_AES_INIT_KEY 133 +# define EVP_F_AES_OCB_CIPHER 169 +# define EVP_F_AES_T4_INIT_KEY 178 +# define EVP_F_AES_WRAP_CIPHER 170 +# define EVP_F_ALG_MODULE_INIT 177 +# define EVP_F_CAMELLIA_INIT_KEY 159 +# define EVP_F_CHACHA20_POLY1305_CTRL 182 +# define EVP_F_CMLL_T4_INIT_KEY 179 +# define EVP_F_DES_EDE3_WRAP_CIPHER 171 +# define EVP_F_DO_SIGVER_INIT 161 +# define EVP_F_EVP_CIPHERINIT_EX 123 +# define EVP_F_EVP_CIPHER_CTX_COPY 163 +# define EVP_F_EVP_CIPHER_CTX_CTRL 124 +# define EVP_F_EVP_CIPHER_CTX_SET_KEY_LENGTH 122 +# define EVP_F_EVP_DECRYPTFINAL_EX 101 +# define EVP_F_EVP_DECRYPTUPDATE 166 +# define EVP_F_EVP_DIGESTINIT_EX 128 +# define EVP_F_EVP_ENCRYPTFINAL_EX 127 +# define EVP_F_EVP_ENCRYPTUPDATE 167 +# define EVP_F_EVP_MD_CTX_COPY_EX 110 +# define EVP_F_EVP_MD_SIZE 162 +# define EVP_F_EVP_OPENINIT 102 +# define EVP_F_EVP_PBE_ALG_ADD 115 +# define EVP_F_EVP_PBE_ALG_ADD_TYPE 160 +# define EVP_F_EVP_PBE_CIPHERINIT 116 +# define EVP_F_EVP_PBE_SCRYPT 181 +# define EVP_F_EVP_PKCS82PKEY 111 +# define EVP_F_EVP_PKEY2PKCS8 113 +# define EVP_F_EVP_PKEY_COPY_PARAMETERS 103 +# define EVP_F_EVP_PKEY_CTX_CTRL 137 +# define EVP_F_EVP_PKEY_CTX_CTRL_STR 150 +# define EVP_F_EVP_PKEY_CTX_DUP 156 +# define EVP_F_EVP_PKEY_DECRYPT 104 +# define EVP_F_EVP_PKEY_DECRYPT_INIT 138 +# define EVP_F_EVP_PKEY_DECRYPT_OLD 151 +# define EVP_F_EVP_PKEY_DERIVE 153 +# define EVP_F_EVP_PKEY_DERIVE_INIT 154 +# define EVP_F_EVP_PKEY_DERIVE_SET_PEER 155 +# define EVP_F_EVP_PKEY_ENCRYPT 105 +# define EVP_F_EVP_PKEY_ENCRYPT_INIT 139 +# define EVP_F_EVP_PKEY_ENCRYPT_OLD 152 +# define EVP_F_EVP_PKEY_GET0_DH 119 +# define EVP_F_EVP_PKEY_GET0_DSA 120 +# define EVP_F_EVP_PKEY_GET0_EC_KEY 131 +# define EVP_F_EVP_PKEY_GET0_HMAC 183 +# define EVP_F_EVP_PKEY_GET0_RSA 121 +# define EVP_F_EVP_PKEY_KEYGEN 146 +# define EVP_F_EVP_PKEY_KEYGEN_INIT 147 +# define EVP_F_EVP_PKEY_NEW 106 +# define EVP_F_EVP_PKEY_PARAMGEN 148 +# define EVP_F_EVP_PKEY_PARAMGEN_INIT 149 +# define EVP_F_EVP_PKEY_SIGN 140 +# define EVP_F_EVP_PKEY_SIGN_INIT 141 +# define EVP_F_EVP_PKEY_VERIFY 142 +# define EVP_F_EVP_PKEY_VERIFY_INIT 143 +# define EVP_F_EVP_PKEY_VERIFY_RECOVER 144 +# define EVP_F_EVP_PKEY_VERIFY_RECOVER_INIT 145 +# define EVP_F_EVP_SIGNFINAL 107 +# define EVP_F_EVP_VERIFYFINAL 108 +# define EVP_F_INT_CTX_NEW 157 +# define EVP_F_PKCS5_PBE_KEYIVGEN 117 +# define EVP_F_PKCS5_V2_PBE_KEYIVGEN 118 +# define EVP_F_PKCS5_V2_PBKDF2_KEYIVGEN 164 +# define EVP_F_PKCS5_V2_SCRYPT_KEYIVGEN 180 +# define EVP_F_PKEY_SET_TYPE 158 +# define EVP_F_RC2_MAGIC_TO_METH 109 +# define EVP_F_RC5_CTRL 125 + +/* Reason codes. */ +# define EVP_R_AES_KEY_SETUP_FAILED 143 +# define EVP_R_BAD_DECRYPT 100 +# define EVP_R_BUFFER_TOO_SMALL 155 +# define EVP_R_CAMELLIA_KEY_SETUP_FAILED 157 +# define EVP_R_CIPHER_PARAMETER_ERROR 122 +# define EVP_R_COMMAND_NOT_SUPPORTED 147 +# define EVP_R_COPY_ERROR 173 +# define EVP_R_CTRL_NOT_IMPLEMENTED 132 +# define EVP_R_CTRL_OPERATION_NOT_IMPLEMENTED 133 +# define EVP_R_DATA_NOT_MULTIPLE_OF_BLOCK_LENGTH 138 +# define EVP_R_DECODE_ERROR 114 +# define EVP_R_DIFFERENT_KEY_TYPES 101 +# define EVP_R_DIFFERENT_PARAMETERS 153 +# define EVP_R_ERROR_LOADING_SECTION 165 +# define EVP_R_ERROR_SETTING_FIPS_MODE 166 +# define EVP_R_EXPECTING_AN_HMAC_KEY 174 +# define EVP_R_EXPECTING_AN_RSA_KEY 127 +# define EVP_R_EXPECTING_A_DH_KEY 128 +# define EVP_R_EXPECTING_A_DSA_KEY 129 +# define EVP_R_EXPECTING_A_EC_KEY 142 +# define EVP_R_FIPS_MODE_NOT_SUPPORTED 167 +# define EVP_R_ILLEGAL_SCRYPT_PARAMETERS 171 +# define EVP_R_INITIALIZATION_ERROR 134 +# define EVP_R_INPUT_NOT_INITIALIZED 111 +# define EVP_R_INVALID_DIGEST 152 +# define EVP_R_INVALID_FIPS_MODE 168 +# define EVP_R_INVALID_KEY 163 +# define EVP_R_INVALID_KEY_LENGTH 130 +# define EVP_R_INVALID_OPERATION 148 +# define EVP_R_KEYGEN_FAILURE 120 +# define EVP_R_MEMORY_LIMIT_EXCEEDED 172 +# define EVP_R_MESSAGE_DIGEST_IS_NULL 159 +# define EVP_R_METHOD_NOT_SUPPORTED 144 +# define EVP_R_MISSING_PARAMETERS 103 +# define EVP_R_NO_CIPHER_SET 131 +# define EVP_R_NO_DEFAULT_DIGEST 158 +# define EVP_R_NO_DIGEST_SET 139 +# define EVP_R_NO_KEY_SET 154 +# define EVP_R_NO_OPERATION_SET 149 +# define EVP_R_OPERATION_NOT_SUPPORTED_FOR_THIS_KEYTYPE 150 +# define EVP_R_OPERATON_NOT_INITIALIZED 151 +# define EVP_R_PARTIALLY_OVERLAPPING 162 +# define EVP_R_PRIVATE_KEY_DECODE_ERROR 145 +# define EVP_R_PRIVATE_KEY_ENCODE_ERROR 146 +# define EVP_R_PUBLIC_KEY_NOT_RSA 106 +# define EVP_R_UNKNOWN_CIPHER 160 +# define EVP_R_UNKNOWN_DIGEST 161 +# define EVP_R_UNKNOWN_OPTION 169 +# define EVP_R_UNKNOWN_PBE_ALGORITHM 121 +# define EVP_R_UNSUPPORTED_ALGORITHM 156 +# define EVP_R_UNSUPPORTED_CIPHER 107 +# define EVP_R_UNSUPPORTED_KEYLENGTH 123 +# define EVP_R_UNSUPPORTED_KEY_DERIVATION_FUNCTION 124 +# define EVP_R_UNSUPPORTED_KEY_SIZE 108 +# define EVP_R_UNSUPPORTED_NUMBER_OF_ROUNDS 135 +# define EVP_R_UNSUPPORTED_PRF 125 +# define EVP_R_UNSUPPORTED_PRIVATE_KEY_ALGORITHM 118 +# define EVP_R_UNSUPPORTED_SALT_TYPE 126 +# define EVP_R_WRAP_MODE_NOT_ALLOWED 170 +# define EVP_R_WRONG_FINAL_BLOCK_LENGTH 109 + +# ifdef __cplusplus +} +# endif +#endif diff --git a/mobile/ios/FxA/FxA/include/openssl/hmac.h b/mobile/ios/FxA/FxA/include/openssl/hmac.h new file mode 100644 index 0000000000..9f06896059 --- /dev/null +++ b/mobile/ios/FxA/FxA/include/openssl/hmac.h @@ -0,0 +1,49 @@ +/* + * Copyright 1995-2016 The OpenSSL Project Authors. All Rights Reserved. + * + * Licensed under the OpenSSL license (the "License"). You may not use + * this file except in compliance with the License. You can obtain a copy + * in the file LICENSE in the source distribution or at + * https://www.openssl.org/source/license.html + */ + +#ifndef HEADER_HMAC_H +# define HEADER_HMAC_H + +# include + +# include + +# define HMAC_MAX_MD_CBLOCK 128/* largest known is SHA512 */ + +#ifdef __cplusplus +extern "C" { +#endif + +size_t HMAC_size(const HMAC_CTX *e); +HMAC_CTX *HMAC_CTX_new(void); +int HMAC_CTX_reset(HMAC_CTX *ctx); +void HMAC_CTX_free(HMAC_CTX *ctx); + +DEPRECATEDIN_1_1_0(__owur int HMAC_Init(HMAC_CTX *ctx, const void *key, int len, + const EVP_MD *md)) + +/*__owur*/ int HMAC_Init_ex(HMAC_CTX *ctx, const void *key, int len, + const EVP_MD *md, ENGINE *impl); +/*__owur*/ int HMAC_Update(HMAC_CTX *ctx, const unsigned char *data, + size_t len); +/*__owur*/ int HMAC_Final(HMAC_CTX *ctx, unsigned char *md, + unsigned int *len); +unsigned char *HMAC(const EVP_MD *evp_md, const void *key, int key_len, + const unsigned char *d, size_t n, unsigned char *md, + unsigned int *md_len); +__owur int HMAC_CTX_copy(HMAC_CTX *dctx, HMAC_CTX *sctx); + +void HMAC_CTX_set_flags(HMAC_CTX *ctx, unsigned long flags); +const EVP_MD *HMAC_CTX_get_md(const HMAC_CTX *ctx); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/mobile/ios/FxA/FxA/include/openssl/idea.h b/mobile/ios/FxA/FxA/include/openssl/idea.h new file mode 100644 index 0000000000..4334f3ea71 --- /dev/null +++ b/mobile/ios/FxA/FxA/include/openssl/idea.h @@ -0,0 +1,64 @@ +/* + * Copyright 1995-2016 The OpenSSL Project Authors. All Rights Reserved. + * + * Licensed under the OpenSSL license (the "License"). You may not use + * this file except in compliance with the License. You can obtain a copy + * in the file LICENSE in the source distribution or at + * https://www.openssl.org/source/license.html + */ + +#ifndef HEADER_IDEA_H +# define HEADER_IDEA_H + +# include + +# ifndef OPENSSL_NO_IDEA +# ifdef __cplusplus +extern "C" { +# endif + +typedef unsigned int IDEA_INT; + +# define IDEA_ENCRYPT 1 +# define IDEA_DECRYPT 0 + +# define IDEA_BLOCK 8 +# define IDEA_KEY_LENGTH 16 + +typedef struct idea_key_st { + IDEA_INT data[9][6]; +} IDEA_KEY_SCHEDULE; + +const char *IDEA_options(void); +void IDEA_ecb_encrypt(const unsigned char *in, unsigned char *out, + IDEA_KEY_SCHEDULE *ks); +void IDEA_set_encrypt_key(const unsigned char *key, IDEA_KEY_SCHEDULE *ks); +void IDEA_set_decrypt_key(IDEA_KEY_SCHEDULE *ek, IDEA_KEY_SCHEDULE *dk); +void IDEA_cbc_encrypt(const unsigned char *in, unsigned char *out, + long length, IDEA_KEY_SCHEDULE *ks, unsigned char *iv, + int enc); +void IDEA_cfb64_encrypt(const unsigned char *in, unsigned char *out, + long length, IDEA_KEY_SCHEDULE *ks, unsigned char *iv, + int *num, int enc); +void IDEA_ofb64_encrypt(const unsigned char *in, unsigned char *out, + long length, IDEA_KEY_SCHEDULE *ks, unsigned char *iv, + int *num); +void IDEA_encrypt(unsigned long *in, IDEA_KEY_SCHEDULE *ks); + +# if OPENSSL_API_COMPAT < 0x10100000L +# define idea_options IDEA_options +# define idea_ecb_encrypt IDEA_ecb_encrypt +# define idea_set_encrypt_key IDEA_set_encrypt_key +# define idea_set_decrypt_key IDEA_set_decrypt_key +# define idea_cbc_encrypt IDEA_cbc_encrypt +# define idea_cfb64_encrypt IDEA_cfb64_encrypt +# define idea_ofb64_encrypt IDEA_ofb64_encrypt +# define idea_encrypt IDEA_encrypt +# endif + +# ifdef __cplusplus +} +# endif +# endif + +#endif diff --git a/mobile/ios/FxA/FxA/include/openssl/kdf.h b/mobile/ios/FxA/FxA/include/openssl/kdf.h new file mode 100644 index 0000000000..9f87f788b2 --- /dev/null +++ b/mobile/ios/FxA/FxA/include/openssl/kdf.h @@ -0,0 +1,75 @@ +/* + * Copyright 2016 The OpenSSL Project Authors. All Rights Reserved. + * + * Licensed under the OpenSSL license (the "License"). You may not use + * this file except in compliance with the License. You can obtain a copy + * in the file LICENSE in the source distribution or at + * https://www.openssl.org/source/license.html + */ + +#ifndef HEADER_KDF_H +# define HEADER_KDF_H + +#ifdef __cplusplus +extern "C" { +#endif + +# define EVP_PKEY_CTRL_TLS_MD (EVP_PKEY_ALG_CTRL) +# define EVP_PKEY_CTRL_TLS_SECRET (EVP_PKEY_ALG_CTRL + 1) +# define EVP_PKEY_CTRL_TLS_SEED (EVP_PKEY_ALG_CTRL + 2) +# define EVP_PKEY_CTRL_HKDF_MD (EVP_PKEY_ALG_CTRL + 3) +# define EVP_PKEY_CTRL_HKDF_SALT (EVP_PKEY_ALG_CTRL + 4) +# define EVP_PKEY_CTRL_HKDF_KEY (EVP_PKEY_ALG_CTRL + 5) +# define EVP_PKEY_CTRL_HKDF_INFO (EVP_PKEY_ALG_CTRL + 6) + +# define EVP_PKEY_CTX_set_tls1_prf_md(pctx, md) \ + EVP_PKEY_CTX_ctrl(pctx, -1, EVP_PKEY_OP_DERIVE, \ + EVP_PKEY_CTRL_TLS_MD, 0, (void *)md) + +# define EVP_PKEY_CTX_set1_tls1_prf_secret(pctx, sec, seclen) \ + EVP_PKEY_CTX_ctrl(pctx, -1, EVP_PKEY_OP_DERIVE, \ + EVP_PKEY_CTRL_TLS_SECRET, seclen, (void *)sec) + +# define EVP_PKEY_CTX_add1_tls1_prf_seed(pctx, seed, seedlen) \ + EVP_PKEY_CTX_ctrl(pctx, -1, EVP_PKEY_OP_DERIVE, \ + EVP_PKEY_CTRL_TLS_SEED, seedlen, (void *)seed) + +# define EVP_PKEY_CTX_set_hkdf_md(pctx, md) \ + EVP_PKEY_CTX_ctrl(pctx, -1, EVP_PKEY_OP_DERIVE, \ + EVP_PKEY_CTRL_HKDF_MD, 0, (void *)md) + +# define EVP_PKEY_CTX_set1_hkdf_salt(pctx, salt, saltlen) \ + EVP_PKEY_CTX_ctrl(pctx, -1, EVP_PKEY_OP_DERIVE, \ + EVP_PKEY_CTRL_HKDF_SALT, saltlen, (void *)salt) + +# define EVP_PKEY_CTX_set1_hkdf_key(pctx, key, keylen) \ + EVP_PKEY_CTX_ctrl(pctx, -1, EVP_PKEY_OP_DERIVE, \ + EVP_PKEY_CTRL_HKDF_KEY, keylen, (void *)key) + +# define EVP_PKEY_CTX_add1_hkdf_info(pctx, info, infolen) \ + EVP_PKEY_CTX_ctrl(pctx, -1, EVP_PKEY_OP_DERIVE, \ + EVP_PKEY_CTRL_HKDF_INFO, infolen, (void *)info) + +/* BEGIN ERROR CODES */ +/* + * The following lines are auto generated by the script mkerr.pl. Any changes + * made after this point may be overwritten when the script is next run. + */ + +int ERR_load_KDF_strings(void); + +/* Error codes for the KDF functions. */ + +/* Function codes. */ +# define KDF_F_PKEY_TLS1_PRF_CTRL_STR 100 +# define KDF_F_PKEY_TLS1_PRF_DERIVE 101 + +/* Reason codes. */ +# define KDF_R_INVALID_DIGEST 100 +# define KDF_R_MISSING_PARAMETER 101 +# define KDF_R_VALUE_MISSING 102 + +# ifdef __cplusplus +} +# endif +#endif diff --git a/mobile/ios/FxA/FxA/include/openssl/lhash.h b/mobile/ios/FxA/FxA/include/openssl/lhash.h new file mode 100644 index 0000000000..e2ccb65d69 --- /dev/null +++ b/mobile/ios/FxA/FxA/include/openssl/lhash.h @@ -0,0 +1,204 @@ +/* + * Copyright 1995-2016 The OpenSSL Project Authors. All Rights Reserved. + * + * Licensed under the OpenSSL license (the "License"). You may not use + * this file except in compliance with the License. You can obtain a copy + * in the file LICENSE in the source distribution or at + * https://www.openssl.org/source/license.html + */ + +/* + * Header for dynamic hash table routines Author - Eric Young + */ + +#ifndef HEADER_LHASH_H +# define HEADER_LHASH_H + +# include +# include + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct lhash_node_st OPENSSL_LH_NODE; +typedef int (*OPENSSL_LH_COMPFUNC) (const void *, const void *); +typedef unsigned long (*OPENSSL_LH_HASHFUNC) (const void *); +typedef void (*OPENSSL_LH_DOALL_FUNC) (void *); +typedef void (*OPENSSL_LH_DOALL_FUNCARG) (void *, void *); +typedef struct lhash_st OPENSSL_LHASH; + +/* + * Macros for declaring and implementing type-safe wrappers for LHASH + * callbacks. This way, callbacks can be provided to LHASH structures without + * function pointer casting and the macro-defined callbacks provide + * per-variable casting before deferring to the underlying type-specific + * callbacks. NB: It is possible to place a "static" in front of both the + * DECLARE and IMPLEMENT macros if the functions are strictly internal. + */ + +/* First: "hash" functions */ +# define DECLARE_LHASH_HASH_FN(name, o_type) \ + unsigned long name##_LHASH_HASH(const void *); +# define IMPLEMENT_LHASH_HASH_FN(name, o_type) \ + unsigned long name##_LHASH_HASH(const void *arg) { \ + const o_type *a = arg; \ + return name##_hash(a); } +# define LHASH_HASH_FN(name) name##_LHASH_HASH + +/* Second: "compare" functions */ +# define DECLARE_LHASH_COMP_FN(name, o_type) \ + int name##_LHASH_COMP(const void *, const void *); +# define IMPLEMENT_LHASH_COMP_FN(name, o_type) \ + int name##_LHASH_COMP(const void *arg1, const void *arg2) { \ + const o_type *a = arg1; \ + const o_type *b = arg2; \ + return name##_cmp(a,b); } +# define LHASH_COMP_FN(name) name##_LHASH_COMP + +/* Fourth: "doall_arg" functions */ +# define DECLARE_LHASH_DOALL_ARG_FN(name, o_type, a_type) \ + void name##_LHASH_DOALL_ARG(void *, void *); +# define IMPLEMENT_LHASH_DOALL_ARG_FN(name, o_type, a_type) \ + void name##_LHASH_DOALL_ARG(void *arg1, void *arg2) { \ + o_type *a = arg1; \ + a_type *b = arg2; \ + name##_doall_arg(a, b); } +# define LHASH_DOALL_ARG_FN(name) name##_LHASH_DOALL_ARG + + +# define LH_LOAD_MULT 256 + +int OPENSSL_LH_error(OPENSSL_LHASH *lh); +OPENSSL_LHASH *OPENSSL_LH_new(OPENSSL_LH_HASHFUNC h, OPENSSL_LH_COMPFUNC c); +void OPENSSL_LH_free(OPENSSL_LHASH *lh); +void *OPENSSL_LH_insert(OPENSSL_LHASH *lh, void *data); +void *OPENSSL_LH_delete(OPENSSL_LHASH *lh, const void *data); +void *OPENSSL_LH_retrieve(OPENSSL_LHASH *lh, const void *data); +void OPENSSL_LH_doall(OPENSSL_LHASH *lh, OPENSSL_LH_DOALL_FUNC func); +void OPENSSL_LH_doall_arg(OPENSSL_LHASH *lh, OPENSSL_LH_DOALL_FUNCARG func, void *arg); +unsigned long OPENSSL_LH_strhash(const char *c); +unsigned long OPENSSL_LH_num_items(const OPENSSL_LHASH *lh); +unsigned long OPENSSL_LH_get_down_load(const OPENSSL_LHASH *lh); +void OPENSSL_LH_set_down_load(OPENSSL_LHASH *lh, unsigned long down_load); + +# ifndef OPENSSL_NO_STDIO +void OPENSSL_LH_stats(const OPENSSL_LHASH *lh, FILE *fp); +void OPENSSL_LH_node_stats(const OPENSSL_LHASH *lh, FILE *fp); +void OPENSSL_LH_node_usage_stats(const OPENSSL_LHASH *lh, FILE *fp); +# endif +void OPENSSL_LH_stats_bio(const OPENSSL_LHASH *lh, BIO *out); +void OPENSSL_LH_node_stats_bio(const OPENSSL_LHASH *lh, BIO *out); +void OPENSSL_LH_node_usage_stats_bio(const OPENSSL_LHASH *lh, BIO *out); + +# if OPENSSL_API_COMPAT < 0x10100000L +# define _LHASH OPENSSL_LHASH +# define LHASH_NODE OPENSSL_LH_NODE +# define lh_error OPENSSL_LH_error +# define lh_new OPENSSL_lh_new +# define lh_free OPENSSL_LH_free +# define lh_insert OPENSSL_LH_insert +# define lh_delete OPENSSL_LH_delete +# define lh_retrieve OPENSSL_LH_retrieve +# define lh_doall OPENSSL_LH_doall +# define lh_doall_arg OPENSSL_LH_doall_arg +# define lh_strhash OPENSSL_LH_strhash +# define lh_num_items OPENSSL_LH_num_items +# ifndef OPENSSL_NO_STDIO +# define lh_stats OPENSSL_LH_stats +# define lh_node_stats OPENSSL_LH_node_stats +# define lh_node_usage_stats OPENSSL_LH_node_usage_stats +# endif +# define lh_stats_bio OPENSSL_LH_stats_bio +# define lh_node_stats_bio OPENSSL_LH_node_stats_bio +# define lh_node_usage_stats_bio OPENSSL_LH_node_usage_stats_bio +# endif + +/* Type checking... */ + +# define LHASH_OF(type) struct lhash_st_##type + +# define DEFINE_LHASH_OF(type) \ + LHASH_OF(type) { union lh_##type##_dummy { void* d1; unsigned long d2; int d3; } dummy; }; \ + static ossl_inline LHASH_OF(type) * \ + lh_##type##_new(unsigned long (*hfn)(const type *), \ + int (*cfn)(const type *, const type *)) \ + { \ + return (LHASH_OF(type) *) \ + OPENSSL_LH_new((OPENSSL_LH_HASHFUNC)hfn, (OPENSSL_LH_COMPFUNC)cfn); \ + } \ + static ossl_inline void lh_##type##_free(LHASH_OF(type) *lh) \ + { \ + OPENSSL_LH_free((OPENSSL_LHASH *)lh); \ + } \ + static ossl_inline type *lh_##type##_insert(LHASH_OF(type) *lh, type *d) \ + { \ + return (type *)OPENSSL_LH_insert((OPENSSL_LHASH *)lh, d); \ + } \ + static ossl_inline type *lh_##type##_delete(LHASH_OF(type) *lh, const type *d) \ + { \ + return (type *)OPENSSL_LH_delete((OPENSSL_LHASH *)lh, d); \ + } \ + static ossl_inline type *lh_##type##_retrieve(LHASH_OF(type) *lh, const type *d) \ + { \ + return (type *)OPENSSL_LH_retrieve((OPENSSL_LHASH *)lh, d); \ + } \ + static ossl_inline int lh_##type##_error(LHASH_OF(type) *lh) \ + { \ + return OPENSSL_LH_error((OPENSSL_LHASH *)lh); \ + } \ + static ossl_inline unsigned long lh_##type##_num_items(LHASH_OF(type) *lh) \ + { \ + return OPENSSL_LH_num_items((OPENSSL_LHASH *)lh); \ + } \ + static ossl_inline void lh_##type##_node_stats_bio(const LHASH_OF(type) *lh, BIO *out) \ + { \ + OPENSSL_LH_node_stats_bio((const OPENSSL_LHASH *)lh, out); \ + } \ + static ossl_inline void lh_##type##_node_usage_stats_bio(const LHASH_OF(type) *lh, BIO *out) \ + { \ + OPENSSL_LH_node_usage_stats_bio((const OPENSSL_LHASH *)lh, out); \ + } \ + static ossl_inline void lh_##type##_stats_bio(const LHASH_OF(type) *lh, BIO *out) \ + { \ + OPENSSL_LH_stats_bio((const OPENSSL_LHASH *)lh, out); \ + } \ + static ossl_inline unsigned long lh_##type##_get_down_load(LHASH_OF(type) *lh) \ + { \ + return OPENSSL_LH_get_down_load((OPENSSL_LHASH *)lh); \ + } \ + static ossl_inline void lh_##type##_set_down_load(LHASH_OF(type) *lh, unsigned long dl) \ + { \ + OPENSSL_LH_set_down_load((OPENSSL_LHASH *)lh, dl); \ + } \ + static ossl_inline void lh_##type##_doall(LHASH_OF(type) *lh, \ + void (*doall)(type *)) \ + { \ + OPENSSL_LH_doall((OPENSSL_LHASH *)lh, (OPENSSL_LH_DOALL_FUNC)doall); \ + } \ + LHASH_OF(type) + +#define IMPLEMENT_LHASH_DOALL_ARG_CONST(type, argtype) \ + int_implement_lhash_doall(type, argtype, const type) + +#define IMPLEMENT_LHASH_DOALL_ARG(type, argtype) \ + int_implement_lhash_doall(type, argtype, type) + +#define int_implement_lhash_doall(type, argtype, cbargtype) \ + static ossl_inline void \ + lh_##type##_doall_##argtype(LHASH_OF(type) *lh, \ + void (*fn)(cbargtype *, argtype *), \ + argtype *arg) \ + { \ + OPENSSL_LH_doall_arg((OPENSSL_LHASH *)lh, (OPENSSL_LH_DOALL_FUNCARG)fn, (void *)arg); \ + } \ + LHASH_OF(type) + +DEFINE_LHASH_OF(OPENSSL_STRING); +DEFINE_LHASH_OF(OPENSSL_CSTRING); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/mobile/ios/FxA/FxA/include/openssl/md2.h b/mobile/ios/FxA/FxA/include/openssl/md2.h new file mode 100644 index 0000000000..7faf8e3d65 --- /dev/null +++ b/mobile/ios/FxA/FxA/include/openssl/md2.h @@ -0,0 +1,44 @@ +/* + * Copyright 1995-2016 The OpenSSL Project Authors. All Rights Reserved. + * + * Licensed under the OpenSSL license (the "License"). You may not use + * this file except in compliance with the License. You can obtain a copy + * in the file LICENSE in the source distribution or at + * https://www.openssl.org/source/license.html + */ + +#ifndef HEADER_MD2_H +# define HEADER_MD2_H + +# include + +# ifndef OPENSSL_NO_MD2 +# include +# ifdef __cplusplus +extern "C" { +# endif + +typedef unsigned char MD2_INT; + +# define MD2_DIGEST_LENGTH 16 +# define MD2_BLOCK 16 + +typedef struct MD2state_st { + unsigned int num; + unsigned char data[MD2_BLOCK]; + MD2_INT cksm[MD2_BLOCK]; + MD2_INT state[MD2_BLOCK]; +} MD2_CTX; + +const char *MD2_options(void); +int MD2_Init(MD2_CTX *c); +int MD2_Update(MD2_CTX *c, const unsigned char *data, size_t len); +int MD2_Final(unsigned char *md, MD2_CTX *c); +unsigned char *MD2(const unsigned char *d, size_t n, unsigned char *md); + +# ifdef __cplusplus +} +# endif +# endif + +#endif diff --git a/mobile/ios/FxA/FxA/include/openssl/md4.h b/mobile/ios/FxA/FxA/include/openssl/md4.h new file mode 100644 index 0000000000..940e29db40 --- /dev/null +++ b/mobile/ios/FxA/FxA/include/openssl/md4.h @@ -0,0 +1,51 @@ +/* + * Copyright 1995-2016 The OpenSSL Project Authors. All Rights Reserved. + * + * Licensed under the OpenSSL license (the "License"). You may not use + * this file except in compliance with the License. You can obtain a copy + * in the file LICENSE in the source distribution or at + * https://www.openssl.org/source/license.html + */ + +#ifndef HEADER_MD4_H +# define HEADER_MD4_H + +# include + +# ifndef OPENSSL_NO_MD4 +# include +# include +# ifdef __cplusplus +extern "C" { +# endif + +/*- + * !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + * ! MD4_LONG has to be at least 32 bits wide. ! + * !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + */ +# define MD4_LONG unsigned int + +# define MD4_CBLOCK 64 +# define MD4_LBLOCK (MD4_CBLOCK/4) +# define MD4_DIGEST_LENGTH 16 + +typedef struct MD4state_st { + MD4_LONG A, B, C, D; + MD4_LONG Nl, Nh; + MD4_LONG data[MD4_LBLOCK]; + unsigned int num; +} MD4_CTX; + +int MD4_Init(MD4_CTX *c); +int MD4_Update(MD4_CTX *c, const void *data, size_t len); +int MD4_Final(unsigned char *md, MD4_CTX *c); +unsigned char *MD4(const unsigned char *d, size_t n, unsigned char *md); +void MD4_Transform(MD4_CTX *c, const unsigned char *b); + +# ifdef __cplusplus +} +# endif +# endif + +#endif diff --git a/mobile/ios/FxA/FxA/include/openssl/md5.h b/mobile/ios/FxA/FxA/include/openssl/md5.h new file mode 100644 index 0000000000..2deb772118 --- /dev/null +++ b/mobile/ios/FxA/FxA/include/openssl/md5.h @@ -0,0 +1,50 @@ +/* + * Copyright 1995-2016 The OpenSSL Project Authors. All Rights Reserved. + * + * Licensed under the OpenSSL license (the "License"). You may not use + * this file except in compliance with the License. You can obtain a copy + * in the file LICENSE in the source distribution or at + * https://www.openssl.org/source/license.html + */ + +#ifndef HEADER_MD5_H +# define HEADER_MD5_H + +# include + +# ifndef OPENSSL_NO_MD5 +# include +# include +# ifdef __cplusplus +extern "C" { +# endif + +/* + * !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + * ! MD5_LONG has to be at least 32 bits wide. ! + * !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + */ +# define MD5_LONG unsigned int + +# define MD5_CBLOCK 64 +# define MD5_LBLOCK (MD5_CBLOCK/4) +# define MD5_DIGEST_LENGTH 16 + +typedef struct MD5state_st { + MD5_LONG A, B, C, D; + MD5_LONG Nl, Nh; + MD5_LONG data[MD5_LBLOCK]; + unsigned int num; +} MD5_CTX; + +int MD5_Init(MD5_CTX *c); +int MD5_Update(MD5_CTX *c, const void *data, size_t len); +int MD5_Final(unsigned char *md, MD5_CTX *c); +unsigned char *MD5(const unsigned char *d, size_t n, unsigned char *md); +void MD5_Transform(MD5_CTX *c, const unsigned char *b); +# ifdef __cplusplus +} +# endif +# endif + +#endif diff --git a/mobile/ios/FxA/FxA/include/openssl/mdc2.h b/mobile/ios/FxA/FxA/include/openssl/mdc2.h new file mode 100644 index 0000000000..aabd2bfaad --- /dev/null +++ b/mobile/ios/FxA/FxA/include/openssl/mdc2.h @@ -0,0 +1,42 @@ +/* + * Copyright 1995-2016 The OpenSSL Project Authors. All Rights Reserved. + * + * Licensed under the OpenSSL license (the "License"). You may not use + * this file except in compliance with the License. You can obtain a copy + * in the file LICENSE in the source distribution or at + * https://www.openssl.org/source/license.html + */ + +#ifndef HEADER_MDC2_H +# define HEADER_MDC2_H + +# include + +#ifndef OPENSSL_NO_MDC2 +# include +# include +# ifdef __cplusplus +extern "C" { +# endif + +# define MDC2_BLOCK 8 +# define MDC2_DIGEST_LENGTH 16 + +typedef struct mdc2_ctx_st { + unsigned int num; + unsigned char data[MDC2_BLOCK]; + DES_cblock h, hh; + int pad_type; /* either 1 or 2, default 1 */ +} MDC2_CTX; + +int MDC2_Init(MDC2_CTX *c); +int MDC2_Update(MDC2_CTX *c, const unsigned char *data, size_t len); +int MDC2_Final(unsigned char *md, MDC2_CTX *c); +unsigned char *MDC2(const unsigned char *d, size_t n, unsigned char *md); + +# ifdef __cplusplus +} +# endif +# endif + +#endif diff --git a/mobile/ios/FxA/FxA/include/openssl/modes.h b/mobile/ios/FxA/FxA/include/openssl/modes.h new file mode 100644 index 0000000000..a04c6a5981 --- /dev/null +++ b/mobile/ios/FxA/FxA/include/openssl/modes.h @@ -0,0 +1,203 @@ +/* + * Copyright 2008-2016 The OpenSSL Project Authors. All Rights Reserved. + * + * Licensed under the OpenSSL license (the "License"). You may not use + * this file except in compliance with the License. You can obtain a copy + * in the file LICENSE in the source distribution or at + * https://www.openssl.org/source/license.html + */ + +#include + +#ifdef __cplusplus +extern "C" { +#endif +typedef void (*block128_f) (const unsigned char in[16], + unsigned char out[16], const void *key); + +typedef void (*cbc128_f) (const unsigned char *in, unsigned char *out, + size_t len, const void *key, + unsigned char ivec[16], int enc); + +typedef void (*ctr128_f) (const unsigned char *in, unsigned char *out, + size_t blocks, const void *key, + const unsigned char ivec[16]); + +typedef void (*ccm128_f) (const unsigned char *in, unsigned char *out, + size_t blocks, const void *key, + const unsigned char ivec[16], + unsigned char cmac[16]); + +void CRYPTO_cbc128_encrypt(const unsigned char *in, unsigned char *out, + size_t len, const void *key, + unsigned char ivec[16], block128_f block); +void CRYPTO_cbc128_decrypt(const unsigned char *in, unsigned char *out, + size_t len, const void *key, + unsigned char ivec[16], block128_f block); + +void CRYPTO_ctr128_encrypt(const unsigned char *in, unsigned char *out, + size_t len, const void *key, + unsigned char ivec[16], + unsigned char ecount_buf[16], unsigned int *num, + block128_f block); + +void CRYPTO_ctr128_encrypt_ctr32(const unsigned char *in, unsigned char *out, + size_t len, const void *key, + unsigned char ivec[16], + unsigned char ecount_buf[16], + unsigned int *num, ctr128_f ctr); + +void CRYPTO_ofb128_encrypt(const unsigned char *in, unsigned char *out, + size_t len, const void *key, + unsigned char ivec[16], int *num, + block128_f block); + +void CRYPTO_cfb128_encrypt(const unsigned char *in, unsigned char *out, + size_t len, const void *key, + unsigned char ivec[16], int *num, + int enc, block128_f block); +void CRYPTO_cfb128_8_encrypt(const unsigned char *in, unsigned char *out, + size_t length, const void *key, + unsigned char ivec[16], int *num, + int enc, block128_f block); +void CRYPTO_cfb128_1_encrypt(const unsigned char *in, unsigned char *out, + size_t bits, const void *key, + unsigned char ivec[16], int *num, + int enc, block128_f block); + +size_t CRYPTO_cts128_encrypt_block(const unsigned char *in, + unsigned char *out, size_t len, + const void *key, unsigned char ivec[16], + block128_f block); +size_t CRYPTO_cts128_encrypt(const unsigned char *in, unsigned char *out, + size_t len, const void *key, + unsigned char ivec[16], cbc128_f cbc); +size_t CRYPTO_cts128_decrypt_block(const unsigned char *in, + unsigned char *out, size_t len, + const void *key, unsigned char ivec[16], + block128_f block); +size_t CRYPTO_cts128_decrypt(const unsigned char *in, unsigned char *out, + size_t len, const void *key, + unsigned char ivec[16], cbc128_f cbc); + +size_t CRYPTO_nistcts128_encrypt_block(const unsigned char *in, + unsigned char *out, size_t len, + const void *key, + unsigned char ivec[16], + block128_f block); +size_t CRYPTO_nistcts128_encrypt(const unsigned char *in, unsigned char *out, + size_t len, const void *key, + unsigned char ivec[16], cbc128_f cbc); +size_t CRYPTO_nistcts128_decrypt_block(const unsigned char *in, + unsigned char *out, size_t len, + const void *key, + unsigned char ivec[16], + block128_f block); +size_t CRYPTO_nistcts128_decrypt(const unsigned char *in, unsigned char *out, + size_t len, const void *key, + unsigned char ivec[16], cbc128_f cbc); + +typedef struct gcm128_context GCM128_CONTEXT; + +GCM128_CONTEXT *CRYPTO_gcm128_new(void *key, block128_f block); +void CRYPTO_gcm128_init(GCM128_CONTEXT *ctx, void *key, block128_f block); +void CRYPTO_gcm128_setiv(GCM128_CONTEXT *ctx, const unsigned char *iv, + size_t len); +int CRYPTO_gcm128_aad(GCM128_CONTEXT *ctx, const unsigned char *aad, + size_t len); +int CRYPTO_gcm128_encrypt(GCM128_CONTEXT *ctx, + const unsigned char *in, unsigned char *out, + size_t len); +int CRYPTO_gcm128_decrypt(GCM128_CONTEXT *ctx, + const unsigned char *in, unsigned char *out, + size_t len); +int CRYPTO_gcm128_encrypt_ctr32(GCM128_CONTEXT *ctx, + const unsigned char *in, unsigned char *out, + size_t len, ctr128_f stream); +int CRYPTO_gcm128_decrypt_ctr32(GCM128_CONTEXT *ctx, + const unsigned char *in, unsigned char *out, + size_t len, ctr128_f stream); +int CRYPTO_gcm128_finish(GCM128_CONTEXT *ctx, const unsigned char *tag, + size_t len); +void CRYPTO_gcm128_tag(GCM128_CONTEXT *ctx, unsigned char *tag, size_t len); +void CRYPTO_gcm128_release(GCM128_CONTEXT *ctx); + +typedef struct ccm128_context CCM128_CONTEXT; + +void CRYPTO_ccm128_init(CCM128_CONTEXT *ctx, + unsigned int M, unsigned int L, void *key, + block128_f block); +int CRYPTO_ccm128_setiv(CCM128_CONTEXT *ctx, const unsigned char *nonce, + size_t nlen, size_t mlen); +void CRYPTO_ccm128_aad(CCM128_CONTEXT *ctx, const unsigned char *aad, + size_t alen); +int CRYPTO_ccm128_encrypt(CCM128_CONTEXT *ctx, const unsigned char *inp, + unsigned char *out, size_t len); +int CRYPTO_ccm128_decrypt(CCM128_CONTEXT *ctx, const unsigned char *inp, + unsigned char *out, size_t len); +int CRYPTO_ccm128_encrypt_ccm64(CCM128_CONTEXT *ctx, const unsigned char *inp, + unsigned char *out, size_t len, + ccm128_f stream); +int CRYPTO_ccm128_decrypt_ccm64(CCM128_CONTEXT *ctx, const unsigned char *inp, + unsigned char *out, size_t len, + ccm128_f stream); +size_t CRYPTO_ccm128_tag(CCM128_CONTEXT *ctx, unsigned char *tag, size_t len); + +typedef struct xts128_context XTS128_CONTEXT; + +int CRYPTO_xts128_encrypt(const XTS128_CONTEXT *ctx, + const unsigned char iv[16], + const unsigned char *inp, unsigned char *out, + size_t len, int enc); + +size_t CRYPTO_128_wrap(void *key, const unsigned char *iv, + unsigned char *out, + const unsigned char *in, size_t inlen, + block128_f block); + +size_t CRYPTO_128_unwrap(void *key, const unsigned char *iv, + unsigned char *out, + const unsigned char *in, size_t inlen, + block128_f block); +size_t CRYPTO_128_wrap_pad(void *key, const unsigned char *icv, + unsigned char *out, const unsigned char *in, + size_t inlen, block128_f block); +size_t CRYPTO_128_unwrap_pad(void *key, const unsigned char *icv, + unsigned char *out, const unsigned char *in, + size_t inlen, block128_f block); + +#ifndef OPENSSL_NO_OCB +typedef struct ocb128_context OCB128_CONTEXT; + +typedef void (*ocb128_f) (const unsigned char *in, unsigned char *out, + size_t blocks, const void *key, + size_t start_block_num, + unsigned char offset_i[16], + const unsigned char L_[][16], + unsigned char checksum[16]); + +OCB128_CONTEXT *CRYPTO_ocb128_new(void *keyenc, void *keydec, + block128_f encrypt, block128_f decrypt, + ocb128_f stream); +int CRYPTO_ocb128_init(OCB128_CONTEXT *ctx, void *keyenc, void *keydec, + block128_f encrypt, block128_f decrypt, + ocb128_f stream); +int CRYPTO_ocb128_copy_ctx(OCB128_CONTEXT *dest, OCB128_CONTEXT *src, + void *keyenc, void *keydec); +int CRYPTO_ocb128_setiv(OCB128_CONTEXT *ctx, const unsigned char *iv, + size_t len, size_t taglen); +int CRYPTO_ocb128_aad(OCB128_CONTEXT *ctx, const unsigned char *aad, + size_t len); +int CRYPTO_ocb128_encrypt(OCB128_CONTEXT *ctx, const unsigned char *in, + unsigned char *out, size_t len); +int CRYPTO_ocb128_decrypt(OCB128_CONTEXT *ctx, const unsigned char *in, + unsigned char *out, size_t len); +int CRYPTO_ocb128_finish(OCB128_CONTEXT *ctx, const unsigned char *tag, + size_t len); +int CRYPTO_ocb128_tag(OCB128_CONTEXT *ctx, unsigned char *tag, size_t len); +void CRYPTO_ocb128_cleanup(OCB128_CONTEXT *ctx); +#endif /* OPENSSL_NO_OCB */ + +#ifdef __cplusplus +} +#endif diff --git a/mobile/ios/FxA/FxA/include/openssl/obj_mac.h b/mobile/ios/FxA/FxA/include/openssl/obj_mac.h new file mode 100644 index 0000000000..f97f3eaa17 --- /dev/null +++ b/mobile/ios/FxA/FxA/include/openssl/obj_mac.h @@ -0,0 +1,4577 @@ +/* + * WARNING: do not edit! + * Generated by crypto/objects/objects.pl + * + * Copyright 2000-2016 The OpenSSL Project Authors. All Rights Reserved. + * Licensed under the OpenSSL license (the "License"). You may not use + * this file except in compliance with the License. You can obtain a copy + * in the file LICENSE in the source distribution or at + * https://www.openssl.org/source/license.html + */ + +#define SN_undef "UNDEF" +#define LN_undef "undefined" +#define NID_undef 0 +#define OBJ_undef 0L + +#define SN_itu_t "ITU-T" +#define LN_itu_t "itu-t" +#define NID_itu_t 645 +#define OBJ_itu_t 0L + +#define NID_ccitt 404 +#define OBJ_ccitt OBJ_itu_t + +#define SN_iso "ISO" +#define LN_iso "iso" +#define NID_iso 181 +#define OBJ_iso 1L + +#define SN_joint_iso_itu_t "JOINT-ISO-ITU-T" +#define LN_joint_iso_itu_t "joint-iso-itu-t" +#define NID_joint_iso_itu_t 646 +#define OBJ_joint_iso_itu_t 2L + +#define NID_joint_iso_ccitt 393 +#define OBJ_joint_iso_ccitt OBJ_joint_iso_itu_t + +#define SN_member_body "member-body" +#define LN_member_body "ISO Member Body" +#define NID_member_body 182 +#define OBJ_member_body OBJ_iso,2L + +#define SN_identified_organization "identified-organization" +#define NID_identified_organization 676 +#define OBJ_identified_organization OBJ_iso,3L + +#define SN_hmac_md5 "HMAC-MD5" +#define LN_hmac_md5 "hmac-md5" +#define NID_hmac_md5 780 +#define OBJ_hmac_md5 OBJ_identified_organization,6L,1L,5L,5L,8L,1L,1L + +#define SN_hmac_sha1 "HMAC-SHA1" +#define LN_hmac_sha1 "hmac-sha1" +#define NID_hmac_sha1 781 +#define OBJ_hmac_sha1 OBJ_identified_organization,6L,1L,5L,5L,8L,1L,2L + +#define SN_certicom_arc "certicom-arc" +#define NID_certicom_arc 677 +#define OBJ_certicom_arc OBJ_identified_organization,132L + +#define SN_international_organizations "international-organizations" +#define LN_international_organizations "International Organizations" +#define NID_international_organizations 647 +#define OBJ_international_organizations OBJ_joint_iso_itu_t,23L + +#define SN_wap "wap" +#define NID_wap 678 +#define OBJ_wap OBJ_international_organizations,43L + +#define SN_wap_wsg "wap-wsg" +#define NID_wap_wsg 679 +#define OBJ_wap_wsg OBJ_wap,1L + +#define SN_selected_attribute_types "selected-attribute-types" +#define LN_selected_attribute_types "Selected Attribute Types" +#define NID_selected_attribute_types 394 +#define OBJ_selected_attribute_types OBJ_joint_iso_itu_t,5L,1L,5L + +#define SN_clearance "clearance" +#define NID_clearance 395 +#define OBJ_clearance OBJ_selected_attribute_types,55L + +#define SN_ISO_US "ISO-US" +#define LN_ISO_US "ISO US Member Body" +#define NID_ISO_US 183 +#define OBJ_ISO_US OBJ_member_body,840L + +#define SN_X9_57 "X9-57" +#define LN_X9_57 "X9.57" +#define NID_X9_57 184 +#define OBJ_X9_57 OBJ_ISO_US,10040L + +#define SN_X9cm "X9cm" +#define LN_X9cm "X9.57 CM ?" +#define NID_X9cm 185 +#define OBJ_X9cm OBJ_X9_57,4L + +#define SN_dsa "DSA" +#define LN_dsa "dsaEncryption" +#define NID_dsa 116 +#define OBJ_dsa OBJ_X9cm,1L + +#define SN_dsaWithSHA1 "DSA-SHA1" +#define LN_dsaWithSHA1 "dsaWithSHA1" +#define NID_dsaWithSHA1 113 +#define OBJ_dsaWithSHA1 OBJ_X9cm,3L + +#define SN_ansi_X9_62 "ansi-X9-62" +#define LN_ansi_X9_62 "ANSI X9.62" +#define NID_ansi_X9_62 405 +#define OBJ_ansi_X9_62 OBJ_ISO_US,10045L + +#define OBJ_X9_62_id_fieldType OBJ_ansi_X9_62,1L + +#define SN_X9_62_prime_field "prime-field" +#define NID_X9_62_prime_field 406 +#define OBJ_X9_62_prime_field OBJ_X9_62_id_fieldType,1L + +#define SN_X9_62_characteristic_two_field "characteristic-two-field" +#define NID_X9_62_characteristic_two_field 407 +#define OBJ_X9_62_characteristic_two_field OBJ_X9_62_id_fieldType,2L + +#define SN_X9_62_id_characteristic_two_basis "id-characteristic-two-basis" +#define NID_X9_62_id_characteristic_two_basis 680 +#define OBJ_X9_62_id_characteristic_two_basis OBJ_X9_62_characteristic_two_field,3L + +#define SN_X9_62_onBasis "onBasis" +#define NID_X9_62_onBasis 681 +#define OBJ_X9_62_onBasis OBJ_X9_62_id_characteristic_two_basis,1L + +#define SN_X9_62_tpBasis "tpBasis" +#define NID_X9_62_tpBasis 682 +#define OBJ_X9_62_tpBasis OBJ_X9_62_id_characteristic_two_basis,2L + +#define SN_X9_62_ppBasis "ppBasis" +#define NID_X9_62_ppBasis 683 +#define OBJ_X9_62_ppBasis OBJ_X9_62_id_characteristic_two_basis,3L + +#define OBJ_X9_62_id_publicKeyType OBJ_ansi_X9_62,2L + +#define SN_X9_62_id_ecPublicKey "id-ecPublicKey" +#define NID_X9_62_id_ecPublicKey 408 +#define OBJ_X9_62_id_ecPublicKey OBJ_X9_62_id_publicKeyType,1L + +#define OBJ_X9_62_ellipticCurve OBJ_ansi_X9_62,3L + +#define OBJ_X9_62_c_TwoCurve OBJ_X9_62_ellipticCurve,0L + +#define SN_X9_62_c2pnb163v1 "c2pnb163v1" +#define NID_X9_62_c2pnb163v1 684 +#define OBJ_X9_62_c2pnb163v1 OBJ_X9_62_c_TwoCurve,1L + +#define SN_X9_62_c2pnb163v2 "c2pnb163v2" +#define NID_X9_62_c2pnb163v2 685 +#define OBJ_X9_62_c2pnb163v2 OBJ_X9_62_c_TwoCurve,2L + +#define SN_X9_62_c2pnb163v3 "c2pnb163v3" +#define NID_X9_62_c2pnb163v3 686 +#define OBJ_X9_62_c2pnb163v3 OBJ_X9_62_c_TwoCurve,3L + +#define SN_X9_62_c2pnb176v1 "c2pnb176v1" +#define NID_X9_62_c2pnb176v1 687 +#define OBJ_X9_62_c2pnb176v1 OBJ_X9_62_c_TwoCurve,4L + +#define SN_X9_62_c2tnb191v1 "c2tnb191v1" +#define NID_X9_62_c2tnb191v1 688 +#define OBJ_X9_62_c2tnb191v1 OBJ_X9_62_c_TwoCurve,5L + +#define SN_X9_62_c2tnb191v2 "c2tnb191v2" +#define NID_X9_62_c2tnb191v2 689 +#define OBJ_X9_62_c2tnb191v2 OBJ_X9_62_c_TwoCurve,6L + +#define SN_X9_62_c2tnb191v3 "c2tnb191v3" +#define NID_X9_62_c2tnb191v3 690 +#define OBJ_X9_62_c2tnb191v3 OBJ_X9_62_c_TwoCurve,7L + +#define SN_X9_62_c2onb191v4 "c2onb191v4" +#define NID_X9_62_c2onb191v4 691 +#define OBJ_X9_62_c2onb191v4 OBJ_X9_62_c_TwoCurve,8L + +#define SN_X9_62_c2onb191v5 "c2onb191v5" +#define NID_X9_62_c2onb191v5 692 +#define OBJ_X9_62_c2onb191v5 OBJ_X9_62_c_TwoCurve,9L + +#define SN_X9_62_c2pnb208w1 "c2pnb208w1" +#define NID_X9_62_c2pnb208w1 693 +#define OBJ_X9_62_c2pnb208w1 OBJ_X9_62_c_TwoCurve,10L + +#define SN_X9_62_c2tnb239v1 "c2tnb239v1" +#define NID_X9_62_c2tnb239v1 694 +#define OBJ_X9_62_c2tnb239v1 OBJ_X9_62_c_TwoCurve,11L + +#define SN_X9_62_c2tnb239v2 "c2tnb239v2" +#define NID_X9_62_c2tnb239v2 695 +#define OBJ_X9_62_c2tnb239v2 OBJ_X9_62_c_TwoCurve,12L + +#define SN_X9_62_c2tnb239v3 "c2tnb239v3" +#define NID_X9_62_c2tnb239v3 696 +#define OBJ_X9_62_c2tnb239v3 OBJ_X9_62_c_TwoCurve,13L + +#define SN_X9_62_c2onb239v4 "c2onb239v4" +#define NID_X9_62_c2onb239v4 697 +#define OBJ_X9_62_c2onb239v4 OBJ_X9_62_c_TwoCurve,14L + +#define SN_X9_62_c2onb239v5 "c2onb239v5" +#define NID_X9_62_c2onb239v5 698 +#define OBJ_X9_62_c2onb239v5 OBJ_X9_62_c_TwoCurve,15L + +#define SN_X9_62_c2pnb272w1 "c2pnb272w1" +#define NID_X9_62_c2pnb272w1 699 +#define OBJ_X9_62_c2pnb272w1 OBJ_X9_62_c_TwoCurve,16L + +#define SN_X9_62_c2pnb304w1 "c2pnb304w1" +#define NID_X9_62_c2pnb304w1 700 +#define OBJ_X9_62_c2pnb304w1 OBJ_X9_62_c_TwoCurve,17L + +#define SN_X9_62_c2tnb359v1 "c2tnb359v1" +#define NID_X9_62_c2tnb359v1 701 +#define OBJ_X9_62_c2tnb359v1 OBJ_X9_62_c_TwoCurve,18L + +#define SN_X9_62_c2pnb368w1 "c2pnb368w1" +#define NID_X9_62_c2pnb368w1 702 +#define OBJ_X9_62_c2pnb368w1 OBJ_X9_62_c_TwoCurve,19L + +#define SN_X9_62_c2tnb431r1 "c2tnb431r1" +#define NID_X9_62_c2tnb431r1 703 +#define OBJ_X9_62_c2tnb431r1 OBJ_X9_62_c_TwoCurve,20L + +#define OBJ_X9_62_primeCurve OBJ_X9_62_ellipticCurve,1L + +#define SN_X9_62_prime192v1 "prime192v1" +#define NID_X9_62_prime192v1 409 +#define OBJ_X9_62_prime192v1 OBJ_X9_62_primeCurve,1L + +#define SN_X9_62_prime192v2 "prime192v2" +#define NID_X9_62_prime192v2 410 +#define OBJ_X9_62_prime192v2 OBJ_X9_62_primeCurve,2L + +#define SN_X9_62_prime192v3 "prime192v3" +#define NID_X9_62_prime192v3 411 +#define OBJ_X9_62_prime192v3 OBJ_X9_62_primeCurve,3L + +#define SN_X9_62_prime239v1 "prime239v1" +#define NID_X9_62_prime239v1 412 +#define OBJ_X9_62_prime239v1 OBJ_X9_62_primeCurve,4L + +#define SN_X9_62_prime239v2 "prime239v2" +#define NID_X9_62_prime239v2 413 +#define OBJ_X9_62_prime239v2 OBJ_X9_62_primeCurve,5L + +#define SN_X9_62_prime239v3 "prime239v3" +#define NID_X9_62_prime239v3 414 +#define OBJ_X9_62_prime239v3 OBJ_X9_62_primeCurve,6L + +#define SN_X9_62_prime256v1 "prime256v1" +#define NID_X9_62_prime256v1 415 +#define OBJ_X9_62_prime256v1 OBJ_X9_62_primeCurve,7L + +#define OBJ_X9_62_id_ecSigType OBJ_ansi_X9_62,4L + +#define SN_ecdsa_with_SHA1 "ecdsa-with-SHA1" +#define NID_ecdsa_with_SHA1 416 +#define OBJ_ecdsa_with_SHA1 OBJ_X9_62_id_ecSigType,1L + +#define SN_ecdsa_with_Recommended "ecdsa-with-Recommended" +#define NID_ecdsa_with_Recommended 791 +#define OBJ_ecdsa_with_Recommended OBJ_X9_62_id_ecSigType,2L + +#define SN_ecdsa_with_Specified "ecdsa-with-Specified" +#define NID_ecdsa_with_Specified 792 +#define OBJ_ecdsa_with_Specified OBJ_X9_62_id_ecSigType,3L + +#define SN_ecdsa_with_SHA224 "ecdsa-with-SHA224" +#define NID_ecdsa_with_SHA224 793 +#define OBJ_ecdsa_with_SHA224 OBJ_ecdsa_with_Specified,1L + +#define SN_ecdsa_with_SHA256 "ecdsa-with-SHA256" +#define NID_ecdsa_with_SHA256 794 +#define OBJ_ecdsa_with_SHA256 OBJ_ecdsa_with_Specified,2L + +#define SN_ecdsa_with_SHA384 "ecdsa-with-SHA384" +#define NID_ecdsa_with_SHA384 795 +#define OBJ_ecdsa_with_SHA384 OBJ_ecdsa_with_Specified,3L + +#define SN_ecdsa_with_SHA512 "ecdsa-with-SHA512" +#define NID_ecdsa_with_SHA512 796 +#define OBJ_ecdsa_with_SHA512 OBJ_ecdsa_with_Specified,4L + +#define OBJ_secg_ellipticCurve OBJ_certicom_arc,0L + +#define SN_secp112r1 "secp112r1" +#define NID_secp112r1 704 +#define OBJ_secp112r1 OBJ_secg_ellipticCurve,6L + +#define SN_secp112r2 "secp112r2" +#define NID_secp112r2 705 +#define OBJ_secp112r2 OBJ_secg_ellipticCurve,7L + +#define SN_secp128r1 "secp128r1" +#define NID_secp128r1 706 +#define OBJ_secp128r1 OBJ_secg_ellipticCurve,28L + +#define SN_secp128r2 "secp128r2" +#define NID_secp128r2 707 +#define OBJ_secp128r2 OBJ_secg_ellipticCurve,29L + +#define SN_secp160k1 "secp160k1" +#define NID_secp160k1 708 +#define OBJ_secp160k1 OBJ_secg_ellipticCurve,9L + +#define SN_secp160r1 "secp160r1" +#define NID_secp160r1 709 +#define OBJ_secp160r1 OBJ_secg_ellipticCurve,8L + +#define SN_secp160r2 "secp160r2" +#define NID_secp160r2 710 +#define OBJ_secp160r2 OBJ_secg_ellipticCurve,30L + +#define SN_secp192k1 "secp192k1" +#define NID_secp192k1 711 +#define OBJ_secp192k1 OBJ_secg_ellipticCurve,31L + +#define SN_secp224k1 "secp224k1" +#define NID_secp224k1 712 +#define OBJ_secp224k1 OBJ_secg_ellipticCurve,32L + +#define SN_secp224r1 "secp224r1" +#define NID_secp224r1 713 +#define OBJ_secp224r1 OBJ_secg_ellipticCurve,33L + +#define SN_secp256k1 "secp256k1" +#define NID_secp256k1 714 +#define OBJ_secp256k1 OBJ_secg_ellipticCurve,10L + +#define SN_secp384r1 "secp384r1" +#define NID_secp384r1 715 +#define OBJ_secp384r1 OBJ_secg_ellipticCurve,34L + +#define SN_secp521r1 "secp521r1" +#define NID_secp521r1 716 +#define OBJ_secp521r1 OBJ_secg_ellipticCurve,35L + +#define SN_sect113r1 "sect113r1" +#define NID_sect113r1 717 +#define OBJ_sect113r1 OBJ_secg_ellipticCurve,4L + +#define SN_sect113r2 "sect113r2" +#define NID_sect113r2 718 +#define OBJ_sect113r2 OBJ_secg_ellipticCurve,5L + +#define SN_sect131r1 "sect131r1" +#define NID_sect131r1 719 +#define OBJ_sect131r1 OBJ_secg_ellipticCurve,22L + +#define SN_sect131r2 "sect131r2" +#define NID_sect131r2 720 +#define OBJ_sect131r2 OBJ_secg_ellipticCurve,23L + +#define SN_sect163k1 "sect163k1" +#define NID_sect163k1 721 +#define OBJ_sect163k1 OBJ_secg_ellipticCurve,1L + +#define SN_sect163r1 "sect163r1" +#define NID_sect163r1 722 +#define OBJ_sect163r1 OBJ_secg_ellipticCurve,2L + +#define SN_sect163r2 "sect163r2" +#define NID_sect163r2 723 +#define OBJ_sect163r2 OBJ_secg_ellipticCurve,15L + +#define SN_sect193r1 "sect193r1" +#define NID_sect193r1 724 +#define OBJ_sect193r1 OBJ_secg_ellipticCurve,24L + +#define SN_sect193r2 "sect193r2" +#define NID_sect193r2 725 +#define OBJ_sect193r2 OBJ_secg_ellipticCurve,25L + +#define SN_sect233k1 "sect233k1" +#define NID_sect233k1 726 +#define OBJ_sect233k1 OBJ_secg_ellipticCurve,26L + +#define SN_sect233r1 "sect233r1" +#define NID_sect233r1 727 +#define OBJ_sect233r1 OBJ_secg_ellipticCurve,27L + +#define SN_sect239k1 "sect239k1" +#define NID_sect239k1 728 +#define OBJ_sect239k1 OBJ_secg_ellipticCurve,3L + +#define SN_sect283k1 "sect283k1" +#define NID_sect283k1 729 +#define OBJ_sect283k1 OBJ_secg_ellipticCurve,16L + +#define SN_sect283r1 "sect283r1" +#define NID_sect283r1 730 +#define OBJ_sect283r1 OBJ_secg_ellipticCurve,17L + +#define SN_sect409k1 "sect409k1" +#define NID_sect409k1 731 +#define OBJ_sect409k1 OBJ_secg_ellipticCurve,36L + +#define SN_sect409r1 "sect409r1" +#define NID_sect409r1 732 +#define OBJ_sect409r1 OBJ_secg_ellipticCurve,37L + +#define SN_sect571k1 "sect571k1" +#define NID_sect571k1 733 +#define OBJ_sect571k1 OBJ_secg_ellipticCurve,38L + +#define SN_sect571r1 "sect571r1" +#define NID_sect571r1 734 +#define OBJ_sect571r1 OBJ_secg_ellipticCurve,39L + +#define OBJ_wap_wsg_idm_ecid OBJ_wap_wsg,4L + +#define SN_wap_wsg_idm_ecid_wtls1 "wap-wsg-idm-ecid-wtls1" +#define NID_wap_wsg_idm_ecid_wtls1 735 +#define OBJ_wap_wsg_idm_ecid_wtls1 OBJ_wap_wsg_idm_ecid,1L + +#define SN_wap_wsg_idm_ecid_wtls3 "wap-wsg-idm-ecid-wtls3" +#define NID_wap_wsg_idm_ecid_wtls3 736 +#define OBJ_wap_wsg_idm_ecid_wtls3 OBJ_wap_wsg_idm_ecid,3L + +#define SN_wap_wsg_idm_ecid_wtls4 "wap-wsg-idm-ecid-wtls4" +#define NID_wap_wsg_idm_ecid_wtls4 737 +#define OBJ_wap_wsg_idm_ecid_wtls4 OBJ_wap_wsg_idm_ecid,4L + +#define SN_wap_wsg_idm_ecid_wtls5 "wap-wsg-idm-ecid-wtls5" +#define NID_wap_wsg_idm_ecid_wtls5 738 +#define OBJ_wap_wsg_idm_ecid_wtls5 OBJ_wap_wsg_idm_ecid,5L + +#define SN_wap_wsg_idm_ecid_wtls6 "wap-wsg-idm-ecid-wtls6" +#define NID_wap_wsg_idm_ecid_wtls6 739 +#define OBJ_wap_wsg_idm_ecid_wtls6 OBJ_wap_wsg_idm_ecid,6L + +#define SN_wap_wsg_idm_ecid_wtls7 "wap-wsg-idm-ecid-wtls7" +#define NID_wap_wsg_idm_ecid_wtls7 740 +#define OBJ_wap_wsg_idm_ecid_wtls7 OBJ_wap_wsg_idm_ecid,7L + +#define SN_wap_wsg_idm_ecid_wtls8 "wap-wsg-idm-ecid-wtls8" +#define NID_wap_wsg_idm_ecid_wtls8 741 +#define OBJ_wap_wsg_idm_ecid_wtls8 OBJ_wap_wsg_idm_ecid,8L + +#define SN_wap_wsg_idm_ecid_wtls9 "wap-wsg-idm-ecid-wtls9" +#define NID_wap_wsg_idm_ecid_wtls9 742 +#define OBJ_wap_wsg_idm_ecid_wtls9 OBJ_wap_wsg_idm_ecid,9L + +#define SN_wap_wsg_idm_ecid_wtls10 "wap-wsg-idm-ecid-wtls10" +#define NID_wap_wsg_idm_ecid_wtls10 743 +#define OBJ_wap_wsg_idm_ecid_wtls10 OBJ_wap_wsg_idm_ecid,10L + +#define SN_wap_wsg_idm_ecid_wtls11 "wap-wsg-idm-ecid-wtls11" +#define NID_wap_wsg_idm_ecid_wtls11 744 +#define OBJ_wap_wsg_idm_ecid_wtls11 OBJ_wap_wsg_idm_ecid,11L + +#define SN_wap_wsg_idm_ecid_wtls12 "wap-wsg-idm-ecid-wtls12" +#define NID_wap_wsg_idm_ecid_wtls12 745 +#define OBJ_wap_wsg_idm_ecid_wtls12 OBJ_wap_wsg_idm_ecid,12L + +#define SN_cast5_cbc "CAST5-CBC" +#define LN_cast5_cbc "cast5-cbc" +#define NID_cast5_cbc 108 +#define OBJ_cast5_cbc OBJ_ISO_US,113533L,7L,66L,10L + +#define SN_cast5_ecb "CAST5-ECB" +#define LN_cast5_ecb "cast5-ecb" +#define NID_cast5_ecb 109 + +#define SN_cast5_cfb64 "CAST5-CFB" +#define LN_cast5_cfb64 "cast5-cfb" +#define NID_cast5_cfb64 110 + +#define SN_cast5_ofb64 "CAST5-OFB" +#define LN_cast5_ofb64 "cast5-ofb" +#define NID_cast5_ofb64 111 + +#define LN_pbeWithMD5AndCast5_CBC "pbeWithMD5AndCast5CBC" +#define NID_pbeWithMD5AndCast5_CBC 112 +#define OBJ_pbeWithMD5AndCast5_CBC OBJ_ISO_US,113533L,7L,66L,12L + +#define SN_id_PasswordBasedMAC "id-PasswordBasedMAC" +#define LN_id_PasswordBasedMAC "password based MAC" +#define NID_id_PasswordBasedMAC 782 +#define OBJ_id_PasswordBasedMAC OBJ_ISO_US,113533L,7L,66L,13L + +#define SN_id_DHBasedMac "id-DHBasedMac" +#define LN_id_DHBasedMac "Diffie-Hellman based MAC" +#define NID_id_DHBasedMac 783 +#define OBJ_id_DHBasedMac OBJ_ISO_US,113533L,7L,66L,30L + +#define SN_rsadsi "rsadsi" +#define LN_rsadsi "RSA Data Security, Inc." +#define NID_rsadsi 1 +#define OBJ_rsadsi OBJ_ISO_US,113549L + +#define SN_pkcs "pkcs" +#define LN_pkcs "RSA Data Security, Inc. PKCS" +#define NID_pkcs 2 +#define OBJ_pkcs OBJ_rsadsi,1L + +#define SN_pkcs1 "pkcs1" +#define NID_pkcs1 186 +#define OBJ_pkcs1 OBJ_pkcs,1L + +#define LN_rsaEncryption "rsaEncryption" +#define NID_rsaEncryption 6 +#define OBJ_rsaEncryption OBJ_pkcs1,1L + +#define SN_md2WithRSAEncryption "RSA-MD2" +#define LN_md2WithRSAEncryption "md2WithRSAEncryption" +#define NID_md2WithRSAEncryption 7 +#define OBJ_md2WithRSAEncryption OBJ_pkcs1,2L + +#define SN_md4WithRSAEncryption "RSA-MD4" +#define LN_md4WithRSAEncryption "md4WithRSAEncryption" +#define NID_md4WithRSAEncryption 396 +#define OBJ_md4WithRSAEncryption OBJ_pkcs1,3L + +#define SN_md5WithRSAEncryption "RSA-MD5" +#define LN_md5WithRSAEncryption "md5WithRSAEncryption" +#define NID_md5WithRSAEncryption 8 +#define OBJ_md5WithRSAEncryption OBJ_pkcs1,4L + +#define SN_sha1WithRSAEncryption "RSA-SHA1" +#define LN_sha1WithRSAEncryption "sha1WithRSAEncryption" +#define NID_sha1WithRSAEncryption 65 +#define OBJ_sha1WithRSAEncryption OBJ_pkcs1,5L + +#define SN_rsaesOaep "RSAES-OAEP" +#define LN_rsaesOaep "rsaesOaep" +#define NID_rsaesOaep 919 +#define OBJ_rsaesOaep OBJ_pkcs1,7L + +#define SN_mgf1 "MGF1" +#define LN_mgf1 "mgf1" +#define NID_mgf1 911 +#define OBJ_mgf1 OBJ_pkcs1,8L + +#define SN_pSpecified "PSPECIFIED" +#define LN_pSpecified "pSpecified" +#define NID_pSpecified 935 +#define OBJ_pSpecified OBJ_pkcs1,9L + +#define SN_rsassaPss "RSASSA-PSS" +#define LN_rsassaPss "rsassaPss" +#define NID_rsassaPss 912 +#define OBJ_rsassaPss OBJ_pkcs1,10L + +#define SN_sha256WithRSAEncryption "RSA-SHA256" +#define LN_sha256WithRSAEncryption "sha256WithRSAEncryption" +#define NID_sha256WithRSAEncryption 668 +#define OBJ_sha256WithRSAEncryption OBJ_pkcs1,11L + +#define SN_sha384WithRSAEncryption "RSA-SHA384" +#define LN_sha384WithRSAEncryption "sha384WithRSAEncryption" +#define NID_sha384WithRSAEncryption 669 +#define OBJ_sha384WithRSAEncryption OBJ_pkcs1,12L + +#define SN_sha512WithRSAEncryption "RSA-SHA512" +#define LN_sha512WithRSAEncryption "sha512WithRSAEncryption" +#define NID_sha512WithRSAEncryption 670 +#define OBJ_sha512WithRSAEncryption OBJ_pkcs1,13L + +#define SN_sha224WithRSAEncryption "RSA-SHA224" +#define LN_sha224WithRSAEncryption "sha224WithRSAEncryption" +#define NID_sha224WithRSAEncryption 671 +#define OBJ_sha224WithRSAEncryption OBJ_pkcs1,14L + +#define SN_pkcs3 "pkcs3" +#define NID_pkcs3 27 +#define OBJ_pkcs3 OBJ_pkcs,3L + +#define LN_dhKeyAgreement "dhKeyAgreement" +#define NID_dhKeyAgreement 28 +#define OBJ_dhKeyAgreement OBJ_pkcs3,1L + +#define SN_pkcs5 "pkcs5" +#define NID_pkcs5 187 +#define OBJ_pkcs5 OBJ_pkcs,5L + +#define SN_pbeWithMD2AndDES_CBC "PBE-MD2-DES" +#define LN_pbeWithMD2AndDES_CBC "pbeWithMD2AndDES-CBC" +#define NID_pbeWithMD2AndDES_CBC 9 +#define OBJ_pbeWithMD2AndDES_CBC OBJ_pkcs5,1L + +#define SN_pbeWithMD5AndDES_CBC "PBE-MD5-DES" +#define LN_pbeWithMD5AndDES_CBC "pbeWithMD5AndDES-CBC" +#define NID_pbeWithMD5AndDES_CBC 10 +#define OBJ_pbeWithMD5AndDES_CBC OBJ_pkcs5,3L + +#define SN_pbeWithMD2AndRC2_CBC "PBE-MD2-RC2-64" +#define LN_pbeWithMD2AndRC2_CBC "pbeWithMD2AndRC2-CBC" +#define NID_pbeWithMD2AndRC2_CBC 168 +#define OBJ_pbeWithMD2AndRC2_CBC OBJ_pkcs5,4L + +#define SN_pbeWithMD5AndRC2_CBC "PBE-MD5-RC2-64" +#define LN_pbeWithMD5AndRC2_CBC "pbeWithMD5AndRC2-CBC" +#define NID_pbeWithMD5AndRC2_CBC 169 +#define OBJ_pbeWithMD5AndRC2_CBC OBJ_pkcs5,6L + +#define SN_pbeWithSHA1AndDES_CBC "PBE-SHA1-DES" +#define LN_pbeWithSHA1AndDES_CBC "pbeWithSHA1AndDES-CBC" +#define NID_pbeWithSHA1AndDES_CBC 170 +#define OBJ_pbeWithSHA1AndDES_CBC OBJ_pkcs5,10L + +#define SN_pbeWithSHA1AndRC2_CBC "PBE-SHA1-RC2-64" +#define LN_pbeWithSHA1AndRC2_CBC "pbeWithSHA1AndRC2-CBC" +#define NID_pbeWithSHA1AndRC2_CBC 68 +#define OBJ_pbeWithSHA1AndRC2_CBC OBJ_pkcs5,11L + +#define LN_id_pbkdf2 "PBKDF2" +#define NID_id_pbkdf2 69 +#define OBJ_id_pbkdf2 OBJ_pkcs5,12L + +#define LN_pbes2 "PBES2" +#define NID_pbes2 161 +#define OBJ_pbes2 OBJ_pkcs5,13L + +#define LN_pbmac1 "PBMAC1" +#define NID_pbmac1 162 +#define OBJ_pbmac1 OBJ_pkcs5,14L + +#define SN_pkcs7 "pkcs7" +#define NID_pkcs7 20 +#define OBJ_pkcs7 OBJ_pkcs,7L + +#define LN_pkcs7_data "pkcs7-data" +#define NID_pkcs7_data 21 +#define OBJ_pkcs7_data OBJ_pkcs7,1L + +#define LN_pkcs7_signed "pkcs7-signedData" +#define NID_pkcs7_signed 22 +#define OBJ_pkcs7_signed OBJ_pkcs7,2L + +#define LN_pkcs7_enveloped "pkcs7-envelopedData" +#define NID_pkcs7_enveloped 23 +#define OBJ_pkcs7_enveloped OBJ_pkcs7,3L + +#define LN_pkcs7_signedAndEnveloped "pkcs7-signedAndEnvelopedData" +#define NID_pkcs7_signedAndEnveloped 24 +#define OBJ_pkcs7_signedAndEnveloped OBJ_pkcs7,4L + +#define LN_pkcs7_digest "pkcs7-digestData" +#define NID_pkcs7_digest 25 +#define OBJ_pkcs7_digest OBJ_pkcs7,5L + +#define LN_pkcs7_encrypted "pkcs7-encryptedData" +#define NID_pkcs7_encrypted 26 +#define OBJ_pkcs7_encrypted OBJ_pkcs7,6L + +#define SN_pkcs9 "pkcs9" +#define NID_pkcs9 47 +#define OBJ_pkcs9 OBJ_pkcs,9L + +#define LN_pkcs9_emailAddress "emailAddress" +#define NID_pkcs9_emailAddress 48 +#define OBJ_pkcs9_emailAddress OBJ_pkcs9,1L + +#define LN_pkcs9_unstructuredName "unstructuredName" +#define NID_pkcs9_unstructuredName 49 +#define OBJ_pkcs9_unstructuredName OBJ_pkcs9,2L + +#define LN_pkcs9_contentType "contentType" +#define NID_pkcs9_contentType 50 +#define OBJ_pkcs9_contentType OBJ_pkcs9,3L + +#define LN_pkcs9_messageDigest "messageDigest" +#define NID_pkcs9_messageDigest 51 +#define OBJ_pkcs9_messageDigest OBJ_pkcs9,4L + +#define LN_pkcs9_signingTime "signingTime" +#define NID_pkcs9_signingTime 52 +#define OBJ_pkcs9_signingTime OBJ_pkcs9,5L + +#define LN_pkcs9_countersignature "countersignature" +#define NID_pkcs9_countersignature 53 +#define OBJ_pkcs9_countersignature OBJ_pkcs9,6L + +#define LN_pkcs9_challengePassword "challengePassword" +#define NID_pkcs9_challengePassword 54 +#define OBJ_pkcs9_challengePassword OBJ_pkcs9,7L + +#define LN_pkcs9_unstructuredAddress "unstructuredAddress" +#define NID_pkcs9_unstructuredAddress 55 +#define OBJ_pkcs9_unstructuredAddress OBJ_pkcs9,8L + +#define LN_pkcs9_extCertAttributes "extendedCertificateAttributes" +#define NID_pkcs9_extCertAttributes 56 +#define OBJ_pkcs9_extCertAttributes OBJ_pkcs9,9L + +#define SN_ext_req "extReq" +#define LN_ext_req "Extension Request" +#define NID_ext_req 172 +#define OBJ_ext_req OBJ_pkcs9,14L + +#define SN_SMIMECapabilities "SMIME-CAPS" +#define LN_SMIMECapabilities "S/MIME Capabilities" +#define NID_SMIMECapabilities 167 +#define OBJ_SMIMECapabilities OBJ_pkcs9,15L + +#define SN_SMIME "SMIME" +#define LN_SMIME "S/MIME" +#define NID_SMIME 188 +#define OBJ_SMIME OBJ_pkcs9,16L + +#define SN_id_smime_mod "id-smime-mod" +#define NID_id_smime_mod 189 +#define OBJ_id_smime_mod OBJ_SMIME,0L + +#define SN_id_smime_ct "id-smime-ct" +#define NID_id_smime_ct 190 +#define OBJ_id_smime_ct OBJ_SMIME,1L + +#define SN_id_smime_aa "id-smime-aa" +#define NID_id_smime_aa 191 +#define OBJ_id_smime_aa OBJ_SMIME,2L + +#define SN_id_smime_alg "id-smime-alg" +#define NID_id_smime_alg 192 +#define OBJ_id_smime_alg OBJ_SMIME,3L + +#define SN_id_smime_cd "id-smime-cd" +#define NID_id_smime_cd 193 +#define OBJ_id_smime_cd OBJ_SMIME,4L + +#define SN_id_smime_spq "id-smime-spq" +#define NID_id_smime_spq 194 +#define OBJ_id_smime_spq OBJ_SMIME,5L + +#define SN_id_smime_cti "id-smime-cti" +#define NID_id_smime_cti 195 +#define OBJ_id_smime_cti OBJ_SMIME,6L + +#define SN_id_smime_mod_cms "id-smime-mod-cms" +#define NID_id_smime_mod_cms 196 +#define OBJ_id_smime_mod_cms OBJ_id_smime_mod,1L + +#define SN_id_smime_mod_ess "id-smime-mod-ess" +#define NID_id_smime_mod_ess 197 +#define OBJ_id_smime_mod_ess OBJ_id_smime_mod,2L + +#define SN_id_smime_mod_oid "id-smime-mod-oid" +#define NID_id_smime_mod_oid 198 +#define OBJ_id_smime_mod_oid OBJ_id_smime_mod,3L + +#define SN_id_smime_mod_msg_v3 "id-smime-mod-msg-v3" +#define NID_id_smime_mod_msg_v3 199 +#define OBJ_id_smime_mod_msg_v3 OBJ_id_smime_mod,4L + +#define SN_id_smime_mod_ets_eSignature_88 "id-smime-mod-ets-eSignature-88" +#define NID_id_smime_mod_ets_eSignature_88 200 +#define OBJ_id_smime_mod_ets_eSignature_88 OBJ_id_smime_mod,5L + +#define SN_id_smime_mod_ets_eSignature_97 "id-smime-mod-ets-eSignature-97" +#define NID_id_smime_mod_ets_eSignature_97 201 +#define OBJ_id_smime_mod_ets_eSignature_97 OBJ_id_smime_mod,6L + +#define SN_id_smime_mod_ets_eSigPolicy_88 "id-smime-mod-ets-eSigPolicy-88" +#define NID_id_smime_mod_ets_eSigPolicy_88 202 +#define OBJ_id_smime_mod_ets_eSigPolicy_88 OBJ_id_smime_mod,7L + +#define SN_id_smime_mod_ets_eSigPolicy_97 "id-smime-mod-ets-eSigPolicy-97" +#define NID_id_smime_mod_ets_eSigPolicy_97 203 +#define OBJ_id_smime_mod_ets_eSigPolicy_97 OBJ_id_smime_mod,8L + +#define SN_id_smime_ct_receipt "id-smime-ct-receipt" +#define NID_id_smime_ct_receipt 204 +#define OBJ_id_smime_ct_receipt OBJ_id_smime_ct,1L + +#define SN_id_smime_ct_authData "id-smime-ct-authData" +#define NID_id_smime_ct_authData 205 +#define OBJ_id_smime_ct_authData OBJ_id_smime_ct,2L + +#define SN_id_smime_ct_publishCert "id-smime-ct-publishCert" +#define NID_id_smime_ct_publishCert 206 +#define OBJ_id_smime_ct_publishCert OBJ_id_smime_ct,3L + +#define SN_id_smime_ct_TSTInfo "id-smime-ct-TSTInfo" +#define NID_id_smime_ct_TSTInfo 207 +#define OBJ_id_smime_ct_TSTInfo OBJ_id_smime_ct,4L + +#define SN_id_smime_ct_TDTInfo "id-smime-ct-TDTInfo" +#define NID_id_smime_ct_TDTInfo 208 +#define OBJ_id_smime_ct_TDTInfo OBJ_id_smime_ct,5L + +#define SN_id_smime_ct_contentInfo "id-smime-ct-contentInfo" +#define NID_id_smime_ct_contentInfo 209 +#define OBJ_id_smime_ct_contentInfo OBJ_id_smime_ct,6L + +#define SN_id_smime_ct_DVCSRequestData "id-smime-ct-DVCSRequestData" +#define NID_id_smime_ct_DVCSRequestData 210 +#define OBJ_id_smime_ct_DVCSRequestData OBJ_id_smime_ct,7L + +#define SN_id_smime_ct_DVCSResponseData "id-smime-ct-DVCSResponseData" +#define NID_id_smime_ct_DVCSResponseData 211 +#define OBJ_id_smime_ct_DVCSResponseData OBJ_id_smime_ct,8L + +#define SN_id_smime_ct_compressedData "id-smime-ct-compressedData" +#define NID_id_smime_ct_compressedData 786 +#define OBJ_id_smime_ct_compressedData OBJ_id_smime_ct,9L + +#define SN_id_smime_ct_contentCollection "id-smime-ct-contentCollection" +#define NID_id_smime_ct_contentCollection 1058 +#define OBJ_id_smime_ct_contentCollection OBJ_id_smime_ct,19L + +#define SN_id_smime_ct_authEnvelopedData "id-smime-ct-authEnvelopedData" +#define NID_id_smime_ct_authEnvelopedData 1059 +#define OBJ_id_smime_ct_authEnvelopedData OBJ_id_smime_ct,23L + +#define SN_id_ct_asciiTextWithCRLF "id-ct-asciiTextWithCRLF" +#define NID_id_ct_asciiTextWithCRLF 787 +#define OBJ_id_ct_asciiTextWithCRLF OBJ_id_smime_ct,27L + +#define SN_id_ct_xml "id-ct-xml" +#define NID_id_ct_xml 1060 +#define OBJ_id_ct_xml OBJ_id_smime_ct,28L + +#define SN_id_smime_aa_receiptRequest "id-smime-aa-receiptRequest" +#define NID_id_smime_aa_receiptRequest 212 +#define OBJ_id_smime_aa_receiptRequest OBJ_id_smime_aa,1L + +#define SN_id_smime_aa_securityLabel "id-smime-aa-securityLabel" +#define NID_id_smime_aa_securityLabel 213 +#define OBJ_id_smime_aa_securityLabel OBJ_id_smime_aa,2L + +#define SN_id_smime_aa_mlExpandHistory "id-smime-aa-mlExpandHistory" +#define NID_id_smime_aa_mlExpandHistory 214 +#define OBJ_id_smime_aa_mlExpandHistory OBJ_id_smime_aa,3L + +#define SN_id_smime_aa_contentHint "id-smime-aa-contentHint" +#define NID_id_smime_aa_contentHint 215 +#define OBJ_id_smime_aa_contentHint OBJ_id_smime_aa,4L + +#define SN_id_smime_aa_msgSigDigest "id-smime-aa-msgSigDigest" +#define NID_id_smime_aa_msgSigDigest 216 +#define OBJ_id_smime_aa_msgSigDigest OBJ_id_smime_aa,5L + +#define SN_id_smime_aa_encapContentType "id-smime-aa-encapContentType" +#define NID_id_smime_aa_encapContentType 217 +#define OBJ_id_smime_aa_encapContentType OBJ_id_smime_aa,6L + +#define SN_id_smime_aa_contentIdentifier "id-smime-aa-contentIdentifier" +#define NID_id_smime_aa_contentIdentifier 218 +#define OBJ_id_smime_aa_contentIdentifier OBJ_id_smime_aa,7L + +#define SN_id_smime_aa_macValue "id-smime-aa-macValue" +#define NID_id_smime_aa_macValue 219 +#define OBJ_id_smime_aa_macValue OBJ_id_smime_aa,8L + +#define SN_id_smime_aa_equivalentLabels "id-smime-aa-equivalentLabels" +#define NID_id_smime_aa_equivalentLabels 220 +#define OBJ_id_smime_aa_equivalentLabels OBJ_id_smime_aa,9L + +#define SN_id_smime_aa_contentReference "id-smime-aa-contentReference" +#define NID_id_smime_aa_contentReference 221 +#define OBJ_id_smime_aa_contentReference OBJ_id_smime_aa,10L + +#define SN_id_smime_aa_encrypKeyPref "id-smime-aa-encrypKeyPref" +#define NID_id_smime_aa_encrypKeyPref 222 +#define OBJ_id_smime_aa_encrypKeyPref OBJ_id_smime_aa,11L + +#define SN_id_smime_aa_signingCertificate "id-smime-aa-signingCertificate" +#define NID_id_smime_aa_signingCertificate 223 +#define OBJ_id_smime_aa_signingCertificate OBJ_id_smime_aa,12L + +#define SN_id_smime_aa_smimeEncryptCerts "id-smime-aa-smimeEncryptCerts" +#define NID_id_smime_aa_smimeEncryptCerts 224 +#define OBJ_id_smime_aa_smimeEncryptCerts OBJ_id_smime_aa,13L + +#define SN_id_smime_aa_timeStampToken "id-smime-aa-timeStampToken" +#define NID_id_smime_aa_timeStampToken 225 +#define OBJ_id_smime_aa_timeStampToken OBJ_id_smime_aa,14L + +#define SN_id_smime_aa_ets_sigPolicyId "id-smime-aa-ets-sigPolicyId" +#define NID_id_smime_aa_ets_sigPolicyId 226 +#define OBJ_id_smime_aa_ets_sigPolicyId OBJ_id_smime_aa,15L + +#define SN_id_smime_aa_ets_commitmentType "id-smime-aa-ets-commitmentType" +#define NID_id_smime_aa_ets_commitmentType 227 +#define OBJ_id_smime_aa_ets_commitmentType OBJ_id_smime_aa,16L + +#define SN_id_smime_aa_ets_signerLocation "id-smime-aa-ets-signerLocation" +#define NID_id_smime_aa_ets_signerLocation 228 +#define OBJ_id_smime_aa_ets_signerLocation OBJ_id_smime_aa,17L + +#define SN_id_smime_aa_ets_signerAttr "id-smime-aa-ets-signerAttr" +#define NID_id_smime_aa_ets_signerAttr 229 +#define OBJ_id_smime_aa_ets_signerAttr OBJ_id_smime_aa,18L + +#define SN_id_smime_aa_ets_otherSigCert "id-smime-aa-ets-otherSigCert" +#define NID_id_smime_aa_ets_otherSigCert 230 +#define OBJ_id_smime_aa_ets_otherSigCert OBJ_id_smime_aa,19L + +#define SN_id_smime_aa_ets_contentTimestamp "id-smime-aa-ets-contentTimestamp" +#define NID_id_smime_aa_ets_contentTimestamp 231 +#define OBJ_id_smime_aa_ets_contentTimestamp OBJ_id_smime_aa,20L + +#define SN_id_smime_aa_ets_CertificateRefs "id-smime-aa-ets-CertificateRefs" +#define NID_id_smime_aa_ets_CertificateRefs 232 +#define OBJ_id_smime_aa_ets_CertificateRefs OBJ_id_smime_aa,21L + +#define SN_id_smime_aa_ets_RevocationRefs "id-smime-aa-ets-RevocationRefs" +#define NID_id_smime_aa_ets_RevocationRefs 233 +#define OBJ_id_smime_aa_ets_RevocationRefs OBJ_id_smime_aa,22L + +#define SN_id_smime_aa_ets_certValues "id-smime-aa-ets-certValues" +#define NID_id_smime_aa_ets_certValues 234 +#define OBJ_id_smime_aa_ets_certValues OBJ_id_smime_aa,23L + +#define SN_id_smime_aa_ets_revocationValues "id-smime-aa-ets-revocationValues" +#define NID_id_smime_aa_ets_revocationValues 235 +#define OBJ_id_smime_aa_ets_revocationValues OBJ_id_smime_aa,24L + +#define SN_id_smime_aa_ets_escTimeStamp "id-smime-aa-ets-escTimeStamp" +#define NID_id_smime_aa_ets_escTimeStamp 236 +#define OBJ_id_smime_aa_ets_escTimeStamp OBJ_id_smime_aa,25L + +#define SN_id_smime_aa_ets_certCRLTimestamp "id-smime-aa-ets-certCRLTimestamp" +#define NID_id_smime_aa_ets_certCRLTimestamp 237 +#define OBJ_id_smime_aa_ets_certCRLTimestamp OBJ_id_smime_aa,26L + +#define SN_id_smime_aa_ets_archiveTimeStamp "id-smime-aa-ets-archiveTimeStamp" +#define NID_id_smime_aa_ets_archiveTimeStamp 238 +#define OBJ_id_smime_aa_ets_archiveTimeStamp OBJ_id_smime_aa,27L + +#define SN_id_smime_aa_signatureType "id-smime-aa-signatureType" +#define NID_id_smime_aa_signatureType 239 +#define OBJ_id_smime_aa_signatureType OBJ_id_smime_aa,28L + +#define SN_id_smime_aa_dvcs_dvc "id-smime-aa-dvcs-dvc" +#define NID_id_smime_aa_dvcs_dvc 240 +#define OBJ_id_smime_aa_dvcs_dvc OBJ_id_smime_aa,29L + +#define SN_id_smime_alg_ESDHwith3DES "id-smime-alg-ESDHwith3DES" +#define NID_id_smime_alg_ESDHwith3DES 241 +#define OBJ_id_smime_alg_ESDHwith3DES OBJ_id_smime_alg,1L + +#define SN_id_smime_alg_ESDHwithRC2 "id-smime-alg-ESDHwithRC2" +#define NID_id_smime_alg_ESDHwithRC2 242 +#define OBJ_id_smime_alg_ESDHwithRC2 OBJ_id_smime_alg,2L + +#define SN_id_smime_alg_3DESwrap "id-smime-alg-3DESwrap" +#define NID_id_smime_alg_3DESwrap 243 +#define OBJ_id_smime_alg_3DESwrap OBJ_id_smime_alg,3L + +#define SN_id_smime_alg_RC2wrap "id-smime-alg-RC2wrap" +#define NID_id_smime_alg_RC2wrap 244 +#define OBJ_id_smime_alg_RC2wrap OBJ_id_smime_alg,4L + +#define SN_id_smime_alg_ESDH "id-smime-alg-ESDH" +#define NID_id_smime_alg_ESDH 245 +#define OBJ_id_smime_alg_ESDH OBJ_id_smime_alg,5L + +#define SN_id_smime_alg_CMS3DESwrap "id-smime-alg-CMS3DESwrap" +#define NID_id_smime_alg_CMS3DESwrap 246 +#define OBJ_id_smime_alg_CMS3DESwrap OBJ_id_smime_alg,6L + +#define SN_id_smime_alg_CMSRC2wrap "id-smime-alg-CMSRC2wrap" +#define NID_id_smime_alg_CMSRC2wrap 247 +#define OBJ_id_smime_alg_CMSRC2wrap OBJ_id_smime_alg,7L + +#define SN_id_alg_PWRI_KEK "id-alg-PWRI-KEK" +#define NID_id_alg_PWRI_KEK 893 +#define OBJ_id_alg_PWRI_KEK OBJ_id_smime_alg,9L + +#define SN_id_smime_cd_ldap "id-smime-cd-ldap" +#define NID_id_smime_cd_ldap 248 +#define OBJ_id_smime_cd_ldap OBJ_id_smime_cd,1L + +#define SN_id_smime_spq_ets_sqt_uri "id-smime-spq-ets-sqt-uri" +#define NID_id_smime_spq_ets_sqt_uri 249 +#define OBJ_id_smime_spq_ets_sqt_uri OBJ_id_smime_spq,1L + +#define SN_id_smime_spq_ets_sqt_unotice "id-smime-spq-ets-sqt-unotice" +#define NID_id_smime_spq_ets_sqt_unotice 250 +#define OBJ_id_smime_spq_ets_sqt_unotice OBJ_id_smime_spq,2L + +#define SN_id_smime_cti_ets_proofOfOrigin "id-smime-cti-ets-proofOfOrigin" +#define NID_id_smime_cti_ets_proofOfOrigin 251 +#define OBJ_id_smime_cti_ets_proofOfOrigin OBJ_id_smime_cti,1L + +#define SN_id_smime_cti_ets_proofOfReceipt "id-smime-cti-ets-proofOfReceipt" +#define NID_id_smime_cti_ets_proofOfReceipt 252 +#define OBJ_id_smime_cti_ets_proofOfReceipt OBJ_id_smime_cti,2L + +#define SN_id_smime_cti_ets_proofOfDelivery "id-smime-cti-ets-proofOfDelivery" +#define NID_id_smime_cti_ets_proofOfDelivery 253 +#define OBJ_id_smime_cti_ets_proofOfDelivery OBJ_id_smime_cti,3L + +#define SN_id_smime_cti_ets_proofOfSender "id-smime-cti-ets-proofOfSender" +#define NID_id_smime_cti_ets_proofOfSender 254 +#define OBJ_id_smime_cti_ets_proofOfSender OBJ_id_smime_cti,4L + +#define SN_id_smime_cti_ets_proofOfApproval "id-smime-cti-ets-proofOfApproval" +#define NID_id_smime_cti_ets_proofOfApproval 255 +#define OBJ_id_smime_cti_ets_proofOfApproval OBJ_id_smime_cti,5L + +#define SN_id_smime_cti_ets_proofOfCreation "id-smime-cti-ets-proofOfCreation" +#define NID_id_smime_cti_ets_proofOfCreation 256 +#define OBJ_id_smime_cti_ets_proofOfCreation OBJ_id_smime_cti,6L + +#define LN_friendlyName "friendlyName" +#define NID_friendlyName 156 +#define OBJ_friendlyName OBJ_pkcs9,20L + +#define LN_localKeyID "localKeyID" +#define NID_localKeyID 157 +#define OBJ_localKeyID OBJ_pkcs9,21L + +#define SN_ms_csp_name "CSPName" +#define LN_ms_csp_name "Microsoft CSP Name" +#define NID_ms_csp_name 417 +#define OBJ_ms_csp_name 1L,3L,6L,1L,4L,1L,311L,17L,1L + +#define SN_LocalKeySet "LocalKeySet" +#define LN_LocalKeySet "Microsoft Local Key set" +#define NID_LocalKeySet 856 +#define OBJ_LocalKeySet 1L,3L,6L,1L,4L,1L,311L,17L,2L + +#define OBJ_certTypes OBJ_pkcs9,22L + +#define LN_x509Certificate "x509Certificate" +#define NID_x509Certificate 158 +#define OBJ_x509Certificate OBJ_certTypes,1L + +#define LN_sdsiCertificate "sdsiCertificate" +#define NID_sdsiCertificate 159 +#define OBJ_sdsiCertificate OBJ_certTypes,2L + +#define OBJ_crlTypes OBJ_pkcs9,23L + +#define LN_x509Crl "x509Crl" +#define NID_x509Crl 160 +#define OBJ_x509Crl OBJ_crlTypes,1L + +#define OBJ_pkcs12 OBJ_pkcs,12L + +#define OBJ_pkcs12_pbeids OBJ_pkcs12,1L + +#define SN_pbe_WithSHA1And128BitRC4 "PBE-SHA1-RC4-128" +#define LN_pbe_WithSHA1And128BitRC4 "pbeWithSHA1And128BitRC4" +#define NID_pbe_WithSHA1And128BitRC4 144 +#define OBJ_pbe_WithSHA1And128BitRC4 OBJ_pkcs12_pbeids,1L + +#define SN_pbe_WithSHA1And40BitRC4 "PBE-SHA1-RC4-40" +#define LN_pbe_WithSHA1And40BitRC4 "pbeWithSHA1And40BitRC4" +#define NID_pbe_WithSHA1And40BitRC4 145 +#define OBJ_pbe_WithSHA1And40BitRC4 OBJ_pkcs12_pbeids,2L + +#define SN_pbe_WithSHA1And3_Key_TripleDES_CBC "PBE-SHA1-3DES" +#define LN_pbe_WithSHA1And3_Key_TripleDES_CBC "pbeWithSHA1And3-KeyTripleDES-CBC" +#define NID_pbe_WithSHA1And3_Key_TripleDES_CBC 146 +#define OBJ_pbe_WithSHA1And3_Key_TripleDES_CBC OBJ_pkcs12_pbeids,3L + +#define SN_pbe_WithSHA1And2_Key_TripleDES_CBC "PBE-SHA1-2DES" +#define LN_pbe_WithSHA1And2_Key_TripleDES_CBC "pbeWithSHA1And2-KeyTripleDES-CBC" +#define NID_pbe_WithSHA1And2_Key_TripleDES_CBC 147 +#define OBJ_pbe_WithSHA1And2_Key_TripleDES_CBC OBJ_pkcs12_pbeids,4L + +#define SN_pbe_WithSHA1And128BitRC2_CBC "PBE-SHA1-RC2-128" +#define LN_pbe_WithSHA1And128BitRC2_CBC "pbeWithSHA1And128BitRC2-CBC" +#define NID_pbe_WithSHA1And128BitRC2_CBC 148 +#define OBJ_pbe_WithSHA1And128BitRC2_CBC OBJ_pkcs12_pbeids,5L + +#define SN_pbe_WithSHA1And40BitRC2_CBC "PBE-SHA1-RC2-40" +#define LN_pbe_WithSHA1And40BitRC2_CBC "pbeWithSHA1And40BitRC2-CBC" +#define NID_pbe_WithSHA1And40BitRC2_CBC 149 +#define OBJ_pbe_WithSHA1And40BitRC2_CBC OBJ_pkcs12_pbeids,6L + +#define OBJ_pkcs12_Version1 OBJ_pkcs12,10L + +#define OBJ_pkcs12_BagIds OBJ_pkcs12_Version1,1L + +#define LN_keyBag "keyBag" +#define NID_keyBag 150 +#define OBJ_keyBag OBJ_pkcs12_BagIds,1L + +#define LN_pkcs8ShroudedKeyBag "pkcs8ShroudedKeyBag" +#define NID_pkcs8ShroudedKeyBag 151 +#define OBJ_pkcs8ShroudedKeyBag OBJ_pkcs12_BagIds,2L + +#define LN_certBag "certBag" +#define NID_certBag 152 +#define OBJ_certBag OBJ_pkcs12_BagIds,3L + +#define LN_crlBag "crlBag" +#define NID_crlBag 153 +#define OBJ_crlBag OBJ_pkcs12_BagIds,4L + +#define LN_secretBag "secretBag" +#define NID_secretBag 154 +#define OBJ_secretBag OBJ_pkcs12_BagIds,5L + +#define LN_safeContentsBag "safeContentsBag" +#define NID_safeContentsBag 155 +#define OBJ_safeContentsBag OBJ_pkcs12_BagIds,6L + +#define SN_md2 "MD2" +#define LN_md2 "md2" +#define NID_md2 3 +#define OBJ_md2 OBJ_rsadsi,2L,2L + +#define SN_md4 "MD4" +#define LN_md4 "md4" +#define NID_md4 257 +#define OBJ_md4 OBJ_rsadsi,2L,4L + +#define SN_md5 "MD5" +#define LN_md5 "md5" +#define NID_md5 4 +#define OBJ_md5 OBJ_rsadsi,2L,5L + +#define SN_md5_sha1 "MD5-SHA1" +#define LN_md5_sha1 "md5-sha1" +#define NID_md5_sha1 114 + +#define LN_hmacWithMD5 "hmacWithMD5" +#define NID_hmacWithMD5 797 +#define OBJ_hmacWithMD5 OBJ_rsadsi,2L,6L + +#define LN_hmacWithSHA1 "hmacWithSHA1" +#define NID_hmacWithSHA1 163 +#define OBJ_hmacWithSHA1 OBJ_rsadsi,2L,7L + +#define LN_hmacWithSHA224 "hmacWithSHA224" +#define NID_hmacWithSHA224 798 +#define OBJ_hmacWithSHA224 OBJ_rsadsi,2L,8L + +#define LN_hmacWithSHA256 "hmacWithSHA256" +#define NID_hmacWithSHA256 799 +#define OBJ_hmacWithSHA256 OBJ_rsadsi,2L,9L + +#define LN_hmacWithSHA384 "hmacWithSHA384" +#define NID_hmacWithSHA384 800 +#define OBJ_hmacWithSHA384 OBJ_rsadsi,2L,10L + +#define LN_hmacWithSHA512 "hmacWithSHA512" +#define NID_hmacWithSHA512 801 +#define OBJ_hmacWithSHA512 OBJ_rsadsi,2L,11L + +#define SN_rc2_cbc "RC2-CBC" +#define LN_rc2_cbc "rc2-cbc" +#define NID_rc2_cbc 37 +#define OBJ_rc2_cbc OBJ_rsadsi,3L,2L + +#define SN_rc2_ecb "RC2-ECB" +#define LN_rc2_ecb "rc2-ecb" +#define NID_rc2_ecb 38 + +#define SN_rc2_cfb64 "RC2-CFB" +#define LN_rc2_cfb64 "rc2-cfb" +#define NID_rc2_cfb64 39 + +#define SN_rc2_ofb64 "RC2-OFB" +#define LN_rc2_ofb64 "rc2-ofb" +#define NID_rc2_ofb64 40 + +#define SN_rc2_40_cbc "RC2-40-CBC" +#define LN_rc2_40_cbc "rc2-40-cbc" +#define NID_rc2_40_cbc 98 + +#define SN_rc2_64_cbc "RC2-64-CBC" +#define LN_rc2_64_cbc "rc2-64-cbc" +#define NID_rc2_64_cbc 166 + +#define SN_rc4 "RC4" +#define LN_rc4 "rc4" +#define NID_rc4 5 +#define OBJ_rc4 OBJ_rsadsi,3L,4L + +#define SN_rc4_40 "RC4-40" +#define LN_rc4_40 "rc4-40" +#define NID_rc4_40 97 + +#define SN_des_ede3_cbc "DES-EDE3-CBC" +#define LN_des_ede3_cbc "des-ede3-cbc" +#define NID_des_ede3_cbc 44 +#define OBJ_des_ede3_cbc OBJ_rsadsi,3L,7L + +#define SN_rc5_cbc "RC5-CBC" +#define LN_rc5_cbc "rc5-cbc" +#define NID_rc5_cbc 120 +#define OBJ_rc5_cbc OBJ_rsadsi,3L,8L + +#define SN_rc5_ecb "RC5-ECB" +#define LN_rc5_ecb "rc5-ecb" +#define NID_rc5_ecb 121 + +#define SN_rc5_cfb64 "RC5-CFB" +#define LN_rc5_cfb64 "rc5-cfb" +#define NID_rc5_cfb64 122 + +#define SN_rc5_ofb64 "RC5-OFB" +#define LN_rc5_ofb64 "rc5-ofb" +#define NID_rc5_ofb64 123 + +#define SN_ms_ext_req "msExtReq" +#define LN_ms_ext_req "Microsoft Extension Request" +#define NID_ms_ext_req 171 +#define OBJ_ms_ext_req 1L,3L,6L,1L,4L,1L,311L,2L,1L,14L + +#define SN_ms_code_ind "msCodeInd" +#define LN_ms_code_ind "Microsoft Individual Code Signing" +#define NID_ms_code_ind 134 +#define OBJ_ms_code_ind 1L,3L,6L,1L,4L,1L,311L,2L,1L,21L + +#define SN_ms_code_com "msCodeCom" +#define LN_ms_code_com "Microsoft Commercial Code Signing" +#define NID_ms_code_com 135 +#define OBJ_ms_code_com 1L,3L,6L,1L,4L,1L,311L,2L,1L,22L + +#define SN_ms_ctl_sign "msCTLSign" +#define LN_ms_ctl_sign "Microsoft Trust List Signing" +#define NID_ms_ctl_sign 136 +#define OBJ_ms_ctl_sign 1L,3L,6L,1L,4L,1L,311L,10L,3L,1L + +#define SN_ms_sgc "msSGC" +#define LN_ms_sgc "Microsoft Server Gated Crypto" +#define NID_ms_sgc 137 +#define OBJ_ms_sgc 1L,3L,6L,1L,4L,1L,311L,10L,3L,3L + +#define SN_ms_efs "msEFS" +#define LN_ms_efs "Microsoft Encrypted File System" +#define NID_ms_efs 138 +#define OBJ_ms_efs 1L,3L,6L,1L,4L,1L,311L,10L,3L,4L + +#define SN_ms_smartcard_login "msSmartcardLogin" +#define LN_ms_smartcard_login "Microsoft Smartcardlogin" +#define NID_ms_smartcard_login 648 +#define OBJ_ms_smartcard_login 1L,3L,6L,1L,4L,1L,311L,20L,2L,2L + +#define SN_ms_upn "msUPN" +#define LN_ms_upn "Microsoft Universal Principal Name" +#define NID_ms_upn 649 +#define OBJ_ms_upn 1L,3L,6L,1L,4L,1L,311L,20L,2L,3L + +#define SN_idea_cbc "IDEA-CBC" +#define LN_idea_cbc "idea-cbc" +#define NID_idea_cbc 34 +#define OBJ_idea_cbc 1L,3L,6L,1L,4L,1L,188L,7L,1L,1L,2L + +#define SN_idea_ecb "IDEA-ECB" +#define LN_idea_ecb "idea-ecb" +#define NID_idea_ecb 36 + +#define SN_idea_cfb64 "IDEA-CFB" +#define LN_idea_cfb64 "idea-cfb" +#define NID_idea_cfb64 35 + +#define SN_idea_ofb64 "IDEA-OFB" +#define LN_idea_ofb64 "idea-ofb" +#define NID_idea_ofb64 46 + +#define SN_bf_cbc "BF-CBC" +#define LN_bf_cbc "bf-cbc" +#define NID_bf_cbc 91 +#define OBJ_bf_cbc 1L,3L,6L,1L,4L,1L,3029L,1L,2L + +#define SN_bf_ecb "BF-ECB" +#define LN_bf_ecb "bf-ecb" +#define NID_bf_ecb 92 + +#define SN_bf_cfb64 "BF-CFB" +#define LN_bf_cfb64 "bf-cfb" +#define NID_bf_cfb64 93 + +#define SN_bf_ofb64 "BF-OFB" +#define LN_bf_ofb64 "bf-ofb" +#define NID_bf_ofb64 94 + +#define SN_id_pkix "PKIX" +#define NID_id_pkix 127 +#define OBJ_id_pkix 1L,3L,6L,1L,5L,5L,7L + +#define SN_id_pkix_mod "id-pkix-mod" +#define NID_id_pkix_mod 258 +#define OBJ_id_pkix_mod OBJ_id_pkix,0L + +#define SN_id_pe "id-pe" +#define NID_id_pe 175 +#define OBJ_id_pe OBJ_id_pkix,1L + +#define SN_id_qt "id-qt" +#define NID_id_qt 259 +#define OBJ_id_qt OBJ_id_pkix,2L + +#define SN_id_kp "id-kp" +#define NID_id_kp 128 +#define OBJ_id_kp OBJ_id_pkix,3L + +#define SN_id_it "id-it" +#define NID_id_it 260 +#define OBJ_id_it OBJ_id_pkix,4L + +#define SN_id_pkip "id-pkip" +#define NID_id_pkip 261 +#define OBJ_id_pkip OBJ_id_pkix,5L + +#define SN_id_alg "id-alg" +#define NID_id_alg 262 +#define OBJ_id_alg OBJ_id_pkix,6L + +#define SN_id_cmc "id-cmc" +#define NID_id_cmc 263 +#define OBJ_id_cmc OBJ_id_pkix,7L + +#define SN_id_on "id-on" +#define NID_id_on 264 +#define OBJ_id_on OBJ_id_pkix,8L + +#define SN_id_pda "id-pda" +#define NID_id_pda 265 +#define OBJ_id_pda OBJ_id_pkix,9L + +#define SN_id_aca "id-aca" +#define NID_id_aca 266 +#define OBJ_id_aca OBJ_id_pkix,10L + +#define SN_id_qcs "id-qcs" +#define NID_id_qcs 267 +#define OBJ_id_qcs OBJ_id_pkix,11L + +#define SN_id_cct "id-cct" +#define NID_id_cct 268 +#define OBJ_id_cct OBJ_id_pkix,12L + +#define SN_id_ppl "id-ppl" +#define NID_id_ppl 662 +#define OBJ_id_ppl OBJ_id_pkix,21L + +#define SN_id_ad "id-ad" +#define NID_id_ad 176 +#define OBJ_id_ad OBJ_id_pkix,48L + +#define SN_id_pkix1_explicit_88 "id-pkix1-explicit-88" +#define NID_id_pkix1_explicit_88 269 +#define OBJ_id_pkix1_explicit_88 OBJ_id_pkix_mod,1L + +#define SN_id_pkix1_implicit_88 "id-pkix1-implicit-88" +#define NID_id_pkix1_implicit_88 270 +#define OBJ_id_pkix1_implicit_88 OBJ_id_pkix_mod,2L + +#define SN_id_pkix1_explicit_93 "id-pkix1-explicit-93" +#define NID_id_pkix1_explicit_93 271 +#define OBJ_id_pkix1_explicit_93 OBJ_id_pkix_mod,3L + +#define SN_id_pkix1_implicit_93 "id-pkix1-implicit-93" +#define NID_id_pkix1_implicit_93 272 +#define OBJ_id_pkix1_implicit_93 OBJ_id_pkix_mod,4L + +#define SN_id_mod_crmf "id-mod-crmf" +#define NID_id_mod_crmf 273 +#define OBJ_id_mod_crmf OBJ_id_pkix_mod,5L + +#define SN_id_mod_cmc "id-mod-cmc" +#define NID_id_mod_cmc 274 +#define OBJ_id_mod_cmc OBJ_id_pkix_mod,6L + +#define SN_id_mod_kea_profile_88 "id-mod-kea-profile-88" +#define NID_id_mod_kea_profile_88 275 +#define OBJ_id_mod_kea_profile_88 OBJ_id_pkix_mod,7L + +#define SN_id_mod_kea_profile_93 "id-mod-kea-profile-93" +#define NID_id_mod_kea_profile_93 276 +#define OBJ_id_mod_kea_profile_93 OBJ_id_pkix_mod,8L + +#define SN_id_mod_cmp "id-mod-cmp" +#define NID_id_mod_cmp 277 +#define OBJ_id_mod_cmp OBJ_id_pkix_mod,9L + +#define SN_id_mod_qualified_cert_88 "id-mod-qualified-cert-88" +#define NID_id_mod_qualified_cert_88 278 +#define OBJ_id_mod_qualified_cert_88 OBJ_id_pkix_mod,10L + +#define SN_id_mod_qualified_cert_93 "id-mod-qualified-cert-93" +#define NID_id_mod_qualified_cert_93 279 +#define OBJ_id_mod_qualified_cert_93 OBJ_id_pkix_mod,11L + +#define SN_id_mod_attribute_cert "id-mod-attribute-cert" +#define NID_id_mod_attribute_cert 280 +#define OBJ_id_mod_attribute_cert OBJ_id_pkix_mod,12L + +#define SN_id_mod_timestamp_protocol "id-mod-timestamp-protocol" +#define NID_id_mod_timestamp_protocol 281 +#define OBJ_id_mod_timestamp_protocol OBJ_id_pkix_mod,13L + +#define SN_id_mod_ocsp "id-mod-ocsp" +#define NID_id_mod_ocsp 282 +#define OBJ_id_mod_ocsp OBJ_id_pkix_mod,14L + +#define SN_id_mod_dvcs "id-mod-dvcs" +#define NID_id_mod_dvcs 283 +#define OBJ_id_mod_dvcs OBJ_id_pkix_mod,15L + +#define SN_id_mod_cmp2000 "id-mod-cmp2000" +#define NID_id_mod_cmp2000 284 +#define OBJ_id_mod_cmp2000 OBJ_id_pkix_mod,16L + +#define SN_info_access "authorityInfoAccess" +#define LN_info_access "Authority Information Access" +#define NID_info_access 177 +#define OBJ_info_access OBJ_id_pe,1L + +#define SN_biometricInfo "biometricInfo" +#define LN_biometricInfo "Biometric Info" +#define NID_biometricInfo 285 +#define OBJ_biometricInfo OBJ_id_pe,2L + +#define SN_qcStatements "qcStatements" +#define NID_qcStatements 286 +#define OBJ_qcStatements OBJ_id_pe,3L + +#define SN_ac_auditEntity "ac-auditEntity" +#define NID_ac_auditEntity 287 +#define OBJ_ac_auditEntity OBJ_id_pe,4L + +#define SN_ac_targeting "ac-targeting" +#define NID_ac_targeting 288 +#define OBJ_ac_targeting OBJ_id_pe,5L + +#define SN_aaControls "aaControls" +#define NID_aaControls 289 +#define OBJ_aaControls OBJ_id_pe,6L + +#define SN_sbgp_ipAddrBlock "sbgp-ipAddrBlock" +#define NID_sbgp_ipAddrBlock 290 +#define OBJ_sbgp_ipAddrBlock OBJ_id_pe,7L + +#define SN_sbgp_autonomousSysNum "sbgp-autonomousSysNum" +#define NID_sbgp_autonomousSysNum 291 +#define OBJ_sbgp_autonomousSysNum OBJ_id_pe,8L + +#define SN_sbgp_routerIdentifier "sbgp-routerIdentifier" +#define NID_sbgp_routerIdentifier 292 +#define OBJ_sbgp_routerIdentifier OBJ_id_pe,9L + +#define SN_ac_proxying "ac-proxying" +#define NID_ac_proxying 397 +#define OBJ_ac_proxying OBJ_id_pe,10L + +#define SN_sinfo_access "subjectInfoAccess" +#define LN_sinfo_access "Subject Information Access" +#define NID_sinfo_access 398 +#define OBJ_sinfo_access OBJ_id_pe,11L + +#define SN_proxyCertInfo "proxyCertInfo" +#define LN_proxyCertInfo "Proxy Certificate Information" +#define NID_proxyCertInfo 663 +#define OBJ_proxyCertInfo OBJ_id_pe,14L + +#define SN_tlsfeature "tlsfeature" +#define LN_tlsfeature "TLS Feature" +#define NID_tlsfeature 1020 +#define OBJ_tlsfeature OBJ_id_pe,24L + +#define SN_id_qt_cps "id-qt-cps" +#define LN_id_qt_cps "Policy Qualifier CPS" +#define NID_id_qt_cps 164 +#define OBJ_id_qt_cps OBJ_id_qt,1L + +#define SN_id_qt_unotice "id-qt-unotice" +#define LN_id_qt_unotice "Policy Qualifier User Notice" +#define NID_id_qt_unotice 165 +#define OBJ_id_qt_unotice OBJ_id_qt,2L + +#define SN_textNotice "textNotice" +#define NID_textNotice 293 +#define OBJ_textNotice OBJ_id_qt,3L + +#define SN_server_auth "serverAuth" +#define LN_server_auth "TLS Web Server Authentication" +#define NID_server_auth 129 +#define OBJ_server_auth OBJ_id_kp,1L + +#define SN_client_auth "clientAuth" +#define LN_client_auth "TLS Web Client Authentication" +#define NID_client_auth 130 +#define OBJ_client_auth OBJ_id_kp,2L + +#define SN_code_sign "codeSigning" +#define LN_code_sign "Code Signing" +#define NID_code_sign 131 +#define OBJ_code_sign OBJ_id_kp,3L + +#define SN_email_protect "emailProtection" +#define LN_email_protect "E-mail Protection" +#define NID_email_protect 132 +#define OBJ_email_protect OBJ_id_kp,4L + +#define SN_ipsecEndSystem "ipsecEndSystem" +#define LN_ipsecEndSystem "IPSec End System" +#define NID_ipsecEndSystem 294 +#define OBJ_ipsecEndSystem OBJ_id_kp,5L + +#define SN_ipsecTunnel "ipsecTunnel" +#define LN_ipsecTunnel "IPSec Tunnel" +#define NID_ipsecTunnel 295 +#define OBJ_ipsecTunnel OBJ_id_kp,6L + +#define SN_ipsecUser "ipsecUser" +#define LN_ipsecUser "IPSec User" +#define NID_ipsecUser 296 +#define OBJ_ipsecUser OBJ_id_kp,7L + +#define SN_time_stamp "timeStamping" +#define LN_time_stamp "Time Stamping" +#define NID_time_stamp 133 +#define OBJ_time_stamp OBJ_id_kp,8L + +#define SN_OCSP_sign "OCSPSigning" +#define LN_OCSP_sign "OCSP Signing" +#define NID_OCSP_sign 180 +#define OBJ_OCSP_sign OBJ_id_kp,9L + +#define SN_dvcs "DVCS" +#define LN_dvcs "dvcs" +#define NID_dvcs 297 +#define OBJ_dvcs OBJ_id_kp,10L + +#define SN_ipsec_IKE "ipsecIKE" +#define LN_ipsec_IKE "ipsec Internet Key Exchange" +#define NID_ipsec_IKE 1022 +#define OBJ_ipsec_IKE OBJ_id_kp,17L + +#define SN_capwapAC "capwapAC" +#define LN_capwapAC "Ctrl/provision WAP Access" +#define NID_capwapAC 1023 +#define OBJ_capwapAC OBJ_id_kp,18L + +#define SN_capwapWTP "capwapWTP" +#define LN_capwapWTP "Ctrl/Provision WAP Termination" +#define NID_capwapWTP 1024 +#define OBJ_capwapWTP OBJ_id_kp,19L + +#define SN_sshClient "secureShellClient" +#define LN_sshClient "SSH Client" +#define NID_sshClient 1025 +#define OBJ_sshClient OBJ_id_kp,21L + +#define SN_sshServer "secureShellServer" +#define LN_sshServer "SSH Server" +#define NID_sshServer 1026 +#define OBJ_sshServer OBJ_id_kp,22L + +#define SN_sendRouter "sendRouter" +#define LN_sendRouter "Send Router" +#define NID_sendRouter 1027 +#define OBJ_sendRouter OBJ_id_kp,23L + +#define SN_sendProxiedRouter "sendProxiedRouter" +#define LN_sendProxiedRouter "Send Proxied Router" +#define NID_sendProxiedRouter 1028 +#define OBJ_sendProxiedRouter OBJ_id_kp,24L + +#define SN_sendOwner "sendOwner" +#define LN_sendOwner "Send Owner" +#define NID_sendOwner 1029 +#define OBJ_sendOwner OBJ_id_kp,25L + +#define SN_sendProxiedOwner "sendProxiedOwner" +#define LN_sendProxiedOwner "Send Proxied Owner" +#define NID_sendProxiedOwner 1030 +#define OBJ_sendProxiedOwner OBJ_id_kp,26L + +#define SN_id_it_caProtEncCert "id-it-caProtEncCert" +#define NID_id_it_caProtEncCert 298 +#define OBJ_id_it_caProtEncCert OBJ_id_it,1L + +#define SN_id_it_signKeyPairTypes "id-it-signKeyPairTypes" +#define NID_id_it_signKeyPairTypes 299 +#define OBJ_id_it_signKeyPairTypes OBJ_id_it,2L + +#define SN_id_it_encKeyPairTypes "id-it-encKeyPairTypes" +#define NID_id_it_encKeyPairTypes 300 +#define OBJ_id_it_encKeyPairTypes OBJ_id_it,3L + +#define SN_id_it_preferredSymmAlg "id-it-preferredSymmAlg" +#define NID_id_it_preferredSymmAlg 301 +#define OBJ_id_it_preferredSymmAlg OBJ_id_it,4L + +#define SN_id_it_caKeyUpdateInfo "id-it-caKeyUpdateInfo" +#define NID_id_it_caKeyUpdateInfo 302 +#define OBJ_id_it_caKeyUpdateInfo OBJ_id_it,5L + +#define SN_id_it_currentCRL "id-it-currentCRL" +#define NID_id_it_currentCRL 303 +#define OBJ_id_it_currentCRL OBJ_id_it,6L + +#define SN_id_it_unsupportedOIDs "id-it-unsupportedOIDs" +#define NID_id_it_unsupportedOIDs 304 +#define OBJ_id_it_unsupportedOIDs OBJ_id_it,7L + +#define SN_id_it_subscriptionRequest "id-it-subscriptionRequest" +#define NID_id_it_subscriptionRequest 305 +#define OBJ_id_it_subscriptionRequest OBJ_id_it,8L + +#define SN_id_it_subscriptionResponse "id-it-subscriptionResponse" +#define NID_id_it_subscriptionResponse 306 +#define OBJ_id_it_subscriptionResponse OBJ_id_it,9L + +#define SN_id_it_keyPairParamReq "id-it-keyPairParamReq" +#define NID_id_it_keyPairParamReq 307 +#define OBJ_id_it_keyPairParamReq OBJ_id_it,10L + +#define SN_id_it_keyPairParamRep "id-it-keyPairParamRep" +#define NID_id_it_keyPairParamRep 308 +#define OBJ_id_it_keyPairParamRep OBJ_id_it,11L + +#define SN_id_it_revPassphrase "id-it-revPassphrase" +#define NID_id_it_revPassphrase 309 +#define OBJ_id_it_revPassphrase OBJ_id_it,12L + +#define SN_id_it_implicitConfirm "id-it-implicitConfirm" +#define NID_id_it_implicitConfirm 310 +#define OBJ_id_it_implicitConfirm OBJ_id_it,13L + +#define SN_id_it_confirmWaitTime "id-it-confirmWaitTime" +#define NID_id_it_confirmWaitTime 311 +#define OBJ_id_it_confirmWaitTime OBJ_id_it,14L + +#define SN_id_it_origPKIMessage "id-it-origPKIMessage" +#define NID_id_it_origPKIMessage 312 +#define OBJ_id_it_origPKIMessage OBJ_id_it,15L + +#define SN_id_it_suppLangTags "id-it-suppLangTags" +#define NID_id_it_suppLangTags 784 +#define OBJ_id_it_suppLangTags OBJ_id_it,16L + +#define SN_id_regCtrl "id-regCtrl" +#define NID_id_regCtrl 313 +#define OBJ_id_regCtrl OBJ_id_pkip,1L + +#define SN_id_regInfo "id-regInfo" +#define NID_id_regInfo 314 +#define OBJ_id_regInfo OBJ_id_pkip,2L + +#define SN_id_regCtrl_regToken "id-regCtrl-regToken" +#define NID_id_regCtrl_regToken 315 +#define OBJ_id_regCtrl_regToken OBJ_id_regCtrl,1L + +#define SN_id_regCtrl_authenticator "id-regCtrl-authenticator" +#define NID_id_regCtrl_authenticator 316 +#define OBJ_id_regCtrl_authenticator OBJ_id_regCtrl,2L + +#define SN_id_regCtrl_pkiPublicationInfo "id-regCtrl-pkiPublicationInfo" +#define NID_id_regCtrl_pkiPublicationInfo 317 +#define OBJ_id_regCtrl_pkiPublicationInfo OBJ_id_regCtrl,3L + +#define SN_id_regCtrl_pkiArchiveOptions "id-regCtrl-pkiArchiveOptions" +#define NID_id_regCtrl_pkiArchiveOptions 318 +#define OBJ_id_regCtrl_pkiArchiveOptions OBJ_id_regCtrl,4L + +#define SN_id_regCtrl_oldCertID "id-regCtrl-oldCertID" +#define NID_id_regCtrl_oldCertID 319 +#define OBJ_id_regCtrl_oldCertID OBJ_id_regCtrl,5L + +#define SN_id_regCtrl_protocolEncrKey "id-regCtrl-protocolEncrKey" +#define NID_id_regCtrl_protocolEncrKey 320 +#define OBJ_id_regCtrl_protocolEncrKey OBJ_id_regCtrl,6L + +#define SN_id_regInfo_utf8Pairs "id-regInfo-utf8Pairs" +#define NID_id_regInfo_utf8Pairs 321 +#define OBJ_id_regInfo_utf8Pairs OBJ_id_regInfo,1L + +#define SN_id_regInfo_certReq "id-regInfo-certReq" +#define NID_id_regInfo_certReq 322 +#define OBJ_id_regInfo_certReq OBJ_id_regInfo,2L + +#define SN_id_alg_des40 "id-alg-des40" +#define NID_id_alg_des40 323 +#define OBJ_id_alg_des40 OBJ_id_alg,1L + +#define SN_id_alg_noSignature "id-alg-noSignature" +#define NID_id_alg_noSignature 324 +#define OBJ_id_alg_noSignature OBJ_id_alg,2L + +#define SN_id_alg_dh_sig_hmac_sha1 "id-alg-dh-sig-hmac-sha1" +#define NID_id_alg_dh_sig_hmac_sha1 325 +#define OBJ_id_alg_dh_sig_hmac_sha1 OBJ_id_alg,3L + +#define SN_id_alg_dh_pop "id-alg-dh-pop" +#define NID_id_alg_dh_pop 326 +#define OBJ_id_alg_dh_pop OBJ_id_alg,4L + +#define SN_id_cmc_statusInfo "id-cmc-statusInfo" +#define NID_id_cmc_statusInfo 327 +#define OBJ_id_cmc_statusInfo OBJ_id_cmc,1L + +#define SN_id_cmc_identification "id-cmc-identification" +#define NID_id_cmc_identification 328 +#define OBJ_id_cmc_identification OBJ_id_cmc,2L + +#define SN_id_cmc_identityProof "id-cmc-identityProof" +#define NID_id_cmc_identityProof 329 +#define OBJ_id_cmc_identityProof OBJ_id_cmc,3L + +#define SN_id_cmc_dataReturn "id-cmc-dataReturn" +#define NID_id_cmc_dataReturn 330 +#define OBJ_id_cmc_dataReturn OBJ_id_cmc,4L + +#define SN_id_cmc_transactionId "id-cmc-transactionId" +#define NID_id_cmc_transactionId 331 +#define OBJ_id_cmc_transactionId OBJ_id_cmc,5L + +#define SN_id_cmc_senderNonce "id-cmc-senderNonce" +#define NID_id_cmc_senderNonce 332 +#define OBJ_id_cmc_senderNonce OBJ_id_cmc,6L + +#define SN_id_cmc_recipientNonce "id-cmc-recipientNonce" +#define NID_id_cmc_recipientNonce 333 +#define OBJ_id_cmc_recipientNonce OBJ_id_cmc,7L + +#define SN_id_cmc_addExtensions "id-cmc-addExtensions" +#define NID_id_cmc_addExtensions 334 +#define OBJ_id_cmc_addExtensions OBJ_id_cmc,8L + +#define SN_id_cmc_encryptedPOP "id-cmc-encryptedPOP" +#define NID_id_cmc_encryptedPOP 335 +#define OBJ_id_cmc_encryptedPOP OBJ_id_cmc,9L + +#define SN_id_cmc_decryptedPOP "id-cmc-decryptedPOP" +#define NID_id_cmc_decryptedPOP 336 +#define OBJ_id_cmc_decryptedPOP OBJ_id_cmc,10L + +#define SN_id_cmc_lraPOPWitness "id-cmc-lraPOPWitness" +#define NID_id_cmc_lraPOPWitness 337 +#define OBJ_id_cmc_lraPOPWitness OBJ_id_cmc,11L + +#define SN_id_cmc_getCert "id-cmc-getCert" +#define NID_id_cmc_getCert 338 +#define OBJ_id_cmc_getCert OBJ_id_cmc,15L + +#define SN_id_cmc_getCRL "id-cmc-getCRL" +#define NID_id_cmc_getCRL 339 +#define OBJ_id_cmc_getCRL OBJ_id_cmc,16L + +#define SN_id_cmc_revokeRequest "id-cmc-revokeRequest" +#define NID_id_cmc_revokeRequest 340 +#define OBJ_id_cmc_revokeRequest OBJ_id_cmc,17L + +#define SN_id_cmc_regInfo "id-cmc-regInfo" +#define NID_id_cmc_regInfo 341 +#define OBJ_id_cmc_regInfo OBJ_id_cmc,18L + +#define SN_id_cmc_responseInfo "id-cmc-responseInfo" +#define NID_id_cmc_responseInfo 342 +#define OBJ_id_cmc_responseInfo OBJ_id_cmc,19L + +#define SN_id_cmc_queryPending "id-cmc-queryPending" +#define NID_id_cmc_queryPending 343 +#define OBJ_id_cmc_queryPending OBJ_id_cmc,21L + +#define SN_id_cmc_popLinkRandom "id-cmc-popLinkRandom" +#define NID_id_cmc_popLinkRandom 344 +#define OBJ_id_cmc_popLinkRandom OBJ_id_cmc,22L + +#define SN_id_cmc_popLinkWitness "id-cmc-popLinkWitness" +#define NID_id_cmc_popLinkWitness 345 +#define OBJ_id_cmc_popLinkWitness OBJ_id_cmc,23L + +#define SN_id_cmc_confirmCertAcceptance "id-cmc-confirmCertAcceptance" +#define NID_id_cmc_confirmCertAcceptance 346 +#define OBJ_id_cmc_confirmCertAcceptance OBJ_id_cmc,24L + +#define SN_id_on_personalData "id-on-personalData" +#define NID_id_on_personalData 347 +#define OBJ_id_on_personalData OBJ_id_on,1L + +#define SN_id_on_permanentIdentifier "id-on-permanentIdentifier" +#define LN_id_on_permanentIdentifier "Permanent Identifier" +#define NID_id_on_permanentIdentifier 858 +#define OBJ_id_on_permanentIdentifier OBJ_id_on,3L + +#define SN_id_pda_dateOfBirth "id-pda-dateOfBirth" +#define NID_id_pda_dateOfBirth 348 +#define OBJ_id_pda_dateOfBirth OBJ_id_pda,1L + +#define SN_id_pda_placeOfBirth "id-pda-placeOfBirth" +#define NID_id_pda_placeOfBirth 349 +#define OBJ_id_pda_placeOfBirth OBJ_id_pda,2L + +#define SN_id_pda_gender "id-pda-gender" +#define NID_id_pda_gender 351 +#define OBJ_id_pda_gender OBJ_id_pda,3L + +#define SN_id_pda_countryOfCitizenship "id-pda-countryOfCitizenship" +#define NID_id_pda_countryOfCitizenship 352 +#define OBJ_id_pda_countryOfCitizenship OBJ_id_pda,4L + +#define SN_id_pda_countryOfResidence "id-pda-countryOfResidence" +#define NID_id_pda_countryOfResidence 353 +#define OBJ_id_pda_countryOfResidence OBJ_id_pda,5L + +#define SN_id_aca_authenticationInfo "id-aca-authenticationInfo" +#define NID_id_aca_authenticationInfo 354 +#define OBJ_id_aca_authenticationInfo OBJ_id_aca,1L + +#define SN_id_aca_accessIdentity "id-aca-accessIdentity" +#define NID_id_aca_accessIdentity 355 +#define OBJ_id_aca_accessIdentity OBJ_id_aca,2L + +#define SN_id_aca_chargingIdentity "id-aca-chargingIdentity" +#define NID_id_aca_chargingIdentity 356 +#define OBJ_id_aca_chargingIdentity OBJ_id_aca,3L + +#define SN_id_aca_group "id-aca-group" +#define NID_id_aca_group 357 +#define OBJ_id_aca_group OBJ_id_aca,4L + +#define SN_id_aca_role "id-aca-role" +#define NID_id_aca_role 358 +#define OBJ_id_aca_role OBJ_id_aca,5L + +#define SN_id_aca_encAttrs "id-aca-encAttrs" +#define NID_id_aca_encAttrs 399 +#define OBJ_id_aca_encAttrs OBJ_id_aca,6L + +#define SN_id_qcs_pkixQCSyntax_v1 "id-qcs-pkixQCSyntax-v1" +#define NID_id_qcs_pkixQCSyntax_v1 359 +#define OBJ_id_qcs_pkixQCSyntax_v1 OBJ_id_qcs,1L + +#define SN_id_cct_crs "id-cct-crs" +#define NID_id_cct_crs 360 +#define OBJ_id_cct_crs OBJ_id_cct,1L + +#define SN_id_cct_PKIData "id-cct-PKIData" +#define NID_id_cct_PKIData 361 +#define OBJ_id_cct_PKIData OBJ_id_cct,2L + +#define SN_id_cct_PKIResponse "id-cct-PKIResponse" +#define NID_id_cct_PKIResponse 362 +#define OBJ_id_cct_PKIResponse OBJ_id_cct,3L + +#define SN_id_ppl_anyLanguage "id-ppl-anyLanguage" +#define LN_id_ppl_anyLanguage "Any language" +#define NID_id_ppl_anyLanguage 664 +#define OBJ_id_ppl_anyLanguage OBJ_id_ppl,0L + +#define SN_id_ppl_inheritAll "id-ppl-inheritAll" +#define LN_id_ppl_inheritAll "Inherit all" +#define NID_id_ppl_inheritAll 665 +#define OBJ_id_ppl_inheritAll OBJ_id_ppl,1L + +#define SN_Independent "id-ppl-independent" +#define LN_Independent "Independent" +#define NID_Independent 667 +#define OBJ_Independent OBJ_id_ppl,2L + +#define SN_ad_OCSP "OCSP" +#define LN_ad_OCSP "OCSP" +#define NID_ad_OCSP 178 +#define OBJ_ad_OCSP OBJ_id_ad,1L + +#define SN_ad_ca_issuers "caIssuers" +#define LN_ad_ca_issuers "CA Issuers" +#define NID_ad_ca_issuers 179 +#define OBJ_ad_ca_issuers OBJ_id_ad,2L + +#define SN_ad_timeStamping "ad_timestamping" +#define LN_ad_timeStamping "AD Time Stamping" +#define NID_ad_timeStamping 363 +#define OBJ_ad_timeStamping OBJ_id_ad,3L + +#define SN_ad_dvcs "AD_DVCS" +#define LN_ad_dvcs "ad dvcs" +#define NID_ad_dvcs 364 +#define OBJ_ad_dvcs OBJ_id_ad,4L + +#define SN_caRepository "caRepository" +#define LN_caRepository "CA Repository" +#define NID_caRepository 785 +#define OBJ_caRepository OBJ_id_ad,5L + +#define OBJ_id_pkix_OCSP OBJ_ad_OCSP + +#define SN_id_pkix_OCSP_basic "basicOCSPResponse" +#define LN_id_pkix_OCSP_basic "Basic OCSP Response" +#define NID_id_pkix_OCSP_basic 365 +#define OBJ_id_pkix_OCSP_basic OBJ_id_pkix_OCSP,1L + +#define SN_id_pkix_OCSP_Nonce "Nonce" +#define LN_id_pkix_OCSP_Nonce "OCSP Nonce" +#define NID_id_pkix_OCSP_Nonce 366 +#define OBJ_id_pkix_OCSP_Nonce OBJ_id_pkix_OCSP,2L + +#define SN_id_pkix_OCSP_CrlID "CrlID" +#define LN_id_pkix_OCSP_CrlID "OCSP CRL ID" +#define NID_id_pkix_OCSP_CrlID 367 +#define OBJ_id_pkix_OCSP_CrlID OBJ_id_pkix_OCSP,3L + +#define SN_id_pkix_OCSP_acceptableResponses "acceptableResponses" +#define LN_id_pkix_OCSP_acceptableResponses "Acceptable OCSP Responses" +#define NID_id_pkix_OCSP_acceptableResponses 368 +#define OBJ_id_pkix_OCSP_acceptableResponses OBJ_id_pkix_OCSP,4L + +#define SN_id_pkix_OCSP_noCheck "noCheck" +#define LN_id_pkix_OCSP_noCheck "OCSP No Check" +#define NID_id_pkix_OCSP_noCheck 369 +#define OBJ_id_pkix_OCSP_noCheck OBJ_id_pkix_OCSP,5L + +#define SN_id_pkix_OCSP_archiveCutoff "archiveCutoff" +#define LN_id_pkix_OCSP_archiveCutoff "OCSP Archive Cutoff" +#define NID_id_pkix_OCSP_archiveCutoff 370 +#define OBJ_id_pkix_OCSP_archiveCutoff OBJ_id_pkix_OCSP,6L + +#define SN_id_pkix_OCSP_serviceLocator "serviceLocator" +#define LN_id_pkix_OCSP_serviceLocator "OCSP Service Locator" +#define NID_id_pkix_OCSP_serviceLocator 371 +#define OBJ_id_pkix_OCSP_serviceLocator OBJ_id_pkix_OCSP,7L + +#define SN_id_pkix_OCSP_extendedStatus "extendedStatus" +#define LN_id_pkix_OCSP_extendedStatus "Extended OCSP Status" +#define NID_id_pkix_OCSP_extendedStatus 372 +#define OBJ_id_pkix_OCSP_extendedStatus OBJ_id_pkix_OCSP,8L + +#define SN_id_pkix_OCSP_valid "valid" +#define NID_id_pkix_OCSP_valid 373 +#define OBJ_id_pkix_OCSP_valid OBJ_id_pkix_OCSP,9L + +#define SN_id_pkix_OCSP_path "path" +#define NID_id_pkix_OCSP_path 374 +#define OBJ_id_pkix_OCSP_path OBJ_id_pkix_OCSP,10L + +#define SN_id_pkix_OCSP_trustRoot "trustRoot" +#define LN_id_pkix_OCSP_trustRoot "Trust Root" +#define NID_id_pkix_OCSP_trustRoot 375 +#define OBJ_id_pkix_OCSP_trustRoot OBJ_id_pkix_OCSP,11L + +#define SN_algorithm "algorithm" +#define LN_algorithm "algorithm" +#define NID_algorithm 376 +#define OBJ_algorithm 1L,3L,14L,3L,2L + +#define SN_md5WithRSA "RSA-NP-MD5" +#define LN_md5WithRSA "md5WithRSA" +#define NID_md5WithRSA 104 +#define OBJ_md5WithRSA OBJ_algorithm,3L + +#define SN_des_ecb "DES-ECB" +#define LN_des_ecb "des-ecb" +#define NID_des_ecb 29 +#define OBJ_des_ecb OBJ_algorithm,6L + +#define SN_des_cbc "DES-CBC" +#define LN_des_cbc "des-cbc" +#define NID_des_cbc 31 +#define OBJ_des_cbc OBJ_algorithm,7L + +#define SN_des_ofb64 "DES-OFB" +#define LN_des_ofb64 "des-ofb" +#define NID_des_ofb64 45 +#define OBJ_des_ofb64 OBJ_algorithm,8L + +#define SN_des_cfb64 "DES-CFB" +#define LN_des_cfb64 "des-cfb" +#define NID_des_cfb64 30 +#define OBJ_des_cfb64 OBJ_algorithm,9L + +#define SN_rsaSignature "rsaSignature" +#define NID_rsaSignature 377 +#define OBJ_rsaSignature OBJ_algorithm,11L + +#define SN_dsa_2 "DSA-old" +#define LN_dsa_2 "dsaEncryption-old" +#define NID_dsa_2 67 +#define OBJ_dsa_2 OBJ_algorithm,12L + +#define SN_dsaWithSHA "DSA-SHA" +#define LN_dsaWithSHA "dsaWithSHA" +#define NID_dsaWithSHA 66 +#define OBJ_dsaWithSHA OBJ_algorithm,13L + +#define SN_shaWithRSAEncryption "RSA-SHA" +#define LN_shaWithRSAEncryption "shaWithRSAEncryption" +#define NID_shaWithRSAEncryption 42 +#define OBJ_shaWithRSAEncryption OBJ_algorithm,15L + +#define SN_des_ede_ecb "DES-EDE" +#define LN_des_ede_ecb "des-ede" +#define NID_des_ede_ecb 32 +#define OBJ_des_ede_ecb OBJ_algorithm,17L + +#define SN_des_ede3_ecb "DES-EDE3" +#define LN_des_ede3_ecb "des-ede3" +#define NID_des_ede3_ecb 33 + +#define SN_des_ede_cbc "DES-EDE-CBC" +#define LN_des_ede_cbc "des-ede-cbc" +#define NID_des_ede_cbc 43 + +#define SN_des_ede_cfb64 "DES-EDE-CFB" +#define LN_des_ede_cfb64 "des-ede-cfb" +#define NID_des_ede_cfb64 60 + +#define SN_des_ede3_cfb64 "DES-EDE3-CFB" +#define LN_des_ede3_cfb64 "des-ede3-cfb" +#define NID_des_ede3_cfb64 61 + +#define SN_des_ede_ofb64 "DES-EDE-OFB" +#define LN_des_ede_ofb64 "des-ede-ofb" +#define NID_des_ede_ofb64 62 + +#define SN_des_ede3_ofb64 "DES-EDE3-OFB" +#define LN_des_ede3_ofb64 "des-ede3-ofb" +#define NID_des_ede3_ofb64 63 + +#define SN_desx_cbc "DESX-CBC" +#define LN_desx_cbc "desx-cbc" +#define NID_desx_cbc 80 + +#define SN_sha "SHA" +#define LN_sha "sha" +#define NID_sha 41 +#define OBJ_sha OBJ_algorithm,18L + +#define SN_sha1 "SHA1" +#define LN_sha1 "sha1" +#define NID_sha1 64 +#define OBJ_sha1 OBJ_algorithm,26L + +#define SN_dsaWithSHA1_2 "DSA-SHA1-old" +#define LN_dsaWithSHA1_2 "dsaWithSHA1-old" +#define NID_dsaWithSHA1_2 70 +#define OBJ_dsaWithSHA1_2 OBJ_algorithm,27L + +#define SN_sha1WithRSA "RSA-SHA1-2" +#define LN_sha1WithRSA "sha1WithRSA" +#define NID_sha1WithRSA 115 +#define OBJ_sha1WithRSA OBJ_algorithm,29L + +#define SN_ripemd160 "RIPEMD160" +#define LN_ripemd160 "ripemd160" +#define NID_ripemd160 117 +#define OBJ_ripemd160 1L,3L,36L,3L,2L,1L + +#define SN_ripemd160WithRSA "RSA-RIPEMD160" +#define LN_ripemd160WithRSA "ripemd160WithRSA" +#define NID_ripemd160WithRSA 119 +#define OBJ_ripemd160WithRSA 1L,3L,36L,3L,3L,1L,2L + +#define SN_blake2b512 "BLAKE2b512" +#define LN_blake2b512 "blake2b512" +#define NID_blake2b512 1056 +#define OBJ_blake2b512 1L,3L,6L,1L,4L,1L,1722L,12L,2L,1L,16L + +#define SN_blake2s256 "BLAKE2s256" +#define LN_blake2s256 "blake2s256" +#define NID_blake2s256 1057 +#define OBJ_blake2s256 1L,3L,6L,1L,4L,1L,1722L,12L,2L,2L,8L + +#define SN_sxnet "SXNetID" +#define LN_sxnet "Strong Extranet ID" +#define NID_sxnet 143 +#define OBJ_sxnet 1L,3L,101L,1L,4L,1L + +#define SN_X500 "X500" +#define LN_X500 "directory services (X.500)" +#define NID_X500 11 +#define OBJ_X500 2L,5L + +#define SN_X509 "X509" +#define NID_X509 12 +#define OBJ_X509 OBJ_X500,4L + +#define SN_commonName "CN" +#define LN_commonName "commonName" +#define NID_commonName 13 +#define OBJ_commonName OBJ_X509,3L + +#define SN_surname "SN" +#define LN_surname "surname" +#define NID_surname 100 +#define OBJ_surname OBJ_X509,4L + +#define LN_serialNumber "serialNumber" +#define NID_serialNumber 105 +#define OBJ_serialNumber OBJ_X509,5L + +#define SN_countryName "C" +#define LN_countryName "countryName" +#define NID_countryName 14 +#define OBJ_countryName OBJ_X509,6L + +#define SN_localityName "L" +#define LN_localityName "localityName" +#define NID_localityName 15 +#define OBJ_localityName OBJ_X509,7L + +#define SN_stateOrProvinceName "ST" +#define LN_stateOrProvinceName "stateOrProvinceName" +#define NID_stateOrProvinceName 16 +#define OBJ_stateOrProvinceName OBJ_X509,8L + +#define SN_streetAddress "street" +#define LN_streetAddress "streetAddress" +#define NID_streetAddress 660 +#define OBJ_streetAddress OBJ_X509,9L + +#define SN_organizationName "O" +#define LN_organizationName "organizationName" +#define NID_organizationName 17 +#define OBJ_organizationName OBJ_X509,10L + +#define SN_organizationalUnitName "OU" +#define LN_organizationalUnitName "organizationalUnitName" +#define NID_organizationalUnitName 18 +#define OBJ_organizationalUnitName OBJ_X509,11L + +#define SN_title "title" +#define LN_title "title" +#define NID_title 106 +#define OBJ_title OBJ_X509,12L + +#define LN_description "description" +#define NID_description 107 +#define OBJ_description OBJ_X509,13L + +#define LN_searchGuide "searchGuide" +#define NID_searchGuide 859 +#define OBJ_searchGuide OBJ_X509,14L + +#define LN_businessCategory "businessCategory" +#define NID_businessCategory 860 +#define OBJ_businessCategory OBJ_X509,15L + +#define LN_postalAddress "postalAddress" +#define NID_postalAddress 861 +#define OBJ_postalAddress OBJ_X509,16L + +#define LN_postalCode "postalCode" +#define NID_postalCode 661 +#define OBJ_postalCode OBJ_X509,17L + +#define LN_postOfficeBox "postOfficeBox" +#define NID_postOfficeBox 862 +#define OBJ_postOfficeBox OBJ_X509,18L + +#define LN_physicalDeliveryOfficeName "physicalDeliveryOfficeName" +#define NID_physicalDeliveryOfficeName 863 +#define OBJ_physicalDeliveryOfficeName OBJ_X509,19L + +#define LN_telephoneNumber "telephoneNumber" +#define NID_telephoneNumber 864 +#define OBJ_telephoneNumber OBJ_X509,20L + +#define LN_telexNumber "telexNumber" +#define NID_telexNumber 865 +#define OBJ_telexNumber OBJ_X509,21L + +#define LN_teletexTerminalIdentifier "teletexTerminalIdentifier" +#define NID_teletexTerminalIdentifier 866 +#define OBJ_teletexTerminalIdentifier OBJ_X509,22L + +#define LN_facsimileTelephoneNumber "facsimileTelephoneNumber" +#define NID_facsimileTelephoneNumber 867 +#define OBJ_facsimileTelephoneNumber OBJ_X509,23L + +#define LN_x121Address "x121Address" +#define NID_x121Address 868 +#define OBJ_x121Address OBJ_X509,24L + +#define LN_internationaliSDNNumber "internationaliSDNNumber" +#define NID_internationaliSDNNumber 869 +#define OBJ_internationaliSDNNumber OBJ_X509,25L + +#define LN_registeredAddress "registeredAddress" +#define NID_registeredAddress 870 +#define OBJ_registeredAddress OBJ_X509,26L + +#define LN_destinationIndicator "destinationIndicator" +#define NID_destinationIndicator 871 +#define OBJ_destinationIndicator OBJ_X509,27L + +#define LN_preferredDeliveryMethod "preferredDeliveryMethod" +#define NID_preferredDeliveryMethod 872 +#define OBJ_preferredDeliveryMethod OBJ_X509,28L + +#define LN_presentationAddress "presentationAddress" +#define NID_presentationAddress 873 +#define OBJ_presentationAddress OBJ_X509,29L + +#define LN_supportedApplicationContext "supportedApplicationContext" +#define NID_supportedApplicationContext 874 +#define OBJ_supportedApplicationContext OBJ_X509,30L + +#define SN_member "member" +#define NID_member 875 +#define OBJ_member OBJ_X509,31L + +#define SN_owner "owner" +#define NID_owner 876 +#define OBJ_owner OBJ_X509,32L + +#define LN_roleOccupant "roleOccupant" +#define NID_roleOccupant 877 +#define OBJ_roleOccupant OBJ_X509,33L + +#define SN_seeAlso "seeAlso" +#define NID_seeAlso 878 +#define OBJ_seeAlso OBJ_X509,34L + +#define LN_userPassword "userPassword" +#define NID_userPassword 879 +#define OBJ_userPassword OBJ_X509,35L + +#define LN_userCertificate "userCertificate" +#define NID_userCertificate 880 +#define OBJ_userCertificate OBJ_X509,36L + +#define LN_cACertificate "cACertificate" +#define NID_cACertificate 881 +#define OBJ_cACertificate OBJ_X509,37L + +#define LN_authorityRevocationList "authorityRevocationList" +#define NID_authorityRevocationList 882 +#define OBJ_authorityRevocationList OBJ_X509,38L + +#define LN_certificateRevocationList "certificateRevocationList" +#define NID_certificateRevocationList 883 +#define OBJ_certificateRevocationList OBJ_X509,39L + +#define LN_crossCertificatePair "crossCertificatePair" +#define NID_crossCertificatePair 884 +#define OBJ_crossCertificatePair OBJ_X509,40L + +#define SN_name "name" +#define LN_name "name" +#define NID_name 173 +#define OBJ_name OBJ_X509,41L + +#define SN_givenName "GN" +#define LN_givenName "givenName" +#define NID_givenName 99 +#define OBJ_givenName OBJ_X509,42L + +#define SN_initials "initials" +#define LN_initials "initials" +#define NID_initials 101 +#define OBJ_initials OBJ_X509,43L + +#define LN_generationQualifier "generationQualifier" +#define NID_generationQualifier 509 +#define OBJ_generationQualifier OBJ_X509,44L + +#define LN_x500UniqueIdentifier "x500UniqueIdentifier" +#define NID_x500UniqueIdentifier 503 +#define OBJ_x500UniqueIdentifier OBJ_X509,45L + +#define SN_dnQualifier "dnQualifier" +#define LN_dnQualifier "dnQualifier" +#define NID_dnQualifier 174 +#define OBJ_dnQualifier OBJ_X509,46L + +#define LN_enhancedSearchGuide "enhancedSearchGuide" +#define NID_enhancedSearchGuide 885 +#define OBJ_enhancedSearchGuide OBJ_X509,47L + +#define LN_protocolInformation "protocolInformation" +#define NID_protocolInformation 886 +#define OBJ_protocolInformation OBJ_X509,48L + +#define LN_distinguishedName "distinguishedName" +#define NID_distinguishedName 887 +#define OBJ_distinguishedName OBJ_X509,49L + +#define LN_uniqueMember "uniqueMember" +#define NID_uniqueMember 888 +#define OBJ_uniqueMember OBJ_X509,50L + +#define LN_houseIdentifier "houseIdentifier" +#define NID_houseIdentifier 889 +#define OBJ_houseIdentifier OBJ_X509,51L + +#define LN_supportedAlgorithms "supportedAlgorithms" +#define NID_supportedAlgorithms 890 +#define OBJ_supportedAlgorithms OBJ_X509,52L + +#define LN_deltaRevocationList "deltaRevocationList" +#define NID_deltaRevocationList 891 +#define OBJ_deltaRevocationList OBJ_X509,53L + +#define SN_dmdName "dmdName" +#define NID_dmdName 892 +#define OBJ_dmdName OBJ_X509,54L + +#define LN_pseudonym "pseudonym" +#define NID_pseudonym 510 +#define OBJ_pseudonym OBJ_X509,65L + +#define SN_role "role" +#define LN_role "role" +#define NID_role 400 +#define OBJ_role OBJ_X509,72L + +#define SN_X500algorithms "X500algorithms" +#define LN_X500algorithms "directory services - algorithms" +#define NID_X500algorithms 378 +#define OBJ_X500algorithms OBJ_X500,8L + +#define SN_rsa "RSA" +#define LN_rsa "rsa" +#define NID_rsa 19 +#define OBJ_rsa OBJ_X500algorithms,1L,1L + +#define SN_mdc2WithRSA "RSA-MDC2" +#define LN_mdc2WithRSA "mdc2WithRSA" +#define NID_mdc2WithRSA 96 +#define OBJ_mdc2WithRSA OBJ_X500algorithms,3L,100L + +#define SN_mdc2 "MDC2" +#define LN_mdc2 "mdc2" +#define NID_mdc2 95 +#define OBJ_mdc2 OBJ_X500algorithms,3L,101L + +#define SN_id_ce "id-ce" +#define NID_id_ce 81 +#define OBJ_id_ce OBJ_X500,29L + +#define SN_subject_directory_attributes "subjectDirectoryAttributes" +#define LN_subject_directory_attributes "X509v3 Subject Directory Attributes" +#define NID_subject_directory_attributes 769 +#define OBJ_subject_directory_attributes OBJ_id_ce,9L + +#define SN_subject_key_identifier "subjectKeyIdentifier" +#define LN_subject_key_identifier "X509v3 Subject Key Identifier" +#define NID_subject_key_identifier 82 +#define OBJ_subject_key_identifier OBJ_id_ce,14L + +#define SN_key_usage "keyUsage" +#define LN_key_usage "X509v3 Key Usage" +#define NID_key_usage 83 +#define OBJ_key_usage OBJ_id_ce,15L + +#define SN_private_key_usage_period "privateKeyUsagePeriod" +#define LN_private_key_usage_period "X509v3 Private Key Usage Period" +#define NID_private_key_usage_period 84 +#define OBJ_private_key_usage_period OBJ_id_ce,16L + +#define SN_subject_alt_name "subjectAltName" +#define LN_subject_alt_name "X509v3 Subject Alternative Name" +#define NID_subject_alt_name 85 +#define OBJ_subject_alt_name OBJ_id_ce,17L + +#define SN_issuer_alt_name "issuerAltName" +#define LN_issuer_alt_name "X509v3 Issuer Alternative Name" +#define NID_issuer_alt_name 86 +#define OBJ_issuer_alt_name OBJ_id_ce,18L + +#define SN_basic_constraints "basicConstraints" +#define LN_basic_constraints "X509v3 Basic Constraints" +#define NID_basic_constraints 87 +#define OBJ_basic_constraints OBJ_id_ce,19L + +#define SN_crl_number "crlNumber" +#define LN_crl_number "X509v3 CRL Number" +#define NID_crl_number 88 +#define OBJ_crl_number OBJ_id_ce,20L + +#define SN_crl_reason "CRLReason" +#define LN_crl_reason "X509v3 CRL Reason Code" +#define NID_crl_reason 141 +#define OBJ_crl_reason OBJ_id_ce,21L + +#define SN_invalidity_date "invalidityDate" +#define LN_invalidity_date "Invalidity Date" +#define NID_invalidity_date 142 +#define OBJ_invalidity_date OBJ_id_ce,24L + +#define SN_delta_crl "deltaCRL" +#define LN_delta_crl "X509v3 Delta CRL Indicator" +#define NID_delta_crl 140 +#define OBJ_delta_crl OBJ_id_ce,27L + +#define SN_issuing_distribution_point "issuingDistributionPoint" +#define LN_issuing_distribution_point "X509v3 Issuing Distribution Point" +#define NID_issuing_distribution_point 770 +#define OBJ_issuing_distribution_point OBJ_id_ce,28L + +#define SN_certificate_issuer "certificateIssuer" +#define LN_certificate_issuer "X509v3 Certificate Issuer" +#define NID_certificate_issuer 771 +#define OBJ_certificate_issuer OBJ_id_ce,29L + +#define SN_name_constraints "nameConstraints" +#define LN_name_constraints "X509v3 Name Constraints" +#define NID_name_constraints 666 +#define OBJ_name_constraints OBJ_id_ce,30L + +#define SN_crl_distribution_points "crlDistributionPoints" +#define LN_crl_distribution_points "X509v3 CRL Distribution Points" +#define NID_crl_distribution_points 103 +#define OBJ_crl_distribution_points OBJ_id_ce,31L + +#define SN_certificate_policies "certificatePolicies" +#define LN_certificate_policies "X509v3 Certificate Policies" +#define NID_certificate_policies 89 +#define OBJ_certificate_policies OBJ_id_ce,32L + +#define SN_any_policy "anyPolicy" +#define LN_any_policy "X509v3 Any Policy" +#define NID_any_policy 746 +#define OBJ_any_policy OBJ_certificate_policies,0L + +#define SN_policy_mappings "policyMappings" +#define LN_policy_mappings "X509v3 Policy Mappings" +#define NID_policy_mappings 747 +#define OBJ_policy_mappings OBJ_id_ce,33L + +#define SN_authority_key_identifier "authorityKeyIdentifier" +#define LN_authority_key_identifier "X509v3 Authority Key Identifier" +#define NID_authority_key_identifier 90 +#define OBJ_authority_key_identifier OBJ_id_ce,35L + +#define SN_policy_constraints "policyConstraints" +#define LN_policy_constraints "X509v3 Policy Constraints" +#define NID_policy_constraints 401 +#define OBJ_policy_constraints OBJ_id_ce,36L + +#define SN_ext_key_usage "extendedKeyUsage" +#define LN_ext_key_usage "X509v3 Extended Key Usage" +#define NID_ext_key_usage 126 +#define OBJ_ext_key_usage OBJ_id_ce,37L + +#define SN_freshest_crl "freshestCRL" +#define LN_freshest_crl "X509v3 Freshest CRL" +#define NID_freshest_crl 857 +#define OBJ_freshest_crl OBJ_id_ce,46L + +#define SN_inhibit_any_policy "inhibitAnyPolicy" +#define LN_inhibit_any_policy "X509v3 Inhibit Any Policy" +#define NID_inhibit_any_policy 748 +#define OBJ_inhibit_any_policy OBJ_id_ce,54L + +#define SN_target_information "targetInformation" +#define LN_target_information "X509v3 AC Targeting" +#define NID_target_information 402 +#define OBJ_target_information OBJ_id_ce,55L + +#define SN_no_rev_avail "noRevAvail" +#define LN_no_rev_avail "X509v3 No Revocation Available" +#define NID_no_rev_avail 403 +#define OBJ_no_rev_avail OBJ_id_ce,56L + +#define SN_anyExtendedKeyUsage "anyExtendedKeyUsage" +#define LN_anyExtendedKeyUsage "Any Extended Key Usage" +#define NID_anyExtendedKeyUsage 910 +#define OBJ_anyExtendedKeyUsage OBJ_ext_key_usage,0L + +#define SN_netscape "Netscape" +#define LN_netscape "Netscape Communications Corp." +#define NID_netscape 57 +#define OBJ_netscape 2L,16L,840L,1L,113730L + +#define SN_netscape_cert_extension "nsCertExt" +#define LN_netscape_cert_extension "Netscape Certificate Extension" +#define NID_netscape_cert_extension 58 +#define OBJ_netscape_cert_extension OBJ_netscape,1L + +#define SN_netscape_data_type "nsDataType" +#define LN_netscape_data_type "Netscape Data Type" +#define NID_netscape_data_type 59 +#define OBJ_netscape_data_type OBJ_netscape,2L + +#define SN_netscape_cert_type "nsCertType" +#define LN_netscape_cert_type "Netscape Cert Type" +#define NID_netscape_cert_type 71 +#define OBJ_netscape_cert_type OBJ_netscape_cert_extension,1L + +#define SN_netscape_base_url "nsBaseUrl" +#define LN_netscape_base_url "Netscape Base Url" +#define NID_netscape_base_url 72 +#define OBJ_netscape_base_url OBJ_netscape_cert_extension,2L + +#define SN_netscape_revocation_url "nsRevocationUrl" +#define LN_netscape_revocation_url "Netscape Revocation Url" +#define NID_netscape_revocation_url 73 +#define OBJ_netscape_revocation_url OBJ_netscape_cert_extension,3L + +#define SN_netscape_ca_revocation_url "nsCaRevocationUrl" +#define LN_netscape_ca_revocation_url "Netscape CA Revocation Url" +#define NID_netscape_ca_revocation_url 74 +#define OBJ_netscape_ca_revocation_url OBJ_netscape_cert_extension,4L + +#define SN_netscape_renewal_url "nsRenewalUrl" +#define LN_netscape_renewal_url "Netscape Renewal Url" +#define NID_netscape_renewal_url 75 +#define OBJ_netscape_renewal_url OBJ_netscape_cert_extension,7L + +#define SN_netscape_ca_policy_url "nsCaPolicyUrl" +#define LN_netscape_ca_policy_url "Netscape CA Policy Url" +#define NID_netscape_ca_policy_url 76 +#define OBJ_netscape_ca_policy_url OBJ_netscape_cert_extension,8L + +#define SN_netscape_ssl_server_name "nsSslServerName" +#define LN_netscape_ssl_server_name "Netscape SSL Server Name" +#define NID_netscape_ssl_server_name 77 +#define OBJ_netscape_ssl_server_name OBJ_netscape_cert_extension,12L + +#define SN_netscape_comment "nsComment" +#define LN_netscape_comment "Netscape Comment" +#define NID_netscape_comment 78 +#define OBJ_netscape_comment OBJ_netscape_cert_extension,13L + +#define SN_netscape_cert_sequence "nsCertSequence" +#define LN_netscape_cert_sequence "Netscape Certificate Sequence" +#define NID_netscape_cert_sequence 79 +#define OBJ_netscape_cert_sequence OBJ_netscape_data_type,5L + +#define SN_ns_sgc "nsSGC" +#define LN_ns_sgc "Netscape Server Gated Crypto" +#define NID_ns_sgc 139 +#define OBJ_ns_sgc OBJ_netscape,4L,1L + +#define SN_org "ORG" +#define LN_org "org" +#define NID_org 379 +#define OBJ_org OBJ_iso,3L + +#define SN_dod "DOD" +#define LN_dod "dod" +#define NID_dod 380 +#define OBJ_dod OBJ_org,6L + +#define SN_iana "IANA" +#define LN_iana "iana" +#define NID_iana 381 +#define OBJ_iana OBJ_dod,1L + +#define OBJ_internet OBJ_iana + +#define SN_Directory "directory" +#define LN_Directory "Directory" +#define NID_Directory 382 +#define OBJ_Directory OBJ_internet,1L + +#define SN_Management "mgmt" +#define LN_Management "Management" +#define NID_Management 383 +#define OBJ_Management OBJ_internet,2L + +#define SN_Experimental "experimental" +#define LN_Experimental "Experimental" +#define NID_Experimental 384 +#define OBJ_Experimental OBJ_internet,3L + +#define SN_Private "private" +#define LN_Private "Private" +#define NID_Private 385 +#define OBJ_Private OBJ_internet,4L + +#define SN_Security "security" +#define LN_Security "Security" +#define NID_Security 386 +#define OBJ_Security OBJ_internet,5L + +#define SN_SNMPv2 "snmpv2" +#define LN_SNMPv2 "SNMPv2" +#define NID_SNMPv2 387 +#define OBJ_SNMPv2 OBJ_internet,6L + +#define LN_Mail "Mail" +#define NID_Mail 388 +#define OBJ_Mail OBJ_internet,7L + +#define SN_Enterprises "enterprises" +#define LN_Enterprises "Enterprises" +#define NID_Enterprises 389 +#define OBJ_Enterprises OBJ_Private,1L + +#define SN_dcObject "dcobject" +#define LN_dcObject "dcObject" +#define NID_dcObject 390 +#define OBJ_dcObject OBJ_Enterprises,1466L,344L + +#define SN_mime_mhs "mime-mhs" +#define LN_mime_mhs "MIME MHS" +#define NID_mime_mhs 504 +#define OBJ_mime_mhs OBJ_Mail,1L + +#define SN_mime_mhs_headings "mime-mhs-headings" +#define LN_mime_mhs_headings "mime-mhs-headings" +#define NID_mime_mhs_headings 505 +#define OBJ_mime_mhs_headings OBJ_mime_mhs,1L + +#define SN_mime_mhs_bodies "mime-mhs-bodies" +#define LN_mime_mhs_bodies "mime-mhs-bodies" +#define NID_mime_mhs_bodies 506 +#define OBJ_mime_mhs_bodies OBJ_mime_mhs,2L + +#define SN_id_hex_partial_message "id-hex-partial-message" +#define LN_id_hex_partial_message "id-hex-partial-message" +#define NID_id_hex_partial_message 507 +#define OBJ_id_hex_partial_message OBJ_mime_mhs_headings,1L + +#define SN_id_hex_multipart_message "id-hex-multipart-message" +#define LN_id_hex_multipart_message "id-hex-multipart-message" +#define NID_id_hex_multipart_message 508 +#define OBJ_id_hex_multipart_message OBJ_mime_mhs_headings,2L + +#define SN_zlib_compression "ZLIB" +#define LN_zlib_compression "zlib compression" +#define NID_zlib_compression 125 +#define OBJ_zlib_compression OBJ_id_smime_alg,8L + +#define OBJ_csor 2L,16L,840L,1L,101L,3L + +#define OBJ_nistAlgorithms OBJ_csor,4L + +#define OBJ_aes OBJ_nistAlgorithms,1L + +#define SN_aes_128_ecb "AES-128-ECB" +#define LN_aes_128_ecb "aes-128-ecb" +#define NID_aes_128_ecb 418 +#define OBJ_aes_128_ecb OBJ_aes,1L + +#define SN_aes_128_cbc "AES-128-CBC" +#define LN_aes_128_cbc "aes-128-cbc" +#define NID_aes_128_cbc 419 +#define OBJ_aes_128_cbc OBJ_aes,2L + +#define SN_aes_128_ofb128 "AES-128-OFB" +#define LN_aes_128_ofb128 "aes-128-ofb" +#define NID_aes_128_ofb128 420 +#define OBJ_aes_128_ofb128 OBJ_aes,3L + +#define SN_aes_128_cfb128 "AES-128-CFB" +#define LN_aes_128_cfb128 "aes-128-cfb" +#define NID_aes_128_cfb128 421 +#define OBJ_aes_128_cfb128 OBJ_aes,4L + +#define SN_id_aes128_wrap "id-aes128-wrap" +#define NID_id_aes128_wrap 788 +#define OBJ_id_aes128_wrap OBJ_aes,5L + +#define SN_aes_128_gcm "id-aes128-GCM" +#define LN_aes_128_gcm "aes-128-gcm" +#define NID_aes_128_gcm 895 +#define OBJ_aes_128_gcm OBJ_aes,6L + +#define SN_aes_128_ccm "id-aes128-CCM" +#define LN_aes_128_ccm "aes-128-ccm" +#define NID_aes_128_ccm 896 +#define OBJ_aes_128_ccm OBJ_aes,7L + +#define SN_id_aes128_wrap_pad "id-aes128-wrap-pad" +#define NID_id_aes128_wrap_pad 897 +#define OBJ_id_aes128_wrap_pad OBJ_aes,8L + +#define SN_aes_192_ecb "AES-192-ECB" +#define LN_aes_192_ecb "aes-192-ecb" +#define NID_aes_192_ecb 422 +#define OBJ_aes_192_ecb OBJ_aes,21L + +#define SN_aes_192_cbc "AES-192-CBC" +#define LN_aes_192_cbc "aes-192-cbc" +#define NID_aes_192_cbc 423 +#define OBJ_aes_192_cbc OBJ_aes,22L + +#define SN_aes_192_ofb128 "AES-192-OFB" +#define LN_aes_192_ofb128 "aes-192-ofb" +#define NID_aes_192_ofb128 424 +#define OBJ_aes_192_ofb128 OBJ_aes,23L + +#define SN_aes_192_cfb128 "AES-192-CFB" +#define LN_aes_192_cfb128 "aes-192-cfb" +#define NID_aes_192_cfb128 425 +#define OBJ_aes_192_cfb128 OBJ_aes,24L + +#define SN_id_aes192_wrap "id-aes192-wrap" +#define NID_id_aes192_wrap 789 +#define OBJ_id_aes192_wrap OBJ_aes,25L + +#define SN_aes_192_gcm "id-aes192-GCM" +#define LN_aes_192_gcm "aes-192-gcm" +#define NID_aes_192_gcm 898 +#define OBJ_aes_192_gcm OBJ_aes,26L + +#define SN_aes_192_ccm "id-aes192-CCM" +#define LN_aes_192_ccm "aes-192-ccm" +#define NID_aes_192_ccm 899 +#define OBJ_aes_192_ccm OBJ_aes,27L + +#define SN_id_aes192_wrap_pad "id-aes192-wrap-pad" +#define NID_id_aes192_wrap_pad 900 +#define OBJ_id_aes192_wrap_pad OBJ_aes,28L + +#define SN_aes_256_ecb "AES-256-ECB" +#define LN_aes_256_ecb "aes-256-ecb" +#define NID_aes_256_ecb 426 +#define OBJ_aes_256_ecb OBJ_aes,41L + +#define SN_aes_256_cbc "AES-256-CBC" +#define LN_aes_256_cbc "aes-256-cbc" +#define NID_aes_256_cbc 427 +#define OBJ_aes_256_cbc OBJ_aes,42L + +#define SN_aes_256_ofb128 "AES-256-OFB" +#define LN_aes_256_ofb128 "aes-256-ofb" +#define NID_aes_256_ofb128 428 +#define OBJ_aes_256_ofb128 OBJ_aes,43L + +#define SN_aes_256_cfb128 "AES-256-CFB" +#define LN_aes_256_cfb128 "aes-256-cfb" +#define NID_aes_256_cfb128 429 +#define OBJ_aes_256_cfb128 OBJ_aes,44L + +#define SN_id_aes256_wrap "id-aes256-wrap" +#define NID_id_aes256_wrap 790 +#define OBJ_id_aes256_wrap OBJ_aes,45L + +#define SN_aes_256_gcm "id-aes256-GCM" +#define LN_aes_256_gcm "aes-256-gcm" +#define NID_aes_256_gcm 901 +#define OBJ_aes_256_gcm OBJ_aes,46L + +#define SN_aes_256_ccm "id-aes256-CCM" +#define LN_aes_256_ccm "aes-256-ccm" +#define NID_aes_256_ccm 902 +#define OBJ_aes_256_ccm OBJ_aes,47L + +#define SN_id_aes256_wrap_pad "id-aes256-wrap-pad" +#define NID_id_aes256_wrap_pad 903 +#define OBJ_id_aes256_wrap_pad OBJ_aes,48L + +#define SN_aes_128_cfb1 "AES-128-CFB1" +#define LN_aes_128_cfb1 "aes-128-cfb1" +#define NID_aes_128_cfb1 650 + +#define SN_aes_192_cfb1 "AES-192-CFB1" +#define LN_aes_192_cfb1 "aes-192-cfb1" +#define NID_aes_192_cfb1 651 + +#define SN_aes_256_cfb1 "AES-256-CFB1" +#define LN_aes_256_cfb1 "aes-256-cfb1" +#define NID_aes_256_cfb1 652 + +#define SN_aes_128_cfb8 "AES-128-CFB8" +#define LN_aes_128_cfb8 "aes-128-cfb8" +#define NID_aes_128_cfb8 653 + +#define SN_aes_192_cfb8 "AES-192-CFB8" +#define LN_aes_192_cfb8 "aes-192-cfb8" +#define NID_aes_192_cfb8 654 + +#define SN_aes_256_cfb8 "AES-256-CFB8" +#define LN_aes_256_cfb8 "aes-256-cfb8" +#define NID_aes_256_cfb8 655 + +#define SN_aes_128_ctr "AES-128-CTR" +#define LN_aes_128_ctr "aes-128-ctr" +#define NID_aes_128_ctr 904 + +#define SN_aes_192_ctr "AES-192-CTR" +#define LN_aes_192_ctr "aes-192-ctr" +#define NID_aes_192_ctr 905 + +#define SN_aes_256_ctr "AES-256-CTR" +#define LN_aes_256_ctr "aes-256-ctr" +#define NID_aes_256_ctr 906 + +#define SN_aes_128_ocb "AES-128-OCB" +#define LN_aes_128_ocb "aes-128-ocb" +#define NID_aes_128_ocb 958 + +#define SN_aes_192_ocb "AES-192-OCB" +#define LN_aes_192_ocb "aes-192-ocb" +#define NID_aes_192_ocb 959 + +#define SN_aes_256_ocb "AES-256-OCB" +#define LN_aes_256_ocb "aes-256-ocb" +#define NID_aes_256_ocb 960 + +#define SN_aes_128_xts "AES-128-XTS" +#define LN_aes_128_xts "aes-128-xts" +#define NID_aes_128_xts 913 + +#define SN_aes_256_xts "AES-256-XTS" +#define LN_aes_256_xts "aes-256-xts" +#define NID_aes_256_xts 914 + +#define SN_des_cfb1 "DES-CFB1" +#define LN_des_cfb1 "des-cfb1" +#define NID_des_cfb1 656 + +#define SN_des_cfb8 "DES-CFB8" +#define LN_des_cfb8 "des-cfb8" +#define NID_des_cfb8 657 + +#define SN_des_ede3_cfb1 "DES-EDE3-CFB1" +#define LN_des_ede3_cfb1 "des-ede3-cfb1" +#define NID_des_ede3_cfb1 658 + +#define SN_des_ede3_cfb8 "DES-EDE3-CFB8" +#define LN_des_ede3_cfb8 "des-ede3-cfb8" +#define NID_des_ede3_cfb8 659 + +#define OBJ_nist_hashalgs OBJ_nistAlgorithms,2L + +#define SN_sha256 "SHA256" +#define LN_sha256 "sha256" +#define NID_sha256 672 +#define OBJ_sha256 OBJ_nist_hashalgs,1L + +#define SN_sha384 "SHA384" +#define LN_sha384 "sha384" +#define NID_sha384 673 +#define OBJ_sha384 OBJ_nist_hashalgs,2L + +#define SN_sha512 "SHA512" +#define LN_sha512 "sha512" +#define NID_sha512 674 +#define OBJ_sha512 OBJ_nist_hashalgs,3L + +#define SN_sha224 "SHA224" +#define LN_sha224 "sha224" +#define NID_sha224 675 +#define OBJ_sha224 OBJ_nist_hashalgs,4L + +#define OBJ_dsa_with_sha2 OBJ_nistAlgorithms,3L + +#define SN_dsa_with_SHA224 "dsa_with_SHA224" +#define NID_dsa_with_SHA224 802 +#define OBJ_dsa_with_SHA224 OBJ_dsa_with_sha2,1L + +#define SN_dsa_with_SHA256 "dsa_with_SHA256" +#define NID_dsa_with_SHA256 803 +#define OBJ_dsa_with_SHA256 OBJ_dsa_with_sha2,2L + +#define SN_hold_instruction_code "holdInstructionCode" +#define LN_hold_instruction_code "Hold Instruction Code" +#define NID_hold_instruction_code 430 +#define OBJ_hold_instruction_code OBJ_id_ce,23L + +#define OBJ_holdInstruction OBJ_X9_57,2L + +#define SN_hold_instruction_none "holdInstructionNone" +#define LN_hold_instruction_none "Hold Instruction None" +#define NID_hold_instruction_none 431 +#define OBJ_hold_instruction_none OBJ_holdInstruction,1L + +#define SN_hold_instruction_call_issuer "holdInstructionCallIssuer" +#define LN_hold_instruction_call_issuer "Hold Instruction Call Issuer" +#define NID_hold_instruction_call_issuer 432 +#define OBJ_hold_instruction_call_issuer OBJ_holdInstruction,2L + +#define SN_hold_instruction_reject "holdInstructionReject" +#define LN_hold_instruction_reject "Hold Instruction Reject" +#define NID_hold_instruction_reject 433 +#define OBJ_hold_instruction_reject OBJ_holdInstruction,3L + +#define SN_data "data" +#define NID_data 434 +#define OBJ_data OBJ_itu_t,9L + +#define SN_pss "pss" +#define NID_pss 435 +#define OBJ_pss OBJ_data,2342L + +#define SN_ucl "ucl" +#define NID_ucl 436 +#define OBJ_ucl OBJ_pss,19200300L + +#define SN_pilot "pilot" +#define NID_pilot 437 +#define OBJ_pilot OBJ_ucl,100L + +#define LN_pilotAttributeType "pilotAttributeType" +#define NID_pilotAttributeType 438 +#define OBJ_pilotAttributeType OBJ_pilot,1L + +#define LN_pilotAttributeSyntax "pilotAttributeSyntax" +#define NID_pilotAttributeSyntax 439 +#define OBJ_pilotAttributeSyntax OBJ_pilot,3L + +#define LN_pilotObjectClass "pilotObjectClass" +#define NID_pilotObjectClass 440 +#define OBJ_pilotObjectClass OBJ_pilot,4L + +#define LN_pilotGroups "pilotGroups" +#define NID_pilotGroups 441 +#define OBJ_pilotGroups OBJ_pilot,10L + +#define LN_iA5StringSyntax "iA5StringSyntax" +#define NID_iA5StringSyntax 442 +#define OBJ_iA5StringSyntax OBJ_pilotAttributeSyntax,4L + +#define LN_caseIgnoreIA5StringSyntax "caseIgnoreIA5StringSyntax" +#define NID_caseIgnoreIA5StringSyntax 443 +#define OBJ_caseIgnoreIA5StringSyntax OBJ_pilotAttributeSyntax,5L + +#define LN_pilotObject "pilotObject" +#define NID_pilotObject 444 +#define OBJ_pilotObject OBJ_pilotObjectClass,3L + +#define LN_pilotPerson "pilotPerson" +#define NID_pilotPerson 445 +#define OBJ_pilotPerson OBJ_pilotObjectClass,4L + +#define SN_account "account" +#define NID_account 446 +#define OBJ_account OBJ_pilotObjectClass,5L + +#define SN_document "document" +#define NID_document 447 +#define OBJ_document OBJ_pilotObjectClass,6L + +#define SN_room "room" +#define NID_room 448 +#define OBJ_room OBJ_pilotObjectClass,7L + +#define LN_documentSeries "documentSeries" +#define NID_documentSeries 449 +#define OBJ_documentSeries OBJ_pilotObjectClass,9L + +#define SN_Domain "domain" +#define LN_Domain "Domain" +#define NID_Domain 392 +#define OBJ_Domain OBJ_pilotObjectClass,13L + +#define LN_rFC822localPart "rFC822localPart" +#define NID_rFC822localPart 450 +#define OBJ_rFC822localPart OBJ_pilotObjectClass,14L + +#define LN_dNSDomain "dNSDomain" +#define NID_dNSDomain 451 +#define OBJ_dNSDomain OBJ_pilotObjectClass,15L + +#define LN_domainRelatedObject "domainRelatedObject" +#define NID_domainRelatedObject 452 +#define OBJ_domainRelatedObject OBJ_pilotObjectClass,17L + +#define LN_friendlyCountry "friendlyCountry" +#define NID_friendlyCountry 453 +#define OBJ_friendlyCountry OBJ_pilotObjectClass,18L + +#define LN_simpleSecurityObject "simpleSecurityObject" +#define NID_simpleSecurityObject 454 +#define OBJ_simpleSecurityObject OBJ_pilotObjectClass,19L + +#define LN_pilotOrganization "pilotOrganization" +#define NID_pilotOrganization 455 +#define OBJ_pilotOrganization OBJ_pilotObjectClass,20L + +#define LN_pilotDSA "pilotDSA" +#define NID_pilotDSA 456 +#define OBJ_pilotDSA OBJ_pilotObjectClass,21L + +#define LN_qualityLabelledData "qualityLabelledData" +#define NID_qualityLabelledData 457 +#define OBJ_qualityLabelledData OBJ_pilotObjectClass,22L + +#define SN_userId "UID" +#define LN_userId "userId" +#define NID_userId 458 +#define OBJ_userId OBJ_pilotAttributeType,1L + +#define LN_textEncodedORAddress "textEncodedORAddress" +#define NID_textEncodedORAddress 459 +#define OBJ_textEncodedORAddress OBJ_pilotAttributeType,2L + +#define SN_rfc822Mailbox "mail" +#define LN_rfc822Mailbox "rfc822Mailbox" +#define NID_rfc822Mailbox 460 +#define OBJ_rfc822Mailbox OBJ_pilotAttributeType,3L + +#define SN_info "info" +#define NID_info 461 +#define OBJ_info OBJ_pilotAttributeType,4L + +#define LN_favouriteDrink "favouriteDrink" +#define NID_favouriteDrink 462 +#define OBJ_favouriteDrink OBJ_pilotAttributeType,5L + +#define LN_roomNumber "roomNumber" +#define NID_roomNumber 463 +#define OBJ_roomNumber OBJ_pilotAttributeType,6L + +#define SN_photo "photo" +#define NID_photo 464 +#define OBJ_photo OBJ_pilotAttributeType,7L + +#define LN_userClass "userClass" +#define NID_userClass 465 +#define OBJ_userClass OBJ_pilotAttributeType,8L + +#define SN_host "host" +#define NID_host 466 +#define OBJ_host OBJ_pilotAttributeType,9L + +#define SN_manager "manager" +#define NID_manager 467 +#define OBJ_manager OBJ_pilotAttributeType,10L + +#define LN_documentIdentifier "documentIdentifier" +#define NID_documentIdentifier 468 +#define OBJ_documentIdentifier OBJ_pilotAttributeType,11L + +#define LN_documentTitle "documentTitle" +#define NID_documentTitle 469 +#define OBJ_documentTitle OBJ_pilotAttributeType,12L + +#define LN_documentVersion "documentVersion" +#define NID_documentVersion 470 +#define OBJ_documentVersion OBJ_pilotAttributeType,13L + +#define LN_documentAuthor "documentAuthor" +#define NID_documentAuthor 471 +#define OBJ_documentAuthor OBJ_pilotAttributeType,14L + +#define LN_documentLocation "documentLocation" +#define NID_documentLocation 472 +#define OBJ_documentLocation OBJ_pilotAttributeType,15L + +#define LN_homeTelephoneNumber "homeTelephoneNumber" +#define NID_homeTelephoneNumber 473 +#define OBJ_homeTelephoneNumber OBJ_pilotAttributeType,20L + +#define SN_secretary "secretary" +#define NID_secretary 474 +#define OBJ_secretary OBJ_pilotAttributeType,21L + +#define LN_otherMailbox "otherMailbox" +#define NID_otherMailbox 475 +#define OBJ_otherMailbox OBJ_pilotAttributeType,22L + +#define LN_lastModifiedTime "lastModifiedTime" +#define NID_lastModifiedTime 476 +#define OBJ_lastModifiedTime OBJ_pilotAttributeType,23L + +#define LN_lastModifiedBy "lastModifiedBy" +#define NID_lastModifiedBy 477 +#define OBJ_lastModifiedBy OBJ_pilotAttributeType,24L + +#define SN_domainComponent "DC" +#define LN_domainComponent "domainComponent" +#define NID_domainComponent 391 +#define OBJ_domainComponent OBJ_pilotAttributeType,25L + +#define LN_aRecord "aRecord" +#define NID_aRecord 478 +#define OBJ_aRecord OBJ_pilotAttributeType,26L + +#define LN_pilotAttributeType27 "pilotAttributeType27" +#define NID_pilotAttributeType27 479 +#define OBJ_pilotAttributeType27 OBJ_pilotAttributeType,27L + +#define LN_mXRecord "mXRecord" +#define NID_mXRecord 480 +#define OBJ_mXRecord OBJ_pilotAttributeType,28L + +#define LN_nSRecord "nSRecord" +#define NID_nSRecord 481 +#define OBJ_nSRecord OBJ_pilotAttributeType,29L + +#define LN_sOARecord "sOARecord" +#define NID_sOARecord 482 +#define OBJ_sOARecord OBJ_pilotAttributeType,30L + +#define LN_cNAMERecord "cNAMERecord" +#define NID_cNAMERecord 483 +#define OBJ_cNAMERecord OBJ_pilotAttributeType,31L + +#define LN_associatedDomain "associatedDomain" +#define NID_associatedDomain 484 +#define OBJ_associatedDomain OBJ_pilotAttributeType,37L + +#define LN_associatedName "associatedName" +#define NID_associatedName 485 +#define OBJ_associatedName OBJ_pilotAttributeType,38L + +#define LN_homePostalAddress "homePostalAddress" +#define NID_homePostalAddress 486 +#define OBJ_homePostalAddress OBJ_pilotAttributeType,39L + +#define LN_personalTitle "personalTitle" +#define NID_personalTitle 487 +#define OBJ_personalTitle OBJ_pilotAttributeType,40L + +#define LN_mobileTelephoneNumber "mobileTelephoneNumber" +#define NID_mobileTelephoneNumber 488 +#define OBJ_mobileTelephoneNumber OBJ_pilotAttributeType,41L + +#define LN_pagerTelephoneNumber "pagerTelephoneNumber" +#define NID_pagerTelephoneNumber 489 +#define OBJ_pagerTelephoneNumber OBJ_pilotAttributeType,42L + +#define LN_friendlyCountryName "friendlyCountryName" +#define NID_friendlyCountryName 490 +#define OBJ_friendlyCountryName OBJ_pilotAttributeType,43L + +#define SN_uniqueIdentifier "uid" +#define LN_uniqueIdentifier "uniqueIdentifier" +#define NID_uniqueIdentifier 102 +#define OBJ_uniqueIdentifier OBJ_pilotAttributeType,44L + +#define LN_organizationalStatus "organizationalStatus" +#define NID_organizationalStatus 491 +#define OBJ_organizationalStatus OBJ_pilotAttributeType,45L + +#define LN_janetMailbox "janetMailbox" +#define NID_janetMailbox 492 +#define OBJ_janetMailbox OBJ_pilotAttributeType,46L + +#define LN_mailPreferenceOption "mailPreferenceOption" +#define NID_mailPreferenceOption 493 +#define OBJ_mailPreferenceOption OBJ_pilotAttributeType,47L + +#define LN_buildingName "buildingName" +#define NID_buildingName 494 +#define OBJ_buildingName OBJ_pilotAttributeType,48L + +#define LN_dSAQuality "dSAQuality" +#define NID_dSAQuality 495 +#define OBJ_dSAQuality OBJ_pilotAttributeType,49L + +#define LN_singleLevelQuality "singleLevelQuality" +#define NID_singleLevelQuality 496 +#define OBJ_singleLevelQuality OBJ_pilotAttributeType,50L + +#define LN_subtreeMinimumQuality "subtreeMinimumQuality" +#define NID_subtreeMinimumQuality 497 +#define OBJ_subtreeMinimumQuality OBJ_pilotAttributeType,51L + +#define LN_subtreeMaximumQuality "subtreeMaximumQuality" +#define NID_subtreeMaximumQuality 498 +#define OBJ_subtreeMaximumQuality OBJ_pilotAttributeType,52L + +#define LN_personalSignature "personalSignature" +#define NID_personalSignature 499 +#define OBJ_personalSignature OBJ_pilotAttributeType,53L + +#define LN_dITRedirect "dITRedirect" +#define NID_dITRedirect 500 +#define OBJ_dITRedirect OBJ_pilotAttributeType,54L + +#define SN_audio "audio" +#define NID_audio 501 +#define OBJ_audio OBJ_pilotAttributeType,55L + +#define LN_documentPublisher "documentPublisher" +#define NID_documentPublisher 502 +#define OBJ_documentPublisher OBJ_pilotAttributeType,56L + +#define SN_id_set "id-set" +#define LN_id_set "Secure Electronic Transactions" +#define NID_id_set 512 +#define OBJ_id_set OBJ_international_organizations,42L + +#define SN_set_ctype "set-ctype" +#define LN_set_ctype "content types" +#define NID_set_ctype 513 +#define OBJ_set_ctype OBJ_id_set,0L + +#define SN_set_msgExt "set-msgExt" +#define LN_set_msgExt "message extensions" +#define NID_set_msgExt 514 +#define OBJ_set_msgExt OBJ_id_set,1L + +#define SN_set_attr "set-attr" +#define NID_set_attr 515 +#define OBJ_set_attr OBJ_id_set,3L + +#define SN_set_policy "set-policy" +#define NID_set_policy 516 +#define OBJ_set_policy OBJ_id_set,5L + +#define SN_set_certExt "set-certExt" +#define LN_set_certExt "certificate extensions" +#define NID_set_certExt 517 +#define OBJ_set_certExt OBJ_id_set,7L + +#define SN_set_brand "set-brand" +#define NID_set_brand 518 +#define OBJ_set_brand OBJ_id_set,8L + +#define SN_setct_PANData "setct-PANData" +#define NID_setct_PANData 519 +#define OBJ_setct_PANData OBJ_set_ctype,0L + +#define SN_setct_PANToken "setct-PANToken" +#define NID_setct_PANToken 520 +#define OBJ_setct_PANToken OBJ_set_ctype,1L + +#define SN_setct_PANOnly "setct-PANOnly" +#define NID_setct_PANOnly 521 +#define OBJ_setct_PANOnly OBJ_set_ctype,2L + +#define SN_setct_OIData "setct-OIData" +#define NID_setct_OIData 522 +#define OBJ_setct_OIData OBJ_set_ctype,3L + +#define SN_setct_PI "setct-PI" +#define NID_setct_PI 523 +#define OBJ_setct_PI OBJ_set_ctype,4L + +#define SN_setct_PIData "setct-PIData" +#define NID_setct_PIData 524 +#define OBJ_setct_PIData OBJ_set_ctype,5L + +#define SN_setct_PIDataUnsigned "setct-PIDataUnsigned" +#define NID_setct_PIDataUnsigned 525 +#define OBJ_setct_PIDataUnsigned OBJ_set_ctype,6L + +#define SN_setct_HODInput "setct-HODInput" +#define NID_setct_HODInput 526 +#define OBJ_setct_HODInput OBJ_set_ctype,7L + +#define SN_setct_AuthResBaggage "setct-AuthResBaggage" +#define NID_setct_AuthResBaggage 527 +#define OBJ_setct_AuthResBaggage OBJ_set_ctype,8L + +#define SN_setct_AuthRevReqBaggage "setct-AuthRevReqBaggage" +#define NID_setct_AuthRevReqBaggage 528 +#define OBJ_setct_AuthRevReqBaggage OBJ_set_ctype,9L + +#define SN_setct_AuthRevResBaggage "setct-AuthRevResBaggage" +#define NID_setct_AuthRevResBaggage 529 +#define OBJ_setct_AuthRevResBaggage OBJ_set_ctype,10L + +#define SN_setct_CapTokenSeq "setct-CapTokenSeq" +#define NID_setct_CapTokenSeq 530 +#define OBJ_setct_CapTokenSeq OBJ_set_ctype,11L + +#define SN_setct_PInitResData "setct-PInitResData" +#define NID_setct_PInitResData 531 +#define OBJ_setct_PInitResData OBJ_set_ctype,12L + +#define SN_setct_PI_TBS "setct-PI-TBS" +#define NID_setct_PI_TBS 532 +#define OBJ_setct_PI_TBS OBJ_set_ctype,13L + +#define SN_setct_PResData "setct-PResData" +#define NID_setct_PResData 533 +#define OBJ_setct_PResData OBJ_set_ctype,14L + +#define SN_setct_AuthReqTBS "setct-AuthReqTBS" +#define NID_setct_AuthReqTBS 534 +#define OBJ_setct_AuthReqTBS OBJ_set_ctype,16L + +#define SN_setct_AuthResTBS "setct-AuthResTBS" +#define NID_setct_AuthResTBS 535 +#define OBJ_setct_AuthResTBS OBJ_set_ctype,17L + +#define SN_setct_AuthResTBSX "setct-AuthResTBSX" +#define NID_setct_AuthResTBSX 536 +#define OBJ_setct_AuthResTBSX OBJ_set_ctype,18L + +#define SN_setct_AuthTokenTBS "setct-AuthTokenTBS" +#define NID_setct_AuthTokenTBS 537 +#define OBJ_setct_AuthTokenTBS OBJ_set_ctype,19L + +#define SN_setct_CapTokenData "setct-CapTokenData" +#define NID_setct_CapTokenData 538 +#define OBJ_setct_CapTokenData OBJ_set_ctype,20L + +#define SN_setct_CapTokenTBS "setct-CapTokenTBS" +#define NID_setct_CapTokenTBS 539 +#define OBJ_setct_CapTokenTBS OBJ_set_ctype,21L + +#define SN_setct_AcqCardCodeMsg "setct-AcqCardCodeMsg" +#define NID_setct_AcqCardCodeMsg 540 +#define OBJ_setct_AcqCardCodeMsg OBJ_set_ctype,22L + +#define SN_setct_AuthRevReqTBS "setct-AuthRevReqTBS" +#define NID_setct_AuthRevReqTBS 541 +#define OBJ_setct_AuthRevReqTBS OBJ_set_ctype,23L + +#define SN_setct_AuthRevResData "setct-AuthRevResData" +#define NID_setct_AuthRevResData 542 +#define OBJ_setct_AuthRevResData OBJ_set_ctype,24L + +#define SN_setct_AuthRevResTBS "setct-AuthRevResTBS" +#define NID_setct_AuthRevResTBS 543 +#define OBJ_setct_AuthRevResTBS OBJ_set_ctype,25L + +#define SN_setct_CapReqTBS "setct-CapReqTBS" +#define NID_setct_CapReqTBS 544 +#define OBJ_setct_CapReqTBS OBJ_set_ctype,26L + +#define SN_setct_CapReqTBSX "setct-CapReqTBSX" +#define NID_setct_CapReqTBSX 545 +#define OBJ_setct_CapReqTBSX OBJ_set_ctype,27L + +#define SN_setct_CapResData "setct-CapResData" +#define NID_setct_CapResData 546 +#define OBJ_setct_CapResData OBJ_set_ctype,28L + +#define SN_setct_CapRevReqTBS "setct-CapRevReqTBS" +#define NID_setct_CapRevReqTBS 547 +#define OBJ_setct_CapRevReqTBS OBJ_set_ctype,29L + +#define SN_setct_CapRevReqTBSX "setct-CapRevReqTBSX" +#define NID_setct_CapRevReqTBSX 548 +#define OBJ_setct_CapRevReqTBSX OBJ_set_ctype,30L + +#define SN_setct_CapRevResData "setct-CapRevResData" +#define NID_setct_CapRevResData 549 +#define OBJ_setct_CapRevResData OBJ_set_ctype,31L + +#define SN_setct_CredReqTBS "setct-CredReqTBS" +#define NID_setct_CredReqTBS 550 +#define OBJ_setct_CredReqTBS OBJ_set_ctype,32L + +#define SN_setct_CredReqTBSX "setct-CredReqTBSX" +#define NID_setct_CredReqTBSX 551 +#define OBJ_setct_CredReqTBSX OBJ_set_ctype,33L + +#define SN_setct_CredResData "setct-CredResData" +#define NID_setct_CredResData 552 +#define OBJ_setct_CredResData OBJ_set_ctype,34L + +#define SN_setct_CredRevReqTBS "setct-CredRevReqTBS" +#define NID_setct_CredRevReqTBS 553 +#define OBJ_setct_CredRevReqTBS OBJ_set_ctype,35L + +#define SN_setct_CredRevReqTBSX "setct-CredRevReqTBSX" +#define NID_setct_CredRevReqTBSX 554 +#define OBJ_setct_CredRevReqTBSX OBJ_set_ctype,36L + +#define SN_setct_CredRevResData "setct-CredRevResData" +#define NID_setct_CredRevResData 555 +#define OBJ_setct_CredRevResData OBJ_set_ctype,37L + +#define SN_setct_PCertReqData "setct-PCertReqData" +#define NID_setct_PCertReqData 556 +#define OBJ_setct_PCertReqData OBJ_set_ctype,38L + +#define SN_setct_PCertResTBS "setct-PCertResTBS" +#define NID_setct_PCertResTBS 557 +#define OBJ_setct_PCertResTBS OBJ_set_ctype,39L + +#define SN_setct_BatchAdminReqData "setct-BatchAdminReqData" +#define NID_setct_BatchAdminReqData 558 +#define OBJ_setct_BatchAdminReqData OBJ_set_ctype,40L + +#define SN_setct_BatchAdminResData "setct-BatchAdminResData" +#define NID_setct_BatchAdminResData 559 +#define OBJ_setct_BatchAdminResData OBJ_set_ctype,41L + +#define SN_setct_CardCInitResTBS "setct-CardCInitResTBS" +#define NID_setct_CardCInitResTBS 560 +#define OBJ_setct_CardCInitResTBS OBJ_set_ctype,42L + +#define SN_setct_MeAqCInitResTBS "setct-MeAqCInitResTBS" +#define NID_setct_MeAqCInitResTBS 561 +#define OBJ_setct_MeAqCInitResTBS OBJ_set_ctype,43L + +#define SN_setct_RegFormResTBS "setct-RegFormResTBS" +#define NID_setct_RegFormResTBS 562 +#define OBJ_setct_RegFormResTBS OBJ_set_ctype,44L + +#define SN_setct_CertReqData "setct-CertReqData" +#define NID_setct_CertReqData 563 +#define OBJ_setct_CertReqData OBJ_set_ctype,45L + +#define SN_setct_CertReqTBS "setct-CertReqTBS" +#define NID_setct_CertReqTBS 564 +#define OBJ_setct_CertReqTBS OBJ_set_ctype,46L + +#define SN_setct_CertResData "setct-CertResData" +#define NID_setct_CertResData 565 +#define OBJ_setct_CertResData OBJ_set_ctype,47L + +#define SN_setct_CertInqReqTBS "setct-CertInqReqTBS" +#define NID_setct_CertInqReqTBS 566 +#define OBJ_setct_CertInqReqTBS OBJ_set_ctype,48L + +#define SN_setct_ErrorTBS "setct-ErrorTBS" +#define NID_setct_ErrorTBS 567 +#define OBJ_setct_ErrorTBS OBJ_set_ctype,49L + +#define SN_setct_PIDualSignedTBE "setct-PIDualSignedTBE" +#define NID_setct_PIDualSignedTBE 568 +#define OBJ_setct_PIDualSignedTBE OBJ_set_ctype,50L + +#define SN_setct_PIUnsignedTBE "setct-PIUnsignedTBE" +#define NID_setct_PIUnsignedTBE 569 +#define OBJ_setct_PIUnsignedTBE OBJ_set_ctype,51L + +#define SN_setct_AuthReqTBE "setct-AuthReqTBE" +#define NID_setct_AuthReqTBE 570 +#define OBJ_setct_AuthReqTBE OBJ_set_ctype,52L + +#define SN_setct_AuthResTBE "setct-AuthResTBE" +#define NID_setct_AuthResTBE 571 +#define OBJ_setct_AuthResTBE OBJ_set_ctype,53L + +#define SN_setct_AuthResTBEX "setct-AuthResTBEX" +#define NID_setct_AuthResTBEX 572 +#define OBJ_setct_AuthResTBEX OBJ_set_ctype,54L + +#define SN_setct_AuthTokenTBE "setct-AuthTokenTBE" +#define NID_setct_AuthTokenTBE 573 +#define OBJ_setct_AuthTokenTBE OBJ_set_ctype,55L + +#define SN_setct_CapTokenTBE "setct-CapTokenTBE" +#define NID_setct_CapTokenTBE 574 +#define OBJ_setct_CapTokenTBE OBJ_set_ctype,56L + +#define SN_setct_CapTokenTBEX "setct-CapTokenTBEX" +#define NID_setct_CapTokenTBEX 575 +#define OBJ_setct_CapTokenTBEX OBJ_set_ctype,57L + +#define SN_setct_AcqCardCodeMsgTBE "setct-AcqCardCodeMsgTBE" +#define NID_setct_AcqCardCodeMsgTBE 576 +#define OBJ_setct_AcqCardCodeMsgTBE OBJ_set_ctype,58L + +#define SN_setct_AuthRevReqTBE "setct-AuthRevReqTBE" +#define NID_setct_AuthRevReqTBE 577 +#define OBJ_setct_AuthRevReqTBE OBJ_set_ctype,59L + +#define SN_setct_AuthRevResTBE "setct-AuthRevResTBE" +#define NID_setct_AuthRevResTBE 578 +#define OBJ_setct_AuthRevResTBE OBJ_set_ctype,60L + +#define SN_setct_AuthRevResTBEB "setct-AuthRevResTBEB" +#define NID_setct_AuthRevResTBEB 579 +#define OBJ_setct_AuthRevResTBEB OBJ_set_ctype,61L + +#define SN_setct_CapReqTBE "setct-CapReqTBE" +#define NID_setct_CapReqTBE 580 +#define OBJ_setct_CapReqTBE OBJ_set_ctype,62L + +#define SN_setct_CapReqTBEX "setct-CapReqTBEX" +#define NID_setct_CapReqTBEX 581 +#define OBJ_setct_CapReqTBEX OBJ_set_ctype,63L + +#define SN_setct_CapResTBE "setct-CapResTBE" +#define NID_setct_CapResTBE 582 +#define OBJ_setct_CapResTBE OBJ_set_ctype,64L + +#define SN_setct_CapRevReqTBE "setct-CapRevReqTBE" +#define NID_setct_CapRevReqTBE 583 +#define OBJ_setct_CapRevReqTBE OBJ_set_ctype,65L + +#define SN_setct_CapRevReqTBEX "setct-CapRevReqTBEX" +#define NID_setct_CapRevReqTBEX 584 +#define OBJ_setct_CapRevReqTBEX OBJ_set_ctype,66L + +#define SN_setct_CapRevResTBE "setct-CapRevResTBE" +#define NID_setct_CapRevResTBE 585 +#define OBJ_setct_CapRevResTBE OBJ_set_ctype,67L + +#define SN_setct_CredReqTBE "setct-CredReqTBE" +#define NID_setct_CredReqTBE 586 +#define OBJ_setct_CredReqTBE OBJ_set_ctype,68L + +#define SN_setct_CredReqTBEX "setct-CredReqTBEX" +#define NID_setct_CredReqTBEX 587 +#define OBJ_setct_CredReqTBEX OBJ_set_ctype,69L + +#define SN_setct_CredResTBE "setct-CredResTBE" +#define NID_setct_CredResTBE 588 +#define OBJ_setct_CredResTBE OBJ_set_ctype,70L + +#define SN_setct_CredRevReqTBE "setct-CredRevReqTBE" +#define NID_setct_CredRevReqTBE 589 +#define OBJ_setct_CredRevReqTBE OBJ_set_ctype,71L + +#define SN_setct_CredRevReqTBEX "setct-CredRevReqTBEX" +#define NID_setct_CredRevReqTBEX 590 +#define OBJ_setct_CredRevReqTBEX OBJ_set_ctype,72L + +#define SN_setct_CredRevResTBE "setct-CredRevResTBE" +#define NID_setct_CredRevResTBE 591 +#define OBJ_setct_CredRevResTBE OBJ_set_ctype,73L + +#define SN_setct_BatchAdminReqTBE "setct-BatchAdminReqTBE" +#define NID_setct_BatchAdminReqTBE 592 +#define OBJ_setct_BatchAdminReqTBE OBJ_set_ctype,74L + +#define SN_setct_BatchAdminResTBE "setct-BatchAdminResTBE" +#define NID_setct_BatchAdminResTBE 593 +#define OBJ_setct_BatchAdminResTBE OBJ_set_ctype,75L + +#define SN_setct_RegFormReqTBE "setct-RegFormReqTBE" +#define NID_setct_RegFormReqTBE 594 +#define OBJ_setct_RegFormReqTBE OBJ_set_ctype,76L + +#define SN_setct_CertReqTBE "setct-CertReqTBE" +#define NID_setct_CertReqTBE 595 +#define OBJ_setct_CertReqTBE OBJ_set_ctype,77L + +#define SN_setct_CertReqTBEX "setct-CertReqTBEX" +#define NID_setct_CertReqTBEX 596 +#define OBJ_setct_CertReqTBEX OBJ_set_ctype,78L + +#define SN_setct_CertResTBE "setct-CertResTBE" +#define NID_setct_CertResTBE 597 +#define OBJ_setct_CertResTBE OBJ_set_ctype,79L + +#define SN_setct_CRLNotificationTBS "setct-CRLNotificationTBS" +#define NID_setct_CRLNotificationTBS 598 +#define OBJ_setct_CRLNotificationTBS OBJ_set_ctype,80L + +#define SN_setct_CRLNotificationResTBS "setct-CRLNotificationResTBS" +#define NID_setct_CRLNotificationResTBS 599 +#define OBJ_setct_CRLNotificationResTBS OBJ_set_ctype,81L + +#define SN_setct_BCIDistributionTBS "setct-BCIDistributionTBS" +#define NID_setct_BCIDistributionTBS 600 +#define OBJ_setct_BCIDistributionTBS OBJ_set_ctype,82L + +#define SN_setext_genCrypt "setext-genCrypt" +#define LN_setext_genCrypt "generic cryptogram" +#define NID_setext_genCrypt 601 +#define OBJ_setext_genCrypt OBJ_set_msgExt,1L + +#define SN_setext_miAuth "setext-miAuth" +#define LN_setext_miAuth "merchant initiated auth" +#define NID_setext_miAuth 602 +#define OBJ_setext_miAuth OBJ_set_msgExt,3L + +#define SN_setext_pinSecure "setext-pinSecure" +#define NID_setext_pinSecure 603 +#define OBJ_setext_pinSecure OBJ_set_msgExt,4L + +#define SN_setext_pinAny "setext-pinAny" +#define NID_setext_pinAny 604 +#define OBJ_setext_pinAny OBJ_set_msgExt,5L + +#define SN_setext_track2 "setext-track2" +#define NID_setext_track2 605 +#define OBJ_setext_track2 OBJ_set_msgExt,7L + +#define SN_setext_cv "setext-cv" +#define LN_setext_cv "additional verification" +#define NID_setext_cv 606 +#define OBJ_setext_cv OBJ_set_msgExt,8L + +#define SN_set_policy_root "set-policy-root" +#define NID_set_policy_root 607 +#define OBJ_set_policy_root OBJ_set_policy,0L + +#define SN_setCext_hashedRoot "setCext-hashedRoot" +#define NID_setCext_hashedRoot 608 +#define OBJ_setCext_hashedRoot OBJ_set_certExt,0L + +#define SN_setCext_certType "setCext-certType" +#define NID_setCext_certType 609 +#define OBJ_setCext_certType OBJ_set_certExt,1L + +#define SN_setCext_merchData "setCext-merchData" +#define NID_setCext_merchData 610 +#define OBJ_setCext_merchData OBJ_set_certExt,2L + +#define SN_setCext_cCertRequired "setCext-cCertRequired" +#define NID_setCext_cCertRequired 611 +#define OBJ_setCext_cCertRequired OBJ_set_certExt,3L + +#define SN_setCext_tunneling "setCext-tunneling" +#define NID_setCext_tunneling 612 +#define OBJ_setCext_tunneling OBJ_set_certExt,4L + +#define SN_setCext_setExt "setCext-setExt" +#define NID_setCext_setExt 613 +#define OBJ_setCext_setExt OBJ_set_certExt,5L + +#define SN_setCext_setQualf "setCext-setQualf" +#define NID_setCext_setQualf 614 +#define OBJ_setCext_setQualf OBJ_set_certExt,6L + +#define SN_setCext_PGWYcapabilities "setCext-PGWYcapabilities" +#define NID_setCext_PGWYcapabilities 615 +#define OBJ_setCext_PGWYcapabilities OBJ_set_certExt,7L + +#define SN_setCext_TokenIdentifier "setCext-TokenIdentifier" +#define NID_setCext_TokenIdentifier 616 +#define OBJ_setCext_TokenIdentifier OBJ_set_certExt,8L + +#define SN_setCext_Track2Data "setCext-Track2Data" +#define NID_setCext_Track2Data 617 +#define OBJ_setCext_Track2Data OBJ_set_certExt,9L + +#define SN_setCext_TokenType "setCext-TokenType" +#define NID_setCext_TokenType 618 +#define OBJ_setCext_TokenType OBJ_set_certExt,10L + +#define SN_setCext_IssuerCapabilities "setCext-IssuerCapabilities" +#define NID_setCext_IssuerCapabilities 619 +#define OBJ_setCext_IssuerCapabilities OBJ_set_certExt,11L + +#define SN_setAttr_Cert "setAttr-Cert" +#define NID_setAttr_Cert 620 +#define OBJ_setAttr_Cert OBJ_set_attr,0L + +#define SN_setAttr_PGWYcap "setAttr-PGWYcap" +#define LN_setAttr_PGWYcap "payment gateway capabilities" +#define NID_setAttr_PGWYcap 621 +#define OBJ_setAttr_PGWYcap OBJ_set_attr,1L + +#define SN_setAttr_TokenType "setAttr-TokenType" +#define NID_setAttr_TokenType 622 +#define OBJ_setAttr_TokenType OBJ_set_attr,2L + +#define SN_setAttr_IssCap "setAttr-IssCap" +#define LN_setAttr_IssCap "issuer capabilities" +#define NID_setAttr_IssCap 623 +#define OBJ_setAttr_IssCap OBJ_set_attr,3L + +#define SN_set_rootKeyThumb "set-rootKeyThumb" +#define NID_set_rootKeyThumb 624 +#define OBJ_set_rootKeyThumb OBJ_setAttr_Cert,0L + +#define SN_set_addPolicy "set-addPolicy" +#define NID_set_addPolicy 625 +#define OBJ_set_addPolicy OBJ_setAttr_Cert,1L + +#define SN_setAttr_Token_EMV "setAttr-Token-EMV" +#define NID_setAttr_Token_EMV 626 +#define OBJ_setAttr_Token_EMV OBJ_setAttr_TokenType,1L + +#define SN_setAttr_Token_B0Prime "setAttr-Token-B0Prime" +#define NID_setAttr_Token_B0Prime 627 +#define OBJ_setAttr_Token_B0Prime OBJ_setAttr_TokenType,2L + +#define SN_setAttr_IssCap_CVM "setAttr-IssCap-CVM" +#define NID_setAttr_IssCap_CVM 628 +#define OBJ_setAttr_IssCap_CVM OBJ_setAttr_IssCap,3L + +#define SN_setAttr_IssCap_T2 "setAttr-IssCap-T2" +#define NID_setAttr_IssCap_T2 629 +#define OBJ_setAttr_IssCap_T2 OBJ_setAttr_IssCap,4L + +#define SN_setAttr_IssCap_Sig "setAttr-IssCap-Sig" +#define NID_setAttr_IssCap_Sig 630 +#define OBJ_setAttr_IssCap_Sig OBJ_setAttr_IssCap,5L + +#define SN_setAttr_GenCryptgrm "setAttr-GenCryptgrm" +#define LN_setAttr_GenCryptgrm "generate cryptogram" +#define NID_setAttr_GenCryptgrm 631 +#define OBJ_setAttr_GenCryptgrm OBJ_setAttr_IssCap_CVM,1L + +#define SN_setAttr_T2Enc "setAttr-T2Enc" +#define LN_setAttr_T2Enc "encrypted track 2" +#define NID_setAttr_T2Enc 632 +#define OBJ_setAttr_T2Enc OBJ_setAttr_IssCap_T2,1L + +#define SN_setAttr_T2cleartxt "setAttr-T2cleartxt" +#define LN_setAttr_T2cleartxt "cleartext track 2" +#define NID_setAttr_T2cleartxt 633 +#define OBJ_setAttr_T2cleartxt OBJ_setAttr_IssCap_T2,2L + +#define SN_setAttr_TokICCsig "setAttr-TokICCsig" +#define LN_setAttr_TokICCsig "ICC or token signature" +#define NID_setAttr_TokICCsig 634 +#define OBJ_setAttr_TokICCsig OBJ_setAttr_IssCap_Sig,1L + +#define SN_setAttr_SecDevSig "setAttr-SecDevSig" +#define LN_setAttr_SecDevSig "secure device signature" +#define NID_setAttr_SecDevSig 635 +#define OBJ_setAttr_SecDevSig OBJ_setAttr_IssCap_Sig,2L + +#define SN_set_brand_IATA_ATA "set-brand-IATA-ATA" +#define NID_set_brand_IATA_ATA 636 +#define OBJ_set_brand_IATA_ATA OBJ_set_brand,1L + +#define SN_set_brand_Diners "set-brand-Diners" +#define NID_set_brand_Diners 637 +#define OBJ_set_brand_Diners OBJ_set_brand,30L + +#define SN_set_brand_AmericanExpress "set-brand-AmericanExpress" +#define NID_set_brand_AmericanExpress 638 +#define OBJ_set_brand_AmericanExpress OBJ_set_brand,34L + +#define SN_set_brand_JCB "set-brand-JCB" +#define NID_set_brand_JCB 639 +#define OBJ_set_brand_JCB OBJ_set_brand,35L + +#define SN_set_brand_Visa "set-brand-Visa" +#define NID_set_brand_Visa 640 +#define OBJ_set_brand_Visa OBJ_set_brand,4L + +#define SN_set_brand_MasterCard "set-brand-MasterCard" +#define NID_set_brand_MasterCard 641 +#define OBJ_set_brand_MasterCard OBJ_set_brand,5L + +#define SN_set_brand_Novus "set-brand-Novus" +#define NID_set_brand_Novus 642 +#define OBJ_set_brand_Novus OBJ_set_brand,6011L + +#define SN_des_cdmf "DES-CDMF" +#define LN_des_cdmf "des-cdmf" +#define NID_des_cdmf 643 +#define OBJ_des_cdmf OBJ_rsadsi,3L,10L + +#define SN_rsaOAEPEncryptionSET "rsaOAEPEncryptionSET" +#define NID_rsaOAEPEncryptionSET 644 +#define OBJ_rsaOAEPEncryptionSET OBJ_rsadsi,1L,1L,6L + +#define SN_ipsec3 "Oakley-EC2N-3" +#define LN_ipsec3 "ipsec3" +#define NID_ipsec3 749 + +#define SN_ipsec4 "Oakley-EC2N-4" +#define LN_ipsec4 "ipsec4" +#define NID_ipsec4 750 + +#define SN_whirlpool "whirlpool" +#define NID_whirlpool 804 +#define OBJ_whirlpool OBJ_iso,0L,10118L,3L,0L,55L + +#define SN_cryptopro "cryptopro" +#define NID_cryptopro 805 +#define OBJ_cryptopro OBJ_member_body,643L,2L,2L + +#define SN_cryptocom "cryptocom" +#define NID_cryptocom 806 +#define OBJ_cryptocom OBJ_member_body,643L,2L,9L + +#define SN_id_tc26 "id-tc26" +#define NID_id_tc26 974 +#define OBJ_id_tc26 OBJ_member_body,643L,7L,1L + +#define SN_id_GostR3411_94_with_GostR3410_2001 "id-GostR3411-94-with-GostR3410-2001" +#define LN_id_GostR3411_94_with_GostR3410_2001 "GOST R 34.11-94 with GOST R 34.10-2001" +#define NID_id_GostR3411_94_with_GostR3410_2001 807 +#define OBJ_id_GostR3411_94_with_GostR3410_2001 OBJ_cryptopro,3L + +#define SN_id_GostR3411_94_with_GostR3410_94 "id-GostR3411-94-with-GostR3410-94" +#define LN_id_GostR3411_94_with_GostR3410_94 "GOST R 34.11-94 with GOST R 34.10-94" +#define NID_id_GostR3411_94_with_GostR3410_94 808 +#define OBJ_id_GostR3411_94_with_GostR3410_94 OBJ_cryptopro,4L + +#define SN_id_GostR3411_94 "md_gost94" +#define LN_id_GostR3411_94 "GOST R 34.11-94" +#define NID_id_GostR3411_94 809 +#define OBJ_id_GostR3411_94 OBJ_cryptopro,9L + +#define SN_id_HMACGostR3411_94 "id-HMACGostR3411-94" +#define LN_id_HMACGostR3411_94 "HMAC GOST 34.11-94" +#define NID_id_HMACGostR3411_94 810 +#define OBJ_id_HMACGostR3411_94 OBJ_cryptopro,10L + +#define SN_id_GostR3410_2001 "gost2001" +#define LN_id_GostR3410_2001 "GOST R 34.10-2001" +#define NID_id_GostR3410_2001 811 +#define OBJ_id_GostR3410_2001 OBJ_cryptopro,19L + +#define SN_id_GostR3410_94 "gost94" +#define LN_id_GostR3410_94 "GOST R 34.10-94" +#define NID_id_GostR3410_94 812 +#define OBJ_id_GostR3410_94 OBJ_cryptopro,20L + +#define SN_id_Gost28147_89 "gost89" +#define LN_id_Gost28147_89 "GOST 28147-89" +#define NID_id_Gost28147_89 813 +#define OBJ_id_Gost28147_89 OBJ_cryptopro,21L + +#define SN_gost89_cnt "gost89-cnt" +#define NID_gost89_cnt 814 + +#define SN_gost89_cnt_12 "gost89-cnt-12" +#define NID_gost89_cnt_12 975 + +#define SN_gost89_cbc "gost89-cbc" +#define NID_gost89_cbc 1009 + +#define SN_gost89_ecb "gost89-ecb" +#define NID_gost89_ecb 1010 + +#define SN_gost89_ctr "gost89-ctr" +#define NID_gost89_ctr 1011 + +#define SN_id_Gost28147_89_MAC "gost-mac" +#define LN_id_Gost28147_89_MAC "GOST 28147-89 MAC" +#define NID_id_Gost28147_89_MAC 815 +#define OBJ_id_Gost28147_89_MAC OBJ_cryptopro,22L + +#define SN_gost_mac_12 "gost-mac-12" +#define NID_gost_mac_12 976 + +#define SN_id_GostR3411_94_prf "prf-gostr3411-94" +#define LN_id_GostR3411_94_prf "GOST R 34.11-94 PRF" +#define NID_id_GostR3411_94_prf 816 +#define OBJ_id_GostR3411_94_prf OBJ_cryptopro,23L + +#define SN_id_GostR3410_2001DH "id-GostR3410-2001DH" +#define LN_id_GostR3410_2001DH "GOST R 34.10-2001 DH" +#define NID_id_GostR3410_2001DH 817 +#define OBJ_id_GostR3410_2001DH OBJ_cryptopro,98L + +#define SN_id_GostR3410_94DH "id-GostR3410-94DH" +#define LN_id_GostR3410_94DH "GOST R 34.10-94 DH" +#define NID_id_GostR3410_94DH 818 +#define OBJ_id_GostR3410_94DH OBJ_cryptopro,99L + +#define SN_id_Gost28147_89_CryptoPro_KeyMeshing "id-Gost28147-89-CryptoPro-KeyMeshing" +#define NID_id_Gost28147_89_CryptoPro_KeyMeshing 819 +#define OBJ_id_Gost28147_89_CryptoPro_KeyMeshing OBJ_cryptopro,14L,1L + +#define SN_id_Gost28147_89_None_KeyMeshing "id-Gost28147-89-None-KeyMeshing" +#define NID_id_Gost28147_89_None_KeyMeshing 820 +#define OBJ_id_Gost28147_89_None_KeyMeshing OBJ_cryptopro,14L,0L + +#define SN_id_GostR3411_94_TestParamSet "id-GostR3411-94-TestParamSet" +#define NID_id_GostR3411_94_TestParamSet 821 +#define OBJ_id_GostR3411_94_TestParamSet OBJ_cryptopro,30L,0L + +#define SN_id_GostR3411_94_CryptoProParamSet "id-GostR3411-94-CryptoProParamSet" +#define NID_id_GostR3411_94_CryptoProParamSet 822 +#define OBJ_id_GostR3411_94_CryptoProParamSet OBJ_cryptopro,30L,1L + +#define SN_id_Gost28147_89_TestParamSet "id-Gost28147-89-TestParamSet" +#define NID_id_Gost28147_89_TestParamSet 823 +#define OBJ_id_Gost28147_89_TestParamSet OBJ_cryptopro,31L,0L + +#define SN_id_Gost28147_89_CryptoPro_A_ParamSet "id-Gost28147-89-CryptoPro-A-ParamSet" +#define NID_id_Gost28147_89_CryptoPro_A_ParamSet 824 +#define OBJ_id_Gost28147_89_CryptoPro_A_ParamSet OBJ_cryptopro,31L,1L + +#define SN_id_Gost28147_89_CryptoPro_B_ParamSet "id-Gost28147-89-CryptoPro-B-ParamSet" +#define NID_id_Gost28147_89_CryptoPro_B_ParamSet 825 +#define OBJ_id_Gost28147_89_CryptoPro_B_ParamSet OBJ_cryptopro,31L,2L + +#define SN_id_Gost28147_89_CryptoPro_C_ParamSet "id-Gost28147-89-CryptoPro-C-ParamSet" +#define NID_id_Gost28147_89_CryptoPro_C_ParamSet 826 +#define OBJ_id_Gost28147_89_CryptoPro_C_ParamSet OBJ_cryptopro,31L,3L + +#define SN_id_Gost28147_89_CryptoPro_D_ParamSet "id-Gost28147-89-CryptoPro-D-ParamSet" +#define NID_id_Gost28147_89_CryptoPro_D_ParamSet 827 +#define OBJ_id_Gost28147_89_CryptoPro_D_ParamSet OBJ_cryptopro,31L,4L + +#define SN_id_Gost28147_89_CryptoPro_Oscar_1_1_ParamSet "id-Gost28147-89-CryptoPro-Oscar-1-1-ParamSet" +#define NID_id_Gost28147_89_CryptoPro_Oscar_1_1_ParamSet 828 +#define OBJ_id_Gost28147_89_CryptoPro_Oscar_1_1_ParamSet OBJ_cryptopro,31L,5L + +#define SN_id_Gost28147_89_CryptoPro_Oscar_1_0_ParamSet "id-Gost28147-89-CryptoPro-Oscar-1-0-ParamSet" +#define NID_id_Gost28147_89_CryptoPro_Oscar_1_0_ParamSet 829 +#define OBJ_id_Gost28147_89_CryptoPro_Oscar_1_0_ParamSet OBJ_cryptopro,31L,6L + +#define SN_id_Gost28147_89_CryptoPro_RIC_1_ParamSet "id-Gost28147-89-CryptoPro-RIC-1-ParamSet" +#define NID_id_Gost28147_89_CryptoPro_RIC_1_ParamSet 830 +#define OBJ_id_Gost28147_89_CryptoPro_RIC_1_ParamSet OBJ_cryptopro,31L,7L + +#define SN_id_GostR3410_94_TestParamSet "id-GostR3410-94-TestParamSet" +#define NID_id_GostR3410_94_TestParamSet 831 +#define OBJ_id_GostR3410_94_TestParamSet OBJ_cryptopro,32L,0L + +#define SN_id_GostR3410_94_CryptoPro_A_ParamSet "id-GostR3410-94-CryptoPro-A-ParamSet" +#define NID_id_GostR3410_94_CryptoPro_A_ParamSet 832 +#define OBJ_id_GostR3410_94_CryptoPro_A_ParamSet OBJ_cryptopro,32L,2L + +#define SN_id_GostR3410_94_CryptoPro_B_ParamSet "id-GostR3410-94-CryptoPro-B-ParamSet" +#define NID_id_GostR3410_94_CryptoPro_B_ParamSet 833 +#define OBJ_id_GostR3410_94_CryptoPro_B_ParamSet OBJ_cryptopro,32L,3L + +#define SN_id_GostR3410_94_CryptoPro_C_ParamSet "id-GostR3410-94-CryptoPro-C-ParamSet" +#define NID_id_GostR3410_94_CryptoPro_C_ParamSet 834 +#define OBJ_id_GostR3410_94_CryptoPro_C_ParamSet OBJ_cryptopro,32L,4L + +#define SN_id_GostR3410_94_CryptoPro_D_ParamSet "id-GostR3410-94-CryptoPro-D-ParamSet" +#define NID_id_GostR3410_94_CryptoPro_D_ParamSet 835 +#define OBJ_id_GostR3410_94_CryptoPro_D_ParamSet OBJ_cryptopro,32L,5L + +#define SN_id_GostR3410_94_CryptoPro_XchA_ParamSet "id-GostR3410-94-CryptoPro-XchA-ParamSet" +#define NID_id_GostR3410_94_CryptoPro_XchA_ParamSet 836 +#define OBJ_id_GostR3410_94_CryptoPro_XchA_ParamSet OBJ_cryptopro,33L,1L + +#define SN_id_GostR3410_94_CryptoPro_XchB_ParamSet "id-GostR3410-94-CryptoPro-XchB-ParamSet" +#define NID_id_GostR3410_94_CryptoPro_XchB_ParamSet 837 +#define OBJ_id_GostR3410_94_CryptoPro_XchB_ParamSet OBJ_cryptopro,33L,2L + +#define SN_id_GostR3410_94_CryptoPro_XchC_ParamSet "id-GostR3410-94-CryptoPro-XchC-ParamSet" +#define NID_id_GostR3410_94_CryptoPro_XchC_ParamSet 838 +#define OBJ_id_GostR3410_94_CryptoPro_XchC_ParamSet OBJ_cryptopro,33L,3L + +#define SN_id_GostR3410_2001_TestParamSet "id-GostR3410-2001-TestParamSet" +#define NID_id_GostR3410_2001_TestParamSet 839 +#define OBJ_id_GostR3410_2001_TestParamSet OBJ_cryptopro,35L,0L + +#define SN_id_GostR3410_2001_CryptoPro_A_ParamSet "id-GostR3410-2001-CryptoPro-A-ParamSet" +#define NID_id_GostR3410_2001_CryptoPro_A_ParamSet 840 +#define OBJ_id_GostR3410_2001_CryptoPro_A_ParamSet OBJ_cryptopro,35L,1L + +#define SN_id_GostR3410_2001_CryptoPro_B_ParamSet "id-GostR3410-2001-CryptoPro-B-ParamSet" +#define NID_id_GostR3410_2001_CryptoPro_B_ParamSet 841 +#define OBJ_id_GostR3410_2001_CryptoPro_B_ParamSet OBJ_cryptopro,35L,2L + +#define SN_id_GostR3410_2001_CryptoPro_C_ParamSet "id-GostR3410-2001-CryptoPro-C-ParamSet" +#define NID_id_GostR3410_2001_CryptoPro_C_ParamSet 842 +#define OBJ_id_GostR3410_2001_CryptoPro_C_ParamSet OBJ_cryptopro,35L,3L + +#define SN_id_GostR3410_2001_CryptoPro_XchA_ParamSet "id-GostR3410-2001-CryptoPro-XchA-ParamSet" +#define NID_id_GostR3410_2001_CryptoPro_XchA_ParamSet 843 +#define OBJ_id_GostR3410_2001_CryptoPro_XchA_ParamSet OBJ_cryptopro,36L,0L + +#define SN_id_GostR3410_2001_CryptoPro_XchB_ParamSet "id-GostR3410-2001-CryptoPro-XchB-ParamSet" +#define NID_id_GostR3410_2001_CryptoPro_XchB_ParamSet 844 +#define OBJ_id_GostR3410_2001_CryptoPro_XchB_ParamSet OBJ_cryptopro,36L,1L + +#define SN_id_GostR3410_94_a "id-GostR3410-94-a" +#define NID_id_GostR3410_94_a 845 +#define OBJ_id_GostR3410_94_a OBJ_id_GostR3410_94,1L + +#define SN_id_GostR3410_94_aBis "id-GostR3410-94-aBis" +#define NID_id_GostR3410_94_aBis 846 +#define OBJ_id_GostR3410_94_aBis OBJ_id_GostR3410_94,2L + +#define SN_id_GostR3410_94_b "id-GostR3410-94-b" +#define NID_id_GostR3410_94_b 847 +#define OBJ_id_GostR3410_94_b OBJ_id_GostR3410_94,3L + +#define SN_id_GostR3410_94_bBis "id-GostR3410-94-bBis" +#define NID_id_GostR3410_94_bBis 848 +#define OBJ_id_GostR3410_94_bBis OBJ_id_GostR3410_94,4L + +#define SN_id_Gost28147_89_cc "id-Gost28147-89-cc" +#define LN_id_Gost28147_89_cc "GOST 28147-89 Cryptocom ParamSet" +#define NID_id_Gost28147_89_cc 849 +#define OBJ_id_Gost28147_89_cc OBJ_cryptocom,1L,6L,1L + +#define SN_id_GostR3410_94_cc "gost94cc" +#define LN_id_GostR3410_94_cc "GOST 34.10-94 Cryptocom" +#define NID_id_GostR3410_94_cc 850 +#define OBJ_id_GostR3410_94_cc OBJ_cryptocom,1L,5L,3L + +#define SN_id_GostR3410_2001_cc "gost2001cc" +#define LN_id_GostR3410_2001_cc "GOST 34.10-2001 Cryptocom" +#define NID_id_GostR3410_2001_cc 851 +#define OBJ_id_GostR3410_2001_cc OBJ_cryptocom,1L,5L,4L + +#define SN_id_GostR3411_94_with_GostR3410_94_cc "id-GostR3411-94-with-GostR3410-94-cc" +#define LN_id_GostR3411_94_with_GostR3410_94_cc "GOST R 34.11-94 with GOST R 34.10-94 Cryptocom" +#define NID_id_GostR3411_94_with_GostR3410_94_cc 852 +#define OBJ_id_GostR3411_94_with_GostR3410_94_cc OBJ_cryptocom,1L,3L,3L + +#define SN_id_GostR3411_94_with_GostR3410_2001_cc "id-GostR3411-94-with-GostR3410-2001-cc" +#define LN_id_GostR3411_94_with_GostR3410_2001_cc "GOST R 34.11-94 with GOST R 34.10-2001 Cryptocom" +#define NID_id_GostR3411_94_with_GostR3410_2001_cc 853 +#define OBJ_id_GostR3411_94_with_GostR3410_2001_cc OBJ_cryptocom,1L,3L,4L + +#define SN_id_GostR3410_2001_ParamSet_cc "id-GostR3410-2001-ParamSet-cc" +#define LN_id_GostR3410_2001_ParamSet_cc "GOST R 3410-2001 Parameter Set Cryptocom" +#define NID_id_GostR3410_2001_ParamSet_cc 854 +#define OBJ_id_GostR3410_2001_ParamSet_cc OBJ_cryptocom,1L,8L,1L + +#define SN_id_tc26_algorithms "id-tc26-algorithms" +#define NID_id_tc26_algorithms 977 +#define OBJ_id_tc26_algorithms OBJ_id_tc26,1L + +#define SN_id_tc26_sign "id-tc26-sign" +#define NID_id_tc26_sign 978 +#define OBJ_id_tc26_sign OBJ_id_tc26_algorithms,1L + +#define SN_id_GostR3410_2012_256 "gost2012_256" +#define LN_id_GostR3410_2012_256 "GOST R 34.10-2012 with 256 bit modulus" +#define NID_id_GostR3410_2012_256 979 +#define OBJ_id_GostR3410_2012_256 OBJ_id_tc26_sign,1L + +#define SN_id_GostR3410_2012_512 "gost2012_512" +#define LN_id_GostR3410_2012_512 "GOST R 34.10-2012 with 512 bit modulus" +#define NID_id_GostR3410_2012_512 980 +#define OBJ_id_GostR3410_2012_512 OBJ_id_tc26_sign,2L + +#define SN_id_tc26_digest "id-tc26-digest" +#define NID_id_tc26_digest 981 +#define OBJ_id_tc26_digest OBJ_id_tc26_algorithms,2L + +#define SN_id_GostR3411_2012_256 "md_gost12_256" +#define LN_id_GostR3411_2012_256 "GOST R 34.11-2012 with 256 bit hash" +#define NID_id_GostR3411_2012_256 982 +#define OBJ_id_GostR3411_2012_256 OBJ_id_tc26_digest,2L + +#define SN_id_GostR3411_2012_512 "md_gost12_512" +#define LN_id_GostR3411_2012_512 "GOST R 34.11-2012 with 512 bit hash" +#define NID_id_GostR3411_2012_512 983 +#define OBJ_id_GostR3411_2012_512 OBJ_id_tc26_digest,3L + +#define SN_id_tc26_signwithdigest "id-tc26-signwithdigest" +#define NID_id_tc26_signwithdigest 984 +#define OBJ_id_tc26_signwithdigest OBJ_id_tc26_algorithms,3L + +#define SN_id_tc26_signwithdigest_gost3410_2012_256 "id-tc26-signwithdigest-gost3410-2012-256" +#define LN_id_tc26_signwithdigest_gost3410_2012_256 "GOST R 34.10-2012 with GOST R 34.11-2012 (256 bit)" +#define NID_id_tc26_signwithdigest_gost3410_2012_256 985 +#define OBJ_id_tc26_signwithdigest_gost3410_2012_256 OBJ_id_tc26_signwithdigest,2L + +#define SN_id_tc26_signwithdigest_gost3410_2012_512 "id-tc26-signwithdigest-gost3410-2012-512" +#define LN_id_tc26_signwithdigest_gost3410_2012_512 "GOST R 34.10-2012 with GOST R 34.11-2012 (512 bit)" +#define NID_id_tc26_signwithdigest_gost3410_2012_512 986 +#define OBJ_id_tc26_signwithdigest_gost3410_2012_512 OBJ_id_tc26_signwithdigest,3L + +#define SN_id_tc26_mac "id-tc26-mac" +#define NID_id_tc26_mac 987 +#define OBJ_id_tc26_mac OBJ_id_tc26_algorithms,4L + +#define SN_id_tc26_hmac_gost_3411_2012_256 "id-tc26-hmac-gost-3411-2012-256" +#define LN_id_tc26_hmac_gost_3411_2012_256 "HMAC GOST 34.11-2012 256 bit" +#define NID_id_tc26_hmac_gost_3411_2012_256 988 +#define OBJ_id_tc26_hmac_gost_3411_2012_256 OBJ_id_tc26_mac,1L + +#define SN_id_tc26_hmac_gost_3411_2012_512 "id-tc26-hmac-gost-3411-2012-512" +#define LN_id_tc26_hmac_gost_3411_2012_512 "HMAC GOST 34.11-2012 512 bit" +#define NID_id_tc26_hmac_gost_3411_2012_512 989 +#define OBJ_id_tc26_hmac_gost_3411_2012_512 OBJ_id_tc26_mac,2L + +#define SN_id_tc26_cipher "id-tc26-cipher" +#define NID_id_tc26_cipher 990 +#define OBJ_id_tc26_cipher OBJ_id_tc26_algorithms,5L + +#define SN_id_tc26_agreement "id-tc26-agreement" +#define NID_id_tc26_agreement 991 +#define OBJ_id_tc26_agreement OBJ_id_tc26_algorithms,6L + +#define SN_id_tc26_agreement_gost_3410_2012_256 "id-tc26-agreement-gost-3410-2012-256" +#define NID_id_tc26_agreement_gost_3410_2012_256 992 +#define OBJ_id_tc26_agreement_gost_3410_2012_256 OBJ_id_tc26_agreement,1L + +#define SN_id_tc26_agreement_gost_3410_2012_512 "id-tc26-agreement-gost-3410-2012-512" +#define NID_id_tc26_agreement_gost_3410_2012_512 993 +#define OBJ_id_tc26_agreement_gost_3410_2012_512 OBJ_id_tc26_agreement,2L + +#define SN_id_tc26_constants "id-tc26-constants" +#define NID_id_tc26_constants 994 +#define OBJ_id_tc26_constants OBJ_id_tc26,2L + +#define SN_id_tc26_sign_constants "id-tc26-sign-constants" +#define NID_id_tc26_sign_constants 995 +#define OBJ_id_tc26_sign_constants OBJ_id_tc26_constants,1L + +#define SN_id_tc26_gost_3410_2012_512_constants "id-tc26-gost-3410-2012-512-constants" +#define NID_id_tc26_gost_3410_2012_512_constants 996 +#define OBJ_id_tc26_gost_3410_2012_512_constants OBJ_id_tc26_sign_constants,2L + +#define SN_id_tc26_gost_3410_2012_512_paramSetTest "id-tc26-gost-3410-2012-512-paramSetTest" +#define LN_id_tc26_gost_3410_2012_512_paramSetTest "GOST R 34.10-2012 (512 bit) testing parameter set" +#define NID_id_tc26_gost_3410_2012_512_paramSetTest 997 +#define OBJ_id_tc26_gost_3410_2012_512_paramSetTest OBJ_id_tc26_gost_3410_2012_512_constants,0L + +#define SN_id_tc26_gost_3410_2012_512_paramSetA "id-tc26-gost-3410-2012-512-paramSetA" +#define LN_id_tc26_gost_3410_2012_512_paramSetA "GOST R 34.10-2012 (512 bit) ParamSet A" +#define NID_id_tc26_gost_3410_2012_512_paramSetA 998 +#define OBJ_id_tc26_gost_3410_2012_512_paramSetA OBJ_id_tc26_gost_3410_2012_512_constants,1L + +#define SN_id_tc26_gost_3410_2012_512_paramSetB "id-tc26-gost-3410-2012-512-paramSetB" +#define LN_id_tc26_gost_3410_2012_512_paramSetB "GOST R 34.10-2012 (512 bit) ParamSet B" +#define NID_id_tc26_gost_3410_2012_512_paramSetB 999 +#define OBJ_id_tc26_gost_3410_2012_512_paramSetB OBJ_id_tc26_gost_3410_2012_512_constants,2L + +#define SN_id_tc26_digest_constants "id-tc26-digest-constants" +#define NID_id_tc26_digest_constants 1000 +#define OBJ_id_tc26_digest_constants OBJ_id_tc26_constants,2L + +#define SN_id_tc26_cipher_constants "id-tc26-cipher-constants" +#define NID_id_tc26_cipher_constants 1001 +#define OBJ_id_tc26_cipher_constants OBJ_id_tc26_constants,5L + +#define SN_id_tc26_gost_28147_constants "id-tc26-gost-28147-constants" +#define NID_id_tc26_gost_28147_constants 1002 +#define OBJ_id_tc26_gost_28147_constants OBJ_id_tc26_cipher_constants,1L + +#define SN_id_tc26_gost_28147_param_Z "id-tc26-gost-28147-param-Z" +#define LN_id_tc26_gost_28147_param_Z "GOST 28147-89 TC26 parameter set" +#define NID_id_tc26_gost_28147_param_Z 1003 +#define OBJ_id_tc26_gost_28147_param_Z OBJ_id_tc26_gost_28147_constants,1L + +#define SN_INN "INN" +#define LN_INN "INN" +#define NID_INN 1004 +#define OBJ_INN OBJ_member_body,643L,3L,131L,1L,1L + +#define SN_OGRN "OGRN" +#define LN_OGRN "OGRN" +#define NID_OGRN 1005 +#define OBJ_OGRN OBJ_member_body,643L,100L,1L + +#define SN_SNILS "SNILS" +#define LN_SNILS "SNILS" +#define NID_SNILS 1006 +#define OBJ_SNILS OBJ_member_body,643L,100L,3L + +#define SN_subjectSignTool "subjectSignTool" +#define LN_subjectSignTool "Signing Tool of Subject" +#define NID_subjectSignTool 1007 +#define OBJ_subjectSignTool OBJ_member_body,643L,100L,111L + +#define SN_issuerSignTool "issuerSignTool" +#define LN_issuerSignTool "Signing Tool of Issuer" +#define NID_issuerSignTool 1008 +#define OBJ_issuerSignTool OBJ_member_body,643L,100L,112L + +#define SN_grasshopper_ecb "grasshopper-ecb" +#define NID_grasshopper_ecb 1012 + +#define SN_grasshopper_ctr "grasshopper-ctr" +#define NID_grasshopper_ctr 1013 + +#define SN_grasshopper_ofb "grasshopper-ofb" +#define NID_grasshopper_ofb 1014 + +#define SN_grasshopper_cbc "grasshopper-cbc" +#define NID_grasshopper_cbc 1015 + +#define SN_grasshopper_cfb "grasshopper-cfb" +#define NID_grasshopper_cfb 1016 + +#define SN_grasshopper_mac "grasshopper-mac" +#define NID_grasshopper_mac 1017 + +#define SN_camellia_128_cbc "CAMELLIA-128-CBC" +#define LN_camellia_128_cbc "camellia-128-cbc" +#define NID_camellia_128_cbc 751 +#define OBJ_camellia_128_cbc 1L,2L,392L,200011L,61L,1L,1L,1L,2L + +#define SN_camellia_192_cbc "CAMELLIA-192-CBC" +#define LN_camellia_192_cbc "camellia-192-cbc" +#define NID_camellia_192_cbc 752 +#define OBJ_camellia_192_cbc 1L,2L,392L,200011L,61L,1L,1L,1L,3L + +#define SN_camellia_256_cbc "CAMELLIA-256-CBC" +#define LN_camellia_256_cbc "camellia-256-cbc" +#define NID_camellia_256_cbc 753 +#define OBJ_camellia_256_cbc 1L,2L,392L,200011L,61L,1L,1L,1L,4L + +#define SN_id_camellia128_wrap "id-camellia128-wrap" +#define NID_id_camellia128_wrap 907 +#define OBJ_id_camellia128_wrap 1L,2L,392L,200011L,61L,1L,1L,3L,2L + +#define SN_id_camellia192_wrap "id-camellia192-wrap" +#define NID_id_camellia192_wrap 908 +#define OBJ_id_camellia192_wrap 1L,2L,392L,200011L,61L,1L,1L,3L,3L + +#define SN_id_camellia256_wrap "id-camellia256-wrap" +#define NID_id_camellia256_wrap 909 +#define OBJ_id_camellia256_wrap 1L,2L,392L,200011L,61L,1L,1L,3L,4L + +#define OBJ_ntt_ds 0L,3L,4401L,5L + +#define OBJ_camellia OBJ_ntt_ds,3L,1L,9L + +#define SN_camellia_128_ecb "CAMELLIA-128-ECB" +#define LN_camellia_128_ecb "camellia-128-ecb" +#define NID_camellia_128_ecb 754 +#define OBJ_camellia_128_ecb OBJ_camellia,1L + +#define SN_camellia_128_ofb128 "CAMELLIA-128-OFB" +#define LN_camellia_128_ofb128 "camellia-128-ofb" +#define NID_camellia_128_ofb128 766 +#define OBJ_camellia_128_ofb128 OBJ_camellia,3L + +#define SN_camellia_128_cfb128 "CAMELLIA-128-CFB" +#define LN_camellia_128_cfb128 "camellia-128-cfb" +#define NID_camellia_128_cfb128 757 +#define OBJ_camellia_128_cfb128 OBJ_camellia,4L + +#define SN_camellia_128_gcm "CAMELLIA-128-GCM" +#define LN_camellia_128_gcm "camellia-128-gcm" +#define NID_camellia_128_gcm 961 +#define OBJ_camellia_128_gcm OBJ_camellia,6L + +#define SN_camellia_128_ccm "CAMELLIA-128-CCM" +#define LN_camellia_128_ccm "camellia-128-ccm" +#define NID_camellia_128_ccm 962 +#define OBJ_camellia_128_ccm OBJ_camellia,7L + +#define SN_camellia_128_ctr "CAMELLIA-128-CTR" +#define LN_camellia_128_ctr "camellia-128-ctr" +#define NID_camellia_128_ctr 963 +#define OBJ_camellia_128_ctr OBJ_camellia,9L + +#define SN_camellia_128_cmac "CAMELLIA-128-CMAC" +#define LN_camellia_128_cmac "camellia-128-cmac" +#define NID_camellia_128_cmac 964 +#define OBJ_camellia_128_cmac OBJ_camellia,10L + +#define SN_camellia_192_ecb "CAMELLIA-192-ECB" +#define LN_camellia_192_ecb "camellia-192-ecb" +#define NID_camellia_192_ecb 755 +#define OBJ_camellia_192_ecb OBJ_camellia,21L + +#define SN_camellia_192_ofb128 "CAMELLIA-192-OFB" +#define LN_camellia_192_ofb128 "camellia-192-ofb" +#define NID_camellia_192_ofb128 767 +#define OBJ_camellia_192_ofb128 OBJ_camellia,23L + +#define SN_camellia_192_cfb128 "CAMELLIA-192-CFB" +#define LN_camellia_192_cfb128 "camellia-192-cfb" +#define NID_camellia_192_cfb128 758 +#define OBJ_camellia_192_cfb128 OBJ_camellia,24L + +#define SN_camellia_192_gcm "CAMELLIA-192-GCM" +#define LN_camellia_192_gcm "camellia-192-gcm" +#define NID_camellia_192_gcm 965 +#define OBJ_camellia_192_gcm OBJ_camellia,26L + +#define SN_camellia_192_ccm "CAMELLIA-192-CCM" +#define LN_camellia_192_ccm "camellia-192-ccm" +#define NID_camellia_192_ccm 966 +#define OBJ_camellia_192_ccm OBJ_camellia,27L + +#define SN_camellia_192_ctr "CAMELLIA-192-CTR" +#define LN_camellia_192_ctr "camellia-192-ctr" +#define NID_camellia_192_ctr 967 +#define OBJ_camellia_192_ctr OBJ_camellia,29L + +#define SN_camellia_192_cmac "CAMELLIA-192-CMAC" +#define LN_camellia_192_cmac "camellia-192-cmac" +#define NID_camellia_192_cmac 968 +#define OBJ_camellia_192_cmac OBJ_camellia,30L + +#define SN_camellia_256_ecb "CAMELLIA-256-ECB" +#define LN_camellia_256_ecb "camellia-256-ecb" +#define NID_camellia_256_ecb 756 +#define OBJ_camellia_256_ecb OBJ_camellia,41L + +#define SN_camellia_256_ofb128 "CAMELLIA-256-OFB" +#define LN_camellia_256_ofb128 "camellia-256-ofb" +#define NID_camellia_256_ofb128 768 +#define OBJ_camellia_256_ofb128 OBJ_camellia,43L + +#define SN_camellia_256_cfb128 "CAMELLIA-256-CFB" +#define LN_camellia_256_cfb128 "camellia-256-cfb" +#define NID_camellia_256_cfb128 759 +#define OBJ_camellia_256_cfb128 OBJ_camellia,44L + +#define SN_camellia_256_gcm "CAMELLIA-256-GCM" +#define LN_camellia_256_gcm "camellia-256-gcm" +#define NID_camellia_256_gcm 969 +#define OBJ_camellia_256_gcm OBJ_camellia,46L + +#define SN_camellia_256_ccm "CAMELLIA-256-CCM" +#define LN_camellia_256_ccm "camellia-256-ccm" +#define NID_camellia_256_ccm 970 +#define OBJ_camellia_256_ccm OBJ_camellia,47L + +#define SN_camellia_256_ctr "CAMELLIA-256-CTR" +#define LN_camellia_256_ctr "camellia-256-ctr" +#define NID_camellia_256_ctr 971 +#define OBJ_camellia_256_ctr OBJ_camellia,49L + +#define SN_camellia_256_cmac "CAMELLIA-256-CMAC" +#define LN_camellia_256_cmac "camellia-256-cmac" +#define NID_camellia_256_cmac 972 +#define OBJ_camellia_256_cmac OBJ_camellia,50L + +#define SN_camellia_128_cfb1 "CAMELLIA-128-CFB1" +#define LN_camellia_128_cfb1 "camellia-128-cfb1" +#define NID_camellia_128_cfb1 760 + +#define SN_camellia_192_cfb1 "CAMELLIA-192-CFB1" +#define LN_camellia_192_cfb1 "camellia-192-cfb1" +#define NID_camellia_192_cfb1 761 + +#define SN_camellia_256_cfb1 "CAMELLIA-256-CFB1" +#define LN_camellia_256_cfb1 "camellia-256-cfb1" +#define NID_camellia_256_cfb1 762 + +#define SN_camellia_128_cfb8 "CAMELLIA-128-CFB8" +#define LN_camellia_128_cfb8 "camellia-128-cfb8" +#define NID_camellia_128_cfb8 763 + +#define SN_camellia_192_cfb8 "CAMELLIA-192-CFB8" +#define LN_camellia_192_cfb8 "camellia-192-cfb8" +#define NID_camellia_192_cfb8 764 + +#define SN_camellia_256_cfb8 "CAMELLIA-256-CFB8" +#define LN_camellia_256_cfb8 "camellia-256-cfb8" +#define NID_camellia_256_cfb8 765 + +#define SN_kisa "KISA" +#define LN_kisa "kisa" +#define NID_kisa 773 +#define OBJ_kisa OBJ_member_body,410L,200004L + +#define SN_seed_ecb "SEED-ECB" +#define LN_seed_ecb "seed-ecb" +#define NID_seed_ecb 776 +#define OBJ_seed_ecb OBJ_kisa,1L,3L + +#define SN_seed_cbc "SEED-CBC" +#define LN_seed_cbc "seed-cbc" +#define NID_seed_cbc 777 +#define OBJ_seed_cbc OBJ_kisa,1L,4L + +#define SN_seed_cfb128 "SEED-CFB" +#define LN_seed_cfb128 "seed-cfb" +#define NID_seed_cfb128 779 +#define OBJ_seed_cfb128 OBJ_kisa,1L,5L + +#define SN_seed_ofb128 "SEED-OFB" +#define LN_seed_ofb128 "seed-ofb" +#define NID_seed_ofb128 778 +#define OBJ_seed_ofb128 OBJ_kisa,1L,6L + +#define SN_hmac "HMAC" +#define LN_hmac "hmac" +#define NID_hmac 855 + +#define SN_cmac "CMAC" +#define LN_cmac "cmac" +#define NID_cmac 894 + +#define SN_rc4_hmac_md5 "RC4-HMAC-MD5" +#define LN_rc4_hmac_md5 "rc4-hmac-md5" +#define NID_rc4_hmac_md5 915 + +#define SN_aes_128_cbc_hmac_sha1 "AES-128-CBC-HMAC-SHA1" +#define LN_aes_128_cbc_hmac_sha1 "aes-128-cbc-hmac-sha1" +#define NID_aes_128_cbc_hmac_sha1 916 + +#define SN_aes_192_cbc_hmac_sha1 "AES-192-CBC-HMAC-SHA1" +#define LN_aes_192_cbc_hmac_sha1 "aes-192-cbc-hmac-sha1" +#define NID_aes_192_cbc_hmac_sha1 917 + +#define SN_aes_256_cbc_hmac_sha1 "AES-256-CBC-HMAC-SHA1" +#define LN_aes_256_cbc_hmac_sha1 "aes-256-cbc-hmac-sha1" +#define NID_aes_256_cbc_hmac_sha1 918 + +#define SN_aes_128_cbc_hmac_sha256 "AES-128-CBC-HMAC-SHA256" +#define LN_aes_128_cbc_hmac_sha256 "aes-128-cbc-hmac-sha256" +#define NID_aes_128_cbc_hmac_sha256 948 + +#define SN_aes_192_cbc_hmac_sha256 "AES-192-CBC-HMAC-SHA256" +#define LN_aes_192_cbc_hmac_sha256 "aes-192-cbc-hmac-sha256" +#define NID_aes_192_cbc_hmac_sha256 949 + +#define SN_aes_256_cbc_hmac_sha256 "AES-256-CBC-HMAC-SHA256" +#define LN_aes_256_cbc_hmac_sha256 "aes-256-cbc-hmac-sha256" +#define NID_aes_256_cbc_hmac_sha256 950 + +#define SN_chacha20_poly1305 "ChaCha20-Poly1305" +#define LN_chacha20_poly1305 "chacha20-poly1305" +#define NID_chacha20_poly1305 1018 + +#define SN_chacha20 "ChaCha20" +#define LN_chacha20 "chacha20" +#define NID_chacha20 1019 + +#define SN_dhpublicnumber "dhpublicnumber" +#define LN_dhpublicnumber "X9.42 DH" +#define NID_dhpublicnumber 920 +#define OBJ_dhpublicnumber OBJ_ISO_US,10046L,2L,1L + +#define SN_brainpoolP160r1 "brainpoolP160r1" +#define NID_brainpoolP160r1 921 +#define OBJ_brainpoolP160r1 1L,3L,36L,3L,3L,2L,8L,1L,1L,1L + +#define SN_brainpoolP160t1 "brainpoolP160t1" +#define NID_brainpoolP160t1 922 +#define OBJ_brainpoolP160t1 1L,3L,36L,3L,3L,2L,8L,1L,1L,2L + +#define SN_brainpoolP192r1 "brainpoolP192r1" +#define NID_brainpoolP192r1 923 +#define OBJ_brainpoolP192r1 1L,3L,36L,3L,3L,2L,8L,1L,1L,3L + +#define SN_brainpoolP192t1 "brainpoolP192t1" +#define NID_brainpoolP192t1 924 +#define OBJ_brainpoolP192t1 1L,3L,36L,3L,3L,2L,8L,1L,1L,4L + +#define SN_brainpoolP224r1 "brainpoolP224r1" +#define NID_brainpoolP224r1 925 +#define OBJ_brainpoolP224r1 1L,3L,36L,3L,3L,2L,8L,1L,1L,5L + +#define SN_brainpoolP224t1 "brainpoolP224t1" +#define NID_brainpoolP224t1 926 +#define OBJ_brainpoolP224t1 1L,3L,36L,3L,3L,2L,8L,1L,1L,6L + +#define SN_brainpoolP256r1 "brainpoolP256r1" +#define NID_brainpoolP256r1 927 +#define OBJ_brainpoolP256r1 1L,3L,36L,3L,3L,2L,8L,1L,1L,7L + +#define SN_brainpoolP256t1 "brainpoolP256t1" +#define NID_brainpoolP256t1 928 +#define OBJ_brainpoolP256t1 1L,3L,36L,3L,3L,2L,8L,1L,1L,8L + +#define SN_brainpoolP320r1 "brainpoolP320r1" +#define NID_brainpoolP320r1 929 +#define OBJ_brainpoolP320r1 1L,3L,36L,3L,3L,2L,8L,1L,1L,9L + +#define SN_brainpoolP320t1 "brainpoolP320t1" +#define NID_brainpoolP320t1 930 +#define OBJ_brainpoolP320t1 1L,3L,36L,3L,3L,2L,8L,1L,1L,10L + +#define SN_brainpoolP384r1 "brainpoolP384r1" +#define NID_brainpoolP384r1 931 +#define OBJ_brainpoolP384r1 1L,3L,36L,3L,3L,2L,8L,1L,1L,11L + +#define SN_brainpoolP384t1 "brainpoolP384t1" +#define NID_brainpoolP384t1 932 +#define OBJ_brainpoolP384t1 1L,3L,36L,3L,3L,2L,8L,1L,1L,12L + +#define SN_brainpoolP512r1 "brainpoolP512r1" +#define NID_brainpoolP512r1 933 +#define OBJ_brainpoolP512r1 1L,3L,36L,3L,3L,2L,8L,1L,1L,13L + +#define SN_brainpoolP512t1 "brainpoolP512t1" +#define NID_brainpoolP512t1 934 +#define OBJ_brainpoolP512t1 1L,3L,36L,3L,3L,2L,8L,1L,1L,14L + +#define OBJ_x9_63_scheme 1L,3L,133L,16L,840L,63L,0L + +#define OBJ_secg_scheme OBJ_certicom_arc,1L + +#define SN_dhSinglePass_stdDH_sha1kdf_scheme "dhSinglePass-stdDH-sha1kdf-scheme" +#define NID_dhSinglePass_stdDH_sha1kdf_scheme 936 +#define OBJ_dhSinglePass_stdDH_sha1kdf_scheme OBJ_x9_63_scheme,2L + +#define SN_dhSinglePass_stdDH_sha224kdf_scheme "dhSinglePass-stdDH-sha224kdf-scheme" +#define NID_dhSinglePass_stdDH_sha224kdf_scheme 937 +#define OBJ_dhSinglePass_stdDH_sha224kdf_scheme OBJ_secg_scheme,11L,0L + +#define SN_dhSinglePass_stdDH_sha256kdf_scheme "dhSinglePass-stdDH-sha256kdf-scheme" +#define NID_dhSinglePass_stdDH_sha256kdf_scheme 938 +#define OBJ_dhSinglePass_stdDH_sha256kdf_scheme OBJ_secg_scheme,11L,1L + +#define SN_dhSinglePass_stdDH_sha384kdf_scheme "dhSinglePass-stdDH-sha384kdf-scheme" +#define NID_dhSinglePass_stdDH_sha384kdf_scheme 939 +#define OBJ_dhSinglePass_stdDH_sha384kdf_scheme OBJ_secg_scheme,11L,2L + +#define SN_dhSinglePass_stdDH_sha512kdf_scheme "dhSinglePass-stdDH-sha512kdf-scheme" +#define NID_dhSinglePass_stdDH_sha512kdf_scheme 940 +#define OBJ_dhSinglePass_stdDH_sha512kdf_scheme OBJ_secg_scheme,11L,3L + +#define SN_dhSinglePass_cofactorDH_sha1kdf_scheme "dhSinglePass-cofactorDH-sha1kdf-scheme" +#define NID_dhSinglePass_cofactorDH_sha1kdf_scheme 941 +#define OBJ_dhSinglePass_cofactorDH_sha1kdf_scheme OBJ_x9_63_scheme,3L + +#define SN_dhSinglePass_cofactorDH_sha224kdf_scheme "dhSinglePass-cofactorDH-sha224kdf-scheme" +#define NID_dhSinglePass_cofactorDH_sha224kdf_scheme 942 +#define OBJ_dhSinglePass_cofactorDH_sha224kdf_scheme OBJ_secg_scheme,14L,0L + +#define SN_dhSinglePass_cofactorDH_sha256kdf_scheme "dhSinglePass-cofactorDH-sha256kdf-scheme" +#define NID_dhSinglePass_cofactorDH_sha256kdf_scheme 943 +#define OBJ_dhSinglePass_cofactorDH_sha256kdf_scheme OBJ_secg_scheme,14L,1L + +#define SN_dhSinglePass_cofactorDH_sha384kdf_scheme "dhSinglePass-cofactorDH-sha384kdf-scheme" +#define NID_dhSinglePass_cofactorDH_sha384kdf_scheme 944 +#define OBJ_dhSinglePass_cofactorDH_sha384kdf_scheme OBJ_secg_scheme,14L,2L + +#define SN_dhSinglePass_cofactorDH_sha512kdf_scheme "dhSinglePass-cofactorDH-sha512kdf-scheme" +#define NID_dhSinglePass_cofactorDH_sha512kdf_scheme 945 +#define OBJ_dhSinglePass_cofactorDH_sha512kdf_scheme OBJ_secg_scheme,14L,3L + +#define SN_dh_std_kdf "dh-std-kdf" +#define NID_dh_std_kdf 946 + +#define SN_dh_cofactor_kdf "dh-cofactor-kdf" +#define NID_dh_cofactor_kdf 947 + +#define SN_ct_precert_scts "ct_precert_scts" +#define LN_ct_precert_scts "CT Precertificate SCTs" +#define NID_ct_precert_scts 951 +#define OBJ_ct_precert_scts 1L,3L,6L,1L,4L,1L,11129L,2L,4L,2L + +#define SN_ct_precert_poison "ct_precert_poison" +#define LN_ct_precert_poison "CT Precertificate Poison" +#define NID_ct_precert_poison 952 +#define OBJ_ct_precert_poison 1L,3L,6L,1L,4L,1L,11129L,2L,4L,3L + +#define SN_ct_precert_signer "ct_precert_signer" +#define LN_ct_precert_signer "CT Precertificate Signer" +#define NID_ct_precert_signer 953 +#define OBJ_ct_precert_signer 1L,3L,6L,1L,4L,1L,11129L,2L,4L,4L + +#define SN_ct_cert_scts "ct_cert_scts" +#define LN_ct_cert_scts "CT Certificate SCTs" +#define NID_ct_cert_scts 954 +#define OBJ_ct_cert_scts 1L,3L,6L,1L,4L,1L,11129L,2L,4L,5L + +#define SN_jurisdictionLocalityName "jurisdictionL" +#define LN_jurisdictionLocalityName "jurisdictionLocalityName" +#define NID_jurisdictionLocalityName 955 +#define OBJ_jurisdictionLocalityName 1L,3L,6L,1L,4L,1L,311L,60L,2L,1L,1L + +#define SN_jurisdictionStateOrProvinceName "jurisdictionST" +#define LN_jurisdictionStateOrProvinceName "jurisdictionStateOrProvinceName" +#define NID_jurisdictionStateOrProvinceName 956 +#define OBJ_jurisdictionStateOrProvinceName 1L,3L,6L,1L,4L,1L,311L,60L,2L,1L,2L + +#define SN_jurisdictionCountryName "jurisdictionC" +#define LN_jurisdictionCountryName "jurisdictionCountryName" +#define NID_jurisdictionCountryName 957 +#define OBJ_jurisdictionCountryName 1L,3L,6L,1L,4L,1L,311L,60L,2L,1L,3L + +#define SN_id_scrypt "id-scrypt" +#define NID_id_scrypt 973 +#define OBJ_id_scrypt 1L,3L,6L,1L,4L,1L,11591L,4L,11L + +#define SN_tls1_prf "TLS1-PRF" +#define LN_tls1_prf "tls1-prf" +#define NID_tls1_prf 1021 + +#define SN_hkdf "HKDF" +#define LN_hkdf "hkdf" +#define NID_hkdf 1036 + +#define SN_id_pkinit "id-pkinit" +#define NID_id_pkinit 1031 +#define OBJ_id_pkinit 1L,3L,6L,1L,5L,2L,3L + +#define SN_pkInitClientAuth "pkInitClientAuth" +#define LN_pkInitClientAuth "PKINIT Client Auth" +#define NID_pkInitClientAuth 1032 +#define OBJ_pkInitClientAuth OBJ_id_pkinit,4L + +#define SN_pkInitKDC "pkInitKDC" +#define LN_pkInitKDC "Signing KDC Response" +#define NID_pkInitKDC 1033 +#define OBJ_pkInitKDC OBJ_id_pkinit,5L + +#define SN_X25519 "X25519" +#define NID_X25519 1034 +#define OBJ_X25519 1L,3L,101L,110L + +#define SN_X448 "X448" +#define NID_X448 1035 +#define OBJ_X448 1L,3L,101L,111L + +#define SN_kx_rsa "KxRSA" +#define LN_kx_rsa "kx-rsa" +#define NID_kx_rsa 1037 + +#define SN_kx_ecdhe "KxECDHE" +#define LN_kx_ecdhe "kx-ecdhe" +#define NID_kx_ecdhe 1038 + +#define SN_kx_dhe "KxDHE" +#define LN_kx_dhe "kx-dhe" +#define NID_kx_dhe 1039 + +#define SN_kx_ecdhe_psk "KxECDHE-PSK" +#define LN_kx_ecdhe_psk "kx-ecdhe-psk" +#define NID_kx_ecdhe_psk 1040 + +#define SN_kx_dhe_psk "KxDHE-PSK" +#define LN_kx_dhe_psk "kx-dhe-psk" +#define NID_kx_dhe_psk 1041 + +#define SN_kx_rsa_psk "KxRSA_PSK" +#define LN_kx_rsa_psk "kx-rsa-psk" +#define NID_kx_rsa_psk 1042 + +#define SN_kx_psk "KxPSK" +#define LN_kx_psk "kx-psk" +#define NID_kx_psk 1043 + +#define SN_kx_srp "KxSRP" +#define LN_kx_srp "kx-srp" +#define NID_kx_srp 1044 + +#define SN_kx_gost "KxGOST" +#define LN_kx_gost "kx-gost" +#define NID_kx_gost 1045 + +#define SN_auth_rsa "AuthRSA" +#define LN_auth_rsa "auth-rsa" +#define NID_auth_rsa 1046 + +#define SN_auth_ecdsa "AuthECDSA" +#define LN_auth_ecdsa "auth-ecdsa" +#define NID_auth_ecdsa 1047 + +#define SN_auth_psk "AuthPSK" +#define LN_auth_psk "auth-psk" +#define NID_auth_psk 1048 + +#define SN_auth_dss "AuthDSS" +#define LN_auth_dss "auth-dss" +#define NID_auth_dss 1049 + +#define SN_auth_gost01 "AuthGOST01" +#define LN_auth_gost01 "auth-gost01" +#define NID_auth_gost01 1050 + +#define SN_auth_gost12 "AuthGOST12" +#define LN_auth_gost12 "auth-gost12" +#define NID_auth_gost12 1051 + +#define SN_auth_srp "AuthSRP" +#define LN_auth_srp "auth-srp" +#define NID_auth_srp 1052 + +#define SN_auth_null "AuthNULL" +#define LN_auth_null "auth-null" +#define NID_auth_null 1053 diff --git a/mobile/ios/FxA/FxA/include/openssl/objects.h b/mobile/ios/FxA/FxA/include/openssl/objects.h new file mode 100644 index 0000000000..09d614ffc0 --- /dev/null +++ b/mobile/ios/FxA/FxA/include/openssl/objects.h @@ -0,0 +1,1097 @@ +/* + * Copyright 1995-2016 The OpenSSL Project Authors. All Rights Reserved. + * + * Licensed under the OpenSSL license (the "License"). You may not use + * this file except in compliance with the License. You can obtain a copy + * in the file LICENSE in the source distribution or at + * https://www.openssl.org/source/license.html + */ + +#ifndef HEADER_OBJECTS_H +# define HEADER_OBJECTS_H + +# define USE_OBJ_MAC + +# ifdef USE_OBJ_MAC +# include +# else +# define SN_undef "UNDEF" +# define LN_undef "undefined" +# define NID_undef 0 +# define OBJ_undef 0L + +# define SN_Algorithm "Algorithm" +# define LN_algorithm "algorithm" +# define NID_algorithm 38 +# define OBJ_algorithm 1L,3L,14L,3L,2L + +# define LN_rsadsi "rsadsi" +# define NID_rsadsi 1 +# define OBJ_rsadsi 1L,2L,840L,113549L + +# define LN_pkcs "pkcs" +# define NID_pkcs 2 +# define OBJ_pkcs OBJ_rsadsi,1L + +# define SN_md2 "MD2" +# define LN_md2 "md2" +# define NID_md2 3 +# define OBJ_md2 OBJ_rsadsi,2L,2L + +# define SN_md5 "MD5" +# define LN_md5 "md5" +# define NID_md5 4 +# define OBJ_md5 OBJ_rsadsi,2L,5L + +# define SN_rc4 "RC4" +# define LN_rc4 "rc4" +# define NID_rc4 5 +# define OBJ_rc4 OBJ_rsadsi,3L,4L + +# define LN_rsaEncryption "rsaEncryption" +# define NID_rsaEncryption 6 +# define OBJ_rsaEncryption OBJ_pkcs,1L,1L + +# define SN_md2WithRSAEncryption "RSA-MD2" +# define LN_md2WithRSAEncryption "md2WithRSAEncryption" +# define NID_md2WithRSAEncryption 7 +# define OBJ_md2WithRSAEncryption OBJ_pkcs,1L,2L + +# define SN_md5WithRSAEncryption "RSA-MD5" +# define LN_md5WithRSAEncryption "md5WithRSAEncryption" +# define NID_md5WithRSAEncryption 8 +# define OBJ_md5WithRSAEncryption OBJ_pkcs,1L,4L + +# define SN_pbeWithMD2AndDES_CBC "PBE-MD2-DES" +# define LN_pbeWithMD2AndDES_CBC "pbeWithMD2AndDES-CBC" +# define NID_pbeWithMD2AndDES_CBC 9 +# define OBJ_pbeWithMD2AndDES_CBC OBJ_pkcs,5L,1L + +# define SN_pbeWithMD5AndDES_CBC "PBE-MD5-DES" +# define LN_pbeWithMD5AndDES_CBC "pbeWithMD5AndDES-CBC" +# define NID_pbeWithMD5AndDES_CBC 10 +# define OBJ_pbeWithMD5AndDES_CBC OBJ_pkcs,5L,3L + +# define LN_X500 "X500" +# define NID_X500 11 +# define OBJ_X500 2L,5L + +# define LN_X509 "X509" +# define NID_X509 12 +# define OBJ_X509 OBJ_X500,4L + +# define SN_commonName "CN" +# define LN_commonName "commonName" +# define NID_commonName 13 +# define OBJ_commonName OBJ_X509,3L + +# define SN_countryName "C" +# define LN_countryName "countryName" +# define NID_countryName 14 +# define OBJ_countryName OBJ_X509,6L + +# define SN_localityName "L" +# define LN_localityName "localityName" +# define NID_localityName 15 +# define OBJ_localityName OBJ_X509,7L + +/* Postal Address? PA */ + +/* should be "ST" (rfc1327) but MS uses 'S' */ +# define SN_stateOrProvinceName "ST" +# define LN_stateOrProvinceName "stateOrProvinceName" +# define NID_stateOrProvinceName 16 +# define OBJ_stateOrProvinceName OBJ_X509,8L + +# define SN_organizationName "O" +# define LN_organizationName "organizationName" +# define NID_organizationName 17 +# define OBJ_organizationName OBJ_X509,10L + +# define SN_organizationalUnitName "OU" +# define LN_organizationalUnitName "organizationalUnitName" +# define NID_organizationalUnitName 18 +# define OBJ_organizationalUnitName OBJ_X509,11L + +# define SN_rsa "RSA" +# define LN_rsa "rsa" +# define NID_rsa 19 +# define OBJ_rsa OBJ_X500,8L,1L,1L + +# define LN_pkcs7 "pkcs7" +# define NID_pkcs7 20 +# define OBJ_pkcs7 OBJ_pkcs,7L + +# define LN_pkcs7_data "pkcs7-data" +# define NID_pkcs7_data 21 +# define OBJ_pkcs7_data OBJ_pkcs7,1L + +# define LN_pkcs7_signed "pkcs7-signedData" +# define NID_pkcs7_signed 22 +# define OBJ_pkcs7_signed OBJ_pkcs7,2L + +# define LN_pkcs7_enveloped "pkcs7-envelopedData" +# define NID_pkcs7_enveloped 23 +# define OBJ_pkcs7_enveloped OBJ_pkcs7,3L + +# define LN_pkcs7_signedAndEnveloped "pkcs7-signedAndEnvelopedData" +# define NID_pkcs7_signedAndEnveloped 24 +# define OBJ_pkcs7_signedAndEnveloped OBJ_pkcs7,4L + +# define LN_pkcs7_digest "pkcs7-digestData" +# define NID_pkcs7_digest 25 +# define OBJ_pkcs7_digest OBJ_pkcs7,5L + +# define LN_pkcs7_encrypted "pkcs7-encryptedData" +# define NID_pkcs7_encrypted 26 +# define OBJ_pkcs7_encrypted OBJ_pkcs7,6L + +# define LN_pkcs3 "pkcs3" +# define NID_pkcs3 27 +# define OBJ_pkcs3 OBJ_pkcs,3L + +# define LN_dhKeyAgreement "dhKeyAgreement" +# define NID_dhKeyAgreement 28 +# define OBJ_dhKeyAgreement OBJ_pkcs3,1L + +# define SN_des_ecb "DES-ECB" +# define LN_des_ecb "des-ecb" +# define NID_des_ecb 29 +# define OBJ_des_ecb OBJ_algorithm,6L + +# define SN_des_cfb64 "DES-CFB" +# define LN_des_cfb64 "des-cfb" +# define NID_des_cfb64 30 +/* IV + num */ +# define OBJ_des_cfb64 OBJ_algorithm,9L + +# define SN_des_cbc "DES-CBC" +# define LN_des_cbc "des-cbc" +# define NID_des_cbc 31 +/* IV */ +# define OBJ_des_cbc OBJ_algorithm,7L + +# define SN_des_ede "DES-EDE" +# define LN_des_ede "des-ede" +# define NID_des_ede 32 +/* ?? */ +# define OBJ_des_ede OBJ_algorithm,17L + +# define SN_des_ede3 "DES-EDE3" +# define LN_des_ede3 "des-ede3" +# define NID_des_ede3 33 + +# define SN_idea_cbc "IDEA-CBC" +# define LN_idea_cbc "idea-cbc" +# define NID_idea_cbc 34 +# define OBJ_idea_cbc 1L,3L,6L,1L,4L,1L,188L,7L,1L,1L,2L + +# define SN_idea_cfb64 "IDEA-CFB" +# define LN_idea_cfb64 "idea-cfb" +# define NID_idea_cfb64 35 + +# define SN_idea_ecb "IDEA-ECB" +# define LN_idea_ecb "idea-ecb" +# define NID_idea_ecb 36 + +# define SN_rc2_cbc "RC2-CBC" +# define LN_rc2_cbc "rc2-cbc" +# define NID_rc2_cbc 37 +# define OBJ_rc2_cbc OBJ_rsadsi,3L,2L + +# define SN_rc2_ecb "RC2-ECB" +# define LN_rc2_ecb "rc2-ecb" +# define NID_rc2_ecb 38 + +# define SN_rc2_cfb64 "RC2-CFB" +# define LN_rc2_cfb64 "rc2-cfb" +# define NID_rc2_cfb64 39 + +# define SN_rc2_ofb64 "RC2-OFB" +# define LN_rc2_ofb64 "rc2-ofb" +# define NID_rc2_ofb64 40 + +# define SN_sha "SHA" +# define LN_sha "sha" +# define NID_sha 41 +# define OBJ_sha OBJ_algorithm,18L + +# define SN_shaWithRSAEncryption "RSA-SHA" +# define LN_shaWithRSAEncryption "shaWithRSAEncryption" +# define NID_shaWithRSAEncryption 42 +# define OBJ_shaWithRSAEncryption OBJ_algorithm,15L + +# define SN_des_ede_cbc "DES-EDE-CBC" +# define LN_des_ede_cbc "des-ede-cbc" +# define NID_des_ede_cbc 43 + +# define SN_des_ede3_cbc "DES-EDE3-CBC" +# define LN_des_ede3_cbc "des-ede3-cbc" +# define NID_des_ede3_cbc 44 +# define OBJ_des_ede3_cbc OBJ_rsadsi,3L,7L + +# define SN_des_ofb64 "DES-OFB" +# define LN_des_ofb64 "des-ofb" +# define NID_des_ofb64 45 +# define OBJ_des_ofb64 OBJ_algorithm,8L + +# define SN_idea_ofb64 "IDEA-OFB" +# define LN_idea_ofb64 "idea-ofb" +# define NID_idea_ofb64 46 + +# define LN_pkcs9 "pkcs9" +# define NID_pkcs9 47 +# define OBJ_pkcs9 OBJ_pkcs,9L + +# define SN_pkcs9_emailAddress "Email" +# define LN_pkcs9_emailAddress "emailAddress" +# define NID_pkcs9_emailAddress 48 +# define OBJ_pkcs9_emailAddress OBJ_pkcs9,1L + +# define LN_pkcs9_unstructuredName "unstructuredName" +# define NID_pkcs9_unstructuredName 49 +# define OBJ_pkcs9_unstructuredName OBJ_pkcs9,2L + +# define LN_pkcs9_contentType "contentType" +# define NID_pkcs9_contentType 50 +# define OBJ_pkcs9_contentType OBJ_pkcs9,3L + +# define LN_pkcs9_messageDigest "messageDigest" +# define NID_pkcs9_messageDigest 51 +# define OBJ_pkcs9_messageDigest OBJ_pkcs9,4L + +# define LN_pkcs9_signingTime "signingTime" +# define NID_pkcs9_signingTime 52 +# define OBJ_pkcs9_signingTime OBJ_pkcs9,5L + +# define LN_pkcs9_countersignature "countersignature" +# define NID_pkcs9_countersignature 53 +# define OBJ_pkcs9_countersignature OBJ_pkcs9,6L + +# define LN_pkcs9_challengePassword "challengePassword" +# define NID_pkcs9_challengePassword 54 +# define OBJ_pkcs9_challengePassword OBJ_pkcs9,7L + +# define LN_pkcs9_unstructuredAddress "unstructuredAddress" +# define NID_pkcs9_unstructuredAddress 55 +# define OBJ_pkcs9_unstructuredAddress OBJ_pkcs9,8L + +# define LN_pkcs9_extCertAttributes "extendedCertificateAttributes" +# define NID_pkcs9_extCertAttributes 56 +# define OBJ_pkcs9_extCertAttributes OBJ_pkcs9,9L + +# define SN_netscape "Netscape" +# define LN_netscape "Netscape Communications Corp." +# define NID_netscape 57 +# define OBJ_netscape 2L,16L,840L,1L,113730L + +# define SN_netscape_cert_extension "nsCertExt" +# define LN_netscape_cert_extension "Netscape Certificate Extension" +# define NID_netscape_cert_extension 58 +# define OBJ_netscape_cert_extension OBJ_netscape,1L + +# define SN_netscape_data_type "nsDataType" +# define LN_netscape_data_type "Netscape Data Type" +# define NID_netscape_data_type 59 +# define OBJ_netscape_data_type OBJ_netscape,2L + +# define SN_des_ede_cfb64 "DES-EDE-CFB" +# define LN_des_ede_cfb64 "des-ede-cfb" +# define NID_des_ede_cfb64 60 + +# define SN_des_ede3_cfb64 "DES-EDE3-CFB" +# define LN_des_ede3_cfb64 "des-ede3-cfb" +# define NID_des_ede3_cfb64 61 + +# define SN_des_ede_ofb64 "DES-EDE-OFB" +# define LN_des_ede_ofb64 "des-ede-ofb" +# define NID_des_ede_ofb64 62 + +# define SN_des_ede3_ofb64 "DES-EDE3-OFB" +# define LN_des_ede3_ofb64 "des-ede3-ofb" +# define NID_des_ede3_ofb64 63 + +/* I'm not sure about the object ID */ +# define SN_sha1 "SHA1" +# define LN_sha1 "sha1" +# define NID_sha1 64 +# define OBJ_sha1 OBJ_algorithm,26L +/* 28 Jun 1996 - eay */ +/* #define OBJ_sha1 1L,3L,14L,2L,26L,05L <- wrong */ + +# define SN_sha1WithRSAEncryption "RSA-SHA1" +# define LN_sha1WithRSAEncryption "sha1WithRSAEncryption" +# define NID_sha1WithRSAEncryption 65 +# define OBJ_sha1WithRSAEncryption OBJ_pkcs,1L,5L + +# define SN_dsaWithSHA "DSA-SHA" +# define LN_dsaWithSHA "dsaWithSHA" +# define NID_dsaWithSHA 66 +# define OBJ_dsaWithSHA OBJ_algorithm,13L + +# define SN_dsa_2 "DSA-old" +# define LN_dsa_2 "dsaEncryption-old" +# define NID_dsa_2 67 +# define OBJ_dsa_2 OBJ_algorithm,12L + +/* proposed by microsoft to RSA */ +# define SN_pbeWithSHA1AndRC2_CBC "PBE-SHA1-RC2-64" +# define LN_pbeWithSHA1AndRC2_CBC "pbeWithSHA1AndRC2-CBC" +# define NID_pbeWithSHA1AndRC2_CBC 68 +# define OBJ_pbeWithSHA1AndRC2_CBC OBJ_pkcs,5L,11L + +/* + * proposed by microsoft to RSA as pbeWithSHA1AndRC4: it is now defined + * explicitly in PKCS#5 v2.0 as id-PBKDF2 which is something completely + * different. + */ +# define LN_id_pbkdf2 "PBKDF2" +# define NID_id_pbkdf2 69 +# define OBJ_id_pbkdf2 OBJ_pkcs,5L,12L + +# define SN_dsaWithSHA1_2 "DSA-SHA1-old" +# define LN_dsaWithSHA1_2 "dsaWithSHA1-old" +# define NID_dsaWithSHA1_2 70 +/* Got this one from 'sdn706r20.pdf' which is actually an NSA document :-) */ +# define OBJ_dsaWithSHA1_2 OBJ_algorithm,27L + +# define SN_netscape_cert_type "nsCertType" +# define LN_netscape_cert_type "Netscape Cert Type" +# define NID_netscape_cert_type 71 +# define OBJ_netscape_cert_type OBJ_netscape_cert_extension,1L + +# define SN_netscape_base_url "nsBaseUrl" +# define LN_netscape_base_url "Netscape Base Url" +# define NID_netscape_base_url 72 +# define OBJ_netscape_base_url OBJ_netscape_cert_extension,2L + +# define SN_netscape_revocation_url "nsRevocationUrl" +# define LN_netscape_revocation_url "Netscape Revocation Url" +# define NID_netscape_revocation_url 73 +# define OBJ_netscape_revocation_url OBJ_netscape_cert_extension,3L + +# define SN_netscape_ca_revocation_url "nsCaRevocationUrl" +# define LN_netscape_ca_revocation_url "Netscape CA Revocation Url" +# define NID_netscape_ca_revocation_url 74 +# define OBJ_netscape_ca_revocation_url OBJ_netscape_cert_extension,4L + +# define SN_netscape_renewal_url "nsRenewalUrl" +# define LN_netscape_renewal_url "Netscape Renewal Url" +# define NID_netscape_renewal_url 75 +# define OBJ_netscape_renewal_url OBJ_netscape_cert_extension,7L + +# define SN_netscape_ca_policy_url "nsCaPolicyUrl" +# define LN_netscape_ca_policy_url "Netscape CA Policy Url" +# define NID_netscape_ca_policy_url 76 +# define OBJ_netscape_ca_policy_url OBJ_netscape_cert_extension,8L + +# define SN_netscape_ssl_server_name "nsSslServerName" +# define LN_netscape_ssl_server_name "Netscape SSL Server Name" +# define NID_netscape_ssl_server_name 77 +# define OBJ_netscape_ssl_server_name OBJ_netscape_cert_extension,12L + +# define SN_netscape_comment "nsComment" +# define LN_netscape_comment "Netscape Comment" +# define NID_netscape_comment 78 +# define OBJ_netscape_comment OBJ_netscape_cert_extension,13L + +# define SN_netscape_cert_sequence "nsCertSequence" +# define LN_netscape_cert_sequence "Netscape Certificate Sequence" +# define NID_netscape_cert_sequence 79 +# define OBJ_netscape_cert_sequence OBJ_netscape_data_type,5L + +# define SN_desx_cbc "DESX-CBC" +# define LN_desx_cbc "desx-cbc" +# define NID_desx_cbc 80 + +# define SN_id_ce "id-ce" +# define NID_id_ce 81 +# define OBJ_id_ce 2L,5L,29L + +# define SN_subject_key_identifier "subjectKeyIdentifier" +# define LN_subject_key_identifier "X509v3 Subject Key Identifier" +# define NID_subject_key_identifier 82 +# define OBJ_subject_key_identifier OBJ_id_ce,14L + +# define SN_key_usage "keyUsage" +# define LN_key_usage "X509v3 Key Usage" +# define NID_key_usage 83 +# define OBJ_key_usage OBJ_id_ce,15L + +# define SN_private_key_usage_period "privateKeyUsagePeriod" +# define LN_private_key_usage_period "X509v3 Private Key Usage Period" +# define NID_private_key_usage_period 84 +# define OBJ_private_key_usage_period OBJ_id_ce,16L + +# define SN_subject_alt_name "subjectAltName" +# define LN_subject_alt_name "X509v3 Subject Alternative Name" +# define NID_subject_alt_name 85 +# define OBJ_subject_alt_name OBJ_id_ce,17L + +# define SN_issuer_alt_name "issuerAltName" +# define LN_issuer_alt_name "X509v3 Issuer Alternative Name" +# define NID_issuer_alt_name 86 +# define OBJ_issuer_alt_name OBJ_id_ce,18L + +# define SN_basic_constraints "basicConstraints" +# define LN_basic_constraints "X509v3 Basic Constraints" +# define NID_basic_constraints 87 +# define OBJ_basic_constraints OBJ_id_ce,19L + +# define SN_crl_number "crlNumber" +# define LN_crl_number "X509v3 CRL Number" +# define NID_crl_number 88 +# define OBJ_crl_number OBJ_id_ce,20L + +# define SN_certificate_policies "certificatePolicies" +# define LN_certificate_policies "X509v3 Certificate Policies" +# define NID_certificate_policies 89 +# define OBJ_certificate_policies OBJ_id_ce,32L + +# define SN_authority_key_identifier "authorityKeyIdentifier" +# define LN_authority_key_identifier "X509v3 Authority Key Identifier" +# define NID_authority_key_identifier 90 +# define OBJ_authority_key_identifier OBJ_id_ce,35L + +# define SN_bf_cbc "BF-CBC" +# define LN_bf_cbc "bf-cbc" +# define NID_bf_cbc 91 +# define OBJ_bf_cbc 1L,3L,6L,1L,4L,1L,3029L,1L,2L + +# define SN_bf_ecb "BF-ECB" +# define LN_bf_ecb "bf-ecb" +# define NID_bf_ecb 92 + +# define SN_bf_cfb64 "BF-CFB" +# define LN_bf_cfb64 "bf-cfb" +# define NID_bf_cfb64 93 + +# define SN_bf_ofb64 "BF-OFB" +# define LN_bf_ofb64 "bf-ofb" +# define NID_bf_ofb64 94 + +# define SN_mdc2 "MDC2" +# define LN_mdc2 "mdc2" +# define NID_mdc2 95 +# define OBJ_mdc2 2L,5L,8L,3L,101L +/* An alternative? 1L,3L,14L,3L,2L,19L */ + +# define SN_mdc2WithRSA "RSA-MDC2" +# define LN_mdc2WithRSA "mdc2withRSA" +# define NID_mdc2WithRSA 96 +# define OBJ_mdc2WithRSA 2L,5L,8L,3L,100L + +# define SN_rc4_40 "RC4-40" +# define LN_rc4_40 "rc4-40" +# define NID_rc4_40 97 + +# define SN_rc2_40_cbc "RC2-40-CBC" +# define LN_rc2_40_cbc "rc2-40-cbc" +# define NID_rc2_40_cbc 98 + +# define SN_givenName "G" +# define LN_givenName "givenName" +# define NID_givenName 99 +# define OBJ_givenName OBJ_X509,42L + +# define SN_surname "S" +# define LN_surname "surname" +# define NID_surname 100 +# define OBJ_surname OBJ_X509,4L + +# define SN_initials "I" +# define LN_initials "initials" +# define NID_initials 101 +# define OBJ_initials OBJ_X509,43L + +# define SN_uniqueIdentifier "UID" +# define LN_uniqueIdentifier "uniqueIdentifier" +# define NID_uniqueIdentifier 102 +# define OBJ_uniqueIdentifier OBJ_X509,45L + +# define SN_crl_distribution_points "crlDistributionPoints" +# define LN_crl_distribution_points "X509v3 CRL Distribution Points" +# define NID_crl_distribution_points 103 +# define OBJ_crl_distribution_points OBJ_id_ce,31L + +# define SN_md5WithRSA "RSA-NP-MD5" +# define LN_md5WithRSA "md5WithRSA" +# define NID_md5WithRSA 104 +# define OBJ_md5WithRSA OBJ_algorithm,3L + +# define SN_serialNumber "SN" +# define LN_serialNumber "serialNumber" +# define NID_serialNumber 105 +# define OBJ_serialNumber OBJ_X509,5L + +# define SN_title "T" +# define LN_title "title" +# define NID_title 106 +# define OBJ_title OBJ_X509,12L + +# define SN_description "D" +# define LN_description "description" +# define NID_description 107 +# define OBJ_description OBJ_X509,13L + +/* CAST5 is CAST-128, I'm just sticking with the documentation */ +# define SN_cast5_cbc "CAST5-CBC" +# define LN_cast5_cbc "cast5-cbc" +# define NID_cast5_cbc 108 +# define OBJ_cast5_cbc 1L,2L,840L,113533L,7L,66L,10L + +# define SN_cast5_ecb "CAST5-ECB" +# define LN_cast5_ecb "cast5-ecb" +# define NID_cast5_ecb 109 + +# define SN_cast5_cfb64 "CAST5-CFB" +# define LN_cast5_cfb64 "cast5-cfb" +# define NID_cast5_cfb64 110 + +# define SN_cast5_ofb64 "CAST5-OFB" +# define LN_cast5_ofb64 "cast5-ofb" +# define NID_cast5_ofb64 111 + +# define LN_pbeWithMD5AndCast5_CBC "pbeWithMD5AndCast5CBC" +# define NID_pbeWithMD5AndCast5_CBC 112 +# define OBJ_pbeWithMD5AndCast5_CBC 1L,2L,840L,113533L,7L,66L,12L + +/*- + * This is one sun will soon be using :-( + * id-dsa-with-sha1 ID ::= { + * iso(1) member-body(2) us(840) x9-57 (10040) x9cm(4) 3 } + */ +# define SN_dsaWithSHA1 "DSA-SHA1" +# define LN_dsaWithSHA1 "dsaWithSHA1" +# define NID_dsaWithSHA1 113 +# define OBJ_dsaWithSHA1 1L,2L,840L,10040L,4L,3L + +# define NID_md5_sha1 114 +# define SN_md5_sha1 "MD5-SHA1" +# define LN_md5_sha1 "md5-sha1" + +# define SN_sha1WithRSA "RSA-SHA1-2" +# define LN_sha1WithRSA "sha1WithRSA" +# define NID_sha1WithRSA 115 +# define OBJ_sha1WithRSA OBJ_algorithm,29L + +# define SN_dsa "DSA" +# define LN_dsa "dsaEncryption" +# define NID_dsa 116 +# define OBJ_dsa 1L,2L,840L,10040L,4L,1L + +# define SN_ripemd160 "RIPEMD160" +# define LN_ripemd160 "ripemd160" +# define NID_ripemd160 117 +# define OBJ_ripemd160 1L,3L,36L,3L,2L,1L + +/* + * The name should actually be rsaSignatureWithripemd160, but I'm going to + * continue using the convention I'm using with the other ciphers + */ +# define SN_ripemd160WithRSA "RSA-RIPEMD160" +# define LN_ripemd160WithRSA "ripemd160WithRSA" +# define NID_ripemd160WithRSA 119 +# define OBJ_ripemd160WithRSA 1L,3L,36L,3L,3L,1L,2L + +/*- + * Taken from rfc2040 + * RC5_CBC_Parameters ::= SEQUENCE { + * version INTEGER (v1_0(16)), + * rounds INTEGER (8..127), + * blockSizeInBits INTEGER (64, 128), + * iv OCTET STRING OPTIONAL + * } + */ +# define SN_rc5_cbc "RC5-CBC" +# define LN_rc5_cbc "rc5-cbc" +# define NID_rc5_cbc 120 +# define OBJ_rc5_cbc OBJ_rsadsi,3L,8L + +# define SN_rc5_ecb "RC5-ECB" +# define LN_rc5_ecb "rc5-ecb" +# define NID_rc5_ecb 121 + +# define SN_rc5_cfb64 "RC5-CFB" +# define LN_rc5_cfb64 "rc5-cfb" +# define NID_rc5_cfb64 122 + +# define SN_rc5_ofb64 "RC5-OFB" +# define LN_rc5_ofb64 "rc5-ofb" +# define NID_rc5_ofb64 123 + +# define SN_rle_compression "RLE" +# define LN_rle_compression "run length compression" +# define NID_rle_compression 124 +# define OBJ_rle_compression 1L,1L,1L,1L,666L,1L + +# define SN_zlib_compression "ZLIB" +# define LN_zlib_compression "zlib compression" +# define NID_zlib_compression 125 +# define OBJ_zlib_compression 1L,1L,1L,1L,666L,2L + +# define SN_ext_key_usage "extendedKeyUsage" +# define LN_ext_key_usage "X509v3 Extended Key Usage" +# define NID_ext_key_usage 126 +# define OBJ_ext_key_usage OBJ_id_ce,37 + +# define SN_id_pkix "PKIX" +# define NID_id_pkix 127 +# define OBJ_id_pkix 1L,3L,6L,1L,5L,5L,7L + +# define SN_id_kp "id-kp" +# define NID_id_kp 128 +# define OBJ_id_kp OBJ_id_pkix,3L + +/* PKIX extended key usage OIDs */ + +# define SN_server_auth "serverAuth" +# define LN_server_auth "TLS Web Server Authentication" +# define NID_server_auth 129 +# define OBJ_server_auth OBJ_id_kp,1L + +# define SN_client_auth "clientAuth" +# define LN_client_auth "TLS Web Client Authentication" +# define NID_client_auth 130 +# define OBJ_client_auth OBJ_id_kp,2L + +# define SN_code_sign "codeSigning" +# define LN_code_sign "Code Signing" +# define NID_code_sign 131 +# define OBJ_code_sign OBJ_id_kp,3L + +# define SN_email_protect "emailProtection" +# define LN_email_protect "E-mail Protection" +# define NID_email_protect 132 +# define OBJ_email_protect OBJ_id_kp,4L + +# define SN_time_stamp "timeStamping" +# define LN_time_stamp "Time Stamping" +# define NID_time_stamp 133 +# define OBJ_time_stamp OBJ_id_kp,8L + +/* Additional extended key usage OIDs: Microsoft */ + +# define SN_ms_code_ind "msCodeInd" +# define LN_ms_code_ind "Microsoft Individual Code Signing" +# define NID_ms_code_ind 134 +# define OBJ_ms_code_ind 1L,3L,6L,1L,4L,1L,311L,2L,1L,21L + +# define SN_ms_code_com "msCodeCom" +# define LN_ms_code_com "Microsoft Commercial Code Signing" +# define NID_ms_code_com 135 +# define OBJ_ms_code_com 1L,3L,6L,1L,4L,1L,311L,2L,1L,22L + +# define SN_ms_ctl_sign "msCTLSign" +# define LN_ms_ctl_sign "Microsoft Trust List Signing" +# define NID_ms_ctl_sign 136 +# define OBJ_ms_ctl_sign 1L,3L,6L,1L,4L,1L,311L,10L,3L,1L + +# define SN_ms_sgc "msSGC" +# define LN_ms_sgc "Microsoft Server Gated Crypto" +# define NID_ms_sgc 137 +# define OBJ_ms_sgc 1L,3L,6L,1L,4L,1L,311L,10L,3L,3L + +# define SN_ms_efs "msEFS" +# define LN_ms_efs "Microsoft Encrypted File System" +# define NID_ms_efs 138 +# define OBJ_ms_efs 1L,3L,6L,1L,4L,1L,311L,10L,3L,4L + +/* Additional usage: Netscape */ + +# define SN_ns_sgc "nsSGC" +# define LN_ns_sgc "Netscape Server Gated Crypto" +# define NID_ns_sgc 139 +# define OBJ_ns_sgc OBJ_netscape,4L,1L + +# define SN_delta_crl "deltaCRL" +# define LN_delta_crl "X509v3 Delta CRL Indicator" +# define NID_delta_crl 140 +# define OBJ_delta_crl OBJ_id_ce,27L + +# define SN_crl_reason "CRLReason" +# define LN_crl_reason "CRL Reason Code" +# define NID_crl_reason 141 +# define OBJ_crl_reason OBJ_id_ce,21L + +# define SN_invalidity_date "invalidityDate" +# define LN_invalidity_date "Invalidity Date" +# define NID_invalidity_date 142 +# define OBJ_invalidity_date OBJ_id_ce,24L + +# define SN_sxnet "SXNetID" +# define LN_sxnet "Strong Extranet ID" +# define NID_sxnet 143 +# define OBJ_sxnet 1L,3L,101L,1L,4L,1L + +/* PKCS12 and related OBJECT IDENTIFIERS */ + +# define OBJ_pkcs12 OBJ_pkcs,12L +# define OBJ_pkcs12_pbeids OBJ_pkcs12, 1 + +# define SN_pbe_WithSHA1And128BitRC4 "PBE-SHA1-RC4-128" +# define LN_pbe_WithSHA1And128BitRC4 "pbeWithSHA1And128BitRC4" +# define NID_pbe_WithSHA1And128BitRC4 144 +# define OBJ_pbe_WithSHA1And128BitRC4 OBJ_pkcs12_pbeids, 1L + +# define SN_pbe_WithSHA1And40BitRC4 "PBE-SHA1-RC4-40" +# define LN_pbe_WithSHA1And40BitRC4 "pbeWithSHA1And40BitRC4" +# define NID_pbe_WithSHA1And40BitRC4 145 +# define OBJ_pbe_WithSHA1And40BitRC4 OBJ_pkcs12_pbeids, 2L + +# define SN_pbe_WithSHA1And3_Key_TripleDES_CBC "PBE-SHA1-3DES" +# define LN_pbe_WithSHA1And3_Key_TripleDES_CBC "pbeWithSHA1And3-KeyTripleDES-CBC" +# define NID_pbe_WithSHA1And3_Key_TripleDES_CBC 146 +# define OBJ_pbe_WithSHA1And3_Key_TripleDES_CBC OBJ_pkcs12_pbeids, 3L + +# define SN_pbe_WithSHA1And2_Key_TripleDES_CBC "PBE-SHA1-2DES" +# define LN_pbe_WithSHA1And2_Key_TripleDES_CBC "pbeWithSHA1And2-KeyTripleDES-CBC" +# define NID_pbe_WithSHA1And2_Key_TripleDES_CBC 147 +# define OBJ_pbe_WithSHA1And2_Key_TripleDES_CBC OBJ_pkcs12_pbeids, 4L + +# define SN_pbe_WithSHA1And128BitRC2_CBC "PBE-SHA1-RC2-128" +# define LN_pbe_WithSHA1And128BitRC2_CBC "pbeWithSHA1And128BitRC2-CBC" +# define NID_pbe_WithSHA1And128BitRC2_CBC 148 +# define OBJ_pbe_WithSHA1And128BitRC2_CBC OBJ_pkcs12_pbeids, 5L + +# define SN_pbe_WithSHA1And40BitRC2_CBC "PBE-SHA1-RC2-40" +# define LN_pbe_WithSHA1And40BitRC2_CBC "pbeWithSHA1And40BitRC2-CBC" +# define NID_pbe_WithSHA1And40BitRC2_CBC 149 +# define OBJ_pbe_WithSHA1And40BitRC2_CBC OBJ_pkcs12_pbeids, 6L + +# define OBJ_pkcs12_Version1 OBJ_pkcs12, 10L + +# define OBJ_pkcs12_BagIds OBJ_pkcs12_Version1, 1L + +# define LN_keyBag "keyBag" +# define NID_keyBag 150 +# define OBJ_keyBag OBJ_pkcs12_BagIds, 1L + +# define LN_pkcs8ShroudedKeyBag "pkcs8ShroudedKeyBag" +# define NID_pkcs8ShroudedKeyBag 151 +# define OBJ_pkcs8ShroudedKeyBag OBJ_pkcs12_BagIds, 2L + +# define LN_certBag "certBag" +# define NID_certBag 152 +# define OBJ_certBag OBJ_pkcs12_BagIds, 3L + +# define LN_crlBag "crlBag" +# define NID_crlBag 153 +# define OBJ_crlBag OBJ_pkcs12_BagIds, 4L + +# define LN_secretBag "secretBag" +# define NID_secretBag 154 +# define OBJ_secretBag OBJ_pkcs12_BagIds, 5L + +# define LN_safeContentsBag "safeContentsBag" +# define NID_safeContentsBag 155 +# define OBJ_safeContentsBag OBJ_pkcs12_BagIds, 6L + +# define LN_friendlyName "friendlyName" +# define NID_friendlyName 156 +# define OBJ_friendlyName OBJ_pkcs9, 20L + +# define LN_localKeyID "localKeyID" +# define NID_localKeyID 157 +# define OBJ_localKeyID OBJ_pkcs9, 21L + +# define OBJ_certTypes OBJ_pkcs9, 22L + +# define LN_x509Certificate "x509Certificate" +# define NID_x509Certificate 158 +# define OBJ_x509Certificate OBJ_certTypes, 1L + +# define LN_sdsiCertificate "sdsiCertificate" +# define NID_sdsiCertificate 159 +# define OBJ_sdsiCertificate OBJ_certTypes, 2L + +# define OBJ_crlTypes OBJ_pkcs9, 23L + +# define LN_x509Crl "x509Crl" +# define NID_x509Crl 160 +# define OBJ_x509Crl OBJ_crlTypes, 1L + +/* PKCS#5 v2 OIDs */ + +# define LN_pbes2 "PBES2" +# define NID_pbes2 161 +# define OBJ_pbes2 OBJ_pkcs,5L,13L + +# define LN_pbmac1 "PBMAC1" +# define NID_pbmac1 162 +# define OBJ_pbmac1 OBJ_pkcs,5L,14L + +# define LN_hmacWithSHA1 "hmacWithSHA1" +# define NID_hmacWithSHA1 163 +# define OBJ_hmacWithSHA1 OBJ_rsadsi,2L,7L + +/* Policy Qualifier Ids */ + +# define LN_id_qt_cps "Policy Qualifier CPS" +# define SN_id_qt_cps "id-qt-cps" +# define NID_id_qt_cps 164 +# define OBJ_id_qt_cps OBJ_id_pkix,2L,1L + +# define LN_id_qt_unotice "Policy Qualifier User Notice" +# define SN_id_qt_unotice "id-qt-unotice" +# define NID_id_qt_unotice 165 +# define OBJ_id_qt_unotice OBJ_id_pkix,2L,2L + +# define SN_rc2_64_cbc "RC2-64-CBC" +# define LN_rc2_64_cbc "rc2-64-cbc" +# define NID_rc2_64_cbc 166 + +# define SN_SMIMECapabilities "SMIME-CAPS" +# define LN_SMIMECapabilities "S/MIME Capabilities" +# define NID_SMIMECapabilities 167 +# define OBJ_SMIMECapabilities OBJ_pkcs9,15L + +# define SN_pbeWithMD2AndRC2_CBC "PBE-MD2-RC2-64" +# define LN_pbeWithMD2AndRC2_CBC "pbeWithMD2AndRC2-CBC" +# define NID_pbeWithMD2AndRC2_CBC 168 +# define OBJ_pbeWithMD2AndRC2_CBC OBJ_pkcs,5L,4L + +# define SN_pbeWithMD5AndRC2_CBC "PBE-MD5-RC2-64" +# define LN_pbeWithMD5AndRC2_CBC "pbeWithMD5AndRC2-CBC" +# define NID_pbeWithMD5AndRC2_CBC 169 +# define OBJ_pbeWithMD5AndRC2_CBC OBJ_pkcs,5L,6L + +# define SN_pbeWithSHA1AndDES_CBC "PBE-SHA1-DES" +# define LN_pbeWithSHA1AndDES_CBC "pbeWithSHA1AndDES-CBC" +# define NID_pbeWithSHA1AndDES_CBC 170 +# define OBJ_pbeWithSHA1AndDES_CBC OBJ_pkcs,5L,10L + +/* Extension request OIDs */ + +# define LN_ms_ext_req "Microsoft Extension Request" +# define SN_ms_ext_req "msExtReq" +# define NID_ms_ext_req 171 +# define OBJ_ms_ext_req 1L,3L,6L,1L,4L,1L,311L,2L,1L,14L + +# define LN_ext_req "Extension Request" +# define SN_ext_req "extReq" +# define NID_ext_req 172 +# define OBJ_ext_req OBJ_pkcs9,14L + +# define SN_name "name" +# define LN_name "name" +# define NID_name 173 +# define OBJ_name OBJ_X509,41L + +# define SN_dnQualifier "dnQualifier" +# define LN_dnQualifier "dnQualifier" +# define NID_dnQualifier 174 +# define OBJ_dnQualifier OBJ_X509,46L + +# define SN_id_pe "id-pe" +# define NID_id_pe 175 +# define OBJ_id_pe OBJ_id_pkix,1L + +# define SN_id_ad "id-ad" +# define NID_id_ad 176 +# define OBJ_id_ad OBJ_id_pkix,48L + +# define SN_info_access "authorityInfoAccess" +# define LN_info_access "Authority Information Access" +# define NID_info_access 177 +# define OBJ_info_access OBJ_id_pe,1L + +# define SN_ad_OCSP "OCSP" +# define LN_ad_OCSP "OCSP" +# define NID_ad_OCSP 178 +# define OBJ_ad_OCSP OBJ_id_ad,1L + +# define SN_ad_ca_issuers "caIssuers" +# define LN_ad_ca_issuers "CA Issuers" +# define NID_ad_ca_issuers 179 +# define OBJ_ad_ca_issuers OBJ_id_ad,2L + +# define SN_OCSP_sign "OCSPSigning" +# define LN_OCSP_sign "OCSP Signing" +# define NID_OCSP_sign 180 +# define OBJ_OCSP_sign OBJ_id_kp,9L +# endif /* USE_OBJ_MAC */ + +# include +# include + +# define OBJ_NAME_TYPE_UNDEF 0x00 +# define OBJ_NAME_TYPE_MD_METH 0x01 +# define OBJ_NAME_TYPE_CIPHER_METH 0x02 +# define OBJ_NAME_TYPE_PKEY_METH 0x03 +# define OBJ_NAME_TYPE_COMP_METH 0x04 +# define OBJ_NAME_TYPE_NUM 0x05 + +# define OBJ_NAME_ALIAS 0x8000 + +# define OBJ_BSEARCH_VALUE_ON_NOMATCH 0x01 +# define OBJ_BSEARCH_FIRST_VALUE_ON_MATCH 0x02 + + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct obj_name_st { + int type; + int alias; + const char *name; + const char *data; +} OBJ_NAME; + +# define OBJ_create_and_add_object(a,b,c) OBJ_create(a,b,c) + +int OBJ_NAME_init(void); +int OBJ_NAME_new_index(unsigned long (*hash_func) (const char *), + int (*cmp_func) (const char *, const char *), + void (*free_func) (const char *, int, const char *)); +const char *OBJ_NAME_get(const char *name, int type); +int OBJ_NAME_add(const char *name, int type, const char *data); +int OBJ_NAME_remove(const char *name, int type); +void OBJ_NAME_cleanup(int type); /* -1 for everything */ +void OBJ_NAME_do_all(int type, void (*fn) (const OBJ_NAME *, void *arg), + void *arg); +void OBJ_NAME_do_all_sorted(int type, + void (*fn) (const OBJ_NAME *, void *arg), + void *arg); + +ASN1_OBJECT *OBJ_dup(const ASN1_OBJECT *o); +ASN1_OBJECT *OBJ_nid2obj(int n); +const char *OBJ_nid2ln(int n); +const char *OBJ_nid2sn(int n); +int OBJ_obj2nid(const ASN1_OBJECT *o); +ASN1_OBJECT *OBJ_txt2obj(const char *s, int no_name); +int OBJ_obj2txt(char *buf, int buf_len, const ASN1_OBJECT *a, int no_name); +int OBJ_txt2nid(const char *s); +int OBJ_ln2nid(const char *s); +int OBJ_sn2nid(const char *s); +int OBJ_cmp(const ASN1_OBJECT *a, const ASN1_OBJECT *b); +const void *OBJ_bsearch_(const void *key, const void *base, int num, int size, + int (*cmp) (const void *, const void *)); +const void *OBJ_bsearch_ex_(const void *key, const void *base, int num, + int size, + int (*cmp) (const void *, const void *), + int flags); + +# define _DECLARE_OBJ_BSEARCH_CMP_FN(scope, type1, type2, nm) \ + static int nm##_cmp_BSEARCH_CMP_FN(const void *, const void *); \ + static int nm##_cmp(type1 const *, type2 const *); \ + scope type2 * OBJ_bsearch_##nm(type1 *key, type2 const *base, int num) + +# define DECLARE_OBJ_BSEARCH_CMP_FN(type1, type2, cmp) \ + _DECLARE_OBJ_BSEARCH_CMP_FN(static, type1, type2, cmp) +# define DECLARE_OBJ_BSEARCH_GLOBAL_CMP_FN(type1, type2, nm) \ + type2 * OBJ_bsearch_##nm(type1 *key, type2 const *base, int num) + +/*- + * Unsolved problem: if a type is actually a pointer type, like + * nid_triple is, then its impossible to get a const where you need + * it. Consider: + * + * typedef int nid_triple[3]; + * const void *a_; + * const nid_triple const *a = a_; + * + * The assignement discards a const because what you really want is: + * + * const int const * const *a = a_; + * + * But if you do that, you lose the fact that a is an array of 3 ints, + * which breaks comparison functions. + * + * Thus we end up having to cast, sadly, or unpack the + * declarations. Or, as I finally did in this case, delcare nid_triple + * to be a struct, which it should have been in the first place. + * + * Ben, August 2008. + * + * Also, strictly speaking not all types need be const, but handling + * the non-constness means a lot of complication, and in practice + * comparison routines do always not touch their arguments. + */ + +# define IMPLEMENT_OBJ_BSEARCH_CMP_FN(type1, type2, nm) \ + static int nm##_cmp_BSEARCH_CMP_FN(const void *a_, const void *b_) \ + { \ + type1 const *a = a_; \ + type2 const *b = b_; \ + return nm##_cmp(a,b); \ + } \ + static type2 *OBJ_bsearch_##nm(type1 *key, type2 const *base, int num) \ + { \ + return (type2 *)OBJ_bsearch_(key, base, num, sizeof(type2), \ + nm##_cmp_BSEARCH_CMP_FN); \ + } \ + extern void dummy_prototype(void) + +# define IMPLEMENT_OBJ_BSEARCH_GLOBAL_CMP_FN(type1, type2, nm) \ + static int nm##_cmp_BSEARCH_CMP_FN(const void *a_, const void *b_) \ + { \ + type1 const *a = a_; \ + type2 const *b = b_; \ + return nm##_cmp(a,b); \ + } \ + type2 *OBJ_bsearch_##nm(type1 *key, type2 const *base, int num) \ + { \ + return (type2 *)OBJ_bsearch_(key, base, num, sizeof(type2), \ + nm##_cmp_BSEARCH_CMP_FN); \ + } \ + extern void dummy_prototype(void) + +# define OBJ_bsearch(type1,key,type2,base,num,cmp) \ + ((type2 *)OBJ_bsearch_(CHECKED_PTR_OF(type1,key),CHECKED_PTR_OF(type2,base), \ + num,sizeof(type2), \ + ((void)CHECKED_PTR_OF(type1,cmp##_type_1), \ + (void)CHECKED_PTR_OF(type2,cmp##_type_2), \ + cmp##_BSEARCH_CMP_FN))) + +# define OBJ_bsearch_ex(type1,key,type2,base,num,cmp,flags) \ + ((type2 *)OBJ_bsearch_ex_(CHECKED_PTR_OF(type1,key),CHECKED_PTR_OF(type2,base), \ + num,sizeof(type2), \ + ((void)CHECKED_PTR_OF(type1,cmp##_type_1), \ + (void)type_2=CHECKED_PTR_OF(type2,cmp##_type_2), \ + cmp##_BSEARCH_CMP_FN)),flags) + +int OBJ_new_nid(int num); +int OBJ_add_object(const ASN1_OBJECT *obj); +int OBJ_create(const char *oid, const char *sn, const char *ln); +#if OPENSSL_API_COMPAT < 0x10100000L +# define OBJ_cleanup() while(0) continue +#endif +int OBJ_create_objects(BIO *in); + +size_t OBJ_length(const ASN1_OBJECT *obj); +const unsigned char *OBJ_get0_data(const ASN1_OBJECT *obj); + +int OBJ_find_sigid_algs(int signid, int *pdig_nid, int *ppkey_nid); +int OBJ_find_sigid_by_algs(int *psignid, int dig_nid, int pkey_nid); +int OBJ_add_sigid(int signid, int dig_id, int pkey_id); +void OBJ_sigid_free(void); + +/* BEGIN ERROR CODES */ +/* + * The following lines are auto generated by the script mkerr.pl. Any changes + * made after this point may be overwritten when the script is next run. + */ + +int ERR_load_OBJ_strings(void); + +/* Error codes for the OBJ functions. */ + +/* Function codes. */ +# define OBJ_F_OBJ_ADD_OBJECT 105 +# define OBJ_F_OBJ_CREATE 100 +# define OBJ_F_OBJ_DUP 101 +# define OBJ_F_OBJ_NAME_NEW_INDEX 106 +# define OBJ_F_OBJ_NID2LN 102 +# define OBJ_F_OBJ_NID2OBJ 103 +# define OBJ_F_OBJ_NID2SN 104 + +/* Reason codes. */ +# define OBJ_R_OID_EXISTS 102 +# define OBJ_R_UNKNOWN_NID 101 + +# ifdef __cplusplus +} +# endif +#endif diff --git a/mobile/ios/FxA/FxA/include/openssl/ocsp.h b/mobile/ios/FxA/FxA/include/openssl/ocsp.h new file mode 100644 index 0000000000..08debc5b78 --- /dev/null +++ b/mobile/ios/FxA/FxA/include/openssl/ocsp.h @@ -0,0 +1,412 @@ +/* + * Copyright 2000-2016 The OpenSSL Project Authors. All Rights Reserved. + * + * Licensed under the OpenSSL license (the "License"). You may not use + * this file except in compliance with the License. You can obtain a copy + * in the file LICENSE in the source distribution or at + * https://www.openssl.org/source/license.html + */ + +#ifndef HEADER_OCSP_H +# define HEADER_OCSP_H + +#include + +/* + * These definitions are outside the OPENSSL_NO_OCSP guard because although for + * historical reasons they have OCSP_* names, they can actually be used + * independently of OCSP. E.g. see RFC5280 + */ +/*- + * CRLReason ::= ENUMERATED { + * unspecified (0), + * keyCompromise (1), + * cACompromise (2), + * affiliationChanged (3), + * superseded (4), + * cessationOfOperation (5), + * certificateHold (6), + * removeFromCRL (8) } + */ +# define OCSP_REVOKED_STATUS_NOSTATUS -1 +# define OCSP_REVOKED_STATUS_UNSPECIFIED 0 +# define OCSP_REVOKED_STATUS_KEYCOMPROMISE 1 +# define OCSP_REVOKED_STATUS_CACOMPROMISE 2 +# define OCSP_REVOKED_STATUS_AFFILIATIONCHANGED 3 +# define OCSP_REVOKED_STATUS_SUPERSEDED 4 +# define OCSP_REVOKED_STATUS_CESSATIONOFOPERATION 5 +# define OCSP_REVOKED_STATUS_CERTIFICATEHOLD 6 +# define OCSP_REVOKED_STATUS_REMOVEFROMCRL 8 + + +# ifndef OPENSSL_NO_OCSP + +# include +# include +# include +# include + +#ifdef __cplusplus +extern "C" { +#endif + +/* Various flags and values */ + +# define OCSP_DEFAULT_NONCE_LENGTH 16 + +# define OCSP_NOCERTS 0x1 +# define OCSP_NOINTERN 0x2 +# define OCSP_NOSIGS 0x4 +# define OCSP_NOCHAIN 0x8 +# define OCSP_NOVERIFY 0x10 +# define OCSP_NOEXPLICIT 0x20 +# define OCSP_NOCASIGN 0x40 +# define OCSP_NODELEGATED 0x80 +# define OCSP_NOCHECKS 0x100 +# define OCSP_TRUSTOTHER 0x200 +# define OCSP_RESPID_KEY 0x400 +# define OCSP_NOTIME 0x800 + +typedef struct ocsp_cert_id_st OCSP_CERTID; + +DEFINE_STACK_OF(OCSP_CERTID) + +typedef struct ocsp_one_request_st OCSP_ONEREQ; + +DEFINE_STACK_OF(OCSP_ONEREQ) + +typedef struct ocsp_req_info_st OCSP_REQINFO; +typedef struct ocsp_signature_st OCSP_SIGNATURE; +typedef struct ocsp_request_st OCSP_REQUEST; + +# define OCSP_RESPONSE_STATUS_SUCCESSFUL 0 +# define OCSP_RESPONSE_STATUS_MALFORMEDREQUEST 1 +# define OCSP_RESPONSE_STATUS_INTERNALERROR 2 +# define OCSP_RESPONSE_STATUS_TRYLATER 3 +# define OCSP_RESPONSE_STATUS_SIGREQUIRED 5 +# define OCSP_RESPONSE_STATUS_UNAUTHORIZED 6 + +typedef struct ocsp_resp_bytes_st OCSP_RESPBYTES; + +# define V_OCSP_RESPID_NAME 0 +# define V_OCSP_RESPID_KEY 1 + +DEFINE_STACK_OF(OCSP_RESPID) +DECLARE_ASN1_FUNCTIONS(OCSP_RESPID) + +typedef struct ocsp_revoked_info_st OCSP_REVOKEDINFO; + +# define V_OCSP_CERTSTATUS_GOOD 0 +# define V_OCSP_CERTSTATUS_REVOKED 1 +# define V_OCSP_CERTSTATUS_UNKNOWN 2 + +typedef struct ocsp_cert_status_st OCSP_CERTSTATUS; +typedef struct ocsp_single_response_st OCSP_SINGLERESP; + +DEFINE_STACK_OF(OCSP_SINGLERESP) + +typedef struct ocsp_response_data_st OCSP_RESPDATA; + +typedef struct ocsp_basic_response_st OCSP_BASICRESP; + +typedef struct ocsp_crl_id_st OCSP_CRLID; +typedef struct ocsp_service_locator_st OCSP_SERVICELOC; + +# define PEM_STRING_OCSP_REQUEST "OCSP REQUEST" +# define PEM_STRING_OCSP_RESPONSE "OCSP RESPONSE" + +# define d2i_OCSP_REQUEST_bio(bp,p) ASN1_d2i_bio_of(OCSP_REQUEST,OCSP_REQUEST_new,d2i_OCSP_REQUEST,bp,p) + +# define d2i_OCSP_RESPONSE_bio(bp,p) ASN1_d2i_bio_of(OCSP_RESPONSE,OCSP_RESPONSE_new,d2i_OCSP_RESPONSE,bp,p) + +# define PEM_read_bio_OCSP_REQUEST(bp,x,cb) (OCSP_REQUEST *)PEM_ASN1_read_bio( \ + (char *(*)())d2i_OCSP_REQUEST,PEM_STRING_OCSP_REQUEST,bp,(char **)x,cb,NULL) + +# define PEM_read_bio_OCSP_RESPONSE(bp,x,cb)(OCSP_RESPONSE *)PEM_ASN1_read_bio(\ + (char *(*)())d2i_OCSP_RESPONSE,PEM_STRING_OCSP_RESPONSE,bp,(char **)x,cb,NULL) + +# define PEM_write_bio_OCSP_REQUEST(bp,o) \ + PEM_ASN1_write_bio((int (*)())i2d_OCSP_REQUEST,PEM_STRING_OCSP_REQUEST,\ + bp,(char *)o, NULL,NULL,0,NULL,NULL) + +# define PEM_write_bio_OCSP_RESPONSE(bp,o) \ + PEM_ASN1_write_bio((int (*)())i2d_OCSP_RESPONSE,PEM_STRING_OCSP_RESPONSE,\ + bp,(char *)o, NULL,NULL,0,NULL,NULL) + +# define i2d_OCSP_RESPONSE_bio(bp,o) ASN1_i2d_bio_of(OCSP_RESPONSE,i2d_OCSP_RESPONSE,bp,o) + +# define i2d_OCSP_REQUEST_bio(bp,o) ASN1_i2d_bio_of(OCSP_REQUEST,i2d_OCSP_REQUEST,bp,o) + +# define OCSP_REQUEST_sign(o,pkey,md) \ + ASN1_item_sign(ASN1_ITEM_rptr(OCSP_REQINFO),\ + &o->optionalSignature->signatureAlgorithm,NULL,\ + o->optionalSignature->signature,&o->tbsRequest,pkey,md) + +# define OCSP_BASICRESP_sign(o,pkey,md,d) \ + ASN1_item_sign(ASN1_ITEM_rptr(OCSP_RESPDATA),&o->signatureAlgorithm,NULL,\ + o->signature,&o->tbsResponseData,pkey,md) + +# define OCSP_REQUEST_verify(a,r) ASN1_item_verify(ASN1_ITEM_rptr(OCSP_REQINFO),\ + &a->optionalSignature->signatureAlgorithm,\ + a->optionalSignature->signature,&a->tbsRequest,r) + +# define OCSP_BASICRESP_verify(a,r,d) ASN1_item_verify(ASN1_ITEM_rptr(OCSP_RESPDATA),\ + &a->signatureAlgorithm,a->signature,&a->tbsResponseData,r) + +# define ASN1_BIT_STRING_digest(data,type,md,len) \ + ASN1_item_digest(ASN1_ITEM_rptr(ASN1_BIT_STRING),type,data,md,len) + +# define OCSP_CERTSTATUS_dup(cs)\ + (OCSP_CERTSTATUS*)ASN1_dup((int(*)())i2d_OCSP_CERTSTATUS,\ + (char *(*)())d2i_OCSP_CERTSTATUS,(char *)(cs)) + +OCSP_CERTID *OCSP_CERTID_dup(OCSP_CERTID *id); + +OCSP_RESPONSE *OCSP_sendreq_bio(BIO *b, const char *path, OCSP_REQUEST *req); +OCSP_REQ_CTX *OCSP_sendreq_new(BIO *io, const char *path, OCSP_REQUEST *req, + int maxline); +int OCSP_REQ_CTX_nbio(OCSP_REQ_CTX *rctx); +int OCSP_sendreq_nbio(OCSP_RESPONSE **presp, OCSP_REQ_CTX *rctx); +OCSP_REQ_CTX *OCSP_REQ_CTX_new(BIO *io, int maxline); +void OCSP_REQ_CTX_free(OCSP_REQ_CTX *rctx); +void OCSP_set_max_response_length(OCSP_REQ_CTX *rctx, unsigned long len); +int OCSP_REQ_CTX_i2d(OCSP_REQ_CTX *rctx, const ASN1_ITEM *it, + ASN1_VALUE *val); +int OCSP_REQ_CTX_nbio_d2i(OCSP_REQ_CTX *rctx, ASN1_VALUE **pval, + const ASN1_ITEM *it); +BIO *OCSP_REQ_CTX_get0_mem_bio(OCSP_REQ_CTX *rctx); +int OCSP_REQ_CTX_i2d(OCSP_REQ_CTX *rctx, const ASN1_ITEM *it, + ASN1_VALUE *val); +int OCSP_REQ_CTX_http(OCSP_REQ_CTX *rctx, const char *op, const char *path); +int OCSP_REQ_CTX_set1_req(OCSP_REQ_CTX *rctx, OCSP_REQUEST *req); +int OCSP_REQ_CTX_add1_header(OCSP_REQ_CTX *rctx, + const char *name, const char *value); + +OCSP_CERTID *OCSP_cert_to_id(const EVP_MD *dgst, const X509 *subject, + const X509 *issuer); + +OCSP_CERTID *OCSP_cert_id_new(const EVP_MD *dgst, + const X509_NAME *issuerName, + const ASN1_BIT_STRING *issuerKey, + const ASN1_INTEGER *serialNumber); + +OCSP_ONEREQ *OCSP_request_add0_id(OCSP_REQUEST *req, OCSP_CERTID *cid); + +int OCSP_request_add1_nonce(OCSP_REQUEST *req, unsigned char *val, int len); +int OCSP_basic_add1_nonce(OCSP_BASICRESP *resp, unsigned char *val, int len); +int OCSP_check_nonce(OCSP_REQUEST *req, OCSP_BASICRESP *bs); +int OCSP_copy_nonce(OCSP_BASICRESP *resp, OCSP_REQUEST *req); + +int OCSP_request_set1_name(OCSP_REQUEST *req, X509_NAME *nm); +int OCSP_request_add1_cert(OCSP_REQUEST *req, X509 *cert); + +int OCSP_request_sign(OCSP_REQUEST *req, + X509 *signer, + EVP_PKEY *key, + const EVP_MD *dgst, + STACK_OF(X509) *certs, unsigned long flags); + +int OCSP_response_status(OCSP_RESPONSE *resp); +OCSP_BASICRESP *OCSP_response_get1_basic(OCSP_RESPONSE *resp); + +const ASN1_OCTET_STRING *OCSP_resp_get0_signature(const OCSP_BASICRESP *bs); + +int OCSP_resp_count(OCSP_BASICRESP *bs); +OCSP_SINGLERESP *OCSP_resp_get0(OCSP_BASICRESP *bs, int idx); +const ASN1_GENERALIZEDTIME *OCSP_resp_get0_produced_at(const OCSP_BASICRESP* bs); +const STACK_OF(X509) *OCSP_resp_get0_certs(const OCSP_BASICRESP *bs); +int OCSP_resp_get0_id(const OCSP_BASICRESP *bs, + const ASN1_OCTET_STRING **pid, + const X509_NAME **pname); + +int OCSP_resp_find(OCSP_BASICRESP *bs, OCSP_CERTID *id, int last); +int OCSP_single_get0_status(OCSP_SINGLERESP *single, int *reason, + ASN1_GENERALIZEDTIME **revtime, + ASN1_GENERALIZEDTIME **thisupd, + ASN1_GENERALIZEDTIME **nextupd); +int OCSP_resp_find_status(OCSP_BASICRESP *bs, OCSP_CERTID *id, int *status, + int *reason, + ASN1_GENERALIZEDTIME **revtime, + ASN1_GENERALIZEDTIME **thisupd, + ASN1_GENERALIZEDTIME **nextupd); +int OCSP_check_validity(ASN1_GENERALIZEDTIME *thisupd, + ASN1_GENERALIZEDTIME *nextupd, long sec, long maxsec); + +int OCSP_request_verify(OCSP_REQUEST *req, STACK_OF(X509) *certs, + X509_STORE *store, unsigned long flags); + +int OCSP_parse_url(const char *url, char **phost, char **pport, char **ppath, + int *pssl); + +int OCSP_id_issuer_cmp(OCSP_CERTID *a, OCSP_CERTID *b); +int OCSP_id_cmp(OCSP_CERTID *a, OCSP_CERTID *b); + +int OCSP_request_onereq_count(OCSP_REQUEST *req); +OCSP_ONEREQ *OCSP_request_onereq_get0(OCSP_REQUEST *req, int i); +OCSP_CERTID *OCSP_onereq_get0_id(OCSP_ONEREQ *one); +int OCSP_id_get0_info(ASN1_OCTET_STRING **piNameHash, ASN1_OBJECT **pmd, + ASN1_OCTET_STRING **pikeyHash, + ASN1_INTEGER **pserial, OCSP_CERTID *cid); +int OCSP_request_is_signed(OCSP_REQUEST *req); +OCSP_RESPONSE *OCSP_response_create(int status, OCSP_BASICRESP *bs); +OCSP_SINGLERESP *OCSP_basic_add1_status(OCSP_BASICRESP *rsp, + OCSP_CERTID *cid, + int status, int reason, + ASN1_TIME *revtime, + ASN1_TIME *thisupd, + ASN1_TIME *nextupd); +int OCSP_basic_add1_cert(OCSP_BASICRESP *resp, X509 *cert); +int OCSP_basic_sign(OCSP_BASICRESP *brsp, + X509 *signer, EVP_PKEY *key, const EVP_MD *dgst, + STACK_OF(X509) *certs, unsigned long flags); +int OCSP_RESPID_set_by_name(OCSP_RESPID *respid, X509 *cert); +int OCSP_RESPID_set_by_key(OCSP_RESPID *respid, X509 *cert); +int OCSP_RESPID_match(OCSP_RESPID *respid, X509 *cert); + +X509_EXTENSION *OCSP_crlID_new(const char *url, long *n, char *tim); + +X509_EXTENSION *OCSP_accept_responses_new(char **oids); + +X509_EXTENSION *OCSP_archive_cutoff_new(char *tim); + +X509_EXTENSION *OCSP_url_svcloc_new(X509_NAME *issuer, const char **urls); + +int OCSP_REQUEST_get_ext_count(OCSP_REQUEST *x); +int OCSP_REQUEST_get_ext_by_NID(OCSP_REQUEST *x, int nid, int lastpos); +int OCSP_REQUEST_get_ext_by_OBJ(OCSP_REQUEST *x, const ASN1_OBJECT *obj, + int lastpos); +int OCSP_REQUEST_get_ext_by_critical(OCSP_REQUEST *x, int crit, int lastpos); +X509_EXTENSION *OCSP_REQUEST_get_ext(OCSP_REQUEST *x, int loc); +X509_EXTENSION *OCSP_REQUEST_delete_ext(OCSP_REQUEST *x, int loc); +void *OCSP_REQUEST_get1_ext_d2i(OCSP_REQUEST *x, int nid, int *crit, + int *idx); +int OCSP_REQUEST_add1_ext_i2d(OCSP_REQUEST *x, int nid, void *value, int crit, + unsigned long flags); +int OCSP_REQUEST_add_ext(OCSP_REQUEST *x, X509_EXTENSION *ex, int loc); + +int OCSP_ONEREQ_get_ext_count(OCSP_ONEREQ *x); +int OCSP_ONEREQ_get_ext_by_NID(OCSP_ONEREQ *x, int nid, int lastpos); +int OCSP_ONEREQ_get_ext_by_OBJ(OCSP_ONEREQ *x, const ASN1_OBJECT *obj, int lastpos); +int OCSP_ONEREQ_get_ext_by_critical(OCSP_ONEREQ *x, int crit, int lastpos); +X509_EXTENSION *OCSP_ONEREQ_get_ext(OCSP_ONEREQ *x, int loc); +X509_EXTENSION *OCSP_ONEREQ_delete_ext(OCSP_ONEREQ *x, int loc); +void *OCSP_ONEREQ_get1_ext_d2i(OCSP_ONEREQ *x, int nid, int *crit, int *idx); +int OCSP_ONEREQ_add1_ext_i2d(OCSP_ONEREQ *x, int nid, void *value, int crit, + unsigned long flags); +int OCSP_ONEREQ_add_ext(OCSP_ONEREQ *x, X509_EXTENSION *ex, int loc); + +int OCSP_BASICRESP_get_ext_count(OCSP_BASICRESP *x); +int OCSP_BASICRESP_get_ext_by_NID(OCSP_BASICRESP *x, int nid, int lastpos); +int OCSP_BASICRESP_get_ext_by_OBJ(OCSP_BASICRESP *x, const ASN1_OBJECT *obj, + int lastpos); +int OCSP_BASICRESP_get_ext_by_critical(OCSP_BASICRESP *x, int crit, + int lastpos); +X509_EXTENSION *OCSP_BASICRESP_get_ext(OCSP_BASICRESP *x, int loc); +X509_EXTENSION *OCSP_BASICRESP_delete_ext(OCSP_BASICRESP *x, int loc); +void *OCSP_BASICRESP_get1_ext_d2i(OCSP_BASICRESP *x, int nid, int *crit, + int *idx); +int OCSP_BASICRESP_add1_ext_i2d(OCSP_BASICRESP *x, int nid, void *value, + int crit, unsigned long flags); +int OCSP_BASICRESP_add_ext(OCSP_BASICRESP *x, X509_EXTENSION *ex, int loc); + +int OCSP_SINGLERESP_get_ext_count(OCSP_SINGLERESP *x); +int OCSP_SINGLERESP_get_ext_by_NID(OCSP_SINGLERESP *x, int nid, int lastpos); +int OCSP_SINGLERESP_get_ext_by_OBJ(OCSP_SINGLERESP *x, const ASN1_OBJECT *obj, + int lastpos); +int OCSP_SINGLERESP_get_ext_by_critical(OCSP_SINGLERESP *x, int crit, + int lastpos); +X509_EXTENSION *OCSP_SINGLERESP_get_ext(OCSP_SINGLERESP *x, int loc); +X509_EXTENSION *OCSP_SINGLERESP_delete_ext(OCSP_SINGLERESP *x, int loc); +void *OCSP_SINGLERESP_get1_ext_d2i(OCSP_SINGLERESP *x, int nid, int *crit, + int *idx); +int OCSP_SINGLERESP_add1_ext_i2d(OCSP_SINGLERESP *x, int nid, void *value, + int crit, unsigned long flags); +int OCSP_SINGLERESP_add_ext(OCSP_SINGLERESP *x, X509_EXTENSION *ex, int loc); +const OCSP_CERTID *OCSP_SINGLERESP_get0_id(const OCSP_SINGLERESP *x); + +DECLARE_ASN1_FUNCTIONS(OCSP_SINGLERESP) +DECLARE_ASN1_FUNCTIONS(OCSP_CERTSTATUS) +DECLARE_ASN1_FUNCTIONS(OCSP_REVOKEDINFO) +DECLARE_ASN1_FUNCTIONS(OCSP_BASICRESP) +DECLARE_ASN1_FUNCTIONS(OCSP_RESPDATA) +DECLARE_ASN1_FUNCTIONS(OCSP_RESPID) +DECLARE_ASN1_FUNCTIONS(OCSP_RESPONSE) +DECLARE_ASN1_FUNCTIONS(OCSP_RESPBYTES) +DECLARE_ASN1_FUNCTIONS(OCSP_ONEREQ) +DECLARE_ASN1_FUNCTIONS(OCSP_CERTID) +DECLARE_ASN1_FUNCTIONS(OCSP_REQUEST) +DECLARE_ASN1_FUNCTIONS(OCSP_SIGNATURE) +DECLARE_ASN1_FUNCTIONS(OCSP_REQINFO) +DECLARE_ASN1_FUNCTIONS(OCSP_CRLID) +DECLARE_ASN1_FUNCTIONS(OCSP_SERVICELOC) + +const char *OCSP_response_status_str(long s); +const char *OCSP_cert_status_str(long s); +const char *OCSP_crl_reason_str(long s); + +int OCSP_REQUEST_print(BIO *bp, OCSP_REQUEST *a, unsigned long flags); +int OCSP_RESPONSE_print(BIO *bp, OCSP_RESPONSE *o, unsigned long flags); + +int OCSP_basic_verify(OCSP_BASICRESP *bs, STACK_OF(X509) *certs, + X509_STORE *st, unsigned long flags); + +/* BEGIN ERROR CODES */ +/* + * The following lines are auto generated by the script mkerr.pl. Any changes + * made after this point may be overwritten when the script is next run. + */ + +int ERR_load_OCSP_strings(void); + +/* Error codes for the OCSP functions. */ + +/* Function codes. */ +# define OCSP_F_D2I_OCSP_NONCE 102 +# define OCSP_F_OCSP_BASIC_ADD1_STATUS 103 +# define OCSP_F_OCSP_BASIC_SIGN 104 +# define OCSP_F_OCSP_BASIC_VERIFY 105 +# define OCSP_F_OCSP_CERT_ID_NEW 101 +# define OCSP_F_OCSP_CHECK_DELEGATED 106 +# define OCSP_F_OCSP_CHECK_IDS 107 +# define OCSP_F_OCSP_CHECK_ISSUER 108 +# define OCSP_F_OCSP_CHECK_VALIDITY 115 +# define OCSP_F_OCSP_MATCH_ISSUERID 109 +# define OCSP_F_OCSP_PARSE_URL 114 +# define OCSP_F_OCSP_REQUEST_SIGN 110 +# define OCSP_F_OCSP_REQUEST_VERIFY 116 +# define OCSP_F_OCSP_RESPONSE_GET1_BASIC 111 +# define OCSP_F_PARSE_HTTP_LINE1 118 + +/* Reason codes. */ +# define OCSP_R_CERTIFICATE_VERIFY_ERROR 101 +# define OCSP_R_DIGEST_ERR 102 +# define OCSP_R_ERROR_IN_NEXTUPDATE_FIELD 122 +# define OCSP_R_ERROR_IN_THISUPDATE_FIELD 123 +# define OCSP_R_ERROR_PARSING_URL 121 +# define OCSP_R_MISSING_OCSPSIGNING_USAGE 103 +# define OCSP_R_NEXTUPDATE_BEFORE_THISUPDATE 124 +# define OCSP_R_NOT_BASIC_RESPONSE 104 +# define OCSP_R_NO_CERTIFICATES_IN_CHAIN 105 +# define OCSP_R_NO_RESPONSE_DATA 108 +# define OCSP_R_NO_REVOKED_TIME 109 +# define OCSP_R_NO_SIGNER_KEY 130 +# define OCSP_R_PRIVATE_KEY_DOES_NOT_MATCH_CERTIFICATE 110 +# define OCSP_R_REQUEST_NOT_SIGNED 128 +# define OCSP_R_RESPONSE_CONTAINS_NO_REVOCATION_DATA 111 +# define OCSP_R_ROOT_CA_NOT_TRUSTED 112 +# define OCSP_R_SERVER_RESPONSE_ERROR 114 +# define OCSP_R_SERVER_RESPONSE_PARSE_ERROR 115 +# define OCSP_R_SIGNATURE_FAILURE 117 +# define OCSP_R_SIGNER_CERTIFICATE_NOT_FOUND 118 +# define OCSP_R_STATUS_EXPIRED 125 +# define OCSP_R_STATUS_NOT_YET_VALID 126 +# define OCSP_R_STATUS_TOO_OLD 127 +# define OCSP_R_UNKNOWN_MESSAGE_DIGEST 119 +# define OCSP_R_UNKNOWN_NID 120 +# define OCSP_R_UNSUPPORTED_REQUESTORNAME_TYPE 129 + +# ifdef __cplusplus +} +# endif +# endif +#endif diff --git a/mobile/ios/FxA/FxA/include/openssl/opensslconf.h b/mobile/ios/FxA/FxA/include/openssl/opensslconf.h new file mode 100644 index 0000000000..f70f152d51 --- /dev/null +++ b/mobile/ios/FxA/FxA/include/openssl/opensslconf.h @@ -0,0 +1,27 @@ +/* + * Building OpenSSL for the different architectures of all iOS and tvOS devices requires different settings. + * In order to be able to use assembly code on all devices, the choice was made to keep optimal settings for all + * devices and use this intermediate header file to use the proper opensslconf.h file for each architecture. + + * See also https://github.com/x2on/OpenSSL-for-iPhone/issues/126 and referenced pull requests + */ + +#include + +#if TARGET_OS_IOS && TARGET_OS_SIMULATOR && TARGET_CPU_X86_64 +# include +#elif TARGET_OS_IOS && TARGET_OS_SIMULATOR && TARGET_CPU_X86 +# include +#elif TARGET_OS_IOS && TARGET_OS_EMBEDDED && TARGET_CPU_ARM64 +# include +#elif TARGET_OS_IOS && TARGET_OS_EMBEDDED && TARGET_CPU_ARM && defined(__ARM_ARCH_7S__) +# include +#elif TARGET_OS_IOS && TARGET_OS_EMBEDDED && TARGET_CPU_ARM && !defined(__ARM_ARCH_7S__) +# include +#elif TARGET_OS_TV && TARGET_OS_SIMULATOR && TARGET_CPU_X86_64 +# include +#elif TARGET_OS_TV && TARGET_OS_EMBEDDED && TARGET_CPU_ARM64 +# include +#else +# error Unable to determine target or target not included in OpenSSL build +#endif diff --git a/mobile/ios/FxA/FxA/include/openssl/opensslconf_ios_arm64.h b/mobile/ios/FxA/FxA/include/openssl/opensslconf_ios_arm64.h new file mode 100644 index 0000000000..19bfb801a6 --- /dev/null +++ b/mobile/ios/FxA/FxA/include/openssl/opensslconf_ios_arm64.h @@ -0,0 +1,179 @@ +/* + * WARNING: do not edit! + * Generated by Makefile from include/openssl/opensslconf.h.in + * + * Copyright 2016 The OpenSSL Project Authors. All Rights Reserved. + * + * Licensed under the OpenSSL license (the "License"). You may not use + * this file except in compliance with the License. You can obtain a copy + * in the file LICENSE in the source distribution or at + * https://www.openssl.org/source/license.html + */ + +#ifdef __cplusplus +extern "C" { +#endif + +#ifdef OPENSSL_ALGORITHM_DEFINES +# error OPENSSL_ALGORITHM_DEFINES no longer supported +#endif + +/* + * OpenSSL was configured with the following options: + */ + +#ifndef OPENSSL_SYS_iOS +# define OPENSSL_SYS_iOS 1 +#endif +#define OPENSSL_MIN_API 0x10100000L +#ifndef OPENSSL_NO_MD2 +# define OPENSSL_NO_MD2 +#endif +#ifndef OPENSSL_NO_RC5 +# define OPENSSL_NO_RC5 +#endif +#ifndef OPENSSL_THREADS +# define OPENSSL_THREADS +#endif +#ifndef OPENSSL_NO_ASAN +# define OPENSSL_NO_ASAN +#endif +#ifndef OPENSSL_NO_ASYNC +# define OPENSSL_NO_ASYNC +#endif +#ifndef OPENSSL_NO_CRYPTO_MDEBUG +# define OPENSSL_NO_CRYPTO_MDEBUG +#endif +#ifndef OPENSSL_NO_CRYPTO_MDEBUG_BACKTRACE +# define OPENSSL_NO_CRYPTO_MDEBUG_BACKTRACE +#endif +#ifndef OPENSSL_NO_DEPRECATED +# define OPENSSL_NO_DEPRECATED +#endif +#ifndef OPENSSL_NO_EC_NISTP_64_GCC_128 +# define OPENSSL_NO_EC_NISTP_64_GCC_128 +#endif +#ifndef OPENSSL_NO_EGD +# define OPENSSL_NO_EGD +#endif +#ifndef OPENSSL_NO_FUZZ_AFL +# define OPENSSL_NO_FUZZ_AFL +#endif +#ifndef OPENSSL_NO_FUZZ_LIBFUZZER +# define OPENSSL_NO_FUZZ_LIBFUZZER +#endif +#ifndef OPENSSL_NO_HEARTBEATS +# define OPENSSL_NO_HEARTBEATS +#endif +#ifndef OPENSSL_NO_MSAN +# define OPENSSL_NO_MSAN +#endif +#ifndef OPENSSL_NO_SCTP +# define OPENSSL_NO_SCTP +#endif +#ifndef OPENSSL_NO_SSL_TRACE +# define OPENSSL_NO_SSL_TRACE +#endif +#ifndef OPENSSL_NO_SSL3 +# define OPENSSL_NO_SSL3 +#endif +#ifndef OPENSSL_NO_SSL3_METHOD +# define OPENSSL_NO_SSL3_METHOD +#endif +#ifndef OPENSSL_NO_UBSAN +# define OPENSSL_NO_UBSAN +#endif +#ifndef OPENSSL_NO_UNIT_TEST +# define OPENSSL_NO_UNIT_TEST +#endif +#ifndef OPENSSL_NO_WEAK_SSL_CIPHERS +# define OPENSSL_NO_WEAK_SSL_CIPHERS +#endif +#ifndef OPENSSL_NO_AFALGENG +# define OPENSSL_NO_AFALGENG +#endif + + +/* + * Sometimes OPENSSSL_NO_xxx ends up with an empty file and some compilers + * don't like that. This will hopefully silence them. + */ +#define NON_EMPTY_TRANSLATION_UNIT static void *dummy = &dummy; + +/* + * Applications should use -DOPENSSL_API_COMPAT= to suppress the + * declarations of functions deprecated in or before . Otherwise, they + * still won't see them if the library has been built to disable deprecated + * functions. + */ +#if defined(OPENSSL_NO_DEPRECATED) +# define DECLARE_DEPRECATED(f) +#elif __GNUC__ > 3 || (__GNUC__ == 3 && __GNUC_MINOR__ > 0) +# define DECLARE_DEPRECATED(f) f __attribute__ ((deprecated)); +#else +# define DECLARE_DEPRECATED(f) f; +#endif + +#ifndef OPENSSL_FILE +# ifdef OPENSSL_NO_FILENAMES +# define OPENSSL_FILE "" +# define OPENSSL_LINE 0 +# else +# define OPENSSL_FILE __FILE__ +# define OPENSSL_LINE __LINE__ +# endif +#endif + +#ifndef OPENSSL_MIN_API +# define OPENSSL_MIN_API 0 +#endif + +#if !defined(OPENSSL_API_COMPAT) || OPENSSL_API_COMPAT < OPENSSL_MIN_API +# undef OPENSSL_API_COMPAT +# define OPENSSL_API_COMPAT OPENSSL_MIN_API +#endif + +#if OPENSSL_API_COMPAT < 0x10100000L +# define DEPRECATEDIN_1_1_0(f) DECLARE_DEPRECATED(f) +#else +# define DEPRECATEDIN_1_1_0(f) +#endif + +#if OPENSSL_API_COMPAT < 0x10000000L +# define DEPRECATEDIN_1_0_0(f) DECLARE_DEPRECATED(f) +#else +# define DEPRECATEDIN_1_0_0(f) +#endif + +#if OPENSSL_API_COMPAT < 0x00908000L +# define DEPRECATEDIN_0_9_8(f) DECLARE_DEPRECATED(f) +#else +# define DEPRECATEDIN_0_9_8(f) +#endif + +#define OPENSSL_CPUID_OBJ + +/* Generate 80386 code? */ +#undef I386_ONLY + +#undef OPENSSL_UNISTD +#define OPENSSL_UNISTD + +#undef OPENSSL_EXPORT_VAR_AS_FUNCTION + +/* + * The following are cipher-specific, but are part of the public API. + */ +#if !defined(OPENSSL_SYS_UEFI) +# undef BN_LLONG +/* Only one for the following should be defined */ +# define SIXTY_FOUR_BIT_LONG +# undef SIXTY_FOUR_BIT +# undef THIRTY_TWO_BIT +#endif + +#define RC4_INT unsigned char + +#ifdef __cplusplus +} +#endif diff --git a/mobile/ios/FxA/FxA/include/openssl/opensslconf_ios_armv7.h b/mobile/ios/FxA/FxA/include/openssl/opensslconf_ios_armv7.h new file mode 100644 index 0000000000..aba635bb58 --- /dev/null +++ b/mobile/ios/FxA/FxA/include/openssl/opensslconf_ios_armv7.h @@ -0,0 +1,179 @@ +/* + * WARNING: do not edit! + * Generated by Makefile from include/openssl/opensslconf.h.in + * + * Copyright 2016 The OpenSSL Project Authors. All Rights Reserved. + * + * Licensed under the OpenSSL license (the "License"). You may not use + * this file except in compliance with the License. You can obtain a copy + * in the file LICENSE in the source distribution or at + * https://www.openssl.org/source/license.html + */ + +#ifdef __cplusplus +extern "C" { +#endif + +#ifdef OPENSSL_ALGORITHM_DEFINES +# error OPENSSL_ALGORITHM_DEFINES no longer supported +#endif + +/* + * OpenSSL was configured with the following options: + */ + +#ifndef OPENSSL_SYS_iOS +# define OPENSSL_SYS_iOS 1 +#endif +#define OPENSSL_MIN_API 0x10100000L +#ifndef OPENSSL_NO_MD2 +# define OPENSSL_NO_MD2 +#endif +#ifndef OPENSSL_NO_RC5 +# define OPENSSL_NO_RC5 +#endif +#ifndef OPENSSL_THREADS +# define OPENSSL_THREADS +#endif +#ifndef OPENSSL_NO_ASAN +# define OPENSSL_NO_ASAN +#endif +#ifndef OPENSSL_NO_ASYNC +# define OPENSSL_NO_ASYNC +#endif +#ifndef OPENSSL_NO_CRYPTO_MDEBUG +# define OPENSSL_NO_CRYPTO_MDEBUG +#endif +#ifndef OPENSSL_NO_CRYPTO_MDEBUG_BACKTRACE +# define OPENSSL_NO_CRYPTO_MDEBUG_BACKTRACE +#endif +#ifndef OPENSSL_NO_DEPRECATED +# define OPENSSL_NO_DEPRECATED +#endif +#ifndef OPENSSL_NO_EC_NISTP_64_GCC_128 +# define OPENSSL_NO_EC_NISTP_64_GCC_128 +#endif +#ifndef OPENSSL_NO_EGD +# define OPENSSL_NO_EGD +#endif +#ifndef OPENSSL_NO_FUZZ_AFL +# define OPENSSL_NO_FUZZ_AFL +#endif +#ifndef OPENSSL_NO_FUZZ_LIBFUZZER +# define OPENSSL_NO_FUZZ_LIBFUZZER +#endif +#ifndef OPENSSL_NO_HEARTBEATS +# define OPENSSL_NO_HEARTBEATS +#endif +#ifndef OPENSSL_NO_MSAN +# define OPENSSL_NO_MSAN +#endif +#ifndef OPENSSL_NO_SCTP +# define OPENSSL_NO_SCTP +#endif +#ifndef OPENSSL_NO_SSL_TRACE +# define OPENSSL_NO_SSL_TRACE +#endif +#ifndef OPENSSL_NO_SSL3 +# define OPENSSL_NO_SSL3 +#endif +#ifndef OPENSSL_NO_SSL3_METHOD +# define OPENSSL_NO_SSL3_METHOD +#endif +#ifndef OPENSSL_NO_UBSAN +# define OPENSSL_NO_UBSAN +#endif +#ifndef OPENSSL_NO_UNIT_TEST +# define OPENSSL_NO_UNIT_TEST +#endif +#ifndef OPENSSL_NO_WEAK_SSL_CIPHERS +# define OPENSSL_NO_WEAK_SSL_CIPHERS +#endif +#ifndef OPENSSL_NO_AFALGENG +# define OPENSSL_NO_AFALGENG +#endif + + +/* + * Sometimes OPENSSSL_NO_xxx ends up with an empty file and some compilers + * don't like that. This will hopefully silence them. + */ +#define NON_EMPTY_TRANSLATION_UNIT static void *dummy = &dummy; + +/* + * Applications should use -DOPENSSL_API_COMPAT= to suppress the + * declarations of functions deprecated in or before . Otherwise, they + * still won't see them if the library has been built to disable deprecated + * functions. + */ +#if defined(OPENSSL_NO_DEPRECATED) +# define DECLARE_DEPRECATED(f) +#elif __GNUC__ > 3 || (__GNUC__ == 3 && __GNUC_MINOR__ > 0) +# define DECLARE_DEPRECATED(f) f __attribute__ ((deprecated)); +#else +# define DECLARE_DEPRECATED(f) f; +#endif + +#ifndef OPENSSL_FILE +# ifdef OPENSSL_NO_FILENAMES +# define OPENSSL_FILE "" +# define OPENSSL_LINE 0 +# else +# define OPENSSL_FILE __FILE__ +# define OPENSSL_LINE __LINE__ +# endif +#endif + +#ifndef OPENSSL_MIN_API +# define OPENSSL_MIN_API 0 +#endif + +#if !defined(OPENSSL_API_COMPAT) || OPENSSL_API_COMPAT < OPENSSL_MIN_API +# undef OPENSSL_API_COMPAT +# define OPENSSL_API_COMPAT OPENSSL_MIN_API +#endif + +#if OPENSSL_API_COMPAT < 0x10100000L +# define DEPRECATEDIN_1_1_0(f) DECLARE_DEPRECATED(f) +#else +# define DEPRECATEDIN_1_1_0(f) +#endif + +#if OPENSSL_API_COMPAT < 0x10000000L +# define DEPRECATEDIN_1_0_0(f) DECLARE_DEPRECATED(f) +#else +# define DEPRECATEDIN_1_0_0(f) +#endif + +#if OPENSSL_API_COMPAT < 0x00908000L +# define DEPRECATEDIN_0_9_8(f) DECLARE_DEPRECATED(f) +#else +# define DEPRECATEDIN_0_9_8(f) +#endif + +#define OPENSSL_CPUID_OBJ + +/* Generate 80386 code? */ +#undef I386_ONLY + +#undef OPENSSL_UNISTD +#define OPENSSL_UNISTD + +#undef OPENSSL_EXPORT_VAR_AS_FUNCTION + +/* + * The following are cipher-specific, but are part of the public API. + */ +#if !defined(OPENSSL_SYS_UEFI) +# define BN_LLONG +/* Only one for the following should be defined */ +# undef SIXTY_FOUR_BIT_LONG +# undef SIXTY_FOUR_BIT +# define THIRTY_TWO_BIT +#endif + +#define RC4_INT unsigned char + +#ifdef __cplusplus +} +#endif diff --git a/mobile/ios/FxA/FxA/include/openssl/opensslconf_ios_armv7s.h b/mobile/ios/FxA/FxA/include/openssl/opensslconf_ios_armv7s.h new file mode 100644 index 0000000000..aba635bb58 --- /dev/null +++ b/mobile/ios/FxA/FxA/include/openssl/opensslconf_ios_armv7s.h @@ -0,0 +1,179 @@ +/* + * WARNING: do not edit! + * Generated by Makefile from include/openssl/opensslconf.h.in + * + * Copyright 2016 The OpenSSL Project Authors. All Rights Reserved. + * + * Licensed under the OpenSSL license (the "License"). You may not use + * this file except in compliance with the License. You can obtain a copy + * in the file LICENSE in the source distribution or at + * https://www.openssl.org/source/license.html + */ + +#ifdef __cplusplus +extern "C" { +#endif + +#ifdef OPENSSL_ALGORITHM_DEFINES +# error OPENSSL_ALGORITHM_DEFINES no longer supported +#endif + +/* + * OpenSSL was configured with the following options: + */ + +#ifndef OPENSSL_SYS_iOS +# define OPENSSL_SYS_iOS 1 +#endif +#define OPENSSL_MIN_API 0x10100000L +#ifndef OPENSSL_NO_MD2 +# define OPENSSL_NO_MD2 +#endif +#ifndef OPENSSL_NO_RC5 +# define OPENSSL_NO_RC5 +#endif +#ifndef OPENSSL_THREADS +# define OPENSSL_THREADS +#endif +#ifndef OPENSSL_NO_ASAN +# define OPENSSL_NO_ASAN +#endif +#ifndef OPENSSL_NO_ASYNC +# define OPENSSL_NO_ASYNC +#endif +#ifndef OPENSSL_NO_CRYPTO_MDEBUG +# define OPENSSL_NO_CRYPTO_MDEBUG +#endif +#ifndef OPENSSL_NO_CRYPTO_MDEBUG_BACKTRACE +# define OPENSSL_NO_CRYPTO_MDEBUG_BACKTRACE +#endif +#ifndef OPENSSL_NO_DEPRECATED +# define OPENSSL_NO_DEPRECATED +#endif +#ifndef OPENSSL_NO_EC_NISTP_64_GCC_128 +# define OPENSSL_NO_EC_NISTP_64_GCC_128 +#endif +#ifndef OPENSSL_NO_EGD +# define OPENSSL_NO_EGD +#endif +#ifndef OPENSSL_NO_FUZZ_AFL +# define OPENSSL_NO_FUZZ_AFL +#endif +#ifndef OPENSSL_NO_FUZZ_LIBFUZZER +# define OPENSSL_NO_FUZZ_LIBFUZZER +#endif +#ifndef OPENSSL_NO_HEARTBEATS +# define OPENSSL_NO_HEARTBEATS +#endif +#ifndef OPENSSL_NO_MSAN +# define OPENSSL_NO_MSAN +#endif +#ifndef OPENSSL_NO_SCTP +# define OPENSSL_NO_SCTP +#endif +#ifndef OPENSSL_NO_SSL_TRACE +# define OPENSSL_NO_SSL_TRACE +#endif +#ifndef OPENSSL_NO_SSL3 +# define OPENSSL_NO_SSL3 +#endif +#ifndef OPENSSL_NO_SSL3_METHOD +# define OPENSSL_NO_SSL3_METHOD +#endif +#ifndef OPENSSL_NO_UBSAN +# define OPENSSL_NO_UBSAN +#endif +#ifndef OPENSSL_NO_UNIT_TEST +# define OPENSSL_NO_UNIT_TEST +#endif +#ifndef OPENSSL_NO_WEAK_SSL_CIPHERS +# define OPENSSL_NO_WEAK_SSL_CIPHERS +#endif +#ifndef OPENSSL_NO_AFALGENG +# define OPENSSL_NO_AFALGENG +#endif + + +/* + * Sometimes OPENSSSL_NO_xxx ends up with an empty file and some compilers + * don't like that. This will hopefully silence them. + */ +#define NON_EMPTY_TRANSLATION_UNIT static void *dummy = &dummy; + +/* + * Applications should use -DOPENSSL_API_COMPAT= to suppress the + * declarations of functions deprecated in or before . Otherwise, they + * still won't see them if the library has been built to disable deprecated + * functions. + */ +#if defined(OPENSSL_NO_DEPRECATED) +# define DECLARE_DEPRECATED(f) +#elif __GNUC__ > 3 || (__GNUC__ == 3 && __GNUC_MINOR__ > 0) +# define DECLARE_DEPRECATED(f) f __attribute__ ((deprecated)); +#else +# define DECLARE_DEPRECATED(f) f; +#endif + +#ifndef OPENSSL_FILE +# ifdef OPENSSL_NO_FILENAMES +# define OPENSSL_FILE "" +# define OPENSSL_LINE 0 +# else +# define OPENSSL_FILE __FILE__ +# define OPENSSL_LINE __LINE__ +# endif +#endif + +#ifndef OPENSSL_MIN_API +# define OPENSSL_MIN_API 0 +#endif + +#if !defined(OPENSSL_API_COMPAT) || OPENSSL_API_COMPAT < OPENSSL_MIN_API +# undef OPENSSL_API_COMPAT +# define OPENSSL_API_COMPAT OPENSSL_MIN_API +#endif + +#if OPENSSL_API_COMPAT < 0x10100000L +# define DEPRECATEDIN_1_1_0(f) DECLARE_DEPRECATED(f) +#else +# define DEPRECATEDIN_1_1_0(f) +#endif + +#if OPENSSL_API_COMPAT < 0x10000000L +# define DEPRECATEDIN_1_0_0(f) DECLARE_DEPRECATED(f) +#else +# define DEPRECATEDIN_1_0_0(f) +#endif + +#if OPENSSL_API_COMPAT < 0x00908000L +# define DEPRECATEDIN_0_9_8(f) DECLARE_DEPRECATED(f) +#else +# define DEPRECATEDIN_0_9_8(f) +#endif + +#define OPENSSL_CPUID_OBJ + +/* Generate 80386 code? */ +#undef I386_ONLY + +#undef OPENSSL_UNISTD +#define OPENSSL_UNISTD + +#undef OPENSSL_EXPORT_VAR_AS_FUNCTION + +/* + * The following are cipher-specific, but are part of the public API. + */ +#if !defined(OPENSSL_SYS_UEFI) +# define BN_LLONG +/* Only one for the following should be defined */ +# undef SIXTY_FOUR_BIT_LONG +# undef SIXTY_FOUR_BIT +# define THIRTY_TWO_BIT +#endif + +#define RC4_INT unsigned char + +#ifdef __cplusplus +} +#endif diff --git a/mobile/ios/FxA/FxA/include/openssl/opensslconf_ios_i386.h b/mobile/ios/FxA/FxA/include/openssl/opensslconf_ios_i386.h new file mode 100644 index 0000000000..e047b0e808 --- /dev/null +++ b/mobile/ios/FxA/FxA/include/openssl/opensslconf_ios_i386.h @@ -0,0 +1,179 @@ +/* + * WARNING: do not edit! + * Generated by Makefile from include/openssl/opensslconf.h.in + * + * Copyright 2016 The OpenSSL Project Authors. All Rights Reserved. + * + * Licensed under the OpenSSL license (the "License"). You may not use + * this file except in compliance with the License. You can obtain a copy + * in the file LICENSE in the source distribution or at + * https://www.openssl.org/source/license.html + */ + +#ifdef __cplusplus +extern "C" { +#endif + +#ifdef OPENSSL_ALGORITHM_DEFINES +# error OPENSSL_ALGORITHM_DEFINES no longer supported +#endif + +/* + * OpenSSL was configured with the following options: + */ + +#ifndef OPENSSL_SYS_iOS +# define OPENSSL_SYS_iOS 1 +#endif +#define OPENSSL_MIN_API 0x10100000L +#ifndef OPENSSL_NO_MD2 +# define OPENSSL_NO_MD2 +#endif +#ifndef OPENSSL_NO_RC5 +# define OPENSSL_NO_RC5 +#endif +#ifndef OPENSSL_THREADS +# define OPENSSL_THREADS +#endif +#ifndef OPENSSL_NO_ASAN +# define OPENSSL_NO_ASAN +#endif +#ifndef OPENSSL_NO_ASYNC +# define OPENSSL_NO_ASYNC +#endif +#ifndef OPENSSL_NO_CRYPTO_MDEBUG +# define OPENSSL_NO_CRYPTO_MDEBUG +#endif +#ifndef OPENSSL_NO_CRYPTO_MDEBUG_BACKTRACE +# define OPENSSL_NO_CRYPTO_MDEBUG_BACKTRACE +#endif +#ifndef OPENSSL_NO_DEPRECATED +# define OPENSSL_NO_DEPRECATED +#endif +#ifndef OPENSSL_NO_EC_NISTP_64_GCC_128 +# define OPENSSL_NO_EC_NISTP_64_GCC_128 +#endif +#ifndef OPENSSL_NO_EGD +# define OPENSSL_NO_EGD +#endif +#ifndef OPENSSL_NO_FUZZ_AFL +# define OPENSSL_NO_FUZZ_AFL +#endif +#ifndef OPENSSL_NO_FUZZ_LIBFUZZER +# define OPENSSL_NO_FUZZ_LIBFUZZER +#endif +#ifndef OPENSSL_NO_HEARTBEATS +# define OPENSSL_NO_HEARTBEATS +#endif +#ifndef OPENSSL_NO_MSAN +# define OPENSSL_NO_MSAN +#endif +#ifndef OPENSSL_NO_SCTP +# define OPENSSL_NO_SCTP +#endif +#ifndef OPENSSL_NO_SSL_TRACE +# define OPENSSL_NO_SSL_TRACE +#endif +#ifndef OPENSSL_NO_SSL3 +# define OPENSSL_NO_SSL3 +#endif +#ifndef OPENSSL_NO_SSL3_METHOD +# define OPENSSL_NO_SSL3_METHOD +#endif +#ifndef OPENSSL_NO_UBSAN +# define OPENSSL_NO_UBSAN +#endif +#ifndef OPENSSL_NO_UNIT_TEST +# define OPENSSL_NO_UNIT_TEST +#endif +#ifndef OPENSSL_NO_WEAK_SSL_CIPHERS +# define OPENSSL_NO_WEAK_SSL_CIPHERS +#endif +#ifndef OPENSSL_NO_AFALGENG +# define OPENSSL_NO_AFALGENG +#endif + + +/* + * Sometimes OPENSSSL_NO_xxx ends up with an empty file and some compilers + * don't like that. This will hopefully silence them. + */ +#define NON_EMPTY_TRANSLATION_UNIT static void *dummy = &dummy; + +/* + * Applications should use -DOPENSSL_API_COMPAT= to suppress the + * declarations of functions deprecated in or before . Otherwise, they + * still won't see them if the library has been built to disable deprecated + * functions. + */ +#if defined(OPENSSL_NO_DEPRECATED) +# define DECLARE_DEPRECATED(f) +#elif __GNUC__ > 3 || (__GNUC__ == 3 && __GNUC_MINOR__ > 0) +# define DECLARE_DEPRECATED(f) f __attribute__ ((deprecated)); +#else +# define DECLARE_DEPRECATED(f) f; +#endif + +#ifndef OPENSSL_FILE +# ifdef OPENSSL_NO_FILENAMES +# define OPENSSL_FILE "" +# define OPENSSL_LINE 0 +# else +# define OPENSSL_FILE __FILE__ +# define OPENSSL_LINE __LINE__ +# endif +#endif + +#ifndef OPENSSL_MIN_API +# define OPENSSL_MIN_API 0 +#endif + +#if !defined(OPENSSL_API_COMPAT) || OPENSSL_API_COMPAT < OPENSSL_MIN_API +# undef OPENSSL_API_COMPAT +# define OPENSSL_API_COMPAT OPENSSL_MIN_API +#endif + +#if OPENSSL_API_COMPAT < 0x10100000L +# define DEPRECATEDIN_1_1_0(f) DECLARE_DEPRECATED(f) +#else +# define DEPRECATEDIN_1_1_0(f) +#endif + +#if OPENSSL_API_COMPAT < 0x10000000L +# define DEPRECATEDIN_1_0_0(f) DECLARE_DEPRECATED(f) +#else +# define DEPRECATEDIN_1_0_0(f) +#endif + +#if OPENSSL_API_COMPAT < 0x00908000L +# define DEPRECATEDIN_0_9_8(f) DECLARE_DEPRECATED(f) +#else +# define DEPRECATEDIN_0_9_8(f) +#endif + +#define OPENSSL_CPUID_OBJ + +/* Generate 80386 code? */ +#undef I386_ONLY + +#undef OPENSSL_UNISTD +#define OPENSSL_UNISTD + +#undef OPENSSL_EXPORT_VAR_AS_FUNCTION + +/* + * The following are cipher-specific, but are part of the public API. + */ +#if !defined(OPENSSL_SYS_UEFI) +# define BN_LLONG +/* Only one for the following should be defined */ +# undef SIXTY_FOUR_BIT_LONG +# undef SIXTY_FOUR_BIT +# define THIRTY_TWO_BIT +#endif + +#define RC4_INT unsigned int + +#ifdef __cplusplus +} +#endif diff --git a/mobile/ios/FxA/FxA/include/openssl/opensslconf_ios_x86_64.h b/mobile/ios/FxA/FxA/include/openssl/opensslconf_ios_x86_64.h new file mode 100644 index 0000000000..b05ebfeac0 --- /dev/null +++ b/mobile/ios/FxA/FxA/include/openssl/opensslconf_ios_x86_64.h @@ -0,0 +1,179 @@ +/* + * WARNING: do not edit! + * Generated by Makefile from include/openssl/opensslconf.h.in + * + * Copyright 2016 The OpenSSL Project Authors. All Rights Reserved. + * + * Licensed under the OpenSSL license (the "License"). You may not use + * this file except in compliance with the License. You can obtain a copy + * in the file LICENSE in the source distribution or at + * https://www.openssl.org/source/license.html + */ + +#ifdef __cplusplus +extern "C" { +#endif + +#ifdef OPENSSL_ALGORITHM_DEFINES +# error OPENSSL_ALGORITHM_DEFINES no longer supported +#endif + +/* + * OpenSSL was configured with the following options: + */ + +#ifndef OPENSSL_SYS_iOS +# define OPENSSL_SYS_iOS 1 +#endif +#define OPENSSL_MIN_API 0x10100000L +#ifndef OPENSSL_NO_MD2 +# define OPENSSL_NO_MD2 +#endif +#ifndef OPENSSL_NO_RC5 +# define OPENSSL_NO_RC5 +#endif +#ifndef OPENSSL_THREADS +# define OPENSSL_THREADS +#endif +#ifndef OPENSSL_NO_ASAN +# define OPENSSL_NO_ASAN +#endif +#ifndef OPENSSL_NO_ASYNC +# define OPENSSL_NO_ASYNC +#endif +#ifndef OPENSSL_NO_CRYPTO_MDEBUG +# define OPENSSL_NO_CRYPTO_MDEBUG +#endif +#ifndef OPENSSL_NO_CRYPTO_MDEBUG_BACKTRACE +# define OPENSSL_NO_CRYPTO_MDEBUG_BACKTRACE +#endif +#ifndef OPENSSL_NO_DEPRECATED +# define OPENSSL_NO_DEPRECATED +#endif +#ifndef OPENSSL_NO_EC_NISTP_64_GCC_128 +# define OPENSSL_NO_EC_NISTP_64_GCC_128 +#endif +#ifndef OPENSSL_NO_EGD +# define OPENSSL_NO_EGD +#endif +#ifndef OPENSSL_NO_FUZZ_AFL +# define OPENSSL_NO_FUZZ_AFL +#endif +#ifndef OPENSSL_NO_FUZZ_LIBFUZZER +# define OPENSSL_NO_FUZZ_LIBFUZZER +#endif +#ifndef OPENSSL_NO_HEARTBEATS +# define OPENSSL_NO_HEARTBEATS +#endif +#ifndef OPENSSL_NO_MSAN +# define OPENSSL_NO_MSAN +#endif +#ifndef OPENSSL_NO_SCTP +# define OPENSSL_NO_SCTP +#endif +#ifndef OPENSSL_NO_SSL_TRACE +# define OPENSSL_NO_SSL_TRACE +#endif +#ifndef OPENSSL_NO_SSL3 +# define OPENSSL_NO_SSL3 +#endif +#ifndef OPENSSL_NO_SSL3_METHOD +# define OPENSSL_NO_SSL3_METHOD +#endif +#ifndef OPENSSL_NO_UBSAN +# define OPENSSL_NO_UBSAN +#endif +#ifndef OPENSSL_NO_UNIT_TEST +# define OPENSSL_NO_UNIT_TEST +#endif +#ifndef OPENSSL_NO_WEAK_SSL_CIPHERS +# define OPENSSL_NO_WEAK_SSL_CIPHERS +#endif +#ifndef OPENSSL_NO_AFALGENG +# define OPENSSL_NO_AFALGENG +#endif + + +/* + * Sometimes OPENSSSL_NO_xxx ends up with an empty file and some compilers + * don't like that. This will hopefully silence them. + */ +#define NON_EMPTY_TRANSLATION_UNIT static void *dummy = &dummy; + +/* + * Applications should use -DOPENSSL_API_COMPAT= to suppress the + * declarations of functions deprecated in or before . Otherwise, they + * still won't see them if the library has been built to disable deprecated + * functions. + */ +#if defined(OPENSSL_NO_DEPRECATED) +# define DECLARE_DEPRECATED(f) +#elif __GNUC__ > 3 || (__GNUC__ == 3 && __GNUC_MINOR__ > 0) +# define DECLARE_DEPRECATED(f) f __attribute__ ((deprecated)); +#else +# define DECLARE_DEPRECATED(f) f; +#endif + +#ifndef OPENSSL_FILE +# ifdef OPENSSL_NO_FILENAMES +# define OPENSSL_FILE "" +# define OPENSSL_LINE 0 +# else +# define OPENSSL_FILE __FILE__ +# define OPENSSL_LINE __LINE__ +# endif +#endif + +#ifndef OPENSSL_MIN_API +# define OPENSSL_MIN_API 0 +#endif + +#if !defined(OPENSSL_API_COMPAT) || OPENSSL_API_COMPAT < OPENSSL_MIN_API +# undef OPENSSL_API_COMPAT +# define OPENSSL_API_COMPAT OPENSSL_MIN_API +#endif + +#if OPENSSL_API_COMPAT < 0x10100000L +# define DEPRECATEDIN_1_1_0(f) DECLARE_DEPRECATED(f) +#else +# define DEPRECATEDIN_1_1_0(f) +#endif + +#if OPENSSL_API_COMPAT < 0x10000000L +# define DEPRECATEDIN_1_0_0(f) DECLARE_DEPRECATED(f) +#else +# define DEPRECATEDIN_1_0_0(f) +#endif + +#if OPENSSL_API_COMPAT < 0x00908000L +# define DEPRECATEDIN_0_9_8(f) DECLARE_DEPRECATED(f) +#else +# define DEPRECATEDIN_0_9_8(f) +#endif + +#define OPENSSL_CPUID_OBJ + +/* Generate 80386 code? */ +#undef I386_ONLY + +#undef OPENSSL_UNISTD +#define OPENSSL_UNISTD + +#undef OPENSSL_EXPORT_VAR_AS_FUNCTION + +/* + * The following are cipher-specific, but are part of the public API. + */ +#if !defined(OPENSSL_SYS_UEFI) +# undef BN_LLONG +/* Only one for the following should be defined */ +# define SIXTY_FOUR_BIT_LONG +# undef SIXTY_FOUR_BIT +# undef THIRTY_TWO_BIT +#endif + +#define RC4_INT unsigned int + +#ifdef __cplusplus +} +#endif diff --git a/mobile/ios/FxA/FxA/include/openssl/opensslconf_tvos_arm64.h b/mobile/ios/FxA/FxA/include/openssl/opensslconf_tvos_arm64.h new file mode 100644 index 0000000000..e54b4c5774 --- /dev/null +++ b/mobile/ios/FxA/FxA/include/openssl/opensslconf_tvos_arm64.h @@ -0,0 +1,179 @@ +/* + * WARNING: do not edit! + * Generated by Makefile from include/openssl/opensslconf.h.in + * + * Copyright 2016 The OpenSSL Project Authors. All Rights Reserved. + * + * Licensed under the OpenSSL license (the "License"). You may not use + * this file except in compliance with the License. You can obtain a copy + * in the file LICENSE in the source distribution or at + * https://www.openssl.org/source/license.html + */ + +#ifdef __cplusplus +extern "C" { +#endif + +#ifdef OPENSSL_ALGORITHM_DEFINES +# error OPENSSL_ALGORITHM_DEFINES no longer supported +#endif + +/* + * OpenSSL was configured with the following options: + */ + +#ifndef OPENSSL_SYS_tvOS +# define OPENSSL_SYS_tvOS 1 +#endif +#define OPENSSL_MIN_API 0x10100000L +#ifndef OPENSSL_NO_MD2 +# define OPENSSL_NO_MD2 +#endif +#ifndef OPENSSL_NO_RC5 +# define OPENSSL_NO_RC5 +#endif +#ifndef OPENSSL_THREADS +# define OPENSSL_THREADS +#endif +#ifndef OPENSSL_NO_ASAN +# define OPENSSL_NO_ASAN +#endif +#ifndef OPENSSL_NO_ASYNC +# define OPENSSL_NO_ASYNC +#endif +#ifndef OPENSSL_NO_CRYPTO_MDEBUG +# define OPENSSL_NO_CRYPTO_MDEBUG +#endif +#ifndef OPENSSL_NO_CRYPTO_MDEBUG_BACKTRACE +# define OPENSSL_NO_CRYPTO_MDEBUG_BACKTRACE +#endif +#ifndef OPENSSL_NO_DEPRECATED +# define OPENSSL_NO_DEPRECATED +#endif +#ifndef OPENSSL_NO_EC_NISTP_64_GCC_128 +# define OPENSSL_NO_EC_NISTP_64_GCC_128 +#endif +#ifndef OPENSSL_NO_EGD +# define OPENSSL_NO_EGD +#endif +#ifndef OPENSSL_NO_FUZZ_AFL +# define OPENSSL_NO_FUZZ_AFL +#endif +#ifndef OPENSSL_NO_FUZZ_LIBFUZZER +# define OPENSSL_NO_FUZZ_LIBFUZZER +#endif +#ifndef OPENSSL_NO_HEARTBEATS +# define OPENSSL_NO_HEARTBEATS +#endif +#ifndef OPENSSL_NO_MSAN +# define OPENSSL_NO_MSAN +#endif +#ifndef OPENSSL_NO_SCTP +# define OPENSSL_NO_SCTP +#endif +#ifndef OPENSSL_NO_SSL_TRACE +# define OPENSSL_NO_SSL_TRACE +#endif +#ifndef OPENSSL_NO_SSL3 +# define OPENSSL_NO_SSL3 +#endif +#ifndef OPENSSL_NO_SSL3_METHOD +# define OPENSSL_NO_SSL3_METHOD +#endif +#ifndef OPENSSL_NO_UBSAN +# define OPENSSL_NO_UBSAN +#endif +#ifndef OPENSSL_NO_UNIT_TEST +# define OPENSSL_NO_UNIT_TEST +#endif +#ifndef OPENSSL_NO_WEAK_SSL_CIPHERS +# define OPENSSL_NO_WEAK_SSL_CIPHERS +#endif +#ifndef OPENSSL_NO_AFALGENG +# define OPENSSL_NO_AFALGENG +#endif + + +/* + * Sometimes OPENSSSL_NO_xxx ends up with an empty file and some compilers + * don't like that. This will hopefully silence them. + */ +#define NON_EMPTY_TRANSLATION_UNIT static void *dummy = &dummy; + +/* + * Applications should use -DOPENSSL_API_COMPAT= to suppress the + * declarations of functions deprecated in or before . Otherwise, they + * still won't see them if the library has been built to disable deprecated + * functions. + */ +#if defined(OPENSSL_NO_DEPRECATED) +# define DECLARE_DEPRECATED(f) +#elif __GNUC__ > 3 || (__GNUC__ == 3 && __GNUC_MINOR__ > 0) +# define DECLARE_DEPRECATED(f) f __attribute__ ((deprecated)); +#else +# define DECLARE_DEPRECATED(f) f; +#endif + +#ifndef OPENSSL_FILE +# ifdef OPENSSL_NO_FILENAMES +# define OPENSSL_FILE "" +# define OPENSSL_LINE 0 +# else +# define OPENSSL_FILE __FILE__ +# define OPENSSL_LINE __LINE__ +# endif +#endif + +#ifndef OPENSSL_MIN_API +# define OPENSSL_MIN_API 0 +#endif + +#if !defined(OPENSSL_API_COMPAT) || OPENSSL_API_COMPAT < OPENSSL_MIN_API +# undef OPENSSL_API_COMPAT +# define OPENSSL_API_COMPAT OPENSSL_MIN_API +#endif + +#if OPENSSL_API_COMPAT < 0x10100000L +# define DEPRECATEDIN_1_1_0(f) DECLARE_DEPRECATED(f) +#else +# define DEPRECATEDIN_1_1_0(f) +#endif + +#if OPENSSL_API_COMPAT < 0x10000000L +# define DEPRECATEDIN_1_0_0(f) DECLARE_DEPRECATED(f) +#else +# define DEPRECATEDIN_1_0_0(f) +#endif + +#if OPENSSL_API_COMPAT < 0x00908000L +# define DEPRECATEDIN_0_9_8(f) DECLARE_DEPRECATED(f) +#else +# define DEPRECATEDIN_0_9_8(f) +#endif + +#define OPENSSL_CPUID_OBJ + +/* Generate 80386 code? */ +#undef I386_ONLY + +#undef OPENSSL_UNISTD +#define OPENSSL_UNISTD + +#undef OPENSSL_EXPORT_VAR_AS_FUNCTION + +/* + * The following are cipher-specific, but are part of the public API. + */ +#if !defined(OPENSSL_SYS_UEFI) +# undef BN_LLONG +/* Only one for the following should be defined */ +# define SIXTY_FOUR_BIT_LONG +# undef SIXTY_FOUR_BIT +# undef THIRTY_TWO_BIT +#endif + +#define RC4_INT unsigned char + +#ifdef __cplusplus +} +#endif diff --git a/mobile/ios/FxA/FxA/include/openssl/opensslconf_tvos_x86_64.h b/mobile/ios/FxA/FxA/include/openssl/opensslconf_tvos_x86_64.h new file mode 100644 index 0000000000..e0314c054b --- /dev/null +++ b/mobile/ios/FxA/FxA/include/openssl/opensslconf_tvos_x86_64.h @@ -0,0 +1,179 @@ +/* + * WARNING: do not edit! + * Generated by Makefile from include/openssl/opensslconf.h.in + * + * Copyright 2016 The OpenSSL Project Authors. All Rights Reserved. + * + * Licensed under the OpenSSL license (the "License"). You may not use + * this file except in compliance with the License. You can obtain a copy + * in the file LICENSE in the source distribution or at + * https://www.openssl.org/source/license.html + */ + +#ifdef __cplusplus +extern "C" { +#endif + +#ifdef OPENSSL_ALGORITHM_DEFINES +# error OPENSSL_ALGORITHM_DEFINES no longer supported +#endif + +/* + * OpenSSL was configured with the following options: + */ + +#ifndef OPENSSL_SYS_tvOS +# define OPENSSL_SYS_tvOS 1 +#endif +#define OPENSSL_MIN_API 0x10100000L +#ifndef OPENSSL_NO_MD2 +# define OPENSSL_NO_MD2 +#endif +#ifndef OPENSSL_NO_RC5 +# define OPENSSL_NO_RC5 +#endif +#ifndef OPENSSL_THREADS +# define OPENSSL_THREADS +#endif +#ifndef OPENSSL_NO_ASAN +# define OPENSSL_NO_ASAN +#endif +#ifndef OPENSSL_NO_ASYNC +# define OPENSSL_NO_ASYNC +#endif +#ifndef OPENSSL_NO_CRYPTO_MDEBUG +# define OPENSSL_NO_CRYPTO_MDEBUG +#endif +#ifndef OPENSSL_NO_CRYPTO_MDEBUG_BACKTRACE +# define OPENSSL_NO_CRYPTO_MDEBUG_BACKTRACE +#endif +#ifndef OPENSSL_NO_DEPRECATED +# define OPENSSL_NO_DEPRECATED +#endif +#ifndef OPENSSL_NO_EC_NISTP_64_GCC_128 +# define OPENSSL_NO_EC_NISTP_64_GCC_128 +#endif +#ifndef OPENSSL_NO_EGD +# define OPENSSL_NO_EGD +#endif +#ifndef OPENSSL_NO_FUZZ_AFL +# define OPENSSL_NO_FUZZ_AFL +#endif +#ifndef OPENSSL_NO_FUZZ_LIBFUZZER +# define OPENSSL_NO_FUZZ_LIBFUZZER +#endif +#ifndef OPENSSL_NO_HEARTBEATS +# define OPENSSL_NO_HEARTBEATS +#endif +#ifndef OPENSSL_NO_MSAN +# define OPENSSL_NO_MSAN +#endif +#ifndef OPENSSL_NO_SCTP +# define OPENSSL_NO_SCTP +#endif +#ifndef OPENSSL_NO_SSL_TRACE +# define OPENSSL_NO_SSL_TRACE +#endif +#ifndef OPENSSL_NO_SSL3 +# define OPENSSL_NO_SSL3 +#endif +#ifndef OPENSSL_NO_SSL3_METHOD +# define OPENSSL_NO_SSL3_METHOD +#endif +#ifndef OPENSSL_NO_UBSAN +# define OPENSSL_NO_UBSAN +#endif +#ifndef OPENSSL_NO_UNIT_TEST +# define OPENSSL_NO_UNIT_TEST +#endif +#ifndef OPENSSL_NO_WEAK_SSL_CIPHERS +# define OPENSSL_NO_WEAK_SSL_CIPHERS +#endif +#ifndef OPENSSL_NO_AFALGENG +# define OPENSSL_NO_AFALGENG +#endif + + +/* + * Sometimes OPENSSSL_NO_xxx ends up with an empty file and some compilers + * don't like that. This will hopefully silence them. + */ +#define NON_EMPTY_TRANSLATION_UNIT static void *dummy = &dummy; + +/* + * Applications should use -DOPENSSL_API_COMPAT= to suppress the + * declarations of functions deprecated in or before . Otherwise, they + * still won't see them if the library has been built to disable deprecated + * functions. + */ +#if defined(OPENSSL_NO_DEPRECATED) +# define DECLARE_DEPRECATED(f) +#elif __GNUC__ > 3 || (__GNUC__ == 3 && __GNUC_MINOR__ > 0) +# define DECLARE_DEPRECATED(f) f __attribute__ ((deprecated)); +#else +# define DECLARE_DEPRECATED(f) f; +#endif + +#ifndef OPENSSL_FILE +# ifdef OPENSSL_NO_FILENAMES +# define OPENSSL_FILE "" +# define OPENSSL_LINE 0 +# else +# define OPENSSL_FILE __FILE__ +# define OPENSSL_LINE __LINE__ +# endif +#endif + +#ifndef OPENSSL_MIN_API +# define OPENSSL_MIN_API 0 +#endif + +#if !defined(OPENSSL_API_COMPAT) || OPENSSL_API_COMPAT < OPENSSL_MIN_API +# undef OPENSSL_API_COMPAT +# define OPENSSL_API_COMPAT OPENSSL_MIN_API +#endif + +#if OPENSSL_API_COMPAT < 0x10100000L +# define DEPRECATEDIN_1_1_0(f) DECLARE_DEPRECATED(f) +#else +# define DEPRECATEDIN_1_1_0(f) +#endif + +#if OPENSSL_API_COMPAT < 0x10000000L +# define DEPRECATEDIN_1_0_0(f) DECLARE_DEPRECATED(f) +#else +# define DEPRECATEDIN_1_0_0(f) +#endif + +#if OPENSSL_API_COMPAT < 0x00908000L +# define DEPRECATEDIN_0_9_8(f) DECLARE_DEPRECATED(f) +#else +# define DEPRECATEDIN_0_9_8(f) +#endif + +#define OPENSSL_CPUID_OBJ + +/* Generate 80386 code? */ +#undef I386_ONLY + +#undef OPENSSL_UNISTD +#define OPENSSL_UNISTD + +#undef OPENSSL_EXPORT_VAR_AS_FUNCTION + +/* + * The following are cipher-specific, but are part of the public API. + */ +#if !defined(OPENSSL_SYS_UEFI) +# undef BN_LLONG +/* Only one for the following should be defined */ +# define SIXTY_FOUR_BIT_LONG +# undef SIXTY_FOUR_BIT +# undef THIRTY_TWO_BIT +#endif + +#define RC4_INT unsigned int + +#ifdef __cplusplus +} +#endif diff --git a/mobile/ios/FxA/FxA/include/openssl/opensslv.h b/mobile/ios/FxA/FxA/include/openssl/opensslv.h new file mode 100644 index 0000000000..9d6708fa8b --- /dev/null +++ b/mobile/ios/FxA/FxA/include/openssl/opensslv.h @@ -0,0 +1,105 @@ +/* + * Copyright 1999-2016 The OpenSSL Project Authors. All Rights Reserved. + * + * Licensed under the OpenSSL license (the "License"). You may not use + * this file except in compliance with the License. You can obtain a copy + * in the file LICENSE in the source distribution or at + * https://www.openssl.org/source/license.html + */ + +#ifndef HEADER_OPENSSLV_H +# define HEADER_OPENSSLV_H + +#ifdef __cplusplus +extern "C" { +#endif + +/*- + * Numeric release version identifier: + * MNNFFPPS: major minor fix patch status + * The status nibble has one of the values 0 for development, 1 to e for betas + * 1 to 14, and f for release. The patch level is exactly that. + * For example: + * 0.9.3-dev 0x00903000 + * 0.9.3-beta1 0x00903001 + * 0.9.3-beta2-dev 0x00903002 + * 0.9.3-beta2 0x00903002 (same as ...beta2-dev) + * 0.9.3 0x0090300f + * 0.9.3a 0x0090301f + * 0.9.4 0x0090400f + * 1.2.3z 0x102031af + * + * For continuity reasons (because 0.9.5 is already out, and is coded + * 0x00905100), between 0.9.5 and 0.9.6 the coding of the patch level + * part is slightly different, by setting the highest bit. This means + * that 0.9.5a looks like this: 0x0090581f. At 0.9.6, we can start + * with 0x0090600S... + * + * (Prior to 0.9.3-dev a different scheme was used: 0.9.2b is 0x0922.) + * (Prior to 0.9.5a beta1, a different scheme was used: MMNNFFRBB for + * major minor fix final patch/beta) + */ +# define OPENSSL_VERSION_NUMBER 0x1010005fL +# ifdef OPENSSL_FIPS +# define OPENSSL_VERSION_TEXT "OpenSSL 1.1.0e-fips 16 Feb 2017" +# else +# define OPENSSL_VERSION_TEXT "OpenSSL 1.1.0e 16 Feb 2017" +# endif + +/*- + * The macros below are to be used for shared library (.so, .dll, ...) + * versioning. That kind of versioning works a bit differently between + * operating systems. The most usual scheme is to set a major and a minor + * number, and have the runtime loader check that the major number is equal + * to what it was at application link time, while the minor number has to + * be greater or equal to what it was at application link time. With this + * scheme, the version number is usually part of the file name, like this: + * + * libcrypto.so.0.9 + * + * Some unixen also make a softlink with the major version number only: + * + * libcrypto.so.0 + * + * On Tru64 and IRIX 6.x it works a little bit differently. There, the + * shared library version is stored in the file, and is actually a series + * of versions, separated by colons. The rightmost version present in the + * library when linking an application is stored in the application to be + * matched at run time. When the application is run, a check is done to + * see if the library version stored in the application matches any of the + * versions in the version string of the library itself. + * This version string can be constructed in any way, depending on what + * kind of matching is desired. However, to implement the same scheme as + * the one used in the other unixen, all compatible versions, from lowest + * to highest, should be part of the string. Consecutive builds would + * give the following versions strings: + * + * 3.0 + * 3.0:3.1 + * 3.0:3.1:3.2 + * 4.0 + * 4.0:4.1 + * + * Notice how version 4 is completely incompatible with version, and + * therefore give the breach you can see. + * + * There may be other schemes as well that I haven't yet discovered. + * + * So, here's the way it works here: first of all, the library version + * number doesn't need at all to match the overall OpenSSL version. + * However, it's nice and more understandable if it actually does. + * The current library version is stored in the macro SHLIB_VERSION_NUMBER, + * which is just a piece of text in the format "M.m.e" (Major, minor, edit). + * For the sake of Tru64, IRIX, and any other OS that behaves in similar ways, + * we need to keep a history of version numbers, which is done in the + * macro SHLIB_VERSION_HISTORY. The numbers are separated by colons and + * should only keep the versions that are binary compatible with the current. + */ +# define SHLIB_VERSION_HISTORY "" +# define SHLIB_VERSION_NUMBER "1.1" + + +#ifdef __cplusplus +} +#endif +#endif /* HEADER_OPENSSLV_H */ diff --git a/mobile/ios/FxA/FxA/include/openssl/ossl_typ.h b/mobile/ios/FxA/FxA/include/openssl/ossl_typ.h new file mode 100644 index 0000000000..129a67f057 --- /dev/null +++ b/mobile/ios/FxA/FxA/include/openssl/ossl_typ.h @@ -0,0 +1,190 @@ +/* + * Copyright 2001-2016 The OpenSSL Project Authors. All Rights Reserved. + * + * Licensed under the OpenSSL license (the "License"). You may not use + * this file except in compliance with the License. You can obtain a copy + * in the file LICENSE in the source distribution or at + * https://www.openssl.org/source/license.html + */ + +#ifndef HEADER_OPENSSL_TYPES_H +# define HEADER_OPENSSL_TYPES_H + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +# include + +# ifdef NO_ASN1_TYPEDEFS +# define ASN1_INTEGER ASN1_STRING +# define ASN1_ENUMERATED ASN1_STRING +# define ASN1_BIT_STRING ASN1_STRING +# define ASN1_OCTET_STRING ASN1_STRING +# define ASN1_PRINTABLESTRING ASN1_STRING +# define ASN1_T61STRING ASN1_STRING +# define ASN1_IA5STRING ASN1_STRING +# define ASN1_UTCTIME ASN1_STRING +# define ASN1_GENERALIZEDTIME ASN1_STRING +# define ASN1_TIME ASN1_STRING +# define ASN1_GENERALSTRING ASN1_STRING +# define ASN1_UNIVERSALSTRING ASN1_STRING +# define ASN1_BMPSTRING ASN1_STRING +# define ASN1_VISIBLESTRING ASN1_STRING +# define ASN1_UTF8STRING ASN1_STRING +# define ASN1_BOOLEAN int +# define ASN1_NULL int +# else +typedef struct asn1_string_st ASN1_INTEGER; +typedef struct asn1_string_st ASN1_ENUMERATED; +typedef struct asn1_string_st ASN1_BIT_STRING; +typedef struct asn1_string_st ASN1_OCTET_STRING; +typedef struct asn1_string_st ASN1_PRINTABLESTRING; +typedef struct asn1_string_st ASN1_T61STRING; +typedef struct asn1_string_st ASN1_IA5STRING; +typedef struct asn1_string_st ASN1_GENERALSTRING; +typedef struct asn1_string_st ASN1_UNIVERSALSTRING; +typedef struct asn1_string_st ASN1_BMPSTRING; +typedef struct asn1_string_st ASN1_UTCTIME; +typedef struct asn1_string_st ASN1_TIME; +typedef struct asn1_string_st ASN1_GENERALIZEDTIME; +typedef struct asn1_string_st ASN1_VISIBLESTRING; +typedef struct asn1_string_st ASN1_UTF8STRING; +typedef struct asn1_string_st ASN1_STRING; +typedef int ASN1_BOOLEAN; +typedef int ASN1_NULL; +# endif + +typedef struct asn1_object_st ASN1_OBJECT; + +typedef struct ASN1_ITEM_st ASN1_ITEM; +typedef struct asn1_pctx_st ASN1_PCTX; +typedef struct asn1_sctx_st ASN1_SCTX; + +# ifdef _WIN32 +# undef X509_NAME +# undef X509_EXTENSIONS +# undef PKCS7_ISSUER_AND_SERIAL +# undef PKCS7_SIGNER_INFO +# undef OCSP_REQUEST +# undef OCSP_RESPONSE +# endif + +# ifdef BIGNUM +# undef BIGNUM +# endif +struct dane_st; +typedef struct bio_st BIO; +typedef struct bignum_st BIGNUM; +typedef struct bignum_ctx BN_CTX; +typedef struct bn_blinding_st BN_BLINDING; +typedef struct bn_mont_ctx_st BN_MONT_CTX; +typedef struct bn_recp_ctx_st BN_RECP_CTX; +typedef struct bn_gencb_st BN_GENCB; + +typedef struct buf_mem_st BUF_MEM; + +typedef struct evp_cipher_st EVP_CIPHER; +typedef struct evp_cipher_ctx_st EVP_CIPHER_CTX; +typedef struct evp_md_st EVP_MD; +typedef struct evp_md_ctx_st EVP_MD_CTX; +typedef struct evp_pkey_st EVP_PKEY; + +typedef struct evp_pkey_asn1_method_st EVP_PKEY_ASN1_METHOD; + +typedef struct evp_pkey_method_st EVP_PKEY_METHOD; +typedef struct evp_pkey_ctx_st EVP_PKEY_CTX; + +typedef struct evp_Encode_Ctx_st EVP_ENCODE_CTX; + +typedef struct hmac_ctx_st HMAC_CTX; + +typedef struct dh_st DH; +typedef struct dh_method DH_METHOD; + +typedef struct dsa_st DSA; +typedef struct dsa_method DSA_METHOD; + +typedef struct rsa_st RSA; +typedef struct rsa_meth_st RSA_METHOD; + +typedef struct ec_key_st EC_KEY; +typedef struct ec_key_method_st EC_KEY_METHOD; + +typedef struct rand_meth_st RAND_METHOD; + +typedef struct ssl_dane_st SSL_DANE; +typedef struct x509_st X509; +typedef struct X509_algor_st X509_ALGOR; +typedef struct X509_crl_st X509_CRL; +typedef struct x509_crl_method_st X509_CRL_METHOD; +typedef struct x509_revoked_st X509_REVOKED; +typedef struct X509_name_st X509_NAME; +typedef struct X509_pubkey_st X509_PUBKEY; +typedef struct x509_store_st X509_STORE; +typedef struct x509_store_ctx_st X509_STORE_CTX; + +typedef struct x509_object_st X509_OBJECT; +typedef struct x509_lookup_st X509_LOOKUP; +typedef struct x509_lookup_method_st X509_LOOKUP_METHOD; +typedef struct X509_VERIFY_PARAM_st X509_VERIFY_PARAM; + +typedef struct pkcs8_priv_key_info_st PKCS8_PRIV_KEY_INFO; + +typedef struct v3_ext_ctx X509V3_CTX; +typedef struct conf_st CONF; +typedef struct ossl_init_settings_st OPENSSL_INIT_SETTINGS; + +typedef struct ui_st UI; +typedef struct ui_method_st UI_METHOD; + +typedef struct engine_st ENGINE; +typedef struct ssl_st SSL; +typedef struct ssl_ctx_st SSL_CTX; + +typedef struct comp_ctx_st COMP_CTX; +typedef struct comp_method_st COMP_METHOD; + +typedef struct X509_POLICY_NODE_st X509_POLICY_NODE; +typedef struct X509_POLICY_LEVEL_st X509_POLICY_LEVEL; +typedef struct X509_POLICY_TREE_st X509_POLICY_TREE; +typedef struct X509_POLICY_CACHE_st X509_POLICY_CACHE; + +typedef struct AUTHORITY_KEYID_st AUTHORITY_KEYID; +typedef struct DIST_POINT_st DIST_POINT; +typedef struct ISSUING_DIST_POINT_st ISSUING_DIST_POINT; +typedef struct NAME_CONSTRAINTS_st NAME_CONSTRAINTS; + +typedef struct crypto_ex_data_st CRYPTO_EX_DATA; + +typedef struct ocsp_req_ctx_st OCSP_REQ_CTX; +typedef struct ocsp_response_st OCSP_RESPONSE; +typedef struct ocsp_responder_id_st OCSP_RESPID; + +typedef struct sct_st SCT; +typedef struct sct_ctx_st SCT_CTX; +typedef struct ctlog_st CTLOG; +typedef struct ctlog_store_st CTLOG_STORE; +typedef struct ct_policy_eval_ctx_st CT_POLICY_EVAL_CTX; + +#if defined(__STDC_VERSION__) && __STDC_VERSION__ >= 199901L && \ + defined(INTMAX_MAX) && defined(UINTMAX_MAX) +typedef intmax_t ossl_intmax_t; +typedef uintmax_t ossl_uintmax_t; +#else +/* + * Not long long, because the C-library can only be expected to provide + * strtoll(), strtoull() at the same time as intmax_t and strtoimax(), + * strtoumax(). Since we use these for parsing arguments, we need the + * conversion functions, not just the sizes. + */ +typedef long ossl_intmax_t; +typedef unsigned long ossl_uintmax_t; +#endif + +#ifdef __cplusplus +} +#endif +#endif /* def HEADER_OPENSSL_TYPES_H */ diff --git a/mobile/ios/FxA/FxA/include/openssl/pem.h b/mobile/ios/FxA/FxA/include/openssl/pem.h new file mode 100644 index 0000000000..2375d63553 --- /dev/null +++ b/mobile/ios/FxA/FxA/include/openssl/pem.h @@ -0,0 +1,501 @@ +/* + * Copyright 1995-2016 The OpenSSL Project Authors. All Rights Reserved. + * + * Licensed under the OpenSSL license (the "License"). You may not use + * this file except in compliance with the License. You can obtain a copy + * in the file LICENSE in the source distribution or at + * https://www.openssl.org/source/license.html + */ + +#ifndef HEADER_PEM_H +# define HEADER_PEM_H + +# include +# include +# include +# include +# include +# include + +#ifdef __cplusplus +extern "C" { +#endif + +# define PEM_BUFSIZE 1024 + +# define PEM_STRING_X509_OLD "X509 CERTIFICATE" +# define PEM_STRING_X509 "CERTIFICATE" +# define PEM_STRING_X509_TRUSTED "TRUSTED CERTIFICATE" +# define PEM_STRING_X509_REQ_OLD "NEW CERTIFICATE REQUEST" +# define PEM_STRING_X509_REQ "CERTIFICATE REQUEST" +# define PEM_STRING_X509_CRL "X509 CRL" +# define PEM_STRING_EVP_PKEY "ANY PRIVATE KEY" +# define PEM_STRING_PUBLIC "PUBLIC KEY" +# define PEM_STRING_RSA "RSA PRIVATE KEY" +# define PEM_STRING_RSA_PUBLIC "RSA PUBLIC KEY" +# define PEM_STRING_DSA "DSA PRIVATE KEY" +# define PEM_STRING_DSA_PUBLIC "DSA PUBLIC KEY" +# define PEM_STRING_PKCS7 "PKCS7" +# define PEM_STRING_PKCS7_SIGNED "PKCS #7 SIGNED DATA" +# define PEM_STRING_PKCS8 "ENCRYPTED PRIVATE KEY" +# define PEM_STRING_PKCS8INF "PRIVATE KEY" +# define PEM_STRING_DHPARAMS "DH PARAMETERS" +# define PEM_STRING_DHXPARAMS "X9.42 DH PARAMETERS" +# define PEM_STRING_SSL_SESSION "SSL SESSION PARAMETERS" +# define PEM_STRING_DSAPARAMS "DSA PARAMETERS" +# define PEM_STRING_ECDSA_PUBLIC "ECDSA PUBLIC KEY" +# define PEM_STRING_ECPARAMETERS "EC PARAMETERS" +# define PEM_STRING_ECPRIVATEKEY "EC PRIVATE KEY" +# define PEM_STRING_PARAMETERS "PARAMETERS" +# define PEM_STRING_CMS "CMS" + +# define PEM_TYPE_ENCRYPTED 10 +# define PEM_TYPE_MIC_ONLY 20 +# define PEM_TYPE_MIC_CLEAR 30 +# define PEM_TYPE_CLEAR 40 + +typedef struct pem_recip_st { + char *name; + X509_NAME *dn; + int cipher; + int key_enc; + /* char iv[8]; unused and wrong size */ +} PEM_USER; + +typedef struct pem_ctx_st { + int type; /* what type of object */ + struct { + int version; + int mode; + } proc_type; + + char *domain; + + struct { + int cipher; + /*- + unused, and wrong size + unsigned char iv[8]; */ + } DEK_info; + + PEM_USER *originator; + + int num_recipient; + PEM_USER **recipient; + +/*- + XXX(ben): don#t think this is used! + STACK *x509_chain; / * certificate chain */ + EVP_MD *md; /* signature type */ + + int md_enc; /* is the md encrypted or not? */ + int md_len; /* length of md_data */ + char *md_data; /* message digest, could be pkey encrypted */ + + EVP_CIPHER *dec; /* date encryption cipher */ + int key_len; /* key length */ + unsigned char *key; /* key */ + /*- + unused, and wrong size + unsigned char iv[8]; */ + + int data_enc; /* is the data encrypted */ + int data_len; + unsigned char *data; +} PEM_CTX; + +/* + * These macros make the PEM_read/PEM_write functions easier to maintain and + * write. Now they are all implemented with either: IMPLEMENT_PEM_rw(...) or + * IMPLEMENT_PEM_rw_cb(...) + */ + +# ifdef OPENSSL_NO_STDIO + +# define IMPLEMENT_PEM_read_fp(name, type, str, asn1) /**/ +# define IMPLEMENT_PEM_write_fp(name, type, str, asn1) /**/ +# define IMPLEMENT_PEM_write_fp_const(name, type, str, asn1) /**/ +# define IMPLEMENT_PEM_write_cb_fp(name, type, str, asn1) /**/ +# define IMPLEMENT_PEM_write_cb_fp_const(name, type, str, asn1) /**/ +# else + +# define IMPLEMENT_PEM_read_fp(name, type, str, asn1) \ +type *PEM_read_##name(FILE *fp, type **x, pem_password_cb *cb, void *u)\ +{ \ +return PEM_ASN1_read((d2i_of_void *)d2i_##asn1, str,fp,(void **)x,cb,u); \ +} + +# define IMPLEMENT_PEM_write_fp(name, type, str, asn1) \ +int PEM_write_##name(FILE *fp, type *x) \ +{ \ +return PEM_ASN1_write((i2d_of_void *)i2d_##asn1,str,fp,x,NULL,NULL,0,NULL,NULL); \ +} + +# define IMPLEMENT_PEM_write_fp_const(name, type, str, asn1) \ +int PEM_write_##name(FILE *fp, const type *x) \ +{ \ +return PEM_ASN1_write((i2d_of_void *)i2d_##asn1,str,fp,(void *)x,NULL,NULL,0,NULL,NULL); \ +} + +# define IMPLEMENT_PEM_write_cb_fp(name, type, str, asn1) \ +int PEM_write_##name(FILE *fp, type *x, const EVP_CIPHER *enc, \ + unsigned char *kstr, int klen, pem_password_cb *cb, \ + void *u) \ + { \ + return PEM_ASN1_write((i2d_of_void *)i2d_##asn1,str,fp,x,enc,kstr,klen,cb,u); \ + } + +# define IMPLEMENT_PEM_write_cb_fp_const(name, type, str, asn1) \ +int PEM_write_##name(FILE *fp, type *x, const EVP_CIPHER *enc, \ + unsigned char *kstr, int klen, pem_password_cb *cb, \ + void *u) \ + { \ + return PEM_ASN1_write((i2d_of_void *)i2d_##asn1,str,fp,x,enc,kstr,klen,cb,u); \ + } + +# endif + +# define IMPLEMENT_PEM_read_bio(name, type, str, asn1) \ +type *PEM_read_bio_##name(BIO *bp, type **x, pem_password_cb *cb, void *u)\ +{ \ +return PEM_ASN1_read_bio((d2i_of_void *)d2i_##asn1, str,bp,(void **)x,cb,u); \ +} + +# define IMPLEMENT_PEM_write_bio(name, type, str, asn1) \ +int PEM_write_bio_##name(BIO *bp, type *x) \ +{ \ +return PEM_ASN1_write_bio((i2d_of_void *)i2d_##asn1,str,bp,x,NULL,NULL,0,NULL,NULL); \ +} + +# define IMPLEMENT_PEM_write_bio_const(name, type, str, asn1) \ +int PEM_write_bio_##name(BIO *bp, const type *x) \ +{ \ +return PEM_ASN1_write_bio((i2d_of_void *)i2d_##asn1,str,bp,(void *)x,NULL,NULL,0,NULL,NULL); \ +} + +# define IMPLEMENT_PEM_write_cb_bio(name, type, str, asn1) \ +int PEM_write_bio_##name(BIO *bp, type *x, const EVP_CIPHER *enc, \ + unsigned char *kstr, int klen, pem_password_cb *cb, void *u) \ + { \ + return PEM_ASN1_write_bio((i2d_of_void *)i2d_##asn1,str,bp,x,enc,kstr,klen,cb,u); \ + } + +# define IMPLEMENT_PEM_write_cb_bio_const(name, type, str, asn1) \ +int PEM_write_bio_##name(BIO *bp, type *x, const EVP_CIPHER *enc, \ + unsigned char *kstr, int klen, pem_password_cb *cb, void *u) \ + { \ + return PEM_ASN1_write_bio((i2d_of_void *)i2d_##asn1,str,bp,(void *)x,enc,kstr,klen,cb,u); \ + } + +# define IMPLEMENT_PEM_write(name, type, str, asn1) \ + IMPLEMENT_PEM_write_bio(name, type, str, asn1) \ + IMPLEMENT_PEM_write_fp(name, type, str, asn1) + +# define IMPLEMENT_PEM_write_const(name, type, str, asn1) \ + IMPLEMENT_PEM_write_bio_const(name, type, str, asn1) \ + IMPLEMENT_PEM_write_fp_const(name, type, str, asn1) + +# define IMPLEMENT_PEM_write_cb(name, type, str, asn1) \ + IMPLEMENT_PEM_write_cb_bio(name, type, str, asn1) \ + IMPLEMENT_PEM_write_cb_fp(name, type, str, asn1) + +# define IMPLEMENT_PEM_write_cb_const(name, type, str, asn1) \ + IMPLEMENT_PEM_write_cb_bio_const(name, type, str, asn1) \ + IMPLEMENT_PEM_write_cb_fp_const(name, type, str, asn1) + +# define IMPLEMENT_PEM_read(name, type, str, asn1) \ + IMPLEMENT_PEM_read_bio(name, type, str, asn1) \ + IMPLEMENT_PEM_read_fp(name, type, str, asn1) + +# define IMPLEMENT_PEM_rw(name, type, str, asn1) \ + IMPLEMENT_PEM_read(name, type, str, asn1) \ + IMPLEMENT_PEM_write(name, type, str, asn1) + +# define IMPLEMENT_PEM_rw_const(name, type, str, asn1) \ + IMPLEMENT_PEM_read(name, type, str, asn1) \ + IMPLEMENT_PEM_write_const(name, type, str, asn1) + +# define IMPLEMENT_PEM_rw_cb(name, type, str, asn1) \ + IMPLEMENT_PEM_read(name, type, str, asn1) \ + IMPLEMENT_PEM_write_cb(name, type, str, asn1) + +/* These are the same except they are for the declarations */ + +# if defined(OPENSSL_NO_STDIO) + +# define DECLARE_PEM_read_fp(name, type) /**/ +# define DECLARE_PEM_write_fp(name, type) /**/ +# define DECLARE_PEM_write_fp_const(name, type) /**/ +# define DECLARE_PEM_write_cb_fp(name, type) /**/ +# else + +# define DECLARE_PEM_read_fp(name, type) \ + type *PEM_read_##name(FILE *fp, type **x, pem_password_cb *cb, void *u); + +# define DECLARE_PEM_write_fp(name, type) \ + int PEM_write_##name(FILE *fp, type *x); + +# define DECLARE_PEM_write_fp_const(name, type) \ + int PEM_write_##name(FILE *fp, const type *x); + +# define DECLARE_PEM_write_cb_fp(name, type) \ + int PEM_write_##name(FILE *fp, type *x, const EVP_CIPHER *enc, \ + unsigned char *kstr, int klen, pem_password_cb *cb, void *u); + +# endif + +# define DECLARE_PEM_read_bio(name, type) \ + type *PEM_read_bio_##name(BIO *bp, type **x, pem_password_cb *cb, void *u); + +# define DECLARE_PEM_write_bio(name, type) \ + int PEM_write_bio_##name(BIO *bp, type *x); + +# define DECLARE_PEM_write_bio_const(name, type) \ + int PEM_write_bio_##name(BIO *bp, const type *x); + +# define DECLARE_PEM_write_cb_bio(name, type) \ + int PEM_write_bio_##name(BIO *bp, type *x, const EVP_CIPHER *enc, \ + unsigned char *kstr, int klen, pem_password_cb *cb, void *u); + +# define DECLARE_PEM_write(name, type) \ + DECLARE_PEM_write_bio(name, type) \ + DECLARE_PEM_write_fp(name, type) +# define DECLARE_PEM_write_const(name, type) \ + DECLARE_PEM_write_bio_const(name, type) \ + DECLARE_PEM_write_fp_const(name, type) +# define DECLARE_PEM_write_cb(name, type) \ + DECLARE_PEM_write_cb_bio(name, type) \ + DECLARE_PEM_write_cb_fp(name, type) +# define DECLARE_PEM_read(name, type) \ + DECLARE_PEM_read_bio(name, type) \ + DECLARE_PEM_read_fp(name, type) +# define DECLARE_PEM_rw(name, type) \ + DECLARE_PEM_read(name, type) \ + DECLARE_PEM_write(name, type) +# define DECLARE_PEM_rw_const(name, type) \ + DECLARE_PEM_read(name, type) \ + DECLARE_PEM_write_const(name, type) +# define DECLARE_PEM_rw_cb(name, type) \ + DECLARE_PEM_read(name, type) \ + DECLARE_PEM_write_cb(name, type) +typedef int pem_password_cb (char *buf, int size, int rwflag, void *userdata); + +int PEM_get_EVP_CIPHER_INFO(char *header, EVP_CIPHER_INFO *cipher); +int PEM_do_header(EVP_CIPHER_INFO *cipher, unsigned char *data, long *len, + pem_password_cb *callback, void *u); + +int PEM_read_bio(BIO *bp, char **name, char **header, + unsigned char **data, long *len); +int PEM_write_bio(BIO *bp, const char *name, const char *hdr, + const unsigned char *data, long len); +int PEM_bytes_read_bio(unsigned char **pdata, long *plen, char **pnm, + const char *name, BIO *bp, pem_password_cb *cb, + void *u); +void *PEM_ASN1_read_bio(d2i_of_void *d2i, const char *name, BIO *bp, void **x, + pem_password_cb *cb, void *u); +int PEM_ASN1_write_bio(i2d_of_void *i2d, const char *name, BIO *bp, void *x, + const EVP_CIPHER *enc, unsigned char *kstr, int klen, + pem_password_cb *cb, void *u); + +STACK_OF(X509_INFO) *PEM_X509_INFO_read_bio(BIO *bp, STACK_OF(X509_INFO) *sk, + pem_password_cb *cb, void *u); +int PEM_X509_INFO_write_bio(BIO *bp, X509_INFO *xi, EVP_CIPHER *enc, + unsigned char *kstr, int klen, + pem_password_cb *cd, void *u); + +#ifndef OPENSSL_NO_STDIO +int PEM_read(FILE *fp, char **name, char **header, + unsigned char **data, long *len); +int PEM_write(FILE *fp, const char *name, const char *hdr, + const unsigned char *data, long len); +void *PEM_ASN1_read(d2i_of_void *d2i, const char *name, FILE *fp, void **x, + pem_password_cb *cb, void *u); +int PEM_ASN1_write(i2d_of_void *i2d, const char *name, FILE *fp, + void *x, const EVP_CIPHER *enc, unsigned char *kstr, + int klen, pem_password_cb *callback, void *u); +STACK_OF(X509_INFO) *PEM_X509_INFO_read(FILE *fp, STACK_OF(X509_INFO) *sk, + pem_password_cb *cb, void *u); +#endif + +int PEM_SignInit(EVP_MD_CTX *ctx, EVP_MD *type); +int PEM_SignUpdate(EVP_MD_CTX *ctx, unsigned char *d, unsigned int cnt); +int PEM_SignFinal(EVP_MD_CTX *ctx, unsigned char *sigret, + unsigned int *siglen, EVP_PKEY *pkey); + +int PEM_def_callback(char *buf, int num, int w, void *key); +void PEM_proc_type(char *buf, int type); +void PEM_dek_info(char *buf, const char *type, int len, char *str); + +# include + +DECLARE_PEM_rw(X509, X509) +DECLARE_PEM_rw(X509_AUX, X509) +DECLARE_PEM_rw(X509_REQ, X509_REQ) +DECLARE_PEM_write(X509_REQ_NEW, X509_REQ) +DECLARE_PEM_rw(X509_CRL, X509_CRL) +DECLARE_PEM_rw(PKCS7, PKCS7) +DECLARE_PEM_rw(NETSCAPE_CERT_SEQUENCE, NETSCAPE_CERT_SEQUENCE) +DECLARE_PEM_rw(PKCS8, X509_SIG) +DECLARE_PEM_rw(PKCS8_PRIV_KEY_INFO, PKCS8_PRIV_KEY_INFO) +# ifndef OPENSSL_NO_RSA +DECLARE_PEM_rw_cb(RSAPrivateKey, RSA) +DECLARE_PEM_rw_const(RSAPublicKey, RSA) +DECLARE_PEM_rw(RSA_PUBKEY, RSA) +# endif +# ifndef OPENSSL_NO_DSA +DECLARE_PEM_rw_cb(DSAPrivateKey, DSA) +DECLARE_PEM_rw(DSA_PUBKEY, DSA) +DECLARE_PEM_rw_const(DSAparams, DSA) +# endif +# ifndef OPENSSL_NO_EC +DECLARE_PEM_rw_const(ECPKParameters, EC_GROUP) +DECLARE_PEM_rw_cb(ECPrivateKey, EC_KEY) +DECLARE_PEM_rw(EC_PUBKEY, EC_KEY) +# endif +# ifndef OPENSSL_NO_DH +DECLARE_PEM_rw_const(DHparams, DH) +DECLARE_PEM_write_const(DHxparams, DH) +# endif +DECLARE_PEM_rw_cb(PrivateKey, EVP_PKEY) +DECLARE_PEM_rw(PUBKEY, EVP_PKEY) + +int PEM_write_bio_PrivateKey_traditional(BIO *bp, EVP_PKEY *x, + const EVP_CIPHER *enc, + unsigned char *kstr, int klen, + pem_password_cb *cb, void *u); + +int PEM_write_bio_PKCS8PrivateKey_nid(BIO *bp, EVP_PKEY *x, int nid, + char *kstr, int klen, + pem_password_cb *cb, void *u); +int PEM_write_bio_PKCS8PrivateKey(BIO *, EVP_PKEY *, const EVP_CIPHER *, + char *, int, pem_password_cb *, void *); +int i2d_PKCS8PrivateKey_bio(BIO *bp, EVP_PKEY *x, const EVP_CIPHER *enc, + char *kstr, int klen, + pem_password_cb *cb, void *u); +int i2d_PKCS8PrivateKey_nid_bio(BIO *bp, EVP_PKEY *x, int nid, + char *kstr, int klen, + pem_password_cb *cb, void *u); +EVP_PKEY *d2i_PKCS8PrivateKey_bio(BIO *bp, EVP_PKEY **x, pem_password_cb *cb, + void *u); + +# ifndef OPENSSL_NO_STDIO +int i2d_PKCS8PrivateKey_fp(FILE *fp, EVP_PKEY *x, const EVP_CIPHER *enc, + char *kstr, int klen, + pem_password_cb *cb, void *u); +int i2d_PKCS8PrivateKey_nid_fp(FILE *fp, EVP_PKEY *x, int nid, + char *kstr, int klen, + pem_password_cb *cb, void *u); +int PEM_write_PKCS8PrivateKey_nid(FILE *fp, EVP_PKEY *x, int nid, + char *kstr, int klen, + pem_password_cb *cb, void *u); + +EVP_PKEY *d2i_PKCS8PrivateKey_fp(FILE *fp, EVP_PKEY **x, pem_password_cb *cb, + void *u); + +int PEM_write_PKCS8PrivateKey(FILE *fp, EVP_PKEY *x, const EVP_CIPHER *enc, + char *kstr, int klen, pem_password_cb *cd, + void *u); +# endif +EVP_PKEY *PEM_read_bio_Parameters(BIO *bp, EVP_PKEY **x); +int PEM_write_bio_Parameters(BIO *bp, EVP_PKEY *x); + +# ifndef OPENSSL_NO_DSA +EVP_PKEY *b2i_PrivateKey(const unsigned char **in, long length); +EVP_PKEY *b2i_PublicKey(const unsigned char **in, long length); +EVP_PKEY *b2i_PrivateKey_bio(BIO *in); +EVP_PKEY *b2i_PublicKey_bio(BIO *in); +int i2b_PrivateKey_bio(BIO *out, EVP_PKEY *pk); +int i2b_PublicKey_bio(BIO *out, EVP_PKEY *pk); +# ifndef OPENSSL_NO_RC4 +EVP_PKEY *b2i_PVK_bio(BIO *in, pem_password_cb *cb, void *u); +int i2b_PVK_bio(BIO *out, EVP_PKEY *pk, int enclevel, + pem_password_cb *cb, void *u); +# endif +# endif + +/* BEGIN ERROR CODES */ +/* + * The following lines are auto generated by the script mkerr.pl. Any changes + * made after this point may be overwritten when the script is next run. + */ + +int ERR_load_PEM_strings(void); + +/* Error codes for the PEM functions. */ + +/* Function codes. */ +# define PEM_F_B2I_DSS 127 +# define PEM_F_B2I_PVK_BIO 128 +# define PEM_F_B2I_RSA 129 +# define PEM_F_CHECK_BITLEN_DSA 130 +# define PEM_F_CHECK_BITLEN_RSA 131 +# define PEM_F_D2I_PKCS8PRIVATEKEY_BIO 120 +# define PEM_F_D2I_PKCS8PRIVATEKEY_FP 121 +# define PEM_F_DO_B2I 132 +# define PEM_F_DO_B2I_BIO 133 +# define PEM_F_DO_BLOB_HEADER 134 +# define PEM_F_DO_PK8PKEY 126 +# define PEM_F_DO_PK8PKEY_FP 125 +# define PEM_F_DO_PVK_BODY 135 +# define PEM_F_DO_PVK_HEADER 136 +# define PEM_F_I2B_PVK 137 +# define PEM_F_I2B_PVK_BIO 138 +# define PEM_F_LOAD_IV 101 +# define PEM_F_PEM_ASN1_READ 102 +# define PEM_F_PEM_ASN1_READ_BIO 103 +# define PEM_F_PEM_ASN1_WRITE 104 +# define PEM_F_PEM_ASN1_WRITE_BIO 105 +# define PEM_F_PEM_DEF_CALLBACK 100 +# define PEM_F_PEM_DO_HEADER 106 +# define PEM_F_PEM_GET_EVP_CIPHER_INFO 107 +# define PEM_F_PEM_READ 108 +# define PEM_F_PEM_READ_BIO 109 +# define PEM_F_PEM_READ_BIO_DHPARAMS 141 +# define PEM_F_PEM_READ_BIO_PARAMETERS 140 +# define PEM_F_PEM_READ_BIO_PRIVATEKEY 123 +# define PEM_F_PEM_READ_DHPARAMS 142 +# define PEM_F_PEM_READ_PRIVATEKEY 124 +# define PEM_F_PEM_SIGNFINAL 112 +# define PEM_F_PEM_WRITE 113 +# define PEM_F_PEM_WRITE_BIO 114 +# define PEM_F_PEM_WRITE_PRIVATEKEY 139 +# define PEM_F_PEM_X509_INFO_READ 115 +# define PEM_F_PEM_X509_INFO_READ_BIO 116 +# define PEM_F_PEM_X509_INFO_WRITE_BIO 117 + +/* Reason codes. */ +# define PEM_R_BAD_BASE64_DECODE 100 +# define PEM_R_BAD_DECRYPT 101 +# define PEM_R_BAD_END_LINE 102 +# define PEM_R_BAD_IV_CHARS 103 +# define PEM_R_BAD_MAGIC_NUMBER 116 +# define PEM_R_BAD_PASSWORD_READ 104 +# define PEM_R_BAD_VERSION_NUMBER 117 +# define PEM_R_BIO_WRITE_FAILURE 118 +# define PEM_R_CIPHER_IS_NULL 127 +# define PEM_R_ERROR_CONVERTING_PRIVATE_KEY 115 +# define PEM_R_EXPECTING_PRIVATE_KEY_BLOB 119 +# define PEM_R_EXPECTING_PUBLIC_KEY_BLOB 120 +# define PEM_R_HEADER_TOO_LONG 128 +# define PEM_R_INCONSISTENT_HEADER 121 +# define PEM_R_KEYBLOB_HEADER_PARSE_ERROR 122 +# define PEM_R_KEYBLOB_TOO_SHORT 123 +# define PEM_R_MISSING_DEK_IV 129 +# define PEM_R_NOT_DEK_INFO 105 +# define PEM_R_NOT_ENCRYPTED 106 +# define PEM_R_NOT_PROC_TYPE 107 +# define PEM_R_NO_START_LINE 108 +# define PEM_R_PROBLEMS_GETTING_PASSWORD 109 +# define PEM_R_PVK_DATA_TOO_SHORT 124 +# define PEM_R_PVK_TOO_SHORT 125 +# define PEM_R_READ_KEY 111 +# define PEM_R_SHORT_HEADER 112 +# define PEM_R_UNEXPECTED_DEK_IV 130 +# define PEM_R_UNSUPPORTED_CIPHER 113 +# define PEM_R_UNSUPPORTED_ENCRYPTION 114 +# define PEM_R_UNSUPPORTED_KEY_COMPONENTS 126 + +# ifdef __cplusplus +} +# endif +#endif diff --git a/mobile/ios/FxA/FxA/include/openssl/pem2.h b/mobile/ios/FxA/FxA/include/openssl/pem2.h new file mode 100644 index 0000000000..cfe73f139e --- /dev/null +++ b/mobile/ios/FxA/FxA/include/openssl/pem2.h @@ -0,0 +1,20 @@ +/* + * Copyright 1999-2016 The OpenSSL Project Authors. All Rights Reserved. + * + * Licensed under the OpenSSL license (the "License"). You may not use + * this file except in compliance with the License. You can obtain a copy + * in the file LICENSE in the source distribution or at + * https://www.openssl.org/source/license.html + */ + +#ifdef __cplusplus +extern "C" { +#endif + +#ifndef HEADER_PEM_H +int ERR_load_PEM_strings(void); +#endif + +#ifdef __cplusplus +} +#endif diff --git a/mobile/ios/FxA/FxA/include/openssl/pkcs12.h b/mobile/ios/FxA/FxA/include/openssl/pkcs12.h new file mode 100644 index 0000000000..deaded9df9 --- /dev/null +++ b/mobile/ios/FxA/FxA/include/openssl/pkcs12.h @@ -0,0 +1,282 @@ +/* + * Copyright 1999-2016 The OpenSSL Project Authors. All Rights Reserved. + * + * Licensed under the OpenSSL license (the "License"). You may not use + * this file except in compliance with the License. You can obtain a copy + * in the file LICENSE in the source distribution or at + * https://www.openssl.org/source/license.html + */ + +#ifndef HEADER_PKCS12_H +# define HEADER_PKCS12_H + +# include +# include + +#ifdef __cplusplus +extern "C" { +#endif + +# define PKCS12_KEY_ID 1 +# define PKCS12_IV_ID 2 +# define PKCS12_MAC_ID 3 + +/* Default iteration count */ +# ifndef PKCS12_DEFAULT_ITER +# define PKCS12_DEFAULT_ITER PKCS5_DEFAULT_ITER +# endif + +# define PKCS12_MAC_KEY_LENGTH 20 + +# define PKCS12_SALT_LEN 8 + +/* It's not clear if these are actually needed... */ +# define PKCS12_key_gen PKCS12_key_gen_utf8 +# define PKCS12_add_friendlyname PKCS12_add_friendlyname_utf8 + +/* MS key usage constants */ + +# define KEY_EX 0x10 +# define KEY_SIG 0x80 + +typedef struct PKCS12_MAC_DATA_st PKCS12_MAC_DATA; + +typedef struct PKCS12_st PKCS12; + +typedef struct PKCS12_SAFEBAG_st PKCS12_SAFEBAG; + +DEFINE_STACK_OF(PKCS12_SAFEBAG) + +typedef struct pkcs12_bag_st PKCS12_BAGS; + +# define PKCS12_ERROR 0 +# define PKCS12_OK 1 + +/* Compatibility macros */ + +#if OPENSSL_API_COMPAT < 0x10100000L + +# define M_PKCS12_bag_type PKCS12_bag_type +# define M_PKCS12_cert_bag_type PKCS12_cert_bag_type +# define M_PKCS12_crl_bag_type PKCS12_cert_bag_type + +# define PKCS12_certbag2x509 PKCS12_SAFEBAG_get1_cert +# define PKCS12_certbag2scrl PKCS12_SAFEBAG_get1_crl +# define PKCS12_bag_type PKCS12_SAFEBAG_get_nid +# define PKCS12_cert_bag_type PKCS12_SAFEBAG_get_bag_nid +# define PKCS12_x5092certbag PKCS12_SAFEBAG_create_cert +# define PKCS12_x509crl2certbag PKCS12_SAFEBAG_create_crl +# define PKCS12_MAKE_KEYBAG PKCS12_SAFEBAG_create0_p8inf +# define PKCS12_MAKE_SHKEYBAG PKCS12_SAFEBAG_create_pkcs8_encrypt + +#endif + +DEPRECATEDIN_1_1_0(ASN1_TYPE *PKCS12_get_attr(const PKCS12_SAFEBAG *bag, int attr_nid)) + +ASN1_TYPE *PKCS8_get_attr(PKCS8_PRIV_KEY_INFO *p8, int attr_nid); +int PKCS12_mac_present(const PKCS12 *p12); +void PKCS12_get0_mac(const ASN1_OCTET_STRING **pmac, + const X509_ALGOR **pmacalg, + const ASN1_OCTET_STRING **psalt, + const ASN1_INTEGER **piter, + const PKCS12 *p12); + +const ASN1_TYPE *PKCS12_SAFEBAG_get0_attr(const PKCS12_SAFEBAG *bag, + int attr_nid); +const ASN1_OBJECT *PKCS12_SAFEBAG_get0_type(const PKCS12_SAFEBAG *bag); +int PKCS12_SAFEBAG_get_nid(const PKCS12_SAFEBAG *bag); +int PKCS12_SAFEBAG_get_bag_nid(const PKCS12_SAFEBAG *bag); + +X509 *PKCS12_SAFEBAG_get1_cert(const PKCS12_SAFEBAG *bag); +X509_CRL *PKCS12_SAFEBAG_get1_crl(const PKCS12_SAFEBAG *bag); +const STACK_OF(PKCS12_SAFEBAG) * +PKCS12_SAFEBAG_get0_safes(const PKCS12_SAFEBAG *bag); +const PKCS8_PRIV_KEY_INFO *PKCS12_SAFEBAG_get0_p8inf(const PKCS12_SAFEBAG *bag); +const X509_SIG *PKCS12_SAFEBAG_get0_pkcs8(const PKCS12_SAFEBAG *bag); + +PKCS12_SAFEBAG *PKCS12_SAFEBAG_create_cert(X509 *x509); +PKCS12_SAFEBAG *PKCS12_SAFEBAG_create_crl(X509_CRL *crl); +PKCS12_SAFEBAG *PKCS12_SAFEBAG_create0_p8inf(PKCS8_PRIV_KEY_INFO *p8); +PKCS12_SAFEBAG *PKCS12_SAFEBAG_create0_pkcs8(X509_SIG *p8); +PKCS12_SAFEBAG *PKCS12_SAFEBAG_create_pkcs8_encrypt(int pbe_nid, + const char *pass, + int passlen, + unsigned char *salt, + int saltlen, int iter, + PKCS8_PRIV_KEY_INFO *p8inf); + +PKCS12_SAFEBAG *PKCS12_item_pack_safebag(void *obj, const ASN1_ITEM *it, + int nid1, int nid2); +PKCS8_PRIV_KEY_INFO *PKCS8_decrypt(const X509_SIG *p8, const char *pass, + int passlen); +PKCS8_PRIV_KEY_INFO *PKCS12_decrypt_skey(const PKCS12_SAFEBAG *bag, + const char *pass, int passlen); +X509_SIG *PKCS8_encrypt(int pbe_nid, const EVP_CIPHER *cipher, + const char *pass, int passlen, unsigned char *salt, + int saltlen, int iter, PKCS8_PRIV_KEY_INFO *p8); +X509_SIG *PKCS8_set0_pbe(const char *pass, int passlen, + PKCS8_PRIV_KEY_INFO *p8inf, X509_ALGOR *pbe); +PKCS7 *PKCS12_pack_p7data(STACK_OF(PKCS12_SAFEBAG) *sk); +STACK_OF(PKCS12_SAFEBAG) *PKCS12_unpack_p7data(PKCS7 *p7); +PKCS7 *PKCS12_pack_p7encdata(int pbe_nid, const char *pass, int passlen, + unsigned char *salt, int saltlen, int iter, + STACK_OF(PKCS12_SAFEBAG) *bags); +STACK_OF(PKCS12_SAFEBAG) *PKCS12_unpack_p7encdata(PKCS7 *p7, const char *pass, + int passlen); + +int PKCS12_pack_authsafes(PKCS12 *p12, STACK_OF(PKCS7) *safes); +STACK_OF(PKCS7) *PKCS12_unpack_authsafes(const PKCS12 *p12); + +int PKCS12_add_localkeyid(PKCS12_SAFEBAG *bag, unsigned char *name, + int namelen); +int PKCS12_add_friendlyname_asc(PKCS12_SAFEBAG *bag, const char *name, + int namelen); +int PKCS12_add_friendlyname_utf8(PKCS12_SAFEBAG *bag, const char *name, + int namelen); +int PKCS12_add_CSPName_asc(PKCS12_SAFEBAG *bag, const char *name, + int namelen); +int PKCS12_add_friendlyname_uni(PKCS12_SAFEBAG *bag, + const unsigned char *name, int namelen); +int PKCS8_add_keyusage(PKCS8_PRIV_KEY_INFO *p8, int usage); +ASN1_TYPE *PKCS12_get_attr_gen(const STACK_OF(X509_ATTRIBUTE) *attrs, + int attr_nid); +char *PKCS12_get_friendlyname(PKCS12_SAFEBAG *bag); +const STACK_OF(X509_ATTRIBUTE) * +PKCS12_SAFEBAG_get0_attrs(const PKCS12_SAFEBAG *bag); +unsigned char *PKCS12_pbe_crypt(const X509_ALGOR *algor, + const char *pass, int passlen, + const unsigned char *in, int inlen, + unsigned char **data, int *datalen, + int en_de); +void *PKCS12_item_decrypt_d2i(const X509_ALGOR *algor, const ASN1_ITEM *it, + const char *pass, int passlen, + const ASN1_OCTET_STRING *oct, int zbuf); +ASN1_OCTET_STRING *PKCS12_item_i2d_encrypt(X509_ALGOR *algor, + const ASN1_ITEM *it, + const char *pass, int passlen, + void *obj, int zbuf); +PKCS12 *PKCS12_init(int mode); +int PKCS12_key_gen_asc(const char *pass, int passlen, unsigned char *salt, + int saltlen, int id, int iter, int n, + unsigned char *out, const EVP_MD *md_type); +int PKCS12_key_gen_uni(unsigned char *pass, int passlen, unsigned char *salt, + int saltlen, int id, int iter, int n, + unsigned char *out, const EVP_MD *md_type); +int PKCS12_key_gen_utf8(const char *pass, int passlen, unsigned char *salt, + int saltlen, int id, int iter, int n, + unsigned char *out, const EVP_MD *md_type); +int PKCS12_PBE_keyivgen(EVP_CIPHER_CTX *ctx, const char *pass, int passlen, + ASN1_TYPE *param, const EVP_CIPHER *cipher, + const EVP_MD *md_type, int en_de); +int PKCS12_gen_mac(PKCS12 *p12, const char *pass, int passlen, + unsigned char *mac, unsigned int *maclen); +int PKCS12_verify_mac(PKCS12 *p12, const char *pass, int passlen); +int PKCS12_set_mac(PKCS12 *p12, const char *pass, int passlen, + unsigned char *salt, int saltlen, int iter, + const EVP_MD *md_type); +int PKCS12_setup_mac(PKCS12 *p12, int iter, unsigned char *salt, + int saltlen, const EVP_MD *md_type); +unsigned char *OPENSSL_asc2uni(const char *asc, int asclen, + unsigned char **uni, int *unilen); +char *OPENSSL_uni2asc(const unsigned char *uni, int unilen); +unsigned char *OPENSSL_utf82uni(const char *asc, int asclen, + unsigned char **uni, int *unilen); +char *OPENSSL_uni2utf8(const unsigned char *uni, int unilen); + +DECLARE_ASN1_FUNCTIONS(PKCS12) +DECLARE_ASN1_FUNCTIONS(PKCS12_MAC_DATA) +DECLARE_ASN1_FUNCTIONS(PKCS12_SAFEBAG) +DECLARE_ASN1_FUNCTIONS(PKCS12_BAGS) + +DECLARE_ASN1_ITEM(PKCS12_SAFEBAGS) +DECLARE_ASN1_ITEM(PKCS12_AUTHSAFES) + +void PKCS12_PBE_add(void); +int PKCS12_parse(PKCS12 *p12, const char *pass, EVP_PKEY **pkey, X509 **cert, + STACK_OF(X509) **ca); +PKCS12 *PKCS12_create(const char *pass, const char *name, EVP_PKEY *pkey, + X509 *cert, STACK_OF(X509) *ca, int nid_key, int nid_cert, + int iter, int mac_iter, int keytype); + +PKCS12_SAFEBAG *PKCS12_add_cert(STACK_OF(PKCS12_SAFEBAG) **pbags, X509 *cert); +PKCS12_SAFEBAG *PKCS12_add_key(STACK_OF(PKCS12_SAFEBAG) **pbags, + EVP_PKEY *key, int key_usage, int iter, + int key_nid, const char *pass); +int PKCS12_add_safe(STACK_OF(PKCS7) **psafes, STACK_OF(PKCS12_SAFEBAG) *bags, + int safe_nid, int iter, const char *pass); +PKCS12 *PKCS12_add_safes(STACK_OF(PKCS7) *safes, int p7_nid); + +int i2d_PKCS12_bio(BIO *bp, PKCS12 *p12); +# ifndef OPENSSL_NO_STDIO +int i2d_PKCS12_fp(FILE *fp, PKCS12 *p12); +# endif +PKCS12 *d2i_PKCS12_bio(BIO *bp, PKCS12 **p12); +# ifndef OPENSSL_NO_STDIO +PKCS12 *d2i_PKCS12_fp(FILE *fp, PKCS12 **p12); +# endif +int PKCS12_newpass(PKCS12 *p12, const char *oldpass, const char *newpass); + +/* BEGIN ERROR CODES */ +/* + * The following lines are auto generated by the script mkerr.pl. Any changes + * made after this point may be overwritten when the script is next run. + */ + +int ERR_load_PKCS12_strings(void); + +/* Error codes for the PKCS12 functions. */ + +/* Function codes. */ +# define PKCS12_F_PKCS12_CREATE 105 +# define PKCS12_F_PKCS12_GEN_MAC 107 +# define PKCS12_F_PKCS12_INIT 109 +# define PKCS12_F_PKCS12_ITEM_DECRYPT_D2I 106 +# define PKCS12_F_PKCS12_ITEM_I2D_ENCRYPT 108 +# define PKCS12_F_PKCS12_ITEM_PACK_SAFEBAG 117 +# define PKCS12_F_PKCS12_KEY_GEN_ASC 110 +# define PKCS12_F_PKCS12_KEY_GEN_UNI 111 +# define PKCS12_F_PKCS12_KEY_GEN_UTF8 116 +# define PKCS12_F_PKCS12_NEWPASS 128 +# define PKCS12_F_PKCS12_PACK_P7DATA 114 +# define PKCS12_F_PKCS12_PACK_P7ENCDATA 115 +# define PKCS12_F_PKCS12_PARSE 118 +# define PKCS12_F_PKCS12_PBE_CRYPT 119 +# define PKCS12_F_PKCS12_PBE_KEYIVGEN 120 +# define PKCS12_F_PKCS12_SAFEBAG_CREATE0_P8INF 112 +# define PKCS12_F_PKCS12_SAFEBAG_CREATE0_PKCS8 113 +# define PKCS12_F_PKCS12_SAFEBAG_CREATE_PKCS8_ENCRYPT 133 +# define PKCS12_F_PKCS12_SETUP_MAC 122 +# define PKCS12_F_PKCS12_SET_MAC 123 +# define PKCS12_F_PKCS12_UNPACK_AUTHSAFES 130 +# define PKCS12_F_PKCS12_UNPACK_P7DATA 131 +# define PKCS12_F_PKCS12_VERIFY_MAC 126 +# define PKCS12_F_PKCS8_ENCRYPT 125 +# define PKCS12_F_PKCS8_SET0_PBE 132 + +/* Reason codes. */ +# define PKCS12_R_CANT_PACK_STRUCTURE 100 +# define PKCS12_R_CONTENT_TYPE_NOT_DATA 121 +# define PKCS12_R_DECODE_ERROR 101 +# define PKCS12_R_ENCODE_ERROR 102 +# define PKCS12_R_ENCRYPT_ERROR 103 +# define PKCS12_R_ERROR_SETTING_ENCRYPTED_DATA_TYPE 120 +# define PKCS12_R_INVALID_NULL_ARGUMENT 104 +# define PKCS12_R_INVALID_NULL_PKCS12_POINTER 105 +# define PKCS12_R_IV_GEN_ERROR 106 +# define PKCS12_R_KEY_GEN_ERROR 107 +# define PKCS12_R_MAC_ABSENT 108 +# define PKCS12_R_MAC_GENERATION_ERROR 109 +# define PKCS12_R_MAC_SETUP_ERROR 110 +# define PKCS12_R_MAC_STRING_SET_ERROR 111 +# define PKCS12_R_MAC_VERIFY_FAILURE 113 +# define PKCS12_R_PARSE_ERROR 114 +# define PKCS12_R_PKCS12_ALGOR_CIPHERINIT_ERROR 115 +# define PKCS12_R_PKCS12_CIPHERFINAL_ERROR 116 +# define PKCS12_R_PKCS12_PBE_CRYPT_ERROR 117 +# define PKCS12_R_UNKNOWN_DIGEST_ALGORITHM 118 +# define PKCS12_R_UNSUPPORTED_PKCS12_MODE 119 + +# ifdef __cplusplus +} +# endif +#endif diff --git a/mobile/ios/FxA/FxA/include/openssl/pkcs7.h b/mobile/ios/FxA/FxA/include/openssl/pkcs7.h new file mode 100644 index 0000000000..691f722022 --- /dev/null +++ b/mobile/ios/FxA/FxA/include/openssl/pkcs7.h @@ -0,0 +1,404 @@ +/* + * Copyright 1995-2016 The OpenSSL Project Authors. All Rights Reserved. + * + * Licensed under the OpenSSL license (the "License"). You may not use + * this file except in compliance with the License. You can obtain a copy + * in the file LICENSE in the source distribution or at + * https://www.openssl.org/source/license.html + */ + +#ifndef HEADER_PKCS7_H +# define HEADER_PKCS7_H + +# include +# include +# include + +# include +# include + +#ifdef __cplusplus +extern "C" { +#endif + +/*- +Encryption_ID DES-CBC +Digest_ID MD5 +Digest_Encryption_ID rsaEncryption +Key_Encryption_ID rsaEncryption +*/ + +typedef struct pkcs7_issuer_and_serial_st { + X509_NAME *issuer; + ASN1_INTEGER *serial; +} PKCS7_ISSUER_AND_SERIAL; + +typedef struct pkcs7_signer_info_st { + ASN1_INTEGER *version; /* version 1 */ + PKCS7_ISSUER_AND_SERIAL *issuer_and_serial; + X509_ALGOR *digest_alg; + STACK_OF(X509_ATTRIBUTE) *auth_attr; /* [ 0 ] */ + X509_ALGOR *digest_enc_alg; + ASN1_OCTET_STRING *enc_digest; + STACK_OF(X509_ATTRIBUTE) *unauth_attr; /* [ 1 ] */ + /* The private key to sign with */ + EVP_PKEY *pkey; +} PKCS7_SIGNER_INFO; + +DEFINE_STACK_OF(PKCS7_SIGNER_INFO) + +typedef struct pkcs7_recip_info_st { + ASN1_INTEGER *version; /* version 0 */ + PKCS7_ISSUER_AND_SERIAL *issuer_and_serial; + X509_ALGOR *key_enc_algor; + ASN1_OCTET_STRING *enc_key; + X509 *cert; /* get the pub-key from this */ +} PKCS7_RECIP_INFO; + +DEFINE_STACK_OF(PKCS7_RECIP_INFO) + +typedef struct pkcs7_signed_st { + ASN1_INTEGER *version; /* version 1 */ + STACK_OF(X509_ALGOR) *md_algs; /* md used */ + STACK_OF(X509) *cert; /* [ 0 ] */ + STACK_OF(X509_CRL) *crl; /* [ 1 ] */ + STACK_OF(PKCS7_SIGNER_INFO) *signer_info; + struct pkcs7_st *contents; +} PKCS7_SIGNED; +/* + * The above structure is very very similar to PKCS7_SIGN_ENVELOPE. How about + * merging the two + */ + +typedef struct pkcs7_enc_content_st { + ASN1_OBJECT *content_type; + X509_ALGOR *algorithm; + ASN1_OCTET_STRING *enc_data; /* [ 0 ] */ + const EVP_CIPHER *cipher; +} PKCS7_ENC_CONTENT; + +typedef struct pkcs7_enveloped_st { + ASN1_INTEGER *version; /* version 0 */ + STACK_OF(PKCS7_RECIP_INFO) *recipientinfo; + PKCS7_ENC_CONTENT *enc_data; +} PKCS7_ENVELOPE; + +typedef struct pkcs7_signedandenveloped_st { + ASN1_INTEGER *version; /* version 1 */ + STACK_OF(X509_ALGOR) *md_algs; /* md used */ + STACK_OF(X509) *cert; /* [ 0 ] */ + STACK_OF(X509_CRL) *crl; /* [ 1 ] */ + STACK_OF(PKCS7_SIGNER_INFO) *signer_info; + PKCS7_ENC_CONTENT *enc_data; + STACK_OF(PKCS7_RECIP_INFO) *recipientinfo; +} PKCS7_SIGN_ENVELOPE; + +typedef struct pkcs7_digest_st { + ASN1_INTEGER *version; /* version 0 */ + X509_ALGOR *md; /* md used */ + struct pkcs7_st *contents; + ASN1_OCTET_STRING *digest; +} PKCS7_DIGEST; + +typedef struct pkcs7_encrypted_st { + ASN1_INTEGER *version; /* version 0 */ + PKCS7_ENC_CONTENT *enc_data; +} PKCS7_ENCRYPT; + +typedef struct pkcs7_st { + /* + * The following is non NULL if it contains ASN1 encoding of this + * structure + */ + unsigned char *asn1; + long length; +# define PKCS7_S_HEADER 0 +# define PKCS7_S_BODY 1 +# define PKCS7_S_TAIL 2 + int state; /* used during processing */ + int detached; + ASN1_OBJECT *type; + /* content as defined by the type */ + /* + * all encryption/message digests are applied to the 'contents', leaving + * out the 'type' field. + */ + union { + char *ptr; + /* NID_pkcs7_data */ + ASN1_OCTET_STRING *data; + /* NID_pkcs7_signed */ + PKCS7_SIGNED *sign; + /* NID_pkcs7_enveloped */ + PKCS7_ENVELOPE *enveloped; + /* NID_pkcs7_signedAndEnveloped */ + PKCS7_SIGN_ENVELOPE *signed_and_enveloped; + /* NID_pkcs7_digest */ + PKCS7_DIGEST *digest; + /* NID_pkcs7_encrypted */ + PKCS7_ENCRYPT *encrypted; + /* Anything else */ + ASN1_TYPE *other; + } d; +} PKCS7; + +DEFINE_STACK_OF(PKCS7) + +# define PKCS7_OP_SET_DETACHED_SIGNATURE 1 +# define PKCS7_OP_GET_DETACHED_SIGNATURE 2 + +# define PKCS7_get_signed_attributes(si) ((si)->auth_attr) +# define PKCS7_get_attributes(si) ((si)->unauth_attr) + +# define PKCS7_type_is_signed(a) (OBJ_obj2nid((a)->type) == NID_pkcs7_signed) +# define PKCS7_type_is_encrypted(a) (OBJ_obj2nid((a)->type) == NID_pkcs7_encrypted) +# define PKCS7_type_is_enveloped(a) (OBJ_obj2nid((a)->type) == NID_pkcs7_enveloped) +# define PKCS7_type_is_signedAndEnveloped(a) \ + (OBJ_obj2nid((a)->type) == NID_pkcs7_signedAndEnveloped) +# define PKCS7_type_is_data(a) (OBJ_obj2nid((a)->type) == NID_pkcs7_data) +# define PKCS7_type_is_digest(a) (OBJ_obj2nid((a)->type) == NID_pkcs7_digest) + +# define PKCS7_set_detached(p,v) \ + PKCS7_ctrl(p,PKCS7_OP_SET_DETACHED_SIGNATURE,v,NULL) +# define PKCS7_get_detached(p) \ + PKCS7_ctrl(p,PKCS7_OP_GET_DETACHED_SIGNATURE,0,NULL) + +# define PKCS7_is_detached(p7) (PKCS7_type_is_signed(p7) && PKCS7_get_detached(p7)) + +/* S/MIME related flags */ + +# define PKCS7_TEXT 0x1 +# define PKCS7_NOCERTS 0x2 +# define PKCS7_NOSIGS 0x4 +# define PKCS7_NOCHAIN 0x8 +# define PKCS7_NOINTERN 0x10 +# define PKCS7_NOVERIFY 0x20 +# define PKCS7_DETACHED 0x40 +# define PKCS7_BINARY 0x80 +# define PKCS7_NOATTR 0x100 +# define PKCS7_NOSMIMECAP 0x200 +# define PKCS7_NOOLDMIMETYPE 0x400 +# define PKCS7_CRLFEOL 0x800 +# define PKCS7_STREAM 0x1000 +# define PKCS7_NOCRL 0x2000 +# define PKCS7_PARTIAL 0x4000 +# define PKCS7_REUSE_DIGEST 0x8000 +# define PKCS7_NO_DUAL_CONTENT 0x10000 + +/* Flags: for compatibility with older code */ + +# define SMIME_TEXT PKCS7_TEXT +# define SMIME_NOCERTS PKCS7_NOCERTS +# define SMIME_NOSIGS PKCS7_NOSIGS +# define SMIME_NOCHAIN PKCS7_NOCHAIN +# define SMIME_NOINTERN PKCS7_NOINTERN +# define SMIME_NOVERIFY PKCS7_NOVERIFY +# define SMIME_DETACHED PKCS7_DETACHED +# define SMIME_BINARY PKCS7_BINARY +# define SMIME_NOATTR PKCS7_NOATTR + +/* CRLF ASCII canonicalisation */ +# define SMIME_ASCIICRLF 0x80000 + +DECLARE_ASN1_FUNCTIONS(PKCS7_ISSUER_AND_SERIAL) + +int PKCS7_ISSUER_AND_SERIAL_digest(PKCS7_ISSUER_AND_SERIAL *data, + const EVP_MD *type, unsigned char *md, + unsigned int *len); +# ifndef OPENSSL_NO_STDIO +PKCS7 *d2i_PKCS7_fp(FILE *fp, PKCS7 **p7); +int i2d_PKCS7_fp(FILE *fp, PKCS7 *p7); +# endif +PKCS7 *PKCS7_dup(PKCS7 *p7); +PKCS7 *d2i_PKCS7_bio(BIO *bp, PKCS7 **p7); +int i2d_PKCS7_bio(BIO *bp, PKCS7 *p7); +int i2d_PKCS7_bio_stream(BIO *out, PKCS7 *p7, BIO *in, int flags); +int PEM_write_bio_PKCS7_stream(BIO *out, PKCS7 *p7, BIO *in, int flags); + +DECLARE_ASN1_FUNCTIONS(PKCS7_SIGNER_INFO) +DECLARE_ASN1_FUNCTIONS(PKCS7_RECIP_INFO) +DECLARE_ASN1_FUNCTIONS(PKCS7_SIGNED) +DECLARE_ASN1_FUNCTIONS(PKCS7_ENC_CONTENT) +DECLARE_ASN1_FUNCTIONS(PKCS7_ENVELOPE) +DECLARE_ASN1_FUNCTIONS(PKCS7_SIGN_ENVELOPE) +DECLARE_ASN1_FUNCTIONS(PKCS7_DIGEST) +DECLARE_ASN1_FUNCTIONS(PKCS7_ENCRYPT) +DECLARE_ASN1_FUNCTIONS(PKCS7) + +DECLARE_ASN1_ITEM(PKCS7_ATTR_SIGN) +DECLARE_ASN1_ITEM(PKCS7_ATTR_VERIFY) + +DECLARE_ASN1_NDEF_FUNCTION(PKCS7) +DECLARE_ASN1_PRINT_FUNCTION(PKCS7) + +long PKCS7_ctrl(PKCS7 *p7, int cmd, long larg, char *parg); + +int PKCS7_set_type(PKCS7 *p7, int type); +int PKCS7_set0_type_other(PKCS7 *p7, int type, ASN1_TYPE *other); +int PKCS7_set_content(PKCS7 *p7, PKCS7 *p7_data); +int PKCS7_SIGNER_INFO_set(PKCS7_SIGNER_INFO *p7i, X509 *x509, EVP_PKEY *pkey, + const EVP_MD *dgst); +int PKCS7_SIGNER_INFO_sign(PKCS7_SIGNER_INFO *si); +int PKCS7_add_signer(PKCS7 *p7, PKCS7_SIGNER_INFO *p7i); +int PKCS7_add_certificate(PKCS7 *p7, X509 *x509); +int PKCS7_add_crl(PKCS7 *p7, X509_CRL *x509); +int PKCS7_content_new(PKCS7 *p7, int nid); +int PKCS7_dataVerify(X509_STORE *cert_store, X509_STORE_CTX *ctx, + BIO *bio, PKCS7 *p7, PKCS7_SIGNER_INFO *si); +int PKCS7_signatureVerify(BIO *bio, PKCS7 *p7, PKCS7_SIGNER_INFO *si, + X509 *x509); + +BIO *PKCS7_dataInit(PKCS7 *p7, BIO *bio); +int PKCS7_dataFinal(PKCS7 *p7, BIO *bio); +BIO *PKCS7_dataDecode(PKCS7 *p7, EVP_PKEY *pkey, BIO *in_bio, X509 *pcert); + +PKCS7_SIGNER_INFO *PKCS7_add_signature(PKCS7 *p7, X509 *x509, + EVP_PKEY *pkey, const EVP_MD *dgst); +X509 *PKCS7_cert_from_signer_info(PKCS7 *p7, PKCS7_SIGNER_INFO *si); +int PKCS7_set_digest(PKCS7 *p7, const EVP_MD *md); +STACK_OF(PKCS7_SIGNER_INFO) *PKCS7_get_signer_info(PKCS7 *p7); + +PKCS7_RECIP_INFO *PKCS7_add_recipient(PKCS7 *p7, X509 *x509); +void PKCS7_SIGNER_INFO_get0_algs(PKCS7_SIGNER_INFO *si, EVP_PKEY **pk, + X509_ALGOR **pdig, X509_ALGOR **psig); +void PKCS7_RECIP_INFO_get0_alg(PKCS7_RECIP_INFO *ri, X509_ALGOR **penc); +int PKCS7_add_recipient_info(PKCS7 *p7, PKCS7_RECIP_INFO *ri); +int PKCS7_RECIP_INFO_set(PKCS7_RECIP_INFO *p7i, X509 *x509); +int PKCS7_set_cipher(PKCS7 *p7, const EVP_CIPHER *cipher); +int PKCS7_stream(unsigned char ***boundary, PKCS7 *p7); + +PKCS7_ISSUER_AND_SERIAL *PKCS7_get_issuer_and_serial(PKCS7 *p7, int idx); +ASN1_OCTET_STRING *PKCS7_digest_from_attributes(STACK_OF(X509_ATTRIBUTE) *sk); +int PKCS7_add_signed_attribute(PKCS7_SIGNER_INFO *p7si, int nid, int type, + void *data); +int PKCS7_add_attribute(PKCS7_SIGNER_INFO *p7si, int nid, int atrtype, + void *value); +ASN1_TYPE *PKCS7_get_attribute(PKCS7_SIGNER_INFO *si, int nid); +ASN1_TYPE *PKCS7_get_signed_attribute(PKCS7_SIGNER_INFO *si, int nid); +int PKCS7_set_signed_attributes(PKCS7_SIGNER_INFO *p7si, + STACK_OF(X509_ATTRIBUTE) *sk); +int PKCS7_set_attributes(PKCS7_SIGNER_INFO *p7si, + STACK_OF(X509_ATTRIBUTE) *sk); + +PKCS7 *PKCS7_sign(X509 *signcert, EVP_PKEY *pkey, STACK_OF(X509) *certs, + BIO *data, int flags); + +PKCS7_SIGNER_INFO *PKCS7_sign_add_signer(PKCS7 *p7, + X509 *signcert, EVP_PKEY *pkey, + const EVP_MD *md, int flags); + +int PKCS7_final(PKCS7 *p7, BIO *data, int flags); +int PKCS7_verify(PKCS7 *p7, STACK_OF(X509) *certs, X509_STORE *store, + BIO *indata, BIO *out, int flags); +STACK_OF(X509) *PKCS7_get0_signers(PKCS7 *p7, STACK_OF(X509) *certs, + int flags); +PKCS7 *PKCS7_encrypt(STACK_OF(X509) *certs, BIO *in, const EVP_CIPHER *cipher, + int flags); +int PKCS7_decrypt(PKCS7 *p7, EVP_PKEY *pkey, X509 *cert, BIO *data, + int flags); + +int PKCS7_add_attrib_smimecap(PKCS7_SIGNER_INFO *si, + STACK_OF(X509_ALGOR) *cap); +STACK_OF(X509_ALGOR) *PKCS7_get_smimecap(PKCS7_SIGNER_INFO *si); +int PKCS7_simple_smimecap(STACK_OF(X509_ALGOR) *sk, int nid, int arg); + +int PKCS7_add_attrib_content_type(PKCS7_SIGNER_INFO *si, ASN1_OBJECT *coid); +int PKCS7_add0_attrib_signing_time(PKCS7_SIGNER_INFO *si, ASN1_TIME *t); +int PKCS7_add1_attrib_digest(PKCS7_SIGNER_INFO *si, + const unsigned char *md, int mdlen); + +int SMIME_write_PKCS7(BIO *bio, PKCS7 *p7, BIO *data, int flags); +PKCS7 *SMIME_read_PKCS7(BIO *bio, BIO **bcont); + +BIO *BIO_new_PKCS7(BIO *out, PKCS7 *p7); + +/* BEGIN ERROR CODES */ +/* + * The following lines are auto generated by the script mkerr.pl. Any changes + * made after this point may be overwritten when the script is next run. + */ + +int ERR_load_PKCS7_strings(void); + +/* Error codes for the PKCS7 functions. */ + +/* Function codes. */ +# define PKCS7_F_DO_PKCS7_SIGNED_ATTRIB 136 +# define PKCS7_F_PKCS7_ADD0_ATTRIB_SIGNING_TIME 135 +# define PKCS7_F_PKCS7_ADD_ATTRIB_SMIMECAP 118 +# define PKCS7_F_PKCS7_ADD_CERTIFICATE 100 +# define PKCS7_F_PKCS7_ADD_CRL 101 +# define PKCS7_F_PKCS7_ADD_RECIPIENT_INFO 102 +# define PKCS7_F_PKCS7_ADD_SIGNATURE 131 +# define PKCS7_F_PKCS7_ADD_SIGNER 103 +# define PKCS7_F_PKCS7_BIO_ADD_DIGEST 125 +# define PKCS7_F_PKCS7_COPY_EXISTING_DIGEST 138 +# define PKCS7_F_PKCS7_CTRL 104 +# define PKCS7_F_PKCS7_DATADECODE 112 +# define PKCS7_F_PKCS7_DATAFINAL 128 +# define PKCS7_F_PKCS7_DATAINIT 105 +# define PKCS7_F_PKCS7_DATAVERIFY 107 +# define PKCS7_F_PKCS7_DECRYPT 114 +# define PKCS7_F_PKCS7_DECRYPT_RINFO 133 +# define PKCS7_F_PKCS7_ENCODE_RINFO 132 +# define PKCS7_F_PKCS7_ENCRYPT 115 +# define PKCS7_F_PKCS7_FINAL 134 +# define PKCS7_F_PKCS7_FIND_DIGEST 127 +# define PKCS7_F_PKCS7_GET0_SIGNERS 124 +# define PKCS7_F_PKCS7_RECIP_INFO_SET 130 +# define PKCS7_F_PKCS7_SET_CIPHER 108 +# define PKCS7_F_PKCS7_SET_CONTENT 109 +# define PKCS7_F_PKCS7_SET_DIGEST 126 +# define PKCS7_F_PKCS7_SET_TYPE 110 +# define PKCS7_F_PKCS7_SIGN 116 +# define PKCS7_F_PKCS7_SIGNATUREVERIFY 113 +# define PKCS7_F_PKCS7_SIGNER_INFO_SET 129 +# define PKCS7_F_PKCS7_SIGNER_INFO_SIGN 139 +# define PKCS7_F_PKCS7_SIGN_ADD_SIGNER 137 +# define PKCS7_F_PKCS7_SIMPLE_SMIMECAP 119 +# define PKCS7_F_PKCS7_VERIFY 117 + +/* Reason codes. */ +# define PKCS7_R_CERTIFICATE_VERIFY_ERROR 117 +# define PKCS7_R_CIPHER_HAS_NO_OBJECT_IDENTIFIER 144 +# define PKCS7_R_CIPHER_NOT_INITIALIZED 116 +# define PKCS7_R_CONTENT_AND_DATA_PRESENT 118 +# define PKCS7_R_CTRL_ERROR 152 +# define PKCS7_R_DECRYPT_ERROR 119 +# define PKCS7_R_DIGEST_FAILURE 101 +# define PKCS7_R_ENCRYPTION_CTRL_FAILURE 149 +# define PKCS7_R_ENCRYPTION_NOT_SUPPORTED_FOR_THIS_KEY_TYPE 150 +# define PKCS7_R_ERROR_ADDING_RECIPIENT 120 +# define PKCS7_R_ERROR_SETTING_CIPHER 121 +# define PKCS7_R_INVALID_NULL_POINTER 143 +# define PKCS7_R_INVALID_SIGNED_DATA_TYPE 155 +# define PKCS7_R_NO_CONTENT 122 +# define PKCS7_R_NO_DEFAULT_DIGEST 151 +# define PKCS7_R_NO_MATCHING_DIGEST_TYPE_FOUND 154 +# define PKCS7_R_NO_RECIPIENT_MATCHES_CERTIFICATE 115 +# define PKCS7_R_NO_SIGNATURES_ON_DATA 123 +# define PKCS7_R_NO_SIGNERS 142 +# define PKCS7_R_OPERATION_NOT_SUPPORTED_ON_THIS_TYPE 104 +# define PKCS7_R_PKCS7_ADD_SIGNATURE_ERROR 124 +# define PKCS7_R_PKCS7_ADD_SIGNER_ERROR 153 +# define PKCS7_R_PKCS7_DATASIGN 145 +# define PKCS7_R_PRIVATE_KEY_DOES_NOT_MATCH_CERTIFICATE 127 +# define PKCS7_R_SIGNATURE_FAILURE 105 +# define PKCS7_R_SIGNER_CERTIFICATE_NOT_FOUND 128 +# define PKCS7_R_SIGNING_CTRL_FAILURE 147 +# define PKCS7_R_SIGNING_NOT_SUPPORTED_FOR_THIS_KEY_TYPE 148 +# define PKCS7_R_SMIME_TEXT_ERROR 129 +# define PKCS7_R_UNABLE_TO_FIND_CERTIFICATE 106 +# define PKCS7_R_UNABLE_TO_FIND_MEM_BIO 107 +# define PKCS7_R_UNABLE_TO_FIND_MESSAGE_DIGEST 108 +# define PKCS7_R_UNKNOWN_DIGEST_TYPE 109 +# define PKCS7_R_UNKNOWN_OPERATION 110 +# define PKCS7_R_UNSUPPORTED_CIPHER_TYPE 111 +# define PKCS7_R_UNSUPPORTED_CONTENT_TYPE 112 +# define PKCS7_R_WRONG_CONTENT_TYPE 113 +# define PKCS7_R_WRONG_PKCS7_TYPE 114 + +# ifdef __cplusplus +} +# endif +#endif diff --git a/mobile/ios/FxA/FxA/include/openssl/rand.h b/mobile/ios/FxA/FxA/include/openssl/rand.h new file mode 100644 index 0000000000..d521ae192a --- /dev/null +++ b/mobile/ios/FxA/FxA/include/openssl/rand.h @@ -0,0 +1,89 @@ +/* + * Copyright 1995-2016 The OpenSSL Project Authors. All Rights Reserved. + * + * Licensed under the OpenSSL license (the "License"). You may not use + * this file except in compliance with the License. You can obtain a copy + * in the file LICENSE in the source distribution or at + * https://www.openssl.org/source/license.html + */ + +#ifndef HEADER_RAND_H +# define HEADER_RAND_H + +# include +# include +# include + +#ifdef __cplusplus +extern "C" { +#endif + +/* Already defined in ossl_typ.h */ +/* typedef struct rand_meth_st RAND_METHOD; */ + +struct rand_meth_st { + int (*seed) (const void *buf, int num); + int (*bytes) (unsigned char *buf, int num); + void (*cleanup) (void); + int (*add) (const void *buf, int num, double entropy); + int (*pseudorand) (unsigned char *buf, int num); + int (*status) (void); +}; + +# ifdef BN_DEBUG +extern int rand_predictable; +# endif + +int RAND_set_rand_method(const RAND_METHOD *meth); +const RAND_METHOD *RAND_get_rand_method(void); +# ifndef OPENSSL_NO_ENGINE +int RAND_set_rand_engine(ENGINE *engine); +# endif +RAND_METHOD *RAND_OpenSSL(void); +#if OPENSSL_API_COMPAT < 0x10100000L +# define RAND_cleanup() while(0) continue +#endif +int RAND_bytes(unsigned char *buf, int num); +DEPRECATEDIN_1_1_0(int RAND_pseudo_bytes(unsigned char *buf, int num)) +void RAND_seed(const void *buf, int num); +#if defined(__ANDROID__) && defined(__NDK_FPABI__) +__NDK_FPABI__ /* __attribute__((pcs("aapcs"))) on ARM */ +#endif +void RAND_add(const void *buf, int num, double entropy); +int RAND_load_file(const char *file, long max_bytes); +int RAND_write_file(const char *file); +const char *RAND_file_name(char *file, size_t num); +int RAND_status(void); +# ifndef OPENSSL_NO_EGD +int RAND_query_egd_bytes(const char *path, unsigned char *buf, int bytes); +int RAND_egd(const char *path); +int RAND_egd_bytes(const char *path, int bytes); +# endif +int RAND_poll(void); + +#if defined(_WIN32) && (defined(BASETYPES) || defined(_WINDEF_H)) +/* application has to include in order to use these */ +DEPRECATEDIN_1_1_0(void RAND_screen(void)) +DEPRECATEDIN_1_1_0(int RAND_event(UINT, WPARAM, LPARAM)) +#endif + +/* BEGIN ERROR CODES */ +/* + * The following lines are auto generated by the script mkerr.pl. Any changes + * made after this point may be overwritten when the script is next run. + */ + +int ERR_load_RAND_strings(void); + +/* Error codes for the RAND functions. */ + +/* Function codes. */ +# define RAND_F_RAND_BYTES 100 + +/* Reason codes. */ +# define RAND_R_PRNG_NOT_SEEDED 100 + +# ifdef __cplusplus +} +# endif +#endif diff --git a/mobile/ios/FxA/FxA/include/openssl/rc2.h b/mobile/ios/FxA/FxA/include/openssl/rc2.h new file mode 100644 index 0000000000..585f9e4c38 --- /dev/null +++ b/mobile/ios/FxA/FxA/include/openssl/rc2.h @@ -0,0 +1,51 @@ +/* + * Copyright 1995-2016 The OpenSSL Project Authors. All Rights Reserved. + * + * Licensed under the OpenSSL license (the "License"). You may not use + * this file except in compliance with the License. You can obtain a copy + * in the file LICENSE in the source distribution or at + * https://www.openssl.org/source/license.html + */ + +#ifndef HEADER_RC2_H +# define HEADER_RC2_H + +# include + +# ifndef OPENSSL_NO_RC2 +# ifdef __cplusplus +extern "C" { +# endif + +typedef unsigned int RC2_INT; + +# define RC2_ENCRYPT 1 +# define RC2_DECRYPT 0 + +# define RC2_BLOCK 8 +# define RC2_KEY_LENGTH 16 + +typedef struct rc2_key_st { + RC2_INT data[64]; +} RC2_KEY; + +void RC2_set_key(RC2_KEY *key, int len, const unsigned char *data, int bits); +void RC2_ecb_encrypt(const unsigned char *in, unsigned char *out, + RC2_KEY *key, int enc); +void RC2_encrypt(unsigned long *data, RC2_KEY *key); +void RC2_decrypt(unsigned long *data, RC2_KEY *key); +void RC2_cbc_encrypt(const unsigned char *in, unsigned char *out, long length, + RC2_KEY *ks, unsigned char *iv, int enc); +void RC2_cfb64_encrypt(const unsigned char *in, unsigned char *out, + long length, RC2_KEY *schedule, unsigned char *ivec, + int *num, int enc); +void RC2_ofb64_encrypt(const unsigned char *in, unsigned char *out, + long length, RC2_KEY *schedule, unsigned char *ivec, + int *num); + +# ifdef __cplusplus +} +# endif +# endif + +#endif diff --git a/mobile/ios/FxA/FxA/include/openssl/rc4.h b/mobile/ios/FxA/FxA/include/openssl/rc4.h new file mode 100644 index 0000000000..86803b37fb --- /dev/null +++ b/mobile/ios/FxA/FxA/include/openssl/rc4.h @@ -0,0 +1,36 @@ +/* + * Copyright 1995-2016 The OpenSSL Project Authors. All Rights Reserved. + * + * Licensed under the OpenSSL license (the "License"). You may not use + * this file except in compliance with the License. You can obtain a copy + * in the file LICENSE in the source distribution or at + * https://www.openssl.org/source/license.html + */ + +#ifndef HEADER_RC4_H +# define HEADER_RC4_H + +# include + +# ifndef OPENSSL_NO_RC4 +# include +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct rc4_key_st { + RC4_INT x, y; + RC4_INT data[256]; +} RC4_KEY; + +const char *RC4_options(void); +void RC4_set_key(RC4_KEY *key, int len, const unsigned char *data); +void RC4(RC4_KEY *key, size_t len, const unsigned char *indata, + unsigned char *outdata); + +# ifdef __cplusplus +} +# endif +# endif + +#endif diff --git a/mobile/ios/FxA/FxA/include/openssl/rc5.h b/mobile/ios/FxA/FxA/include/openssl/rc5.h new file mode 100644 index 0000000000..793f88e4e8 --- /dev/null +++ b/mobile/ios/FxA/FxA/include/openssl/rc5.h @@ -0,0 +1,63 @@ +/* + * Copyright 1995-2016 The OpenSSL Project Authors. All Rights Reserved. + * + * Licensed under the OpenSSL license (the "License"). You may not use + * this file except in compliance with the License. You can obtain a copy + * in the file LICENSE in the source distribution or at + * https://www.openssl.org/source/license.html + */ + +#ifndef HEADER_RC5_H +# define HEADER_RC5_H + +# include + +# ifndef OPENSSL_NO_RC5 +# ifdef __cplusplus +extern "C" { +# endif + +# define RC5_ENCRYPT 1 +# define RC5_DECRYPT 0 + +# define RC5_32_INT unsigned int + +# define RC5_32_BLOCK 8 +# define RC5_32_KEY_LENGTH 16/* This is a default, max is 255 */ + +/* + * This are the only values supported. Tweak the code if you want more The + * most supported modes will be RC5-32/12/16 RC5-32/16/8 + */ +# define RC5_8_ROUNDS 8 +# define RC5_12_ROUNDS 12 +# define RC5_16_ROUNDS 16 + +typedef struct rc5_key_st { + /* Number of rounds */ + int rounds; + RC5_32_INT data[2 * (RC5_16_ROUNDS + 1)]; +} RC5_32_KEY; + +void RC5_32_set_key(RC5_32_KEY *key, int len, const unsigned char *data, + int rounds); +void RC5_32_ecb_encrypt(const unsigned char *in, unsigned char *out, + RC5_32_KEY *key, int enc); +void RC5_32_encrypt(unsigned long *data, RC5_32_KEY *key); +void RC5_32_decrypt(unsigned long *data, RC5_32_KEY *key); +void RC5_32_cbc_encrypt(const unsigned char *in, unsigned char *out, + long length, RC5_32_KEY *ks, unsigned char *iv, + int enc); +void RC5_32_cfb64_encrypt(const unsigned char *in, unsigned char *out, + long length, RC5_32_KEY *schedule, + unsigned char *ivec, int *num, int enc); +void RC5_32_ofb64_encrypt(const unsigned char *in, unsigned char *out, + long length, RC5_32_KEY *schedule, + unsigned char *ivec, int *num); + +# ifdef __cplusplus +} +# endif +# endif + +#endif diff --git a/mobile/ios/FxA/FxA/include/openssl/ripemd.h b/mobile/ios/FxA/FxA/include/openssl/ripemd.h new file mode 100644 index 0000000000..c42026aa42 --- /dev/null +++ b/mobile/ios/FxA/FxA/include/openssl/ripemd.h @@ -0,0 +1,47 @@ +/* + * Copyright 1995-2016 The OpenSSL Project Authors. All Rights Reserved. + * + * Licensed under the OpenSSL license (the "License"). You may not use + * this file except in compliance with the License. You can obtain a copy + * in the file LICENSE in the source distribution or at + * https://www.openssl.org/source/license.html + */ + +#ifndef HEADER_RIPEMD_H +# define HEADER_RIPEMD_H + +# include + +#ifndef OPENSSL_NO_RMD160 +# include +# include +# ifdef __cplusplus +extern "C" { +# endif + +# define RIPEMD160_LONG unsigned int + +# define RIPEMD160_CBLOCK 64 +# define RIPEMD160_LBLOCK (RIPEMD160_CBLOCK/4) +# define RIPEMD160_DIGEST_LENGTH 20 + +typedef struct RIPEMD160state_st { + RIPEMD160_LONG A, B, C, D, E; + RIPEMD160_LONG Nl, Nh; + RIPEMD160_LONG data[RIPEMD160_LBLOCK]; + unsigned int num; +} RIPEMD160_CTX; + +int RIPEMD160_Init(RIPEMD160_CTX *c); +int RIPEMD160_Update(RIPEMD160_CTX *c, const void *data, size_t len); +int RIPEMD160_Final(unsigned char *md, RIPEMD160_CTX *c); +unsigned char *RIPEMD160(const unsigned char *d, size_t n, unsigned char *md); +void RIPEMD160_Transform(RIPEMD160_CTX *c, const unsigned char *b); + +# ifdef __cplusplus +} +# endif +# endif + + +#endif diff --git a/mobile/ios/FxA/FxA/include/openssl/rsa.h b/mobile/ios/FxA/FxA/include/openssl/rsa.h new file mode 100644 index 0000000000..d97d6e075a --- /dev/null +++ b/mobile/ios/FxA/FxA/include/openssl/rsa.h @@ -0,0 +1,590 @@ +/* + * Copyright 1995-2016 The OpenSSL Project Authors. All Rights Reserved. + * + * Licensed under the OpenSSL license (the "License"). You may not use + * this file except in compliance with the License. You can obtain a copy + * in the file LICENSE in the source distribution or at + * https://www.openssl.org/source/license.html + */ + +#ifndef HEADER_RSA_H +# define HEADER_RSA_H + +# include + +# ifndef OPENSSL_NO_RSA +# include +# include +# include +# include +# if OPENSSL_API_COMPAT < 0x10100000L +# include +# endif +# ifdef __cplusplus +extern "C" { +# endif + +/* The types RSA and RSA_METHOD are defined in ossl_typ.h */ + +# ifndef OPENSSL_RSA_MAX_MODULUS_BITS +# define OPENSSL_RSA_MAX_MODULUS_BITS 16384 +# endif + +# define OPENSSL_RSA_FIPS_MIN_MODULUS_BITS 1024 + +# ifndef OPENSSL_RSA_SMALL_MODULUS_BITS +# define OPENSSL_RSA_SMALL_MODULUS_BITS 3072 +# endif +# ifndef OPENSSL_RSA_MAX_PUBEXP_BITS + +/* exponent limit enforced for "large" modulus only */ +# define OPENSSL_RSA_MAX_PUBEXP_BITS 64 +# endif + +# define RSA_3 0x3L +# define RSA_F4 0x10001L + +# define RSA_METHOD_FLAG_NO_CHECK 0x0001/* don't check pub/private + * match */ + +# define RSA_FLAG_CACHE_PUBLIC 0x0002 +# define RSA_FLAG_CACHE_PRIVATE 0x0004 +# define RSA_FLAG_BLINDING 0x0008 +# define RSA_FLAG_THREAD_SAFE 0x0010 +/* + * This flag means the private key operations will be handled by rsa_mod_exp + * and that they do not depend on the private key components being present: + * for example a key stored in external hardware. Without this flag + * bn_mod_exp gets called when private key components are absent. + */ +# define RSA_FLAG_EXT_PKEY 0x0020 + +/* + * new with 0.9.6j and 0.9.7b; the built-in + * RSA implementation now uses blinding by + * default (ignoring RSA_FLAG_BLINDING), + * but other engines might not need it + */ +# define RSA_FLAG_NO_BLINDING 0x0080 +# if OPENSSL_API_COMPAT < 0x10100000L +/* + * Does nothing. Previously this switched off constant time behaviour. + */ +# define RSA_FLAG_NO_CONSTTIME 0x0000 +# endif +# if OPENSSL_API_COMPAT < 0x00908000L +/* deprecated name for the flag*/ +/* + * new with 0.9.7h; the built-in RSA + * implementation now uses constant time + * modular exponentiation for secret exponents + * by default. This flag causes the + * faster variable sliding window method to + * be used for all exponents. + */ +# define RSA_FLAG_NO_EXP_CONSTTIME RSA_FLAG_NO_CONSTTIME +# endif + +# define EVP_PKEY_CTX_set_rsa_padding(ctx, pad) \ + EVP_PKEY_CTX_ctrl(ctx, EVP_PKEY_RSA, -1, EVP_PKEY_CTRL_RSA_PADDING, \ + pad, NULL) + +# define EVP_PKEY_CTX_get_rsa_padding(ctx, ppad) \ + EVP_PKEY_CTX_ctrl(ctx, EVP_PKEY_RSA, -1, \ + EVP_PKEY_CTRL_GET_RSA_PADDING, 0, ppad) + +# define EVP_PKEY_CTX_set_rsa_pss_saltlen(ctx, len) \ + EVP_PKEY_CTX_ctrl(ctx, EVP_PKEY_RSA, \ + (EVP_PKEY_OP_SIGN|EVP_PKEY_OP_VERIFY), \ + EVP_PKEY_CTRL_RSA_PSS_SALTLEN, \ + len, NULL) + +# define EVP_PKEY_CTX_get_rsa_pss_saltlen(ctx, plen) \ + EVP_PKEY_CTX_ctrl(ctx, EVP_PKEY_RSA, \ + (EVP_PKEY_OP_SIGN|EVP_PKEY_OP_VERIFY), \ + EVP_PKEY_CTRL_GET_RSA_PSS_SALTLEN, \ + 0, plen) + +# define EVP_PKEY_CTX_set_rsa_keygen_bits(ctx, bits) \ + EVP_PKEY_CTX_ctrl(ctx, EVP_PKEY_RSA, EVP_PKEY_OP_KEYGEN, \ + EVP_PKEY_CTRL_RSA_KEYGEN_BITS, bits, NULL) + +# define EVP_PKEY_CTX_set_rsa_keygen_pubexp(ctx, pubexp) \ + EVP_PKEY_CTX_ctrl(ctx, EVP_PKEY_RSA, EVP_PKEY_OP_KEYGEN, \ + EVP_PKEY_CTRL_RSA_KEYGEN_PUBEXP, 0, pubexp) + +# define EVP_PKEY_CTX_set_rsa_mgf1_md(ctx, md) \ + EVP_PKEY_CTX_ctrl(ctx, EVP_PKEY_RSA, \ + EVP_PKEY_OP_TYPE_SIG | EVP_PKEY_OP_TYPE_CRYPT, \ + EVP_PKEY_CTRL_RSA_MGF1_MD, 0, (void *)md) + +# define EVP_PKEY_CTX_set_rsa_oaep_md(ctx, md) \ + EVP_PKEY_CTX_ctrl(ctx, EVP_PKEY_RSA, EVP_PKEY_OP_TYPE_CRYPT, \ + EVP_PKEY_CTRL_RSA_OAEP_MD, 0, (void *)md) + +# define EVP_PKEY_CTX_get_rsa_mgf1_md(ctx, pmd) \ + EVP_PKEY_CTX_ctrl(ctx, EVP_PKEY_RSA, \ + EVP_PKEY_OP_TYPE_SIG | EVP_PKEY_OP_TYPE_CRYPT, \ + EVP_PKEY_CTRL_GET_RSA_MGF1_MD, 0, (void *)pmd) + +# define EVP_PKEY_CTX_get_rsa_oaep_md(ctx, pmd) \ + EVP_PKEY_CTX_ctrl(ctx, EVP_PKEY_RSA, EVP_PKEY_OP_TYPE_CRYPT, \ + EVP_PKEY_CTRL_GET_RSA_OAEP_MD, 0, (void *)pmd) + +# define EVP_PKEY_CTX_set0_rsa_oaep_label(ctx, l, llen) \ + EVP_PKEY_CTX_ctrl(ctx, EVP_PKEY_RSA, EVP_PKEY_OP_TYPE_CRYPT, \ + EVP_PKEY_CTRL_RSA_OAEP_LABEL, llen, (void *)l) + +# define EVP_PKEY_CTX_get0_rsa_oaep_label(ctx, l) \ + EVP_PKEY_CTX_ctrl(ctx, EVP_PKEY_RSA, EVP_PKEY_OP_TYPE_CRYPT, \ + EVP_PKEY_CTRL_GET_RSA_OAEP_LABEL, 0, (void *)l) + +# define EVP_PKEY_CTRL_RSA_PADDING (EVP_PKEY_ALG_CTRL + 1) +# define EVP_PKEY_CTRL_RSA_PSS_SALTLEN (EVP_PKEY_ALG_CTRL + 2) + +# define EVP_PKEY_CTRL_RSA_KEYGEN_BITS (EVP_PKEY_ALG_CTRL + 3) +# define EVP_PKEY_CTRL_RSA_KEYGEN_PUBEXP (EVP_PKEY_ALG_CTRL + 4) +# define EVP_PKEY_CTRL_RSA_MGF1_MD (EVP_PKEY_ALG_CTRL + 5) + +# define EVP_PKEY_CTRL_GET_RSA_PADDING (EVP_PKEY_ALG_CTRL + 6) +# define EVP_PKEY_CTRL_GET_RSA_PSS_SALTLEN (EVP_PKEY_ALG_CTRL + 7) +# define EVP_PKEY_CTRL_GET_RSA_MGF1_MD (EVP_PKEY_ALG_CTRL + 8) + +# define EVP_PKEY_CTRL_RSA_OAEP_MD (EVP_PKEY_ALG_CTRL + 9) +# define EVP_PKEY_CTRL_RSA_OAEP_LABEL (EVP_PKEY_ALG_CTRL + 10) + +# define EVP_PKEY_CTRL_GET_RSA_OAEP_MD (EVP_PKEY_ALG_CTRL + 11) +# define EVP_PKEY_CTRL_GET_RSA_OAEP_LABEL (EVP_PKEY_ALG_CTRL + 12) + +# define RSA_PKCS1_PADDING 1 +# define RSA_SSLV23_PADDING 2 +# define RSA_NO_PADDING 3 +# define RSA_PKCS1_OAEP_PADDING 4 +# define RSA_X931_PADDING 5 +/* EVP_PKEY_ only */ +# define RSA_PKCS1_PSS_PADDING 6 + +# define RSA_PKCS1_PADDING_SIZE 11 + +# define RSA_set_app_data(s,arg) RSA_set_ex_data(s,0,arg) +# define RSA_get_app_data(s) RSA_get_ex_data(s,0) + +RSA *RSA_new(void); +RSA *RSA_new_method(ENGINE *engine); +int RSA_bits(const RSA *rsa); +int RSA_size(const RSA *rsa); +int RSA_security_bits(const RSA *rsa); + +int RSA_set0_key(RSA *r, BIGNUM *n, BIGNUM *e, BIGNUM *d); +int RSA_set0_factors(RSA *r, BIGNUM *p, BIGNUM *q); +int RSA_set0_crt_params(RSA *r,BIGNUM *dmp1, BIGNUM *dmq1, BIGNUM *iqmp); +void RSA_get0_key(const RSA *r, + const BIGNUM **n, const BIGNUM **e, const BIGNUM **d); +void RSA_get0_factors(const RSA *r, const BIGNUM **p, const BIGNUM **q); +void RSA_get0_crt_params(const RSA *r, + const BIGNUM **dmp1, const BIGNUM **dmq1, + const BIGNUM **iqmp); +void RSA_clear_flags(RSA *r, int flags); +int RSA_test_flags(const RSA *r, int flags); +void RSA_set_flags(RSA *r, int flags); +ENGINE *RSA_get0_engine(const RSA *r); + +/* Deprecated version */ +DEPRECATEDIN_0_9_8(RSA *RSA_generate_key(int bits, unsigned long e, void + (*callback) (int, int, void *), + void *cb_arg)) + +/* New version */ +int RSA_generate_key_ex(RSA *rsa, int bits, BIGNUM *e, BN_GENCB *cb); + +int RSA_X931_derive_ex(RSA *rsa, BIGNUM *p1, BIGNUM *p2, BIGNUM *q1, + BIGNUM *q2, const BIGNUM *Xp1, const BIGNUM *Xp2, + const BIGNUM *Xp, const BIGNUM *Xq1, const BIGNUM *Xq2, + const BIGNUM *Xq, const BIGNUM *e, BN_GENCB *cb); +int RSA_X931_generate_key_ex(RSA *rsa, int bits, const BIGNUM *e, + BN_GENCB *cb); + +int RSA_check_key(const RSA *); +int RSA_check_key_ex(const RSA *, BN_GENCB *cb); + /* next 4 return -1 on error */ +int RSA_public_encrypt(int flen, const unsigned char *from, + unsigned char *to, RSA *rsa, int padding); +int RSA_private_encrypt(int flen, const unsigned char *from, + unsigned char *to, RSA *rsa, int padding); +int RSA_public_decrypt(int flen, const unsigned char *from, + unsigned char *to, RSA *rsa, int padding); +int RSA_private_decrypt(int flen, const unsigned char *from, + unsigned char *to, RSA *rsa, int padding); +void RSA_free(RSA *r); +/* "up" the RSA object's reference count */ +int RSA_up_ref(RSA *r); + +int RSA_flags(const RSA *r); + +void RSA_set_default_method(const RSA_METHOD *meth); +const RSA_METHOD *RSA_get_default_method(void); +const RSA_METHOD *RSA_get_method(const RSA *rsa); +int RSA_set_method(RSA *rsa, const RSA_METHOD *meth); + +/* these are the actual RSA functions */ +const RSA_METHOD *RSA_PKCS1_OpenSSL(void); + +const RSA_METHOD *RSA_null_method(void); + +DECLARE_ASN1_ENCODE_FUNCTIONS_const(RSA, RSAPublicKey) +DECLARE_ASN1_ENCODE_FUNCTIONS_const(RSA, RSAPrivateKey) + +typedef struct rsa_pss_params_st { + X509_ALGOR *hashAlgorithm; + X509_ALGOR *maskGenAlgorithm; + ASN1_INTEGER *saltLength; + ASN1_INTEGER *trailerField; +} RSA_PSS_PARAMS; + +DECLARE_ASN1_FUNCTIONS(RSA_PSS_PARAMS) + +typedef struct rsa_oaep_params_st { + X509_ALGOR *hashFunc; + X509_ALGOR *maskGenFunc; + X509_ALGOR *pSourceFunc; +} RSA_OAEP_PARAMS; + +DECLARE_ASN1_FUNCTIONS(RSA_OAEP_PARAMS) + +# ifndef OPENSSL_NO_STDIO +int RSA_print_fp(FILE *fp, const RSA *r, int offset); +# endif + +int RSA_print(BIO *bp, const RSA *r, int offset); + +/* + * The following 2 functions sign and verify a X509_SIG ASN1 object inside + * PKCS#1 padded RSA encryption + */ +int RSA_sign(int type, const unsigned char *m, unsigned int m_length, + unsigned char *sigret, unsigned int *siglen, RSA *rsa); +int RSA_verify(int type, const unsigned char *m, unsigned int m_length, + const unsigned char *sigbuf, unsigned int siglen, RSA *rsa); + +/* + * The following 2 function sign and verify a ASN1_OCTET_STRING object inside + * PKCS#1 padded RSA encryption + */ +int RSA_sign_ASN1_OCTET_STRING(int type, + const unsigned char *m, unsigned int m_length, + unsigned char *sigret, unsigned int *siglen, + RSA *rsa); +int RSA_verify_ASN1_OCTET_STRING(int type, const unsigned char *m, + unsigned int m_length, unsigned char *sigbuf, + unsigned int siglen, RSA *rsa); + +int RSA_blinding_on(RSA *rsa, BN_CTX *ctx); +void RSA_blinding_off(RSA *rsa); +BN_BLINDING *RSA_setup_blinding(RSA *rsa, BN_CTX *ctx); + +int RSA_padding_add_PKCS1_type_1(unsigned char *to, int tlen, + const unsigned char *f, int fl); +int RSA_padding_check_PKCS1_type_1(unsigned char *to, int tlen, + const unsigned char *f, int fl, + int rsa_len); +int RSA_padding_add_PKCS1_type_2(unsigned char *to, int tlen, + const unsigned char *f, int fl); +int RSA_padding_check_PKCS1_type_2(unsigned char *to, int tlen, + const unsigned char *f, int fl, + int rsa_len); +int PKCS1_MGF1(unsigned char *mask, long len, const unsigned char *seed, + long seedlen, const EVP_MD *dgst); +int RSA_padding_add_PKCS1_OAEP(unsigned char *to, int tlen, + const unsigned char *f, int fl, + const unsigned char *p, int pl); +int RSA_padding_check_PKCS1_OAEP(unsigned char *to, int tlen, + const unsigned char *f, int fl, int rsa_len, + const unsigned char *p, int pl); +int RSA_padding_add_PKCS1_OAEP_mgf1(unsigned char *to, int tlen, + const unsigned char *from, int flen, + const unsigned char *param, int plen, + const EVP_MD *md, const EVP_MD *mgf1md); +int RSA_padding_check_PKCS1_OAEP_mgf1(unsigned char *to, int tlen, + const unsigned char *from, int flen, + int num, const unsigned char *param, + int plen, const EVP_MD *md, + const EVP_MD *mgf1md); +int RSA_padding_add_SSLv23(unsigned char *to, int tlen, + const unsigned char *f, int fl); +int RSA_padding_check_SSLv23(unsigned char *to, int tlen, + const unsigned char *f, int fl, int rsa_len); +int RSA_padding_add_none(unsigned char *to, int tlen, const unsigned char *f, + int fl); +int RSA_padding_check_none(unsigned char *to, int tlen, + const unsigned char *f, int fl, int rsa_len); +int RSA_padding_add_X931(unsigned char *to, int tlen, const unsigned char *f, + int fl); +int RSA_padding_check_X931(unsigned char *to, int tlen, + const unsigned char *f, int fl, int rsa_len); +int RSA_X931_hash_id(int nid); + +int RSA_verify_PKCS1_PSS(RSA *rsa, const unsigned char *mHash, + const EVP_MD *Hash, const unsigned char *EM, + int sLen); +int RSA_padding_add_PKCS1_PSS(RSA *rsa, unsigned char *EM, + const unsigned char *mHash, const EVP_MD *Hash, + int sLen); + +int RSA_verify_PKCS1_PSS_mgf1(RSA *rsa, const unsigned char *mHash, + const EVP_MD *Hash, const EVP_MD *mgf1Hash, + const unsigned char *EM, int sLen); + +int RSA_padding_add_PKCS1_PSS_mgf1(RSA *rsa, unsigned char *EM, + const unsigned char *mHash, + const EVP_MD *Hash, const EVP_MD *mgf1Hash, + int sLen); + +#define RSA_get_ex_new_index(l, p, newf, dupf, freef) \ + CRYPTO_get_ex_new_index(CRYPTO_EX_INDEX_RSA, l, p, newf, dupf, freef) +int RSA_set_ex_data(RSA *r, int idx, void *arg); +void *RSA_get_ex_data(const RSA *r, int idx); + +RSA *RSAPublicKey_dup(RSA *rsa); +RSA *RSAPrivateKey_dup(RSA *rsa); + +/* + * If this flag is set the RSA method is FIPS compliant and can be used in + * FIPS mode. This is set in the validated module method. If an application + * sets this flag in its own methods it is its responsibility to ensure the + * result is compliant. + */ + +# define RSA_FLAG_FIPS_METHOD 0x0400 + +/* + * If this flag is set the operations normally disabled in FIPS mode are + * permitted it is then the applications responsibility to ensure that the + * usage is compliant. + */ + +# define RSA_FLAG_NON_FIPS_ALLOW 0x0400 +/* + * Application has decided PRNG is good enough to generate a key: don't + * check. + */ +# define RSA_FLAG_CHECKED 0x0800 + +RSA_METHOD *RSA_meth_new(const char *name, int flags); +void RSA_meth_free(RSA_METHOD *meth); +RSA_METHOD *RSA_meth_dup(const RSA_METHOD *meth); +const char *RSA_meth_get0_name(const RSA_METHOD *meth); +int RSA_meth_set1_name(RSA_METHOD *meth, const char *name); +int RSA_meth_get_flags(RSA_METHOD *meth); +int RSA_meth_set_flags(RSA_METHOD *meth, int flags); +void *RSA_meth_get0_app_data(const RSA_METHOD *meth); +int RSA_meth_set0_app_data(RSA_METHOD *meth, void *app_data); +int (*RSA_meth_get_pub_enc(const RSA_METHOD *meth)) + (int flen, const unsigned char *from, + unsigned char *to, RSA *rsa, int padding); +int RSA_meth_set_pub_enc(RSA_METHOD *rsa, + int (*pub_enc) (int flen, const unsigned char *from, + unsigned char *to, RSA *rsa, + int padding)); +int (*RSA_meth_get_pub_dec(const RSA_METHOD *meth)) + (int flen, const unsigned char *from, + unsigned char *to, RSA *rsa, int padding); +int RSA_meth_set_pub_dec(RSA_METHOD *rsa, + int (*pub_dec) (int flen, const unsigned char *from, + unsigned char *to, RSA *rsa, + int padding)); +int (*RSA_meth_get_priv_enc(const RSA_METHOD *meth)) + (int flen, const unsigned char *from, + unsigned char *to, RSA *rsa, int padding); +int RSA_meth_set_priv_enc(RSA_METHOD *rsa, + int (*priv_enc) (int flen, const unsigned char *from, + unsigned char *to, RSA *rsa, + int padding)); +int (*RSA_meth_get_priv_dec(const RSA_METHOD *meth)) + (int flen, const unsigned char *from, + unsigned char *to, RSA *rsa, int padding); +int RSA_meth_set_priv_dec(RSA_METHOD *rsa, + int (*priv_dec) (int flen, const unsigned char *from, + unsigned char *to, RSA *rsa, + int padding)); +int (*RSA_meth_get_mod_exp(const RSA_METHOD *meth)) + (BIGNUM *r0, const BIGNUM *I, RSA *rsa, BN_CTX *ctx); +int RSA_meth_set_mod_exp(RSA_METHOD *rsa, + int (*mod_exp) (BIGNUM *r0, const BIGNUM *I, RSA *rsa, + BN_CTX *ctx)); +int (*RSA_meth_get_bn_mod_exp(const RSA_METHOD *meth)) + (BIGNUM *r, const BIGNUM *a, const BIGNUM *p, + const BIGNUM *m, BN_CTX *ctx, BN_MONT_CTX *m_ctx); +int RSA_meth_set_bn_mod_exp(RSA_METHOD *rsa, + int (*bn_mod_exp) (BIGNUM *r, + const BIGNUM *a, + const BIGNUM *p, + const BIGNUM *m, + BN_CTX *ctx, + BN_MONT_CTX *m_ctx)); +int (*RSA_meth_get_init(const RSA_METHOD *meth)) (RSA *rsa); +int RSA_meth_set_init(RSA_METHOD *rsa, int (*init) (RSA *rsa)); +int (*RSA_meth_get_finish(const RSA_METHOD *meth)) (RSA *rsa); +int RSA_meth_set_finish(RSA_METHOD *rsa, int (*finish) (RSA *rsa)); +int (*RSA_meth_get_sign(const RSA_METHOD *meth)) + (int type, + const unsigned char *m, unsigned int m_length, + unsigned char *sigret, unsigned int *siglen, + const RSA *rsa); +int RSA_meth_set_sign(RSA_METHOD *rsa, + int (*sign) (int type, const unsigned char *m, + unsigned int m_length, + unsigned char *sigret, unsigned int *siglen, + const RSA *rsa)); +int (*RSA_meth_get_verify(const RSA_METHOD *meth)) + (int dtype, const unsigned char *m, + unsigned int m_length, const unsigned char *sigbuf, + unsigned int siglen, const RSA *rsa); +int RSA_meth_set_verify(RSA_METHOD *rsa, + int (*verify) (int dtype, const unsigned char *m, + unsigned int m_length, + const unsigned char *sigbuf, + unsigned int siglen, const RSA *rsa)); +int (*RSA_meth_get_keygen(const RSA_METHOD *meth)) + (RSA *rsa, int bits, BIGNUM *e, BN_GENCB *cb); +int RSA_meth_set_keygen(RSA_METHOD *rsa, + int (*keygen) (RSA *rsa, int bits, BIGNUM *e, + BN_GENCB *cb)); + +/* BEGIN ERROR CODES */ +/* + * The following lines are auto generated by the script mkerr.pl. Any changes + * made after this point may be overwritten when the script is next run. + */ + +int ERR_load_RSA_strings(void); + +/* Error codes for the RSA functions. */ + +/* Function codes. */ +# define RSA_F_CHECK_PADDING_MD 140 +# define RSA_F_ENCODE_PKCS1 146 +# define RSA_F_INT_RSA_VERIFY 145 +# define RSA_F_OLD_RSA_PRIV_DECODE 147 +# define RSA_F_PKEY_RSA_CTRL 143 +# define RSA_F_PKEY_RSA_CTRL_STR 144 +# define RSA_F_PKEY_RSA_SIGN 142 +# define RSA_F_PKEY_RSA_VERIFY 149 +# define RSA_F_PKEY_RSA_VERIFYRECOVER 141 +# define RSA_F_RSA_ALGOR_TO_MD 156 +# define RSA_F_RSA_BUILTIN_KEYGEN 129 +# define RSA_F_RSA_CHECK_KEY 123 +# define RSA_F_RSA_CHECK_KEY_EX 160 +# define RSA_F_RSA_CMS_DECRYPT 159 +# define RSA_F_RSA_ITEM_VERIFY 148 +# define RSA_F_RSA_METH_DUP 161 +# define RSA_F_RSA_METH_NEW 162 +# define RSA_F_RSA_METH_SET1_NAME 163 +# define RSA_F_RSA_MGF1_TO_MD 157 +# define RSA_F_RSA_NEW_METHOD 106 +# define RSA_F_RSA_NULL 124 +# define RSA_F_RSA_NULL_PRIVATE_DECRYPT 132 +# define RSA_F_RSA_NULL_PRIVATE_ENCRYPT 133 +# define RSA_F_RSA_NULL_PUBLIC_DECRYPT 134 +# define RSA_F_RSA_NULL_PUBLIC_ENCRYPT 135 +# define RSA_F_RSA_OSSL_PRIVATE_DECRYPT 101 +# define RSA_F_RSA_OSSL_PRIVATE_ENCRYPT 102 +# define RSA_F_RSA_OSSL_PUBLIC_DECRYPT 103 +# define RSA_F_RSA_OSSL_PUBLIC_ENCRYPT 104 +# define RSA_F_RSA_PADDING_ADD_NONE 107 +# define RSA_F_RSA_PADDING_ADD_PKCS1_OAEP 121 +# define RSA_F_RSA_PADDING_ADD_PKCS1_OAEP_MGF1 154 +# define RSA_F_RSA_PADDING_ADD_PKCS1_PSS 125 +# define RSA_F_RSA_PADDING_ADD_PKCS1_PSS_MGF1 152 +# define RSA_F_RSA_PADDING_ADD_PKCS1_TYPE_1 108 +# define RSA_F_RSA_PADDING_ADD_PKCS1_TYPE_2 109 +# define RSA_F_RSA_PADDING_ADD_SSLV23 110 +# define RSA_F_RSA_PADDING_ADD_X931 127 +# define RSA_F_RSA_PADDING_CHECK_NONE 111 +# define RSA_F_RSA_PADDING_CHECK_PKCS1_OAEP 122 +# define RSA_F_RSA_PADDING_CHECK_PKCS1_OAEP_MGF1 153 +# define RSA_F_RSA_PADDING_CHECK_PKCS1_TYPE_1 112 +# define RSA_F_RSA_PADDING_CHECK_PKCS1_TYPE_2 113 +# define RSA_F_RSA_PADDING_CHECK_SSLV23 114 +# define RSA_F_RSA_PADDING_CHECK_X931 128 +# define RSA_F_RSA_PRINT 115 +# define RSA_F_RSA_PRINT_FP 116 +# define RSA_F_RSA_PRIV_ENCODE 138 +# define RSA_F_RSA_PSS_TO_CTX 155 +# define RSA_F_RSA_PUB_DECODE 139 +# define RSA_F_RSA_SETUP_BLINDING 136 +# define RSA_F_RSA_SIGN 117 +# define RSA_F_RSA_SIGN_ASN1_OCTET_STRING 118 +# define RSA_F_RSA_VERIFY 119 +# define RSA_F_RSA_VERIFY_ASN1_OCTET_STRING 120 +# define RSA_F_RSA_VERIFY_PKCS1_PSS_MGF1 126 + +/* Reason codes. */ +# define RSA_R_ALGORITHM_MISMATCH 100 +# define RSA_R_BAD_E_VALUE 101 +# define RSA_R_BAD_FIXED_HEADER_DECRYPT 102 +# define RSA_R_BAD_PAD_BYTE_COUNT 103 +# define RSA_R_BAD_SIGNATURE 104 +# define RSA_R_BLOCK_TYPE_IS_NOT_01 106 +# define RSA_R_BLOCK_TYPE_IS_NOT_02 107 +# define RSA_R_DATA_GREATER_THAN_MOD_LEN 108 +# define RSA_R_DATA_TOO_LARGE 109 +# define RSA_R_DATA_TOO_LARGE_FOR_KEY_SIZE 110 +# define RSA_R_DATA_TOO_LARGE_FOR_MODULUS 132 +# define RSA_R_DATA_TOO_SMALL 111 +# define RSA_R_DATA_TOO_SMALL_FOR_KEY_SIZE 122 +# define RSA_R_DIGEST_DOES_NOT_MATCH 158 +# define RSA_R_DIGEST_TOO_BIG_FOR_RSA_KEY 112 +# define RSA_R_DMP1_NOT_CONGRUENT_TO_D 124 +# define RSA_R_DMQ1_NOT_CONGRUENT_TO_D 125 +# define RSA_R_D_E_NOT_CONGRUENT_TO_1 123 +# define RSA_R_FIRST_OCTET_INVALID 133 +# define RSA_R_ILLEGAL_OR_UNSUPPORTED_PADDING_MODE 144 +# define RSA_R_INVALID_DIGEST 157 +# define RSA_R_INVALID_DIGEST_LENGTH 143 +# define RSA_R_INVALID_HEADER 137 +# define RSA_R_INVALID_LABEL 160 +# define RSA_R_INVALID_MESSAGE_LENGTH 131 +# define RSA_R_INVALID_MGF1_MD 156 +# define RSA_R_INVALID_OAEP_PARAMETERS 161 +# define RSA_R_INVALID_PADDING 138 +# define RSA_R_INVALID_PADDING_MODE 141 +# define RSA_R_INVALID_PSS_PARAMETERS 149 +# define RSA_R_INVALID_PSS_SALTLEN 146 +# define RSA_R_INVALID_SALT_LENGTH 150 +# define RSA_R_INVALID_TRAILER 139 +# define RSA_R_INVALID_X931_DIGEST 142 +# define RSA_R_IQMP_NOT_INVERSE_OF_Q 126 +# define RSA_R_KEY_SIZE_TOO_SMALL 120 +# define RSA_R_LAST_OCTET_INVALID 134 +# define RSA_R_MODULUS_TOO_LARGE 105 +# define RSA_R_NO_PUBLIC_EXPONENT 140 +# define RSA_R_NULL_BEFORE_BLOCK_MISSING 113 +# define RSA_R_N_DOES_NOT_EQUAL_P_Q 127 +# define RSA_R_OAEP_DECODING_ERROR 121 +# define RSA_R_OPERATION_NOT_SUPPORTED_FOR_THIS_KEYTYPE 148 +# define RSA_R_PADDING_CHECK_FAILED 114 +# define RSA_R_PKCS_DECODING_ERROR 159 +# define RSA_R_P_NOT_PRIME 128 +# define RSA_R_Q_NOT_PRIME 129 +# define RSA_R_RSA_OPERATIONS_NOT_SUPPORTED 130 +# define RSA_R_SLEN_CHECK_FAILED 136 +# define RSA_R_SLEN_RECOVERY_FAILED 135 +# define RSA_R_SSLV3_ROLLBACK_ATTACK 115 +# define RSA_R_THE_ASN1_OBJECT_IDENTIFIER_IS_NOT_KNOWN_FOR_THIS_MD 116 +# define RSA_R_UNKNOWN_ALGORITHM_TYPE 117 +# define RSA_R_UNKNOWN_DIGEST 166 +# define RSA_R_UNKNOWN_MASK_DIGEST 151 +# define RSA_R_UNKNOWN_PADDING_TYPE 118 +# define RSA_R_UNSUPPORTED_ENCRYPTION_TYPE 162 +# define RSA_R_UNSUPPORTED_LABEL_SOURCE 163 +# define RSA_R_UNSUPPORTED_MASK_ALGORITHM 153 +# define RSA_R_UNSUPPORTED_MASK_PARAMETER 154 +# define RSA_R_UNSUPPORTED_SIGNATURE_TYPE 155 +# define RSA_R_VALUE_MISSING 147 +# define RSA_R_WRONG_SIGNATURE_LENGTH 119 + +# ifdef __cplusplus +} +# endif +# endif +#endif diff --git a/mobile/ios/FxA/FxA/include/openssl/safestack.h b/mobile/ios/FxA/FxA/include/openssl/safestack.h new file mode 100644 index 0000000000..9fe733c24e --- /dev/null +++ b/mobile/ios/FxA/FxA/include/openssl/safestack.h @@ -0,0 +1,164 @@ +/* + * Copyright 1999-2016 The OpenSSL Project Authors. All Rights Reserved. + * + * Licensed under the OpenSSL license (the "License"). You may not use + * this file except in compliance with the License. You can obtain a copy + * in the file LICENSE in the source distribution or at + * https://www.openssl.org/source/license.html + */ + +#ifndef HEADER_SAFESTACK_H +# define HEADER_SAFESTACK_H + +# include +# include + +#ifdef __cplusplus +extern "C" { +#endif + +# define STACK_OF(type) struct stack_st_##type + +# define SKM_DEFINE_STACK_OF(t1, t2, t3) \ + STACK_OF(t1); \ + typedef int (*sk_##t1##_compfunc)(const t3 * const *a, const t3 *const *b); \ + typedef void (*sk_##t1##_freefunc)(t3 *a); \ + typedef t3 * (*sk_##t1##_copyfunc)(const t3 *a); \ + static ossl_inline int sk_##t1##_num(const STACK_OF(t1) *sk) \ + { \ + return OPENSSL_sk_num((const OPENSSL_STACK *)sk); \ + } \ + static ossl_inline t2 *sk_##t1##_value(const STACK_OF(t1) *sk, int idx) \ + { \ + return (t2 *)OPENSSL_sk_value((const OPENSSL_STACK *)sk, idx); \ + } \ + static ossl_inline STACK_OF(t1) *sk_##t1##_new(sk_##t1##_compfunc compare) \ + { \ + return (STACK_OF(t1) *)OPENSSL_sk_new((OPENSSL_sk_compfunc)compare); \ + } \ + static ossl_inline STACK_OF(t1) *sk_##t1##_new_null(void) \ + { \ + return (STACK_OF(t1) *)OPENSSL_sk_new_null(); \ + } \ + static ossl_inline void sk_##t1##_free(STACK_OF(t1) *sk) \ + { \ + OPENSSL_sk_free((OPENSSL_STACK *)sk); \ + } \ + static ossl_inline void sk_##t1##_zero(STACK_OF(t1) *sk) \ + { \ + OPENSSL_sk_zero((OPENSSL_STACK *)sk); \ + } \ + static ossl_inline t2 *sk_##t1##_delete(STACK_OF(t1) *sk, int i) \ + { \ + return (t2 *)OPENSSL_sk_delete((OPENSSL_STACK *)sk, i); \ + } \ + static ossl_inline t2 *sk_##t1##_delete_ptr(STACK_OF(t1) *sk, t2 *ptr) \ + { \ + return (t2 *)OPENSSL_sk_delete_ptr((OPENSSL_STACK *)sk, \ + (const void *)ptr); \ + } \ + static ossl_inline int sk_##t1##_push(STACK_OF(t1) *sk, t2 *ptr) \ + { \ + return OPENSSL_sk_push((OPENSSL_STACK *)sk, (const void *)ptr); \ + } \ + static ossl_inline int sk_##t1##_unshift(STACK_OF(t1) *sk, t2 *ptr) \ + { \ + return OPENSSL_sk_unshift((OPENSSL_STACK *)sk, (const void *)ptr); \ + } \ + static ossl_inline t2 *sk_##t1##_pop(STACK_OF(t1) *sk) \ + { \ + return (t2 *)OPENSSL_sk_pop((OPENSSL_STACK *)sk); \ + } \ + static ossl_inline t2 *sk_##t1##_shift(STACK_OF(t1) *sk) \ + { \ + return (t2 *)OPENSSL_sk_shift((OPENSSL_STACK *)sk); \ + } \ + static ossl_inline void sk_##t1##_pop_free(STACK_OF(t1) *sk, sk_##t1##_freefunc freefunc) \ + { \ + OPENSSL_sk_pop_free((OPENSSL_STACK *)sk, (OPENSSL_sk_freefunc)freefunc); \ + } \ + static ossl_inline int sk_##t1##_insert(STACK_OF(t1) *sk, t2 *ptr, int idx) \ + { \ + return OPENSSL_sk_insert((OPENSSL_STACK *)sk, (const void *)ptr, idx); \ + } \ + static ossl_inline t2 *sk_##t1##_set(STACK_OF(t1) *sk, int idx, t2 *ptr) \ + { \ + return (t2 *)OPENSSL_sk_set((OPENSSL_STACK *)sk, idx, (const void *)ptr); \ + } \ + static ossl_inline int sk_##t1##_find(STACK_OF(t1) *sk, t2 *ptr) \ + { \ + return OPENSSL_sk_find((OPENSSL_STACK *)sk, (const void *)ptr); \ + } \ + static ossl_inline int sk_##t1##_find_ex(STACK_OF(t1) *sk, t2 *ptr) \ + { \ + return OPENSSL_sk_find_ex((OPENSSL_STACK *)sk, (const void *)ptr); \ + } \ + static ossl_inline void sk_##t1##_sort(STACK_OF(t1) *sk) \ + { \ + OPENSSL_sk_sort((OPENSSL_STACK *)sk); \ + } \ + static ossl_inline int sk_##t1##_is_sorted(const STACK_OF(t1) *sk) \ + { \ + return OPENSSL_sk_is_sorted((const OPENSSL_STACK *)sk); \ + } \ + static ossl_inline STACK_OF(t1) * sk_##t1##_dup(const STACK_OF(t1) *sk) \ + { \ + return (STACK_OF(t1) *)OPENSSL_sk_dup((const OPENSSL_STACK *)sk); \ + } \ + static ossl_inline STACK_OF(t1) *sk_##t1##_deep_copy(const STACK_OF(t1) *sk, \ + sk_##t1##_copyfunc copyfunc, \ + sk_##t1##_freefunc freefunc) \ + { \ + return (STACK_OF(t1) *)OPENSSL_sk_deep_copy((const OPENSSL_STACK *)sk, \ + (OPENSSL_sk_copyfunc)copyfunc, \ + (OPENSSL_sk_freefunc)freefunc); \ + } \ + static ossl_inline sk_##t1##_compfunc sk_##t1##_set_cmp_func(STACK_OF(t1) *sk, sk_##t1##_compfunc compare) \ + { \ + return (sk_##t1##_compfunc)OPENSSL_sk_set_cmp_func((OPENSSL_STACK *)sk, (OPENSSL_sk_compfunc)compare); \ + } + +# define DEFINE_SPECIAL_STACK_OF(t1, t2) SKM_DEFINE_STACK_OF(t1, t2, t2) +# define DEFINE_STACK_OF(t) SKM_DEFINE_STACK_OF(t, t, t) +# define DEFINE_SPECIAL_STACK_OF_CONST(t1, t2) \ + SKM_DEFINE_STACK_OF(t1, const t2, t2) +# define DEFINE_STACK_OF_CONST(t) SKM_DEFINE_STACK_OF(t, const t, t) + +/*- + * Strings are special: normally an lhash entry will point to a single + * (somewhat) mutable object. In the case of strings: + * + * a) Instead of a single char, there is an array of chars, NUL-terminated. + * b) The string may have be immutable. + * + * So, they need their own declarations. Especially important for + * type-checking tools, such as Deputy. + * + * In practice, however, it appears to be hard to have a const + * string. For now, I'm settling for dealing with the fact it is a + * string at all. + */ +typedef char *OPENSSL_STRING; +typedef const char *OPENSSL_CSTRING; + +/*- + * Confusingly, LHASH_OF(STRING) deals with char ** throughout, but + * STACK_OF(STRING) is really more like STACK_OF(char), only, as mentioned + * above, instead of a single char each entry is a NUL-terminated array of + * chars. So, we have to implement STRING specially for STACK_OF. This is + * dealt with in the autogenerated macros below. + */ +DEFINE_SPECIAL_STACK_OF(OPENSSL_STRING, char) +DEFINE_SPECIAL_STACK_OF_CONST(OPENSSL_CSTRING, char) + +/* + * Similarly, we sometimes use a block of characters, NOT nul-terminated. + * These should also be distinguished from "normal" stacks. + */ +typedef void *OPENSSL_BLOCK; +DEFINE_SPECIAL_STACK_OF(OPENSSL_BLOCK, void) + +# ifdef __cplusplus +} +# endif +#endif diff --git a/mobile/ios/FxA/FxA/include/openssl/seed.h b/mobile/ios/FxA/FxA/include/openssl/seed.h new file mode 100644 index 0000000000..bb97131d76 --- /dev/null +++ b/mobile/ios/FxA/FxA/include/openssl/seed.h @@ -0,0 +1,98 @@ +/* + * Copyright 2007-2016 The OpenSSL Project Authors. All Rights Reserved. + * + * Licensed under the OpenSSL license (the "License"). You may not use + * this file except in compliance with the License. You can obtain a copy + * in the file LICENSE in the source distribution or at + * https://www.openssl.org/source/license.html + */ + +/* + * Copyright (c) 2007 KISA(Korea Information Security Agency). All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Neither the name of author nor the names of its contributors may + * be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY AUTHOR AND CONTRIBUTORS ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL AUTHOR OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + */ + +#ifndef HEADER_SEED_H +# define HEADER_SEED_H + +# include + +# ifndef OPENSSL_NO_SEED +# include +# include + +#ifdef __cplusplus +extern "C" { +#endif + +/* look whether we need 'long' to get 32 bits */ +# ifdef AES_LONG +# ifndef SEED_LONG +# define SEED_LONG 1 +# endif +# endif + +# if !defined(NO_SYS_TYPES_H) +# include +# endif + +# define SEED_BLOCK_SIZE 16 +# define SEED_KEY_LENGTH 16 + +typedef struct seed_key_st { +# ifdef SEED_LONG + unsigned long data[32]; +# else + unsigned int data[32]; +# endif +} SEED_KEY_SCHEDULE; + +void SEED_set_key(const unsigned char rawkey[SEED_KEY_LENGTH], + SEED_KEY_SCHEDULE *ks); + +void SEED_encrypt(const unsigned char s[SEED_BLOCK_SIZE], + unsigned char d[SEED_BLOCK_SIZE], + const SEED_KEY_SCHEDULE *ks); +void SEED_decrypt(const unsigned char s[SEED_BLOCK_SIZE], + unsigned char d[SEED_BLOCK_SIZE], + const SEED_KEY_SCHEDULE *ks); + +void SEED_ecb_encrypt(const unsigned char *in, unsigned char *out, + const SEED_KEY_SCHEDULE *ks, int enc); +void SEED_cbc_encrypt(const unsigned char *in, unsigned char *out, size_t len, + const SEED_KEY_SCHEDULE *ks, + unsigned char ivec[SEED_BLOCK_SIZE], int enc); +void SEED_cfb128_encrypt(const unsigned char *in, unsigned char *out, + size_t len, const SEED_KEY_SCHEDULE *ks, + unsigned char ivec[SEED_BLOCK_SIZE], int *num, + int enc); +void SEED_ofb128_encrypt(const unsigned char *in, unsigned char *out, + size_t len, const SEED_KEY_SCHEDULE *ks, + unsigned char ivec[SEED_BLOCK_SIZE], int *num); + +# ifdef __cplusplus +} +# endif +# endif + +#endif diff --git a/mobile/ios/FxA/FxA/include/openssl/sha.h b/mobile/ios/FxA/FxA/include/openssl/sha.h new file mode 100644 index 0000000000..6a1eb0de8b --- /dev/null +++ b/mobile/ios/FxA/FxA/include/openssl/sha.h @@ -0,0 +1,119 @@ +/* + * Copyright 1995-2016 The OpenSSL Project Authors. All Rights Reserved. + * + * Licensed under the OpenSSL license (the "License"). You may not use + * this file except in compliance with the License. You can obtain a copy + * in the file LICENSE in the source distribution or at + * https://www.openssl.org/source/license.html + */ + +#ifndef HEADER_SHA_H +# define HEADER_SHA_H + +# include +# include + +#ifdef __cplusplus +extern "C" { +#endif + +/*- + * !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + * ! SHA_LONG has to be at least 32 bits wide. ! + * !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + */ +# define SHA_LONG unsigned int + +# define SHA_LBLOCK 16 +# define SHA_CBLOCK (SHA_LBLOCK*4)/* SHA treats input data as a + * contiguous array of 32 bit wide + * big-endian values. */ +# define SHA_LAST_BLOCK (SHA_CBLOCK-8) +# define SHA_DIGEST_LENGTH 20 + +typedef struct SHAstate_st { + SHA_LONG h0, h1, h2, h3, h4; + SHA_LONG Nl, Nh; + SHA_LONG data[SHA_LBLOCK]; + unsigned int num; +} SHA_CTX; + +int SHA1_Init(SHA_CTX *c); +int SHA1_Update(SHA_CTX *c, const void *data, size_t len); +int SHA1_Final(unsigned char *md, SHA_CTX *c); +unsigned char *SHA1(const unsigned char *d, size_t n, unsigned char *md); +void SHA1_Transform(SHA_CTX *c, const unsigned char *data); + +# define SHA256_CBLOCK (SHA_LBLOCK*4)/* SHA-256 treats input data as a + * contiguous array of 32 bit wide + * big-endian values. */ + +typedef struct SHA256state_st { + SHA_LONG h[8]; + SHA_LONG Nl, Nh; + SHA_LONG data[SHA_LBLOCK]; + unsigned int num, md_len; +} SHA256_CTX; + +int SHA224_Init(SHA256_CTX *c); +int SHA224_Update(SHA256_CTX *c, const void *data, size_t len); +int SHA224_Final(unsigned char *md, SHA256_CTX *c); +unsigned char *SHA224(const unsigned char *d, size_t n, unsigned char *md); +int SHA256_Init(SHA256_CTX *c); +int SHA256_Update(SHA256_CTX *c, const void *data, size_t len); +int SHA256_Final(unsigned char *md, SHA256_CTX *c); +unsigned char *SHA256(const unsigned char *d, size_t n, unsigned char *md); +void SHA256_Transform(SHA256_CTX *c, const unsigned char *data); + +# define SHA224_DIGEST_LENGTH 28 +# define SHA256_DIGEST_LENGTH 32 +# define SHA384_DIGEST_LENGTH 48 +# define SHA512_DIGEST_LENGTH 64 + +/* + * Unlike 32-bit digest algorithms, SHA-512 *relies* on SHA_LONG64 + * being exactly 64-bit wide. See Implementation Notes in sha512.c + * for further details. + */ +/* + * SHA-512 treats input data as a + * contiguous array of 64 bit + * wide big-endian values. + */ +# define SHA512_CBLOCK (SHA_LBLOCK*8) +# if (defined(_WIN32) || defined(_WIN64)) && !defined(__MINGW32__) +# define SHA_LONG64 unsigned __int64 +# define U64(C) C##UI64 +# elif defined(__arch64__) +# define SHA_LONG64 unsigned long +# define U64(C) C##UL +# else +# define SHA_LONG64 unsigned long long +# define U64(C) C##ULL +# endif + +typedef struct SHA512state_st { + SHA_LONG64 h[8]; + SHA_LONG64 Nl, Nh; + union { + SHA_LONG64 d[SHA_LBLOCK]; + unsigned char p[SHA512_CBLOCK]; + } u; + unsigned int num, md_len; +} SHA512_CTX; + +int SHA384_Init(SHA512_CTX *c); +int SHA384_Update(SHA512_CTX *c, const void *data, size_t len); +int SHA384_Final(unsigned char *md, SHA512_CTX *c); +unsigned char *SHA384(const unsigned char *d, size_t n, unsigned char *md); +int SHA512_Init(SHA512_CTX *c); +int SHA512_Update(SHA512_CTX *c, const void *data, size_t len); +int SHA512_Final(unsigned char *md, SHA512_CTX *c); +unsigned char *SHA512(const unsigned char *d, size_t n, unsigned char *md); +void SHA512_Transform(SHA512_CTX *c, const unsigned char *data); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/mobile/ios/FxA/FxA/include/openssl/srp.h b/mobile/ios/FxA/FxA/include/openssl/srp.h new file mode 100644 index 0000000000..f2b6ec750d --- /dev/null +++ b/mobile/ios/FxA/FxA/include/openssl/srp.h @@ -0,0 +1,131 @@ +/* + * Copyright 2011-2016 The OpenSSL Project Authors. All Rights Reserved. + * + * Licensed under the OpenSSL license (the "License"). You may not use + * this file except in compliance with the License. You can obtain a copy + * in the file LICENSE in the source distribution or at + * https://www.openssl.org/source/license.html + */ + +#ifndef HEADER_SRP_H +# define HEADER_SRP_H + +#include + +#ifndef OPENSSL_NO_SRP +# include +# include +# include +# include +# include + +# ifdef __cplusplus +extern "C" { +# endif + +typedef struct SRP_gN_cache_st { + char *b64_bn; + BIGNUM *bn; +} SRP_gN_cache; + + +DEFINE_STACK_OF(SRP_gN_cache) + +typedef struct SRP_user_pwd_st { + /* Owned by us. */ + char *id; + BIGNUM *s; + BIGNUM *v; + /* Not owned by us. */ + const BIGNUM *g; + const BIGNUM *N; + /* Owned by us. */ + char *info; +} SRP_user_pwd; + +void SRP_user_pwd_free(SRP_user_pwd *user_pwd); + +DEFINE_STACK_OF(SRP_user_pwd) + +typedef struct SRP_VBASE_st { + STACK_OF(SRP_user_pwd) *users_pwd; + STACK_OF(SRP_gN_cache) *gN_cache; +/* to simulate a user */ + char *seed_key; + const BIGNUM *default_g; + const BIGNUM *default_N; +} SRP_VBASE; + +/* + * Internal structure storing N and g pair + */ +typedef struct SRP_gN_st { + char *id; + const BIGNUM *g; + const BIGNUM *N; +} SRP_gN; + +DEFINE_STACK_OF(SRP_gN) + +SRP_VBASE *SRP_VBASE_new(char *seed_key); +void SRP_VBASE_free(SRP_VBASE *vb); +int SRP_VBASE_init(SRP_VBASE *vb, char *verifier_file); + +/* This method ignores the configured seed and fails for an unknown user. */ +DEPRECATEDIN_1_1_0(SRP_user_pwd *SRP_VBASE_get_by_user(SRP_VBASE *vb, char *username)) +/* NOTE: unlike in SRP_VBASE_get_by_user, caller owns the returned pointer.*/ +SRP_user_pwd *SRP_VBASE_get1_by_user(SRP_VBASE *vb, char *username); + +char *SRP_create_verifier(const char *user, const char *pass, char **salt, + char **verifier, const char *N, const char *g); +int SRP_create_verifier_BN(const char *user, const char *pass, BIGNUM **salt, + BIGNUM **verifier, const BIGNUM *N, + const BIGNUM *g); + +# define SRP_NO_ERROR 0 +# define SRP_ERR_VBASE_INCOMPLETE_FILE 1 +# define SRP_ERR_VBASE_BN_LIB 2 +# define SRP_ERR_OPEN_FILE 3 +# define SRP_ERR_MEMORY 4 + +# define DB_srptype 0 +# define DB_srpverifier 1 +# define DB_srpsalt 2 +# define DB_srpid 3 +# define DB_srpgN 4 +# define DB_srpinfo 5 +# undef DB_NUMBER +# define DB_NUMBER 6 + +# define DB_SRP_INDEX 'I' +# define DB_SRP_VALID 'V' +# define DB_SRP_REVOKED 'R' +# define DB_SRP_MODIF 'v' + +/* see srp.c */ +char *SRP_check_known_gN_param(const BIGNUM *g, const BIGNUM *N); +SRP_gN *SRP_get_default_gN(const char *id); + +/* server side .... */ +BIGNUM *SRP_Calc_server_key(const BIGNUM *A, const BIGNUM *v, const BIGNUM *u, + const BIGNUM *b, const BIGNUM *N); +BIGNUM *SRP_Calc_B(const BIGNUM *b, const BIGNUM *N, const BIGNUM *g, + const BIGNUM *v); +int SRP_Verify_A_mod_N(const BIGNUM *A, const BIGNUM *N); +BIGNUM *SRP_Calc_u(const BIGNUM *A, const BIGNUM *B, const BIGNUM *N); + +/* client side .... */ +BIGNUM *SRP_Calc_x(const BIGNUM *s, const char *user, const char *pass); +BIGNUM *SRP_Calc_A(const BIGNUM *a, const BIGNUM *N, const BIGNUM *g); +BIGNUM *SRP_Calc_client_key(const BIGNUM *N, const BIGNUM *B, const BIGNUM *g, + const BIGNUM *x, const BIGNUM *a, const BIGNUM *u); +int SRP_Verify_B_mod_N(const BIGNUM *B, const BIGNUM *N); + +# define SRP_MINIMAL_N 1024 + +# ifdef __cplusplus +} +# endif +# endif + +#endif diff --git a/mobile/ios/FxA/FxA/include/openssl/srtp.h b/mobile/ios/FxA/FxA/include/openssl/srtp.h new file mode 100644 index 0000000000..5ddfa46d9b --- /dev/null +++ b/mobile/ios/FxA/FxA/include/openssl/srtp.h @@ -0,0 +1,50 @@ +/* + * Copyright 2011-2016 The OpenSSL Project Authors. All Rights Reserved. + * + * Licensed under the OpenSSL license (the "License"). You may not use + * this file except in compliance with the License. You can obtain a copy + * in the file LICENSE in the source distribution or at + * https://www.openssl.org/source/license.html + */ + +/* + * DTLS code by Eric Rescorla + * + * Copyright (C) 2006, Network Resonance, Inc. Copyright (C) 2011, RTFM, Inc. + */ + +#ifndef HEADER_D1_SRTP_H +# define HEADER_D1_SRTP_H + +# include + +#ifdef __cplusplus +extern "C" { +#endif + +# define SRTP_AES128_CM_SHA1_80 0x0001 +# define SRTP_AES128_CM_SHA1_32 0x0002 +# define SRTP_AES128_F8_SHA1_80 0x0003 +# define SRTP_AES128_F8_SHA1_32 0x0004 +# define SRTP_NULL_SHA1_80 0x0005 +# define SRTP_NULL_SHA1_32 0x0006 + +/* AEAD SRTP protection profiles from RFC 7714 */ +# define SRTP_AEAD_AES_128_GCM 0x0007 +# define SRTP_AEAD_AES_256_GCM 0x0008 + +# ifndef OPENSSL_NO_SRTP + +__owur int SSL_CTX_set_tlsext_use_srtp(SSL_CTX *ctx, const char *profiles); +__owur int SSL_set_tlsext_use_srtp(SSL *ctx, const char *profiles); + +__owur STACK_OF(SRTP_PROTECTION_PROFILE) *SSL_get_srtp_profiles(SSL *ssl); +__owur SRTP_PROTECTION_PROFILE *SSL_get_selected_srtp_profile(SSL *s); + +# endif + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/mobile/ios/FxA/FxA/include/openssl/ssl.h b/mobile/ios/FxA/FxA/include/openssl/ssl.h new file mode 100644 index 0000000000..8d75d53eca --- /dev/null +++ b/mobile/ios/FxA/FxA/include/openssl/ssl.h @@ -0,0 +1,2531 @@ +/* + * Copyright 1995-2016 The OpenSSL Project Authors. All Rights Reserved. + * + * Licensed under the OpenSSL license (the "License"). You may not use + * this file except in compliance with the License. You can obtain a copy + * in the file LICENSE in the source distribution or at + * https://www.openssl.org/source/license.html + */ + +/* ==================================================================== + * Copyright 2002 Sun Microsystems, Inc. ALL RIGHTS RESERVED. + * ECC cipher suite support in OpenSSL originally developed by + * SUN MICROSYSTEMS, INC., and contributed to the OpenSSL project. + */ +/* ==================================================================== + * Copyright 2005 Nokia. All rights reserved. + * + * The portions of the attached software ("Contribution") is developed by + * Nokia Corporation and is licensed pursuant to the OpenSSL open source + * license. + * + * The Contribution, originally written by Mika Kousa and Pasi Eronen of + * Nokia Corporation, consists of the "PSK" (Pre-Shared Key) ciphersuites + * support (see RFC 4279) to OpenSSL. + * + * No patent licenses or other rights except those expressly stated in + * the OpenSSL open source license shall be deemed granted or received + * expressly, by implication, estoppel, or otherwise. + * + * No assurances are provided by Nokia that the Contribution does not + * infringe the patent or other intellectual property rights of any third + * party or that the license provides you with all the necessary rights + * to make use of the Contribution. + * + * THE SOFTWARE IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND. IN + * ADDITION TO THE DISCLAIMERS INCLUDED IN THE LICENSE, NOKIA + * SPECIFICALLY DISCLAIMS ANY LIABILITY FOR CLAIMS BROUGHT BY YOU OR ANY + * OTHER ENTITY BASED ON INFRINGEMENT OF INTELLECTUAL PROPERTY RIGHTS OR + * OTHERWISE. + */ + +#ifndef HEADER_SSL_H +# define HEADER_SSL_H + +# include +# include +# include +# include +# if OPENSSL_API_COMPAT < 0x10100000L +# include +# include +# include +# include +# endif +# include +# include +# include + +# include +# include +# include + +#ifdef __cplusplus +extern "C" { +#endif + +/* OpenSSL version number for ASN.1 encoding of the session information */ +/*- + * Version 0 - initial version + * Version 1 - added the optional peer certificate + */ +# define SSL_SESSION_ASN1_VERSION 0x0001 + +# define SSL_MAX_SSL_SESSION_ID_LENGTH 32 +# define SSL_MAX_SID_CTX_LENGTH 32 + +# define SSL_MIN_RSA_MODULUS_LENGTH_IN_BYTES (512/8) +# define SSL_MAX_KEY_ARG_LENGTH 8 +# define SSL_MAX_MASTER_KEY_LENGTH 48 + +/* The maximum number of encrypt/decrypt pipelines we can support */ +# define SSL_MAX_PIPELINES 32 + +/* text strings for the ciphers */ + +/* These are used to specify which ciphers to use and not to use */ + +# define SSL_TXT_LOW "LOW" +# define SSL_TXT_MEDIUM "MEDIUM" +# define SSL_TXT_HIGH "HIGH" +# define SSL_TXT_FIPS "FIPS" + +# define SSL_TXT_aNULL "aNULL" +# define SSL_TXT_eNULL "eNULL" +# define SSL_TXT_NULL "NULL" + +# define SSL_TXT_kRSA "kRSA" +# define SSL_TXT_kDHr "kDHr" +# define SSL_TXT_kDHd "kDHd" +# define SSL_TXT_kDH "kDH" +# define SSL_TXT_kEDH "kEDH"/* alias for kDHE */ +# define SSL_TXT_kDHE "kDHE" +# define SSL_TXT_kECDHr "kECDHr" +# define SSL_TXT_kECDHe "kECDHe" +# define SSL_TXT_kECDH "kECDH" +# define SSL_TXT_kEECDH "kEECDH"/* alias for kECDHE */ +# define SSL_TXT_kECDHE "kECDHE" +# define SSL_TXT_kPSK "kPSK" +# define SSL_TXT_kRSAPSK "kRSAPSK" +# define SSL_TXT_kECDHEPSK "kECDHEPSK" +# define SSL_TXT_kDHEPSK "kDHEPSK" +# define SSL_TXT_kGOST "kGOST" +# define SSL_TXT_kSRP "kSRP" + +# define SSL_TXT_aRSA "aRSA" +# define SSL_TXT_aDSS "aDSS" +# define SSL_TXT_aDH "aDH" +# define SSL_TXT_aECDH "aECDH" +# define SSL_TXT_aECDSA "aECDSA" +# define SSL_TXT_aPSK "aPSK" +# define SSL_TXT_aGOST94 "aGOST94" +# define SSL_TXT_aGOST01 "aGOST01" +# define SSL_TXT_aGOST12 "aGOST12" +# define SSL_TXT_aGOST "aGOST" +# define SSL_TXT_aSRP "aSRP" + +# define SSL_TXT_DSS "DSS" +# define SSL_TXT_DH "DH" +# define SSL_TXT_DHE "DHE"/* same as "kDHE:-ADH" */ +# define SSL_TXT_EDH "EDH"/* alias for DHE */ +# define SSL_TXT_ADH "ADH" +# define SSL_TXT_RSA "RSA" +# define SSL_TXT_ECDH "ECDH" +# define SSL_TXT_EECDH "EECDH"/* alias for ECDHE" */ +# define SSL_TXT_ECDHE "ECDHE"/* same as "kECDHE:-AECDH" */ +# define SSL_TXT_AECDH "AECDH" +# define SSL_TXT_ECDSA "ECDSA" +# define SSL_TXT_PSK "PSK" +# define SSL_TXT_SRP "SRP" + +# define SSL_TXT_DES "DES" +# define SSL_TXT_3DES "3DES" +# define SSL_TXT_RC4 "RC4" +# define SSL_TXT_RC2 "RC2" +# define SSL_TXT_IDEA "IDEA" +# define SSL_TXT_SEED "SEED" +# define SSL_TXT_AES128 "AES128" +# define SSL_TXT_AES256 "AES256" +# define SSL_TXT_AES "AES" +# define SSL_TXT_AES_GCM "AESGCM" +# define SSL_TXT_AES_CCM "AESCCM" +# define SSL_TXT_AES_CCM_8 "AESCCM8" +# define SSL_TXT_CAMELLIA128 "CAMELLIA128" +# define SSL_TXT_CAMELLIA256 "CAMELLIA256" +# define SSL_TXT_CAMELLIA "CAMELLIA" +# define SSL_TXT_CHACHA20 "CHACHA20" +# define SSL_TXT_GOST "GOST89" + +# define SSL_TXT_MD5 "MD5" +# define SSL_TXT_SHA1 "SHA1" +# define SSL_TXT_SHA "SHA"/* same as "SHA1" */ +# define SSL_TXT_GOST94 "GOST94" +# define SSL_TXT_GOST89MAC "GOST89MAC" +# define SSL_TXT_GOST12 "GOST12" +# define SSL_TXT_GOST89MAC12 "GOST89MAC12" +# define SSL_TXT_SHA256 "SHA256" +# define SSL_TXT_SHA384 "SHA384" + +# define SSL_TXT_SSLV3 "SSLv3" +# define SSL_TXT_TLSV1 "TLSv1" +# define SSL_TXT_TLSV1_1 "TLSv1.1" +# define SSL_TXT_TLSV1_2 "TLSv1.2" + +# define SSL_TXT_ALL "ALL" + +/*- + * COMPLEMENTOF* definitions. These identifiers are used to (de-select) + * ciphers normally not being used. + * Example: "RC4" will activate all ciphers using RC4 including ciphers + * without authentication, which would normally disabled by DEFAULT (due + * the "!ADH" being part of default). Therefore "RC4:!COMPLEMENTOFDEFAULT" + * will make sure that it is also disabled in the specific selection. + * COMPLEMENTOF* identifiers are portable between version, as adjustments + * to the default cipher setup will also be included here. + * + * COMPLEMENTOFDEFAULT does not experience the same special treatment that + * DEFAULT gets, as only selection is being done and no sorting as needed + * for DEFAULT. + */ +# define SSL_TXT_CMPALL "COMPLEMENTOFALL" +# define SSL_TXT_CMPDEF "COMPLEMENTOFDEFAULT" + +/* + * The following cipher list is used by default. It also is substituted when + * an application-defined cipher list string starts with 'DEFAULT'. + */ +# define SSL_DEFAULT_CIPHER_LIST "ALL:!COMPLEMENTOFDEFAULT:!eNULL" +/* + * As of OpenSSL 1.0.0, ssl_create_cipher_list() in ssl/ssl_ciph.c always + * starts with a reasonable order, and all we have to do for DEFAULT is + * throwing out anonymous and unencrypted ciphersuites! (The latter are not + * actually enabled by ALL, but "ALL:RSA" would enable some of them.) + */ + +/* Used in SSL_set_shutdown()/SSL_get_shutdown(); */ +# define SSL_SENT_SHUTDOWN 1 +# define SSL_RECEIVED_SHUTDOWN 2 + +#ifdef __cplusplus +} +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +# define SSL_FILETYPE_ASN1 X509_FILETYPE_ASN1 +# define SSL_FILETYPE_PEM X509_FILETYPE_PEM + +/* + * This is needed to stop compilers complaining about the 'struct ssl_st *' + * function parameters used to prototype callbacks in SSL_CTX. + */ +typedef struct ssl_st *ssl_crock_st; +typedef struct tls_session_ticket_ext_st TLS_SESSION_TICKET_EXT; +typedef struct ssl_method_st SSL_METHOD; +typedef struct ssl_cipher_st SSL_CIPHER; +typedef struct ssl_session_st SSL_SESSION; +typedef struct tls_sigalgs_st TLS_SIGALGS; +typedef struct ssl_conf_ctx_st SSL_CONF_CTX; +typedef struct ssl_comp_st SSL_COMP; + +STACK_OF(SSL_CIPHER); +STACK_OF(SSL_COMP); + +/* SRTP protection profiles for use with the use_srtp extension (RFC 5764)*/ +typedef struct srtp_protection_profile_st { + const char *name; + unsigned long id; +} SRTP_PROTECTION_PROFILE; + +DEFINE_STACK_OF(SRTP_PROTECTION_PROFILE) + +typedef int (*tls_session_ticket_ext_cb_fn) (SSL *s, + const unsigned char *data, + int len, void *arg); +typedef int (*tls_session_secret_cb_fn) (SSL *s, void *secret, + int *secret_len, + STACK_OF(SSL_CIPHER) *peer_ciphers, + const SSL_CIPHER **cipher, void *arg); + +/* Typedefs for handling custom extensions */ + +typedef int (*custom_ext_add_cb) (SSL *s, unsigned int ext_type, + const unsigned char **out, + size_t *outlen, int *al, void *add_arg); + +typedef void (*custom_ext_free_cb) (SSL *s, unsigned int ext_type, + const unsigned char *out, void *add_arg); + +typedef int (*custom_ext_parse_cb) (SSL *s, unsigned int ext_type, + const unsigned char *in, + size_t inlen, int *al, void *parse_arg); + +/* Typedef for verification callback */ +typedef int (*SSL_verify_cb)(int preverify_ok, X509_STORE_CTX *x509_ctx); + +/* Allow initial connection to servers that don't support RI */ +# define SSL_OP_LEGACY_SERVER_CONNECT 0x00000004U +/* Removed from OpenSSL 0.9.8q and 1.0.0c */ +/* Dead forever, see CVE-2010-4180. */ +# define SSL_OP_NETSCAPE_REUSE_CIPHER_CHANGE_BUG 0x0U +# define SSL_OP_TLSEXT_PADDING 0x00000010U +# define SSL_OP_MICROSOFT_BIG_SSLV3_BUFFER 0x0U +# define SSL_OP_SAFARI_ECDHE_ECDSA_BUG 0x00000040U +/* Ancient SSLeay version, retained for compatibility */ +# define SSL_OP_SSLEAY_080_CLIENT_DH_BUG 0x0 +# define SSL_OP_TLS_D5_BUG 0x0U +/* Removed from OpenSSL 1.1.0 */ +# define SSL_OP_TLS_BLOCK_PADDING_BUG 0x0U + +/* Hasn't done anything since OpenSSL 0.9.7h, retained for compatibility */ +# define SSL_OP_MSIE_SSLV2_RSA_PADDING 0x0 +/* Refers to ancient SSLREF and SSLv2, retained for compatibility */ +# define SSL_OP_SSLREF2_REUSE_CERT_TYPE_BUG 0x0 +/* Related to removed SSLv2 */ +# define SSL_OP_MICROSOFT_SESS_ID_BUG 0x0 +# define SSL_OP_NETSCAPE_CHALLENGE_BUG 0x0 + +/* + * Disable SSL 3.0/TLS 1.0 CBC vulnerability workaround that was added in + * OpenSSL 0.9.6d. Usually (depending on the application protocol) the + * workaround is not needed. Unfortunately some broken SSL/TLS + * implementations cannot handle it at all, which is why we include it in + * SSL_OP_ALL. + */ +/* added in 0.9.6e */ +# define SSL_OP_DONT_INSERT_EMPTY_FRAGMENTS 0x00000800U + +/* + * SSL_OP_ALL: various bug workarounds that should be rather harmless. This + * used to be 0x000FFFFFL before 0.9.7. + */ +# define SSL_OP_ALL 0x80000BFFU + +/* DTLS options */ +# define SSL_OP_NO_QUERY_MTU 0x00001000U +/* Turn on Cookie Exchange (on relevant for servers) */ +# define SSL_OP_COOKIE_EXCHANGE 0x00002000U +/* Don't use RFC4507 ticket extension */ +# define SSL_OP_NO_TICKET 0x00004000U +# ifndef OPENSSL_NO_DTLS1_METHOD +/* Use Cisco's "speshul" version of DTLS_BAD_VER + * (only with deprecated DTLSv1_client_method()) */ +# define SSL_OP_CISCO_ANYCONNECT 0x00008000U +# endif + +/* As server, disallow session resumption on renegotiation */ +# define SSL_OP_NO_SESSION_RESUMPTION_ON_RENEGOTIATION 0x00010000U +/* Don't use compression even if supported */ +# define SSL_OP_NO_COMPRESSION 0x00020000U +/* Permit unsafe legacy renegotiation */ +# define SSL_OP_ALLOW_UNSAFE_LEGACY_RENEGOTIATION 0x00040000U +/* Does nothing: retained for compatibility */ +# define SSL_OP_SINGLE_ECDH_USE 0x0 +/* Does nothing: retained for compatibility */ +# define SSL_OP_SINGLE_DH_USE 0x0 +/* Does nothing: retained for compatibility */ +# define SSL_OP_EPHEMERAL_RSA 0x0 +/* + * Set on servers to choose the cipher according to the server's preferences + */ +# define SSL_OP_CIPHER_SERVER_PREFERENCE 0x00400000U +/* + * If set, a server will allow a client to issue a SSLv3.0 version number as + * latest version supported in the premaster secret, even when TLSv1.0 + * (version 3.1) was announced in the client hello. Normally this is + * forbidden to prevent version rollback attacks. + */ +# define SSL_OP_TLS_ROLLBACK_BUG 0x00800000U + +# define SSL_OP_NO_SSLv2 0x00000000U +# define SSL_OP_NO_SSLv3 0x02000000U +# define SSL_OP_NO_TLSv1 0x04000000U +# define SSL_OP_NO_TLSv1_2 0x08000000U +# define SSL_OP_NO_TLSv1_1 0x10000000U + +# define SSL_OP_NO_DTLSv1 0x04000000U +# define SSL_OP_NO_DTLSv1_2 0x08000000U + +# define SSL_OP_NO_SSL_MASK (SSL_OP_NO_SSLv3|\ + SSL_OP_NO_TLSv1|SSL_OP_NO_TLSv1_1|SSL_OP_NO_TLSv1_2) +# define SSL_OP_NO_DTLS_MASK (SSL_OP_NO_DTLSv1|SSL_OP_NO_DTLSv1_2) + + +/* Removed from previous versions */ +# define SSL_OP_PKCS1_CHECK_1 0x0 +# define SSL_OP_PKCS1_CHECK_2 0x0 +# define SSL_OP_NETSCAPE_CA_DN_BUG 0x0 +# define SSL_OP_NETSCAPE_DEMO_CIPHER_CHANGE_BUG 0x0U +/* + * Make server add server-hello extension from early version of cryptopro + * draft, when GOST ciphersuite is negotiated. Required for interoperability + * with CryptoPro CSP 3.x + */ +# define SSL_OP_CRYPTOPRO_TLSEXT_BUG 0x80000000U + +/* + * Allow SSL_write(..., n) to return r with 0 < r < n (i.e. report success + * when just a single record has been written): + */ +# define SSL_MODE_ENABLE_PARTIAL_WRITE 0x00000001U +/* + * Make it possible to retry SSL_write() with changed buffer location (buffer + * contents must stay the same!); this is not the default to avoid the + * misconception that non-blocking SSL_write() behaves like non-blocking + * write(): + */ +# define SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER 0x00000002U +/* + * Never bother the application with retries if the transport is blocking: + */ +# define SSL_MODE_AUTO_RETRY 0x00000004U +/* Don't attempt to automatically build certificate chain */ +# define SSL_MODE_NO_AUTO_CHAIN 0x00000008U +/* + * Save RAM by releasing read and write buffers when they're empty. (SSL3 and + * TLS only.) "Released" buffers are put onto a free-list in the context or + * just freed (depending on the context's setting for freelist_max_len). + */ +# define SSL_MODE_RELEASE_BUFFERS 0x00000010U +/* + * Send the current time in the Random fields of the ClientHello and + * ServerHello records for compatibility with hypothetical implementations + * that require it. + */ +# define SSL_MODE_SEND_CLIENTHELLO_TIME 0x00000020U +# define SSL_MODE_SEND_SERVERHELLO_TIME 0x00000040U +/* + * Send TLS_FALLBACK_SCSV in the ClientHello. To be set only by applications + * that reconnect with a downgraded protocol version; see + * draft-ietf-tls-downgrade-scsv-00 for details. DO NOT ENABLE THIS if your + * application attempts a normal handshake. Only use this in explicit + * fallback retries, following the guidance in + * draft-ietf-tls-downgrade-scsv-00. + */ +# define SSL_MODE_SEND_FALLBACK_SCSV 0x00000080U +/* + * Support Asynchronous operation + */ +# define SSL_MODE_ASYNC 0x00000100U + +/* Cert related flags */ +/* + * Many implementations ignore some aspects of the TLS standards such as + * enforcing certificate chain algorithms. When this is set we enforce them. + */ +# define SSL_CERT_FLAG_TLS_STRICT 0x00000001U + +/* Suite B modes, takes same values as certificate verify flags */ +# define SSL_CERT_FLAG_SUITEB_128_LOS_ONLY 0x10000 +/* Suite B 192 bit only mode */ +# define SSL_CERT_FLAG_SUITEB_192_LOS 0x20000 +/* Suite B 128 bit mode allowing 192 bit algorithms */ +# define SSL_CERT_FLAG_SUITEB_128_LOS 0x30000 + +/* Perform all sorts of protocol violations for testing purposes */ +# define SSL_CERT_FLAG_BROKEN_PROTOCOL 0x10000000 + +/* Flags for building certificate chains */ +/* Treat any existing certificates as untrusted CAs */ +# define SSL_BUILD_CHAIN_FLAG_UNTRUSTED 0x1 +/* Don't include root CA in chain */ +# define SSL_BUILD_CHAIN_FLAG_NO_ROOT 0x2 +/* Just check certificates already there */ +# define SSL_BUILD_CHAIN_FLAG_CHECK 0x4 +/* Ignore verification errors */ +# define SSL_BUILD_CHAIN_FLAG_IGNORE_ERROR 0x8 +/* Clear verification errors from queue */ +# define SSL_BUILD_CHAIN_FLAG_CLEAR_ERROR 0x10 + +/* Flags returned by SSL_check_chain */ +/* Certificate can be used with this session */ +# define CERT_PKEY_VALID 0x1 +/* Certificate can also be used for signing */ +# define CERT_PKEY_SIGN 0x2 +/* EE certificate signing algorithm OK */ +# define CERT_PKEY_EE_SIGNATURE 0x10 +/* CA signature algorithms OK */ +# define CERT_PKEY_CA_SIGNATURE 0x20 +/* EE certificate parameters OK */ +# define CERT_PKEY_EE_PARAM 0x40 +/* CA certificate parameters OK */ +# define CERT_PKEY_CA_PARAM 0x80 +/* Signing explicitly allowed as opposed to SHA1 fallback */ +# define CERT_PKEY_EXPLICIT_SIGN 0x100 +/* Client CA issuer names match (always set for server cert) */ +# define CERT_PKEY_ISSUER_NAME 0x200 +/* Cert type matches client types (always set for server cert) */ +# define CERT_PKEY_CERT_TYPE 0x400 +/* Cert chain suitable to Suite B */ +# define CERT_PKEY_SUITEB 0x800 + +# define SSL_CONF_FLAG_CMDLINE 0x1 +# define SSL_CONF_FLAG_FILE 0x2 +# define SSL_CONF_FLAG_CLIENT 0x4 +# define SSL_CONF_FLAG_SERVER 0x8 +# define SSL_CONF_FLAG_SHOW_ERRORS 0x10 +# define SSL_CONF_FLAG_CERTIFICATE 0x20 +# define SSL_CONF_FLAG_REQUIRE_PRIVATE 0x40 +/* Configuration value types */ +# define SSL_CONF_TYPE_UNKNOWN 0x0 +# define SSL_CONF_TYPE_STRING 0x1 +# define SSL_CONF_TYPE_FILE 0x2 +# define SSL_CONF_TYPE_DIR 0x3 +# define SSL_CONF_TYPE_NONE 0x4 + +/* + * Note: SSL[_CTX]_set_{options,mode} use |= op on the previous value, they + * cannot be used to clear bits. + */ + +unsigned long SSL_CTX_get_options(const SSL_CTX *ctx); +unsigned long SSL_get_options(const SSL* s); +unsigned long SSL_CTX_clear_options(SSL_CTX *ctx, unsigned long op); +unsigned long SSL_clear_options(SSL *s, unsigned long op); +unsigned long SSL_CTX_set_options(SSL_CTX *ctx, unsigned long op); +unsigned long SSL_set_options(SSL *s, unsigned long op); + +# define SSL_CTX_set_mode(ctx,op) \ + SSL_CTX_ctrl((ctx),SSL_CTRL_MODE,(op),NULL) +# define SSL_CTX_clear_mode(ctx,op) \ + SSL_CTX_ctrl((ctx),SSL_CTRL_CLEAR_MODE,(op),NULL) +# define SSL_CTX_get_mode(ctx) \ + SSL_CTX_ctrl((ctx),SSL_CTRL_MODE,0,NULL) +# define SSL_clear_mode(ssl,op) \ + SSL_ctrl((ssl),SSL_CTRL_CLEAR_MODE,(op),NULL) +# define SSL_set_mode(ssl,op) \ + SSL_ctrl((ssl),SSL_CTRL_MODE,(op),NULL) +# define SSL_get_mode(ssl) \ + SSL_ctrl((ssl),SSL_CTRL_MODE,0,NULL) +# define SSL_set_mtu(ssl, mtu) \ + SSL_ctrl((ssl),SSL_CTRL_SET_MTU,(mtu),NULL) +# define DTLS_set_link_mtu(ssl, mtu) \ + SSL_ctrl((ssl),DTLS_CTRL_SET_LINK_MTU,(mtu),NULL) +# define DTLS_get_link_min_mtu(ssl) \ + SSL_ctrl((ssl),DTLS_CTRL_GET_LINK_MIN_MTU,0,NULL) + +# define SSL_get_secure_renegotiation_support(ssl) \ + SSL_ctrl((ssl), SSL_CTRL_GET_RI_SUPPORT, 0, NULL) + +# ifndef OPENSSL_NO_HEARTBEATS +# define SSL_heartbeat(ssl) \ + SSL_ctrl((ssl),SSL_CTRL_DTLS_EXT_SEND_HEARTBEAT,0,NULL) +# endif + +# define SSL_CTX_set_cert_flags(ctx,op) \ + SSL_CTX_ctrl((ctx),SSL_CTRL_CERT_FLAGS,(op),NULL) +# define SSL_set_cert_flags(s,op) \ + SSL_ctrl((s),SSL_CTRL_CERT_FLAGS,(op),NULL) +# define SSL_CTX_clear_cert_flags(ctx,op) \ + SSL_CTX_ctrl((ctx),SSL_CTRL_CLEAR_CERT_FLAGS,(op),NULL) +# define SSL_clear_cert_flags(s,op) \ + SSL_ctrl((s),SSL_CTRL_CLEAR_CERT_FLAGS,(op),NULL) + +void SSL_CTX_set_msg_callback(SSL_CTX *ctx, + void (*cb) (int write_p, int version, + int content_type, const void *buf, + size_t len, SSL *ssl, void *arg)); +void SSL_set_msg_callback(SSL *ssl, + void (*cb) (int write_p, int version, + int content_type, const void *buf, + size_t len, SSL *ssl, void *arg)); +# define SSL_CTX_set_msg_callback_arg(ctx, arg) SSL_CTX_ctrl((ctx), SSL_CTRL_SET_MSG_CALLBACK_ARG, 0, (arg)) +# define SSL_set_msg_callback_arg(ssl, arg) SSL_ctrl((ssl), SSL_CTRL_SET_MSG_CALLBACK_ARG, 0, (arg)) + +# define SSL_get_extms_support(s) \ + SSL_ctrl((s),SSL_CTRL_GET_EXTMS_SUPPORT,0,NULL) + +# ifndef OPENSSL_NO_SRP + +/* see tls_srp.c */ +__owur int SSL_SRP_CTX_init(SSL *s); +__owur int SSL_CTX_SRP_CTX_init(SSL_CTX *ctx); +int SSL_SRP_CTX_free(SSL *ctx); +int SSL_CTX_SRP_CTX_free(SSL_CTX *ctx); +__owur int SSL_srp_server_param_with_username(SSL *s, int *ad); +__owur int SRP_Calc_A_param(SSL *s); + +# endif + +/* 100k max cert list */ +# define SSL_MAX_CERT_LIST_DEFAULT 1024*100 + +# define SSL_SESSION_CACHE_MAX_SIZE_DEFAULT (1024*20) + +/* + * This callback type is used inside SSL_CTX, SSL, and in the functions that + * set them. It is used to override the generation of SSL/TLS session IDs in + * a server. Return value should be zero on an error, non-zero to proceed. + * Also, callbacks should themselves check if the id they generate is unique + * otherwise the SSL handshake will fail with an error - callbacks can do + * this using the 'ssl' value they're passed by; + * SSL_has_matching_session_id(ssl, id, *id_len) The length value passed in + * is set at the maximum size the session ID can be. In SSLv3/TLSv1 it is 32 + * bytes. The callback can alter this length to be less if desired. It is + * also an error for the callback to set the size to zero. + */ +typedef int (*GEN_SESSION_CB) (const SSL *ssl, unsigned char *id, + unsigned int *id_len); + +# define SSL_SESS_CACHE_OFF 0x0000 +# define SSL_SESS_CACHE_CLIENT 0x0001 +# define SSL_SESS_CACHE_SERVER 0x0002 +# define SSL_SESS_CACHE_BOTH (SSL_SESS_CACHE_CLIENT|SSL_SESS_CACHE_SERVER) +# define SSL_SESS_CACHE_NO_AUTO_CLEAR 0x0080 +/* enough comments already ... see SSL_CTX_set_session_cache_mode(3) */ +# define SSL_SESS_CACHE_NO_INTERNAL_LOOKUP 0x0100 +# define SSL_SESS_CACHE_NO_INTERNAL_STORE 0x0200 +# define SSL_SESS_CACHE_NO_INTERNAL \ + (SSL_SESS_CACHE_NO_INTERNAL_LOOKUP|SSL_SESS_CACHE_NO_INTERNAL_STORE) + +LHASH_OF(SSL_SESSION) *SSL_CTX_sessions(SSL_CTX *ctx); +# define SSL_CTX_sess_number(ctx) \ + SSL_CTX_ctrl(ctx,SSL_CTRL_SESS_NUMBER,0,NULL) +# define SSL_CTX_sess_connect(ctx) \ + SSL_CTX_ctrl(ctx,SSL_CTRL_SESS_CONNECT,0,NULL) +# define SSL_CTX_sess_connect_good(ctx) \ + SSL_CTX_ctrl(ctx,SSL_CTRL_SESS_CONNECT_GOOD,0,NULL) +# define SSL_CTX_sess_connect_renegotiate(ctx) \ + SSL_CTX_ctrl(ctx,SSL_CTRL_SESS_CONNECT_RENEGOTIATE,0,NULL) +# define SSL_CTX_sess_accept(ctx) \ + SSL_CTX_ctrl(ctx,SSL_CTRL_SESS_ACCEPT,0,NULL) +# define SSL_CTX_sess_accept_renegotiate(ctx) \ + SSL_CTX_ctrl(ctx,SSL_CTRL_SESS_ACCEPT_RENEGOTIATE,0,NULL) +# define SSL_CTX_sess_accept_good(ctx) \ + SSL_CTX_ctrl(ctx,SSL_CTRL_SESS_ACCEPT_GOOD,0,NULL) +# define SSL_CTX_sess_hits(ctx) \ + SSL_CTX_ctrl(ctx,SSL_CTRL_SESS_HIT,0,NULL) +# define SSL_CTX_sess_cb_hits(ctx) \ + SSL_CTX_ctrl(ctx,SSL_CTRL_SESS_CB_HIT,0,NULL) +# define SSL_CTX_sess_misses(ctx) \ + SSL_CTX_ctrl(ctx,SSL_CTRL_SESS_MISSES,0,NULL) +# define SSL_CTX_sess_timeouts(ctx) \ + SSL_CTX_ctrl(ctx,SSL_CTRL_SESS_TIMEOUTS,0,NULL) +# define SSL_CTX_sess_cache_full(ctx) \ + SSL_CTX_ctrl(ctx,SSL_CTRL_SESS_CACHE_FULL,0,NULL) + +void SSL_CTX_sess_set_new_cb(SSL_CTX *ctx, + int (*new_session_cb) (struct ssl_st *ssl, + SSL_SESSION *sess)); +int (*SSL_CTX_sess_get_new_cb(SSL_CTX *ctx)) (struct ssl_st *ssl, + SSL_SESSION *sess); +void SSL_CTX_sess_set_remove_cb(SSL_CTX *ctx, + void (*remove_session_cb) (struct ssl_ctx_st + *ctx, + SSL_SESSION + *sess)); +void (*SSL_CTX_sess_get_remove_cb(SSL_CTX *ctx)) (struct ssl_ctx_st *ctx, + SSL_SESSION *sess); +void SSL_CTX_sess_set_get_cb(SSL_CTX *ctx, + SSL_SESSION *(*get_session_cb) (struct ssl_st + *ssl, + const unsigned char + *data, int len, + int *copy)); +SSL_SESSION *(*SSL_CTX_sess_get_get_cb(SSL_CTX *ctx)) (struct ssl_st *ssl, + const unsigned char *data, + int len, int *copy); +void SSL_CTX_set_info_callback(SSL_CTX *ctx, + void (*cb) (const SSL *ssl, int type, + int val)); +void (*SSL_CTX_get_info_callback(SSL_CTX *ctx)) (const SSL *ssl, int type, + int val); +void SSL_CTX_set_client_cert_cb(SSL_CTX *ctx, + int (*client_cert_cb) (SSL *ssl, X509 **x509, + EVP_PKEY **pkey)); +int (*SSL_CTX_get_client_cert_cb(SSL_CTX *ctx)) (SSL *ssl, X509 **x509, + EVP_PKEY **pkey); +# ifndef OPENSSL_NO_ENGINE +__owur int SSL_CTX_set_client_cert_engine(SSL_CTX *ctx, ENGINE *e); +# endif +void SSL_CTX_set_cookie_generate_cb(SSL_CTX *ctx, + int (*app_gen_cookie_cb) (SSL *ssl, + unsigned char + *cookie, + unsigned int + *cookie_len)); +void SSL_CTX_set_cookie_verify_cb(SSL_CTX *ctx, + int (*app_verify_cookie_cb) (SSL *ssl, + const unsigned char + *cookie, + unsigned int + cookie_len)); +# ifndef OPENSSL_NO_NEXTPROTONEG +void SSL_CTX_set_next_protos_advertised_cb(SSL_CTX *s, + int (*cb) (SSL *ssl, + const unsigned char + **out, + unsigned int *outlen, + void *arg), void *arg); +void SSL_CTX_set_next_proto_select_cb(SSL_CTX *s, + int (*cb) (SSL *ssl, + unsigned char **out, + unsigned char *outlen, + const unsigned char *in, + unsigned int inlen, + void *arg), void *arg); +void SSL_get0_next_proto_negotiated(const SSL *s, const unsigned char **data, + unsigned *len); +# endif + +__owur int SSL_select_next_proto(unsigned char **out, unsigned char *outlen, + const unsigned char *in, unsigned int inlen, + const unsigned char *client, + unsigned int client_len); + +# define OPENSSL_NPN_UNSUPPORTED 0 +# define OPENSSL_NPN_NEGOTIATED 1 +# define OPENSSL_NPN_NO_OVERLAP 2 + +__owur int SSL_CTX_set_alpn_protos(SSL_CTX *ctx, const unsigned char *protos, + unsigned int protos_len); +__owur int SSL_set_alpn_protos(SSL *ssl, const unsigned char *protos, + unsigned int protos_len); +void SSL_CTX_set_alpn_select_cb(SSL_CTX *ctx, + int (*cb) (SSL *ssl, + const unsigned char **out, + unsigned char *outlen, + const unsigned char *in, + unsigned int inlen, + void *arg), void *arg); +void SSL_get0_alpn_selected(const SSL *ssl, const unsigned char **data, + unsigned int *len); + +# ifndef OPENSSL_NO_PSK +/* + * the maximum length of the buffer given to callbacks containing the + * resulting identity/psk + */ +# define PSK_MAX_IDENTITY_LEN 128 +# define PSK_MAX_PSK_LEN 256 +void SSL_CTX_set_psk_client_callback(SSL_CTX *ctx, + unsigned int (*psk_client_callback) (SSL + *ssl, + const + char + *hint, + char + *identity, + unsigned + int + max_identity_len, + unsigned + char + *psk, + unsigned + int + max_psk_len)); +void SSL_set_psk_client_callback(SSL *ssl, + unsigned int (*psk_client_callback) (SSL + *ssl, + const + char + *hint, + char + *identity, + unsigned + int + max_identity_len, + unsigned + char + *psk, + unsigned + int + max_psk_len)); +void SSL_CTX_set_psk_server_callback(SSL_CTX *ctx, + unsigned int (*psk_server_callback) (SSL + *ssl, + const + char + *identity, + unsigned + char + *psk, + unsigned + int + max_psk_len)); +void SSL_set_psk_server_callback(SSL *ssl, + unsigned int (*psk_server_callback) (SSL + *ssl, + const + char + *identity, + unsigned + char + *psk, + unsigned + int + max_psk_len)); +__owur int SSL_CTX_use_psk_identity_hint(SSL_CTX *ctx, const char *identity_hint); +__owur int SSL_use_psk_identity_hint(SSL *s, const char *identity_hint); +const char *SSL_get_psk_identity_hint(const SSL *s); +const char *SSL_get_psk_identity(const SSL *s); +# endif + +/* Register callbacks to handle custom TLS Extensions for client or server. */ + +__owur int SSL_CTX_has_client_custom_ext(const SSL_CTX *ctx, + unsigned int ext_type); + +__owur int SSL_CTX_add_client_custom_ext(SSL_CTX *ctx, unsigned int ext_type, + custom_ext_add_cb add_cb, + custom_ext_free_cb free_cb, + void *add_arg, + custom_ext_parse_cb parse_cb, + void *parse_arg); + +__owur int SSL_CTX_add_server_custom_ext(SSL_CTX *ctx, unsigned int ext_type, + custom_ext_add_cb add_cb, + custom_ext_free_cb free_cb, + void *add_arg, + custom_ext_parse_cb parse_cb, + void *parse_arg); + +__owur int SSL_extension_supported(unsigned int ext_type); + +# define SSL_NOTHING 1 +# define SSL_WRITING 2 +# define SSL_READING 3 +# define SSL_X509_LOOKUP 4 +# define SSL_ASYNC_PAUSED 5 +# define SSL_ASYNC_NO_JOBS 6 + +/* These will only be used when doing non-blocking IO */ +# define SSL_want_nothing(s) (SSL_want(s) == SSL_NOTHING) +# define SSL_want_read(s) (SSL_want(s) == SSL_READING) +# define SSL_want_write(s) (SSL_want(s) == SSL_WRITING) +# define SSL_want_x509_lookup(s) (SSL_want(s) == SSL_X509_LOOKUP) +# define SSL_want_async(s) (SSL_want(s) == SSL_ASYNC_PAUSED) +# define SSL_want_async_job(s) (SSL_want(s) == SSL_ASYNC_NO_JOBS) + +# define SSL_MAC_FLAG_READ_MAC_STREAM 1 +# define SSL_MAC_FLAG_WRITE_MAC_STREAM 2 + +#ifdef __cplusplus +} +#endif + +# include +# include +# include /* This is mostly sslv3 with a few tweaks */ +# include /* Datagram TLS */ +# include /* Support for the use_srtp extension */ + +#ifdef __cplusplus +extern "C" { +#endif + +/* + * These need to be after the above set of includes due to a compiler bug + * in VisualStudio 2015 + */ +DEFINE_STACK_OF_CONST(SSL_CIPHER) +DEFINE_STACK_OF(SSL_COMP) + +/* compatibility */ +# define SSL_set_app_data(s,arg) (SSL_set_ex_data(s,0,(char *)arg)) +# define SSL_get_app_data(s) (SSL_get_ex_data(s,0)) +# define SSL_SESSION_set_app_data(s,a) (SSL_SESSION_set_ex_data(s,0,(char *)a)) +# define SSL_SESSION_get_app_data(s) (SSL_SESSION_get_ex_data(s,0)) +# define SSL_CTX_get_app_data(ctx) (SSL_CTX_get_ex_data(ctx,0)) +# define SSL_CTX_set_app_data(ctx,arg) (SSL_CTX_set_ex_data(ctx,0,(char *)arg)) +DEPRECATEDIN_1_1_0(void SSL_set_debug(SSL *s, int debug)) + + +/* + * The valid handshake states (one for each type message sent and one for each + * type of message received). There are also two "special" states: + * TLS = TLS or DTLS state + * DTLS = DTLS specific state + * CR/SR = Client Read/Server Read + * CW/SW = Client Write/Server Write + * + * The "special" states are: + * TLS_ST_BEFORE = No handshake has been initiated yet + * TLS_ST_OK = A handshake has been successfully completed + */ +typedef enum { + TLS_ST_BEFORE, + TLS_ST_OK, + DTLS_ST_CR_HELLO_VERIFY_REQUEST, + TLS_ST_CR_SRVR_HELLO, + TLS_ST_CR_CERT, + TLS_ST_CR_CERT_STATUS, + TLS_ST_CR_KEY_EXCH, + TLS_ST_CR_CERT_REQ, + TLS_ST_CR_SRVR_DONE, + TLS_ST_CR_SESSION_TICKET, + TLS_ST_CR_CHANGE, + TLS_ST_CR_FINISHED, + TLS_ST_CW_CLNT_HELLO, + TLS_ST_CW_CERT, + TLS_ST_CW_KEY_EXCH, + TLS_ST_CW_CERT_VRFY, + TLS_ST_CW_CHANGE, + TLS_ST_CW_NEXT_PROTO, + TLS_ST_CW_FINISHED, + TLS_ST_SW_HELLO_REQ, + TLS_ST_SR_CLNT_HELLO, + DTLS_ST_SW_HELLO_VERIFY_REQUEST, + TLS_ST_SW_SRVR_HELLO, + TLS_ST_SW_CERT, + TLS_ST_SW_KEY_EXCH, + TLS_ST_SW_CERT_REQ, + TLS_ST_SW_SRVR_DONE, + TLS_ST_SR_CERT, + TLS_ST_SR_KEY_EXCH, + TLS_ST_SR_CERT_VRFY, + TLS_ST_SR_NEXT_PROTO, + TLS_ST_SR_CHANGE, + TLS_ST_SR_FINISHED, + TLS_ST_SW_SESSION_TICKET, + TLS_ST_SW_CERT_STATUS, + TLS_ST_SW_CHANGE, + TLS_ST_SW_FINISHED +} OSSL_HANDSHAKE_STATE; + +/* + * Most of the following state values are no longer used and are defined to be + * the closest equivalent value in the current state machine code. Not all + * defines have an equivalent and are set to a dummy value (-1). SSL_ST_CONNECT + * and SSL_ST_ACCEPT are still in use in the definition of SSL_CB_ACCEPT_LOOP, + * SSL_CB_ACCEPT_EXIT, SSL_CB_CONNECT_LOOP and SSL_CB_CONNECT_EXIT. + */ + +# define SSL_ST_CONNECT 0x1000 +# define SSL_ST_ACCEPT 0x2000 + +# define SSL_ST_MASK 0x0FFF + +# define SSL_CB_LOOP 0x01 +# define SSL_CB_EXIT 0x02 +# define SSL_CB_READ 0x04 +# define SSL_CB_WRITE 0x08 +# define SSL_CB_ALERT 0x4000/* used in callback */ +# define SSL_CB_READ_ALERT (SSL_CB_ALERT|SSL_CB_READ) +# define SSL_CB_WRITE_ALERT (SSL_CB_ALERT|SSL_CB_WRITE) +# define SSL_CB_ACCEPT_LOOP (SSL_ST_ACCEPT|SSL_CB_LOOP) +# define SSL_CB_ACCEPT_EXIT (SSL_ST_ACCEPT|SSL_CB_EXIT) +# define SSL_CB_CONNECT_LOOP (SSL_ST_CONNECT|SSL_CB_LOOP) +# define SSL_CB_CONNECT_EXIT (SSL_ST_CONNECT|SSL_CB_EXIT) +# define SSL_CB_HANDSHAKE_START 0x10 +# define SSL_CB_HANDSHAKE_DONE 0x20 + +/* Is the SSL_connection established? */ +# define SSL_in_connect_init(a) (SSL_in_init(a) && !SSL_is_server(a)) +# define SSL_in_accept_init(a) (SSL_in_init(a) && SSL_is_server(a)) +int SSL_in_init(SSL *s); +int SSL_in_before(SSL *s); +int SSL_is_init_finished(SSL *s); + +/* + * The following 3 states are kept in ssl->rlayer.rstate when reads fail, you + * should not need these + */ +# define SSL_ST_READ_HEADER 0xF0 +# define SSL_ST_READ_BODY 0xF1 +# define SSL_ST_READ_DONE 0xF2 + +/*- + * Obtain latest Finished message + * -- that we sent (SSL_get_finished) + * -- that we expected from peer (SSL_get_peer_finished). + * Returns length (0 == no Finished so far), copies up to 'count' bytes. + */ +size_t SSL_get_finished(const SSL *s, void *buf, size_t count); +size_t SSL_get_peer_finished(const SSL *s, void *buf, size_t count); + +/* + * use either SSL_VERIFY_NONE or SSL_VERIFY_PEER, the last 2 options are + * 'ored' with SSL_VERIFY_PEER if they are desired + */ +# define SSL_VERIFY_NONE 0x00 +# define SSL_VERIFY_PEER 0x01 +# define SSL_VERIFY_FAIL_IF_NO_PEER_CERT 0x02 +# define SSL_VERIFY_CLIENT_ONCE 0x04 + +# define OpenSSL_add_ssl_algorithms() SSL_library_init() +# if OPENSSL_API_COMPAT < 0x10100000L +# define SSLeay_add_ssl_algorithms() SSL_library_init() +# endif + +/* More backward compatibility */ +# define SSL_get_cipher(s) \ + SSL_CIPHER_get_name(SSL_get_current_cipher(s)) +# define SSL_get_cipher_bits(s,np) \ + SSL_CIPHER_get_bits(SSL_get_current_cipher(s),np) +# define SSL_get_cipher_version(s) \ + SSL_CIPHER_get_version(SSL_get_current_cipher(s)) +# define SSL_get_cipher_name(s) \ + SSL_CIPHER_get_name(SSL_get_current_cipher(s)) +# define SSL_get_time(a) SSL_SESSION_get_time(a) +# define SSL_set_time(a,b) SSL_SESSION_set_time((a),(b)) +# define SSL_get_timeout(a) SSL_SESSION_get_timeout(a) +# define SSL_set_timeout(a,b) SSL_SESSION_set_timeout((a),(b)) + +# define d2i_SSL_SESSION_bio(bp,s_id) ASN1_d2i_bio_of(SSL_SESSION,SSL_SESSION_new,d2i_SSL_SESSION,bp,s_id) +# define i2d_SSL_SESSION_bio(bp,s_id) ASN1_i2d_bio_of(SSL_SESSION,i2d_SSL_SESSION,bp,s_id) + +DECLARE_PEM_rw(SSL_SESSION, SSL_SESSION) +# define SSL_AD_REASON_OFFSET 1000/* offset to get SSL_R_... value + * from SSL_AD_... */ +/* These alert types are for SSLv3 and TLSv1 */ +# define SSL_AD_CLOSE_NOTIFY SSL3_AD_CLOSE_NOTIFY +/* fatal */ +# define SSL_AD_UNEXPECTED_MESSAGE SSL3_AD_UNEXPECTED_MESSAGE +/* fatal */ +# define SSL_AD_BAD_RECORD_MAC SSL3_AD_BAD_RECORD_MAC +# define SSL_AD_DECRYPTION_FAILED TLS1_AD_DECRYPTION_FAILED +# define SSL_AD_RECORD_OVERFLOW TLS1_AD_RECORD_OVERFLOW +/* fatal */ +# define SSL_AD_DECOMPRESSION_FAILURE SSL3_AD_DECOMPRESSION_FAILURE +/* fatal */ +# define SSL_AD_HANDSHAKE_FAILURE SSL3_AD_HANDSHAKE_FAILURE +/* Not for TLS */ +# define SSL_AD_NO_CERTIFICATE SSL3_AD_NO_CERTIFICATE +# define SSL_AD_BAD_CERTIFICATE SSL3_AD_BAD_CERTIFICATE +# define SSL_AD_UNSUPPORTED_CERTIFICATE SSL3_AD_UNSUPPORTED_CERTIFICATE +# define SSL_AD_CERTIFICATE_REVOKED SSL3_AD_CERTIFICATE_REVOKED +# define SSL_AD_CERTIFICATE_EXPIRED SSL3_AD_CERTIFICATE_EXPIRED +# define SSL_AD_CERTIFICATE_UNKNOWN SSL3_AD_CERTIFICATE_UNKNOWN +/* fatal */ +# define SSL_AD_ILLEGAL_PARAMETER SSL3_AD_ILLEGAL_PARAMETER +/* fatal */ +# define SSL_AD_UNKNOWN_CA TLS1_AD_UNKNOWN_CA +/* fatal */ +# define SSL_AD_ACCESS_DENIED TLS1_AD_ACCESS_DENIED +/* fatal */ +# define SSL_AD_DECODE_ERROR TLS1_AD_DECODE_ERROR +# define SSL_AD_DECRYPT_ERROR TLS1_AD_DECRYPT_ERROR +/* fatal */ +# define SSL_AD_EXPORT_RESTRICTION TLS1_AD_EXPORT_RESTRICTION +/* fatal */ +# define SSL_AD_PROTOCOL_VERSION TLS1_AD_PROTOCOL_VERSION +/* fatal */ +# define SSL_AD_INSUFFICIENT_SECURITY TLS1_AD_INSUFFICIENT_SECURITY +/* fatal */ +# define SSL_AD_INTERNAL_ERROR TLS1_AD_INTERNAL_ERROR +# define SSL_AD_USER_CANCELLED TLS1_AD_USER_CANCELLED +# define SSL_AD_NO_RENEGOTIATION TLS1_AD_NO_RENEGOTIATION +# define SSL_AD_UNSUPPORTED_EXTENSION TLS1_AD_UNSUPPORTED_EXTENSION +# define SSL_AD_CERTIFICATE_UNOBTAINABLE TLS1_AD_CERTIFICATE_UNOBTAINABLE +# define SSL_AD_UNRECOGNIZED_NAME TLS1_AD_UNRECOGNIZED_NAME +# define SSL_AD_BAD_CERTIFICATE_STATUS_RESPONSE TLS1_AD_BAD_CERTIFICATE_STATUS_RESPONSE +# define SSL_AD_BAD_CERTIFICATE_HASH_VALUE TLS1_AD_BAD_CERTIFICATE_HASH_VALUE +/* fatal */ +# define SSL_AD_UNKNOWN_PSK_IDENTITY TLS1_AD_UNKNOWN_PSK_IDENTITY +/* fatal */ +# define SSL_AD_INAPPROPRIATE_FALLBACK TLS1_AD_INAPPROPRIATE_FALLBACK +# define SSL_AD_NO_APPLICATION_PROTOCOL TLS1_AD_NO_APPLICATION_PROTOCOL +# define SSL_ERROR_NONE 0 +# define SSL_ERROR_SSL 1 +# define SSL_ERROR_WANT_READ 2 +# define SSL_ERROR_WANT_WRITE 3 +# define SSL_ERROR_WANT_X509_LOOKUP 4 +# define SSL_ERROR_SYSCALL 5/* look at error stack/return + * value/errno */ +# define SSL_ERROR_ZERO_RETURN 6 +# define SSL_ERROR_WANT_CONNECT 7 +# define SSL_ERROR_WANT_ACCEPT 8 +# define SSL_ERROR_WANT_ASYNC 9 +# define SSL_ERROR_WANT_ASYNC_JOB 10 +# define SSL_CTRL_SET_TMP_DH 3 +# define SSL_CTRL_SET_TMP_ECDH 4 +# define SSL_CTRL_SET_TMP_DH_CB 6 +# define SSL_CTRL_GET_CLIENT_CERT_REQUEST 9 +# define SSL_CTRL_GET_NUM_RENEGOTIATIONS 10 +# define SSL_CTRL_CLEAR_NUM_RENEGOTIATIONS 11 +# define SSL_CTRL_GET_TOTAL_RENEGOTIATIONS 12 +# define SSL_CTRL_GET_FLAGS 13 +# define SSL_CTRL_EXTRA_CHAIN_CERT 14 +# define SSL_CTRL_SET_MSG_CALLBACK 15 +# define SSL_CTRL_SET_MSG_CALLBACK_ARG 16 +/* only applies to datagram connections */ +# define SSL_CTRL_SET_MTU 17 +/* Stats */ +# define SSL_CTRL_SESS_NUMBER 20 +# define SSL_CTRL_SESS_CONNECT 21 +# define SSL_CTRL_SESS_CONNECT_GOOD 22 +# define SSL_CTRL_SESS_CONNECT_RENEGOTIATE 23 +# define SSL_CTRL_SESS_ACCEPT 24 +# define SSL_CTRL_SESS_ACCEPT_GOOD 25 +# define SSL_CTRL_SESS_ACCEPT_RENEGOTIATE 26 +# define SSL_CTRL_SESS_HIT 27 +# define SSL_CTRL_SESS_CB_HIT 28 +# define SSL_CTRL_SESS_MISSES 29 +# define SSL_CTRL_SESS_TIMEOUTS 30 +# define SSL_CTRL_SESS_CACHE_FULL 31 +# define SSL_CTRL_MODE 33 +# define SSL_CTRL_GET_READ_AHEAD 40 +# define SSL_CTRL_SET_READ_AHEAD 41 +# define SSL_CTRL_SET_SESS_CACHE_SIZE 42 +# define SSL_CTRL_GET_SESS_CACHE_SIZE 43 +# define SSL_CTRL_SET_SESS_CACHE_MODE 44 +# define SSL_CTRL_GET_SESS_CACHE_MODE 45 +# define SSL_CTRL_GET_MAX_CERT_LIST 50 +# define SSL_CTRL_SET_MAX_CERT_LIST 51 +# define SSL_CTRL_SET_MAX_SEND_FRAGMENT 52 +/* see tls1.h for macros based on these */ +# define SSL_CTRL_SET_TLSEXT_SERVERNAME_CB 53 +# define SSL_CTRL_SET_TLSEXT_SERVERNAME_ARG 54 +# define SSL_CTRL_SET_TLSEXT_HOSTNAME 55 +# define SSL_CTRL_SET_TLSEXT_DEBUG_CB 56 +# define SSL_CTRL_SET_TLSEXT_DEBUG_ARG 57 +# define SSL_CTRL_GET_TLSEXT_TICKET_KEYS 58 +# define SSL_CTRL_SET_TLSEXT_TICKET_KEYS 59 +/*# define SSL_CTRL_SET_TLSEXT_OPAQUE_PRF_INPUT 60 */ +/*# define SSL_CTRL_SET_TLSEXT_OPAQUE_PRF_INPUT_CB 61 */ +/*# define SSL_CTRL_SET_TLSEXT_OPAQUE_PRF_INPUT_CB_ARG 62 */ +# define SSL_CTRL_SET_TLSEXT_STATUS_REQ_CB 63 +# define SSL_CTRL_SET_TLSEXT_STATUS_REQ_CB_ARG 64 +# define SSL_CTRL_SET_TLSEXT_STATUS_REQ_TYPE 65 +# define SSL_CTRL_GET_TLSEXT_STATUS_REQ_EXTS 66 +# define SSL_CTRL_SET_TLSEXT_STATUS_REQ_EXTS 67 +# define SSL_CTRL_GET_TLSEXT_STATUS_REQ_IDS 68 +# define SSL_CTRL_SET_TLSEXT_STATUS_REQ_IDS 69 +# define SSL_CTRL_GET_TLSEXT_STATUS_REQ_OCSP_RESP 70 +# define SSL_CTRL_SET_TLSEXT_STATUS_REQ_OCSP_RESP 71 +# define SSL_CTRL_SET_TLSEXT_TICKET_KEY_CB 72 +# define SSL_CTRL_SET_TLS_EXT_SRP_USERNAME_CB 75 +# define SSL_CTRL_SET_SRP_VERIFY_PARAM_CB 76 +# define SSL_CTRL_SET_SRP_GIVE_CLIENT_PWD_CB 77 +# define SSL_CTRL_SET_SRP_ARG 78 +# define SSL_CTRL_SET_TLS_EXT_SRP_USERNAME 79 +# define SSL_CTRL_SET_TLS_EXT_SRP_STRENGTH 80 +# define SSL_CTRL_SET_TLS_EXT_SRP_PASSWORD 81 +# ifndef OPENSSL_NO_HEARTBEATS +# define SSL_CTRL_DTLS_EXT_SEND_HEARTBEAT 85 +# define SSL_CTRL_GET_DTLS_EXT_HEARTBEAT_PENDING 86 +# define SSL_CTRL_SET_DTLS_EXT_HEARTBEAT_NO_REQUESTS 87 +# endif +# define DTLS_CTRL_GET_TIMEOUT 73 +# define DTLS_CTRL_HANDLE_TIMEOUT 74 +# define SSL_CTRL_GET_RI_SUPPORT 76 +# define SSL_CTRL_CLEAR_MODE 78 +# define SSL_CTRL_SET_NOT_RESUMABLE_SESS_CB 79 +# define SSL_CTRL_GET_EXTRA_CHAIN_CERTS 82 +# define SSL_CTRL_CLEAR_EXTRA_CHAIN_CERTS 83 +# define SSL_CTRL_CHAIN 88 +# define SSL_CTRL_CHAIN_CERT 89 +# define SSL_CTRL_GET_CURVES 90 +# define SSL_CTRL_SET_CURVES 91 +# define SSL_CTRL_SET_CURVES_LIST 92 +# define SSL_CTRL_GET_SHARED_CURVE 93 +# define SSL_CTRL_SET_SIGALGS 97 +# define SSL_CTRL_SET_SIGALGS_LIST 98 +# define SSL_CTRL_CERT_FLAGS 99 +# define SSL_CTRL_CLEAR_CERT_FLAGS 100 +# define SSL_CTRL_SET_CLIENT_SIGALGS 101 +# define SSL_CTRL_SET_CLIENT_SIGALGS_LIST 102 +# define SSL_CTRL_GET_CLIENT_CERT_TYPES 103 +# define SSL_CTRL_SET_CLIENT_CERT_TYPES 104 +# define SSL_CTRL_BUILD_CERT_CHAIN 105 +# define SSL_CTRL_SET_VERIFY_CERT_STORE 106 +# define SSL_CTRL_SET_CHAIN_CERT_STORE 107 +# define SSL_CTRL_GET_PEER_SIGNATURE_NID 108 +# define SSL_CTRL_GET_SERVER_TMP_KEY 109 +# define SSL_CTRL_GET_RAW_CIPHERLIST 110 +# define SSL_CTRL_GET_EC_POINT_FORMATS 111 +# define SSL_CTRL_GET_CHAIN_CERTS 115 +# define SSL_CTRL_SELECT_CURRENT_CERT 116 +# define SSL_CTRL_SET_CURRENT_CERT 117 +# define SSL_CTRL_SET_DH_AUTO 118 +# define DTLS_CTRL_SET_LINK_MTU 120 +# define DTLS_CTRL_GET_LINK_MIN_MTU 121 +# define SSL_CTRL_GET_EXTMS_SUPPORT 122 +# define SSL_CTRL_SET_MIN_PROTO_VERSION 123 +# define SSL_CTRL_SET_MAX_PROTO_VERSION 124 +# define SSL_CTRL_SET_SPLIT_SEND_FRAGMENT 125 +# define SSL_CTRL_SET_MAX_PIPELINES 126 +# define SSL_CTRL_GET_TLSEXT_STATUS_REQ_TYPE 127 +# define SSL_CTRL_GET_TLSEXT_STATUS_REQ_CB 128 +# define SSL_CTRL_GET_TLSEXT_STATUS_REQ_CB_ARG 129 +# define SSL_CERT_SET_FIRST 1 +# define SSL_CERT_SET_NEXT 2 +# define SSL_CERT_SET_SERVER 3 +# define DTLSv1_get_timeout(ssl, arg) \ + SSL_ctrl(ssl,DTLS_CTRL_GET_TIMEOUT,0, (void *)arg) +# define DTLSv1_handle_timeout(ssl) \ + SSL_ctrl(ssl,DTLS_CTRL_HANDLE_TIMEOUT,0, NULL) +# define SSL_num_renegotiations(ssl) \ + SSL_ctrl((ssl),SSL_CTRL_GET_NUM_RENEGOTIATIONS,0,NULL) +# define SSL_clear_num_renegotiations(ssl) \ + SSL_ctrl((ssl),SSL_CTRL_CLEAR_NUM_RENEGOTIATIONS,0,NULL) +# define SSL_total_renegotiations(ssl) \ + SSL_ctrl((ssl),SSL_CTRL_GET_TOTAL_RENEGOTIATIONS,0,NULL) +# define SSL_CTX_set_tmp_dh(ctx,dh) \ + SSL_CTX_ctrl(ctx,SSL_CTRL_SET_TMP_DH,0,(char *)dh) +# define SSL_CTX_set_tmp_ecdh(ctx,ecdh) \ + SSL_CTX_ctrl(ctx,SSL_CTRL_SET_TMP_ECDH,0,(char *)ecdh) +# define SSL_CTX_set_dh_auto(ctx, onoff) \ + SSL_CTX_ctrl(ctx,SSL_CTRL_SET_DH_AUTO,onoff,NULL) +# define SSL_set_dh_auto(s, onoff) \ + SSL_ctrl(s,SSL_CTRL_SET_DH_AUTO,onoff,NULL) +# define SSL_set_tmp_dh(ssl,dh) \ + SSL_ctrl(ssl,SSL_CTRL_SET_TMP_DH,0,(char *)dh) +# define SSL_set_tmp_ecdh(ssl,ecdh) \ + SSL_ctrl(ssl,SSL_CTRL_SET_TMP_ECDH,0,(char *)ecdh) +# define SSL_CTX_add_extra_chain_cert(ctx,x509) \ + SSL_CTX_ctrl(ctx,SSL_CTRL_EXTRA_CHAIN_CERT,0,(char *)x509) +# define SSL_CTX_get_extra_chain_certs(ctx,px509) \ + SSL_CTX_ctrl(ctx,SSL_CTRL_GET_EXTRA_CHAIN_CERTS,0,px509) +# define SSL_CTX_get_extra_chain_certs_only(ctx,px509) \ + SSL_CTX_ctrl(ctx,SSL_CTRL_GET_EXTRA_CHAIN_CERTS,1,px509) +# define SSL_CTX_clear_extra_chain_certs(ctx) \ + SSL_CTX_ctrl(ctx,SSL_CTRL_CLEAR_EXTRA_CHAIN_CERTS,0,NULL) +# define SSL_CTX_set0_chain(ctx,sk) \ + SSL_CTX_ctrl(ctx,SSL_CTRL_CHAIN,0,(char *)sk) +# define SSL_CTX_set1_chain(ctx,sk) \ + SSL_CTX_ctrl(ctx,SSL_CTRL_CHAIN,1,(char *)sk) +# define SSL_CTX_add0_chain_cert(ctx,x509) \ + SSL_CTX_ctrl(ctx,SSL_CTRL_CHAIN_CERT,0,(char *)x509) +# define SSL_CTX_add1_chain_cert(ctx,x509) \ + SSL_CTX_ctrl(ctx,SSL_CTRL_CHAIN_CERT,1,(char *)x509) +# define SSL_CTX_get0_chain_certs(ctx,px509) \ + SSL_CTX_ctrl(ctx,SSL_CTRL_GET_CHAIN_CERTS,0,px509) +# define SSL_CTX_clear_chain_certs(ctx) \ + SSL_CTX_set0_chain(ctx,NULL) +# define SSL_CTX_build_cert_chain(ctx, flags) \ + SSL_CTX_ctrl(ctx,SSL_CTRL_BUILD_CERT_CHAIN, flags, NULL) +# define SSL_CTX_select_current_cert(ctx,x509) \ + SSL_CTX_ctrl(ctx,SSL_CTRL_SELECT_CURRENT_CERT,0,(char *)x509) +# define SSL_CTX_set_current_cert(ctx, op) \ + SSL_CTX_ctrl(ctx,SSL_CTRL_SET_CURRENT_CERT, op, NULL) +# define SSL_CTX_set0_verify_cert_store(ctx,st) \ + SSL_CTX_ctrl(ctx,SSL_CTRL_SET_VERIFY_CERT_STORE,0,(char *)st) +# define SSL_CTX_set1_verify_cert_store(ctx,st) \ + SSL_CTX_ctrl(ctx,SSL_CTRL_SET_VERIFY_CERT_STORE,1,(char *)st) +# define SSL_CTX_set0_chain_cert_store(ctx,st) \ + SSL_CTX_ctrl(ctx,SSL_CTRL_SET_CHAIN_CERT_STORE,0,(char *)st) +# define SSL_CTX_set1_chain_cert_store(ctx,st) \ + SSL_CTX_ctrl(ctx,SSL_CTRL_SET_CHAIN_CERT_STORE,1,(char *)st) +# define SSL_set0_chain(ctx,sk) \ + SSL_ctrl(ctx,SSL_CTRL_CHAIN,0,(char *)sk) +# define SSL_set1_chain(ctx,sk) \ + SSL_ctrl(ctx,SSL_CTRL_CHAIN,1,(char *)sk) +# define SSL_add0_chain_cert(ctx,x509) \ + SSL_ctrl(ctx,SSL_CTRL_CHAIN_CERT,0,(char *)x509) +# define SSL_add1_chain_cert(ctx,x509) \ + SSL_ctrl(ctx,SSL_CTRL_CHAIN_CERT,1,(char *)x509) +# define SSL_get0_chain_certs(ctx,px509) \ + SSL_ctrl(ctx,SSL_CTRL_GET_CHAIN_CERTS,0,px509) +# define SSL_clear_chain_certs(ctx) \ + SSL_set0_chain(ctx,NULL) +# define SSL_build_cert_chain(s, flags) \ + SSL_ctrl(s,SSL_CTRL_BUILD_CERT_CHAIN, flags, NULL) +# define SSL_select_current_cert(ctx,x509) \ + SSL_ctrl(ctx,SSL_CTRL_SELECT_CURRENT_CERT,0,(char *)x509) +# define SSL_set_current_cert(ctx,op) \ + SSL_ctrl(ctx,SSL_CTRL_SET_CURRENT_CERT, op, NULL) +# define SSL_set0_verify_cert_store(s,st) \ + SSL_ctrl(s,SSL_CTRL_SET_VERIFY_CERT_STORE,0,(char *)st) +# define SSL_set1_verify_cert_store(s,st) \ + SSL_ctrl(s,SSL_CTRL_SET_VERIFY_CERT_STORE,1,(char *)st) +# define SSL_set0_chain_cert_store(s,st) \ + SSL_ctrl(s,SSL_CTRL_SET_CHAIN_CERT_STORE,0,(char *)st) +# define SSL_set1_chain_cert_store(s,st) \ + SSL_ctrl(s,SSL_CTRL_SET_CHAIN_CERT_STORE,1,(char *)st) +# define SSL_get1_curves(ctx, s) \ + SSL_ctrl(ctx,SSL_CTRL_GET_CURVES,0,(char *)s) +# define SSL_CTX_set1_curves(ctx, clist, clistlen) \ + SSL_CTX_ctrl(ctx,SSL_CTRL_SET_CURVES,clistlen,(char *)clist) +# define SSL_CTX_set1_curves_list(ctx, s) \ + SSL_CTX_ctrl(ctx,SSL_CTRL_SET_CURVES_LIST,0,(char *)s) +# define SSL_set1_curves(ctx, clist, clistlen) \ + SSL_ctrl(ctx,SSL_CTRL_SET_CURVES,clistlen,(char *)clist) +# define SSL_set1_curves_list(ctx, s) \ + SSL_ctrl(ctx,SSL_CTRL_SET_CURVES_LIST,0,(char *)s) +# define SSL_get_shared_curve(s, n) \ + SSL_ctrl(s,SSL_CTRL_GET_SHARED_CURVE,n,NULL) +# define SSL_CTX_set1_sigalgs(ctx, slist, slistlen) \ + SSL_CTX_ctrl(ctx,SSL_CTRL_SET_SIGALGS,slistlen,(int *)slist) +# define SSL_CTX_set1_sigalgs_list(ctx, s) \ + SSL_CTX_ctrl(ctx,SSL_CTRL_SET_SIGALGS_LIST,0,(char *)s) +# define SSL_set1_sigalgs(ctx, slist, slistlen) \ + SSL_ctrl(ctx,SSL_CTRL_SET_SIGALGS,slistlen,(int *)slist) +# define SSL_set1_sigalgs_list(ctx, s) \ + SSL_ctrl(ctx,SSL_CTRL_SET_SIGALGS_LIST,0,(char *)s) +# define SSL_CTX_set1_client_sigalgs(ctx, slist, slistlen) \ + SSL_CTX_ctrl(ctx,SSL_CTRL_SET_CLIENT_SIGALGS,slistlen,(int *)slist) +# define SSL_CTX_set1_client_sigalgs_list(ctx, s) \ + SSL_CTX_ctrl(ctx,SSL_CTRL_SET_CLIENT_SIGALGS_LIST,0,(char *)s) +# define SSL_set1_client_sigalgs(ctx, slist, slistlen) \ + SSL_ctrl(ctx,SSL_CTRL_SET_CLIENT_SIGALGS,clistlen,(int *)slist) +# define SSL_set1_client_sigalgs_list(ctx, s) \ + SSL_ctrl(ctx,SSL_CTRL_SET_CLIENT_SIGALGS_LIST,0,(char *)s) +# define SSL_get0_certificate_types(s, clist) \ + SSL_ctrl(s, SSL_CTRL_GET_CLIENT_CERT_TYPES, 0, (char *)clist) +# define SSL_CTX_set1_client_certificate_types(ctx, clist, clistlen) \ + SSL_CTX_ctrl(ctx,SSL_CTRL_SET_CLIENT_CERT_TYPES,clistlen,(char *)clist) +# define SSL_set1_client_certificate_types(s, clist, clistlen) \ + SSL_ctrl(s,SSL_CTRL_SET_CLIENT_CERT_TYPES,clistlen,(char *)clist) +# define SSL_get_peer_signature_nid(s, pn) \ + SSL_ctrl(s,SSL_CTRL_GET_PEER_SIGNATURE_NID,0,pn) +# define SSL_get_server_tmp_key(s, pk) \ + SSL_ctrl(s,SSL_CTRL_GET_SERVER_TMP_KEY,0,pk) +# define SSL_get0_raw_cipherlist(s, plst) \ + SSL_ctrl(s,SSL_CTRL_GET_RAW_CIPHERLIST,0,plst) +# define SSL_get0_ec_point_formats(s, plst) \ + SSL_ctrl(s,SSL_CTRL_GET_EC_POINT_FORMATS,0,plst) +#define SSL_CTX_set_min_proto_version(ctx, version) \ + SSL_CTX_ctrl(ctx, SSL_CTRL_SET_MIN_PROTO_VERSION, version, NULL) +#define SSL_CTX_set_max_proto_version(ctx, version) \ + SSL_CTX_ctrl(ctx, SSL_CTRL_SET_MAX_PROTO_VERSION, version, NULL) +#define SSL_set_min_proto_version(s, version) \ + SSL_ctrl(s, SSL_CTRL_SET_MIN_PROTO_VERSION, version, NULL) +#define SSL_set_max_proto_version(s, version) \ + SSL_ctrl(s, SSL_CTRL_SET_MAX_PROTO_VERSION, version, NULL) + +#if OPENSSL_API_COMPAT < 0x10100000L +/* Provide some compatibility macros for removed functionality. */ +# define SSL_CTX_need_tmp_RSA(ctx) 0 +# define SSL_CTX_set_tmp_rsa(ctx,rsa) 1 +# define SSL_need_tmp_RSA(ssl) 0 +# define SSL_set_tmp_rsa(ssl,rsa) 1 +# define SSL_CTX_set_ecdh_auto(dummy, onoff) ((onoff) != 0) +# define SSL_set_ecdh_auto(dummy, onoff) ((onoff) != 0) +/* + * We "pretend" to call the callback to avoid warnings about unused static + * functions. + */ +# define SSL_CTX_set_tmp_rsa_callback(ctx, cb) while(0) (cb)(NULL, 0, 0) +# define SSL_set_tmp_rsa_callback(ssl, cb) while(0) (cb)(NULL, 0, 0) +#endif + +__owur const BIO_METHOD *BIO_f_ssl(void); +__owur BIO *BIO_new_ssl(SSL_CTX *ctx, int client); +__owur BIO *BIO_new_ssl_connect(SSL_CTX *ctx); +__owur BIO *BIO_new_buffer_ssl_connect(SSL_CTX *ctx); +__owur int BIO_ssl_copy_session_id(BIO *to, BIO *from); +void BIO_ssl_shutdown(BIO *ssl_bio); + +__owur int SSL_CTX_set_cipher_list(SSL_CTX *, const char *str); +__owur SSL_CTX *SSL_CTX_new(const SSL_METHOD *meth); +int SSL_CTX_up_ref(SSL_CTX *ctx); +void SSL_CTX_free(SSL_CTX *); +__owur long SSL_CTX_set_timeout(SSL_CTX *ctx, long t); +__owur long SSL_CTX_get_timeout(const SSL_CTX *ctx); +__owur X509_STORE *SSL_CTX_get_cert_store(const SSL_CTX *); +void SSL_CTX_set_cert_store(SSL_CTX *, X509_STORE *); +__owur int SSL_want(const SSL *s); +__owur int SSL_clear(SSL *s); + +void SSL_CTX_flush_sessions(SSL_CTX *ctx, long tm); + +__owur const SSL_CIPHER *SSL_get_current_cipher(const SSL *s); +__owur int SSL_CIPHER_get_bits(const SSL_CIPHER *c, int *alg_bits); +__owur const char *SSL_CIPHER_get_version(const SSL_CIPHER *c); +__owur const char *SSL_CIPHER_get_name(const SSL_CIPHER *c); +__owur uint32_t SSL_CIPHER_get_id(const SSL_CIPHER *c); +__owur int SSL_CIPHER_get_kx_nid(const SSL_CIPHER *c); +__owur int SSL_CIPHER_get_auth_nid(const SSL_CIPHER *c); +__owur int SSL_CIPHER_is_aead(const SSL_CIPHER *c); + +__owur int SSL_get_fd(const SSL *s); +__owur int SSL_get_rfd(const SSL *s); +__owur int SSL_get_wfd(const SSL *s); +__owur const char *SSL_get_cipher_list(const SSL *s, int n); +__owur char *SSL_get_shared_ciphers(const SSL *s, char *buf, int len); +__owur int SSL_get_read_ahead(const SSL *s); +__owur int SSL_pending(const SSL *s); +__owur int SSL_has_pending(const SSL *s); +# ifndef OPENSSL_NO_SOCK +__owur int SSL_set_fd(SSL *s, int fd); +__owur int SSL_set_rfd(SSL *s, int fd); +__owur int SSL_set_wfd(SSL *s, int fd); +# endif +void SSL_set0_rbio(SSL *s, BIO *rbio); +void SSL_set0_wbio(SSL *s, BIO *wbio); +void SSL_set_bio(SSL *s, BIO *rbio, BIO *wbio); +__owur BIO *SSL_get_rbio(const SSL *s); +__owur BIO *SSL_get_wbio(const SSL *s); +__owur int SSL_set_cipher_list(SSL *s, const char *str); +void SSL_set_read_ahead(SSL *s, int yes); +__owur int SSL_get_verify_mode(const SSL *s); +__owur int SSL_get_verify_depth(const SSL *s); +__owur SSL_verify_cb SSL_get_verify_callback(const SSL *s); +void SSL_set_verify(SSL *s, int mode, SSL_verify_cb callback); +void SSL_set_verify_depth(SSL *s, int depth); +void SSL_set_cert_cb(SSL *s, int (*cb) (SSL *ssl, void *arg), void *arg); +# ifndef OPENSSL_NO_RSA +__owur int SSL_use_RSAPrivateKey(SSL *ssl, RSA *rsa); +__owur int SSL_use_RSAPrivateKey_ASN1(SSL *ssl, const unsigned char *d, long len); +# endif +__owur int SSL_use_PrivateKey(SSL *ssl, EVP_PKEY *pkey); +__owur int SSL_use_PrivateKey_ASN1(int pk, SSL *ssl, const unsigned char *d, + long len); +__owur int SSL_use_certificate(SSL *ssl, X509 *x); +__owur int SSL_use_certificate_ASN1(SSL *ssl, const unsigned char *d, int len); + +/* Set serverinfo data for the current active cert. */ +__owur int SSL_CTX_use_serverinfo(SSL_CTX *ctx, const unsigned char *serverinfo, + size_t serverinfo_length); +__owur int SSL_CTX_use_serverinfo_file(SSL_CTX *ctx, const char *file); + +#ifndef OPENSSL_NO_RSA +__owur int SSL_use_RSAPrivateKey_file(SSL *ssl, const char *file, int type); +#endif + +__owur int SSL_use_PrivateKey_file(SSL *ssl, const char *file, int type); +__owur int SSL_use_certificate_file(SSL *ssl, const char *file, int type); + +#ifndef OPENSSL_NO_RSA +__owur int SSL_CTX_use_RSAPrivateKey_file(SSL_CTX *ctx, const char *file, int type); +#endif +__owur int SSL_CTX_use_PrivateKey_file(SSL_CTX *ctx, const char *file, int type); +__owur int SSL_CTX_use_certificate_file(SSL_CTX *ctx, const char *file, int type); +/* PEM type */ +__owur int SSL_CTX_use_certificate_chain_file(SSL_CTX *ctx, const char *file); +__owur int SSL_use_certificate_chain_file(SSL *ssl, const char *file); +__owur STACK_OF(X509_NAME) *SSL_load_client_CA_file(const char *file); +__owur int SSL_add_file_cert_subjects_to_stack(STACK_OF(X509_NAME) *stackCAs, + const char *file); +int SSL_add_dir_cert_subjects_to_stack(STACK_OF(X509_NAME) *stackCAs, + const char *dir); + +#if OPENSSL_API_COMPAT < 0x10100000L +# define SSL_load_error_strings() \ + OPENSSL_init_ssl(OPENSSL_INIT_LOAD_SSL_STRINGS \ + | OPENSSL_INIT_LOAD_CRYPTO_STRINGS, NULL) +#endif + +__owur const char *SSL_state_string(const SSL *s); +__owur const char *SSL_rstate_string(const SSL *s); +__owur const char *SSL_state_string_long(const SSL *s); +__owur const char *SSL_rstate_string_long(const SSL *s); +__owur long SSL_SESSION_get_time(const SSL_SESSION *s); +__owur long SSL_SESSION_set_time(SSL_SESSION *s, long t); +__owur long SSL_SESSION_get_timeout(const SSL_SESSION *s); +__owur long SSL_SESSION_set_timeout(SSL_SESSION *s, long t); +__owur int SSL_SESSION_get_protocol_version(const SSL_SESSION *s); +__owur const char *SSL_SESSION_get0_hostname(const SSL_SESSION *s); +__owur const SSL_CIPHER *SSL_SESSION_get0_cipher(const SSL_SESSION *s); +__owur int SSL_SESSION_has_ticket(const SSL_SESSION *s); +__owur unsigned long SSL_SESSION_get_ticket_lifetime_hint(const SSL_SESSION *s); +void SSL_SESSION_get0_ticket(const SSL_SESSION *s, const unsigned char **tick, + size_t *len); +__owur int SSL_copy_session_id(SSL *to, const SSL *from); +__owur X509 *SSL_SESSION_get0_peer(SSL_SESSION *s); +__owur int SSL_SESSION_set1_id_context(SSL_SESSION *s, const unsigned char *sid_ctx, + unsigned int sid_ctx_len); +__owur int SSL_SESSION_set1_id(SSL_SESSION *s, const unsigned char *sid, + unsigned int sid_len); + +__owur SSL_SESSION *SSL_SESSION_new(void); +const unsigned char *SSL_SESSION_get_id(const SSL_SESSION *s, + unsigned int *len); +const unsigned char *SSL_SESSION_get0_id_context(const SSL_SESSION *s, + unsigned int *len); +__owur unsigned int SSL_SESSION_get_compress_id(const SSL_SESSION *s); +# ifndef OPENSSL_NO_STDIO +int SSL_SESSION_print_fp(FILE *fp, const SSL_SESSION *ses); +# endif +int SSL_SESSION_print(BIO *fp, const SSL_SESSION *ses); +int SSL_SESSION_print_keylog(BIO *bp, const SSL_SESSION *x); +int SSL_SESSION_up_ref(SSL_SESSION *ses); +void SSL_SESSION_free(SSL_SESSION *ses); +__owur int i2d_SSL_SESSION(SSL_SESSION *in, unsigned char **pp); +__owur int SSL_set_session(SSL *to, SSL_SESSION *session); +__owur int SSL_CTX_add_session(SSL_CTX *s, SSL_SESSION *c); +int SSL_CTX_remove_session(SSL_CTX *, SSL_SESSION *c); +__owur int SSL_CTX_set_generate_session_id(SSL_CTX *, GEN_SESSION_CB); +__owur int SSL_set_generate_session_id(SSL *, GEN_SESSION_CB); +__owur int SSL_has_matching_session_id(const SSL *ssl, const unsigned char *id, + unsigned int id_len); +SSL_SESSION *d2i_SSL_SESSION(SSL_SESSION **a, const unsigned char **pp, + long length); + +# ifdef HEADER_X509_H +__owur X509 *SSL_get_peer_certificate(const SSL *s); +# endif + +__owur STACK_OF(X509) *SSL_get_peer_cert_chain(const SSL *s); + +__owur int SSL_CTX_get_verify_mode(const SSL_CTX *ctx); +__owur int SSL_CTX_get_verify_depth(const SSL_CTX *ctx); +__owur SSL_verify_cb SSL_CTX_get_verify_callback(const SSL_CTX *ctx); +void SSL_CTX_set_verify(SSL_CTX *ctx, int mode, SSL_verify_cb callback); +void SSL_CTX_set_verify_depth(SSL_CTX *ctx, int depth); +void SSL_CTX_set_cert_verify_callback(SSL_CTX *ctx, + int (*cb) (X509_STORE_CTX *, void *), + void *arg); +void SSL_CTX_set_cert_cb(SSL_CTX *c, int (*cb) (SSL *ssl, void *arg), + void *arg); +# ifndef OPENSSL_NO_RSA +__owur int SSL_CTX_use_RSAPrivateKey(SSL_CTX *ctx, RSA *rsa); +__owur int SSL_CTX_use_RSAPrivateKey_ASN1(SSL_CTX *ctx, const unsigned char *d, + long len); +# endif +__owur int SSL_CTX_use_PrivateKey(SSL_CTX *ctx, EVP_PKEY *pkey); +__owur int SSL_CTX_use_PrivateKey_ASN1(int pk, SSL_CTX *ctx, + const unsigned char *d, long len); +__owur int SSL_CTX_use_certificate(SSL_CTX *ctx, X509 *x); +__owur int SSL_CTX_use_certificate_ASN1(SSL_CTX *ctx, int len, + const unsigned char *d); + +void SSL_CTX_set_default_passwd_cb(SSL_CTX *ctx, pem_password_cb *cb); +void SSL_CTX_set_default_passwd_cb_userdata(SSL_CTX *ctx, void *u); +pem_password_cb *SSL_CTX_get_default_passwd_cb(SSL_CTX *ctx); +void *SSL_CTX_get_default_passwd_cb_userdata(SSL_CTX *ctx); +void SSL_set_default_passwd_cb(SSL *s, pem_password_cb *cb); +void SSL_set_default_passwd_cb_userdata(SSL *s, void *u); +pem_password_cb *SSL_get_default_passwd_cb(SSL *s); +void *SSL_get_default_passwd_cb_userdata(SSL *s); + +__owur int SSL_CTX_check_private_key(const SSL_CTX *ctx); +__owur int SSL_check_private_key(const SSL *ctx); + +__owur int SSL_CTX_set_session_id_context(SSL_CTX *ctx, const unsigned char *sid_ctx, + unsigned int sid_ctx_len); + +SSL *SSL_new(SSL_CTX *ctx); +int SSL_up_ref(SSL *s); +int SSL_is_dtls(const SSL *s); +__owur int SSL_set_session_id_context(SSL *ssl, const unsigned char *sid_ctx, + unsigned int sid_ctx_len); + +__owur int SSL_CTX_set_purpose(SSL_CTX *s, int purpose); +__owur int SSL_set_purpose(SSL *s, int purpose); +__owur int SSL_CTX_set_trust(SSL_CTX *s, int trust); +__owur int SSL_set_trust(SSL *s, int trust); + +__owur int SSL_set1_host(SSL *s, const char *hostname); +__owur int SSL_add1_host(SSL *s, const char *hostname); +__owur const char *SSL_get0_peername(SSL *s); +void SSL_set_hostflags(SSL *s, unsigned int flags); + +__owur int SSL_CTX_dane_enable(SSL_CTX *ctx); +__owur int SSL_CTX_dane_mtype_set(SSL_CTX *ctx, const EVP_MD *md, + uint8_t mtype, uint8_t ord); +__owur int SSL_dane_enable(SSL *s, const char *basedomain); +__owur int SSL_dane_tlsa_add(SSL *s, uint8_t usage, uint8_t selector, + uint8_t mtype, unsigned char *data, size_t dlen); +__owur int SSL_get0_dane_authority(SSL *s, X509 **mcert, EVP_PKEY **mspki); +__owur int SSL_get0_dane_tlsa(SSL *s, uint8_t *usage, uint8_t *selector, + uint8_t *mtype, unsigned const char **data, + size_t *dlen); +/* + * Bridge opacity barrier between libcrypt and libssl, also needed to support + * offline testing in test/danetest.c + */ +SSL_DANE *SSL_get0_dane(SSL *ssl); +/* + * DANE flags + */ +unsigned long SSL_CTX_dane_set_flags(SSL_CTX *ctx, unsigned long flags); +unsigned long SSL_CTX_dane_clear_flags(SSL_CTX *ctx, unsigned long flags); +unsigned long SSL_dane_set_flags(SSL *ssl, unsigned long flags); +unsigned long SSL_dane_clear_flags(SSL *ssl, unsigned long flags); + +__owur int SSL_CTX_set1_param(SSL_CTX *ctx, X509_VERIFY_PARAM *vpm); +__owur int SSL_set1_param(SSL *ssl, X509_VERIFY_PARAM *vpm); + +__owur X509_VERIFY_PARAM *SSL_CTX_get0_param(SSL_CTX *ctx); +__owur X509_VERIFY_PARAM *SSL_get0_param(SSL *ssl); + +# ifndef OPENSSL_NO_SRP +int SSL_CTX_set_srp_username(SSL_CTX *ctx, char *name); +int SSL_CTX_set_srp_password(SSL_CTX *ctx, char *password); +int SSL_CTX_set_srp_strength(SSL_CTX *ctx, int strength); +int SSL_CTX_set_srp_client_pwd_callback(SSL_CTX *ctx, + char *(*cb) (SSL *, void *)); +int SSL_CTX_set_srp_verify_param_callback(SSL_CTX *ctx, + int (*cb) (SSL *, void *)); +int SSL_CTX_set_srp_username_callback(SSL_CTX *ctx, + int (*cb) (SSL *, int *, void *)); +int SSL_CTX_set_srp_cb_arg(SSL_CTX *ctx, void *arg); + +int SSL_set_srp_server_param(SSL *s, const BIGNUM *N, const BIGNUM *g, + BIGNUM *sa, BIGNUM *v, char *info); +int SSL_set_srp_server_param_pw(SSL *s, const char *user, const char *pass, + const char *grp); + +__owur BIGNUM *SSL_get_srp_g(SSL *s); +__owur BIGNUM *SSL_get_srp_N(SSL *s); + +__owur char *SSL_get_srp_username(SSL *s); +__owur char *SSL_get_srp_userinfo(SSL *s); +# endif + +void SSL_certs_clear(SSL *s); +void SSL_free(SSL *ssl); +# ifdef OSSL_ASYNC_FD +/* + * Windows application developer has to include windows.h to use these. + */ +__owur int SSL_waiting_for_async(SSL *s); +__owur int SSL_get_all_async_fds(SSL *s, OSSL_ASYNC_FD *fds, size_t *numfds); +__owur int SSL_get_changed_async_fds(SSL *s, OSSL_ASYNC_FD *addfd, + size_t *numaddfds, OSSL_ASYNC_FD *delfd, + size_t *numdelfds); +# endif +__owur int SSL_accept(SSL *ssl); +__owur int SSL_connect(SSL *ssl); +__owur int SSL_read(SSL *ssl, void *buf, int num); +__owur int SSL_peek(SSL *ssl, void *buf, int num); +__owur int SSL_write(SSL *ssl, const void *buf, int num); +long SSL_ctrl(SSL *ssl, int cmd, long larg, void *parg); +long SSL_callback_ctrl(SSL *, int, void (*)(void)); +long SSL_CTX_ctrl(SSL_CTX *ctx, int cmd, long larg, void *parg); +long SSL_CTX_callback_ctrl(SSL_CTX *, int, void (*)(void)); + +__owur int SSL_get_error(const SSL *s, int ret_code); +__owur const char *SSL_get_version(const SSL *s); + +/* This sets the 'default' SSL version that SSL_new() will create */ +__owur int SSL_CTX_set_ssl_version(SSL_CTX *ctx, const SSL_METHOD *meth); + +# ifndef OPENSSL_NO_SSL3_METHOD +DEPRECATEDIN_1_1_0(__owur const SSL_METHOD *SSLv3_method(void)) /* SSLv3 */ +DEPRECATEDIN_1_1_0(__owur const SSL_METHOD *SSLv3_server_method(void)) /* SSLv3 */ +DEPRECATEDIN_1_1_0(__owur const SSL_METHOD *SSLv3_client_method(void)) /* SSLv3 */ +# endif + +#define SSLv23_method TLS_method +#define SSLv23_server_method TLS_server_method +#define SSLv23_client_method TLS_client_method + +/* Negotiate highest available SSL/TLS version */ +__owur const SSL_METHOD *TLS_method(void); +__owur const SSL_METHOD *TLS_server_method(void); +__owur const SSL_METHOD *TLS_client_method(void); + +# ifndef OPENSSL_NO_TLS1_METHOD +DEPRECATEDIN_1_1_0(__owur const SSL_METHOD *TLSv1_method(void)) /* TLSv1.0 */ +DEPRECATEDIN_1_1_0(__owur const SSL_METHOD *TLSv1_server_method(void)) /* TLSv1.0 */ +DEPRECATEDIN_1_1_0(__owur const SSL_METHOD *TLSv1_client_method(void)) /* TLSv1.0 */ +# endif + +# ifndef OPENSSL_NO_TLS1_1_METHOD +DEPRECATEDIN_1_1_0(__owur const SSL_METHOD *TLSv1_1_method(void)) /* TLSv1.1 */ +DEPRECATEDIN_1_1_0(__owur const SSL_METHOD *TLSv1_1_server_method(void)) /* TLSv1.1 */ +DEPRECATEDIN_1_1_0(__owur const SSL_METHOD *TLSv1_1_client_method(void)) /* TLSv1.1 */ +# endif + +# ifndef OPENSSL_NO_TLS1_2_METHOD +DEPRECATEDIN_1_1_0(__owur const SSL_METHOD *TLSv1_2_method(void)) /* TLSv1.2 */ +DEPRECATEDIN_1_1_0(__owur const SSL_METHOD *TLSv1_2_server_method(void)) /* TLSv1.2 */ +DEPRECATEDIN_1_1_0(__owur const SSL_METHOD *TLSv1_2_client_method(void)) /* TLSv1.2 */ +# endif + +# ifndef OPENSSL_NO_DTLS1_METHOD +DEPRECATEDIN_1_1_0(__owur const SSL_METHOD *DTLSv1_method(void)) /* DTLSv1.0 */ +DEPRECATEDIN_1_1_0(__owur const SSL_METHOD *DTLSv1_server_method(void)) /* DTLSv1.0 */ +DEPRECATEDIN_1_1_0(__owur const SSL_METHOD *DTLSv1_client_method(void)) /* DTLSv1.0 */ +# endif + +# ifndef OPENSSL_NO_DTLS1_2_METHOD +DEPRECATEDIN_1_1_0(__owur const SSL_METHOD *DTLSv1_2_method(void)) /* DTLSv1.2 */ +DEPRECATEDIN_1_1_0(__owur const SSL_METHOD *DTLSv1_2_server_method(void)) /* DTLSv1.2 */ +DEPRECATEDIN_1_1_0(__owur const SSL_METHOD *DTLSv1_2_client_method(void)) /* DTLSv1.2 */ +#endif + +__owur const SSL_METHOD *DTLS_method(void); /* DTLS 1.0 and 1.2 */ +__owur const SSL_METHOD *DTLS_server_method(void); /* DTLS 1.0 and 1.2 */ +__owur const SSL_METHOD *DTLS_client_method(void); /* DTLS 1.0 and 1.2 */ + +__owur STACK_OF(SSL_CIPHER) *SSL_get_ciphers(const SSL *s); +__owur STACK_OF(SSL_CIPHER) *SSL_CTX_get_ciphers(const SSL_CTX *ctx); +__owur STACK_OF(SSL_CIPHER) *SSL_get_client_ciphers(const SSL *s); +__owur STACK_OF(SSL_CIPHER) *SSL_get1_supported_ciphers(SSL *s); + +__owur int SSL_do_handshake(SSL *s); +int SSL_renegotiate(SSL *s); +__owur int SSL_renegotiate_abbreviated(SSL *s); +__owur int SSL_renegotiate_pending(SSL *s); +int SSL_shutdown(SSL *s); + +__owur const SSL_METHOD *SSL_CTX_get_ssl_method(SSL_CTX *ctx); +__owur const SSL_METHOD *SSL_get_ssl_method(SSL *s); +__owur int SSL_set_ssl_method(SSL *s, const SSL_METHOD *method); +__owur const char *SSL_alert_type_string_long(int value); +__owur const char *SSL_alert_type_string(int value); +__owur const char *SSL_alert_desc_string_long(int value); +__owur const char *SSL_alert_desc_string(int value); + +void SSL_set_client_CA_list(SSL *s, STACK_OF(X509_NAME) *name_list); +void SSL_CTX_set_client_CA_list(SSL_CTX *ctx, STACK_OF(X509_NAME) *name_list); +__owur STACK_OF(X509_NAME) *SSL_get_client_CA_list(const SSL *s); +__owur STACK_OF(X509_NAME) *SSL_CTX_get_client_CA_list(const SSL_CTX *s); +__owur int SSL_add_client_CA(SSL *ssl, X509 *x); +__owur int SSL_CTX_add_client_CA(SSL_CTX *ctx, X509 *x); + +void SSL_set_connect_state(SSL *s); +void SSL_set_accept_state(SSL *s); + +__owur long SSL_get_default_timeout(const SSL *s); + +#if OPENSSL_API_COMPAT < 0x10100000L +# define SSL_library_init() OPENSSL_init_ssl(0, NULL) +#endif + +__owur char *SSL_CIPHER_description(const SSL_CIPHER *, char *buf, int size); +__owur STACK_OF(X509_NAME) *SSL_dup_CA_list(STACK_OF(X509_NAME) *sk); + +__owur SSL *SSL_dup(SSL *ssl); + +__owur X509 *SSL_get_certificate(const SSL *ssl); +/* + * EVP_PKEY + */ struct evp_pkey_st *SSL_get_privatekey(const SSL *ssl); + +__owur X509 *SSL_CTX_get0_certificate(const SSL_CTX *ctx); +__owur EVP_PKEY *SSL_CTX_get0_privatekey(const SSL_CTX *ctx); + +void SSL_CTX_set_quiet_shutdown(SSL_CTX *ctx, int mode); +__owur int SSL_CTX_get_quiet_shutdown(const SSL_CTX *ctx); +void SSL_set_quiet_shutdown(SSL *ssl, int mode); +__owur int SSL_get_quiet_shutdown(const SSL *ssl); +void SSL_set_shutdown(SSL *ssl, int mode); +__owur int SSL_get_shutdown(const SSL *ssl); +__owur int SSL_version(const SSL *ssl); +__owur int SSL_client_version(const SSL *s); +__owur int SSL_CTX_set_default_verify_paths(SSL_CTX *ctx); +__owur int SSL_CTX_set_default_verify_dir(SSL_CTX *ctx); +__owur int SSL_CTX_set_default_verify_file(SSL_CTX *ctx); +__owur int SSL_CTX_load_verify_locations(SSL_CTX *ctx, const char *CAfile, + const char *CApath); +# define SSL_get0_session SSL_get_session/* just peek at pointer */ +__owur SSL_SESSION *SSL_get_session(const SSL *ssl); +__owur SSL_SESSION *SSL_get1_session(SSL *ssl); /* obtain a reference count */ +__owur SSL_CTX *SSL_get_SSL_CTX(const SSL *ssl); +SSL_CTX *SSL_set_SSL_CTX(SSL *ssl, SSL_CTX *ctx); +void SSL_set_info_callback(SSL *ssl, + void (*cb) (const SSL *ssl, int type, int val)); +void (*SSL_get_info_callback(const SSL *ssl)) (const SSL *ssl, int type, + int val); +__owur OSSL_HANDSHAKE_STATE SSL_get_state(const SSL *ssl); + +void SSL_set_verify_result(SSL *ssl, long v); +__owur long SSL_get_verify_result(const SSL *ssl); +__owur STACK_OF(X509) *SSL_get0_verified_chain(const SSL *s); + +__owur size_t SSL_get_client_random(const SSL *ssl, unsigned char *out, + size_t outlen); +__owur size_t SSL_get_server_random(const SSL *ssl, unsigned char *out, + size_t outlen); +__owur size_t SSL_SESSION_get_master_key(const SSL_SESSION *ssl, + unsigned char *out, size_t outlen); + +#define SSL_get_ex_new_index(l, p, newf, dupf, freef) \ + CRYPTO_get_ex_new_index(CRYPTO_EX_INDEX_SSL, l, p, newf, dupf, freef) +__owur int SSL_set_ex_data(SSL *ssl, int idx, void *data); +void *SSL_get_ex_data(const SSL *ssl, int idx); +#define SSL_SESSION_get_ex_new_index(l, p, newf, dupf, freef) \ + CRYPTO_get_ex_new_index(CRYPTO_EX_INDEX_SSL_SESSION, l, p, newf, dupf, freef) +__owur int SSL_SESSION_set_ex_data(SSL_SESSION *ss, int idx, void *data); +void *SSL_SESSION_get_ex_data(const SSL_SESSION *ss, int idx); +#define SSL_CTX_get_ex_new_index(l, p, newf, dupf, freef) \ + CRYPTO_get_ex_new_index(CRYPTO_EX_INDEX_SSL_CTX, l, p, newf, dupf, freef) +__owur int SSL_CTX_set_ex_data(SSL_CTX *ssl, int idx, void *data); +void *SSL_CTX_get_ex_data(const SSL_CTX *ssl, int idx); + +__owur int SSL_get_ex_data_X509_STORE_CTX_idx(void); + +# define SSL_CTX_sess_set_cache_size(ctx,t) \ + SSL_CTX_ctrl(ctx,SSL_CTRL_SET_SESS_CACHE_SIZE,t,NULL) +# define SSL_CTX_sess_get_cache_size(ctx) \ + SSL_CTX_ctrl(ctx,SSL_CTRL_GET_SESS_CACHE_SIZE,0,NULL) +# define SSL_CTX_set_session_cache_mode(ctx,m) \ + SSL_CTX_ctrl(ctx,SSL_CTRL_SET_SESS_CACHE_MODE,m,NULL) +# define SSL_CTX_get_session_cache_mode(ctx) \ + SSL_CTX_ctrl(ctx,SSL_CTRL_GET_SESS_CACHE_MODE,0,NULL) + +# define SSL_CTX_get_default_read_ahead(ctx) SSL_CTX_get_read_ahead(ctx) +# define SSL_CTX_set_default_read_ahead(ctx,m) SSL_CTX_set_read_ahead(ctx,m) +# define SSL_CTX_get_read_ahead(ctx) \ + SSL_CTX_ctrl(ctx,SSL_CTRL_GET_READ_AHEAD,0,NULL) +# define SSL_CTX_set_read_ahead(ctx,m) \ + SSL_CTX_ctrl(ctx,SSL_CTRL_SET_READ_AHEAD,m,NULL) +# define SSL_CTX_get_max_cert_list(ctx) \ + SSL_CTX_ctrl(ctx,SSL_CTRL_GET_MAX_CERT_LIST,0,NULL) +# define SSL_CTX_set_max_cert_list(ctx,m) \ + SSL_CTX_ctrl(ctx,SSL_CTRL_SET_MAX_CERT_LIST,m,NULL) +# define SSL_get_max_cert_list(ssl) \ + SSL_ctrl(ssl,SSL_CTRL_GET_MAX_CERT_LIST,0,NULL) +# define SSL_set_max_cert_list(ssl,m) \ + SSL_ctrl(ssl,SSL_CTRL_SET_MAX_CERT_LIST,m,NULL) + +# define SSL_CTX_set_max_send_fragment(ctx,m) \ + SSL_CTX_ctrl(ctx,SSL_CTRL_SET_MAX_SEND_FRAGMENT,m,NULL) +# define SSL_set_max_send_fragment(ssl,m) \ + SSL_ctrl(ssl,SSL_CTRL_SET_MAX_SEND_FRAGMENT,m,NULL) +# define SSL_CTX_set_split_send_fragment(ctx,m) \ + SSL_CTX_ctrl(ctx,SSL_CTRL_SET_SPLIT_SEND_FRAGMENT,m,NULL) +# define SSL_set_split_send_fragment(ssl,m) \ + SSL_ctrl(ssl,SSL_CTRL_SET_SPLIT_SEND_FRAGMENT,m,NULL) +# define SSL_CTX_set_max_pipelines(ctx,m) \ + SSL_CTX_ctrl(ctx,SSL_CTRL_SET_MAX_PIPELINES,m,NULL) +# define SSL_set_max_pipelines(ssl,m) \ + SSL_ctrl(ssl,SSL_CTRL_SET_MAX_PIPELINES,m,NULL) + +void SSL_CTX_set_default_read_buffer_len(SSL_CTX *ctx, size_t len); +void SSL_set_default_read_buffer_len(SSL *s, size_t len); + +# ifndef OPENSSL_NO_DH +/* NB: the |keylength| is only applicable when is_export is true */ +void SSL_CTX_set_tmp_dh_callback(SSL_CTX *ctx, + DH *(*dh) (SSL *ssl, int is_export, + int keylength)); +void SSL_set_tmp_dh_callback(SSL *ssl, + DH *(*dh) (SSL *ssl, int is_export, + int keylength)); +# endif + +__owur const COMP_METHOD *SSL_get_current_compression(SSL *s); +__owur const COMP_METHOD *SSL_get_current_expansion(SSL *s); +__owur const char *SSL_COMP_get_name(const COMP_METHOD *comp); +__owur const char *SSL_COMP_get0_name(const SSL_COMP *comp); +__owur int SSL_COMP_get_id(const SSL_COMP *comp); +STACK_OF(SSL_COMP) *SSL_COMP_get_compression_methods(void); +__owur STACK_OF(SSL_COMP) *SSL_COMP_set0_compression_methods(STACK_OF(SSL_COMP) + *meths); +#if OPENSSL_API_COMPAT < 0x10100000L +# define SSL_COMP_free_compression_methods() while(0) continue +#endif +__owur int SSL_COMP_add_compression_method(int id, COMP_METHOD *cm); + +const SSL_CIPHER *SSL_CIPHER_find(SSL *ssl, const unsigned char *ptr); +int SSL_CIPHER_get_cipher_nid(const SSL_CIPHER *c); +int SSL_CIPHER_get_digest_nid(const SSL_CIPHER *c); + +/* TLS extensions functions */ +__owur int SSL_set_session_ticket_ext(SSL *s, void *ext_data, int ext_len); + +__owur int SSL_set_session_ticket_ext_cb(SSL *s, tls_session_ticket_ext_cb_fn cb, + void *arg); + +/* Pre-shared secret session resumption functions */ +__owur int SSL_set_session_secret_cb(SSL *s, + tls_session_secret_cb_fn tls_session_secret_cb, + void *arg); + +void SSL_CTX_set_not_resumable_session_callback(SSL_CTX *ctx, + int (*cb) (SSL *ssl, + int + is_forward_secure)); + +void SSL_set_not_resumable_session_callback(SSL *ssl, + int (*cb) (SSL *ssl, + int + is_forward_secure)); +# if OPENSSL_API_COMPAT < 0x10100000L +# define SSL_cache_hit(s) SSL_session_reused(s) +# endif + +__owur int SSL_session_reused(SSL *s); +__owur int SSL_is_server(SSL *s); + +__owur __owur SSL_CONF_CTX *SSL_CONF_CTX_new(void); +int SSL_CONF_CTX_finish(SSL_CONF_CTX *cctx); +void SSL_CONF_CTX_free(SSL_CONF_CTX *cctx); +unsigned int SSL_CONF_CTX_set_flags(SSL_CONF_CTX *cctx, unsigned int flags); +__owur unsigned int SSL_CONF_CTX_clear_flags(SSL_CONF_CTX *cctx, unsigned int flags); +__owur int SSL_CONF_CTX_set1_prefix(SSL_CONF_CTX *cctx, const char *pre); + +void SSL_CONF_CTX_set_ssl(SSL_CONF_CTX *cctx, SSL *ssl); +void SSL_CONF_CTX_set_ssl_ctx(SSL_CONF_CTX *cctx, SSL_CTX *ctx); + +__owur int SSL_CONF_cmd(SSL_CONF_CTX *cctx, const char *cmd, const char *value); +__owur int SSL_CONF_cmd_argv(SSL_CONF_CTX *cctx, int *pargc, char ***pargv); +__owur int SSL_CONF_cmd_value_type(SSL_CONF_CTX *cctx, const char *cmd); + +void SSL_add_ssl_module(void); +int SSL_config(SSL *s, const char *name); +int SSL_CTX_config(SSL_CTX *ctx, const char *name); + +# ifndef OPENSSL_NO_SSL_TRACE +void SSL_trace(int write_p, int version, int content_type, + const void *buf, size_t len, SSL *ssl, void *arg); +__owur const char *SSL_CIPHER_standard_name(const SSL_CIPHER *c); +# endif + +# ifndef OPENSSL_NO_SOCK +int DTLSv1_listen(SSL *s, BIO_ADDR *client); +# endif + +# ifndef OPENSSL_NO_CT + +/* + * A callback for verifying that the received SCTs are sufficient. + * Expected to return 1 if they are sufficient, otherwise 0. + * May return a negative integer if an error occurs. + * A connection should be aborted if the SCTs are deemed insufficient. + */ +typedef int(*ssl_ct_validation_cb)(const CT_POLICY_EVAL_CTX *ctx, + const STACK_OF(SCT) *scts, void *arg); + +/* + * Sets a |callback| that is invoked upon receipt of ServerHelloDone to validate + * the received SCTs. + * If the callback returns a non-positive result, the connection is terminated. + * Call this function before beginning a handshake. + * If a NULL |callback| is provided, SCT validation is disabled. + * |arg| is arbitrary userdata that will be passed to the callback whenever it + * is invoked. Ownership of |arg| remains with the caller. + * + * NOTE: A side-effect of setting a CT callback is that an OCSP stapled response + * will be requested. + */ +int SSL_set_ct_validation_callback(SSL *s, ssl_ct_validation_cb callback, + void *arg); +int SSL_CTX_set_ct_validation_callback(SSL_CTX *ctx, + ssl_ct_validation_cb callback, + void *arg); +#define SSL_disable_ct(s) \ + ((void) SSL_set_validation_callback((s), NULL, NULL)) +#define SSL_CTX_disable_ct(ctx) \ + ((void) SSL_CTX_set_validation_callback((ctx), NULL, NULL)) + +/* + * The validation type enumerates the available behaviours of the built-in SSL + * CT validation callback selected via SSL_enable_ct() and SSL_CTX_enable_ct(). + * The underlying callback is a static function in libssl. + */ +enum { + SSL_CT_VALIDATION_PERMISSIVE = 0, + SSL_CT_VALIDATION_STRICT +}; + +/* + * Enable CT by setting up a callback that implements one of the built-in + * validation variants. The SSL_CT_VALIDATION_PERMISSIVE variant always + * continues the handshake, the application can make appropriate decisions at + * handshake completion. The SSL_CT_VALIDATION_STRICT variant requires at + * least one valid SCT, or else handshake termination will be requested. The + * handshake may continue anyway if SSL_VERIFY_NONE is in effect. + */ +int SSL_enable_ct(SSL *s, int validation_mode); +int SSL_CTX_enable_ct(SSL_CTX *ctx, int validation_mode); + +/* + * Report whether a non-NULL callback is enabled. + */ +int SSL_ct_is_enabled(const SSL *s); +int SSL_CTX_ct_is_enabled(const SSL_CTX *ctx); + +/* Gets the SCTs received from a connection */ +const STACK_OF(SCT) *SSL_get0_peer_scts(SSL *s); + +/* + * Loads the CT log list from the default location. + * If a CTLOG_STORE has previously been set using SSL_CTX_set_ctlog_store, + * the log information loaded from this file will be appended to the + * CTLOG_STORE. + * Returns 1 on success, 0 otherwise. + */ +int SSL_CTX_set_default_ctlog_list_file(SSL_CTX *ctx); + +/* + * Loads the CT log list from the specified file path. + * If a CTLOG_STORE has previously been set using SSL_CTX_set_ctlog_store, + * the log information loaded from this file will be appended to the + * CTLOG_STORE. + * Returns 1 on success, 0 otherwise. + */ +int SSL_CTX_set_ctlog_list_file(SSL_CTX *ctx, const char *path); + +/* + * Sets the CT log list used by all SSL connections created from this SSL_CTX. + * Ownership of the CTLOG_STORE is transferred to the SSL_CTX. + */ +void SSL_CTX_set0_ctlog_store(SSL_CTX *ctx, CTLOG_STORE *logs); + +/* + * Gets the CT log list used by all SSL connections created from this SSL_CTX. + * This will be NULL unless one of the following functions has been called: + * - SSL_CTX_set_default_ctlog_list_file + * - SSL_CTX_set_ctlog_list_file + * - SSL_CTX_set_ctlog_store + */ +const CTLOG_STORE *SSL_CTX_get0_ctlog_store(const SSL_CTX *ctx); + +# endif /* OPENSSL_NO_CT */ + +/* What the "other" parameter contains in security callback */ +/* Mask for type */ +# define SSL_SECOP_OTHER_TYPE 0xffff0000 +# define SSL_SECOP_OTHER_NONE 0 +# define SSL_SECOP_OTHER_CIPHER (1 << 16) +# define SSL_SECOP_OTHER_CURVE (2 << 16) +# define SSL_SECOP_OTHER_DH (3 << 16) +# define SSL_SECOP_OTHER_PKEY (4 << 16) +# define SSL_SECOP_OTHER_SIGALG (5 << 16) +# define SSL_SECOP_OTHER_CERT (6 << 16) + +/* Indicated operation refers to peer key or certificate */ +# define SSL_SECOP_PEER 0x1000 + +/* Values for "op" parameter in security callback */ + +/* Called to filter ciphers */ +/* Ciphers client supports */ +# define SSL_SECOP_CIPHER_SUPPORTED (1 | SSL_SECOP_OTHER_CIPHER) +/* Cipher shared by client/server */ +# define SSL_SECOP_CIPHER_SHARED (2 | SSL_SECOP_OTHER_CIPHER) +/* Sanity check of cipher server selects */ +# define SSL_SECOP_CIPHER_CHECK (3 | SSL_SECOP_OTHER_CIPHER) +/* Curves supported by client */ +# define SSL_SECOP_CURVE_SUPPORTED (4 | SSL_SECOP_OTHER_CURVE) +/* Curves shared by client/server */ +# define SSL_SECOP_CURVE_SHARED (5 | SSL_SECOP_OTHER_CURVE) +/* Sanity check of curve server selects */ +# define SSL_SECOP_CURVE_CHECK (6 | SSL_SECOP_OTHER_CURVE) +/* Temporary DH key */ +# define SSL_SECOP_TMP_DH (7 | SSL_SECOP_OTHER_PKEY) +/* SSL/TLS version */ +# define SSL_SECOP_VERSION (9 | SSL_SECOP_OTHER_NONE) +/* Session tickets */ +# define SSL_SECOP_TICKET (10 | SSL_SECOP_OTHER_NONE) +/* Supported signature algorithms sent to peer */ +# define SSL_SECOP_SIGALG_SUPPORTED (11 | SSL_SECOP_OTHER_SIGALG) +/* Shared signature algorithm */ +# define SSL_SECOP_SIGALG_SHARED (12 | SSL_SECOP_OTHER_SIGALG) +/* Sanity check signature algorithm allowed */ +# define SSL_SECOP_SIGALG_CHECK (13 | SSL_SECOP_OTHER_SIGALG) +/* Used to get mask of supported public key signature algorithms */ +# define SSL_SECOP_SIGALG_MASK (14 | SSL_SECOP_OTHER_SIGALG) +/* Use to see if compression is allowed */ +# define SSL_SECOP_COMPRESSION (15 | SSL_SECOP_OTHER_NONE) +/* EE key in certificate */ +# define SSL_SECOP_EE_KEY (16 | SSL_SECOP_OTHER_CERT) +/* CA key in certificate */ +# define SSL_SECOP_CA_KEY (17 | SSL_SECOP_OTHER_CERT) +/* CA digest algorithm in certificate */ +# define SSL_SECOP_CA_MD (18 | SSL_SECOP_OTHER_CERT) +/* Peer EE key in certificate */ +# define SSL_SECOP_PEER_EE_KEY (SSL_SECOP_EE_KEY | SSL_SECOP_PEER) +/* Peer CA key in certificate */ +# define SSL_SECOP_PEER_CA_KEY (SSL_SECOP_CA_KEY | SSL_SECOP_PEER) +/* Peer CA digest algorithm in certificate */ +# define SSL_SECOP_PEER_CA_MD (SSL_SECOP_CA_MD | SSL_SECOP_PEER) + +void SSL_set_security_level(SSL *s, int level); +__owur int SSL_get_security_level(const SSL *s); +void SSL_set_security_callback(SSL *s, + int (*cb) (const SSL *s, const SSL_CTX *ctx, int op, + int bits, int nid, void *other, + void *ex)); +int (*SSL_get_security_callback(const SSL *s)) (const SSL *s, const SSL_CTX *ctx, int op, + int bits, int nid, + void *other, void *ex); +void SSL_set0_security_ex_data(SSL *s, void *ex); +__owur void *SSL_get0_security_ex_data(const SSL *s); + +void SSL_CTX_set_security_level(SSL_CTX *ctx, int level); +__owur int SSL_CTX_get_security_level(const SSL_CTX *ctx); +void SSL_CTX_set_security_callback(SSL_CTX *ctx, + int (*cb) (const SSL *s, const SSL_CTX *ctx, int op, + int bits, int nid, void *other, + void *ex)); +int (*SSL_CTX_get_security_callback(const SSL_CTX *ctx)) (const SSL *s, + const SSL_CTX *ctx, + int op, int bits, + int nid, + void *other, + void *ex); +void SSL_CTX_set0_security_ex_data(SSL_CTX *ctx, void *ex); +__owur void *SSL_CTX_get0_security_ex_data(const SSL_CTX *ctx); + +/* OPENSSL_INIT flag 0x010000 reserved for internal use */ +#define OPENSSL_INIT_NO_LOAD_SSL_STRINGS 0x00100000L +#define OPENSSL_INIT_LOAD_SSL_STRINGS 0x00200000L + +#define OPENSSL_INIT_SSL_DEFAULT \ + (OPENSSL_INIT_LOAD_SSL_STRINGS | OPENSSL_INIT_LOAD_CRYPTO_STRINGS) + +int OPENSSL_init_ssl(uint64_t opts, const OPENSSL_INIT_SETTINGS *settings); + +# ifndef OPENSSL_NO_UNIT_TEST +__owur const struct openssl_ssl_test_functions *SSL_test_functions(void); +# endif + +extern const char SSL_version_str[]; + +/* BEGIN ERROR CODES */ +/* + * The following lines are auto generated by the script mkerr.pl. Any changes + * made after this point may be overwritten when the script is next run. + */ + +int ERR_load_SSL_strings(void); + +/* Error codes for the SSL functions. */ + +/* Function codes. */ +# define SSL_F_CHECK_SUITEB_CIPHER_LIST 331 +# define SSL_F_CT_MOVE_SCTS 345 +# define SSL_F_CT_STRICT 349 +# define SSL_F_D2I_SSL_SESSION 103 +# define SSL_F_DANE_CTX_ENABLE 347 +# define SSL_F_DANE_MTYPE_SET 393 +# define SSL_F_DANE_TLSA_ADD 394 +# define SSL_F_DO_DTLS1_WRITE 245 +# define SSL_F_DO_SSL3_WRITE 104 +# define SSL_F_DTLS1_BUFFER_RECORD 247 +# define SSL_F_DTLS1_CHECK_TIMEOUT_NUM 318 +# define SSL_F_DTLS1_HEARTBEAT 305 +# define SSL_F_DTLS1_PREPROCESS_FRAGMENT 288 +# define SSL_F_DTLS1_PROCESS_BUFFERED_RECORDS 424 +# define SSL_F_DTLS1_PROCESS_RECORD 257 +# define SSL_F_DTLS1_READ_BYTES 258 +# define SSL_F_DTLS1_READ_FAILED 339 +# define SSL_F_DTLS1_RETRANSMIT_MESSAGE 390 +# define SSL_F_DTLS1_WRITE_APP_DATA_BYTES 268 +# define SSL_F_DTLSV1_LISTEN 350 +# define SSL_F_DTLS_CONSTRUCT_CHANGE_CIPHER_SPEC 371 +# define SSL_F_DTLS_CONSTRUCT_HELLO_VERIFY_REQUEST 385 +# define SSL_F_DTLS_GET_REASSEMBLED_MESSAGE 370 +# define SSL_F_DTLS_PROCESS_HELLO_VERIFY 386 +# define SSL_F_OPENSSL_INIT_SSL 342 +# define SSL_F_OSSL_STATEM_CLIENT_READ_TRANSITION 417 +# define SSL_F_OSSL_STATEM_SERVER_READ_TRANSITION 418 +# define SSL_F_READ_STATE_MACHINE 352 +# define SSL_F_SSL3_CHANGE_CIPHER_STATE 129 +# define SSL_F_SSL3_CHECK_CERT_AND_ALGORITHM 130 +# define SSL_F_SSL3_CTRL 213 +# define SSL_F_SSL3_CTX_CTRL 133 +# define SSL_F_SSL3_DIGEST_CACHED_RECORDS 293 +# define SSL_F_SSL3_DO_CHANGE_CIPHER_SPEC 292 +# define SSL_F_SSL3_FINAL_FINISH_MAC 285 +# define SSL_F_SSL3_GENERATE_KEY_BLOCK 238 +# define SSL_F_SSL3_GENERATE_MASTER_SECRET 388 +# define SSL_F_SSL3_GET_RECORD 143 +# define SSL_F_SSL3_INIT_FINISHED_MAC 397 +# define SSL_F_SSL3_OUTPUT_CERT_CHAIN 147 +# define SSL_F_SSL3_READ_BYTES 148 +# define SSL_F_SSL3_READ_N 149 +# define SSL_F_SSL3_SETUP_KEY_BLOCK 157 +# define SSL_F_SSL3_SETUP_READ_BUFFER 156 +# define SSL_F_SSL3_SETUP_WRITE_BUFFER 291 +# define SSL_F_SSL3_WRITE_BYTES 158 +# define SSL_F_SSL3_WRITE_PENDING 159 +# define SSL_F_SSL_ADD_CERT_CHAIN 316 +# define SSL_F_SSL_ADD_CERT_TO_BUF 319 +# define SSL_F_SSL_ADD_CLIENTHELLO_RENEGOTIATE_EXT 298 +# define SSL_F_SSL_ADD_CLIENTHELLO_TLSEXT 277 +# define SSL_F_SSL_ADD_CLIENTHELLO_USE_SRTP_EXT 307 +# define SSL_F_SSL_ADD_DIR_CERT_SUBJECTS_TO_STACK 215 +# define SSL_F_SSL_ADD_FILE_CERT_SUBJECTS_TO_STACK 216 +# define SSL_F_SSL_ADD_SERVERHELLO_RENEGOTIATE_EXT 299 +# define SSL_F_SSL_ADD_SERVERHELLO_TLSEXT 278 +# define SSL_F_SSL_ADD_SERVERHELLO_USE_SRTP_EXT 308 +# define SSL_F_SSL_BAD_METHOD 160 +# define SSL_F_SSL_BUILD_CERT_CHAIN 332 +# define SSL_F_SSL_BYTES_TO_CIPHER_LIST 161 +# define SSL_F_SSL_CERT_ADD0_CHAIN_CERT 346 +# define SSL_F_SSL_CERT_DUP 221 +# define SSL_F_SSL_CERT_NEW 162 +# define SSL_F_SSL_CERT_SET0_CHAIN 340 +# define SSL_F_SSL_CHECK_PRIVATE_KEY 163 +# define SSL_F_SSL_CHECK_SERVERHELLO_TLSEXT 280 +# define SSL_F_SSL_CHECK_SRVR_ECC_CERT_AND_ALG 279 +# define SSL_F_SSL_CIPHER_PROCESS_RULESTR 230 +# define SSL_F_SSL_CIPHER_STRENGTH_SORT 231 +# define SSL_F_SSL_CLEAR 164 +# define SSL_F_SSL_COMP_ADD_COMPRESSION_METHOD 165 +# define SSL_F_SSL_CONF_CMD 334 +# define SSL_F_SSL_CREATE_CIPHER_LIST 166 +# define SSL_F_SSL_CTRL 232 +# define SSL_F_SSL_CTX_CHECK_PRIVATE_KEY 168 +# define SSL_F_SSL_CTX_ENABLE_CT 398 +# define SSL_F_SSL_CTX_MAKE_PROFILES 309 +# define SSL_F_SSL_CTX_NEW 169 +# define SSL_F_SSL_CTX_SET_ALPN_PROTOS 343 +# define SSL_F_SSL_CTX_SET_CIPHER_LIST 269 +# define SSL_F_SSL_CTX_SET_CLIENT_CERT_ENGINE 290 +# define SSL_F_SSL_CTX_SET_CT_VALIDATION_CALLBACK 396 +# define SSL_F_SSL_CTX_SET_SESSION_ID_CONTEXT 219 +# define SSL_F_SSL_CTX_SET_SSL_VERSION 170 +# define SSL_F_SSL_CTX_USE_CERTIFICATE 171 +# define SSL_F_SSL_CTX_USE_CERTIFICATE_ASN1 172 +# define SSL_F_SSL_CTX_USE_CERTIFICATE_FILE 173 +# define SSL_F_SSL_CTX_USE_PRIVATEKEY 174 +# define SSL_F_SSL_CTX_USE_PRIVATEKEY_ASN1 175 +# define SSL_F_SSL_CTX_USE_PRIVATEKEY_FILE 176 +# define SSL_F_SSL_CTX_USE_PSK_IDENTITY_HINT 272 +# define SSL_F_SSL_CTX_USE_RSAPRIVATEKEY 177 +# define SSL_F_SSL_CTX_USE_RSAPRIVATEKEY_ASN1 178 +# define SSL_F_SSL_CTX_USE_RSAPRIVATEKEY_FILE 179 +# define SSL_F_SSL_CTX_USE_SERVERINFO 336 +# define SSL_F_SSL_CTX_USE_SERVERINFO_FILE 337 +# define SSL_F_SSL_DANE_DUP 403 +# define SSL_F_SSL_DANE_ENABLE 395 +# define SSL_F_SSL_DO_CONFIG 391 +# define SSL_F_SSL_DO_HANDSHAKE 180 +# define SSL_F_SSL_DUP_CA_LIST 408 +# define SSL_F_SSL_ENABLE_CT 402 +# define SSL_F_SSL_GET_NEW_SESSION 181 +# define SSL_F_SSL_GET_PREV_SESSION 217 +# define SSL_F_SSL_GET_SERVER_CERT_INDEX 322 +# define SSL_F_SSL_GET_SIGN_PKEY 183 +# define SSL_F_SSL_INIT_WBIO_BUFFER 184 +# define SSL_F_SSL_LOAD_CLIENT_CA_FILE 185 +# define SSL_F_SSL_MODULE_INIT 392 +# define SSL_F_SSL_NEW 186 +# define SSL_F_SSL_PARSE_CLIENTHELLO_RENEGOTIATE_EXT 300 +# define SSL_F_SSL_PARSE_CLIENTHELLO_TLSEXT 302 +# define SSL_F_SSL_PARSE_CLIENTHELLO_USE_SRTP_EXT 310 +# define SSL_F_SSL_PARSE_SERVERHELLO_RENEGOTIATE_EXT 301 +# define SSL_F_SSL_PARSE_SERVERHELLO_TLSEXT 303 +# define SSL_F_SSL_PARSE_SERVERHELLO_USE_SRTP_EXT 311 +# define SSL_F_SSL_PEEK 270 +# define SSL_F_SSL_READ 223 +# define SSL_F_SSL_SCAN_CLIENTHELLO_TLSEXT 320 +# define SSL_F_SSL_SCAN_SERVERHELLO_TLSEXT 321 +# define SSL_F_SSL_SESSION_DUP 348 +# define SSL_F_SSL_SESSION_NEW 189 +# define SSL_F_SSL_SESSION_PRINT_FP 190 +# define SSL_F_SSL_SESSION_SET1_ID 423 +# define SSL_F_SSL_SESSION_SET1_ID_CONTEXT 312 +# define SSL_F_SSL_SET_ALPN_PROTOS 344 +# define SSL_F_SSL_SET_CERT 191 +# define SSL_F_SSL_SET_CIPHER_LIST 271 +# define SSL_F_SSL_SET_CT_VALIDATION_CALLBACK 399 +# define SSL_F_SSL_SET_FD 192 +# define SSL_F_SSL_SET_PKEY 193 +# define SSL_F_SSL_SET_RFD 194 +# define SSL_F_SSL_SET_SESSION 195 +# define SSL_F_SSL_SET_SESSION_ID_CONTEXT 218 +# define SSL_F_SSL_SET_SESSION_TICKET_EXT 294 +# define SSL_F_SSL_SET_WFD 196 +# define SSL_F_SSL_SHUTDOWN 224 +# define SSL_F_SSL_SRP_CTX_INIT 313 +# define SSL_F_SSL_START_ASYNC_JOB 389 +# define SSL_F_SSL_UNDEFINED_FUNCTION 197 +# define SSL_F_SSL_UNDEFINED_VOID_FUNCTION 244 +# define SSL_F_SSL_USE_CERTIFICATE 198 +# define SSL_F_SSL_USE_CERTIFICATE_ASN1 199 +# define SSL_F_SSL_USE_CERTIFICATE_FILE 200 +# define SSL_F_SSL_USE_PRIVATEKEY 201 +# define SSL_F_SSL_USE_PRIVATEKEY_ASN1 202 +# define SSL_F_SSL_USE_PRIVATEKEY_FILE 203 +# define SSL_F_SSL_USE_PSK_IDENTITY_HINT 273 +# define SSL_F_SSL_USE_RSAPRIVATEKEY 204 +# define SSL_F_SSL_USE_RSAPRIVATEKEY_ASN1 205 +# define SSL_F_SSL_USE_RSAPRIVATEKEY_FILE 206 +# define SSL_F_SSL_VALIDATE_CT 400 +# define SSL_F_SSL_VERIFY_CERT_CHAIN 207 +# define SSL_F_SSL_WRITE 208 +# define SSL_F_STATE_MACHINE 353 +# define SSL_F_TLS12_CHECK_PEER_SIGALG 333 +# define SSL_F_TLS1_CHANGE_CIPHER_STATE 209 +# define SSL_F_TLS1_CHECK_DUPLICATE_EXTENSIONS 341 +# define SSL_F_TLS1_ENC 401 +# define SSL_F_TLS1_EXPORT_KEYING_MATERIAL 314 +# define SSL_F_TLS1_GET_CURVELIST 338 +# define SSL_F_TLS1_PRF 284 +# define SSL_F_TLS1_SETUP_KEY_BLOCK 211 +# define SSL_F_TLS1_SET_SERVER_SIGALGS 335 +# define SSL_F_TLS_CLIENT_KEY_EXCHANGE_POST_WORK 354 +# define SSL_F_TLS_CONSTRUCT_CERTIFICATE_REQUEST 372 +# define SSL_F_TLS_CONSTRUCT_CKE_DHE 404 +# define SSL_F_TLS_CONSTRUCT_CKE_ECDHE 405 +# define SSL_F_TLS_CONSTRUCT_CKE_GOST 406 +# define SSL_F_TLS_CONSTRUCT_CKE_PSK_PREAMBLE 407 +# define SSL_F_TLS_CONSTRUCT_CKE_RSA 409 +# define SSL_F_TLS_CONSTRUCT_CKE_SRP 410 +# define SSL_F_TLS_CONSTRUCT_CLIENT_CERTIFICATE 355 +# define SSL_F_TLS_CONSTRUCT_CLIENT_HELLO 356 +# define SSL_F_TLS_CONSTRUCT_CLIENT_KEY_EXCHANGE 357 +# define SSL_F_TLS_CONSTRUCT_CLIENT_VERIFY 358 +# define SSL_F_TLS_CONSTRUCT_FINISHED 359 +# define SSL_F_TLS_CONSTRUCT_HELLO_REQUEST 373 +# define SSL_F_TLS_CONSTRUCT_NEW_SESSION_TICKET 428 +# define SSL_F_TLS_CONSTRUCT_SERVER_CERTIFICATE 374 +# define SSL_F_TLS_CONSTRUCT_SERVER_DONE 375 +# define SSL_F_TLS_CONSTRUCT_SERVER_HELLO 376 +# define SSL_F_TLS_CONSTRUCT_SERVER_KEY_EXCHANGE 377 +# define SSL_F_TLS_GET_MESSAGE_BODY 351 +# define SSL_F_TLS_GET_MESSAGE_HEADER 387 +# define SSL_F_TLS_POST_PROCESS_CLIENT_HELLO 378 +# define SSL_F_TLS_POST_PROCESS_CLIENT_KEY_EXCHANGE 384 +# define SSL_F_TLS_PREPARE_CLIENT_CERTIFICATE 360 +# define SSL_F_TLS_PROCESS_CERTIFICATE_REQUEST 361 +# define SSL_F_TLS_PROCESS_CERT_STATUS 362 +# define SSL_F_TLS_PROCESS_CERT_VERIFY 379 +# define SSL_F_TLS_PROCESS_CHANGE_CIPHER_SPEC 363 +# define SSL_F_TLS_PROCESS_CKE_DHE 411 +# define SSL_F_TLS_PROCESS_CKE_ECDHE 412 +# define SSL_F_TLS_PROCESS_CKE_GOST 413 +# define SSL_F_TLS_PROCESS_CKE_PSK_PREAMBLE 414 +# define SSL_F_TLS_PROCESS_CKE_RSA 415 +# define SSL_F_TLS_PROCESS_CKE_SRP 416 +# define SSL_F_TLS_PROCESS_CLIENT_CERTIFICATE 380 +# define SSL_F_TLS_PROCESS_CLIENT_HELLO 381 +# define SSL_F_TLS_PROCESS_CLIENT_KEY_EXCHANGE 382 +# define SSL_F_TLS_PROCESS_FINISHED 364 +# define SSL_F_TLS_PROCESS_KEY_EXCHANGE 365 +# define SSL_F_TLS_PROCESS_NEW_SESSION_TICKET 366 +# define SSL_F_TLS_PROCESS_NEXT_PROTO 383 +# define SSL_F_TLS_PROCESS_SERVER_CERTIFICATE 367 +# define SSL_F_TLS_PROCESS_SERVER_DONE 368 +# define SSL_F_TLS_PROCESS_SERVER_HELLO 369 +# define SSL_F_TLS_PROCESS_SKE_DHE 419 +# define SSL_F_TLS_PROCESS_SKE_ECDHE 420 +# define SSL_F_TLS_PROCESS_SKE_PSK_PREAMBLE 421 +# define SSL_F_TLS_PROCESS_SKE_SRP 422 +# define SSL_F_USE_CERTIFICATE_CHAIN_FILE 220 + +/* Reason codes. */ +# define SSL_R_APP_DATA_IN_HANDSHAKE 100 +# define SSL_R_ATTEMPT_TO_REUSE_SESSION_IN_DIFFERENT_CONTEXT 272 +# define SSL_R_AT_LEAST_TLS_1_0_NEEDED_IN_FIPS_MODE 143 +# define SSL_R_AT_LEAST_TLS_1_2_NEEDED_IN_SUITEB_MODE 158 +# define SSL_R_BAD_CHANGE_CIPHER_SPEC 103 +# define SSL_R_BAD_DATA 390 +# define SSL_R_BAD_DATA_RETURNED_BY_CALLBACK 106 +# define SSL_R_BAD_DECOMPRESSION 107 +# define SSL_R_BAD_DH_VALUE 102 +# define SSL_R_BAD_DIGEST_LENGTH 111 +# define SSL_R_BAD_ECC_CERT 304 +# define SSL_R_BAD_ECPOINT 306 +# define SSL_R_BAD_HANDSHAKE_LENGTH 332 +# define SSL_R_BAD_HELLO_REQUEST 105 +# define SSL_R_BAD_LENGTH 271 +# define SSL_R_BAD_PACKET_LENGTH 115 +# define SSL_R_BAD_PROTOCOL_VERSION_NUMBER 116 +# define SSL_R_BAD_RSA_ENCRYPT 119 +# define SSL_R_BAD_SIGNATURE 123 +# define SSL_R_BAD_SRP_A_LENGTH 347 +# define SSL_R_BAD_SRP_PARAMETERS 371 +# define SSL_R_BAD_SRTP_MKI_VALUE 352 +# define SSL_R_BAD_SRTP_PROTECTION_PROFILE_LIST 353 +# define SSL_R_BAD_SSL_FILETYPE 124 +# define SSL_R_BAD_VALUE 384 +# define SSL_R_BAD_WRITE_RETRY 127 +# define SSL_R_BIO_NOT_SET 128 +# define SSL_R_BLOCK_CIPHER_PAD_IS_WRONG 129 +# define SSL_R_BN_LIB 130 +# define SSL_R_CA_DN_LENGTH_MISMATCH 131 +# define SSL_R_CA_KEY_TOO_SMALL 397 +# define SSL_R_CA_MD_TOO_WEAK 398 +# define SSL_R_CCS_RECEIVED_EARLY 133 +# define SSL_R_CERTIFICATE_VERIFY_FAILED 134 +# define SSL_R_CERT_CB_ERROR 377 +# define SSL_R_CERT_LENGTH_MISMATCH 135 +# define SSL_R_CIPHER_CODE_WRONG_LENGTH 137 +# define SSL_R_CIPHER_OR_HASH_UNAVAILABLE 138 +# define SSL_R_CLIENTHELLO_TLSEXT 226 +# define SSL_R_COMPRESSED_LENGTH_TOO_LONG 140 +# define SSL_R_COMPRESSION_DISABLED 343 +# define SSL_R_COMPRESSION_FAILURE 141 +# define SSL_R_COMPRESSION_ID_NOT_WITHIN_PRIVATE_RANGE 307 +# define SSL_R_COMPRESSION_LIBRARY_ERROR 142 +# define SSL_R_CONNECTION_TYPE_NOT_SET 144 +# define SSL_R_CONTEXT_NOT_DANE_ENABLED 167 +# define SSL_R_COOKIE_GEN_CALLBACK_FAILURE 400 +# define SSL_R_COOKIE_MISMATCH 308 +# define SSL_R_CUSTOM_EXT_HANDLER_ALREADY_INSTALLED 206 +# define SSL_R_DANE_ALREADY_ENABLED 172 +# define SSL_R_DANE_CANNOT_OVERRIDE_MTYPE_FULL 173 +# define SSL_R_DANE_NOT_ENABLED 175 +# define SSL_R_DANE_TLSA_BAD_CERTIFICATE 180 +# define SSL_R_DANE_TLSA_BAD_CERTIFICATE_USAGE 184 +# define SSL_R_DANE_TLSA_BAD_DATA_LENGTH 189 +# define SSL_R_DANE_TLSA_BAD_DIGEST_LENGTH 192 +# define SSL_R_DANE_TLSA_BAD_MATCHING_TYPE 200 +# define SSL_R_DANE_TLSA_BAD_PUBLIC_KEY 201 +# define SSL_R_DANE_TLSA_BAD_SELECTOR 202 +# define SSL_R_DANE_TLSA_NULL_DATA 203 +# define SSL_R_DATA_BETWEEN_CCS_AND_FINISHED 145 +# define SSL_R_DATA_LENGTH_TOO_LONG 146 +# define SSL_R_DECRYPTION_FAILED 147 +# define SSL_R_DECRYPTION_FAILED_OR_BAD_RECORD_MAC 281 +# define SSL_R_DH_KEY_TOO_SMALL 394 +# define SSL_R_DH_PUBLIC_VALUE_LENGTH_IS_WRONG 148 +# define SSL_R_DIGEST_CHECK_FAILED 149 +# define SSL_R_DTLS_MESSAGE_TOO_BIG 334 +# define SSL_R_DUPLICATE_COMPRESSION_ID 309 +# define SSL_R_ECC_CERT_NOT_FOR_SIGNING 318 +# define SSL_R_ECDH_REQUIRED_FOR_SUITEB_MODE 374 +# define SSL_R_EE_KEY_TOO_SMALL 399 +# define SSL_R_EMPTY_SRTP_PROTECTION_PROFILE_LIST 354 +# define SSL_R_ENCRYPTED_LENGTH_TOO_LONG 150 +# define SSL_R_ERROR_IN_RECEIVED_CIPHER_LIST 151 +# define SSL_R_ERROR_SETTING_TLSA_BASE_DOMAIN 204 +# define SSL_R_EXCESSIVE_MESSAGE_SIZE 152 +# define SSL_R_EXTRA_DATA_IN_MESSAGE 153 +# define SSL_R_FAILED_TO_INIT_ASYNC 405 +# define SSL_R_FRAGMENTED_CLIENT_HELLO 401 +# define SSL_R_GOT_A_FIN_BEFORE_A_CCS 154 +# define SSL_R_HTTPS_PROXY_REQUEST 155 +# define SSL_R_HTTP_REQUEST 156 +# define SSL_R_ILLEGAL_SUITEB_DIGEST 380 +# define SSL_R_INAPPROPRIATE_FALLBACK 373 +# define SSL_R_INCONSISTENT_COMPRESSION 340 +# define SSL_R_INCONSISTENT_EXTMS 104 +# define SSL_R_INVALID_COMMAND 280 +# define SSL_R_INVALID_COMPRESSION_ALGORITHM 341 +# define SSL_R_INVALID_CONFIGURATION_NAME 113 +# define SSL_R_INVALID_CT_VALIDATION_TYPE 212 +# define SSL_R_INVALID_NULL_CMD_NAME 385 +# define SSL_R_INVALID_SEQUENCE_NUMBER 402 +# define SSL_R_INVALID_SERVERINFO_DATA 388 +# define SSL_R_INVALID_SRP_USERNAME 357 +# define SSL_R_INVALID_STATUS_RESPONSE 328 +# define SSL_R_INVALID_TICKET_KEYS_LENGTH 325 +# define SSL_R_LENGTH_MISMATCH 159 +# define SSL_R_LENGTH_TOO_LONG 404 +# define SSL_R_LENGTH_TOO_SHORT 160 +# define SSL_R_LIBRARY_BUG 274 +# define SSL_R_LIBRARY_HAS_NO_CIPHERS 161 +# define SSL_R_MISSING_DSA_SIGNING_CERT 165 +# define SSL_R_MISSING_ECDSA_SIGNING_CERT 381 +# define SSL_R_MISSING_RSA_CERTIFICATE 168 +# define SSL_R_MISSING_RSA_ENCRYPTING_CERT 169 +# define SSL_R_MISSING_RSA_SIGNING_CERT 170 +# define SSL_R_MISSING_SRP_PARAM 358 +# define SSL_R_MISSING_TMP_DH_KEY 171 +# define SSL_R_MISSING_TMP_ECDH_KEY 311 +# define SSL_R_NO_CERTIFICATES_RETURNED 176 +# define SSL_R_NO_CERTIFICATE_ASSIGNED 177 +# define SSL_R_NO_CERTIFICATE_SET 179 +# define SSL_R_NO_CIPHERS_AVAILABLE 181 +# define SSL_R_NO_CIPHERS_SPECIFIED 183 +# define SSL_R_NO_CIPHER_MATCH 185 +# define SSL_R_NO_CLIENT_CERT_METHOD 331 +# define SSL_R_NO_COMPRESSION_SPECIFIED 187 +# define SSL_R_NO_GOST_CERTIFICATE_SENT_BY_PEER 330 +# define SSL_R_NO_METHOD_SPECIFIED 188 +# define SSL_R_NO_PEM_EXTENSIONS 389 +# define SSL_R_NO_PRIVATE_KEY_ASSIGNED 190 +# define SSL_R_NO_PROTOCOLS_AVAILABLE 191 +# define SSL_R_NO_RENEGOTIATION 339 +# define SSL_R_NO_REQUIRED_DIGEST 324 +# define SSL_R_NO_SHARED_CIPHER 193 +# define SSL_R_NO_SHARED_SIGNATURE_ALGORITHMS 376 +# define SSL_R_NO_SRTP_PROFILES 359 +# define SSL_R_NO_VALID_SCTS 216 +# define SSL_R_NO_VERIFY_COOKIE_CALLBACK 403 +# define SSL_R_NULL_SSL_CTX 195 +# define SSL_R_NULL_SSL_METHOD_PASSED 196 +# define SSL_R_OLD_SESSION_CIPHER_NOT_RETURNED 197 +# define SSL_R_OLD_SESSION_COMPRESSION_ALGORITHM_NOT_RETURNED 344 +# define SSL_R_PACKET_LENGTH_TOO_LONG 198 +# define SSL_R_PARSE_TLSEXT 227 +# define SSL_R_PATH_TOO_LONG 270 +# define SSL_R_PEER_DID_NOT_RETURN_A_CERTIFICATE 199 +# define SSL_R_PEM_NAME_BAD_PREFIX 391 +# define SSL_R_PEM_NAME_TOO_SHORT 392 +# define SSL_R_PIPELINE_FAILURE 406 +# define SSL_R_PROTOCOL_IS_SHUTDOWN 207 +# define SSL_R_PSK_IDENTITY_NOT_FOUND 223 +# define SSL_R_PSK_NO_CLIENT_CB 224 +# define SSL_R_PSK_NO_SERVER_CB 225 +# define SSL_R_READ_BIO_NOT_SET 211 +# define SSL_R_READ_TIMEOUT_EXPIRED 312 +# define SSL_R_RECORD_LENGTH_MISMATCH 213 +# define SSL_R_RECORD_TOO_SMALL 298 +# define SSL_R_RENEGOTIATE_EXT_TOO_LONG 335 +# define SSL_R_RENEGOTIATION_ENCODING_ERR 336 +# define SSL_R_RENEGOTIATION_MISMATCH 337 +# define SSL_R_REQUIRED_CIPHER_MISSING 215 +# define SSL_R_REQUIRED_COMPRESSION_ALGORITHM_MISSING 342 +# define SSL_R_SCSV_RECEIVED_WHEN_RENEGOTIATING 345 +# define SSL_R_SCT_VERIFICATION_FAILED 208 +# define SSL_R_SERVERHELLO_TLSEXT 275 +# define SSL_R_SESSION_ID_CONTEXT_UNINITIALIZED 277 +# define SSL_R_SHUTDOWN_WHILE_IN_INIT 407 +# define SSL_R_SIGNATURE_ALGORITHMS_ERROR 360 +# define SSL_R_SIGNATURE_FOR_NON_SIGNING_CERTIFICATE 220 +# define SSL_R_SRP_A_CALC 361 +# define SSL_R_SRTP_COULD_NOT_ALLOCATE_PROFILES 362 +# define SSL_R_SRTP_PROTECTION_PROFILE_LIST_TOO_LONG 363 +# define SSL_R_SRTP_UNKNOWN_PROTECTION_PROFILE 364 +# define SSL_R_SSL3_EXT_INVALID_SERVERNAME 319 +# define SSL_R_SSL3_EXT_INVALID_SERVERNAME_TYPE 320 +# define SSL_R_SSL3_SESSION_ID_TOO_LONG 300 +# define SSL_R_SSLV3_ALERT_BAD_CERTIFICATE 1042 +# define SSL_R_SSLV3_ALERT_BAD_RECORD_MAC 1020 +# define SSL_R_SSLV3_ALERT_CERTIFICATE_EXPIRED 1045 +# define SSL_R_SSLV3_ALERT_CERTIFICATE_REVOKED 1044 +# define SSL_R_SSLV3_ALERT_CERTIFICATE_UNKNOWN 1046 +# define SSL_R_SSLV3_ALERT_DECOMPRESSION_FAILURE 1030 +# define SSL_R_SSLV3_ALERT_HANDSHAKE_FAILURE 1040 +# define SSL_R_SSLV3_ALERT_ILLEGAL_PARAMETER 1047 +# define SSL_R_SSLV3_ALERT_NO_CERTIFICATE 1041 +# define SSL_R_SSLV3_ALERT_UNEXPECTED_MESSAGE 1010 +# define SSL_R_SSLV3_ALERT_UNSUPPORTED_CERTIFICATE 1043 +# define SSL_R_SSL_COMMAND_SECTION_EMPTY 117 +# define SSL_R_SSL_COMMAND_SECTION_NOT_FOUND 125 +# define SSL_R_SSL_CTX_HAS_NO_DEFAULT_SSL_VERSION 228 +# define SSL_R_SSL_HANDSHAKE_FAILURE 229 +# define SSL_R_SSL_LIBRARY_HAS_NO_CIPHERS 230 +# define SSL_R_SSL_NEGATIVE_LENGTH 372 +# define SSL_R_SSL_SECTION_EMPTY 126 +# define SSL_R_SSL_SECTION_NOT_FOUND 136 +# define SSL_R_SSL_SESSION_ID_CALLBACK_FAILED 301 +# define SSL_R_SSL_SESSION_ID_CONFLICT 302 +# define SSL_R_SSL_SESSION_ID_TOO_LONG 408 +# define SSL_R_SSL_SESSION_ID_CONTEXT_TOO_LONG 273 +# define SSL_R_SSL_SESSION_ID_HAS_BAD_LENGTH 303 +# define SSL_R_SSL_SESSION_VERSION_MISMATCH 210 +# define SSL_R_TLSV1_ALERT_ACCESS_DENIED 1049 +# define SSL_R_TLSV1_ALERT_DECODE_ERROR 1050 +# define SSL_R_TLSV1_ALERT_DECRYPTION_FAILED 1021 +# define SSL_R_TLSV1_ALERT_DECRYPT_ERROR 1051 +# define SSL_R_TLSV1_ALERT_EXPORT_RESTRICTION 1060 +# define SSL_R_TLSV1_ALERT_INAPPROPRIATE_FALLBACK 1086 +# define SSL_R_TLSV1_ALERT_INSUFFICIENT_SECURITY 1071 +# define SSL_R_TLSV1_ALERT_INTERNAL_ERROR 1080 +# define SSL_R_TLSV1_ALERT_NO_RENEGOTIATION 1100 +# define SSL_R_TLSV1_ALERT_PROTOCOL_VERSION 1070 +# define SSL_R_TLSV1_ALERT_RECORD_OVERFLOW 1022 +# define SSL_R_TLSV1_ALERT_UNKNOWN_CA 1048 +# define SSL_R_TLSV1_ALERT_USER_CANCELLED 1090 +# define SSL_R_TLSV1_BAD_CERTIFICATE_HASH_VALUE 1114 +# define SSL_R_TLSV1_BAD_CERTIFICATE_STATUS_RESPONSE 1113 +# define SSL_R_TLSV1_CERTIFICATE_UNOBTAINABLE 1111 +# define SSL_R_TLSV1_UNRECOGNIZED_NAME 1112 +# define SSL_R_TLSV1_UNSUPPORTED_EXTENSION 1110 +# define SSL_R_TLS_HEARTBEAT_PEER_DOESNT_ACCEPT 365 +# define SSL_R_TLS_HEARTBEAT_PENDING 366 +# define SSL_R_TLS_ILLEGAL_EXPORTER_LABEL 367 +# define SSL_R_TLS_INVALID_ECPOINTFORMAT_LIST 157 +# define SSL_R_TOO_MANY_WARN_ALERTS 409 +# define SSL_R_UNABLE_TO_FIND_ECDH_PARAMETERS 314 +# define SSL_R_UNABLE_TO_FIND_PUBLIC_KEY_PARAMETERS 239 +# define SSL_R_UNABLE_TO_LOAD_SSL3_MD5_ROUTINES 242 +# define SSL_R_UNABLE_TO_LOAD_SSL3_SHA1_ROUTINES 243 +# define SSL_R_UNEXPECTED_MESSAGE 244 +# define SSL_R_UNEXPECTED_RECORD 245 +# define SSL_R_UNINITIALIZED 276 +# define SSL_R_UNKNOWN_ALERT_TYPE 246 +# define SSL_R_UNKNOWN_CERTIFICATE_TYPE 247 +# define SSL_R_UNKNOWN_CIPHER_RETURNED 248 +# define SSL_R_UNKNOWN_CIPHER_TYPE 249 +# define SSL_R_UNKNOWN_CMD_NAME 386 +# define SSL_R_UNKNOWN_COMMAND 139 +# define SSL_R_UNKNOWN_DIGEST 368 +# define SSL_R_UNKNOWN_KEY_EXCHANGE_TYPE 250 +# define SSL_R_UNKNOWN_PKEY_TYPE 251 +# define SSL_R_UNKNOWN_PROTOCOL 252 +# define SSL_R_UNKNOWN_SSL_VERSION 254 +# define SSL_R_UNKNOWN_STATE 255 +# define SSL_R_UNSAFE_LEGACY_RENEGOTIATION_DISABLED 338 +# define SSL_R_UNSUPPORTED_COMPRESSION_ALGORITHM 257 +# define SSL_R_UNSUPPORTED_ELLIPTIC_CURVE 315 +# define SSL_R_UNSUPPORTED_PROTOCOL 258 +# define SSL_R_UNSUPPORTED_SSL_VERSION 259 +# define SSL_R_UNSUPPORTED_STATUS_TYPE 329 +# define SSL_R_USE_SRTP_NOT_NEGOTIATED 369 +# define SSL_R_VERSION_TOO_HIGH 166 +# define SSL_R_VERSION_TOO_LOW 396 +# define SSL_R_WRONG_CERTIFICATE_TYPE 383 +# define SSL_R_WRONG_CIPHER_RETURNED 261 +# define SSL_R_WRONG_CURVE 378 +# define SSL_R_WRONG_SIGNATURE_LENGTH 264 +# define SSL_R_WRONG_SIGNATURE_SIZE 265 +# define SSL_R_WRONG_SIGNATURE_TYPE 370 +# define SSL_R_WRONG_SSL_VERSION 266 +# define SSL_R_WRONG_VERSION_NUMBER 267 +# define SSL_R_X509_LIB 268 +# define SSL_R_X509_VERIFICATION_SETUP_PROBLEMS 269 + +# ifdef __cplusplus +} +# endif +#endif diff --git a/mobile/ios/FxA/FxA/include/openssl/ssl2.h b/mobile/ios/FxA/FxA/include/openssl/ssl2.h new file mode 100644 index 0000000000..5321bd272c --- /dev/null +++ b/mobile/ios/FxA/FxA/include/openssl/ssl2.h @@ -0,0 +1,24 @@ +/* + * Copyright 1995-2016 The OpenSSL Project Authors. All Rights Reserved. + * + * Licensed under the OpenSSL license (the "License"). You may not use + * this file except in compliance with the License. You can obtain a copy + * in the file LICENSE in the source distribution or at + * https://www.openssl.org/source/license.html + */ + +#ifndef HEADER_SSL2_H +# define HEADER_SSL2_H + +#ifdef __cplusplus +extern "C" { +#endif + +# define SSL2_VERSION 0x0002 + +# define SSL2_MT_CLIENT_HELLO 1 + +#ifdef __cplusplus +} +#endif +#endif diff --git a/mobile/ios/FxA/FxA/include/openssl/ssl3.h b/mobile/ios/FxA/FxA/include/openssl/ssl3.h new file mode 100644 index 0000000000..4ca434e760 --- /dev/null +++ b/mobile/ios/FxA/FxA/include/openssl/ssl3.h @@ -0,0 +1,310 @@ +/* + * Copyright 1995-2016 The OpenSSL Project Authors. All Rights Reserved. + * + * Licensed under the OpenSSL license (the "License"). You may not use + * this file except in compliance with the License. You can obtain a copy + * in the file LICENSE in the source distribution or at + * https://www.openssl.org/source/license.html + */ + +/* ==================================================================== + * Copyright 2002 Sun Microsystems, Inc. ALL RIGHTS RESERVED. + * ECC cipher suite support in OpenSSL originally developed by + * SUN MICROSYSTEMS, INC., and contributed to the OpenSSL project. + */ + +#ifndef HEADER_SSL3_H +# define HEADER_SSL3_H + +# include +# include +# include +# include + +#ifdef __cplusplus +extern "C" { +#endif + +/* + * Signalling cipher suite value from RFC 5746 + * (TLS_EMPTY_RENEGOTIATION_INFO_SCSV) + */ +# define SSL3_CK_SCSV 0x030000FF + +/* + * Signalling cipher suite value from draft-ietf-tls-downgrade-scsv-00 + * (TLS_FALLBACK_SCSV) + */ +# define SSL3_CK_FALLBACK_SCSV 0x03005600 + +# define SSL3_CK_RSA_NULL_MD5 0x03000001 +# define SSL3_CK_RSA_NULL_SHA 0x03000002 +# define SSL3_CK_RSA_RC4_40_MD5 0x03000003 +# define SSL3_CK_RSA_RC4_128_MD5 0x03000004 +# define SSL3_CK_RSA_RC4_128_SHA 0x03000005 +# define SSL3_CK_RSA_RC2_40_MD5 0x03000006 +# define SSL3_CK_RSA_IDEA_128_SHA 0x03000007 +# define SSL3_CK_RSA_DES_40_CBC_SHA 0x03000008 +# define SSL3_CK_RSA_DES_64_CBC_SHA 0x03000009 +# define SSL3_CK_RSA_DES_192_CBC3_SHA 0x0300000A + +# define SSL3_CK_DH_DSS_DES_40_CBC_SHA 0x0300000B +# define SSL3_CK_DH_DSS_DES_64_CBC_SHA 0x0300000C +# define SSL3_CK_DH_DSS_DES_192_CBC3_SHA 0x0300000D +# define SSL3_CK_DH_RSA_DES_40_CBC_SHA 0x0300000E +# define SSL3_CK_DH_RSA_DES_64_CBC_SHA 0x0300000F +# define SSL3_CK_DH_RSA_DES_192_CBC3_SHA 0x03000010 + +# define SSL3_CK_DHE_DSS_DES_40_CBC_SHA 0x03000011 +# define SSL3_CK_EDH_DSS_DES_40_CBC_SHA SSL3_CK_DHE_DSS_DES_40_CBC_SHA +# define SSL3_CK_DHE_DSS_DES_64_CBC_SHA 0x03000012 +# define SSL3_CK_EDH_DSS_DES_64_CBC_SHA SSL3_CK_DHE_DSS_DES_64_CBC_SHA +# define SSL3_CK_DHE_DSS_DES_192_CBC3_SHA 0x03000013 +# define SSL3_CK_EDH_DSS_DES_192_CBC3_SHA SSL3_CK_DHE_DSS_DES_192_CBC3_SHA +# define SSL3_CK_DHE_RSA_DES_40_CBC_SHA 0x03000014 +# define SSL3_CK_EDH_RSA_DES_40_CBC_SHA SSL3_CK_DHE_RSA_DES_40_CBC_SHA +# define SSL3_CK_DHE_RSA_DES_64_CBC_SHA 0x03000015 +# define SSL3_CK_EDH_RSA_DES_64_CBC_SHA SSL3_CK_DHE_RSA_DES_64_CBC_SHA +# define SSL3_CK_DHE_RSA_DES_192_CBC3_SHA 0x03000016 +# define SSL3_CK_EDH_RSA_DES_192_CBC3_SHA SSL3_CK_DHE_RSA_DES_192_CBC3_SHA + +# define SSL3_CK_ADH_RC4_40_MD5 0x03000017 +# define SSL3_CK_ADH_RC4_128_MD5 0x03000018 +# define SSL3_CK_ADH_DES_40_CBC_SHA 0x03000019 +# define SSL3_CK_ADH_DES_64_CBC_SHA 0x0300001A +# define SSL3_CK_ADH_DES_192_CBC_SHA 0x0300001B + +# define SSL3_TXT_RSA_NULL_MD5 "NULL-MD5" +# define SSL3_TXT_RSA_NULL_SHA "NULL-SHA" +# define SSL3_TXT_RSA_RC4_40_MD5 "EXP-RC4-MD5" +# define SSL3_TXT_RSA_RC4_128_MD5 "RC4-MD5" +# define SSL3_TXT_RSA_RC4_128_SHA "RC4-SHA" +# define SSL3_TXT_RSA_RC2_40_MD5 "EXP-RC2-CBC-MD5" +# define SSL3_TXT_RSA_IDEA_128_SHA "IDEA-CBC-SHA" +# define SSL3_TXT_RSA_DES_40_CBC_SHA "EXP-DES-CBC-SHA" +# define SSL3_TXT_RSA_DES_64_CBC_SHA "DES-CBC-SHA" +# define SSL3_TXT_RSA_DES_192_CBC3_SHA "DES-CBC3-SHA" + +# define SSL3_TXT_DH_DSS_DES_40_CBC_SHA "EXP-DH-DSS-DES-CBC-SHA" +# define SSL3_TXT_DH_DSS_DES_64_CBC_SHA "DH-DSS-DES-CBC-SHA" +# define SSL3_TXT_DH_DSS_DES_192_CBC3_SHA "DH-DSS-DES-CBC3-SHA" +# define SSL3_TXT_DH_RSA_DES_40_CBC_SHA "EXP-DH-RSA-DES-CBC-SHA" +# define SSL3_TXT_DH_RSA_DES_64_CBC_SHA "DH-RSA-DES-CBC-SHA" +# define SSL3_TXT_DH_RSA_DES_192_CBC3_SHA "DH-RSA-DES-CBC3-SHA" + +# define SSL3_TXT_DHE_DSS_DES_40_CBC_SHA "EXP-DHE-DSS-DES-CBC-SHA" +# define SSL3_TXT_DHE_DSS_DES_64_CBC_SHA "DHE-DSS-DES-CBC-SHA" +# define SSL3_TXT_DHE_DSS_DES_192_CBC3_SHA "DHE-DSS-DES-CBC3-SHA" +# define SSL3_TXT_DHE_RSA_DES_40_CBC_SHA "EXP-DHE-RSA-DES-CBC-SHA" +# define SSL3_TXT_DHE_RSA_DES_64_CBC_SHA "DHE-RSA-DES-CBC-SHA" +# define SSL3_TXT_DHE_RSA_DES_192_CBC3_SHA "DHE-RSA-DES-CBC3-SHA" + +/* + * This next block of six "EDH" labels is for backward compatibility with + * older versions of OpenSSL. New code should use the six "DHE" labels above + * instead: + */ +# define SSL3_TXT_EDH_DSS_DES_40_CBC_SHA "EXP-EDH-DSS-DES-CBC-SHA" +# define SSL3_TXT_EDH_DSS_DES_64_CBC_SHA "EDH-DSS-DES-CBC-SHA" +# define SSL3_TXT_EDH_DSS_DES_192_CBC3_SHA "EDH-DSS-DES-CBC3-SHA" +# define SSL3_TXT_EDH_RSA_DES_40_CBC_SHA "EXP-EDH-RSA-DES-CBC-SHA" +# define SSL3_TXT_EDH_RSA_DES_64_CBC_SHA "EDH-RSA-DES-CBC-SHA" +# define SSL3_TXT_EDH_RSA_DES_192_CBC3_SHA "EDH-RSA-DES-CBC3-SHA" + +# define SSL3_TXT_ADH_RC4_40_MD5 "EXP-ADH-RC4-MD5" +# define SSL3_TXT_ADH_RC4_128_MD5 "ADH-RC4-MD5" +# define SSL3_TXT_ADH_DES_40_CBC_SHA "EXP-ADH-DES-CBC-SHA" +# define SSL3_TXT_ADH_DES_64_CBC_SHA "ADH-DES-CBC-SHA" +# define SSL3_TXT_ADH_DES_192_CBC_SHA "ADH-DES-CBC3-SHA" + +# define SSL3_SSL_SESSION_ID_LENGTH 32 +# define SSL3_MAX_SSL_SESSION_ID_LENGTH 32 + +# define SSL3_MASTER_SECRET_SIZE 48 +# define SSL3_RANDOM_SIZE 32 +# define SSL3_SESSION_ID_SIZE 32 +# define SSL3_RT_HEADER_LENGTH 5 + +# define SSL3_HM_HEADER_LENGTH 4 + +# ifndef SSL3_ALIGN_PAYLOAD + /* + * Some will argue that this increases memory footprint, but it's not + * actually true. Point is that malloc has to return at least 64-bit aligned + * pointers, meaning that allocating 5 bytes wastes 3 bytes in either case. + * Suggested pre-gaping simply moves these wasted bytes from the end of + * allocated region to its front, but makes data payload aligned, which + * improves performance:-) + */ +# define SSL3_ALIGN_PAYLOAD 8 +# else +# if (SSL3_ALIGN_PAYLOAD&(SSL3_ALIGN_PAYLOAD-1))!=0 +# error "insane SSL3_ALIGN_PAYLOAD" +# undef SSL3_ALIGN_PAYLOAD +# endif +# endif + +/* + * This is the maximum MAC (digest) size used by the SSL library. Currently + * maximum of 20 is used by SHA1, but we reserve for future extension for + * 512-bit hashes. + */ + +# define SSL3_RT_MAX_MD_SIZE 64 + +/* + * Maximum block size used in all ciphersuites. Currently 16 for AES. + */ + +# define SSL_RT_MAX_CIPHER_BLOCK_SIZE 16 + +# define SSL3_RT_MAX_EXTRA (16384) + +/* Maximum plaintext length: defined by SSL/TLS standards */ +# define SSL3_RT_MAX_PLAIN_LENGTH 16384 +/* Maximum compression overhead: defined by SSL/TLS standards */ +# define SSL3_RT_MAX_COMPRESSED_OVERHEAD 1024 + +/* + * The standards give a maximum encryption overhead of 1024 bytes. In + * practice the value is lower than this. The overhead is the maximum number + * of padding bytes (256) plus the mac size. + */ +# define SSL3_RT_MAX_ENCRYPTED_OVERHEAD (256 + SSL3_RT_MAX_MD_SIZE) + +/* + * OpenSSL currently only uses a padding length of at most one block so the + * send overhead is smaller. + */ + +# define SSL3_RT_SEND_MAX_ENCRYPTED_OVERHEAD \ + (SSL_RT_MAX_CIPHER_BLOCK_SIZE + SSL3_RT_MAX_MD_SIZE) + +/* If compression isn't used don't include the compression overhead */ + +# ifdef OPENSSL_NO_COMP +# define SSL3_RT_MAX_COMPRESSED_LENGTH SSL3_RT_MAX_PLAIN_LENGTH +# else +# define SSL3_RT_MAX_COMPRESSED_LENGTH \ + (SSL3_RT_MAX_PLAIN_LENGTH+SSL3_RT_MAX_COMPRESSED_OVERHEAD) +# endif +# define SSL3_RT_MAX_ENCRYPTED_LENGTH \ + (SSL3_RT_MAX_ENCRYPTED_OVERHEAD+SSL3_RT_MAX_COMPRESSED_LENGTH) +# define SSL3_RT_MAX_PACKET_SIZE \ + (SSL3_RT_MAX_ENCRYPTED_LENGTH+SSL3_RT_HEADER_LENGTH) + +# define SSL3_MD_CLIENT_FINISHED_CONST "\x43\x4C\x4E\x54" +# define SSL3_MD_SERVER_FINISHED_CONST "\x53\x52\x56\x52" + +# define SSL3_VERSION 0x0300 +# define SSL3_VERSION_MAJOR 0x03 +# define SSL3_VERSION_MINOR 0x00 + +# define SSL3_RT_CHANGE_CIPHER_SPEC 20 +# define SSL3_RT_ALERT 21 +# define SSL3_RT_HANDSHAKE 22 +# define SSL3_RT_APPLICATION_DATA 23 +# define DTLS1_RT_HEARTBEAT 24 + +/* Pseudo content types to indicate additional parameters */ +# define TLS1_RT_CRYPTO 0x1000 +# define TLS1_RT_CRYPTO_PREMASTER (TLS1_RT_CRYPTO | 0x1) +# define TLS1_RT_CRYPTO_CLIENT_RANDOM (TLS1_RT_CRYPTO | 0x2) +# define TLS1_RT_CRYPTO_SERVER_RANDOM (TLS1_RT_CRYPTO | 0x3) +# define TLS1_RT_CRYPTO_MASTER (TLS1_RT_CRYPTO | 0x4) + +# define TLS1_RT_CRYPTO_READ 0x0000 +# define TLS1_RT_CRYPTO_WRITE 0x0100 +# define TLS1_RT_CRYPTO_MAC (TLS1_RT_CRYPTO | 0x5) +# define TLS1_RT_CRYPTO_KEY (TLS1_RT_CRYPTO | 0x6) +# define TLS1_RT_CRYPTO_IV (TLS1_RT_CRYPTO | 0x7) +# define TLS1_RT_CRYPTO_FIXED_IV (TLS1_RT_CRYPTO | 0x8) + +/* Pseudo content type for SSL/TLS header info */ +# define SSL3_RT_HEADER 0x100 + +# define SSL3_AL_WARNING 1 +# define SSL3_AL_FATAL 2 + +# define SSL3_AD_CLOSE_NOTIFY 0 +# define SSL3_AD_UNEXPECTED_MESSAGE 10/* fatal */ +# define SSL3_AD_BAD_RECORD_MAC 20/* fatal */ +# define SSL3_AD_DECOMPRESSION_FAILURE 30/* fatal */ +# define SSL3_AD_HANDSHAKE_FAILURE 40/* fatal */ +# define SSL3_AD_NO_CERTIFICATE 41 +# define SSL3_AD_BAD_CERTIFICATE 42 +# define SSL3_AD_UNSUPPORTED_CERTIFICATE 43 +# define SSL3_AD_CERTIFICATE_REVOKED 44 +# define SSL3_AD_CERTIFICATE_EXPIRED 45 +# define SSL3_AD_CERTIFICATE_UNKNOWN 46 +# define SSL3_AD_ILLEGAL_PARAMETER 47/* fatal */ + +# define TLS1_HB_REQUEST 1 +# define TLS1_HB_RESPONSE 2 + + +# define SSL3_CT_RSA_SIGN 1 +# define SSL3_CT_DSS_SIGN 2 +# define SSL3_CT_RSA_FIXED_DH 3 +# define SSL3_CT_DSS_FIXED_DH 4 +# define SSL3_CT_RSA_EPHEMERAL_DH 5 +# define SSL3_CT_DSS_EPHEMERAL_DH 6 +# define SSL3_CT_FORTEZZA_DMS 20 +/* + * SSL3_CT_NUMBER is used to size arrays and it must be large enough to + * contain all of the cert types defined either for SSLv3 and TLSv1. + */ +# define SSL3_CT_NUMBER 9 + +# define SSL3_FLAGS_NO_RENEGOTIATE_CIPHERS 0x0001 + +/* Removed from OpenSSL 1.1.0 */ +# define TLS1_FLAGS_TLS_PADDING_BUG 0x0 + +# define TLS1_FLAGS_SKIP_CERT_VERIFY 0x0010 + +/* Set if we encrypt then mac instead of usual mac then encrypt */ +# define TLS1_FLAGS_ENCRYPT_THEN_MAC_READ 0x0100 +# define TLS1_FLAGS_ENCRYPT_THEN_MAC TLS1_FLAGS_ENCRYPT_THEN_MAC_READ + +/* Set if extended master secret extension received from peer */ +# define TLS1_FLAGS_RECEIVED_EXTMS 0x0200 + +# define TLS1_FLAGS_ENCRYPT_THEN_MAC_WRITE 0x0400 + +# define SSL3_MT_HELLO_REQUEST 0 +# define SSL3_MT_CLIENT_HELLO 1 +# define SSL3_MT_SERVER_HELLO 2 +# define SSL3_MT_NEWSESSION_TICKET 4 +# define SSL3_MT_CERTIFICATE 11 +# define SSL3_MT_SERVER_KEY_EXCHANGE 12 +# define SSL3_MT_CERTIFICATE_REQUEST 13 +# define SSL3_MT_SERVER_DONE 14 +# define SSL3_MT_CERTIFICATE_VERIFY 15 +# define SSL3_MT_CLIENT_KEY_EXCHANGE 16 +# define SSL3_MT_FINISHED 20 +# define SSL3_MT_CERTIFICATE_STATUS 22 +# ifndef OPENSSL_NO_NEXTPROTONEG +# define SSL3_MT_NEXT_PROTO 67 +# endif +# define DTLS1_MT_HELLO_VERIFY_REQUEST 3 + +/* Dummy message type for handling CCS like a normal handshake message */ +# define SSL3_MT_CHANGE_CIPHER_SPEC 0x0101 + +# define SSL3_MT_CCS 1 + +/* These are used when changing over to a new cipher */ +# define SSL3_CC_READ 0x01 +# define SSL3_CC_WRITE 0x02 +# define SSL3_CC_CLIENT 0x10 +# define SSL3_CC_SERVER 0x20 +# define SSL3_CHANGE_CIPHER_CLIENT_WRITE (SSL3_CC_CLIENT|SSL3_CC_WRITE) +# define SSL3_CHANGE_CIPHER_SERVER_READ (SSL3_CC_SERVER|SSL3_CC_READ) +# define SSL3_CHANGE_CIPHER_CLIENT_READ (SSL3_CC_CLIENT|SSL3_CC_READ) +# define SSL3_CHANGE_CIPHER_SERVER_WRITE (SSL3_CC_SERVER|SSL3_CC_WRITE) + +#ifdef __cplusplus +} +#endif +#endif diff --git a/mobile/ios/FxA/FxA/include/openssl/stack.h b/mobile/ios/FxA/FxA/include/openssl/stack.h new file mode 100644 index 0000000000..23ad3b89f9 --- /dev/null +++ b/mobile/ios/FxA/FxA/include/openssl/stack.h @@ -0,0 +1,78 @@ +/* + * Copyright 1995-2016 The OpenSSL Project Authors. All Rights Reserved. + * + * Licensed under the OpenSSL license (the "License"). You may not use + * this file except in compliance with the License. You can obtain a copy + * in the file LICENSE in the source distribution or at + * https://www.openssl.org/source/license.html + */ + +#ifndef HEADER_STACK_H +# define HEADER_STACK_H + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct stack_st OPENSSL_STACK; /* Use STACK_OF(...) instead */ + +typedef int (*OPENSSL_sk_compfunc)(const void *, const void *); +typedef void (*OPENSSL_sk_freefunc)(void *); +typedef void *(*OPENSSL_sk_copyfunc)(const void *); + +int OPENSSL_sk_num(const OPENSSL_STACK *); +void *OPENSSL_sk_value(const OPENSSL_STACK *, int); + +void *OPENSSL_sk_set(OPENSSL_STACK *st, int i, const void *data); + +OPENSSL_STACK *OPENSSL_sk_new(OPENSSL_sk_compfunc cmp); +OPENSSL_STACK *OPENSSL_sk_new_null(void); +void OPENSSL_sk_free(OPENSSL_STACK *); +void OPENSSL_sk_pop_free(OPENSSL_STACK *st, void (*func) (void *)); +OPENSSL_STACK *OPENSSL_sk_deep_copy(const OPENSSL_STACK *, OPENSSL_sk_copyfunc c, OPENSSL_sk_freefunc f); +int OPENSSL_sk_insert(OPENSSL_STACK *sk, const void *data, int where); +void *OPENSSL_sk_delete(OPENSSL_STACK *st, int loc); +void *OPENSSL_sk_delete_ptr(OPENSSL_STACK *st, const void *p); +int OPENSSL_sk_find(OPENSSL_STACK *st, const void *data); +int OPENSSL_sk_find_ex(OPENSSL_STACK *st, const void *data); +int OPENSSL_sk_push(OPENSSL_STACK *st, const void *data); +int OPENSSL_sk_unshift(OPENSSL_STACK *st, const void *data); +void *OPENSSL_sk_shift(OPENSSL_STACK *st); +void *OPENSSL_sk_pop(OPENSSL_STACK *st); +void OPENSSL_sk_zero(OPENSSL_STACK *st); +OPENSSL_sk_compfunc OPENSSL_sk_set_cmp_func(OPENSSL_STACK *sk, OPENSSL_sk_compfunc cmp); +OPENSSL_STACK *OPENSSL_sk_dup(const OPENSSL_STACK *st); +void OPENSSL_sk_sort(OPENSSL_STACK *st); +int OPENSSL_sk_is_sorted(const OPENSSL_STACK *st); + +# if OPENSSL_API_COMPAT < 0x10100000L +# define _STACK OPENSSL_STACK +# define sk_num OPENSSL_sk_num +# define sk_value OPENSSL_sk_value +# define sk_set OPENSSL_sk_set +# define sk_new OPENSSL_sk_new +# define sk_new_null OPENSSL_sk_new_null +# define sk_free OPENSSL_sk_free +# define sk_pop_free OPENSSL_sk_pop_free +# define sk_deep_copy OPENSSL_sk_deep_copy +# define sk_insert OPENSSL_sk_insert +# define sk_delete OPENSSL_sk_delete +# define sk_delete_ptr OPENSSL_sk_delete_ptr +# define sk_find OPENSSL_sk_find +# define sk_find_ex OPENSSL_sk_find_ex +# define sk_push OPENSSL_sk_push +# define sk_unshift OPENSSL_sk_unshift +# define sk_shift OPENSSL_sk_shift +# define sk_pop OPENSSL_sk_pop +# define sk_zero OPENSSL_sk_zero +# define sk_set_cmp_func OPENSSL_sk_set_cmp_func +# define sk_dup OPENSSL_sk_dup +# define sk_sort OPENSSL_sk_sort +# define sk_is_sorted OPENSSL_sk_is_sorted +# endif + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/mobile/ios/FxA/FxA/include/openssl/symhacks.h b/mobile/ios/FxA/FxA/include/openssl/symhacks.h new file mode 100644 index 0000000000..caf1f1a75d --- /dev/null +++ b/mobile/ios/FxA/FxA/include/openssl/symhacks.h @@ -0,0 +1,52 @@ +/* + * Copyright 1999-2016 The OpenSSL Project Authors. All Rights Reserved. + * + * Licensed under the OpenSSL license (the "License"). You may not use + * this file except in compliance with the License. You can obtain a copy + * in the file LICENSE in the source distribution or at + * https://www.openssl.org/source/license.html + */ + +#ifndef HEADER_SYMHACKS_H +# define HEADER_SYMHACKS_H + +# include + +/* Case insensitive linking causes problems.... */ +# if defined(OPENSSL_SYS_VMS) +# undef ERR_load_CRYPTO_strings +# define ERR_load_CRYPTO_strings ERR_load_CRYPTOlib_strings +# undef OCSP_crlID_new +# define OCSP_crlID_new OCSP_crlID2_new + +# undef d2i_ECPARAMETERS +# define d2i_ECPARAMETERS d2i_UC_ECPARAMETERS +# undef i2d_ECPARAMETERS +# define i2d_ECPARAMETERS i2d_UC_ECPARAMETERS +# undef d2i_ECPKPARAMETERS +# define d2i_ECPKPARAMETERS d2i_UC_ECPKPARAMETERS +# undef i2d_ECPKPARAMETERS +# define i2d_ECPKPARAMETERS i2d_UC_ECPKPARAMETERS + +/* + * These functions do not seem to exist! However, I'm paranoid... Original + * command in x509v3.h: These functions are being redefined in another + * directory, and clash when the linker is case-insensitive, so let's hide + * them a little, by giving them an extra 'o' at the beginning of the name... + */ +# undef X509v3_cleanup_extensions +# define X509v3_cleanup_extensions oX509v3_cleanup_extensions +# undef X509v3_add_extension +# define X509v3_add_extension oX509v3_add_extension +# undef X509v3_add_netscape_extensions +# define X509v3_add_netscape_extensions oX509v3_add_netscape_extensions +# undef X509v3_add_standard_extensions +# define X509v3_add_standard_extensions oX509v3_add_standard_extensions + +/* This one clashes with CMS_data_create */ +# undef cms_Data_create +# define cms_Data_create priv_cms_Data_create + +# endif + +#endif /* ! defined HEADER_VMS_IDHACKS_H */ diff --git a/mobile/ios/FxA/FxA/include/openssl/tls1.h b/mobile/ios/FxA/FxA/include/openssl/tls1.h new file mode 100644 index 0000000000..23e382cdd3 --- /dev/null +++ b/mobile/ios/FxA/FxA/include/openssl/tls1.h @@ -0,0 +1,972 @@ +/* + * Copyright 1995-2016 The OpenSSL Project Authors. All Rights Reserved. + * + * Licensed under the OpenSSL license (the "License"). You may not use + * this file except in compliance with the License. You can obtain a copy + * in the file LICENSE in the source distribution or at + * https://www.openssl.org/source/license.html + */ + +/* ==================================================================== + * Copyright 2002 Sun Microsystems, Inc. ALL RIGHTS RESERVED. + * + * Portions of the attached software ("Contribution") are developed by + * SUN MICROSYSTEMS, INC., and are contributed to the OpenSSL project. + * + * The Contribution is licensed pursuant to the OpenSSL open source + * license provided above. + * + * ECC cipher suite support in OpenSSL originally written by + * Vipul Gupta and Sumit Gupta of Sun Microsystems Laboratories. + * + */ +/* ==================================================================== + * Copyright 2005 Nokia. All rights reserved. + * + * The portions of the attached software ("Contribution") is developed by + * Nokia Corporation and is licensed pursuant to the OpenSSL open source + * license. + * + * The Contribution, originally written by Mika Kousa and Pasi Eronen of + * Nokia Corporation, consists of the "PSK" (Pre-Shared Key) ciphersuites + * support (see RFC 4279) to OpenSSL. + * + * No patent licenses or other rights except those expressly stated in + * the OpenSSL open source license shall be deemed granted or received + * expressly, by implication, estoppel, or otherwise. + * + * No assurances are provided by Nokia that the Contribution does not + * infringe the patent or other intellectual property rights of any third + * party or that the license provides you with all the necessary rights + * to make use of the Contribution. + * + * THE SOFTWARE IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND. IN + * ADDITION TO THE DISCLAIMERS INCLUDED IN THE LICENSE, NOKIA + * SPECIFICALLY DISCLAIMS ANY LIABILITY FOR CLAIMS BROUGHT BY YOU OR ANY + * OTHER ENTITY BASED ON INFRINGEMENT OF INTELLECTUAL PROPERTY RIGHTS OR + * OTHERWISE. + */ + +#ifndef HEADER_TLS1_H +# define HEADER_TLS1_H + +# include +# include + +#ifdef __cplusplus +extern "C" { +#endif + +/* Default security level if not overridden at config time */ +# ifndef OPENSSL_TLS_SECURITY_LEVEL +# define OPENSSL_TLS_SECURITY_LEVEL 1 +# endif + +# define TLS1_VERSION 0x0301 +# define TLS1_1_VERSION 0x0302 +# define TLS1_2_VERSION 0x0303 +# define TLS_MAX_VERSION TLS1_2_VERSION + +/* Special value for method supporting multiple versions */ +# define TLS_ANY_VERSION 0x10000 + +# define TLS1_VERSION_MAJOR 0x03 +# define TLS1_VERSION_MINOR 0x01 + +# define TLS1_1_VERSION_MAJOR 0x03 +# define TLS1_1_VERSION_MINOR 0x02 + +# define TLS1_2_VERSION_MAJOR 0x03 +# define TLS1_2_VERSION_MINOR 0x03 + +# define TLS1_get_version(s) \ + ((SSL_version(s) >> 8) == TLS1_VERSION_MAJOR ? SSL_version(s) : 0) + +# define TLS1_get_client_version(s) \ + ((SSL_client_version(s) >> 8) == TLS1_VERSION_MAJOR ? SSL_client_version(s) : 0) + +# define TLS1_AD_DECRYPTION_FAILED 21 +# define TLS1_AD_RECORD_OVERFLOW 22 +# define TLS1_AD_UNKNOWN_CA 48/* fatal */ +# define TLS1_AD_ACCESS_DENIED 49/* fatal */ +# define TLS1_AD_DECODE_ERROR 50/* fatal */ +# define TLS1_AD_DECRYPT_ERROR 51 +# define TLS1_AD_EXPORT_RESTRICTION 60/* fatal */ +# define TLS1_AD_PROTOCOL_VERSION 70/* fatal */ +# define TLS1_AD_INSUFFICIENT_SECURITY 71/* fatal */ +# define TLS1_AD_INTERNAL_ERROR 80/* fatal */ +# define TLS1_AD_INAPPROPRIATE_FALLBACK 86/* fatal */ +# define TLS1_AD_USER_CANCELLED 90 +# define TLS1_AD_NO_RENEGOTIATION 100 +/* codes 110-114 are from RFC3546 */ +# define TLS1_AD_UNSUPPORTED_EXTENSION 110 +# define TLS1_AD_CERTIFICATE_UNOBTAINABLE 111 +# define TLS1_AD_UNRECOGNIZED_NAME 112 +# define TLS1_AD_BAD_CERTIFICATE_STATUS_RESPONSE 113 +# define TLS1_AD_BAD_CERTIFICATE_HASH_VALUE 114 +# define TLS1_AD_UNKNOWN_PSK_IDENTITY 115/* fatal */ +# define TLS1_AD_NO_APPLICATION_PROTOCOL 120 /* fatal */ + +/* ExtensionType values from RFC3546 / RFC4366 / RFC6066 */ +# define TLSEXT_TYPE_server_name 0 +# define TLSEXT_TYPE_max_fragment_length 1 +# define TLSEXT_TYPE_client_certificate_url 2 +# define TLSEXT_TYPE_trusted_ca_keys 3 +# define TLSEXT_TYPE_truncated_hmac 4 +# define TLSEXT_TYPE_status_request 5 +/* ExtensionType values from RFC4681 */ +# define TLSEXT_TYPE_user_mapping 6 +/* ExtensionType values from RFC5878 */ +# define TLSEXT_TYPE_client_authz 7 +# define TLSEXT_TYPE_server_authz 8 +/* ExtensionType values from RFC6091 */ +# define TLSEXT_TYPE_cert_type 9 + +/* ExtensionType values from RFC4492 */ +# define TLSEXT_TYPE_elliptic_curves 10 +# define TLSEXT_TYPE_ec_point_formats 11 + +/* ExtensionType value from RFC5054 */ +# define TLSEXT_TYPE_srp 12 + +/* ExtensionType values from RFC5246 */ +# define TLSEXT_TYPE_signature_algorithms 13 + +/* ExtensionType value from RFC5764 */ +# define TLSEXT_TYPE_use_srtp 14 + +/* ExtensionType value from RFC5620 */ +# define TLSEXT_TYPE_heartbeat 15 + +/* ExtensionType value from RFC7301 */ +# define TLSEXT_TYPE_application_layer_protocol_negotiation 16 + +/* + * Extension type for Certificate Transparency + * https://tools.ietf.org/html/rfc6962#section-3.3.1 + */ +# define TLSEXT_TYPE_signed_certificate_timestamp 18 + +/* + * ExtensionType value for TLS padding extension. + * http://tools.ietf.org/html/draft-agl-tls-padding + */ +# define TLSEXT_TYPE_padding 21 + +/* ExtensionType value from RFC7366 */ +# define TLSEXT_TYPE_encrypt_then_mac 22 + +/* ExtensionType value from RFC7627 */ +# define TLSEXT_TYPE_extended_master_secret 23 + +/* ExtensionType value from RFC4507 */ +# define TLSEXT_TYPE_session_ticket 35 + +/* Temporary extension type */ +# define TLSEXT_TYPE_renegotiate 0xff01 + +# ifndef OPENSSL_NO_NEXTPROTONEG +/* This is not an IANA defined extension number */ +# define TLSEXT_TYPE_next_proto_neg 13172 +# endif + +/* NameType value from RFC3546 */ +# define TLSEXT_NAMETYPE_host_name 0 +/* status request value from RFC3546 */ +# define TLSEXT_STATUSTYPE_ocsp 1 + +/* ECPointFormat values from RFC4492 */ +# define TLSEXT_ECPOINTFORMAT_first 0 +# define TLSEXT_ECPOINTFORMAT_uncompressed 0 +# define TLSEXT_ECPOINTFORMAT_ansiX962_compressed_prime 1 +# define TLSEXT_ECPOINTFORMAT_ansiX962_compressed_char2 2 +# define TLSEXT_ECPOINTFORMAT_last 2 + +/* Signature and hash algorithms from RFC5246 */ +# define TLSEXT_signature_anonymous 0 +# define TLSEXT_signature_rsa 1 +# define TLSEXT_signature_dsa 2 +# define TLSEXT_signature_ecdsa 3 +# define TLSEXT_signature_gostr34102001 237 +# define TLSEXT_signature_gostr34102012_256 238 +# define TLSEXT_signature_gostr34102012_512 239 + +/* Total number of different signature algorithms */ +# define TLSEXT_signature_num 7 + +# define TLSEXT_hash_none 0 +# define TLSEXT_hash_md5 1 +# define TLSEXT_hash_sha1 2 +# define TLSEXT_hash_sha224 3 +# define TLSEXT_hash_sha256 4 +# define TLSEXT_hash_sha384 5 +# define TLSEXT_hash_sha512 6 +# define TLSEXT_hash_gostr3411 237 +# define TLSEXT_hash_gostr34112012_256 238 +# define TLSEXT_hash_gostr34112012_512 239 + +/* Total number of different digest algorithms */ + +# define TLSEXT_hash_num 10 + +/* Flag set for unrecognised algorithms */ +# define TLSEXT_nid_unknown 0x1000000 + +/* ECC curves */ + +# define TLSEXT_curve_P_256 23 +# define TLSEXT_curve_P_384 24 + +# define TLSEXT_MAXLEN_host_name 255 + +__owur const char *SSL_get_servername(const SSL *s, const int type); +__owur int SSL_get_servername_type(const SSL *s); +/* + * SSL_export_keying_material exports a value derived from the master secret, + * as specified in RFC 5705. It writes |olen| bytes to |out| given a label and + * optional context. (Since a zero length context is allowed, the |use_context| + * flag controls whether a context is included.) It returns 1 on success and + * zero otherwise. + */ +__owur int SSL_export_keying_material(SSL *s, unsigned char *out, size_t olen, + const char *label, size_t llen, + const unsigned char *p, size_t plen, + int use_context); + +int SSL_get_sigalgs(SSL *s, int idx, + int *psign, int *phash, int *psignandhash, + unsigned char *rsig, unsigned char *rhash); + +int SSL_get_shared_sigalgs(SSL *s, int idx, + int *psign, int *phash, int *psignandhash, + unsigned char *rsig, unsigned char *rhash); + +__owur int SSL_check_chain(SSL *s, X509 *x, EVP_PKEY *pk, STACK_OF(X509) *chain); + +# define SSL_set_tlsext_host_name(s,name) \ +SSL_ctrl(s,SSL_CTRL_SET_TLSEXT_HOSTNAME,TLSEXT_NAMETYPE_host_name,(char *)name) + +# define SSL_set_tlsext_debug_callback(ssl, cb) \ +SSL_callback_ctrl(ssl,SSL_CTRL_SET_TLSEXT_DEBUG_CB,(void (*)(void))cb) + +# define SSL_set_tlsext_debug_arg(ssl, arg) \ +SSL_ctrl(ssl,SSL_CTRL_SET_TLSEXT_DEBUG_ARG,0, (void *)arg) + +# define SSL_get_tlsext_status_type(ssl) \ +SSL_ctrl(ssl,SSL_CTRL_GET_TLSEXT_STATUS_REQ_TYPE,0, NULL) + +# define SSL_set_tlsext_status_type(ssl, type) \ +SSL_ctrl(ssl,SSL_CTRL_SET_TLSEXT_STATUS_REQ_TYPE,type, NULL) + +# define SSL_get_tlsext_status_exts(ssl, arg) \ +SSL_ctrl(ssl,SSL_CTRL_GET_TLSEXT_STATUS_REQ_EXTS,0, (void *)arg) + +# define SSL_set_tlsext_status_exts(ssl, arg) \ +SSL_ctrl(ssl,SSL_CTRL_SET_TLSEXT_STATUS_REQ_EXTS,0, (void *)arg) + +# define SSL_get_tlsext_status_ids(ssl, arg) \ +SSL_ctrl(ssl,SSL_CTRL_GET_TLSEXT_STATUS_REQ_IDS,0, (void *)arg) + +# define SSL_set_tlsext_status_ids(ssl, arg) \ +SSL_ctrl(ssl,SSL_CTRL_SET_TLSEXT_STATUS_REQ_IDS,0, (void *)arg) + +# define SSL_get_tlsext_status_ocsp_resp(ssl, arg) \ +SSL_ctrl(ssl,SSL_CTRL_GET_TLSEXT_STATUS_REQ_OCSP_RESP,0, (void *)arg) + +# define SSL_set_tlsext_status_ocsp_resp(ssl, arg, arglen) \ +SSL_ctrl(ssl,SSL_CTRL_SET_TLSEXT_STATUS_REQ_OCSP_RESP,arglen, (void *)arg) + +# define SSL_CTX_set_tlsext_servername_callback(ctx, cb) \ +SSL_CTX_callback_ctrl(ctx,SSL_CTRL_SET_TLSEXT_SERVERNAME_CB,(void (*)(void))cb) + +# define SSL_TLSEXT_ERR_OK 0 +# define SSL_TLSEXT_ERR_ALERT_WARNING 1 +# define SSL_TLSEXT_ERR_ALERT_FATAL 2 +# define SSL_TLSEXT_ERR_NOACK 3 + +# define SSL_CTX_set_tlsext_servername_arg(ctx, arg) \ +SSL_CTX_ctrl(ctx,SSL_CTRL_SET_TLSEXT_SERVERNAME_ARG,0, (void *)arg) + +# define SSL_CTX_get_tlsext_ticket_keys(ctx, keys, keylen) \ + SSL_CTX_ctrl((ctx),SSL_CTRL_GET_TLSEXT_TICKET_KEYS,(keylen),(keys)) +# define SSL_CTX_set_tlsext_ticket_keys(ctx, keys, keylen) \ + SSL_CTX_ctrl((ctx),SSL_CTRL_SET_TLSEXT_TICKET_KEYS,(keylen),(keys)) + +# define SSL_CTX_get_tlsext_status_cb(ssl, cb) \ +SSL_CTX_ctrl(ssl,SSL_CTRL_GET_TLSEXT_STATUS_REQ_CB,0, (void (**)(void))cb) +# define SSL_CTX_set_tlsext_status_cb(ssl, cb) \ +SSL_CTX_callback_ctrl(ssl,SSL_CTRL_SET_TLSEXT_STATUS_REQ_CB,(void (*)(void))cb) + +# define SSL_CTX_get_tlsext_status_arg(ssl, arg) \ +SSL_CTX_ctrl(ssl,SSL_CTRL_GET_TLSEXT_STATUS_REQ_CB_ARG,0, (void *)arg +# define SSL_CTX_set_tlsext_status_arg(ssl, arg) \ +SSL_CTX_ctrl(ssl,SSL_CTRL_SET_TLSEXT_STATUS_REQ_CB_ARG,0, (void *)arg) + +#define SSL_CTX_set_tlsext_status_type(ssl, type) \ + SSL_CTX_ctrl(ssl, SSL_CTRL_SET_TLSEXT_STATUS_REQ_TYPE, type, NULL) + +#define SSL_CTX_get_tlsext_status_type(ssl) \ + SSL_CTX_ctrl(ssl, SSL_CTRL_GET_TLSEXT_STATUS_REQ_TYPE, 0, NULL) + +# define SSL_CTX_set_tlsext_ticket_key_cb(ssl, cb) \ +SSL_CTX_callback_ctrl(ssl,SSL_CTRL_SET_TLSEXT_TICKET_KEY_CB,(void (*)(void))cb) + +# ifndef OPENSSL_NO_HEARTBEATS +# define SSL_DTLSEXT_HB_ENABLED 0x01 +# define SSL_DTLSEXT_HB_DONT_SEND_REQUESTS 0x02 +# define SSL_DTLSEXT_HB_DONT_RECV_REQUESTS 0x04 +# define SSL_get_dtlsext_heartbeat_pending(ssl) \ + SSL_ctrl((ssl),SSL_CTRL_GET_DTLS_EXT_HEARTBEAT_PENDING,0,NULL) +# define SSL_set_dtlsext_heartbeat_no_requests(ssl, arg) \ + SSL_ctrl((ssl),SSL_CTRL_SET_DTLS_EXT_HEARTBEAT_NO_REQUESTS,arg,NULL) + +# if OPENSSL_API_COMPAT < 0x10100000L +# define SSL_CTRL_TLS_EXT_SEND_HEARTBEAT \ + SSL_CTRL_DTLS_EXT_SEND_HEARTBEAT +# define SSL_CTRL_GET_TLS_EXT_HEARTBEAT_PENDING \ + SSL_CTRL_GET_DTLS_EXT_HEARTBEAT_PENDING +# define SSL_CTRL_SET_TLS_EXT_HEARTBEAT_NO_REQUESTS \ + SSL_CTRL_SET_DTLS_EXT_HEARTBEAT_NO_REQUESTS +# define SSL_TLSEXT_HB_ENABLED \ + SSL_DTLSEXT_HB_ENABLED +# define SSL_TLSEXT_HB_DONT_SEND_REQUESTS \ + SSL_DTLSEXT_HB_DONT_SEND_REQUESTS +# define SSL_TLSEXT_HB_DONT_RECV_REQUESTS \ + SSL_DTLSEXT_HB_DONT_RECV_REQUESTS +# define SSL_get_tlsext_heartbeat_pending(ssl) \ + SSL_get_dtlsext_heartbeat_pending(ssl) +# define SSL_set_tlsext_heartbeat_no_requests(ssl, arg) \ + SSL_set_dtlsext_heartbeat_no_requests(ssl, arg) +# endif +# endif + +/* PSK ciphersuites from 4279 */ +# define TLS1_CK_PSK_WITH_RC4_128_SHA 0x0300008A +# define TLS1_CK_PSK_WITH_3DES_EDE_CBC_SHA 0x0300008B +# define TLS1_CK_PSK_WITH_AES_128_CBC_SHA 0x0300008C +# define TLS1_CK_PSK_WITH_AES_256_CBC_SHA 0x0300008D + +# define TLS1_CK_DHE_PSK_WITH_RC4_128_SHA 0x0300008E +# define TLS1_CK_DHE_PSK_WITH_3DES_EDE_CBC_SHA 0x0300008F +# define TLS1_CK_DHE_PSK_WITH_AES_128_CBC_SHA 0x03000090 +# define TLS1_CK_DHE_PSK_WITH_AES_256_CBC_SHA 0x03000091 + +# define TLS1_CK_RSA_PSK_WITH_RC4_128_SHA 0x03000092 +# define TLS1_CK_RSA_PSK_WITH_3DES_EDE_CBC_SHA 0x03000093 +# define TLS1_CK_RSA_PSK_WITH_AES_128_CBC_SHA 0x03000094 +# define TLS1_CK_RSA_PSK_WITH_AES_256_CBC_SHA 0x03000095 + +/* PSK ciphersuites from 5487 */ +# define TLS1_CK_PSK_WITH_AES_128_GCM_SHA256 0x030000A8 +# define TLS1_CK_PSK_WITH_AES_256_GCM_SHA384 0x030000A9 +# define TLS1_CK_DHE_PSK_WITH_AES_128_GCM_SHA256 0x030000AA +# define TLS1_CK_DHE_PSK_WITH_AES_256_GCM_SHA384 0x030000AB +# define TLS1_CK_RSA_PSK_WITH_AES_128_GCM_SHA256 0x030000AC +# define TLS1_CK_RSA_PSK_WITH_AES_256_GCM_SHA384 0x030000AD + +# define TLS1_CK_PSK_WITH_AES_128_CBC_SHA256 0x030000AE +# define TLS1_CK_PSK_WITH_AES_256_CBC_SHA384 0x030000AF +# define TLS1_CK_PSK_WITH_NULL_SHA256 0x030000B0 +# define TLS1_CK_PSK_WITH_NULL_SHA384 0x030000B1 + +# define TLS1_CK_DHE_PSK_WITH_AES_128_CBC_SHA256 0x030000B2 +# define TLS1_CK_DHE_PSK_WITH_AES_256_CBC_SHA384 0x030000B3 +# define TLS1_CK_DHE_PSK_WITH_NULL_SHA256 0x030000B4 +# define TLS1_CK_DHE_PSK_WITH_NULL_SHA384 0x030000B5 + +# define TLS1_CK_RSA_PSK_WITH_AES_128_CBC_SHA256 0x030000B6 +# define TLS1_CK_RSA_PSK_WITH_AES_256_CBC_SHA384 0x030000B7 +# define TLS1_CK_RSA_PSK_WITH_NULL_SHA256 0x030000B8 +# define TLS1_CK_RSA_PSK_WITH_NULL_SHA384 0x030000B9 + +/* NULL PSK ciphersuites from RFC4785 */ +# define TLS1_CK_PSK_WITH_NULL_SHA 0x0300002C +# define TLS1_CK_DHE_PSK_WITH_NULL_SHA 0x0300002D +# define TLS1_CK_RSA_PSK_WITH_NULL_SHA 0x0300002E + +/* AES ciphersuites from RFC3268 */ +# define TLS1_CK_RSA_WITH_AES_128_SHA 0x0300002F +# define TLS1_CK_DH_DSS_WITH_AES_128_SHA 0x03000030 +# define TLS1_CK_DH_RSA_WITH_AES_128_SHA 0x03000031 +# define TLS1_CK_DHE_DSS_WITH_AES_128_SHA 0x03000032 +# define TLS1_CK_DHE_RSA_WITH_AES_128_SHA 0x03000033 +# define TLS1_CK_ADH_WITH_AES_128_SHA 0x03000034 + +# define TLS1_CK_RSA_WITH_AES_256_SHA 0x03000035 +# define TLS1_CK_DH_DSS_WITH_AES_256_SHA 0x03000036 +# define TLS1_CK_DH_RSA_WITH_AES_256_SHA 0x03000037 +# define TLS1_CK_DHE_DSS_WITH_AES_256_SHA 0x03000038 +# define TLS1_CK_DHE_RSA_WITH_AES_256_SHA 0x03000039 +# define TLS1_CK_ADH_WITH_AES_256_SHA 0x0300003A + +/* TLS v1.2 ciphersuites */ +# define TLS1_CK_RSA_WITH_NULL_SHA256 0x0300003B +# define TLS1_CK_RSA_WITH_AES_128_SHA256 0x0300003C +# define TLS1_CK_RSA_WITH_AES_256_SHA256 0x0300003D +# define TLS1_CK_DH_DSS_WITH_AES_128_SHA256 0x0300003E +# define TLS1_CK_DH_RSA_WITH_AES_128_SHA256 0x0300003F +# define TLS1_CK_DHE_DSS_WITH_AES_128_SHA256 0x03000040 + +/* Camellia ciphersuites from RFC4132 */ +# define TLS1_CK_RSA_WITH_CAMELLIA_128_CBC_SHA 0x03000041 +# define TLS1_CK_DH_DSS_WITH_CAMELLIA_128_CBC_SHA 0x03000042 +# define TLS1_CK_DH_RSA_WITH_CAMELLIA_128_CBC_SHA 0x03000043 +# define TLS1_CK_DHE_DSS_WITH_CAMELLIA_128_CBC_SHA 0x03000044 +# define TLS1_CK_DHE_RSA_WITH_CAMELLIA_128_CBC_SHA 0x03000045 +# define TLS1_CK_ADH_WITH_CAMELLIA_128_CBC_SHA 0x03000046 + +/* TLS v1.2 ciphersuites */ +# define TLS1_CK_DHE_RSA_WITH_AES_128_SHA256 0x03000067 +# define TLS1_CK_DH_DSS_WITH_AES_256_SHA256 0x03000068 +# define TLS1_CK_DH_RSA_WITH_AES_256_SHA256 0x03000069 +# define TLS1_CK_DHE_DSS_WITH_AES_256_SHA256 0x0300006A +# define TLS1_CK_DHE_RSA_WITH_AES_256_SHA256 0x0300006B +# define TLS1_CK_ADH_WITH_AES_128_SHA256 0x0300006C +# define TLS1_CK_ADH_WITH_AES_256_SHA256 0x0300006D + +/* Camellia ciphersuites from RFC4132 */ +# define TLS1_CK_RSA_WITH_CAMELLIA_256_CBC_SHA 0x03000084 +# define TLS1_CK_DH_DSS_WITH_CAMELLIA_256_CBC_SHA 0x03000085 +# define TLS1_CK_DH_RSA_WITH_CAMELLIA_256_CBC_SHA 0x03000086 +# define TLS1_CK_DHE_DSS_WITH_CAMELLIA_256_CBC_SHA 0x03000087 +# define TLS1_CK_DHE_RSA_WITH_CAMELLIA_256_CBC_SHA 0x03000088 +# define TLS1_CK_ADH_WITH_CAMELLIA_256_CBC_SHA 0x03000089 + +/* SEED ciphersuites from RFC4162 */ +# define TLS1_CK_RSA_WITH_SEED_SHA 0x03000096 +# define TLS1_CK_DH_DSS_WITH_SEED_SHA 0x03000097 +# define TLS1_CK_DH_RSA_WITH_SEED_SHA 0x03000098 +# define TLS1_CK_DHE_DSS_WITH_SEED_SHA 0x03000099 +# define TLS1_CK_DHE_RSA_WITH_SEED_SHA 0x0300009A +# define TLS1_CK_ADH_WITH_SEED_SHA 0x0300009B + +/* TLS v1.2 GCM ciphersuites from RFC5288 */ +# define TLS1_CK_RSA_WITH_AES_128_GCM_SHA256 0x0300009C +# define TLS1_CK_RSA_WITH_AES_256_GCM_SHA384 0x0300009D +# define TLS1_CK_DHE_RSA_WITH_AES_128_GCM_SHA256 0x0300009E +# define TLS1_CK_DHE_RSA_WITH_AES_256_GCM_SHA384 0x0300009F +# define TLS1_CK_DH_RSA_WITH_AES_128_GCM_SHA256 0x030000A0 +# define TLS1_CK_DH_RSA_WITH_AES_256_GCM_SHA384 0x030000A1 +# define TLS1_CK_DHE_DSS_WITH_AES_128_GCM_SHA256 0x030000A2 +# define TLS1_CK_DHE_DSS_WITH_AES_256_GCM_SHA384 0x030000A3 +# define TLS1_CK_DH_DSS_WITH_AES_128_GCM_SHA256 0x030000A4 +# define TLS1_CK_DH_DSS_WITH_AES_256_GCM_SHA384 0x030000A5 +# define TLS1_CK_ADH_WITH_AES_128_GCM_SHA256 0x030000A6 +# define TLS1_CK_ADH_WITH_AES_256_GCM_SHA384 0x030000A7 + +/* CCM ciphersuites from RFC6655 */ +# define TLS1_CK_RSA_WITH_AES_128_CCM 0x0300C09C +# define TLS1_CK_RSA_WITH_AES_256_CCM 0x0300C09D +# define TLS1_CK_DHE_RSA_WITH_AES_128_CCM 0x0300C09E +# define TLS1_CK_DHE_RSA_WITH_AES_256_CCM 0x0300C09F +# define TLS1_CK_RSA_WITH_AES_128_CCM_8 0x0300C0A0 +# define TLS1_CK_RSA_WITH_AES_256_CCM_8 0x0300C0A1 +# define TLS1_CK_DHE_RSA_WITH_AES_128_CCM_8 0x0300C0A2 +# define TLS1_CK_DHE_RSA_WITH_AES_256_CCM_8 0x0300C0A3 +# define TLS1_CK_PSK_WITH_AES_128_CCM 0x0300C0A4 +# define TLS1_CK_PSK_WITH_AES_256_CCM 0x0300C0A5 +# define TLS1_CK_DHE_PSK_WITH_AES_128_CCM 0x0300C0A6 +# define TLS1_CK_DHE_PSK_WITH_AES_256_CCM 0x0300C0A7 +# define TLS1_CK_PSK_WITH_AES_128_CCM_8 0x0300C0A8 +# define TLS1_CK_PSK_WITH_AES_256_CCM_8 0x0300C0A9 +# define TLS1_CK_DHE_PSK_WITH_AES_128_CCM_8 0x0300C0AA +# define TLS1_CK_DHE_PSK_WITH_AES_256_CCM_8 0x0300C0AB + +/* CCM ciphersuites from RFC7251 */ +# define TLS1_CK_ECDHE_ECDSA_WITH_AES_128_CCM 0x0300C0AC +# define TLS1_CK_ECDHE_ECDSA_WITH_AES_256_CCM 0x0300C0AD +# define TLS1_CK_ECDHE_ECDSA_WITH_AES_128_CCM_8 0x0300C0AE +# define TLS1_CK_ECDHE_ECDSA_WITH_AES_256_CCM_8 0x0300C0AF + +/* TLS 1.2 Camellia SHA-256 ciphersuites from RFC5932 */ +# define TLS1_CK_RSA_WITH_CAMELLIA_128_CBC_SHA256 0x030000BA +# define TLS1_CK_DH_DSS_WITH_CAMELLIA_128_CBC_SHA256 0x030000BB +# define TLS1_CK_DH_RSA_WITH_CAMELLIA_128_CBC_SHA256 0x030000BC +# define TLS1_CK_DHE_DSS_WITH_CAMELLIA_128_CBC_SHA256 0x030000BD +# define TLS1_CK_DHE_RSA_WITH_CAMELLIA_128_CBC_SHA256 0x030000BE +# define TLS1_CK_ADH_WITH_CAMELLIA_128_CBC_SHA256 0x030000BF + +# define TLS1_CK_RSA_WITH_CAMELLIA_256_CBC_SHA256 0x030000C0 +# define TLS1_CK_DH_DSS_WITH_CAMELLIA_256_CBC_SHA256 0x030000C1 +# define TLS1_CK_DH_RSA_WITH_CAMELLIA_256_CBC_SHA256 0x030000C2 +# define TLS1_CK_DHE_DSS_WITH_CAMELLIA_256_CBC_SHA256 0x030000C3 +# define TLS1_CK_DHE_RSA_WITH_CAMELLIA_256_CBC_SHA256 0x030000C4 +# define TLS1_CK_ADH_WITH_CAMELLIA_256_CBC_SHA256 0x030000C5 + +/* ECC ciphersuites from RFC4492 */ +# define TLS1_CK_ECDH_ECDSA_WITH_NULL_SHA 0x0300C001 +# define TLS1_CK_ECDH_ECDSA_WITH_RC4_128_SHA 0x0300C002 +# define TLS1_CK_ECDH_ECDSA_WITH_DES_192_CBC3_SHA 0x0300C003 +# define TLS1_CK_ECDH_ECDSA_WITH_AES_128_CBC_SHA 0x0300C004 +# define TLS1_CK_ECDH_ECDSA_WITH_AES_256_CBC_SHA 0x0300C005 + +# define TLS1_CK_ECDHE_ECDSA_WITH_NULL_SHA 0x0300C006 +# define TLS1_CK_ECDHE_ECDSA_WITH_RC4_128_SHA 0x0300C007 +# define TLS1_CK_ECDHE_ECDSA_WITH_DES_192_CBC3_SHA 0x0300C008 +# define TLS1_CK_ECDHE_ECDSA_WITH_AES_128_CBC_SHA 0x0300C009 +# define TLS1_CK_ECDHE_ECDSA_WITH_AES_256_CBC_SHA 0x0300C00A + +# define TLS1_CK_ECDH_RSA_WITH_NULL_SHA 0x0300C00B +# define TLS1_CK_ECDH_RSA_WITH_RC4_128_SHA 0x0300C00C +# define TLS1_CK_ECDH_RSA_WITH_DES_192_CBC3_SHA 0x0300C00D +# define TLS1_CK_ECDH_RSA_WITH_AES_128_CBC_SHA 0x0300C00E +# define TLS1_CK_ECDH_RSA_WITH_AES_256_CBC_SHA 0x0300C00F + +# define TLS1_CK_ECDHE_RSA_WITH_NULL_SHA 0x0300C010 +# define TLS1_CK_ECDHE_RSA_WITH_RC4_128_SHA 0x0300C011 +# define TLS1_CK_ECDHE_RSA_WITH_DES_192_CBC3_SHA 0x0300C012 +# define TLS1_CK_ECDHE_RSA_WITH_AES_128_CBC_SHA 0x0300C013 +# define TLS1_CK_ECDHE_RSA_WITH_AES_256_CBC_SHA 0x0300C014 + +# define TLS1_CK_ECDH_anon_WITH_NULL_SHA 0x0300C015 +# define TLS1_CK_ECDH_anon_WITH_RC4_128_SHA 0x0300C016 +# define TLS1_CK_ECDH_anon_WITH_DES_192_CBC3_SHA 0x0300C017 +# define TLS1_CK_ECDH_anon_WITH_AES_128_CBC_SHA 0x0300C018 +# define TLS1_CK_ECDH_anon_WITH_AES_256_CBC_SHA 0x0300C019 + +/* SRP ciphersuites from RFC 5054 */ +# define TLS1_CK_SRP_SHA_WITH_3DES_EDE_CBC_SHA 0x0300C01A +# define TLS1_CK_SRP_SHA_RSA_WITH_3DES_EDE_CBC_SHA 0x0300C01B +# define TLS1_CK_SRP_SHA_DSS_WITH_3DES_EDE_CBC_SHA 0x0300C01C +# define TLS1_CK_SRP_SHA_WITH_AES_128_CBC_SHA 0x0300C01D +# define TLS1_CK_SRP_SHA_RSA_WITH_AES_128_CBC_SHA 0x0300C01E +# define TLS1_CK_SRP_SHA_DSS_WITH_AES_128_CBC_SHA 0x0300C01F +# define TLS1_CK_SRP_SHA_WITH_AES_256_CBC_SHA 0x0300C020 +# define TLS1_CK_SRP_SHA_RSA_WITH_AES_256_CBC_SHA 0x0300C021 +# define TLS1_CK_SRP_SHA_DSS_WITH_AES_256_CBC_SHA 0x0300C022 + +/* ECDH HMAC based ciphersuites from RFC5289 */ + +# define TLS1_CK_ECDHE_ECDSA_WITH_AES_128_SHA256 0x0300C023 +# define TLS1_CK_ECDHE_ECDSA_WITH_AES_256_SHA384 0x0300C024 +# define TLS1_CK_ECDH_ECDSA_WITH_AES_128_SHA256 0x0300C025 +# define TLS1_CK_ECDH_ECDSA_WITH_AES_256_SHA384 0x0300C026 +# define TLS1_CK_ECDHE_RSA_WITH_AES_128_SHA256 0x0300C027 +# define TLS1_CK_ECDHE_RSA_WITH_AES_256_SHA384 0x0300C028 +# define TLS1_CK_ECDH_RSA_WITH_AES_128_SHA256 0x0300C029 +# define TLS1_CK_ECDH_RSA_WITH_AES_256_SHA384 0x0300C02A + +/* ECDH GCM based ciphersuites from RFC5289 */ +# define TLS1_CK_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 0x0300C02B +# define TLS1_CK_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384 0x0300C02C +# define TLS1_CK_ECDH_ECDSA_WITH_AES_128_GCM_SHA256 0x0300C02D +# define TLS1_CK_ECDH_ECDSA_WITH_AES_256_GCM_SHA384 0x0300C02E +# define TLS1_CK_ECDHE_RSA_WITH_AES_128_GCM_SHA256 0x0300C02F +# define TLS1_CK_ECDHE_RSA_WITH_AES_256_GCM_SHA384 0x0300C030 +# define TLS1_CK_ECDH_RSA_WITH_AES_128_GCM_SHA256 0x0300C031 +# define TLS1_CK_ECDH_RSA_WITH_AES_256_GCM_SHA384 0x0300C032 + +/* ECDHE PSK ciphersuites from RFC5489 */ +# define TLS1_CK_ECDHE_PSK_WITH_RC4_128_SHA 0x0300C033 +# define TLS1_CK_ECDHE_PSK_WITH_3DES_EDE_CBC_SHA 0x0300C034 +# define TLS1_CK_ECDHE_PSK_WITH_AES_128_CBC_SHA 0x0300C035 +# define TLS1_CK_ECDHE_PSK_WITH_AES_256_CBC_SHA 0x0300C036 + +# define TLS1_CK_ECDHE_PSK_WITH_AES_128_CBC_SHA256 0x0300C037 +# define TLS1_CK_ECDHE_PSK_WITH_AES_256_CBC_SHA384 0x0300C038 + +/* NULL PSK ciphersuites from RFC4785 */ + +# define TLS1_CK_ECDHE_PSK_WITH_NULL_SHA 0x0300C039 +# define TLS1_CK_ECDHE_PSK_WITH_NULL_SHA256 0x0300C03A +# define TLS1_CK_ECDHE_PSK_WITH_NULL_SHA384 0x0300C03B + +/* Camellia-CBC ciphersuites from RFC6367 */ +# define TLS1_CK_ECDHE_ECDSA_WITH_CAMELLIA_128_CBC_SHA256 0x0300C072 +# define TLS1_CK_ECDHE_ECDSA_WITH_CAMELLIA_256_CBC_SHA384 0x0300C073 +# define TLS1_CK_ECDH_ECDSA_WITH_CAMELLIA_128_CBC_SHA256 0x0300C074 +# define TLS1_CK_ECDH_ECDSA_WITH_CAMELLIA_256_CBC_SHA384 0x0300C075 +# define TLS1_CK_ECDHE_RSA_WITH_CAMELLIA_128_CBC_SHA256 0x0300C076 +# define TLS1_CK_ECDHE_RSA_WITH_CAMELLIA_256_CBC_SHA384 0x0300C077 +# define TLS1_CK_ECDH_RSA_WITH_CAMELLIA_128_CBC_SHA256 0x0300C078 +# define TLS1_CK_ECDH_RSA_WITH_CAMELLIA_256_CBC_SHA384 0x0300C079 + +# define TLS1_CK_PSK_WITH_CAMELLIA_128_CBC_SHA256 0x0300C094 +# define TLS1_CK_PSK_WITH_CAMELLIA_256_CBC_SHA384 0x0300C095 +# define TLS1_CK_DHE_PSK_WITH_CAMELLIA_128_CBC_SHA256 0x0300C096 +# define TLS1_CK_DHE_PSK_WITH_CAMELLIA_256_CBC_SHA384 0x0300C097 +# define TLS1_CK_RSA_PSK_WITH_CAMELLIA_128_CBC_SHA256 0x0300C098 +# define TLS1_CK_RSA_PSK_WITH_CAMELLIA_256_CBC_SHA384 0x0300C099 +# define TLS1_CK_ECDHE_PSK_WITH_CAMELLIA_128_CBC_SHA256 0x0300C09A +# define TLS1_CK_ECDHE_PSK_WITH_CAMELLIA_256_CBC_SHA384 0x0300C09B + +/* draft-ietf-tls-chacha20-poly1305-03 */ +# define TLS1_CK_ECDHE_RSA_WITH_CHACHA20_POLY1305 0x0300CCA8 +# define TLS1_CK_ECDHE_ECDSA_WITH_CHACHA20_POLY1305 0x0300CCA9 +# define TLS1_CK_DHE_RSA_WITH_CHACHA20_POLY1305 0x0300CCAA +# define TLS1_CK_PSK_WITH_CHACHA20_POLY1305 0x0300CCAB +# define TLS1_CK_ECDHE_PSK_WITH_CHACHA20_POLY1305 0x0300CCAC +# define TLS1_CK_DHE_PSK_WITH_CHACHA20_POLY1305 0x0300CCAD +# define TLS1_CK_RSA_PSK_WITH_CHACHA20_POLY1305 0x0300CCAE + +/* + * XXX Backward compatibility alert: Older versions of OpenSSL gave some DHE + * ciphers names with "EDH" instead of "DHE". Going forward, we should be + * using DHE everywhere, though we may indefinitely maintain aliases for + * users or configurations that used "EDH" + */ +# define TLS1_TXT_DHE_DSS_WITH_RC4_128_SHA "DHE-DSS-RC4-SHA" + +# define TLS1_TXT_PSK_WITH_NULL_SHA "PSK-NULL-SHA" +# define TLS1_TXT_DHE_PSK_WITH_NULL_SHA "DHE-PSK-NULL-SHA" +# define TLS1_TXT_RSA_PSK_WITH_NULL_SHA "RSA-PSK-NULL-SHA" + +/* AES ciphersuites from RFC3268 */ +# define TLS1_TXT_RSA_WITH_AES_128_SHA "AES128-SHA" +# define TLS1_TXT_DH_DSS_WITH_AES_128_SHA "DH-DSS-AES128-SHA" +# define TLS1_TXT_DH_RSA_WITH_AES_128_SHA "DH-RSA-AES128-SHA" +# define TLS1_TXT_DHE_DSS_WITH_AES_128_SHA "DHE-DSS-AES128-SHA" +# define TLS1_TXT_DHE_RSA_WITH_AES_128_SHA "DHE-RSA-AES128-SHA" +# define TLS1_TXT_ADH_WITH_AES_128_SHA "ADH-AES128-SHA" + +# define TLS1_TXT_RSA_WITH_AES_256_SHA "AES256-SHA" +# define TLS1_TXT_DH_DSS_WITH_AES_256_SHA "DH-DSS-AES256-SHA" +# define TLS1_TXT_DH_RSA_WITH_AES_256_SHA "DH-RSA-AES256-SHA" +# define TLS1_TXT_DHE_DSS_WITH_AES_256_SHA "DHE-DSS-AES256-SHA" +# define TLS1_TXT_DHE_RSA_WITH_AES_256_SHA "DHE-RSA-AES256-SHA" +# define TLS1_TXT_ADH_WITH_AES_256_SHA "ADH-AES256-SHA" + +/* ECC ciphersuites from RFC4492 */ +# define TLS1_TXT_ECDH_ECDSA_WITH_NULL_SHA "ECDH-ECDSA-NULL-SHA" +# define TLS1_TXT_ECDH_ECDSA_WITH_RC4_128_SHA "ECDH-ECDSA-RC4-SHA" +# define TLS1_TXT_ECDH_ECDSA_WITH_DES_192_CBC3_SHA "ECDH-ECDSA-DES-CBC3-SHA" +# define TLS1_TXT_ECDH_ECDSA_WITH_AES_128_CBC_SHA "ECDH-ECDSA-AES128-SHA" +# define TLS1_TXT_ECDH_ECDSA_WITH_AES_256_CBC_SHA "ECDH-ECDSA-AES256-SHA" + +# define TLS1_TXT_ECDHE_ECDSA_WITH_NULL_SHA "ECDHE-ECDSA-NULL-SHA" +# define TLS1_TXT_ECDHE_ECDSA_WITH_RC4_128_SHA "ECDHE-ECDSA-RC4-SHA" +# define TLS1_TXT_ECDHE_ECDSA_WITH_DES_192_CBC3_SHA "ECDHE-ECDSA-DES-CBC3-SHA" +# define TLS1_TXT_ECDHE_ECDSA_WITH_AES_128_CBC_SHA "ECDHE-ECDSA-AES128-SHA" +# define TLS1_TXT_ECDHE_ECDSA_WITH_AES_256_CBC_SHA "ECDHE-ECDSA-AES256-SHA" + +# define TLS1_TXT_ECDH_RSA_WITH_NULL_SHA "ECDH-RSA-NULL-SHA" +# define TLS1_TXT_ECDH_RSA_WITH_RC4_128_SHA "ECDH-RSA-RC4-SHA" +# define TLS1_TXT_ECDH_RSA_WITH_DES_192_CBC3_SHA "ECDH-RSA-DES-CBC3-SHA" +# define TLS1_TXT_ECDH_RSA_WITH_AES_128_CBC_SHA "ECDH-RSA-AES128-SHA" +# define TLS1_TXT_ECDH_RSA_WITH_AES_256_CBC_SHA "ECDH-RSA-AES256-SHA" + +# define TLS1_TXT_ECDHE_RSA_WITH_NULL_SHA "ECDHE-RSA-NULL-SHA" +# define TLS1_TXT_ECDHE_RSA_WITH_RC4_128_SHA "ECDHE-RSA-RC4-SHA" +# define TLS1_TXT_ECDHE_RSA_WITH_DES_192_CBC3_SHA "ECDHE-RSA-DES-CBC3-SHA" +# define TLS1_TXT_ECDHE_RSA_WITH_AES_128_CBC_SHA "ECDHE-RSA-AES128-SHA" +# define TLS1_TXT_ECDHE_RSA_WITH_AES_256_CBC_SHA "ECDHE-RSA-AES256-SHA" + +# define TLS1_TXT_ECDH_anon_WITH_NULL_SHA "AECDH-NULL-SHA" +# define TLS1_TXT_ECDH_anon_WITH_RC4_128_SHA "AECDH-RC4-SHA" +# define TLS1_TXT_ECDH_anon_WITH_DES_192_CBC3_SHA "AECDH-DES-CBC3-SHA" +# define TLS1_TXT_ECDH_anon_WITH_AES_128_CBC_SHA "AECDH-AES128-SHA" +# define TLS1_TXT_ECDH_anon_WITH_AES_256_CBC_SHA "AECDH-AES256-SHA" + +/* PSK ciphersuites from RFC 4279 */ +# define TLS1_TXT_PSK_WITH_RC4_128_SHA "PSK-RC4-SHA" +# define TLS1_TXT_PSK_WITH_3DES_EDE_CBC_SHA "PSK-3DES-EDE-CBC-SHA" +# define TLS1_TXT_PSK_WITH_AES_128_CBC_SHA "PSK-AES128-CBC-SHA" +# define TLS1_TXT_PSK_WITH_AES_256_CBC_SHA "PSK-AES256-CBC-SHA" + +# define TLS1_TXT_DHE_PSK_WITH_RC4_128_SHA "DHE-PSK-RC4-SHA" +# define TLS1_TXT_DHE_PSK_WITH_3DES_EDE_CBC_SHA "DHE-PSK-3DES-EDE-CBC-SHA" +# define TLS1_TXT_DHE_PSK_WITH_AES_128_CBC_SHA "DHE-PSK-AES128-CBC-SHA" +# define TLS1_TXT_DHE_PSK_WITH_AES_256_CBC_SHA "DHE-PSK-AES256-CBC-SHA" +# define TLS1_TXT_RSA_PSK_WITH_RC4_128_SHA "RSA-PSK-RC4-SHA" +# define TLS1_TXT_RSA_PSK_WITH_3DES_EDE_CBC_SHA "RSA-PSK-3DES-EDE-CBC-SHA" +# define TLS1_TXT_RSA_PSK_WITH_AES_128_CBC_SHA "RSA-PSK-AES128-CBC-SHA" +# define TLS1_TXT_RSA_PSK_WITH_AES_256_CBC_SHA "RSA-PSK-AES256-CBC-SHA" + +/* PSK ciphersuites from RFC 5487 */ +# define TLS1_TXT_PSK_WITH_AES_128_GCM_SHA256 "PSK-AES128-GCM-SHA256" +# define TLS1_TXT_PSK_WITH_AES_256_GCM_SHA384 "PSK-AES256-GCM-SHA384" +# define TLS1_TXT_DHE_PSK_WITH_AES_128_GCM_SHA256 "DHE-PSK-AES128-GCM-SHA256" +# define TLS1_TXT_DHE_PSK_WITH_AES_256_GCM_SHA384 "DHE-PSK-AES256-GCM-SHA384" +# define TLS1_TXT_RSA_PSK_WITH_AES_128_GCM_SHA256 "RSA-PSK-AES128-GCM-SHA256" +# define TLS1_TXT_RSA_PSK_WITH_AES_256_GCM_SHA384 "RSA-PSK-AES256-GCM-SHA384" + +# define TLS1_TXT_PSK_WITH_AES_128_CBC_SHA256 "PSK-AES128-CBC-SHA256" +# define TLS1_TXT_PSK_WITH_AES_256_CBC_SHA384 "PSK-AES256-CBC-SHA384" +# define TLS1_TXT_PSK_WITH_NULL_SHA256 "PSK-NULL-SHA256" +# define TLS1_TXT_PSK_WITH_NULL_SHA384 "PSK-NULL-SHA384" + +# define TLS1_TXT_DHE_PSK_WITH_AES_128_CBC_SHA256 "DHE-PSK-AES128-CBC-SHA256" +# define TLS1_TXT_DHE_PSK_WITH_AES_256_CBC_SHA384 "DHE-PSK-AES256-CBC-SHA384" +# define TLS1_TXT_DHE_PSK_WITH_NULL_SHA256 "DHE-PSK-NULL-SHA256" +# define TLS1_TXT_DHE_PSK_WITH_NULL_SHA384 "DHE-PSK-NULL-SHA384" + +# define TLS1_TXT_RSA_PSK_WITH_AES_128_CBC_SHA256 "RSA-PSK-AES128-CBC-SHA256" +# define TLS1_TXT_RSA_PSK_WITH_AES_256_CBC_SHA384 "RSA-PSK-AES256-CBC-SHA384" +# define TLS1_TXT_RSA_PSK_WITH_NULL_SHA256 "RSA-PSK-NULL-SHA256" +# define TLS1_TXT_RSA_PSK_WITH_NULL_SHA384 "RSA-PSK-NULL-SHA384" + +/* SRP ciphersuite from RFC 5054 */ +# define TLS1_TXT_SRP_SHA_WITH_3DES_EDE_CBC_SHA "SRP-3DES-EDE-CBC-SHA" +# define TLS1_TXT_SRP_SHA_RSA_WITH_3DES_EDE_CBC_SHA "SRP-RSA-3DES-EDE-CBC-SHA" +# define TLS1_TXT_SRP_SHA_DSS_WITH_3DES_EDE_CBC_SHA "SRP-DSS-3DES-EDE-CBC-SHA" +# define TLS1_TXT_SRP_SHA_WITH_AES_128_CBC_SHA "SRP-AES-128-CBC-SHA" +# define TLS1_TXT_SRP_SHA_RSA_WITH_AES_128_CBC_SHA "SRP-RSA-AES-128-CBC-SHA" +# define TLS1_TXT_SRP_SHA_DSS_WITH_AES_128_CBC_SHA "SRP-DSS-AES-128-CBC-SHA" +# define TLS1_TXT_SRP_SHA_WITH_AES_256_CBC_SHA "SRP-AES-256-CBC-SHA" +# define TLS1_TXT_SRP_SHA_RSA_WITH_AES_256_CBC_SHA "SRP-RSA-AES-256-CBC-SHA" +# define TLS1_TXT_SRP_SHA_DSS_WITH_AES_256_CBC_SHA "SRP-DSS-AES-256-CBC-SHA" + +/* Camellia ciphersuites from RFC4132 */ +# define TLS1_TXT_RSA_WITH_CAMELLIA_128_CBC_SHA "CAMELLIA128-SHA" +# define TLS1_TXT_DH_DSS_WITH_CAMELLIA_128_CBC_SHA "DH-DSS-CAMELLIA128-SHA" +# define TLS1_TXT_DH_RSA_WITH_CAMELLIA_128_CBC_SHA "DH-RSA-CAMELLIA128-SHA" +# define TLS1_TXT_DHE_DSS_WITH_CAMELLIA_128_CBC_SHA "DHE-DSS-CAMELLIA128-SHA" +# define TLS1_TXT_DHE_RSA_WITH_CAMELLIA_128_CBC_SHA "DHE-RSA-CAMELLIA128-SHA" +# define TLS1_TXT_ADH_WITH_CAMELLIA_128_CBC_SHA "ADH-CAMELLIA128-SHA" + +# define TLS1_TXT_RSA_WITH_CAMELLIA_256_CBC_SHA "CAMELLIA256-SHA" +# define TLS1_TXT_DH_DSS_WITH_CAMELLIA_256_CBC_SHA "DH-DSS-CAMELLIA256-SHA" +# define TLS1_TXT_DH_RSA_WITH_CAMELLIA_256_CBC_SHA "DH-RSA-CAMELLIA256-SHA" +# define TLS1_TXT_DHE_DSS_WITH_CAMELLIA_256_CBC_SHA "DHE-DSS-CAMELLIA256-SHA" +# define TLS1_TXT_DHE_RSA_WITH_CAMELLIA_256_CBC_SHA "DHE-RSA-CAMELLIA256-SHA" +# define TLS1_TXT_ADH_WITH_CAMELLIA_256_CBC_SHA "ADH-CAMELLIA256-SHA" + +/* TLS 1.2 Camellia SHA-256 ciphersuites from RFC5932 */ +# define TLS1_TXT_RSA_WITH_CAMELLIA_128_CBC_SHA256 "CAMELLIA128-SHA256" +# define TLS1_TXT_DH_DSS_WITH_CAMELLIA_128_CBC_SHA256 "DH-DSS-CAMELLIA128-SHA256" +# define TLS1_TXT_DH_RSA_WITH_CAMELLIA_128_CBC_SHA256 "DH-RSA-CAMELLIA128-SHA256" +# define TLS1_TXT_DHE_DSS_WITH_CAMELLIA_128_CBC_SHA256 "DHE-DSS-CAMELLIA128-SHA256" +# define TLS1_TXT_DHE_RSA_WITH_CAMELLIA_128_CBC_SHA256 "DHE-RSA-CAMELLIA128-SHA256" +# define TLS1_TXT_ADH_WITH_CAMELLIA_128_CBC_SHA256 "ADH-CAMELLIA128-SHA256" + +# define TLS1_TXT_RSA_WITH_CAMELLIA_256_CBC_SHA256 "CAMELLIA256-SHA256" +# define TLS1_TXT_DH_DSS_WITH_CAMELLIA_256_CBC_SHA256 "DH-DSS-CAMELLIA256-SHA256" +# define TLS1_TXT_DH_RSA_WITH_CAMELLIA_256_CBC_SHA256 "DH-RSA-CAMELLIA256-SHA256" +# define TLS1_TXT_DHE_DSS_WITH_CAMELLIA_256_CBC_SHA256 "DHE-DSS-CAMELLIA256-SHA256" +# define TLS1_TXT_DHE_RSA_WITH_CAMELLIA_256_CBC_SHA256 "DHE-RSA-CAMELLIA256-SHA256" +# define TLS1_TXT_ADH_WITH_CAMELLIA_256_CBC_SHA256 "ADH-CAMELLIA256-SHA256" + +# define TLS1_TXT_PSK_WITH_CAMELLIA_128_CBC_SHA256 "PSK-CAMELLIA128-SHA256" +# define TLS1_TXT_PSK_WITH_CAMELLIA_256_CBC_SHA384 "PSK-CAMELLIA256-SHA384" +# define TLS1_TXT_DHE_PSK_WITH_CAMELLIA_128_CBC_SHA256 "DHE-PSK-CAMELLIA128-SHA256" +# define TLS1_TXT_DHE_PSK_WITH_CAMELLIA_256_CBC_SHA384 "DHE-PSK-CAMELLIA256-SHA384" +# define TLS1_TXT_RSA_PSK_WITH_CAMELLIA_128_CBC_SHA256 "RSA-PSK-CAMELLIA128-SHA256" +# define TLS1_TXT_RSA_PSK_WITH_CAMELLIA_256_CBC_SHA384 "RSA-PSK-CAMELLIA256-SHA384" +# define TLS1_TXT_ECDHE_PSK_WITH_CAMELLIA_128_CBC_SHA256 "ECDHE-PSK-CAMELLIA128-SHA256" +# define TLS1_TXT_ECDHE_PSK_WITH_CAMELLIA_256_CBC_SHA384 "ECDHE-PSK-CAMELLIA256-SHA384" + +/* SEED ciphersuites from RFC4162 */ +# define TLS1_TXT_RSA_WITH_SEED_SHA "SEED-SHA" +# define TLS1_TXT_DH_DSS_WITH_SEED_SHA "DH-DSS-SEED-SHA" +# define TLS1_TXT_DH_RSA_WITH_SEED_SHA "DH-RSA-SEED-SHA" +# define TLS1_TXT_DHE_DSS_WITH_SEED_SHA "DHE-DSS-SEED-SHA" +# define TLS1_TXT_DHE_RSA_WITH_SEED_SHA "DHE-RSA-SEED-SHA" +# define TLS1_TXT_ADH_WITH_SEED_SHA "ADH-SEED-SHA" + +/* TLS v1.2 ciphersuites */ +# define TLS1_TXT_RSA_WITH_NULL_SHA256 "NULL-SHA256" +# define TLS1_TXT_RSA_WITH_AES_128_SHA256 "AES128-SHA256" +# define TLS1_TXT_RSA_WITH_AES_256_SHA256 "AES256-SHA256" +# define TLS1_TXT_DH_DSS_WITH_AES_128_SHA256 "DH-DSS-AES128-SHA256" +# define TLS1_TXT_DH_RSA_WITH_AES_128_SHA256 "DH-RSA-AES128-SHA256" +# define TLS1_TXT_DHE_DSS_WITH_AES_128_SHA256 "DHE-DSS-AES128-SHA256" +# define TLS1_TXT_DHE_RSA_WITH_AES_128_SHA256 "DHE-RSA-AES128-SHA256" +# define TLS1_TXT_DH_DSS_WITH_AES_256_SHA256 "DH-DSS-AES256-SHA256" +# define TLS1_TXT_DH_RSA_WITH_AES_256_SHA256 "DH-RSA-AES256-SHA256" +# define TLS1_TXT_DHE_DSS_WITH_AES_256_SHA256 "DHE-DSS-AES256-SHA256" +# define TLS1_TXT_DHE_RSA_WITH_AES_256_SHA256 "DHE-RSA-AES256-SHA256" +# define TLS1_TXT_ADH_WITH_AES_128_SHA256 "ADH-AES128-SHA256" +# define TLS1_TXT_ADH_WITH_AES_256_SHA256 "ADH-AES256-SHA256" + +/* TLS v1.2 GCM ciphersuites from RFC5288 */ +# define TLS1_TXT_RSA_WITH_AES_128_GCM_SHA256 "AES128-GCM-SHA256" +# define TLS1_TXT_RSA_WITH_AES_256_GCM_SHA384 "AES256-GCM-SHA384" +# define TLS1_TXT_DHE_RSA_WITH_AES_128_GCM_SHA256 "DHE-RSA-AES128-GCM-SHA256" +# define TLS1_TXT_DHE_RSA_WITH_AES_256_GCM_SHA384 "DHE-RSA-AES256-GCM-SHA384" +# define TLS1_TXT_DH_RSA_WITH_AES_128_GCM_SHA256 "DH-RSA-AES128-GCM-SHA256" +# define TLS1_TXT_DH_RSA_WITH_AES_256_GCM_SHA384 "DH-RSA-AES256-GCM-SHA384" +# define TLS1_TXT_DHE_DSS_WITH_AES_128_GCM_SHA256 "DHE-DSS-AES128-GCM-SHA256" +# define TLS1_TXT_DHE_DSS_WITH_AES_256_GCM_SHA384 "DHE-DSS-AES256-GCM-SHA384" +# define TLS1_TXT_DH_DSS_WITH_AES_128_GCM_SHA256 "DH-DSS-AES128-GCM-SHA256" +# define TLS1_TXT_DH_DSS_WITH_AES_256_GCM_SHA384 "DH-DSS-AES256-GCM-SHA384" +# define TLS1_TXT_ADH_WITH_AES_128_GCM_SHA256 "ADH-AES128-GCM-SHA256" +# define TLS1_TXT_ADH_WITH_AES_256_GCM_SHA384 "ADH-AES256-GCM-SHA384" + +/* CCM ciphersuites from RFC6655 */ + +# define TLS1_TXT_RSA_WITH_AES_128_CCM "AES128-CCM" +# define TLS1_TXT_RSA_WITH_AES_256_CCM "AES256-CCM" +# define TLS1_TXT_DHE_RSA_WITH_AES_128_CCM "DHE-RSA-AES128-CCM" +# define TLS1_TXT_DHE_RSA_WITH_AES_256_CCM "DHE-RSA-AES256-CCM" + +# define TLS1_TXT_RSA_WITH_AES_128_CCM_8 "AES128-CCM8" +# define TLS1_TXT_RSA_WITH_AES_256_CCM_8 "AES256-CCM8" +# define TLS1_TXT_DHE_RSA_WITH_AES_128_CCM_8 "DHE-RSA-AES128-CCM8" +# define TLS1_TXT_DHE_RSA_WITH_AES_256_CCM_8 "DHE-RSA-AES256-CCM8" + +# define TLS1_TXT_PSK_WITH_AES_128_CCM "PSK-AES128-CCM" +# define TLS1_TXT_PSK_WITH_AES_256_CCM "PSK-AES256-CCM" +# define TLS1_TXT_DHE_PSK_WITH_AES_128_CCM "DHE-PSK-AES128-CCM" +# define TLS1_TXT_DHE_PSK_WITH_AES_256_CCM "DHE-PSK-AES256-CCM" + +# define TLS1_TXT_PSK_WITH_AES_128_CCM_8 "PSK-AES128-CCM8" +# define TLS1_TXT_PSK_WITH_AES_256_CCM_8 "PSK-AES256-CCM8" +# define TLS1_TXT_DHE_PSK_WITH_AES_128_CCM_8 "DHE-PSK-AES128-CCM8" +# define TLS1_TXT_DHE_PSK_WITH_AES_256_CCM_8 "DHE-PSK-AES256-CCM8" + +/* CCM ciphersuites from RFC7251 */ + +# define TLS1_TXT_ECDHE_ECDSA_WITH_AES_128_CCM "ECDHE-ECDSA-AES128-CCM" +# define TLS1_TXT_ECDHE_ECDSA_WITH_AES_256_CCM "ECDHE-ECDSA-AES256-CCM" +# define TLS1_TXT_ECDHE_ECDSA_WITH_AES_128_CCM_8 "ECDHE-ECDSA-AES128-CCM8" +# define TLS1_TXT_ECDHE_ECDSA_WITH_AES_256_CCM_8 "ECDHE-ECDSA-AES256-CCM8" + +/* ECDH HMAC based ciphersuites from RFC5289 */ + +# define TLS1_TXT_ECDHE_ECDSA_WITH_AES_128_SHA256 "ECDHE-ECDSA-AES128-SHA256" +# define TLS1_TXT_ECDHE_ECDSA_WITH_AES_256_SHA384 "ECDHE-ECDSA-AES256-SHA384" +# define TLS1_TXT_ECDH_ECDSA_WITH_AES_128_SHA256 "ECDH-ECDSA-AES128-SHA256" +# define TLS1_TXT_ECDH_ECDSA_WITH_AES_256_SHA384 "ECDH-ECDSA-AES256-SHA384" +# define TLS1_TXT_ECDHE_RSA_WITH_AES_128_SHA256 "ECDHE-RSA-AES128-SHA256" +# define TLS1_TXT_ECDHE_RSA_WITH_AES_256_SHA384 "ECDHE-RSA-AES256-SHA384" +# define TLS1_TXT_ECDH_RSA_WITH_AES_128_SHA256 "ECDH-RSA-AES128-SHA256" +# define TLS1_TXT_ECDH_RSA_WITH_AES_256_SHA384 "ECDH-RSA-AES256-SHA384" + +/* ECDH GCM based ciphersuites from RFC5289 */ +# define TLS1_TXT_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 "ECDHE-ECDSA-AES128-GCM-SHA256" +# define TLS1_TXT_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384 "ECDHE-ECDSA-AES256-GCM-SHA384" +# define TLS1_TXT_ECDH_ECDSA_WITH_AES_128_GCM_SHA256 "ECDH-ECDSA-AES128-GCM-SHA256" +# define TLS1_TXT_ECDH_ECDSA_WITH_AES_256_GCM_SHA384 "ECDH-ECDSA-AES256-GCM-SHA384" +# define TLS1_TXT_ECDHE_RSA_WITH_AES_128_GCM_SHA256 "ECDHE-RSA-AES128-GCM-SHA256" +# define TLS1_TXT_ECDHE_RSA_WITH_AES_256_GCM_SHA384 "ECDHE-RSA-AES256-GCM-SHA384" +# define TLS1_TXT_ECDH_RSA_WITH_AES_128_GCM_SHA256 "ECDH-RSA-AES128-GCM-SHA256" +# define TLS1_TXT_ECDH_RSA_WITH_AES_256_GCM_SHA384 "ECDH-RSA-AES256-GCM-SHA384" + +/* TLS v1.2 PSK GCM ciphersuites from RFC5487 */ +# define TLS1_TXT_PSK_WITH_AES_128_GCM_SHA256 "PSK-AES128-GCM-SHA256" +# define TLS1_TXT_PSK_WITH_AES_256_GCM_SHA384 "PSK-AES256-GCM-SHA384" + +/* ECDHE PSK ciphersuites from RFC 5489 */ +# define TLS1_TXT_ECDHE_PSK_WITH_RC4_128_SHA "ECDHE-PSK-RC4-SHA" +# define TLS1_TXT_ECDHE_PSK_WITH_3DES_EDE_CBC_SHA "ECDHE-PSK-3DES-EDE-CBC-SHA" +# define TLS1_TXT_ECDHE_PSK_WITH_AES_128_CBC_SHA "ECDHE-PSK-AES128-CBC-SHA" +# define TLS1_TXT_ECDHE_PSK_WITH_AES_256_CBC_SHA "ECDHE-PSK-AES256-CBC-SHA" + +# define TLS1_TXT_ECDHE_PSK_WITH_AES_128_CBC_SHA256 "ECDHE-PSK-AES128-CBC-SHA256" +# define TLS1_TXT_ECDHE_PSK_WITH_AES_256_CBC_SHA384 "ECDHE-PSK-AES256-CBC-SHA384" + +# define TLS1_TXT_ECDHE_PSK_WITH_NULL_SHA "ECDHE-PSK-NULL-SHA" +# define TLS1_TXT_ECDHE_PSK_WITH_NULL_SHA256 "ECDHE-PSK-NULL-SHA256" +# define TLS1_TXT_ECDHE_PSK_WITH_NULL_SHA384 "ECDHE-PSK-NULL-SHA384" + +/* Camellia-CBC ciphersuites from RFC6367 */ +# define TLS1_TXT_ECDHE_ECDSA_WITH_CAMELLIA_128_CBC_SHA256 "ECDHE-ECDSA-CAMELLIA128-SHA256" +# define TLS1_TXT_ECDHE_ECDSA_WITH_CAMELLIA_256_CBC_SHA384 "ECDHE-ECDSA-CAMELLIA256-SHA384" +# define TLS1_TXT_ECDH_ECDSA_WITH_CAMELLIA_128_CBC_SHA256 "ECDH-ECDSA-CAMELLIA128-SHA256" +# define TLS1_TXT_ECDH_ECDSA_WITH_CAMELLIA_256_CBC_SHA384 "ECDH-ECDSA-CAMELLIA256-SHA384" +# define TLS1_TXT_ECDHE_RSA_WITH_CAMELLIA_128_CBC_SHA256 "ECDHE-RSA-CAMELLIA128-SHA256" +# define TLS1_TXT_ECDHE_RSA_WITH_CAMELLIA_256_CBC_SHA384 "ECDHE-RSA-CAMELLIA256-SHA384" +# define TLS1_TXT_ECDH_RSA_WITH_CAMELLIA_128_CBC_SHA256 "ECDH-RSA-CAMELLIA128-SHA256" +# define TLS1_TXT_ECDH_RSA_WITH_CAMELLIA_256_CBC_SHA384 "ECDH-RSA-CAMELLIA256-SHA384" + +/* draft-ietf-tls-chacha20-poly1305-03 */ +# define TLS1_TXT_ECDHE_RSA_WITH_CHACHA20_POLY1305 "ECDHE-RSA-CHACHA20-POLY1305" +# define TLS1_TXT_ECDHE_ECDSA_WITH_CHACHA20_POLY1305 "ECDHE-ECDSA-CHACHA20-POLY1305" +# define TLS1_TXT_DHE_RSA_WITH_CHACHA20_POLY1305 "DHE-RSA-CHACHA20-POLY1305" +# define TLS1_TXT_PSK_WITH_CHACHA20_POLY1305 "PSK-CHACHA20-POLY1305" +# define TLS1_TXT_ECDHE_PSK_WITH_CHACHA20_POLY1305 "ECDHE-PSK-CHACHA20-POLY1305" +# define TLS1_TXT_DHE_PSK_WITH_CHACHA20_POLY1305 "DHE-PSK-CHACHA20-POLY1305" +# define TLS1_TXT_RSA_PSK_WITH_CHACHA20_POLY1305 "RSA-PSK-CHACHA20-POLY1305" + +# define TLS_CT_RSA_SIGN 1 +# define TLS_CT_DSS_SIGN 2 +# define TLS_CT_RSA_FIXED_DH 3 +# define TLS_CT_DSS_FIXED_DH 4 +# define TLS_CT_ECDSA_SIGN 64 +# define TLS_CT_RSA_FIXED_ECDH 65 +# define TLS_CT_ECDSA_FIXED_ECDH 66 +# define TLS_CT_GOST01_SIGN 22 +# define TLS_CT_GOST12_SIGN 238 +# define TLS_CT_GOST12_512_SIGN 239 + +/* + * when correcting this number, correct also SSL3_CT_NUMBER in ssl3.h (see + * comment there) + */ +# define TLS_CT_NUMBER 9 + +# define TLS1_FINISH_MAC_LENGTH 12 + +# define TLS_MD_MAX_CONST_SIZE 22 +# define TLS_MD_CLIENT_FINISH_CONST "client finished" +# define TLS_MD_CLIENT_FINISH_CONST_SIZE 15 +# define TLS_MD_SERVER_FINISH_CONST "server finished" +# define TLS_MD_SERVER_FINISH_CONST_SIZE 15 +# define TLS_MD_KEY_EXPANSION_CONST "key expansion" +# define TLS_MD_KEY_EXPANSION_CONST_SIZE 13 +# define TLS_MD_CLIENT_WRITE_KEY_CONST "client write key" +# define TLS_MD_CLIENT_WRITE_KEY_CONST_SIZE 16 +# define TLS_MD_SERVER_WRITE_KEY_CONST "server write key" +# define TLS_MD_SERVER_WRITE_KEY_CONST_SIZE 16 +# define TLS_MD_IV_BLOCK_CONST "IV block" +# define TLS_MD_IV_BLOCK_CONST_SIZE 8 +# define TLS_MD_MASTER_SECRET_CONST "master secret" +# define TLS_MD_MASTER_SECRET_CONST_SIZE 13 +# define TLS_MD_EXTENDED_MASTER_SECRET_CONST "extended master secret" +# define TLS_MD_EXTENDED_MASTER_SECRET_CONST_SIZE 22 + +# ifdef CHARSET_EBCDIC +# undef TLS_MD_CLIENT_FINISH_CONST +/* + * client finished + */ +# define TLS_MD_CLIENT_FINISH_CONST "\x63\x6c\x69\x65\x6e\x74\x20\x66\x69\x6e\x69\x73\x68\x65\x64" + +# undef TLS_MD_SERVER_FINISH_CONST +/* + * server finished + */ +# define TLS_MD_SERVER_FINISH_CONST "\x73\x65\x72\x76\x65\x72\x20\x66\x69\x6e\x69\x73\x68\x65\x64" + +# undef TLS_MD_SERVER_WRITE_KEY_CONST +/* + * server write key + */ +# define TLS_MD_SERVER_WRITE_KEY_CONST "\x73\x65\x72\x76\x65\x72\x20\x77\x72\x69\x74\x65\x20\x6b\x65\x79" + +# undef TLS_MD_KEY_EXPANSION_CONST +/* + * key expansion + */ +# define TLS_MD_KEY_EXPANSION_CONST "\x6b\x65\x79\x20\x65\x78\x70\x61\x6e\x73\x69\x6f\x6e" + +# undef TLS_MD_CLIENT_WRITE_KEY_CONST +/* + * client write key + */ +# define TLS_MD_CLIENT_WRITE_KEY_CONST "\x63\x6c\x69\x65\x6e\x74\x20\x77\x72\x69\x74\x65\x20\x6b\x65\x79" + +# undef TLS_MD_SERVER_WRITE_KEY_CONST +/* + * server write key + */ +# define TLS_MD_SERVER_WRITE_KEY_CONST "\x73\x65\x72\x76\x65\x72\x20\x77\x72\x69\x74\x65\x20\x6b\x65\x79" + +# undef TLS_MD_IV_BLOCK_CONST +/* + * IV block + */ +# define TLS_MD_IV_BLOCK_CONST "\x49\x56\x20\x62\x6c\x6f\x63\x6b" + +# undef TLS_MD_MASTER_SECRET_CONST +/* + * master secret + */ +# define TLS_MD_MASTER_SECRET_CONST "\x6d\x61\x73\x74\x65\x72\x20\x73\x65\x63\x72\x65\x74" +# undef TLS_MD_EXTENDED_MASTER_SECRET_CONST +/* + * extended master secret + */ +# define TLS_MD_EXTENDED_MASTER_SECRET_CONST "\x65\x78\x74\x65\x63\x64\x65\x64\x20\x6d\x61\x73\x74\x65\x72\x20\x73\x65\x63\x72\x65\x74" +# endif + +/* TLS Session Ticket extension struct */ +struct tls_session_ticket_ext_st { + unsigned short length; + void *data; +}; + +#ifdef __cplusplus +} +#endif +#endif diff --git a/mobile/ios/FxA/FxA/include/openssl/ts.h b/mobile/ios/FxA/FxA/include/openssl/ts.h new file mode 100644 index 0000000000..a5659825fb --- /dev/null +++ b/mobile/ios/FxA/FxA/include/openssl/ts.h @@ -0,0 +1,643 @@ +/* + * Copyright 2006-2016 The OpenSSL Project Authors. All Rights Reserved. + * + * Licensed under the OpenSSL license (the "License"). You may not use + * this file except in compliance with the License. You can obtain a copy + * in the file LICENSE in the source distribution or at + * https://www.openssl.org/source/license.html + */ + +#ifndef HEADER_TS_H +# define HEADER_TS_H + +# include + +# ifndef OPENSSL_NO_TS +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# ifdef __cplusplus +extern "C" { +# endif + +# include +# include + +typedef struct TS_msg_imprint_st TS_MSG_IMPRINT; +typedef struct TS_req_st TS_REQ; +typedef struct TS_accuracy_st TS_ACCURACY; +typedef struct TS_tst_info_st TS_TST_INFO; + +/* Possible values for status. */ +# define TS_STATUS_GRANTED 0 +# define TS_STATUS_GRANTED_WITH_MODS 1 +# define TS_STATUS_REJECTION 2 +# define TS_STATUS_WAITING 3 +# define TS_STATUS_REVOCATION_WARNING 4 +# define TS_STATUS_REVOCATION_NOTIFICATION 5 + +/* Possible values for failure_info. */ +# define TS_INFO_BAD_ALG 0 +# define TS_INFO_BAD_REQUEST 2 +# define TS_INFO_BAD_DATA_FORMAT 5 +# define TS_INFO_TIME_NOT_AVAILABLE 14 +# define TS_INFO_UNACCEPTED_POLICY 15 +# define TS_INFO_UNACCEPTED_EXTENSION 16 +# define TS_INFO_ADD_INFO_NOT_AVAILABLE 17 +# define TS_INFO_SYSTEM_FAILURE 25 + + +typedef struct TS_status_info_st TS_STATUS_INFO; +typedef struct ESS_issuer_serial ESS_ISSUER_SERIAL; +typedef struct ESS_cert_id ESS_CERT_ID; +typedef struct ESS_signing_cert ESS_SIGNING_CERT; + +DEFINE_STACK_OF(ESS_CERT_ID) + +typedef struct TS_resp_st TS_RESP; + +TS_REQ *TS_REQ_new(void); +void TS_REQ_free(TS_REQ *a); +int i2d_TS_REQ(const TS_REQ *a, unsigned char **pp); +TS_REQ *d2i_TS_REQ(TS_REQ **a, const unsigned char **pp, long length); + +TS_REQ *TS_REQ_dup(TS_REQ *a); + +#ifndef OPENSSL_NO_STDIO +TS_REQ *d2i_TS_REQ_fp(FILE *fp, TS_REQ **a); +int i2d_TS_REQ_fp(FILE *fp, TS_REQ *a); +#endif +TS_REQ *d2i_TS_REQ_bio(BIO *fp, TS_REQ **a); +int i2d_TS_REQ_bio(BIO *fp, TS_REQ *a); + +TS_MSG_IMPRINT *TS_MSG_IMPRINT_new(void); +void TS_MSG_IMPRINT_free(TS_MSG_IMPRINT *a); +int i2d_TS_MSG_IMPRINT(const TS_MSG_IMPRINT *a, unsigned char **pp); +TS_MSG_IMPRINT *d2i_TS_MSG_IMPRINT(TS_MSG_IMPRINT **a, + const unsigned char **pp, long length); + +TS_MSG_IMPRINT *TS_MSG_IMPRINT_dup(TS_MSG_IMPRINT *a); + +#ifndef OPENSSL_NO_STDIO +TS_MSG_IMPRINT *d2i_TS_MSG_IMPRINT_fp(FILE *fp, TS_MSG_IMPRINT **a); +int i2d_TS_MSG_IMPRINT_fp(FILE *fp, TS_MSG_IMPRINT *a); +#endif +TS_MSG_IMPRINT *d2i_TS_MSG_IMPRINT_bio(BIO *bio, TS_MSG_IMPRINT **a); +int i2d_TS_MSG_IMPRINT_bio(BIO *bio, TS_MSG_IMPRINT *a); + +TS_RESP *TS_RESP_new(void); +void TS_RESP_free(TS_RESP *a); +int i2d_TS_RESP(const TS_RESP *a, unsigned char **pp); +TS_RESP *d2i_TS_RESP(TS_RESP **a, const unsigned char **pp, long length); +TS_TST_INFO *PKCS7_to_TS_TST_INFO(PKCS7 *token); +TS_RESP *TS_RESP_dup(TS_RESP *a); + +#ifndef OPENSSL_NO_STDIO +TS_RESP *d2i_TS_RESP_fp(FILE *fp, TS_RESP **a); +int i2d_TS_RESP_fp(FILE *fp, TS_RESP *a); +#endif +TS_RESP *d2i_TS_RESP_bio(BIO *bio, TS_RESP **a); +int i2d_TS_RESP_bio(BIO *bio, TS_RESP *a); + +TS_STATUS_INFO *TS_STATUS_INFO_new(void); +void TS_STATUS_INFO_free(TS_STATUS_INFO *a); +int i2d_TS_STATUS_INFO(const TS_STATUS_INFO *a, unsigned char **pp); +TS_STATUS_INFO *d2i_TS_STATUS_INFO(TS_STATUS_INFO **a, + const unsigned char **pp, long length); +TS_STATUS_INFO *TS_STATUS_INFO_dup(TS_STATUS_INFO *a); + +TS_TST_INFO *TS_TST_INFO_new(void); +void TS_TST_INFO_free(TS_TST_INFO *a); +int i2d_TS_TST_INFO(const TS_TST_INFO *a, unsigned char **pp); +TS_TST_INFO *d2i_TS_TST_INFO(TS_TST_INFO **a, const unsigned char **pp, + long length); +TS_TST_INFO *TS_TST_INFO_dup(TS_TST_INFO *a); + +#ifndef OPENSSL_NO_STDIO +TS_TST_INFO *d2i_TS_TST_INFO_fp(FILE *fp, TS_TST_INFO **a); +int i2d_TS_TST_INFO_fp(FILE *fp, TS_TST_INFO *a); +#endif +TS_TST_INFO *d2i_TS_TST_INFO_bio(BIO *bio, TS_TST_INFO **a); +int i2d_TS_TST_INFO_bio(BIO *bio, TS_TST_INFO *a); + +TS_ACCURACY *TS_ACCURACY_new(void); +void TS_ACCURACY_free(TS_ACCURACY *a); +int i2d_TS_ACCURACY(const TS_ACCURACY *a, unsigned char **pp); +TS_ACCURACY *d2i_TS_ACCURACY(TS_ACCURACY **a, const unsigned char **pp, + long length); +TS_ACCURACY *TS_ACCURACY_dup(TS_ACCURACY *a); + +ESS_ISSUER_SERIAL *ESS_ISSUER_SERIAL_new(void); +void ESS_ISSUER_SERIAL_free(ESS_ISSUER_SERIAL *a); +int i2d_ESS_ISSUER_SERIAL(const ESS_ISSUER_SERIAL *a, unsigned char **pp); +ESS_ISSUER_SERIAL *d2i_ESS_ISSUER_SERIAL(ESS_ISSUER_SERIAL **a, + const unsigned char **pp, + long length); +ESS_ISSUER_SERIAL *ESS_ISSUER_SERIAL_dup(ESS_ISSUER_SERIAL *a); + +ESS_CERT_ID *ESS_CERT_ID_new(void); +void ESS_CERT_ID_free(ESS_CERT_ID *a); +int i2d_ESS_CERT_ID(const ESS_CERT_ID *a, unsigned char **pp); +ESS_CERT_ID *d2i_ESS_CERT_ID(ESS_CERT_ID **a, const unsigned char **pp, + long length); +ESS_CERT_ID *ESS_CERT_ID_dup(ESS_CERT_ID *a); + +ESS_SIGNING_CERT *ESS_SIGNING_CERT_new(void); +void ESS_SIGNING_CERT_free(ESS_SIGNING_CERT *a); +int i2d_ESS_SIGNING_CERT(const ESS_SIGNING_CERT *a, unsigned char **pp); +ESS_SIGNING_CERT *d2i_ESS_SIGNING_CERT(ESS_SIGNING_CERT **a, + const unsigned char **pp, long length); +ESS_SIGNING_CERT *ESS_SIGNING_CERT_dup(ESS_SIGNING_CERT *a); + +int TS_REQ_set_version(TS_REQ *a, long version); +long TS_REQ_get_version(const TS_REQ *a); + +int TS_STATUS_INFO_set_status(TS_STATUS_INFO *a, int i); +const ASN1_INTEGER *TS_STATUS_INFO_get0_status(const TS_STATUS_INFO *a); + +const STACK_OF(ASN1_UTF8STRING) * +TS_STATUS_INFO_get0_text(const TS_STATUS_INFO *a); + +const ASN1_BIT_STRING * +TS_STATUS_INFO_get0_failure_info(const TS_STATUS_INFO *a); + +int TS_REQ_set_msg_imprint(TS_REQ *a, TS_MSG_IMPRINT *msg_imprint); +TS_MSG_IMPRINT *TS_REQ_get_msg_imprint(TS_REQ *a); + +int TS_MSG_IMPRINT_set_algo(TS_MSG_IMPRINT *a, X509_ALGOR *alg); +X509_ALGOR *TS_MSG_IMPRINT_get_algo(TS_MSG_IMPRINT *a); + +int TS_MSG_IMPRINT_set_msg(TS_MSG_IMPRINT *a, unsigned char *d, int len); +ASN1_OCTET_STRING *TS_MSG_IMPRINT_get_msg(TS_MSG_IMPRINT *a); + +int TS_REQ_set_policy_id(TS_REQ *a, const ASN1_OBJECT *policy); +ASN1_OBJECT *TS_REQ_get_policy_id(TS_REQ *a); + +int TS_REQ_set_nonce(TS_REQ *a, const ASN1_INTEGER *nonce); +const ASN1_INTEGER *TS_REQ_get_nonce(const TS_REQ *a); + +int TS_REQ_set_cert_req(TS_REQ *a, int cert_req); +int TS_REQ_get_cert_req(const TS_REQ *a); + +STACK_OF(X509_EXTENSION) *TS_REQ_get_exts(TS_REQ *a); +void TS_REQ_ext_free(TS_REQ *a); +int TS_REQ_get_ext_count(TS_REQ *a); +int TS_REQ_get_ext_by_NID(TS_REQ *a, int nid, int lastpos); +int TS_REQ_get_ext_by_OBJ(TS_REQ *a, const ASN1_OBJECT *obj, int lastpos); +int TS_REQ_get_ext_by_critical(TS_REQ *a, int crit, int lastpos); +X509_EXTENSION *TS_REQ_get_ext(TS_REQ *a, int loc); +X509_EXTENSION *TS_REQ_delete_ext(TS_REQ *a, int loc); +int TS_REQ_add_ext(TS_REQ *a, X509_EXTENSION *ex, int loc); +void *TS_REQ_get_ext_d2i(TS_REQ *a, int nid, int *crit, int *idx); + +/* Function declarations for TS_REQ defined in ts/ts_req_print.c */ + +int TS_REQ_print_bio(BIO *bio, TS_REQ *a); + +/* Function declarations for TS_RESP defined in ts/ts_resp_utils.c */ + +int TS_RESP_set_status_info(TS_RESP *a, TS_STATUS_INFO *info); +TS_STATUS_INFO *TS_RESP_get_status_info(TS_RESP *a); + +/* Caller loses ownership of PKCS7 and TS_TST_INFO objects. */ +void TS_RESP_set_tst_info(TS_RESP *a, PKCS7 *p7, TS_TST_INFO *tst_info); +PKCS7 *TS_RESP_get_token(TS_RESP *a); +TS_TST_INFO *TS_RESP_get_tst_info(TS_RESP *a); + +int TS_TST_INFO_set_version(TS_TST_INFO *a, long version); +long TS_TST_INFO_get_version(const TS_TST_INFO *a); + +int TS_TST_INFO_set_policy_id(TS_TST_INFO *a, ASN1_OBJECT *policy_id); +ASN1_OBJECT *TS_TST_INFO_get_policy_id(TS_TST_INFO *a); + +int TS_TST_INFO_set_msg_imprint(TS_TST_INFO *a, TS_MSG_IMPRINT *msg_imprint); +TS_MSG_IMPRINT *TS_TST_INFO_get_msg_imprint(TS_TST_INFO *a); + +int TS_TST_INFO_set_serial(TS_TST_INFO *a, const ASN1_INTEGER *serial); +const ASN1_INTEGER *TS_TST_INFO_get_serial(const TS_TST_INFO *a); + +int TS_TST_INFO_set_time(TS_TST_INFO *a, const ASN1_GENERALIZEDTIME *gtime); +const ASN1_GENERALIZEDTIME *TS_TST_INFO_get_time(const TS_TST_INFO *a); + +int TS_TST_INFO_set_accuracy(TS_TST_INFO *a, TS_ACCURACY *accuracy); +TS_ACCURACY *TS_TST_INFO_get_accuracy(TS_TST_INFO *a); + +int TS_ACCURACY_set_seconds(TS_ACCURACY *a, const ASN1_INTEGER *seconds); +const ASN1_INTEGER *TS_ACCURACY_get_seconds(const TS_ACCURACY *a); + +int TS_ACCURACY_set_millis(TS_ACCURACY *a, const ASN1_INTEGER *millis); +const ASN1_INTEGER *TS_ACCURACY_get_millis(const TS_ACCURACY *a); + +int TS_ACCURACY_set_micros(TS_ACCURACY *a, const ASN1_INTEGER *micros); +const ASN1_INTEGER *TS_ACCURACY_get_micros(const TS_ACCURACY *a); + +int TS_TST_INFO_set_ordering(TS_TST_INFO *a, int ordering); +int TS_TST_INFO_get_ordering(const TS_TST_INFO *a); + +int TS_TST_INFO_set_nonce(TS_TST_INFO *a, const ASN1_INTEGER *nonce); +const ASN1_INTEGER *TS_TST_INFO_get_nonce(const TS_TST_INFO *a); + +int TS_TST_INFO_set_tsa(TS_TST_INFO *a, GENERAL_NAME *tsa); +GENERAL_NAME *TS_TST_INFO_get_tsa(TS_TST_INFO *a); + +STACK_OF(X509_EXTENSION) *TS_TST_INFO_get_exts(TS_TST_INFO *a); +void TS_TST_INFO_ext_free(TS_TST_INFO *a); +int TS_TST_INFO_get_ext_count(TS_TST_INFO *a); +int TS_TST_INFO_get_ext_by_NID(TS_TST_INFO *a, int nid, int lastpos); +int TS_TST_INFO_get_ext_by_OBJ(TS_TST_INFO *a, const ASN1_OBJECT *obj, + int lastpos); +int TS_TST_INFO_get_ext_by_critical(TS_TST_INFO *a, int crit, int lastpos); +X509_EXTENSION *TS_TST_INFO_get_ext(TS_TST_INFO *a, int loc); +X509_EXTENSION *TS_TST_INFO_delete_ext(TS_TST_INFO *a, int loc); +int TS_TST_INFO_add_ext(TS_TST_INFO *a, X509_EXTENSION *ex, int loc); +void *TS_TST_INFO_get_ext_d2i(TS_TST_INFO *a, int nid, int *crit, int *idx); + +/* + * Declarations related to response generation, defined in ts/ts_resp_sign.c. + */ + +/* Optional flags for response generation. */ + +/* Don't include the TSA name in response. */ +# define TS_TSA_NAME 0x01 + +/* Set ordering to true in response. */ +# define TS_ORDERING 0x02 + +/* + * Include the signer certificate and the other specified certificates in + * the ESS signing certificate attribute beside the PKCS7 signed data. + * Only the signer certificates is included by default. + */ +# define TS_ESS_CERT_ID_CHAIN 0x04 + +/* Forward declaration. */ +struct TS_resp_ctx; + +/* This must return a unique number less than 160 bits long. */ +typedef ASN1_INTEGER *(*TS_serial_cb) (struct TS_resp_ctx *, void *); + +/* + * This must return the seconds and microseconds since Jan 1, 1970 in the sec + * and usec variables allocated by the caller. Return non-zero for success + * and zero for failure. + */ +typedef int (*TS_time_cb) (struct TS_resp_ctx *, void *, long *sec, + long *usec); + +/* + * This must process the given extension. It can modify the TS_TST_INFO + * object of the context. Return values: !0 (processed), 0 (error, it must + * set the status info/failure info of the response). + */ +typedef int (*TS_extension_cb) (struct TS_resp_ctx *, X509_EXTENSION *, + void *); + +typedef struct TS_resp_ctx TS_RESP_CTX; + +DEFINE_STACK_OF_CONST(EVP_MD) + +/* Creates a response context that can be used for generating responses. */ +TS_RESP_CTX *TS_RESP_CTX_new(void); +void TS_RESP_CTX_free(TS_RESP_CTX *ctx); + +/* This parameter must be set. */ +int TS_RESP_CTX_set_signer_cert(TS_RESP_CTX *ctx, X509 *signer); + +/* This parameter must be set. */ +int TS_RESP_CTX_set_signer_key(TS_RESP_CTX *ctx, EVP_PKEY *key); + +int TS_RESP_CTX_set_signer_digest(TS_RESP_CTX *ctx, + const EVP_MD *signer_digest); + +/* This parameter must be set. */ +int TS_RESP_CTX_set_def_policy(TS_RESP_CTX *ctx, const ASN1_OBJECT *def_policy); + +/* No additional certs are included in the response by default. */ +int TS_RESP_CTX_set_certs(TS_RESP_CTX *ctx, STACK_OF(X509) *certs); + +/* + * Adds a new acceptable policy, only the default policy is accepted by + * default. + */ +int TS_RESP_CTX_add_policy(TS_RESP_CTX *ctx, const ASN1_OBJECT *policy); + +/* + * Adds a new acceptable message digest. Note that no message digests are + * accepted by default. The md argument is shared with the caller. + */ +int TS_RESP_CTX_add_md(TS_RESP_CTX *ctx, const EVP_MD *md); + +/* Accuracy is not included by default. */ +int TS_RESP_CTX_set_accuracy(TS_RESP_CTX *ctx, + int secs, int millis, int micros); + +/* + * Clock precision digits, i.e. the number of decimal digits: '0' means sec, + * '3' msec, '6' usec, and so on. Default is 0. + */ +int TS_RESP_CTX_set_clock_precision_digits(TS_RESP_CTX *ctx, + unsigned clock_precision_digits); +/* At most we accept usec precision. */ +# define TS_MAX_CLOCK_PRECISION_DIGITS 6 + +/* Maximum status message length */ +# define TS_MAX_STATUS_LENGTH (1024 * 1024) + +/* No flags are set by default. */ +void TS_RESP_CTX_add_flags(TS_RESP_CTX *ctx, int flags); + +/* Default callback always returns a constant. */ +void TS_RESP_CTX_set_serial_cb(TS_RESP_CTX *ctx, TS_serial_cb cb, void *data); + +/* Default callback uses the gettimeofday() and gmtime() system calls. */ +void TS_RESP_CTX_set_time_cb(TS_RESP_CTX *ctx, TS_time_cb cb, void *data); + +/* + * Default callback rejects all extensions. The extension callback is called + * when the TS_TST_INFO object is already set up and not signed yet. + */ +/* FIXME: extension handling is not tested yet. */ +void TS_RESP_CTX_set_extension_cb(TS_RESP_CTX *ctx, + TS_extension_cb cb, void *data); + +/* The following methods can be used in the callbacks. */ +int TS_RESP_CTX_set_status_info(TS_RESP_CTX *ctx, + int status, const char *text); + +/* Sets the status info only if it is still TS_STATUS_GRANTED. */ +int TS_RESP_CTX_set_status_info_cond(TS_RESP_CTX *ctx, + int status, const char *text); + +int TS_RESP_CTX_add_failure_info(TS_RESP_CTX *ctx, int failure); + +/* The get methods below can be used in the extension callback. */ +TS_REQ *TS_RESP_CTX_get_request(TS_RESP_CTX *ctx); + +TS_TST_INFO *TS_RESP_CTX_get_tst_info(TS_RESP_CTX *ctx); + +/* + * Creates the signed TS_TST_INFO and puts it in TS_RESP. + * In case of errors it sets the status info properly. + * Returns NULL only in case of memory allocation/fatal error. + */ +TS_RESP *TS_RESP_create_response(TS_RESP_CTX *ctx, BIO *req_bio); + +/* + * Declarations related to response verification, + * they are defined in ts/ts_resp_verify.c. + */ + +int TS_RESP_verify_signature(PKCS7 *token, STACK_OF(X509) *certs, + X509_STORE *store, X509 **signer_out); + +/* Context structure for the generic verify method. */ + +/* Verify the signer's certificate and the signature of the response. */ +# define TS_VFY_SIGNATURE (1u << 0) +/* Verify the version number of the response. */ +# define TS_VFY_VERSION (1u << 1) +/* Verify if the policy supplied by the user matches the policy of the TSA. */ +# define TS_VFY_POLICY (1u << 2) +/* + * Verify the message imprint provided by the user. This flag should not be + * specified with TS_VFY_DATA. + */ +# define TS_VFY_IMPRINT (1u << 3) +/* + * Verify the message imprint computed by the verify method from the user + * provided data and the MD algorithm of the response. This flag should not + * be specified with TS_VFY_IMPRINT. + */ +# define TS_VFY_DATA (1u << 4) +/* Verify the nonce value. */ +# define TS_VFY_NONCE (1u << 5) +/* Verify if the TSA name field matches the signer certificate. */ +# define TS_VFY_SIGNER (1u << 6) +/* Verify if the TSA name field equals to the user provided name. */ +# define TS_VFY_TSA_NAME (1u << 7) + +/* You can use the following convenience constants. */ +# define TS_VFY_ALL_IMPRINT (TS_VFY_SIGNATURE \ + | TS_VFY_VERSION \ + | TS_VFY_POLICY \ + | TS_VFY_IMPRINT \ + | TS_VFY_NONCE \ + | TS_VFY_SIGNER \ + | TS_VFY_TSA_NAME) +# define TS_VFY_ALL_DATA (TS_VFY_SIGNATURE \ + | TS_VFY_VERSION \ + | TS_VFY_POLICY \ + | TS_VFY_DATA \ + | TS_VFY_NONCE \ + | TS_VFY_SIGNER \ + | TS_VFY_TSA_NAME) + +typedef struct TS_verify_ctx TS_VERIFY_CTX; + +int TS_RESP_verify_response(TS_VERIFY_CTX *ctx, TS_RESP *response); +int TS_RESP_verify_token(TS_VERIFY_CTX *ctx, PKCS7 *token); + +/* + * Declarations related to response verification context, + */ +TS_VERIFY_CTX *TS_VERIFY_CTX_new(void); +void TS_VERIFY_CTX_init(TS_VERIFY_CTX *ctx); +void TS_VERIFY_CTX_free(TS_VERIFY_CTX *ctx); +void TS_VERIFY_CTX_cleanup(TS_VERIFY_CTX *ctx); +int TS_VERIFY_CTX_set_flags(TS_VERIFY_CTX *ctx, int f); +int TS_VERIFY_CTX_add_flags(TS_VERIFY_CTX *ctx, int f); +BIO *TS_VERIFY_CTX_set_data(TS_VERIFY_CTX *ctx, BIO *b); +unsigned char *TS_VERIFY_CTX_set_imprint(TS_VERIFY_CTX *ctx, + unsigned char *hexstr, long len); +X509_STORE *TS_VERIFY_CTX_set_store(TS_VERIFY_CTX *ctx, X509_STORE *s); +STACK_OF(X509) *TS_VERIFY_CTS_set_certs(TS_VERIFY_CTX *ctx, STACK_OF(X509) *certs); + +/*- + * If ctx is NULL, it allocates and returns a new object, otherwise + * it returns ctx. It initialises all the members as follows: + * flags = TS_VFY_ALL_IMPRINT & ~(TS_VFY_TSA_NAME | TS_VFY_SIGNATURE) + * certs = NULL + * store = NULL + * policy = policy from the request or NULL if absent (in this case + * TS_VFY_POLICY is cleared from flags as well) + * md_alg = MD algorithm from request + * imprint, imprint_len = imprint from request + * data = NULL + * nonce, nonce_len = nonce from the request or NULL if absent (in this case + * TS_VFY_NONCE is cleared from flags as well) + * tsa_name = NULL + * Important: after calling this method TS_VFY_SIGNATURE should be added! + */ +TS_VERIFY_CTX *TS_REQ_to_TS_VERIFY_CTX(TS_REQ *req, TS_VERIFY_CTX *ctx); + +/* Function declarations for TS_RESP defined in ts/ts_resp_print.c */ + +int TS_RESP_print_bio(BIO *bio, TS_RESP *a); +int TS_STATUS_INFO_print_bio(BIO *bio, TS_STATUS_INFO *a); +int TS_TST_INFO_print_bio(BIO *bio, TS_TST_INFO *a); + +/* Common utility functions defined in ts/ts_lib.c */ + +int TS_ASN1_INTEGER_print_bio(BIO *bio, const ASN1_INTEGER *num); +int TS_OBJ_print_bio(BIO *bio, const ASN1_OBJECT *obj); +int TS_ext_print_bio(BIO *bio, const STACK_OF(X509_EXTENSION) *extensions); +int TS_X509_ALGOR_print_bio(BIO *bio, const X509_ALGOR *alg); +int TS_MSG_IMPRINT_print_bio(BIO *bio, TS_MSG_IMPRINT *msg); + +/* + * Function declarations for handling configuration options, defined in + * ts/ts_conf.c + */ + +X509 *TS_CONF_load_cert(const char *file); +STACK_OF(X509) *TS_CONF_load_certs(const char *file); +EVP_PKEY *TS_CONF_load_key(const char *file, const char *pass); +const char *TS_CONF_get_tsa_section(CONF *conf, const char *section); +int TS_CONF_set_serial(CONF *conf, const char *section, TS_serial_cb cb, + TS_RESP_CTX *ctx); +#ifndef OPENSSL_NO_ENGINE +int TS_CONF_set_crypto_device(CONF *conf, const char *section, + const char *device); +int TS_CONF_set_default_engine(const char *name); +#endif +int TS_CONF_set_signer_cert(CONF *conf, const char *section, + const char *cert, TS_RESP_CTX *ctx); +int TS_CONF_set_certs(CONF *conf, const char *section, const char *certs, + TS_RESP_CTX *ctx); +int TS_CONF_set_signer_key(CONF *conf, const char *section, + const char *key, const char *pass, + TS_RESP_CTX *ctx); +int TS_CONF_set_signer_digest(CONF *conf, const char *section, + const char *md, TS_RESP_CTX *ctx); +int TS_CONF_set_def_policy(CONF *conf, const char *section, + const char *policy, TS_RESP_CTX *ctx); +int TS_CONF_set_policies(CONF *conf, const char *section, TS_RESP_CTX *ctx); +int TS_CONF_set_digests(CONF *conf, const char *section, TS_RESP_CTX *ctx); +int TS_CONF_set_accuracy(CONF *conf, const char *section, TS_RESP_CTX *ctx); +int TS_CONF_set_clock_precision_digits(CONF *conf, const char *section, + TS_RESP_CTX *ctx); +int TS_CONF_set_ordering(CONF *conf, const char *section, TS_RESP_CTX *ctx); +int TS_CONF_set_tsa_name(CONF *conf, const char *section, TS_RESP_CTX *ctx); +int TS_CONF_set_ess_cert_id_chain(CONF *conf, const char *section, + TS_RESP_CTX *ctx); + +/* -------------------------------------------------- */ +/* BEGIN ERROR CODES */ +/* + * The following lines are auto generated by the script mkerr.pl. Any changes + * made after this point may be overwritten when the script is next run. + */ + +int ERR_load_TS_strings(void); + +/* Error codes for the TS functions. */ + +/* Function codes. */ +# define TS_F_DEF_SERIAL_CB 110 +# define TS_F_DEF_TIME_CB 111 +# define TS_F_ESS_ADD_SIGNING_CERT 112 +# define TS_F_ESS_CERT_ID_NEW_INIT 113 +# define TS_F_ESS_SIGNING_CERT_NEW_INIT 114 +# define TS_F_INT_TS_RESP_VERIFY_TOKEN 149 +# define TS_F_PKCS7_TO_TS_TST_INFO 148 +# define TS_F_TS_ACCURACY_SET_MICROS 115 +# define TS_F_TS_ACCURACY_SET_MILLIS 116 +# define TS_F_TS_ACCURACY_SET_SECONDS 117 +# define TS_F_TS_CHECK_IMPRINTS 100 +# define TS_F_TS_CHECK_NONCES 101 +# define TS_F_TS_CHECK_POLICY 102 +# define TS_F_TS_CHECK_SIGNING_CERTS 103 +# define TS_F_TS_CHECK_STATUS_INFO 104 +# define TS_F_TS_COMPUTE_IMPRINT 145 +# define TS_F_TS_CONF_INVALID 151 +# define TS_F_TS_CONF_LOAD_CERT 153 +# define TS_F_TS_CONF_LOAD_CERTS 154 +# define TS_F_TS_CONF_LOAD_KEY 155 +# define TS_F_TS_CONF_LOOKUP_FAIL 152 +# define TS_F_TS_CONF_SET_DEFAULT_ENGINE 146 +# define TS_F_TS_GET_STATUS_TEXT 105 +# define TS_F_TS_MSG_IMPRINT_SET_ALGO 118 +# define TS_F_TS_REQ_SET_MSG_IMPRINT 119 +# define TS_F_TS_REQ_SET_NONCE 120 +# define TS_F_TS_REQ_SET_POLICY_ID 121 +# define TS_F_TS_RESP_CREATE_RESPONSE 122 +# define TS_F_TS_RESP_CREATE_TST_INFO 123 +# define TS_F_TS_RESP_CTX_ADD_FAILURE_INFO 124 +# define TS_F_TS_RESP_CTX_ADD_MD 125 +# define TS_F_TS_RESP_CTX_ADD_POLICY 126 +# define TS_F_TS_RESP_CTX_NEW 127 +# define TS_F_TS_RESP_CTX_SET_ACCURACY 128 +# define TS_F_TS_RESP_CTX_SET_CERTS 129 +# define TS_F_TS_RESP_CTX_SET_DEF_POLICY 130 +# define TS_F_TS_RESP_CTX_SET_SIGNER_CERT 131 +# define TS_F_TS_RESP_CTX_SET_STATUS_INFO 132 +# define TS_F_TS_RESP_GET_POLICY 133 +# define TS_F_TS_RESP_SET_GENTIME_WITH_PRECISION 134 +# define TS_F_TS_RESP_SET_STATUS_INFO 135 +# define TS_F_TS_RESP_SET_TST_INFO 150 +# define TS_F_TS_RESP_SIGN 136 +# define TS_F_TS_RESP_VERIFY_SIGNATURE 106 +# define TS_F_TS_TST_INFO_SET_ACCURACY 137 +# define TS_F_TS_TST_INFO_SET_MSG_IMPRINT 138 +# define TS_F_TS_TST_INFO_SET_NONCE 139 +# define TS_F_TS_TST_INFO_SET_POLICY_ID 140 +# define TS_F_TS_TST_INFO_SET_SERIAL 141 +# define TS_F_TS_TST_INFO_SET_TIME 142 +# define TS_F_TS_TST_INFO_SET_TSA 143 +# define TS_F_TS_VERIFY 108 +# define TS_F_TS_VERIFY_CERT 109 +# define TS_F_TS_VERIFY_CTX_NEW 144 + +/* Reason codes. */ +# define TS_R_BAD_PKCS7_TYPE 132 +# define TS_R_BAD_TYPE 133 +# define TS_R_CANNOT_LOAD_CERT 137 +# define TS_R_CANNOT_LOAD_KEY 138 +# define TS_R_CERTIFICATE_VERIFY_ERROR 100 +# define TS_R_COULD_NOT_SET_ENGINE 127 +# define TS_R_COULD_NOT_SET_TIME 115 +# define TS_R_DETACHED_CONTENT 134 +# define TS_R_ESS_ADD_SIGNING_CERT_ERROR 116 +# define TS_R_ESS_SIGNING_CERTIFICATE_ERROR 101 +# define TS_R_INVALID_NULL_POINTER 102 +# define TS_R_INVALID_SIGNER_CERTIFICATE_PURPOSE 117 +# define TS_R_MESSAGE_IMPRINT_MISMATCH 103 +# define TS_R_NONCE_MISMATCH 104 +# define TS_R_NONCE_NOT_RETURNED 105 +# define TS_R_NO_CONTENT 106 +# define TS_R_NO_TIME_STAMP_TOKEN 107 +# define TS_R_PKCS7_ADD_SIGNATURE_ERROR 118 +# define TS_R_PKCS7_ADD_SIGNED_ATTR_ERROR 119 +# define TS_R_PKCS7_TO_TS_TST_INFO_FAILED 129 +# define TS_R_POLICY_MISMATCH 108 +# define TS_R_PRIVATE_KEY_DOES_NOT_MATCH_CERTIFICATE 120 +# define TS_R_RESPONSE_SETUP_ERROR 121 +# define TS_R_SIGNATURE_FAILURE 109 +# define TS_R_THERE_MUST_BE_ONE_SIGNER 110 +# define TS_R_TIME_SYSCALL_ERROR 122 +# define TS_R_TOKEN_NOT_PRESENT 130 +# define TS_R_TOKEN_PRESENT 131 +# define TS_R_TSA_NAME_MISMATCH 111 +# define TS_R_TSA_UNTRUSTED 112 +# define TS_R_TST_INFO_SETUP_ERROR 123 +# define TS_R_TS_DATASIGN 124 +# define TS_R_UNACCEPTABLE_POLICY 125 +# define TS_R_UNSUPPORTED_MD_ALGORITHM 126 +# define TS_R_UNSUPPORTED_VERSION 113 +# define TS_R_VAR_BAD_VALUE 135 +# define TS_R_VAR_LOOKUP_FAILURE 136 +# define TS_R_WRONG_CONTENT_TYPE 114 + +# ifdef __cplusplus +} +# endif +# endif +#endif diff --git a/mobile/ios/FxA/FxA/include/openssl/txt_db.h b/mobile/ios/FxA/FxA/include/openssl/txt_db.h new file mode 100644 index 0000000000..0e6c943e0e --- /dev/null +++ b/mobile/ios/FxA/FxA/include/openssl/txt_db.h @@ -0,0 +1,57 @@ +/* + * Copyright 1995-2016 The OpenSSL Project Authors. All Rights Reserved. + * + * Licensed under the OpenSSL license (the "License"). You may not use + * this file except in compliance with the License. You can obtain a copy + * in the file LICENSE in the source distribution or at + * https://www.openssl.org/source/license.html + */ + +#ifndef HEADER_TXT_DB_H +# define HEADER_TXT_DB_H + +# include +# include +# include +# include + +# define DB_ERROR_OK 0 +# define DB_ERROR_MALLOC 1 +# define DB_ERROR_INDEX_CLASH 2 +# define DB_ERROR_INDEX_OUT_OF_RANGE 3 +# define DB_ERROR_NO_INDEX 4 +# define DB_ERROR_INSERT_INDEX_CLASH 5 +# define DB_ERROR_WRONG_NUM_FIELDS 6 + +#ifdef __cplusplus +extern "C" { +#endif + +typedef OPENSSL_STRING *OPENSSL_PSTRING; +DEFINE_SPECIAL_STACK_OF(OPENSSL_PSTRING, OPENSSL_STRING) + +typedef struct txt_db_st { + int num_fields; + STACK_OF(OPENSSL_PSTRING) *data; + LHASH_OF(OPENSSL_STRING) **index; + int (**qual) (OPENSSL_STRING *); + long error; + long arg1; + long arg2; + OPENSSL_STRING *arg_row; +} TXT_DB; + +TXT_DB *TXT_DB_read(BIO *in, int num); +long TXT_DB_write(BIO *out, TXT_DB *db); +int TXT_DB_create_index(TXT_DB *db, int field, int (*qual) (OPENSSL_STRING *), + OPENSSL_LH_HASHFUNC hash, OPENSSL_LH_COMPFUNC cmp); +void TXT_DB_free(TXT_DB *db); +OPENSSL_STRING *TXT_DB_get_by_index(TXT_DB *db, int idx, + OPENSSL_STRING *value); +int TXT_DB_insert(TXT_DB *db, OPENSSL_STRING *value); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/mobile/ios/FxA/FxA/include/openssl/ui.h b/mobile/ios/FxA/FxA/include/openssl/ui.h new file mode 100644 index 0000000000..49e763de3e --- /dev/null +++ b/mobile/ios/FxA/FxA/include/openssl/ui.h @@ -0,0 +1,378 @@ +/* + * Copyright 2001-2016 The OpenSSL Project Authors. All Rights Reserved. + * + * Licensed under the OpenSSL license (the "License"). You may not use + * this file except in compliance with the License. You can obtain a copy + * in the file LICENSE in the source distribution or at + * https://www.openssl.org/source/license.html + */ + +#ifndef HEADER_UI_H +# define HEADER_UI_H + +# include + +# ifndef OPENSSL_NO_UI + +# if OPENSSL_API_COMPAT < 0x10100000L +# include +# endif +# include +# include + +#ifdef __cplusplus +extern "C" { +#endif + +/* + * All the following functions return -1 or NULL on error and in some cases + * (UI_process()) -2 if interrupted or in some other way cancelled. When + * everything is fine, they return 0, a positive value or a non-NULL pointer, + * all depending on their purpose. + */ + +/* Creators and destructor. */ +UI *UI_new(void); +UI *UI_new_method(const UI_METHOD *method); +void UI_free(UI *ui); + +/*- + The following functions are used to add strings to be printed and prompt + strings to prompt for data. The names are UI_{add,dup}__string + and UI_{add,dup}_input_boolean. + + UI_{add,dup}__string have the following meanings: + add add a text or prompt string. The pointers given to these + functions are used verbatim, no copying is done. + dup make a copy of the text or prompt string, then add the copy + to the collection of strings in the user interface. + + The function is a name for the functionality that the given + string shall be used for. It can be one of: + input use the string as data prompt. + verify use the string as verification prompt. This + is used to verify a previous input. + info use the string for informational output. + error use the string for error output. + Honestly, there's currently no difference between info and error for the + moment. + + UI_{add,dup}_input_boolean have the same semantics for "add" and "dup", + and are typically used when one wants to prompt for a yes/no response. + + All of the functions in this group take a UI and a prompt string. + The string input and verify addition functions also take a flag argument, + a buffer for the result to end up with, a minimum input size and a maximum + input size (the result buffer MUST be large enough to be able to contain + the maximum number of characters). Additionally, the verify addition + functions takes another buffer to compare the result against. + The boolean input functions take an action description string (which should + be safe to ignore if the expected user action is obvious, for example with + a dialog box with an OK button and a Cancel button), a string of acceptable + characters to mean OK and to mean Cancel. The two last strings are checked + to make sure they don't have common characters. Additionally, the same + flag argument as for the string input is taken, as well as a result buffer. + The result buffer is required to be at least one byte long. Depending on + the answer, the first character from the OK or the Cancel character strings + will be stored in the first byte of the result buffer. No NUL will be + added, so the result is *not* a string. + + On success, the all return an index of the added information. That index + is useful when retrieving results with UI_get0_result(). */ +int UI_add_input_string(UI *ui, const char *prompt, int flags, + char *result_buf, int minsize, int maxsize); +int UI_dup_input_string(UI *ui, const char *prompt, int flags, + char *result_buf, int minsize, int maxsize); +int UI_add_verify_string(UI *ui, const char *prompt, int flags, + char *result_buf, int minsize, int maxsize, + const char *test_buf); +int UI_dup_verify_string(UI *ui, const char *prompt, int flags, + char *result_buf, int minsize, int maxsize, + const char *test_buf); +int UI_add_input_boolean(UI *ui, const char *prompt, const char *action_desc, + const char *ok_chars, const char *cancel_chars, + int flags, char *result_buf); +int UI_dup_input_boolean(UI *ui, const char *prompt, const char *action_desc, + const char *ok_chars, const char *cancel_chars, + int flags, char *result_buf); +int UI_add_info_string(UI *ui, const char *text); +int UI_dup_info_string(UI *ui, const char *text); +int UI_add_error_string(UI *ui, const char *text); +int UI_dup_error_string(UI *ui, const char *text); + +/* These are the possible flags. They can be or'ed together. */ +/* Use to have echoing of input */ +# define UI_INPUT_FLAG_ECHO 0x01 +/* + * Use a default password. Where that password is found is completely up to + * the application, it might for example be in the user data set with + * UI_add_user_data(). It is not recommended to have more than one input in + * each UI being marked with this flag, or the application might get + * confused. + */ +# define UI_INPUT_FLAG_DEFAULT_PWD 0x02 + +/*- + * The user of these routines may want to define flags of their own. The core + * UI won't look at those, but will pass them on to the method routines. They + * must use higher bits so they don't get confused with the UI bits above. + * UI_INPUT_FLAG_USER_BASE tells which is the lowest bit to use. A good + * example of use is this: + * + * #define MY_UI_FLAG1 (0x01 << UI_INPUT_FLAG_USER_BASE) + * +*/ +# define UI_INPUT_FLAG_USER_BASE 16 + +/*- + * The following function helps construct a prompt. object_desc is a + * textual short description of the object, for example "pass phrase", + * and object_name is the name of the object (might be a card name or + * a file name. + * The returned string shall always be allocated on the heap with + * OPENSSL_malloc(), and need to be free'd with OPENSSL_free(). + * + * If the ui_method doesn't contain a pointer to a user-defined prompt + * constructor, a default string is built, looking like this: + * + * "Enter {object_desc} for {object_name}:" + * + * So, if object_desc has the value "pass phrase" and object_name has + * the value "foo.key", the resulting string is: + * + * "Enter pass phrase for foo.key:" +*/ +char *UI_construct_prompt(UI *ui_method, + const char *object_desc, const char *object_name); + +/* + * The following function is used to store a pointer to user-specific data. + * Any previous such pointer will be returned and replaced. + * + * For callback purposes, this function makes a lot more sense than using + * ex_data, since the latter requires that different parts of OpenSSL or + * applications share the same ex_data index. + * + * Note that the UI_OpenSSL() method completely ignores the user data. Other + * methods may not, however. + */ +void *UI_add_user_data(UI *ui, void *user_data); +/* We need a user data retrieving function as well. */ +void *UI_get0_user_data(UI *ui); + +/* Return the result associated with a prompt given with the index i. */ +const char *UI_get0_result(UI *ui, int i); + +/* When all strings have been added, process the whole thing. */ +int UI_process(UI *ui); + +/* + * Give a user interface parametrised control commands. This can be used to + * send down an integer, a data pointer or a function pointer, as well as be + * used to get information from a UI. + */ +int UI_ctrl(UI *ui, int cmd, long i, void *p, void (*f) (void)); + +/* The commands */ +/* + * Use UI_CONTROL_PRINT_ERRORS with the value 1 to have UI_process print the + * OpenSSL error stack before printing any info or added error messages and + * before any prompting. + */ +# define UI_CTRL_PRINT_ERRORS 1 +/* + * Check if a UI_process() is possible to do again with the same instance of + * a user interface. This makes UI_ctrl() return 1 if it is redoable, and 0 + * if not. + */ +# define UI_CTRL_IS_REDOABLE 2 + +/* Some methods may use extra data */ +# define UI_set_app_data(s,arg) UI_set_ex_data(s,0,arg) +# define UI_get_app_data(s) UI_get_ex_data(s,0) + +#define UI_get_ex_new_index(l, p, newf, dupf, freef) \ + CRYPTO_get_ex_new_index(CRYPTO_EX_INDEX_UI, l, p, newf, dupf, freef) +int UI_set_ex_data(UI *r, int idx, void *arg); +void *UI_get_ex_data(UI *r, int idx); + +/* Use specific methods instead of the built-in one */ +void UI_set_default_method(const UI_METHOD *meth); +const UI_METHOD *UI_get_default_method(void); +const UI_METHOD *UI_get_method(UI *ui); +const UI_METHOD *UI_set_method(UI *ui, const UI_METHOD *meth); + +/* The method with all the built-in thingies */ +UI_METHOD *UI_OpenSSL(void); + +/* ---------- For method writers ---------- */ +/*- + A method contains a number of functions that implement the low level + of the User Interface. The functions are: + + an opener This function starts a session, maybe by opening + a channel to a tty, or by opening a window. + a writer This function is called to write a given string, + maybe to the tty, maybe as a field label in a + window. + a flusher This function is called to flush everything that + has been output so far. It can be used to actually + display a dialog box after it has been built. + a reader This function is called to read a given prompt, + maybe from the tty, maybe from a field in a + window. Note that it's called with all string + structures, not only the prompt ones, so it must + check such things itself. + a closer This function closes the session, maybe by closing + the channel to the tty, or closing the window. + + All these functions are expected to return: + + 0 on error. + 1 on success. + -1 on out-of-band events, for example if some prompting has + been canceled (by pressing Ctrl-C, for example). This is + only checked when returned by the flusher or the reader. + + The way this is used, the opener is first called, then the writer for all + strings, then the flusher, then the reader for all strings and finally the + closer. Note that if you want to prompt from a terminal or other command + line interface, the best is to have the reader also write the prompts + instead of having the writer do it. If you want to prompt from a dialog + box, the writer can be used to build up the contents of the box, and the + flusher to actually display the box and run the event loop until all data + has been given, after which the reader only grabs the given data and puts + them back into the UI strings. + + All method functions take a UI as argument. Additionally, the writer and + the reader take a UI_STRING. +*/ + +/* + * The UI_STRING type is the data structure that contains all the needed info + * about a string or a prompt, including test data for a verification prompt. + */ +typedef struct ui_string_st UI_STRING; +DEFINE_STACK_OF(UI_STRING) + +/* + * The different types of strings that are currently supported. This is only + * needed by method authors. + */ +enum UI_string_types { + UIT_NONE = 0, + UIT_PROMPT, /* Prompt for a string */ + UIT_VERIFY, /* Prompt for a string and verify */ + UIT_BOOLEAN, /* Prompt for a yes/no response */ + UIT_INFO, /* Send info to the user */ + UIT_ERROR /* Send an error message to the user */ +}; + +/* Create and manipulate methods */ +UI_METHOD *UI_create_method(const char *name); +void UI_destroy_method(UI_METHOD *ui_method); +int UI_method_set_opener(UI_METHOD *method, int (*opener) (UI *ui)); +int UI_method_set_writer(UI_METHOD *method, + int (*writer) (UI *ui, UI_STRING *uis)); +int UI_method_set_flusher(UI_METHOD *method, int (*flusher) (UI *ui)); +int UI_method_set_reader(UI_METHOD *method, + int (*reader) (UI *ui, UI_STRING *uis)); +int UI_method_set_closer(UI_METHOD *method, int (*closer) (UI *ui)); +int UI_method_set_prompt_constructor(UI_METHOD *method, + char *(*prompt_constructor) (UI *ui, + const char + *object_desc, + const char + *object_name)); +int (*UI_method_get_opener(UI_METHOD *method)) (UI *); +int (*UI_method_get_writer(UI_METHOD *method)) (UI *, UI_STRING *); +int (*UI_method_get_flusher(UI_METHOD *method)) (UI *); +int (*UI_method_get_reader(UI_METHOD *method)) (UI *, UI_STRING *); +int (*UI_method_get_closer(UI_METHOD *method)) (UI *); +char *(*UI_method_get_prompt_constructor(UI_METHOD *method)) (UI *, + const char *, + const char *); + +/* + * The following functions are helpers for method writers to access relevant + * data from a UI_STRING. + */ + +/* Return type of the UI_STRING */ +enum UI_string_types UI_get_string_type(UI_STRING *uis); +/* Return input flags of the UI_STRING */ +int UI_get_input_flags(UI_STRING *uis); +/* Return the actual string to output (the prompt, info or error) */ +const char *UI_get0_output_string(UI_STRING *uis); +/* + * Return the optional action string to output (the boolean prompt + * instruction) + */ +const char *UI_get0_action_string(UI_STRING *uis); +/* Return the result of a prompt */ +const char *UI_get0_result_string(UI_STRING *uis); +/* + * Return the string to test the result against. Only useful with verifies. + */ +const char *UI_get0_test_string(UI_STRING *uis); +/* Return the required minimum size of the result */ +int UI_get_result_minsize(UI_STRING *uis); +/* Return the required maximum size of the result */ +int UI_get_result_maxsize(UI_STRING *uis); +/* Set the result of a UI_STRING. */ +int UI_set_result(UI *ui, UI_STRING *uis, const char *result); + +/* A couple of popular utility functions */ +int UI_UTIL_read_pw_string(char *buf, int length, const char *prompt, + int verify); +int UI_UTIL_read_pw(char *buf, char *buff, int size, const char *prompt, + int verify); + +/* BEGIN ERROR CODES */ +/* + * The following lines are auto generated by the script mkerr.pl. Any changes + * made after this point may be overwritten when the script is next run. + */ + +int ERR_load_UI_strings(void); + +/* Error codes for the UI functions. */ + +/* Function codes. */ +# define UI_F_CLOSE_CONSOLE 115 +# define UI_F_ECHO_CONSOLE 116 +# define UI_F_GENERAL_ALLOCATE_BOOLEAN 108 +# define UI_F_GENERAL_ALLOCATE_PROMPT 109 +# define UI_F_NOECHO_CONSOLE 117 +# define UI_F_OPEN_CONSOLE 114 +# define UI_F_UI_CREATE_METHOD 112 +# define UI_F_UI_CTRL 111 +# define UI_F_UI_DUP_ERROR_STRING 101 +# define UI_F_UI_DUP_INFO_STRING 102 +# define UI_F_UI_DUP_INPUT_BOOLEAN 110 +# define UI_F_UI_DUP_INPUT_STRING 103 +# define UI_F_UI_DUP_VERIFY_STRING 106 +# define UI_F_UI_GET0_RESULT 107 +# define UI_F_UI_NEW_METHOD 104 +# define UI_F_UI_PROCESS 113 +# define UI_F_UI_SET_RESULT 105 + +/* Reason codes. */ +# define UI_R_COMMON_OK_AND_CANCEL_CHARACTERS 104 +# define UI_R_INDEX_TOO_LARGE 102 +# define UI_R_INDEX_TOO_SMALL 103 +# define UI_R_NO_RESULT_BUFFER 105 +# define UI_R_PROCESSING_ERROR 107 +# define UI_R_RESULT_TOO_LARGE 100 +# define UI_R_RESULT_TOO_SMALL 101 +# define UI_R_SYSASSIGN_ERROR 109 +# define UI_R_SYSDASSGN_ERROR 110 +# define UI_R_SYSQIOW_ERROR 111 +# define UI_R_UNKNOWN_CONTROL_COMMAND 106 +# define UI_R_UNKNOWN_TTYGET_ERRNO_VALUE 108 + +# ifdef __cplusplus +} +# endif +# endif +#endif diff --git a/mobile/ios/FxA/FxA/include/openssl/whrlpool.h b/mobile/ios/FxA/FxA/include/openssl/whrlpool.h new file mode 100644 index 0000000000..20ea3503b7 --- /dev/null +++ b/mobile/ios/FxA/FxA/include/openssl/whrlpool.h @@ -0,0 +1,48 @@ +/* + * Copyright 2005-2016 The OpenSSL Project Authors. All Rights Reserved. + * + * Licensed under the OpenSSL license (the "License"). You may not use + * this file except in compliance with the License. You can obtain a copy + * in the file LICENSE in the source distribution or at + * https://www.openssl.org/source/license.html + */ + +#ifndef HEADER_WHRLPOOL_H +# define HEADER_WHRLPOOL_H + +#include + +# ifndef OPENSSL_NO_WHIRLPOOL +# include +# include +# ifdef __cplusplus +extern "C" { +# endif + +# define WHIRLPOOL_DIGEST_LENGTH (512/8) +# define WHIRLPOOL_BBLOCK 512 +# define WHIRLPOOL_COUNTER (256/8) + +typedef struct { + union { + unsigned char c[WHIRLPOOL_DIGEST_LENGTH]; + /* double q is here to ensure 64-bit alignment */ + double q[WHIRLPOOL_DIGEST_LENGTH / sizeof(double)]; + } H; + unsigned char data[WHIRLPOOL_BBLOCK / 8]; + unsigned int bitoff; + size_t bitlen[WHIRLPOOL_COUNTER / sizeof(size_t)]; +} WHIRLPOOL_CTX; + +int WHIRLPOOL_Init(WHIRLPOOL_CTX *c); +int WHIRLPOOL_Update(WHIRLPOOL_CTX *c, const void *inp, size_t bytes); +void WHIRLPOOL_BitUpdate(WHIRLPOOL_CTX *c, const void *inp, size_t bits); +int WHIRLPOOL_Final(unsigned char *md, WHIRLPOOL_CTX *c); +unsigned char *WHIRLPOOL(const void *inp, size_t bytes, unsigned char *md); + +# ifdef __cplusplus +} +# endif +# endif + +#endif diff --git a/mobile/ios/FxA/FxA/include/openssl/x509.h b/mobile/ios/FxA/FxA/include/openssl/x509.h new file mode 100644 index 0000000000..c8996f3520 --- /dev/null +++ b/mobile/ios/FxA/FxA/include/openssl/x509.h @@ -0,0 +1,1123 @@ +/* + * Copyright 1995-2016 The OpenSSL Project Authors. All Rights Reserved. + * + * Licensed under the OpenSSL license (the "License"). You may not use + * this file except in compliance with the License. You can obtain a copy + * in the file LICENSE in the source distribution or at + * https://www.openssl.org/source/license.html + */ + +/* ==================================================================== + * Copyright 2002 Sun Microsystems, Inc. ALL RIGHTS RESERVED. + * ECDH support in OpenSSL originally developed by + * SUN MICROSYSTEMS, INC., and contributed to the OpenSSL project. + */ + +#ifndef HEADER_X509_H +# define HEADER_X509_H + +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include + +# if OPENSSL_API_COMPAT < 0x10100000L +# include +# include +# include +# endif + +# include + +#ifdef __cplusplus +extern "C" { +#endif + +# define X509_FILETYPE_PEM 1 +# define X509_FILETYPE_ASN1 2 +# define X509_FILETYPE_DEFAULT 3 + +# define X509v3_KU_DIGITAL_SIGNATURE 0x0080 +# define X509v3_KU_NON_REPUDIATION 0x0040 +# define X509v3_KU_KEY_ENCIPHERMENT 0x0020 +# define X509v3_KU_DATA_ENCIPHERMENT 0x0010 +# define X509v3_KU_KEY_AGREEMENT 0x0008 +# define X509v3_KU_KEY_CERT_SIGN 0x0004 +# define X509v3_KU_CRL_SIGN 0x0002 +# define X509v3_KU_ENCIPHER_ONLY 0x0001 +# define X509v3_KU_DECIPHER_ONLY 0x8000 +# define X509v3_KU_UNDEF 0xffff + +struct X509_algor_st { + ASN1_OBJECT *algorithm; + ASN1_TYPE *parameter; +} /* X509_ALGOR */ ; + +typedef STACK_OF(X509_ALGOR) X509_ALGORS; + +typedef struct X509_val_st { + ASN1_TIME *notBefore; + ASN1_TIME *notAfter; +} X509_VAL; + +typedef struct X509_sig_st X509_SIG; + +typedef struct X509_name_entry_st X509_NAME_ENTRY; + +DEFINE_STACK_OF(X509_NAME_ENTRY) + +DEFINE_STACK_OF(X509_NAME) + +# define X509_EX_V_NETSCAPE_HACK 0x8000 +# define X509_EX_V_INIT 0x0001 +typedef struct X509_extension_st X509_EXTENSION; + +typedef STACK_OF(X509_EXTENSION) X509_EXTENSIONS; + +DEFINE_STACK_OF(X509_EXTENSION) + +typedef struct x509_attributes_st X509_ATTRIBUTE; + +DEFINE_STACK_OF(X509_ATTRIBUTE) + +typedef struct X509_req_info_st X509_REQ_INFO; + +typedef struct X509_req_st X509_REQ; + +typedef struct x509_cert_aux_st X509_CERT_AUX; + +typedef struct x509_cinf_st X509_CINF; + +DEFINE_STACK_OF(X509) + +/* This is used for a table of trust checking functions */ + +typedef struct x509_trust_st { + int trust; + int flags; + int (*check_trust) (struct x509_trust_st *, X509 *, int); + char *name; + int arg1; + void *arg2; +} X509_TRUST; + +DEFINE_STACK_OF(X509_TRUST) + +/* standard trust ids */ + +# define X509_TRUST_DEFAULT 0 /* Only valid in purpose settings */ + +# define X509_TRUST_COMPAT 1 +# define X509_TRUST_SSL_CLIENT 2 +# define X509_TRUST_SSL_SERVER 3 +# define X509_TRUST_EMAIL 4 +# define X509_TRUST_OBJECT_SIGN 5 +# define X509_TRUST_OCSP_SIGN 6 +# define X509_TRUST_OCSP_REQUEST 7 +# define X509_TRUST_TSA 8 + +/* Keep these up to date! */ +# define X509_TRUST_MIN 1 +# define X509_TRUST_MAX 8 + +/* trust_flags values */ +# define X509_TRUST_DYNAMIC (1U << 0) +# define X509_TRUST_DYNAMIC_NAME (1U << 1) +/* No compat trust if self-signed, preempts "DO_SS" */ +# define X509_TRUST_NO_SS_COMPAT (1U << 2) +/* Compat trust if no explicit accepted trust EKUs */ +# define X509_TRUST_DO_SS_COMPAT (1U << 3) +/* Accept "anyEKU" as a wildcard trust OID */ +# define X509_TRUST_OK_ANY_EKU (1U << 4) + +/* check_trust return codes */ + +# define X509_TRUST_TRUSTED 1 +# define X509_TRUST_REJECTED 2 +# define X509_TRUST_UNTRUSTED 3 + +/* Flags for X509_print_ex() */ + +# define X509_FLAG_COMPAT 0 +# define X509_FLAG_NO_HEADER 1L +# define X509_FLAG_NO_VERSION (1L << 1) +# define X509_FLAG_NO_SERIAL (1L << 2) +# define X509_FLAG_NO_SIGNAME (1L << 3) +# define X509_FLAG_NO_ISSUER (1L << 4) +# define X509_FLAG_NO_VALIDITY (1L << 5) +# define X509_FLAG_NO_SUBJECT (1L << 6) +# define X509_FLAG_NO_PUBKEY (1L << 7) +# define X509_FLAG_NO_EXTENSIONS (1L << 8) +# define X509_FLAG_NO_SIGDUMP (1L << 9) +# define X509_FLAG_NO_AUX (1L << 10) +# define X509_FLAG_NO_ATTRIBUTES (1L << 11) +# define X509_FLAG_NO_IDS (1L << 12) + +/* Flags specific to X509_NAME_print_ex() */ + +/* The field separator information */ + +# define XN_FLAG_SEP_MASK (0xf << 16) + +# define XN_FLAG_COMPAT 0/* Traditional; use old X509_NAME_print */ +# define XN_FLAG_SEP_COMMA_PLUS (1 << 16)/* RFC2253 ,+ */ +# define XN_FLAG_SEP_CPLUS_SPC (2 << 16)/* ,+ spaced: more readable */ +# define XN_FLAG_SEP_SPLUS_SPC (3 << 16)/* ;+ spaced */ +# define XN_FLAG_SEP_MULTILINE (4 << 16)/* One line per field */ + +# define XN_FLAG_DN_REV (1 << 20)/* Reverse DN order */ + +/* How the field name is shown */ + +# define XN_FLAG_FN_MASK (0x3 << 21) + +# define XN_FLAG_FN_SN 0/* Object short name */ +# define XN_FLAG_FN_LN (1 << 21)/* Object long name */ +# define XN_FLAG_FN_OID (2 << 21)/* Always use OIDs */ +# define XN_FLAG_FN_NONE (3 << 21)/* No field names */ + +# define XN_FLAG_SPC_EQ (1 << 23)/* Put spaces round '=' */ + +/* + * This determines if we dump fields we don't recognise: RFC2253 requires + * this. + */ + +# define XN_FLAG_DUMP_UNKNOWN_FIELDS (1 << 24) + +# define XN_FLAG_FN_ALIGN (1 << 25)/* Align field names to 20 + * characters */ + +/* Complete set of RFC2253 flags */ + +# define XN_FLAG_RFC2253 (ASN1_STRFLGS_RFC2253 | \ + XN_FLAG_SEP_COMMA_PLUS | \ + XN_FLAG_DN_REV | \ + XN_FLAG_FN_SN | \ + XN_FLAG_DUMP_UNKNOWN_FIELDS) + +/* readable oneline form */ + +# define XN_FLAG_ONELINE (ASN1_STRFLGS_RFC2253 | \ + ASN1_STRFLGS_ESC_QUOTE | \ + XN_FLAG_SEP_CPLUS_SPC | \ + XN_FLAG_SPC_EQ | \ + XN_FLAG_FN_SN) + +/* readable multiline form */ + +# define XN_FLAG_MULTILINE (ASN1_STRFLGS_ESC_CTRL | \ + ASN1_STRFLGS_ESC_MSB | \ + XN_FLAG_SEP_MULTILINE | \ + XN_FLAG_SPC_EQ | \ + XN_FLAG_FN_LN | \ + XN_FLAG_FN_ALIGN) + +DEFINE_STACK_OF(X509_REVOKED) + +typedef struct X509_crl_info_st X509_CRL_INFO; + +DEFINE_STACK_OF(X509_CRL) + +typedef struct private_key_st { + int version; + /* The PKCS#8 data types */ + X509_ALGOR *enc_algor; + ASN1_OCTET_STRING *enc_pkey; /* encrypted pub key */ + /* When decrypted, the following will not be NULL */ + EVP_PKEY *dec_pkey; + /* used to encrypt and decrypt */ + int key_length; + char *key_data; + int key_free; /* true if we should auto free key_data */ + /* expanded version of 'enc_algor' */ + EVP_CIPHER_INFO cipher; +} X509_PKEY; + +typedef struct X509_info_st { + X509 *x509; + X509_CRL *crl; + X509_PKEY *x_pkey; + EVP_CIPHER_INFO enc_cipher; + int enc_len; + char *enc_data; +} X509_INFO; + +DEFINE_STACK_OF(X509_INFO) + +/* + * The next 2 structures and their 8 routines were sent to me by Pat Richard + * and are used to manipulate Netscapes spki structures - + * useful if you are writing a CA web page + */ +typedef struct Netscape_spkac_st { + X509_PUBKEY *pubkey; + ASN1_IA5STRING *challenge; /* challenge sent in atlas >= PR2 */ +} NETSCAPE_SPKAC; + +typedef struct Netscape_spki_st { + NETSCAPE_SPKAC *spkac; /* signed public key and challenge */ + X509_ALGOR sig_algor; + ASN1_BIT_STRING *signature; +} NETSCAPE_SPKI; + +/* Netscape certificate sequence structure */ +typedef struct Netscape_certificate_sequence { + ASN1_OBJECT *type; + STACK_OF(X509) *certs; +} NETSCAPE_CERT_SEQUENCE; + +/*- Unused (and iv length is wrong) +typedef struct CBCParameter_st + { + unsigned char iv[8]; + } CBC_PARAM; +*/ + +/* Password based encryption structure */ + +typedef struct PBEPARAM_st { + ASN1_OCTET_STRING *salt; + ASN1_INTEGER *iter; +} PBEPARAM; + +/* Password based encryption V2 structures */ + +typedef struct PBE2PARAM_st { + X509_ALGOR *keyfunc; + X509_ALGOR *encryption; +} PBE2PARAM; + +typedef struct PBKDF2PARAM_st { +/* Usually OCTET STRING but could be anything */ + ASN1_TYPE *salt; + ASN1_INTEGER *iter; + ASN1_INTEGER *keylength; + X509_ALGOR *prf; +} PBKDF2PARAM; + +#ifdef __cplusplus +} +#endif + +# include +# include + +#ifdef __cplusplus +extern "C" { +#endif + +# define X509_EXT_PACK_UNKNOWN 1 +# define X509_EXT_PACK_STRING 2 + +# define X509_extract_key(x) X509_get_pubkey(x)/*****/ +# define X509_REQ_extract_key(a) X509_REQ_get_pubkey(a) +# define X509_name_cmp(a,b) X509_NAME_cmp((a),(b)) + +void X509_CRL_set_default_method(const X509_CRL_METHOD *meth); +X509_CRL_METHOD *X509_CRL_METHOD_new(int (*crl_init) (X509_CRL *crl), + int (*crl_free) (X509_CRL *crl), + int (*crl_lookup) (X509_CRL *crl, + X509_REVOKED **ret, + ASN1_INTEGER *ser, + X509_NAME *issuer), + int (*crl_verify) (X509_CRL *crl, + EVP_PKEY *pk)); +void X509_CRL_METHOD_free(X509_CRL_METHOD *m); + +void X509_CRL_set_meth_data(X509_CRL *crl, void *dat); +void *X509_CRL_get_meth_data(X509_CRL *crl); + +const char *X509_verify_cert_error_string(long n); + +int X509_verify(X509 *a, EVP_PKEY *r); + +int X509_REQ_verify(X509_REQ *a, EVP_PKEY *r); +int X509_CRL_verify(X509_CRL *a, EVP_PKEY *r); +int NETSCAPE_SPKI_verify(NETSCAPE_SPKI *a, EVP_PKEY *r); + +NETSCAPE_SPKI *NETSCAPE_SPKI_b64_decode(const char *str, int len); +char *NETSCAPE_SPKI_b64_encode(NETSCAPE_SPKI *x); +EVP_PKEY *NETSCAPE_SPKI_get_pubkey(NETSCAPE_SPKI *x); +int NETSCAPE_SPKI_set_pubkey(NETSCAPE_SPKI *x, EVP_PKEY *pkey); + +int NETSCAPE_SPKI_print(BIO *out, NETSCAPE_SPKI *spki); + +int X509_signature_dump(BIO *bp, const ASN1_STRING *sig, int indent); +int X509_signature_print(BIO *bp, const X509_ALGOR *alg, + const ASN1_STRING *sig); + +int X509_sign(X509 *x, EVP_PKEY *pkey, const EVP_MD *md); +int X509_sign_ctx(X509 *x, EVP_MD_CTX *ctx); +# ifndef OPENSSL_NO_OCSP +int X509_http_nbio(OCSP_REQ_CTX *rctx, X509 **pcert); +# endif +int X509_REQ_sign(X509_REQ *x, EVP_PKEY *pkey, const EVP_MD *md); +int X509_REQ_sign_ctx(X509_REQ *x, EVP_MD_CTX *ctx); +int X509_CRL_sign(X509_CRL *x, EVP_PKEY *pkey, const EVP_MD *md); +int X509_CRL_sign_ctx(X509_CRL *x, EVP_MD_CTX *ctx); +# ifndef OPENSSL_NO_OCSP +int X509_CRL_http_nbio(OCSP_REQ_CTX *rctx, X509_CRL **pcrl); +# endif +int NETSCAPE_SPKI_sign(NETSCAPE_SPKI *x, EVP_PKEY *pkey, const EVP_MD *md); + +int X509_pubkey_digest(const X509 *data, const EVP_MD *type, + unsigned char *md, unsigned int *len); +int X509_digest(const X509 *data, const EVP_MD *type, + unsigned char *md, unsigned int *len); +int X509_CRL_digest(const X509_CRL *data, const EVP_MD *type, + unsigned char *md, unsigned int *len); +int X509_REQ_digest(const X509_REQ *data, const EVP_MD *type, + unsigned char *md, unsigned int *len); +int X509_NAME_digest(const X509_NAME *data, const EVP_MD *type, + unsigned char *md, unsigned int *len); + +# ifndef OPENSSL_NO_STDIO +X509 *d2i_X509_fp(FILE *fp, X509 **x509); +int i2d_X509_fp(FILE *fp, X509 *x509); +X509_CRL *d2i_X509_CRL_fp(FILE *fp, X509_CRL **crl); +int i2d_X509_CRL_fp(FILE *fp, X509_CRL *crl); +X509_REQ *d2i_X509_REQ_fp(FILE *fp, X509_REQ **req); +int i2d_X509_REQ_fp(FILE *fp, X509_REQ *req); +# ifndef OPENSSL_NO_RSA +RSA *d2i_RSAPrivateKey_fp(FILE *fp, RSA **rsa); +int i2d_RSAPrivateKey_fp(FILE *fp, RSA *rsa); +RSA *d2i_RSAPublicKey_fp(FILE *fp, RSA **rsa); +int i2d_RSAPublicKey_fp(FILE *fp, RSA *rsa); +RSA *d2i_RSA_PUBKEY_fp(FILE *fp, RSA **rsa); +int i2d_RSA_PUBKEY_fp(FILE *fp, RSA *rsa); +# endif +# ifndef OPENSSL_NO_DSA +DSA *d2i_DSA_PUBKEY_fp(FILE *fp, DSA **dsa); +int i2d_DSA_PUBKEY_fp(FILE *fp, DSA *dsa); +DSA *d2i_DSAPrivateKey_fp(FILE *fp, DSA **dsa); +int i2d_DSAPrivateKey_fp(FILE *fp, DSA *dsa); +# endif +# ifndef OPENSSL_NO_EC +EC_KEY *d2i_EC_PUBKEY_fp(FILE *fp, EC_KEY **eckey); +int i2d_EC_PUBKEY_fp(FILE *fp, EC_KEY *eckey); +EC_KEY *d2i_ECPrivateKey_fp(FILE *fp, EC_KEY **eckey); +int i2d_ECPrivateKey_fp(FILE *fp, EC_KEY *eckey); +# endif +X509_SIG *d2i_PKCS8_fp(FILE *fp, X509_SIG **p8); +int i2d_PKCS8_fp(FILE *fp, X509_SIG *p8); +PKCS8_PRIV_KEY_INFO *d2i_PKCS8_PRIV_KEY_INFO_fp(FILE *fp, + PKCS8_PRIV_KEY_INFO **p8inf); +int i2d_PKCS8_PRIV_KEY_INFO_fp(FILE *fp, PKCS8_PRIV_KEY_INFO *p8inf); +int i2d_PKCS8PrivateKeyInfo_fp(FILE *fp, EVP_PKEY *key); +int i2d_PrivateKey_fp(FILE *fp, EVP_PKEY *pkey); +EVP_PKEY *d2i_PrivateKey_fp(FILE *fp, EVP_PKEY **a); +int i2d_PUBKEY_fp(FILE *fp, EVP_PKEY *pkey); +EVP_PKEY *d2i_PUBKEY_fp(FILE *fp, EVP_PKEY **a); +# endif + +X509 *d2i_X509_bio(BIO *bp, X509 **x509); +int i2d_X509_bio(BIO *bp, X509 *x509); +X509_CRL *d2i_X509_CRL_bio(BIO *bp, X509_CRL **crl); +int i2d_X509_CRL_bio(BIO *bp, X509_CRL *crl); +X509_REQ *d2i_X509_REQ_bio(BIO *bp, X509_REQ **req); +int i2d_X509_REQ_bio(BIO *bp, X509_REQ *req); +# ifndef OPENSSL_NO_RSA +RSA *d2i_RSAPrivateKey_bio(BIO *bp, RSA **rsa); +int i2d_RSAPrivateKey_bio(BIO *bp, RSA *rsa); +RSA *d2i_RSAPublicKey_bio(BIO *bp, RSA **rsa); +int i2d_RSAPublicKey_bio(BIO *bp, RSA *rsa); +RSA *d2i_RSA_PUBKEY_bio(BIO *bp, RSA **rsa); +int i2d_RSA_PUBKEY_bio(BIO *bp, RSA *rsa); +# endif +# ifndef OPENSSL_NO_DSA +DSA *d2i_DSA_PUBKEY_bio(BIO *bp, DSA **dsa); +int i2d_DSA_PUBKEY_bio(BIO *bp, DSA *dsa); +DSA *d2i_DSAPrivateKey_bio(BIO *bp, DSA **dsa); +int i2d_DSAPrivateKey_bio(BIO *bp, DSA *dsa); +# endif +# ifndef OPENSSL_NO_EC +EC_KEY *d2i_EC_PUBKEY_bio(BIO *bp, EC_KEY **eckey); +int i2d_EC_PUBKEY_bio(BIO *bp, EC_KEY *eckey); +EC_KEY *d2i_ECPrivateKey_bio(BIO *bp, EC_KEY **eckey); +int i2d_ECPrivateKey_bio(BIO *bp, EC_KEY *eckey); +# endif +X509_SIG *d2i_PKCS8_bio(BIO *bp, X509_SIG **p8); +int i2d_PKCS8_bio(BIO *bp, X509_SIG *p8); +PKCS8_PRIV_KEY_INFO *d2i_PKCS8_PRIV_KEY_INFO_bio(BIO *bp, + PKCS8_PRIV_KEY_INFO **p8inf); +int i2d_PKCS8_PRIV_KEY_INFO_bio(BIO *bp, PKCS8_PRIV_KEY_INFO *p8inf); +int i2d_PKCS8PrivateKeyInfo_bio(BIO *bp, EVP_PKEY *key); +int i2d_PrivateKey_bio(BIO *bp, EVP_PKEY *pkey); +EVP_PKEY *d2i_PrivateKey_bio(BIO *bp, EVP_PKEY **a); +int i2d_PUBKEY_bio(BIO *bp, EVP_PKEY *pkey); +EVP_PKEY *d2i_PUBKEY_bio(BIO *bp, EVP_PKEY **a); + +X509 *X509_dup(X509 *x509); +X509_ATTRIBUTE *X509_ATTRIBUTE_dup(X509_ATTRIBUTE *xa); +X509_EXTENSION *X509_EXTENSION_dup(X509_EXTENSION *ex); +X509_CRL *X509_CRL_dup(X509_CRL *crl); +X509_REVOKED *X509_REVOKED_dup(X509_REVOKED *rev); +X509_REQ *X509_REQ_dup(X509_REQ *req); +X509_ALGOR *X509_ALGOR_dup(X509_ALGOR *xn); +int X509_ALGOR_set0(X509_ALGOR *alg, ASN1_OBJECT *aobj, int ptype, + void *pval); +void X509_ALGOR_get0(const ASN1_OBJECT **paobj, int *pptype, + const void **ppval, const X509_ALGOR *algor); +void X509_ALGOR_set_md(X509_ALGOR *alg, const EVP_MD *md); +int X509_ALGOR_cmp(const X509_ALGOR *a, const X509_ALGOR *b); + +X509_NAME *X509_NAME_dup(X509_NAME *xn); +X509_NAME_ENTRY *X509_NAME_ENTRY_dup(X509_NAME_ENTRY *ne); + +int X509_cmp_time(const ASN1_TIME *s, time_t *t); +int X509_cmp_current_time(const ASN1_TIME *s); +ASN1_TIME *X509_time_adj(ASN1_TIME *s, long adj, time_t *t); +ASN1_TIME *X509_time_adj_ex(ASN1_TIME *s, + int offset_day, long offset_sec, time_t *t); +ASN1_TIME *X509_gmtime_adj(ASN1_TIME *s, long adj); + +const char *X509_get_default_cert_area(void); +const char *X509_get_default_cert_dir(void); +const char *X509_get_default_cert_file(void); +const char *X509_get_default_cert_dir_env(void); +const char *X509_get_default_cert_file_env(void); +const char *X509_get_default_private_dir(void); + +X509_REQ *X509_to_X509_REQ(X509 *x, EVP_PKEY *pkey, const EVP_MD *md); +X509 *X509_REQ_to_X509(X509_REQ *r, int days, EVP_PKEY *pkey); + +DECLARE_ASN1_FUNCTIONS(X509_ALGOR) +DECLARE_ASN1_ENCODE_FUNCTIONS(X509_ALGORS, X509_ALGORS, X509_ALGORS) +DECLARE_ASN1_FUNCTIONS(X509_VAL) + +DECLARE_ASN1_FUNCTIONS(X509_PUBKEY) + +int X509_PUBKEY_set(X509_PUBKEY **x, EVP_PKEY *pkey); +EVP_PKEY *X509_PUBKEY_get0(X509_PUBKEY *key); +EVP_PKEY *X509_PUBKEY_get(X509_PUBKEY *key); +int X509_get_pubkey_parameters(EVP_PKEY *pkey, STACK_OF(X509) *chain); +long X509_get_pathlen(X509 *x); +int i2d_PUBKEY(EVP_PKEY *a, unsigned char **pp); +EVP_PKEY *d2i_PUBKEY(EVP_PKEY **a, const unsigned char **pp, long length); +# ifndef OPENSSL_NO_RSA +int i2d_RSA_PUBKEY(RSA *a, unsigned char **pp); +RSA *d2i_RSA_PUBKEY(RSA **a, const unsigned char **pp, long length); +# endif +# ifndef OPENSSL_NO_DSA +int i2d_DSA_PUBKEY(DSA *a, unsigned char **pp); +DSA *d2i_DSA_PUBKEY(DSA **a, const unsigned char **pp, long length); +# endif +# ifndef OPENSSL_NO_EC +int i2d_EC_PUBKEY(EC_KEY *a, unsigned char **pp); +EC_KEY *d2i_EC_PUBKEY(EC_KEY **a, const unsigned char **pp, long length); +# endif + +DECLARE_ASN1_FUNCTIONS(X509_SIG) +void X509_SIG_get0(const X509_SIG *sig, const X509_ALGOR **palg, + const ASN1_OCTET_STRING **pdigest); +void X509_SIG_getm(X509_SIG *sig, X509_ALGOR **palg, + ASN1_OCTET_STRING **pdigest); + +DECLARE_ASN1_FUNCTIONS(X509_REQ_INFO) +DECLARE_ASN1_FUNCTIONS(X509_REQ) + +DECLARE_ASN1_FUNCTIONS(X509_ATTRIBUTE) +X509_ATTRIBUTE *X509_ATTRIBUTE_create(int nid, int atrtype, void *value); + +DECLARE_ASN1_FUNCTIONS(X509_EXTENSION) +DECLARE_ASN1_ENCODE_FUNCTIONS(X509_EXTENSIONS, X509_EXTENSIONS, X509_EXTENSIONS) + +DECLARE_ASN1_FUNCTIONS(X509_NAME_ENTRY) + +DECLARE_ASN1_FUNCTIONS(X509_NAME) + +int X509_NAME_set(X509_NAME **xn, X509_NAME *name); + +DECLARE_ASN1_FUNCTIONS(X509_CINF) + +DECLARE_ASN1_FUNCTIONS(X509) +DECLARE_ASN1_FUNCTIONS(X509_CERT_AUX) + +#define X509_get_ex_new_index(l, p, newf, dupf, freef) \ + CRYPTO_get_ex_new_index(CRYPTO_EX_INDEX_X509, l, p, newf, dupf, freef) +int X509_set_ex_data(X509 *r, int idx, void *arg); +void *X509_get_ex_data(X509 *r, int idx); +int i2d_X509_AUX(X509 *a, unsigned char **pp); +X509 *d2i_X509_AUX(X509 **a, const unsigned char **pp, long length); + +int i2d_re_X509_tbs(X509 *x, unsigned char **pp); + +void X509_get0_signature(const ASN1_BIT_STRING **psig, + const X509_ALGOR **palg, const X509 *x); +int X509_get_signature_nid(const X509 *x); + +int X509_trusted(const X509 *x); +int X509_alias_set1(X509 *x, const unsigned char *name, int len); +int X509_keyid_set1(X509 *x, const unsigned char *id, int len); +unsigned char *X509_alias_get0(X509 *x, int *len); +unsigned char *X509_keyid_get0(X509 *x, int *len); +int (*X509_TRUST_set_default(int (*trust) (int, X509 *, int))) (int, X509 *, + int); +int X509_TRUST_set(int *t, int trust); +int X509_add1_trust_object(X509 *x, const ASN1_OBJECT *obj); +int X509_add1_reject_object(X509 *x, const ASN1_OBJECT *obj); +void X509_trust_clear(X509 *x); +void X509_reject_clear(X509 *x); + +STACK_OF(ASN1_OBJECT) *X509_get0_trust_objects(X509 *x); +STACK_OF(ASN1_OBJECT) *X509_get0_reject_objects(X509 *x); + +DECLARE_ASN1_FUNCTIONS(X509_REVOKED) +DECLARE_ASN1_FUNCTIONS(X509_CRL_INFO) +DECLARE_ASN1_FUNCTIONS(X509_CRL) + +int X509_CRL_add0_revoked(X509_CRL *crl, X509_REVOKED *rev); +int X509_CRL_get0_by_serial(X509_CRL *crl, + X509_REVOKED **ret, ASN1_INTEGER *serial); +int X509_CRL_get0_by_cert(X509_CRL *crl, X509_REVOKED **ret, X509 *x); + +X509_PKEY *X509_PKEY_new(void); +void X509_PKEY_free(X509_PKEY *a); + +DECLARE_ASN1_FUNCTIONS(NETSCAPE_SPKI) +DECLARE_ASN1_FUNCTIONS(NETSCAPE_SPKAC) +DECLARE_ASN1_FUNCTIONS(NETSCAPE_CERT_SEQUENCE) + +X509_INFO *X509_INFO_new(void); +void X509_INFO_free(X509_INFO *a); +char *X509_NAME_oneline(const X509_NAME *a, char *buf, int size); + +int ASN1_verify(i2d_of_void *i2d, X509_ALGOR *algor1, + ASN1_BIT_STRING *signature, char *data, EVP_PKEY *pkey); + +int ASN1_digest(i2d_of_void *i2d, const EVP_MD *type, char *data, + unsigned char *md, unsigned int *len); + +int ASN1_sign(i2d_of_void *i2d, X509_ALGOR *algor1, + X509_ALGOR *algor2, ASN1_BIT_STRING *signature, + char *data, EVP_PKEY *pkey, const EVP_MD *type); + +int ASN1_item_digest(const ASN1_ITEM *it, const EVP_MD *type, void *data, + unsigned char *md, unsigned int *len); + +int ASN1_item_verify(const ASN1_ITEM *it, X509_ALGOR *algor1, + ASN1_BIT_STRING *signature, void *data, EVP_PKEY *pkey); + +int ASN1_item_sign(const ASN1_ITEM *it, X509_ALGOR *algor1, + X509_ALGOR *algor2, ASN1_BIT_STRING *signature, void *data, + EVP_PKEY *pkey, const EVP_MD *type); +int ASN1_item_sign_ctx(const ASN1_ITEM *it, X509_ALGOR *algor1, + X509_ALGOR *algor2, ASN1_BIT_STRING *signature, + void *asn, EVP_MD_CTX *ctx); + +long X509_get_version(const X509 *x); +int X509_set_version(X509 *x, long version); +int X509_set_serialNumber(X509 *x, ASN1_INTEGER *serial); +ASN1_INTEGER *X509_get_serialNumber(X509 *x); +const ASN1_INTEGER *X509_get0_serialNumber(const X509 *x); +int X509_set_issuer_name(X509 *x, X509_NAME *name); +X509_NAME *X509_get_issuer_name(const X509 *a); +int X509_set_subject_name(X509 *x, X509_NAME *name); +X509_NAME *X509_get_subject_name(const X509 *a); +const ASN1_TIME * X509_get0_notBefore(const X509 *x); +ASN1_TIME *X509_getm_notBefore(const X509 *x); +int X509_set1_notBefore(X509 *x, const ASN1_TIME *tm); +const ASN1_TIME *X509_get0_notAfter(const X509 *x); +ASN1_TIME *X509_getm_notAfter(const X509 *x); +int X509_set1_notAfter(X509 *x, const ASN1_TIME *tm); +int X509_set_pubkey(X509 *x, EVP_PKEY *pkey); +int X509_up_ref(X509 *x); +int X509_get_signature_type(const X509 *x); + +# if OPENSSL_API_COMPAT < 0x10100000L +# define X509_get_notBefore X509_getm_notBefore +# define X509_get_notAfter X509_getm_notAfter +# define X509_set_notBefore X509_set1_notBefore +# define X509_set_notAfter X509_set1_notAfter +#endif + + +/* + * This one is only used so that a binary form can output, as in + * i2d_X509_NAME(X509_get_X509_PUBKEY(x),&buf) + */ +X509_PUBKEY *X509_get_X509_PUBKEY(const X509 *x); +const STACK_OF(X509_EXTENSION) *X509_get0_extensions(const X509 *x); +void X509_get0_uids(const X509 *x, const ASN1_BIT_STRING **piuid, + const ASN1_BIT_STRING **psuid); +const X509_ALGOR *X509_get0_tbs_sigalg(const X509 *x); + +EVP_PKEY *X509_get0_pubkey(const X509 *x); +EVP_PKEY *X509_get_pubkey(X509 *x); +ASN1_BIT_STRING *X509_get0_pubkey_bitstr(const X509 *x); +int X509_certificate_type(const X509 *x, const EVP_PKEY *pubkey); + +long X509_REQ_get_version(const X509_REQ *req); +int X509_REQ_set_version(X509_REQ *x, long version); +X509_NAME *X509_REQ_get_subject_name(const X509_REQ *req); +int X509_REQ_set_subject_name(X509_REQ *req, X509_NAME *name); +void X509_REQ_get0_signature(const X509_REQ *req, const ASN1_BIT_STRING **psig, + const X509_ALGOR **palg); +int X509_REQ_get_signature_nid(const X509_REQ *req); +int i2d_re_X509_REQ_tbs(X509_REQ *req, unsigned char **pp); +int X509_REQ_set_pubkey(X509_REQ *x, EVP_PKEY *pkey); +EVP_PKEY *X509_REQ_get_pubkey(X509_REQ *req); +EVP_PKEY *X509_REQ_get0_pubkey(X509_REQ *req); +X509_PUBKEY *X509_REQ_get_X509_PUBKEY(X509_REQ *req); +int X509_REQ_extension_nid(int nid); +int *X509_REQ_get_extension_nids(void); +void X509_REQ_set_extension_nids(int *nids); +STACK_OF(X509_EXTENSION) *X509_REQ_get_extensions(X509_REQ *req); +int X509_REQ_add_extensions_nid(X509_REQ *req, STACK_OF(X509_EXTENSION) *exts, + int nid); +int X509_REQ_add_extensions(X509_REQ *req, STACK_OF(X509_EXTENSION) *exts); +int X509_REQ_get_attr_count(const X509_REQ *req); +int X509_REQ_get_attr_by_NID(const X509_REQ *req, int nid, int lastpos); +int X509_REQ_get_attr_by_OBJ(const X509_REQ *req, const ASN1_OBJECT *obj, + int lastpos); +X509_ATTRIBUTE *X509_REQ_get_attr(const X509_REQ *req, int loc); +X509_ATTRIBUTE *X509_REQ_delete_attr(X509_REQ *req, int loc); +int X509_REQ_add1_attr(X509_REQ *req, X509_ATTRIBUTE *attr); +int X509_REQ_add1_attr_by_OBJ(X509_REQ *req, + const ASN1_OBJECT *obj, int type, + const unsigned char *bytes, int len); +int X509_REQ_add1_attr_by_NID(X509_REQ *req, + int nid, int type, + const unsigned char *bytes, int len); +int X509_REQ_add1_attr_by_txt(X509_REQ *req, + const char *attrname, int type, + const unsigned char *bytes, int len); + +int X509_CRL_set_version(X509_CRL *x, long version); +int X509_CRL_set_issuer_name(X509_CRL *x, X509_NAME *name); +int X509_CRL_set1_lastUpdate(X509_CRL *x, const ASN1_TIME *tm); +int X509_CRL_set1_nextUpdate(X509_CRL *x, const ASN1_TIME *tm); +int X509_CRL_sort(X509_CRL *crl); +int X509_CRL_up_ref(X509_CRL *crl); + +# if OPENSSL_API_COMPAT < 0x10100000L +# define X509_CRL_set_lastUpdate X509_CRL_set1_lastUpdate +# define X509_CRL_set_nextUpdate X509_CRL_set1_nextUpdate +#endif + +long X509_CRL_get_version(const X509_CRL *crl); +const ASN1_TIME *X509_CRL_get0_lastUpdate(const X509_CRL *crl); +const ASN1_TIME *X509_CRL_get0_nextUpdate(const X509_CRL *crl); +DEPRECATEDIN_1_1_0(ASN1_TIME *X509_CRL_get_lastUpdate(X509_CRL *crl)) +DEPRECATEDIN_1_1_0(ASN1_TIME *X509_CRL_get_nextUpdate(X509_CRL *crl)) +X509_NAME *X509_CRL_get_issuer(const X509_CRL *crl); +const STACK_OF(X509_EXTENSION) *X509_CRL_get0_extensions(const X509_CRL *crl); +STACK_OF(X509_REVOKED) *X509_CRL_get_REVOKED(X509_CRL *crl); +void X509_CRL_get0_signature(const X509_CRL *crl, const ASN1_BIT_STRING **psig, + const X509_ALGOR **palg); +int X509_CRL_get_signature_nid(const X509_CRL *crl); +int i2d_re_X509_CRL_tbs(X509_CRL *req, unsigned char **pp); + +const ASN1_INTEGER *X509_REVOKED_get0_serialNumber(const X509_REVOKED *x); +int X509_REVOKED_set_serialNumber(X509_REVOKED *x, ASN1_INTEGER *serial); +const ASN1_TIME *X509_REVOKED_get0_revocationDate(const X509_REVOKED *x); +int X509_REVOKED_set_revocationDate(X509_REVOKED *r, ASN1_TIME *tm); +const STACK_OF(X509_EXTENSION) * +X509_REVOKED_get0_extensions(const X509_REVOKED *r); + +X509_CRL *X509_CRL_diff(X509_CRL *base, X509_CRL *newer, + EVP_PKEY *skey, const EVP_MD *md, unsigned int flags); + +int X509_REQ_check_private_key(X509_REQ *x509, EVP_PKEY *pkey); + +int X509_check_private_key(const X509 *x509, const EVP_PKEY *pkey); +int X509_chain_check_suiteb(int *perror_depth, + X509 *x, STACK_OF(X509) *chain, + unsigned long flags); +int X509_CRL_check_suiteb(X509_CRL *crl, EVP_PKEY *pk, unsigned long flags); +STACK_OF(X509) *X509_chain_up_ref(STACK_OF(X509) *chain); + +int X509_issuer_and_serial_cmp(const X509 *a, const X509 *b); +unsigned long X509_issuer_and_serial_hash(X509 *a); + +int X509_issuer_name_cmp(const X509 *a, const X509 *b); +unsigned long X509_issuer_name_hash(X509 *a); + +int X509_subject_name_cmp(const X509 *a, const X509 *b); +unsigned long X509_subject_name_hash(X509 *x); + +# ifndef OPENSSL_NO_MD5 +unsigned long X509_issuer_name_hash_old(X509 *a); +unsigned long X509_subject_name_hash_old(X509 *x); +# endif + +int X509_cmp(const X509 *a, const X509 *b); +int X509_NAME_cmp(const X509_NAME *a, const X509_NAME *b); +unsigned long X509_NAME_hash(X509_NAME *x); +unsigned long X509_NAME_hash_old(X509_NAME *x); + +int X509_CRL_cmp(const X509_CRL *a, const X509_CRL *b); +int X509_CRL_match(const X509_CRL *a, const X509_CRL *b); +int X509_aux_print(BIO *out, X509 *x, int indent); +# ifndef OPENSSL_NO_STDIO +int X509_print_ex_fp(FILE *bp, X509 *x, unsigned long nmflag, + unsigned long cflag); +int X509_print_fp(FILE *bp, X509 *x); +int X509_CRL_print_fp(FILE *bp, X509_CRL *x); +int X509_REQ_print_fp(FILE *bp, X509_REQ *req); +int X509_NAME_print_ex_fp(FILE *fp, const X509_NAME *nm, int indent, + unsigned long flags); +# endif + +int X509_NAME_print(BIO *bp, const X509_NAME *name, int obase); +int X509_NAME_print_ex(BIO *out, const X509_NAME *nm, int indent, + unsigned long flags); +int X509_print_ex(BIO *bp, X509 *x, unsigned long nmflag, + unsigned long cflag); +int X509_print(BIO *bp, X509 *x); +int X509_ocspid_print(BIO *bp, X509 *x); +int X509_CRL_print(BIO *bp, X509_CRL *x); +int X509_REQ_print_ex(BIO *bp, X509_REQ *x, unsigned long nmflag, + unsigned long cflag); +int X509_REQ_print(BIO *bp, X509_REQ *req); + +int X509_NAME_entry_count(const X509_NAME *name); +int X509_NAME_get_text_by_NID(X509_NAME *name, int nid, char *buf, int len); +int X509_NAME_get_text_by_OBJ(X509_NAME *name, const ASN1_OBJECT *obj, + char *buf, int len); + +/* + * NOTE: you should be passing -1, not 0 as lastpos. The functions that use + * lastpos, search after that position on. + */ +int X509_NAME_get_index_by_NID(X509_NAME *name, int nid, int lastpos); +int X509_NAME_get_index_by_OBJ(X509_NAME *name, const ASN1_OBJECT *obj, + int lastpos); +X509_NAME_ENTRY *X509_NAME_get_entry(const X509_NAME *name, int loc); +X509_NAME_ENTRY *X509_NAME_delete_entry(X509_NAME *name, int loc); +int X509_NAME_add_entry(X509_NAME *name, const X509_NAME_ENTRY *ne, + int loc, int set); +int X509_NAME_add_entry_by_OBJ(X509_NAME *name, const ASN1_OBJECT *obj, int type, + const unsigned char *bytes, int len, int loc, + int set); +int X509_NAME_add_entry_by_NID(X509_NAME *name, int nid, int type, + const unsigned char *bytes, int len, int loc, + int set); +X509_NAME_ENTRY *X509_NAME_ENTRY_create_by_txt(X509_NAME_ENTRY **ne, + const char *field, int type, + const unsigned char *bytes, + int len); +X509_NAME_ENTRY *X509_NAME_ENTRY_create_by_NID(X509_NAME_ENTRY **ne, int nid, + int type, + const unsigned char *bytes, + int len); +int X509_NAME_add_entry_by_txt(X509_NAME *name, const char *field, int type, + const unsigned char *bytes, int len, int loc, + int set); +X509_NAME_ENTRY *X509_NAME_ENTRY_create_by_OBJ(X509_NAME_ENTRY **ne, + const ASN1_OBJECT *obj, int type, + const unsigned char *bytes, + int len); +int X509_NAME_ENTRY_set_object(X509_NAME_ENTRY *ne, const ASN1_OBJECT *obj); +int X509_NAME_ENTRY_set_data(X509_NAME_ENTRY *ne, int type, + const unsigned char *bytes, int len); +ASN1_OBJECT *X509_NAME_ENTRY_get_object(const X509_NAME_ENTRY *ne); +ASN1_STRING * X509_NAME_ENTRY_get_data(const X509_NAME_ENTRY *ne); +int X509_NAME_ENTRY_set(const X509_NAME_ENTRY *ne); + +int X509_NAME_get0_der(X509_NAME *nm, const unsigned char **pder, + size_t *pderlen); + +int X509v3_get_ext_count(const STACK_OF(X509_EXTENSION) *x); +int X509v3_get_ext_by_NID(const STACK_OF(X509_EXTENSION) *x, + int nid, int lastpos); +int X509v3_get_ext_by_OBJ(const STACK_OF(X509_EXTENSION) *x, + const ASN1_OBJECT *obj, int lastpos); +int X509v3_get_ext_by_critical(const STACK_OF(X509_EXTENSION) *x, + int crit, int lastpos); +X509_EXTENSION *X509v3_get_ext(const STACK_OF(X509_EXTENSION) *x, int loc); +X509_EXTENSION *X509v3_delete_ext(STACK_OF(X509_EXTENSION) *x, int loc); +STACK_OF(X509_EXTENSION) *X509v3_add_ext(STACK_OF(X509_EXTENSION) **x, + X509_EXTENSION *ex, int loc); + +int X509_get_ext_count(const X509 *x); +int X509_get_ext_by_NID(const X509 *x, int nid, int lastpos); +int X509_get_ext_by_OBJ(const X509 *x, const ASN1_OBJECT *obj, int lastpos); +int X509_get_ext_by_critical(const X509 *x, int crit, int lastpos); +X509_EXTENSION *X509_get_ext(const X509 *x, int loc); +X509_EXTENSION *X509_delete_ext(X509 *x, int loc); +int X509_add_ext(X509 *x, X509_EXTENSION *ex, int loc); +void *X509_get_ext_d2i(const X509 *x, int nid, int *crit, int *idx); +int X509_add1_ext_i2d(X509 *x, int nid, void *value, int crit, + unsigned long flags); + +int X509_CRL_get_ext_count(const X509_CRL *x); +int X509_CRL_get_ext_by_NID(const X509_CRL *x, int nid, int lastpos); +int X509_CRL_get_ext_by_OBJ(const X509_CRL *x, const ASN1_OBJECT *obj, + int lastpos); +int X509_CRL_get_ext_by_critical(const X509_CRL *x, int crit, int lastpos); +X509_EXTENSION *X509_CRL_get_ext(const X509_CRL *x, int loc); +X509_EXTENSION *X509_CRL_delete_ext(X509_CRL *x, int loc); +int X509_CRL_add_ext(X509_CRL *x, X509_EXTENSION *ex, int loc); +void *X509_CRL_get_ext_d2i(const X509_CRL *x, int nid, int *crit, int *idx); +int X509_CRL_add1_ext_i2d(X509_CRL *x, int nid, void *value, int crit, + unsigned long flags); + +int X509_REVOKED_get_ext_count(const X509_REVOKED *x); +int X509_REVOKED_get_ext_by_NID(const X509_REVOKED *x, int nid, int lastpos); +int X509_REVOKED_get_ext_by_OBJ(const X509_REVOKED *x, const ASN1_OBJECT *obj, + int lastpos); +int X509_REVOKED_get_ext_by_critical(const X509_REVOKED *x, int crit, + int lastpos); +X509_EXTENSION *X509_REVOKED_get_ext(const X509_REVOKED *x, int loc); +X509_EXTENSION *X509_REVOKED_delete_ext(X509_REVOKED *x, int loc); +int X509_REVOKED_add_ext(X509_REVOKED *x, X509_EXTENSION *ex, int loc); +void *X509_REVOKED_get_ext_d2i(const X509_REVOKED *x, int nid, int *crit, + int *idx); +int X509_REVOKED_add1_ext_i2d(X509_REVOKED *x, int nid, void *value, int crit, + unsigned long flags); + +X509_EXTENSION *X509_EXTENSION_create_by_NID(X509_EXTENSION **ex, + int nid, int crit, + ASN1_OCTET_STRING *data); +X509_EXTENSION *X509_EXTENSION_create_by_OBJ(X509_EXTENSION **ex, + const ASN1_OBJECT *obj, int crit, + ASN1_OCTET_STRING *data); +int X509_EXTENSION_set_object(X509_EXTENSION *ex, const ASN1_OBJECT *obj); +int X509_EXTENSION_set_critical(X509_EXTENSION *ex, int crit); +int X509_EXTENSION_set_data(X509_EXTENSION *ex, ASN1_OCTET_STRING *data); +ASN1_OBJECT *X509_EXTENSION_get_object(X509_EXTENSION *ex); +ASN1_OCTET_STRING *X509_EXTENSION_get_data(X509_EXTENSION *ne); +int X509_EXTENSION_get_critical(const X509_EXTENSION *ex); + +int X509at_get_attr_count(const STACK_OF(X509_ATTRIBUTE) *x); +int X509at_get_attr_by_NID(const STACK_OF(X509_ATTRIBUTE) *x, int nid, + int lastpos); +int X509at_get_attr_by_OBJ(const STACK_OF(X509_ATTRIBUTE) *sk, + const ASN1_OBJECT *obj, int lastpos); +X509_ATTRIBUTE *X509at_get_attr(const STACK_OF(X509_ATTRIBUTE) *x, int loc); +X509_ATTRIBUTE *X509at_delete_attr(STACK_OF(X509_ATTRIBUTE) *x, int loc); +STACK_OF(X509_ATTRIBUTE) *X509at_add1_attr(STACK_OF(X509_ATTRIBUTE) **x, + X509_ATTRIBUTE *attr); +STACK_OF(X509_ATTRIBUTE) *X509at_add1_attr_by_OBJ(STACK_OF(X509_ATTRIBUTE) + **x, const ASN1_OBJECT *obj, + int type, + const unsigned char *bytes, + int len); +STACK_OF(X509_ATTRIBUTE) *X509at_add1_attr_by_NID(STACK_OF(X509_ATTRIBUTE) + **x, int nid, int type, + const unsigned char *bytes, + int len); +STACK_OF(X509_ATTRIBUTE) *X509at_add1_attr_by_txt(STACK_OF(X509_ATTRIBUTE) + **x, const char *attrname, + int type, + const unsigned char *bytes, + int len); +void *X509at_get0_data_by_OBJ(STACK_OF(X509_ATTRIBUTE) *x, + const ASN1_OBJECT *obj, int lastpos, int type); +X509_ATTRIBUTE *X509_ATTRIBUTE_create_by_NID(X509_ATTRIBUTE **attr, int nid, + int atrtype, const void *data, + int len); +X509_ATTRIBUTE *X509_ATTRIBUTE_create_by_OBJ(X509_ATTRIBUTE **attr, + const ASN1_OBJECT *obj, + int atrtype, const void *data, + int len); +X509_ATTRIBUTE *X509_ATTRIBUTE_create_by_txt(X509_ATTRIBUTE **attr, + const char *atrname, int type, + const unsigned char *bytes, + int len); +int X509_ATTRIBUTE_set1_object(X509_ATTRIBUTE *attr, const ASN1_OBJECT *obj); +int X509_ATTRIBUTE_set1_data(X509_ATTRIBUTE *attr, int attrtype, + const void *data, int len); +void *X509_ATTRIBUTE_get0_data(X509_ATTRIBUTE *attr, int idx, int atrtype, + void *data); +int X509_ATTRIBUTE_count(const X509_ATTRIBUTE *attr); +ASN1_OBJECT *X509_ATTRIBUTE_get0_object(X509_ATTRIBUTE *attr); +ASN1_TYPE *X509_ATTRIBUTE_get0_type(X509_ATTRIBUTE *attr, int idx); + +int EVP_PKEY_get_attr_count(const EVP_PKEY *key); +int EVP_PKEY_get_attr_by_NID(const EVP_PKEY *key, int nid, int lastpos); +int EVP_PKEY_get_attr_by_OBJ(const EVP_PKEY *key, const ASN1_OBJECT *obj, + int lastpos); +X509_ATTRIBUTE *EVP_PKEY_get_attr(const EVP_PKEY *key, int loc); +X509_ATTRIBUTE *EVP_PKEY_delete_attr(EVP_PKEY *key, int loc); +int EVP_PKEY_add1_attr(EVP_PKEY *key, X509_ATTRIBUTE *attr); +int EVP_PKEY_add1_attr_by_OBJ(EVP_PKEY *key, + const ASN1_OBJECT *obj, int type, + const unsigned char *bytes, int len); +int EVP_PKEY_add1_attr_by_NID(EVP_PKEY *key, + int nid, int type, + const unsigned char *bytes, int len); +int EVP_PKEY_add1_attr_by_txt(EVP_PKEY *key, + const char *attrname, int type, + const unsigned char *bytes, int len); + +int X509_verify_cert(X509_STORE_CTX *ctx); + +/* lookup a cert from a X509 STACK */ +X509 *X509_find_by_issuer_and_serial(STACK_OF(X509) *sk, X509_NAME *name, + ASN1_INTEGER *serial); +X509 *X509_find_by_subject(STACK_OF(X509) *sk, X509_NAME *name); + +DECLARE_ASN1_FUNCTIONS(PBEPARAM) +DECLARE_ASN1_FUNCTIONS(PBE2PARAM) +DECLARE_ASN1_FUNCTIONS(PBKDF2PARAM) + +int PKCS5_pbe_set0_algor(X509_ALGOR *algor, int alg, int iter, + const unsigned char *salt, int saltlen); + +X509_ALGOR *PKCS5_pbe_set(int alg, int iter, + const unsigned char *salt, int saltlen); +X509_ALGOR *PKCS5_pbe2_set(const EVP_CIPHER *cipher, int iter, + unsigned char *salt, int saltlen); +X509_ALGOR *PKCS5_pbe2_set_iv(const EVP_CIPHER *cipher, int iter, + unsigned char *salt, int saltlen, + unsigned char *aiv, int prf_nid); + +#ifndef OPENSSL_NO_SCRYPT +X509_ALGOR *PKCS5_pbe2_set_scrypt(const EVP_CIPHER *cipher, + const unsigned char *salt, int saltlen, + unsigned char *aiv, uint64_t N, uint64_t r, + uint64_t p); +#endif + +X509_ALGOR *PKCS5_pbkdf2_set(int iter, unsigned char *salt, int saltlen, + int prf_nid, int keylen); + +/* PKCS#8 utilities */ + +DECLARE_ASN1_FUNCTIONS(PKCS8_PRIV_KEY_INFO) + +EVP_PKEY *EVP_PKCS82PKEY(const PKCS8_PRIV_KEY_INFO *p8); +PKCS8_PRIV_KEY_INFO *EVP_PKEY2PKCS8(EVP_PKEY *pkey); + +int PKCS8_pkey_set0(PKCS8_PRIV_KEY_INFO *priv, ASN1_OBJECT *aobj, + int version, int ptype, void *pval, + unsigned char *penc, int penclen); +int PKCS8_pkey_get0(const ASN1_OBJECT **ppkalg, + const unsigned char **pk, int *ppklen, + const X509_ALGOR **pa, const PKCS8_PRIV_KEY_INFO *p8); + +const STACK_OF(X509_ATTRIBUTE) * +PKCS8_pkey_get0_attrs(const PKCS8_PRIV_KEY_INFO *p8); +int PKCS8_pkey_add1_attr_by_NID(PKCS8_PRIV_KEY_INFO *p8, int nid, int type, + const unsigned char *bytes, int len); + +int X509_PUBKEY_set0_param(X509_PUBKEY *pub, ASN1_OBJECT *aobj, + int ptype, void *pval, + unsigned char *penc, int penclen); +int X509_PUBKEY_get0_param(ASN1_OBJECT **ppkalg, + const unsigned char **pk, int *ppklen, + X509_ALGOR **pa, X509_PUBKEY *pub); + +int X509_check_trust(X509 *x, int id, int flags); +int X509_TRUST_get_count(void); +X509_TRUST *X509_TRUST_get0(int idx); +int X509_TRUST_get_by_id(int id); +int X509_TRUST_add(int id, int flags, int (*ck) (X509_TRUST *, X509 *, int), + const char *name, int arg1, void *arg2); +void X509_TRUST_cleanup(void); +int X509_TRUST_get_flags(const X509_TRUST *xp); +char *X509_TRUST_get0_name(const X509_TRUST *xp); +int X509_TRUST_get_trust(const X509_TRUST *xp); + +/* BEGIN ERROR CODES */ +/* + * The following lines are auto generated by the script mkerr.pl. Any changes + * made after this point may be overwritten when the script is next run. + */ + +int ERR_load_X509_strings(void); + +/* Error codes for the X509 functions. */ + +/* Function codes. */ +# define X509_F_ADD_CERT_DIR 100 +# define X509_F_BUILD_CHAIN 106 +# define X509_F_BY_FILE_CTRL 101 +# define X509_F_CHECK_NAME_CONSTRAINTS 149 +# define X509_F_CHECK_POLICY 145 +# define X509_F_DANE_I2D 107 +# define X509_F_DIR_CTRL 102 +# define X509_F_GET_CERT_BY_SUBJECT 103 +# define X509_F_NETSCAPE_SPKI_B64_DECODE 129 +# define X509_F_NETSCAPE_SPKI_B64_ENCODE 130 +# define X509_F_X509AT_ADD1_ATTR 135 +# define X509_F_X509V3_ADD_EXT 104 +# define X509_F_X509_ATTRIBUTE_CREATE_BY_NID 136 +# define X509_F_X509_ATTRIBUTE_CREATE_BY_OBJ 137 +# define X509_F_X509_ATTRIBUTE_CREATE_BY_TXT 140 +# define X509_F_X509_ATTRIBUTE_GET0_DATA 139 +# define X509_F_X509_ATTRIBUTE_SET1_DATA 138 +# define X509_F_X509_CHECK_PRIVATE_KEY 128 +# define X509_F_X509_CRL_DIFF 105 +# define X509_F_X509_CRL_PRINT_FP 147 +# define X509_F_X509_EXTENSION_CREATE_BY_NID 108 +# define X509_F_X509_EXTENSION_CREATE_BY_OBJ 109 +# define X509_F_X509_GET_PUBKEY_PARAMETERS 110 +# define X509_F_X509_LOAD_CERT_CRL_FILE 132 +# define X509_F_X509_LOAD_CERT_FILE 111 +# define X509_F_X509_LOAD_CRL_FILE 112 +# define X509_F_X509_NAME_ADD_ENTRY 113 +# define X509_F_X509_NAME_ENTRY_CREATE_BY_NID 114 +# define X509_F_X509_NAME_ENTRY_CREATE_BY_TXT 131 +# define X509_F_X509_NAME_ENTRY_SET_OBJECT 115 +# define X509_F_X509_NAME_ONELINE 116 +# define X509_F_X509_NAME_PRINT 117 +# define X509_F_X509_OBJECT_NEW 150 +# define X509_F_X509_PRINT_EX_FP 118 +# define X509_F_X509_PUBKEY_DECODE 148 +# define X509_F_X509_PUBKEY_GET0 119 +# define X509_F_X509_PUBKEY_SET 120 +# define X509_F_X509_REQ_CHECK_PRIVATE_KEY 144 +# define X509_F_X509_REQ_PRINT_EX 121 +# define X509_F_X509_REQ_PRINT_FP 122 +# define X509_F_X509_REQ_TO_X509 123 +# define X509_F_X509_STORE_ADD_CERT 124 +# define X509_F_X509_STORE_ADD_CRL 125 +# define X509_F_X509_STORE_CTX_GET1_ISSUER 146 +# define X509_F_X509_STORE_CTX_INIT 143 +# define X509_F_X509_STORE_CTX_NEW 142 +# define X509_F_X509_STORE_CTX_PURPOSE_INHERIT 134 +# define X509_F_X509_TO_X509_REQ 126 +# define X509_F_X509_TRUST_ADD 133 +# define X509_F_X509_TRUST_SET 141 +# define X509_F_X509_VERIFY_CERT 127 + +/* Reason codes. */ +# define X509_R_AKID_MISMATCH 110 +# define X509_R_BAD_SELECTOR 133 +# define X509_R_BAD_X509_FILETYPE 100 +# define X509_R_BASE64_DECODE_ERROR 118 +# define X509_R_CANT_CHECK_DH_KEY 114 +# define X509_R_CERT_ALREADY_IN_HASH_TABLE 101 +# define X509_R_CRL_ALREADY_DELTA 127 +# define X509_R_CRL_VERIFY_FAILURE 131 +# define X509_R_IDP_MISMATCH 128 +# define X509_R_INVALID_DIRECTORY 113 +# define X509_R_INVALID_FIELD_NAME 119 +# define X509_R_INVALID_TRUST 123 +# define X509_R_ISSUER_MISMATCH 129 +# define X509_R_KEY_TYPE_MISMATCH 115 +# define X509_R_KEY_VALUES_MISMATCH 116 +# define X509_R_LOADING_CERT_DIR 103 +# define X509_R_LOADING_DEFAULTS 104 +# define X509_R_METHOD_NOT_SUPPORTED 124 +# define X509_R_NAME_TOO_LONG 134 +# define X509_R_NEWER_CRL_NOT_NEWER 132 +# define X509_R_NO_CERT_SET_FOR_US_TO_VERIFY 105 +# define X509_R_NO_CRL_NUMBER 130 +# define X509_R_PUBLIC_KEY_DECODE_ERROR 125 +# define X509_R_PUBLIC_KEY_ENCODE_ERROR 126 +# define X509_R_SHOULD_RETRY 106 +# define X509_R_UNABLE_TO_FIND_PARAMETERS_IN_CHAIN 107 +# define X509_R_UNABLE_TO_GET_CERTS_PUBLIC_KEY 108 +# define X509_R_UNKNOWN_KEY_TYPE 117 +# define X509_R_UNKNOWN_NID 109 +# define X509_R_UNKNOWN_PURPOSE_ID 121 +# define X509_R_UNKNOWN_TRUST_ID 120 +# define X509_R_UNSUPPORTED_ALGORITHM 111 +# define X509_R_WRONG_LOOKUP_TYPE 112 +# define X509_R_WRONG_TYPE 122 + +# ifdef __cplusplus +} +# endif +#endif diff --git a/mobile/ios/FxA/FxA/include/openssl/x509_vfy.h b/mobile/ios/FxA/FxA/include/openssl/x509_vfy.h new file mode 100644 index 0000000000..64f56df7f0 --- /dev/null +++ b/mobile/ios/FxA/FxA/include/openssl/x509_vfy.h @@ -0,0 +1,545 @@ +/* + * Copyright 1995-2016 The OpenSSL Project Authors. All Rights Reserved. + * + * Licensed under the OpenSSL license (the "License"). You may not use + * this file except in compliance with the License. You can obtain a copy + * in the file LICENSE in the source distribution or at + * https://www.openssl.org/source/license.html + */ + +#ifndef HEADER_X509_VFY_H +# define HEADER_X509_VFY_H + +/* + * Protect against recursion, x509.h and x509_vfy.h each include the other. + */ +# ifndef HEADER_X509_H +# include +# endif + +# include +# include +# include +# include +# include + +#ifdef __cplusplus +extern "C" { +#endif + +/*- +SSL_CTX -> X509_STORE + -> X509_LOOKUP + ->X509_LOOKUP_METHOD + -> X509_LOOKUP + ->X509_LOOKUP_METHOD + +SSL -> X509_STORE_CTX + ->X509_STORE + +The X509_STORE holds the tables etc for verification stuff. +A X509_STORE_CTX is used while validating a single certificate. +The X509_STORE has X509_LOOKUPs for looking up certs. +The X509_STORE then calls a function to actually verify the +certificate chain. +*/ + +typedef enum { + X509_LU_NONE = 0, + X509_LU_X509, X509_LU_CRL +} X509_LOOKUP_TYPE; + +#if OPENSSL_API_COMPAT < 0x10100000L +#define X509_LU_RETRY -1 +#define X509_LU_FAIL 0 +#endif + +DEFINE_STACK_OF(X509_LOOKUP) +DEFINE_STACK_OF(X509_OBJECT) +DEFINE_STACK_OF(X509_VERIFY_PARAM) + +int X509_STORE_set_depth(X509_STORE *store, int depth); + +typedef int (*X509_STORE_CTX_verify_cb)(int, X509_STORE_CTX *); +typedef int (*X509_STORE_CTX_verify_fn)(X509_STORE_CTX *); +typedef int (*X509_STORE_CTX_get_issuer_fn)(X509 **issuer, + X509_STORE_CTX *ctx, X509 *x); +typedef int (*X509_STORE_CTX_check_issued_fn)(X509_STORE_CTX *ctx, + X509 *x, X509 *issuer); +typedef int (*X509_STORE_CTX_check_revocation_fn)(X509_STORE_CTX *ctx); +typedef int (*X509_STORE_CTX_get_crl_fn)(X509_STORE_CTX *ctx, + X509_CRL **crl, X509 *x); +typedef int (*X509_STORE_CTX_check_crl_fn)(X509_STORE_CTX *ctx, X509_CRL *crl); +typedef int (*X509_STORE_CTX_cert_crl_fn)(X509_STORE_CTX *ctx, + X509_CRL *crl, X509 *x); +typedef int (*X509_STORE_CTX_check_policy_fn)(X509_STORE_CTX *ctx); +typedef STACK_OF(X509) *(*X509_STORE_CTX_lookup_certs_fn)(X509_STORE_CTX *ctx, + X509_NAME *nm); +typedef STACK_OF(X509_CRL) *(*X509_STORE_CTX_lookup_crls_fn)(X509_STORE_CTX *ctx, + X509_NAME *nm); +typedef int (*X509_STORE_CTX_cleanup_fn)(X509_STORE_CTX *ctx); + + +void X509_STORE_CTX_set_depth(X509_STORE_CTX *ctx, int depth); + +# define X509_STORE_CTX_set_app_data(ctx,data) \ + X509_STORE_CTX_set_ex_data(ctx,0,data) +# define X509_STORE_CTX_get_app_data(ctx) \ + X509_STORE_CTX_get_ex_data(ctx,0) + +# define X509_L_FILE_LOAD 1 +# define X509_L_ADD_DIR 2 + +# define X509_LOOKUP_load_file(x,name,type) \ + X509_LOOKUP_ctrl((x),X509_L_FILE_LOAD,(name),(long)(type),NULL) + +# define X509_LOOKUP_add_dir(x,name,type) \ + X509_LOOKUP_ctrl((x),X509_L_ADD_DIR,(name),(long)(type),NULL) + +# define X509_V_OK 0 +# define X509_V_ERR_UNSPECIFIED 1 +# define X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT 2 +# define X509_V_ERR_UNABLE_TO_GET_CRL 3 +# define X509_V_ERR_UNABLE_TO_DECRYPT_CERT_SIGNATURE 4 +# define X509_V_ERR_UNABLE_TO_DECRYPT_CRL_SIGNATURE 5 +# define X509_V_ERR_UNABLE_TO_DECODE_ISSUER_PUBLIC_KEY 6 +# define X509_V_ERR_CERT_SIGNATURE_FAILURE 7 +# define X509_V_ERR_CRL_SIGNATURE_FAILURE 8 +# define X509_V_ERR_CERT_NOT_YET_VALID 9 +# define X509_V_ERR_CERT_HAS_EXPIRED 10 +# define X509_V_ERR_CRL_NOT_YET_VALID 11 +# define X509_V_ERR_CRL_HAS_EXPIRED 12 +# define X509_V_ERR_ERROR_IN_CERT_NOT_BEFORE_FIELD 13 +# define X509_V_ERR_ERROR_IN_CERT_NOT_AFTER_FIELD 14 +# define X509_V_ERR_ERROR_IN_CRL_LAST_UPDATE_FIELD 15 +# define X509_V_ERR_ERROR_IN_CRL_NEXT_UPDATE_FIELD 16 +# define X509_V_ERR_OUT_OF_MEM 17 +# define X509_V_ERR_DEPTH_ZERO_SELF_SIGNED_CERT 18 +# define X509_V_ERR_SELF_SIGNED_CERT_IN_CHAIN 19 +# define X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT_LOCALLY 20 +# define X509_V_ERR_UNABLE_TO_VERIFY_LEAF_SIGNATURE 21 +# define X509_V_ERR_CERT_CHAIN_TOO_LONG 22 +# define X509_V_ERR_CERT_REVOKED 23 +# define X509_V_ERR_INVALID_CA 24 +# define X509_V_ERR_PATH_LENGTH_EXCEEDED 25 +# define X509_V_ERR_INVALID_PURPOSE 26 +# define X509_V_ERR_CERT_UNTRUSTED 27 +# define X509_V_ERR_CERT_REJECTED 28 +/* These are 'informational' when looking for issuer cert */ +# define X509_V_ERR_SUBJECT_ISSUER_MISMATCH 29 +# define X509_V_ERR_AKID_SKID_MISMATCH 30 +# define X509_V_ERR_AKID_ISSUER_SERIAL_MISMATCH 31 +# define X509_V_ERR_KEYUSAGE_NO_CERTSIGN 32 +# define X509_V_ERR_UNABLE_TO_GET_CRL_ISSUER 33 +# define X509_V_ERR_UNHANDLED_CRITICAL_EXTENSION 34 +# define X509_V_ERR_KEYUSAGE_NO_CRL_SIGN 35 +# define X509_V_ERR_UNHANDLED_CRITICAL_CRL_EXTENSION 36 +# define X509_V_ERR_INVALID_NON_CA 37 +# define X509_V_ERR_PROXY_PATH_LENGTH_EXCEEDED 38 +# define X509_V_ERR_KEYUSAGE_NO_DIGITAL_SIGNATURE 39 +# define X509_V_ERR_PROXY_CERTIFICATES_NOT_ALLOWED 40 +# define X509_V_ERR_INVALID_EXTENSION 41 +# define X509_V_ERR_INVALID_POLICY_EXTENSION 42 +# define X509_V_ERR_NO_EXPLICIT_POLICY 43 +# define X509_V_ERR_DIFFERENT_CRL_SCOPE 44 +# define X509_V_ERR_UNSUPPORTED_EXTENSION_FEATURE 45 +# define X509_V_ERR_UNNESTED_RESOURCE 46 +# define X509_V_ERR_PERMITTED_VIOLATION 47 +# define X509_V_ERR_EXCLUDED_VIOLATION 48 +# define X509_V_ERR_SUBTREE_MINMAX 49 +/* The application is not happy */ +# define X509_V_ERR_APPLICATION_VERIFICATION 50 +# define X509_V_ERR_UNSUPPORTED_CONSTRAINT_TYPE 51 +# define X509_V_ERR_UNSUPPORTED_CONSTRAINT_SYNTAX 52 +# define X509_V_ERR_UNSUPPORTED_NAME_SYNTAX 53 +# define X509_V_ERR_CRL_PATH_VALIDATION_ERROR 54 +/* Another issuer check debug option */ +# define X509_V_ERR_PATH_LOOP 55 +/* Suite B mode algorithm violation */ +# define X509_V_ERR_SUITE_B_INVALID_VERSION 56 +# define X509_V_ERR_SUITE_B_INVALID_ALGORITHM 57 +# define X509_V_ERR_SUITE_B_INVALID_CURVE 58 +# define X509_V_ERR_SUITE_B_INVALID_SIGNATURE_ALGORITHM 59 +# define X509_V_ERR_SUITE_B_LOS_NOT_ALLOWED 60 +# define X509_V_ERR_SUITE_B_CANNOT_SIGN_P_384_WITH_P_256 61 +/* Host, email and IP check errors */ +# define X509_V_ERR_HOSTNAME_MISMATCH 62 +# define X509_V_ERR_EMAIL_MISMATCH 63 +# define X509_V_ERR_IP_ADDRESS_MISMATCH 64 +/* DANE TLSA errors */ +# define X509_V_ERR_DANE_NO_MATCH 65 +/* security level errors */ +# define X509_V_ERR_EE_KEY_TOO_SMALL 66 +# define X509_V_ERR_CA_KEY_TOO_SMALL 67 +# define X509_V_ERR_CA_MD_TOO_WEAK 68 +/* Caller error */ +# define X509_V_ERR_INVALID_CALL 69 +/* Issuer lookup error */ +# define X509_V_ERR_STORE_LOOKUP 70 +/* Certificate transparency */ +# define X509_V_ERR_NO_VALID_SCTS 71 + +# define X509_V_ERR_PROXY_SUBJECT_NAME_VIOLATION 72 + +/* Certificate verify flags */ + +# if OPENSSL_API_COMPAT < 0x10100000L +# define X509_V_FLAG_CB_ISSUER_CHECK 0x0 /* Deprecated */ +# endif +/* Use check time instead of current time */ +# define X509_V_FLAG_USE_CHECK_TIME 0x2 +/* Lookup CRLs */ +# define X509_V_FLAG_CRL_CHECK 0x4 +/* Lookup CRLs for whole chain */ +# define X509_V_FLAG_CRL_CHECK_ALL 0x8 +/* Ignore unhandled critical extensions */ +# define X509_V_FLAG_IGNORE_CRITICAL 0x10 +/* Disable workarounds for broken certificates */ +# define X509_V_FLAG_X509_STRICT 0x20 +/* Enable proxy certificate validation */ +# define X509_V_FLAG_ALLOW_PROXY_CERTS 0x40 +/* Enable policy checking */ +# define X509_V_FLAG_POLICY_CHECK 0x80 +/* Policy variable require-explicit-policy */ +# define X509_V_FLAG_EXPLICIT_POLICY 0x100 +/* Policy variable inhibit-any-policy */ +# define X509_V_FLAG_INHIBIT_ANY 0x200 +/* Policy variable inhibit-policy-mapping */ +# define X509_V_FLAG_INHIBIT_MAP 0x400 +/* Notify callback that policy is OK */ +# define X509_V_FLAG_NOTIFY_POLICY 0x800 +/* Extended CRL features such as indirect CRLs, alternate CRL signing keys */ +# define X509_V_FLAG_EXTENDED_CRL_SUPPORT 0x1000 +/* Delta CRL support */ +# define X509_V_FLAG_USE_DELTAS 0x2000 +/* Check self-signed CA signature */ +# define X509_V_FLAG_CHECK_SS_SIGNATURE 0x4000 +/* Use trusted store first */ +# define X509_V_FLAG_TRUSTED_FIRST 0x8000 +/* Suite B 128 bit only mode: not normally used */ +# define X509_V_FLAG_SUITEB_128_LOS_ONLY 0x10000 +/* Suite B 192 bit only mode */ +# define X509_V_FLAG_SUITEB_192_LOS 0x20000 +/* Suite B 128 bit mode allowing 192 bit algorithms */ +# define X509_V_FLAG_SUITEB_128_LOS 0x30000 +/* Allow partial chains if at least one certificate is in trusted store */ +# define X509_V_FLAG_PARTIAL_CHAIN 0x80000 +/* + * If the initial chain is not trusted, do not attempt to build an alternative + * chain. Alternate chain checking was introduced in 1.1.0. Setting this flag + * will force the behaviour to match that of previous versions. + */ +# define X509_V_FLAG_NO_ALT_CHAINS 0x100000 +/* Do not check certificate/CRL validity against current time */ +# define X509_V_FLAG_NO_CHECK_TIME 0x200000 + +# define X509_VP_FLAG_DEFAULT 0x1 +# define X509_VP_FLAG_OVERWRITE 0x2 +# define X509_VP_FLAG_RESET_FLAGS 0x4 +# define X509_VP_FLAG_LOCKED 0x8 +# define X509_VP_FLAG_ONCE 0x10 + +/* Internal use: mask of policy related options */ +# define X509_V_FLAG_POLICY_MASK (X509_V_FLAG_POLICY_CHECK \ + | X509_V_FLAG_EXPLICIT_POLICY \ + | X509_V_FLAG_INHIBIT_ANY \ + | X509_V_FLAG_INHIBIT_MAP) + +int X509_OBJECT_idx_by_subject(STACK_OF(X509_OBJECT) *h, X509_LOOKUP_TYPE type, + X509_NAME *name); +X509_OBJECT *X509_OBJECT_retrieve_by_subject(STACK_OF(X509_OBJECT) *h, + X509_LOOKUP_TYPE type, + X509_NAME *name); +X509_OBJECT *X509_OBJECT_retrieve_match(STACK_OF(X509_OBJECT) *h, + X509_OBJECT *x); +int X509_OBJECT_up_ref_count(X509_OBJECT *a); +X509_OBJECT *X509_OBJECT_new(void); +void X509_OBJECT_free(X509_OBJECT *a); +X509_LOOKUP_TYPE X509_OBJECT_get_type(const X509_OBJECT *a); +X509 *X509_OBJECT_get0_X509(const X509_OBJECT *a); +X509_CRL *X509_OBJECT_get0_X509_CRL(X509_OBJECT *a); +X509_STORE *X509_STORE_new(void); +void X509_STORE_free(X509_STORE *v); +int X509_STORE_lock(X509_STORE *ctx); +int X509_STORE_unlock(X509_STORE *ctx); +int X509_STORE_up_ref(X509_STORE *v); +STACK_OF(X509_OBJECT) *X509_STORE_get0_objects(X509_STORE *v); + +STACK_OF(X509) *X509_STORE_CTX_get1_certs(X509_STORE_CTX *st, X509_NAME *nm); +STACK_OF(X509_CRL) *X509_STORE_CTX_get1_crls(X509_STORE_CTX *st, X509_NAME *nm); +int X509_STORE_set_flags(X509_STORE *ctx, unsigned long flags); +int X509_STORE_set_purpose(X509_STORE *ctx, int purpose); +int X509_STORE_set_trust(X509_STORE *ctx, int trust); +int X509_STORE_set1_param(X509_STORE *ctx, X509_VERIFY_PARAM *pm); +X509_VERIFY_PARAM *X509_STORE_get0_param(X509_STORE *ctx); +int X509_STORE_set_flags(X509_STORE *ctx, unsigned long flags); + +void X509_STORE_set_verify(X509_STORE *ctx, X509_STORE_CTX_verify_fn verify); +#define X509_STORE_set_verify_func(ctx, func) \ + X509_STORE_set_verify((ctx),(func)) +void X509_STORE_CTX_set_verify(X509_STORE_CTX *ctx, + X509_STORE_CTX_verify_fn verify); +X509_STORE_CTX_verify_fn X509_STORE_get_verify(X509_STORE *ctx); +void X509_STORE_set_verify_cb(X509_STORE *ctx, + X509_STORE_CTX_verify_cb verify_cb); +# define X509_STORE_set_verify_cb_func(ctx,func) \ + X509_STORE_set_verify_cb((ctx),(func)) +X509_STORE_CTX_verify_cb X509_STORE_get_verify_cb(X509_STORE *ctx); +void X509_STORE_set_get_issuer(X509_STORE *ctx, + X509_STORE_CTX_get_issuer_fn get_issuer); +X509_STORE_CTX_get_issuer_fn X509_STORE_get_get_issuer(X509_STORE *ctx); +void X509_STORE_set_check_issued(X509_STORE *ctx, + X509_STORE_CTX_check_issued_fn check_issued); +X509_STORE_CTX_check_issued_fn X509_STORE_get_check_issued(X509_STORE *ctx); +void X509_STORE_set_check_revocation(X509_STORE *ctx, + X509_STORE_CTX_check_revocation_fn check_revocation); +X509_STORE_CTX_check_revocation_fn X509_STORE_get_check_revocation(X509_STORE *ctx); +void X509_STORE_set_get_crl(X509_STORE *ctx, + X509_STORE_CTX_get_crl_fn get_crl); +X509_STORE_CTX_get_crl_fn X509_STORE_get_get_crl(X509_STORE *ctx); +void X509_STORE_set_check_crl(X509_STORE *ctx, + X509_STORE_CTX_check_crl_fn check_crl); +X509_STORE_CTX_check_crl_fn X509_STORE_get_check_crl(X509_STORE *ctx); +void X509_STORE_set_cert_crl(X509_STORE *ctx, + X509_STORE_CTX_cert_crl_fn cert_crl); +X509_STORE_CTX_cert_crl_fn X509_STORE_get_cert_crl(X509_STORE *ctx); +void X509_STORE_set_check_policy(X509_STORE *ctx, + X509_STORE_CTX_check_policy_fn check_policy); +X509_STORE_CTX_check_policy_fn X509_STORE_get_check_policy(X509_STORE *ctx); +void X509_STORE_set_lookup_certs(X509_STORE *ctx, + X509_STORE_CTX_lookup_certs_fn lookup_certs); +X509_STORE_CTX_lookup_certs_fn X509_STORE_get_lookup_certs(X509_STORE *ctx); +void X509_STORE_set_lookup_crls(X509_STORE *ctx, + X509_STORE_CTX_lookup_crls_fn lookup_crls); +#define X509_STORE_set_lookup_crls_cb(ctx, func) \ + X509_STORE_set_lookup_crls((ctx), (func)) +X509_STORE_CTX_lookup_crls_fn X509_STORE_get_lookup_crls(X509_STORE *ctx); +void X509_STORE_set_cleanup(X509_STORE *ctx, + X509_STORE_CTX_cleanup_fn cleanup); +X509_STORE_CTX_cleanup_fn X509_STORE_get_cleanup(X509_STORE *ctx); + +#define X509_STORE_get_ex_new_index(l, p, newf, dupf, freef) \ + CRYPTO_get_ex_new_index(CRYPTO_EX_INDEX_X509_STORE, l, p, newf, dupf, freef) +int X509_STORE_set_ex_data(X509_STORE *ctx, int idx, void *data); +void *X509_STORE_get_ex_data(X509_STORE *ctx, int idx); + +X509_STORE_CTX *X509_STORE_CTX_new(void); + +int X509_STORE_CTX_get1_issuer(X509 **issuer, X509_STORE_CTX *ctx, X509 *x); + +void X509_STORE_CTX_free(X509_STORE_CTX *ctx); +int X509_STORE_CTX_init(X509_STORE_CTX *ctx, X509_STORE *store, + X509 *x509, STACK_OF(X509) *chain); +void X509_STORE_CTX_set0_trusted_stack(X509_STORE_CTX *ctx, STACK_OF(X509) *sk); +void X509_STORE_CTX_cleanup(X509_STORE_CTX *ctx); + +X509_STORE *X509_STORE_CTX_get0_store(X509_STORE_CTX *ctx); +X509 *X509_STORE_CTX_get0_cert(X509_STORE_CTX *ctx); +STACK_OF(X509)* X509_STORE_CTX_get0_untrusted(X509_STORE_CTX *ctx); +void X509_STORE_CTX_set0_untrusted(X509_STORE_CTX *ctx, STACK_OF(X509) *sk); +void X509_STORE_CTX_set_verify_cb(X509_STORE_CTX *ctx, + X509_STORE_CTX_verify_cb verify); +X509_STORE_CTX_verify_cb X509_STORE_CTX_get_verify_cb(X509_STORE_CTX *ctx); +X509_STORE_CTX_verify_fn X509_STORE_CTX_get_verify(X509_STORE_CTX *ctx); +X509_STORE_CTX_get_issuer_fn X509_STORE_CTX_get_get_issuer(X509_STORE_CTX *ctx); +X509_STORE_CTX_check_issued_fn X509_STORE_CTX_get_check_issued(X509_STORE_CTX *ctx); +X509_STORE_CTX_check_revocation_fn X509_STORE_CTX_get_check_revocation(X509_STORE_CTX *ctx); +X509_STORE_CTX_get_crl_fn X509_STORE_CTX_get_get_crl(X509_STORE_CTX *ctx); +X509_STORE_CTX_check_crl_fn X509_STORE_CTX_get_check_crl(X509_STORE_CTX *ctx); +X509_STORE_CTX_cert_crl_fn X509_STORE_CTX_get_cert_crl(X509_STORE_CTX *ctx); +X509_STORE_CTX_check_policy_fn X509_STORE_CTX_get_check_policy(X509_STORE_CTX *ctx); +X509_STORE_CTX_lookup_certs_fn X509_STORE_CTX_get_lookup_certs(X509_STORE_CTX *ctx); +X509_STORE_CTX_lookup_crls_fn X509_STORE_CTX_get_lookup_crls(X509_STORE_CTX *ctx); +X509_STORE_CTX_cleanup_fn X509_STORE_CTX_get_cleanup(X509_STORE_CTX *ctx); + +#if OPENSSL_API_COMPAT < 0x10100000L +# define X509_STORE_CTX_get_chain X509_STORE_CTX_get0_chain +# define X509_STORE_CTX_set_chain X509_STORE_CTX_set0_untrusted +# define X509_STORE_CTX_trusted_stack X509_STORE_CTX_set0_trusted_stack +# define X509_STORE_get_by_subject X509_STORE_CTX_get_by_subject +# define X509_STORE_get1_cert X509_STORE_CTX_get1_certs +# define X509_STORE_get1_crl X509_STORE_CTX_get1_crls +#endif + +X509_LOOKUP *X509_STORE_add_lookup(X509_STORE *v, X509_LOOKUP_METHOD *m); +X509_LOOKUP_METHOD *X509_LOOKUP_hash_dir(void); +X509_LOOKUP_METHOD *X509_LOOKUP_file(void); + +int X509_STORE_add_cert(X509_STORE *ctx, X509 *x); +int X509_STORE_add_crl(X509_STORE *ctx, X509_CRL *x); + +int X509_STORE_CTX_get_by_subject(X509_STORE_CTX *vs, X509_LOOKUP_TYPE type, + X509_NAME *name, X509_OBJECT *ret); +X509_OBJECT *X509_STORE_CTX_get_obj_by_subject(X509_STORE_CTX *vs, + X509_LOOKUP_TYPE type, + X509_NAME *name); + +int X509_LOOKUP_ctrl(X509_LOOKUP *ctx, int cmd, const char *argc, + long argl, char **ret); + +int X509_load_cert_file(X509_LOOKUP *ctx, const char *file, int type); +int X509_load_crl_file(X509_LOOKUP *ctx, const char *file, int type); +int X509_load_cert_crl_file(X509_LOOKUP *ctx, const char *file, int type); + +X509_LOOKUP *X509_LOOKUP_new(X509_LOOKUP_METHOD *method); +void X509_LOOKUP_free(X509_LOOKUP *ctx); +int X509_LOOKUP_init(X509_LOOKUP *ctx); +int X509_LOOKUP_by_subject(X509_LOOKUP *ctx, X509_LOOKUP_TYPE type, + X509_NAME *name, X509_OBJECT *ret); +int X509_LOOKUP_by_issuer_serial(X509_LOOKUP *ctx, X509_LOOKUP_TYPE type, + X509_NAME *name, ASN1_INTEGER *serial, + X509_OBJECT *ret); +int X509_LOOKUP_by_fingerprint(X509_LOOKUP *ctx, X509_LOOKUP_TYPE type, + const unsigned char *bytes, int len, + X509_OBJECT *ret); +int X509_LOOKUP_by_alias(X509_LOOKUP *ctx, X509_LOOKUP_TYPE type, + const char *str, int len, X509_OBJECT *ret); +int X509_LOOKUP_shutdown(X509_LOOKUP *ctx); + +int X509_STORE_load_locations(X509_STORE *ctx, + const char *file, const char *dir); +int X509_STORE_set_default_paths(X509_STORE *ctx); + +#define X509_STORE_CTX_get_ex_new_index(l, p, newf, dupf, freef) \ + CRYPTO_get_ex_new_index(CRYPTO_EX_INDEX_X509_STORE_CTX, l, p, newf, dupf, freef) +int X509_STORE_CTX_set_ex_data(X509_STORE_CTX *ctx, int idx, void *data); +void *X509_STORE_CTX_get_ex_data(X509_STORE_CTX *ctx, int idx); +int X509_STORE_CTX_get_error(X509_STORE_CTX *ctx); +void X509_STORE_CTX_set_error(X509_STORE_CTX *ctx, int s); +int X509_STORE_CTX_get_error_depth(X509_STORE_CTX *ctx); +void X509_STORE_CTX_set_error_depth(X509_STORE_CTX *ctx, int depth); +X509 *X509_STORE_CTX_get_current_cert(X509_STORE_CTX *ctx); +void X509_STORE_CTX_set_current_cert(X509_STORE_CTX *ctx, X509 *x); +X509 *X509_STORE_CTX_get0_current_issuer(X509_STORE_CTX *ctx); +X509_CRL *X509_STORE_CTX_get0_current_crl(X509_STORE_CTX *ctx); +X509_STORE_CTX *X509_STORE_CTX_get0_parent_ctx(X509_STORE_CTX *ctx); +STACK_OF(X509) *X509_STORE_CTX_get0_chain(X509_STORE_CTX *ctx); +STACK_OF(X509) *X509_STORE_CTX_get1_chain(X509_STORE_CTX *ctx); +void X509_STORE_CTX_set_cert(X509_STORE_CTX *c, X509 *x); +void X509_STORE_CTX_set0_verified_chain(X509_STORE_CTX *c, STACK_OF(X509) *sk); +void X509_STORE_CTX_set0_crls(X509_STORE_CTX *c, STACK_OF(X509_CRL) *sk); +int X509_STORE_CTX_set_purpose(X509_STORE_CTX *ctx, int purpose); +int X509_STORE_CTX_set_trust(X509_STORE_CTX *ctx, int trust); +int X509_STORE_CTX_purpose_inherit(X509_STORE_CTX *ctx, int def_purpose, + int purpose, int trust); +void X509_STORE_CTX_set_flags(X509_STORE_CTX *ctx, unsigned long flags); +void X509_STORE_CTX_set_time(X509_STORE_CTX *ctx, unsigned long flags, + time_t t); + +X509_POLICY_TREE *X509_STORE_CTX_get0_policy_tree(X509_STORE_CTX *ctx); +int X509_STORE_CTX_get_explicit_policy(X509_STORE_CTX *ctx); +int X509_STORE_CTX_get_num_untrusted(X509_STORE_CTX *ctx); + +X509_VERIFY_PARAM *X509_STORE_CTX_get0_param(X509_STORE_CTX *ctx); +void X509_STORE_CTX_set0_param(X509_STORE_CTX *ctx, X509_VERIFY_PARAM *param); +int X509_STORE_CTX_set_default(X509_STORE_CTX *ctx, const char *name); + +/* + * Bridge opacity barrier between libcrypt and libssl, also needed to support + * offline testing in test/danetest.c + */ +void X509_STORE_CTX_set0_dane(X509_STORE_CTX *ctx, SSL_DANE *dane); +#define DANE_FLAG_NO_DANE_EE_NAMECHECKS (1L << 0) + +/* X509_VERIFY_PARAM functions */ + +X509_VERIFY_PARAM *X509_VERIFY_PARAM_new(void); +void X509_VERIFY_PARAM_free(X509_VERIFY_PARAM *param); +int X509_VERIFY_PARAM_inherit(X509_VERIFY_PARAM *to, + const X509_VERIFY_PARAM *from); +int X509_VERIFY_PARAM_set1(X509_VERIFY_PARAM *to, + const X509_VERIFY_PARAM *from); +int X509_VERIFY_PARAM_set1_name(X509_VERIFY_PARAM *param, const char *name); +int X509_VERIFY_PARAM_set_flags(X509_VERIFY_PARAM *param, + unsigned long flags); +int X509_VERIFY_PARAM_clear_flags(X509_VERIFY_PARAM *param, + unsigned long flags); +unsigned long X509_VERIFY_PARAM_get_flags(X509_VERIFY_PARAM *param); +int X509_VERIFY_PARAM_set_purpose(X509_VERIFY_PARAM *param, int purpose); +int X509_VERIFY_PARAM_set_trust(X509_VERIFY_PARAM *param, int trust); +void X509_VERIFY_PARAM_set_depth(X509_VERIFY_PARAM *param, int depth); +void X509_VERIFY_PARAM_set_auth_level(X509_VERIFY_PARAM *param, int auth_level); +time_t X509_VERIFY_PARAM_get_time(const X509_VERIFY_PARAM *param); +void X509_VERIFY_PARAM_set_time(X509_VERIFY_PARAM *param, time_t t); +int X509_VERIFY_PARAM_add0_policy(X509_VERIFY_PARAM *param, + ASN1_OBJECT *policy); +int X509_VERIFY_PARAM_set1_policies(X509_VERIFY_PARAM *param, + STACK_OF(ASN1_OBJECT) *policies); + +int X509_VERIFY_PARAM_set_inh_flags(X509_VERIFY_PARAM *param, + uint32_t flags); +uint32_t X509_VERIFY_PARAM_get_inh_flags(const X509_VERIFY_PARAM *param); + +int X509_VERIFY_PARAM_set1_host(X509_VERIFY_PARAM *param, + const char *name, size_t namelen); +int X509_VERIFY_PARAM_add1_host(X509_VERIFY_PARAM *param, + const char *name, size_t namelen); +void X509_VERIFY_PARAM_set_hostflags(X509_VERIFY_PARAM *param, + unsigned int flags); +char *X509_VERIFY_PARAM_get0_peername(X509_VERIFY_PARAM *); +void X509_VERIFY_PARAM_move_peername(X509_VERIFY_PARAM *, X509_VERIFY_PARAM *); +int X509_VERIFY_PARAM_set1_email(X509_VERIFY_PARAM *param, + const char *email, size_t emaillen); +int X509_VERIFY_PARAM_set1_ip(X509_VERIFY_PARAM *param, + const unsigned char *ip, size_t iplen); +int X509_VERIFY_PARAM_set1_ip_asc(X509_VERIFY_PARAM *param, + const char *ipasc); + +int X509_VERIFY_PARAM_get_depth(const X509_VERIFY_PARAM *param); +int X509_VERIFY_PARAM_get_auth_level(const X509_VERIFY_PARAM *param); +const char *X509_VERIFY_PARAM_get0_name(const X509_VERIFY_PARAM *param); + +int X509_VERIFY_PARAM_add0_table(X509_VERIFY_PARAM *param); +int X509_VERIFY_PARAM_get_count(void); +const X509_VERIFY_PARAM *X509_VERIFY_PARAM_get0(int id); +const X509_VERIFY_PARAM *X509_VERIFY_PARAM_lookup(const char *name); +void X509_VERIFY_PARAM_table_cleanup(void); + +/* Non positive return values are errors */ +#define X509_PCY_TREE_FAILURE -2 /* Failure to satisfy explicit policy */ +#define X509_PCY_TREE_INVALID -1 /* Inconsistent or invalid extensions */ +#define X509_PCY_TREE_INTERNAL 0 /* Internal error, most likely malloc */ + +/* + * Positive return values form a bit mask, all but the first are internal to + * the library and don't appear in results from X509_policy_check(). + */ +#define X509_PCY_TREE_VALID 1 /* The policy tree is valid */ +#define X509_PCY_TREE_EMPTY 2 /* The policy tree is empty */ +#define X509_PCY_TREE_EXPLICIT 4 /* Explicit policy required */ + +int X509_policy_check(X509_POLICY_TREE **ptree, int *pexplicit_policy, + STACK_OF(X509) *certs, + STACK_OF(ASN1_OBJECT) *policy_oids, unsigned int flags); + +void X509_policy_tree_free(X509_POLICY_TREE *tree); + +int X509_policy_tree_level_count(const X509_POLICY_TREE *tree); +X509_POLICY_LEVEL *X509_policy_tree_get0_level(const X509_POLICY_TREE *tree, + int i); + +STACK_OF(X509_POLICY_NODE) *X509_policy_tree_get0_policies(const + X509_POLICY_TREE + *tree); + +STACK_OF(X509_POLICY_NODE) *X509_policy_tree_get0_user_policies(const + X509_POLICY_TREE + *tree); + +int X509_policy_level_node_count(X509_POLICY_LEVEL *level); + +X509_POLICY_NODE *X509_policy_level_get0_node(X509_POLICY_LEVEL *level, + int i); + +const ASN1_OBJECT *X509_policy_node_get0_policy(const X509_POLICY_NODE *node); + +STACK_OF(POLICYQUALINFO) *X509_policy_node_get0_qualifiers(const + X509_POLICY_NODE + *node); +const X509_POLICY_NODE *X509_policy_node_get0_parent(const X509_POLICY_NODE + *node); + +#ifdef __cplusplus +} +#endif +#endif diff --git a/mobile/ios/FxA/FxA/include/openssl/x509v3.h b/mobile/ios/FxA/FxA/include/openssl/x509v3.h new file mode 100644 index 0000000000..1d8ef87c4c --- /dev/null +++ b/mobile/ios/FxA/FxA/include/openssl/x509v3.h @@ -0,0 +1,1006 @@ +/* + * Copyright 1999-2016 The OpenSSL Project Authors. All Rights Reserved. + * + * Licensed under the OpenSSL license (the "License"). You may not use + * this file except in compliance with the License. You can obtain a copy + * in the file LICENSE in the source distribution or at + * https://www.openssl.org/source/license.html + */ + +#ifndef HEADER_X509V3_H +# define HEADER_X509V3_H + +# include +# include +# include + +#ifdef __cplusplus +extern "C" { +#endif + +/* Forward reference */ +struct v3_ext_method; +struct v3_ext_ctx; + +/* Useful typedefs */ + +typedef void *(*X509V3_EXT_NEW)(void); +typedef void (*X509V3_EXT_FREE) (void *); +typedef void *(*X509V3_EXT_D2I)(void *, const unsigned char **, long); +typedef int (*X509V3_EXT_I2D) (void *, unsigned char **); +typedef STACK_OF(CONF_VALUE) * + (*X509V3_EXT_I2V) (const struct v3_ext_method *method, void *ext, + STACK_OF(CONF_VALUE) *extlist); +typedef void *(*X509V3_EXT_V2I)(const struct v3_ext_method *method, + struct v3_ext_ctx *ctx, + STACK_OF(CONF_VALUE) *values); +typedef char *(*X509V3_EXT_I2S)(const struct v3_ext_method *method, + void *ext); +typedef void *(*X509V3_EXT_S2I)(const struct v3_ext_method *method, + struct v3_ext_ctx *ctx, const char *str); +typedef int (*X509V3_EXT_I2R) (const struct v3_ext_method *method, void *ext, + BIO *out, int indent); +typedef void *(*X509V3_EXT_R2I)(const struct v3_ext_method *method, + struct v3_ext_ctx *ctx, const char *str); + +/* V3 extension structure */ + +struct v3_ext_method { + int ext_nid; + int ext_flags; +/* If this is set the following four fields are ignored */ + ASN1_ITEM_EXP *it; +/* Old style ASN1 calls */ + X509V3_EXT_NEW ext_new; + X509V3_EXT_FREE ext_free; + X509V3_EXT_D2I d2i; + X509V3_EXT_I2D i2d; +/* The following pair is used for string extensions */ + X509V3_EXT_I2S i2s; + X509V3_EXT_S2I s2i; +/* The following pair is used for multi-valued extensions */ + X509V3_EXT_I2V i2v; + X509V3_EXT_V2I v2i; +/* The following are used for raw extensions */ + X509V3_EXT_I2R i2r; + X509V3_EXT_R2I r2i; + void *usr_data; /* Any extension specific data */ +}; + +typedef struct X509V3_CONF_METHOD_st { + char *(*get_string) (void *db, const char *section, const char *value); + STACK_OF(CONF_VALUE) *(*get_section) (void *db, const char *section); + void (*free_string) (void *db, char *string); + void (*free_section) (void *db, STACK_OF(CONF_VALUE) *section); +} X509V3_CONF_METHOD; + +/* Context specific info */ +struct v3_ext_ctx { +# define CTX_TEST 0x1 +# define X509V3_CTX_REPLACE 0x2 + int flags; + X509 *issuer_cert; + X509 *subject_cert; + X509_REQ *subject_req; + X509_CRL *crl; + X509V3_CONF_METHOD *db_meth; + void *db; +/* Maybe more here */ +}; + +typedef struct v3_ext_method X509V3_EXT_METHOD; + +DEFINE_STACK_OF(X509V3_EXT_METHOD) + +/* ext_flags values */ +# define X509V3_EXT_DYNAMIC 0x1 +# define X509V3_EXT_CTX_DEP 0x2 +# define X509V3_EXT_MULTILINE 0x4 + +typedef BIT_STRING_BITNAME ENUMERATED_NAMES; + +typedef struct BASIC_CONSTRAINTS_st { + int ca; + ASN1_INTEGER *pathlen; +} BASIC_CONSTRAINTS; + +typedef struct PKEY_USAGE_PERIOD_st { + ASN1_GENERALIZEDTIME *notBefore; + ASN1_GENERALIZEDTIME *notAfter; +} PKEY_USAGE_PERIOD; + +typedef struct otherName_st { + ASN1_OBJECT *type_id; + ASN1_TYPE *value; +} OTHERNAME; + +typedef struct EDIPartyName_st { + ASN1_STRING *nameAssigner; + ASN1_STRING *partyName; +} EDIPARTYNAME; + +typedef struct GENERAL_NAME_st { +# define GEN_OTHERNAME 0 +# define GEN_EMAIL 1 +# define GEN_DNS 2 +# define GEN_X400 3 +# define GEN_DIRNAME 4 +# define GEN_EDIPARTY 5 +# define GEN_URI 6 +# define GEN_IPADD 7 +# define GEN_RID 8 + int type; + union { + char *ptr; + OTHERNAME *otherName; /* otherName */ + ASN1_IA5STRING *rfc822Name; + ASN1_IA5STRING *dNSName; + ASN1_TYPE *x400Address; + X509_NAME *directoryName; + EDIPARTYNAME *ediPartyName; + ASN1_IA5STRING *uniformResourceIdentifier; + ASN1_OCTET_STRING *iPAddress; + ASN1_OBJECT *registeredID; + /* Old names */ + ASN1_OCTET_STRING *ip; /* iPAddress */ + X509_NAME *dirn; /* dirn */ + ASN1_IA5STRING *ia5; /* rfc822Name, dNSName, + * uniformResourceIdentifier */ + ASN1_OBJECT *rid; /* registeredID */ + ASN1_TYPE *other; /* x400Address */ + } d; +} GENERAL_NAME; + +typedef struct ACCESS_DESCRIPTION_st { + ASN1_OBJECT *method; + GENERAL_NAME *location; +} ACCESS_DESCRIPTION; + +typedef STACK_OF(ACCESS_DESCRIPTION) AUTHORITY_INFO_ACCESS; + +typedef STACK_OF(ASN1_OBJECT) EXTENDED_KEY_USAGE; + +typedef STACK_OF(ASN1_INTEGER) TLS_FEATURE; + +DEFINE_STACK_OF(GENERAL_NAME) +typedef STACK_OF(GENERAL_NAME) GENERAL_NAMES; +DEFINE_STACK_OF(GENERAL_NAMES) + +DEFINE_STACK_OF(ACCESS_DESCRIPTION) + +typedef struct DIST_POINT_NAME_st { + int type; + union { + GENERAL_NAMES *fullname; + STACK_OF(X509_NAME_ENTRY) *relativename; + } name; +/* If relativename then this contains the full distribution point name */ + X509_NAME *dpname; +} DIST_POINT_NAME; +/* All existing reasons */ +# define CRLDP_ALL_REASONS 0x807f + +# define CRL_REASON_NONE -1 +# define CRL_REASON_UNSPECIFIED 0 +# define CRL_REASON_KEY_COMPROMISE 1 +# define CRL_REASON_CA_COMPROMISE 2 +# define CRL_REASON_AFFILIATION_CHANGED 3 +# define CRL_REASON_SUPERSEDED 4 +# define CRL_REASON_CESSATION_OF_OPERATION 5 +# define CRL_REASON_CERTIFICATE_HOLD 6 +# define CRL_REASON_REMOVE_FROM_CRL 8 +# define CRL_REASON_PRIVILEGE_WITHDRAWN 9 +# define CRL_REASON_AA_COMPROMISE 10 + +struct DIST_POINT_st { + DIST_POINT_NAME *distpoint; + ASN1_BIT_STRING *reasons; + GENERAL_NAMES *CRLissuer; + int dp_reasons; +}; + +typedef STACK_OF(DIST_POINT) CRL_DIST_POINTS; + +DEFINE_STACK_OF(DIST_POINT) + +struct AUTHORITY_KEYID_st { + ASN1_OCTET_STRING *keyid; + GENERAL_NAMES *issuer; + ASN1_INTEGER *serial; +}; + +/* Strong extranet structures */ + +typedef struct SXNET_ID_st { + ASN1_INTEGER *zone; + ASN1_OCTET_STRING *user; +} SXNETID; + +DEFINE_STACK_OF(SXNETID) + +typedef struct SXNET_st { + ASN1_INTEGER *version; + STACK_OF(SXNETID) *ids; +} SXNET; + +typedef struct NOTICEREF_st { + ASN1_STRING *organization; + STACK_OF(ASN1_INTEGER) *noticenos; +} NOTICEREF; + +typedef struct USERNOTICE_st { + NOTICEREF *noticeref; + ASN1_STRING *exptext; +} USERNOTICE; + +typedef struct POLICYQUALINFO_st { + ASN1_OBJECT *pqualid; + union { + ASN1_IA5STRING *cpsuri; + USERNOTICE *usernotice; + ASN1_TYPE *other; + } d; +} POLICYQUALINFO; + +DEFINE_STACK_OF(POLICYQUALINFO) + +typedef struct POLICYINFO_st { + ASN1_OBJECT *policyid; + STACK_OF(POLICYQUALINFO) *qualifiers; +} POLICYINFO; + +typedef STACK_OF(POLICYINFO) CERTIFICATEPOLICIES; + +DEFINE_STACK_OF(POLICYINFO) + +typedef struct POLICY_MAPPING_st { + ASN1_OBJECT *issuerDomainPolicy; + ASN1_OBJECT *subjectDomainPolicy; +} POLICY_MAPPING; + +DEFINE_STACK_OF(POLICY_MAPPING) + +typedef STACK_OF(POLICY_MAPPING) POLICY_MAPPINGS; + +typedef struct GENERAL_SUBTREE_st { + GENERAL_NAME *base; + ASN1_INTEGER *minimum; + ASN1_INTEGER *maximum; +} GENERAL_SUBTREE; + +DEFINE_STACK_OF(GENERAL_SUBTREE) + +struct NAME_CONSTRAINTS_st { + STACK_OF(GENERAL_SUBTREE) *permittedSubtrees; + STACK_OF(GENERAL_SUBTREE) *excludedSubtrees; +}; + +typedef struct POLICY_CONSTRAINTS_st { + ASN1_INTEGER *requireExplicitPolicy; + ASN1_INTEGER *inhibitPolicyMapping; +} POLICY_CONSTRAINTS; + +/* Proxy certificate structures, see RFC 3820 */ +typedef struct PROXY_POLICY_st { + ASN1_OBJECT *policyLanguage; + ASN1_OCTET_STRING *policy; +} PROXY_POLICY; + +typedef struct PROXY_CERT_INFO_EXTENSION_st { + ASN1_INTEGER *pcPathLengthConstraint; + PROXY_POLICY *proxyPolicy; +} PROXY_CERT_INFO_EXTENSION; + +DECLARE_ASN1_FUNCTIONS(PROXY_POLICY) +DECLARE_ASN1_FUNCTIONS(PROXY_CERT_INFO_EXTENSION) + +struct ISSUING_DIST_POINT_st { + DIST_POINT_NAME *distpoint; + int onlyuser; + int onlyCA; + ASN1_BIT_STRING *onlysomereasons; + int indirectCRL; + int onlyattr; +}; + +/* Values in idp_flags field */ +/* IDP present */ +# define IDP_PRESENT 0x1 +/* IDP values inconsistent */ +# define IDP_INVALID 0x2 +/* onlyuser true */ +# define IDP_ONLYUSER 0x4 +/* onlyCA true */ +# define IDP_ONLYCA 0x8 +/* onlyattr true */ +# define IDP_ONLYATTR 0x10 +/* indirectCRL true */ +# define IDP_INDIRECT 0x20 +/* onlysomereasons present */ +# define IDP_REASONS 0x40 + +# define X509V3_conf_err(val) ERR_add_error_data(6, "section:", val->section, \ +",name:", val->name, ",value:", val->value); + +# define X509V3_set_ctx_test(ctx) \ + X509V3_set_ctx(ctx, NULL, NULL, NULL, NULL, CTX_TEST) +# define X509V3_set_ctx_nodb(ctx) (ctx)->db = NULL; + +# define EXT_BITSTRING(nid, table) { nid, 0, ASN1_ITEM_ref(ASN1_BIT_STRING), \ + 0,0,0,0, \ + 0,0, \ + (X509V3_EXT_I2V)i2v_ASN1_BIT_STRING, \ + (X509V3_EXT_V2I)v2i_ASN1_BIT_STRING, \ + NULL, NULL, \ + table} + +# define EXT_IA5STRING(nid) { nid, 0, ASN1_ITEM_ref(ASN1_IA5STRING), \ + 0,0,0,0, \ + (X509V3_EXT_I2S)i2s_ASN1_IA5STRING, \ + (X509V3_EXT_S2I)s2i_ASN1_IA5STRING, \ + 0,0,0,0, \ + NULL} + +# define EXT_END { -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0} + +/* X509_PURPOSE stuff */ + +# define EXFLAG_BCONS 0x1 +# define EXFLAG_KUSAGE 0x2 +# define EXFLAG_XKUSAGE 0x4 +# define EXFLAG_NSCERT 0x8 + +# define EXFLAG_CA 0x10 +/* Really self issued not necessarily self signed */ +# define EXFLAG_SI 0x20 +# define EXFLAG_V1 0x40 +# define EXFLAG_INVALID 0x80 +/* EXFLAG_SET is set to indicate that some values have been precomputed */ +# define EXFLAG_SET 0x100 +# define EXFLAG_CRITICAL 0x200 +# define EXFLAG_PROXY 0x400 + +# define EXFLAG_INVALID_POLICY 0x800 +# define EXFLAG_FRESHEST 0x1000 +/* Self signed */ +# define EXFLAG_SS 0x2000 + +# define KU_DIGITAL_SIGNATURE 0x0080 +# define KU_NON_REPUDIATION 0x0040 +# define KU_KEY_ENCIPHERMENT 0x0020 +# define KU_DATA_ENCIPHERMENT 0x0010 +# define KU_KEY_AGREEMENT 0x0008 +# define KU_KEY_CERT_SIGN 0x0004 +# define KU_CRL_SIGN 0x0002 +# define KU_ENCIPHER_ONLY 0x0001 +# define KU_DECIPHER_ONLY 0x8000 + +# define NS_SSL_CLIENT 0x80 +# define NS_SSL_SERVER 0x40 +# define NS_SMIME 0x20 +# define NS_OBJSIGN 0x10 +# define NS_SSL_CA 0x04 +# define NS_SMIME_CA 0x02 +# define NS_OBJSIGN_CA 0x01 +# define NS_ANY_CA (NS_SSL_CA|NS_SMIME_CA|NS_OBJSIGN_CA) + +# define XKU_SSL_SERVER 0x1 +# define XKU_SSL_CLIENT 0x2 +# define XKU_SMIME 0x4 +# define XKU_CODE_SIGN 0x8 +# define XKU_SGC 0x10 +# define XKU_OCSP_SIGN 0x20 +# define XKU_TIMESTAMP 0x40 +# define XKU_DVCS 0x80 +# define XKU_ANYEKU 0x100 + +# define X509_PURPOSE_DYNAMIC 0x1 +# define X509_PURPOSE_DYNAMIC_NAME 0x2 + +typedef struct x509_purpose_st { + int purpose; + int trust; /* Default trust ID */ + int flags; + int (*check_purpose) (const struct x509_purpose_st *, const X509 *, int); + char *name; + char *sname; + void *usr_data; +} X509_PURPOSE; + +# define X509_PURPOSE_SSL_CLIENT 1 +# define X509_PURPOSE_SSL_SERVER 2 +# define X509_PURPOSE_NS_SSL_SERVER 3 +# define X509_PURPOSE_SMIME_SIGN 4 +# define X509_PURPOSE_SMIME_ENCRYPT 5 +# define X509_PURPOSE_CRL_SIGN 6 +# define X509_PURPOSE_ANY 7 +# define X509_PURPOSE_OCSP_HELPER 8 +# define X509_PURPOSE_TIMESTAMP_SIGN 9 + +# define X509_PURPOSE_MIN 1 +# define X509_PURPOSE_MAX 9 + +/* Flags for X509V3_EXT_print() */ + +# define X509V3_EXT_UNKNOWN_MASK (0xfL << 16) +/* Return error for unknown extensions */ +# define X509V3_EXT_DEFAULT 0 +/* Print error for unknown extensions */ +# define X509V3_EXT_ERROR_UNKNOWN (1L << 16) +/* ASN1 parse unknown extensions */ +# define X509V3_EXT_PARSE_UNKNOWN (2L << 16) +/* BIO_dump unknown extensions */ +# define X509V3_EXT_DUMP_UNKNOWN (3L << 16) + +/* Flags for X509V3_add1_i2d */ + +# define X509V3_ADD_OP_MASK 0xfL +# define X509V3_ADD_DEFAULT 0L +# define X509V3_ADD_APPEND 1L +# define X509V3_ADD_REPLACE 2L +# define X509V3_ADD_REPLACE_EXISTING 3L +# define X509V3_ADD_KEEP_EXISTING 4L +# define X509V3_ADD_DELETE 5L +# define X509V3_ADD_SILENT 0x10 + +DEFINE_STACK_OF(X509_PURPOSE) + +DECLARE_ASN1_FUNCTIONS(BASIC_CONSTRAINTS) + +DECLARE_ASN1_FUNCTIONS(SXNET) +DECLARE_ASN1_FUNCTIONS(SXNETID) + +int SXNET_add_id_asc(SXNET **psx, const char *zone, const char *user, int userlen); +int SXNET_add_id_ulong(SXNET **psx, unsigned long lzone, const char *user, + int userlen); +int SXNET_add_id_INTEGER(SXNET **psx, ASN1_INTEGER *izone, const char *user, + int userlen); + +ASN1_OCTET_STRING *SXNET_get_id_asc(SXNET *sx, const char *zone); +ASN1_OCTET_STRING *SXNET_get_id_ulong(SXNET *sx, unsigned long lzone); +ASN1_OCTET_STRING *SXNET_get_id_INTEGER(SXNET *sx, ASN1_INTEGER *zone); + +DECLARE_ASN1_FUNCTIONS(AUTHORITY_KEYID) + +DECLARE_ASN1_FUNCTIONS(PKEY_USAGE_PERIOD) + +DECLARE_ASN1_FUNCTIONS(GENERAL_NAME) +GENERAL_NAME *GENERAL_NAME_dup(GENERAL_NAME *a); +int GENERAL_NAME_cmp(GENERAL_NAME *a, GENERAL_NAME *b); + +ASN1_BIT_STRING *v2i_ASN1_BIT_STRING(X509V3_EXT_METHOD *method, + X509V3_CTX *ctx, + STACK_OF(CONF_VALUE) *nval); +STACK_OF(CONF_VALUE) *i2v_ASN1_BIT_STRING(X509V3_EXT_METHOD *method, + ASN1_BIT_STRING *bits, + STACK_OF(CONF_VALUE) *extlist); +char *i2s_ASN1_IA5STRING(X509V3_EXT_METHOD *method, ASN1_IA5STRING *ia5); +ASN1_IA5STRING *s2i_ASN1_IA5STRING(X509V3_EXT_METHOD *method, + X509V3_CTX *ctx, const char *str); + +STACK_OF(CONF_VALUE) *i2v_GENERAL_NAME(X509V3_EXT_METHOD *method, + GENERAL_NAME *gen, + STACK_OF(CONF_VALUE) *ret); +int GENERAL_NAME_print(BIO *out, GENERAL_NAME *gen); + +DECLARE_ASN1_FUNCTIONS(GENERAL_NAMES) + +STACK_OF(CONF_VALUE) *i2v_GENERAL_NAMES(X509V3_EXT_METHOD *method, + GENERAL_NAMES *gen, + STACK_OF(CONF_VALUE) *extlist); +GENERAL_NAMES *v2i_GENERAL_NAMES(const X509V3_EXT_METHOD *method, + X509V3_CTX *ctx, STACK_OF(CONF_VALUE) *nval); + +DECLARE_ASN1_FUNCTIONS(OTHERNAME) +DECLARE_ASN1_FUNCTIONS(EDIPARTYNAME) +int OTHERNAME_cmp(OTHERNAME *a, OTHERNAME *b); +void GENERAL_NAME_set0_value(GENERAL_NAME *a, int type, void *value); +void *GENERAL_NAME_get0_value(GENERAL_NAME *a, int *ptype); +int GENERAL_NAME_set0_othername(GENERAL_NAME *gen, + ASN1_OBJECT *oid, ASN1_TYPE *value); +int GENERAL_NAME_get0_otherName(GENERAL_NAME *gen, + ASN1_OBJECT **poid, ASN1_TYPE **pvalue); + +char *i2s_ASN1_OCTET_STRING(X509V3_EXT_METHOD *method, + const ASN1_OCTET_STRING *ia5); +ASN1_OCTET_STRING *s2i_ASN1_OCTET_STRING(X509V3_EXT_METHOD *method, + X509V3_CTX *ctx, const char *str); + +DECLARE_ASN1_FUNCTIONS(EXTENDED_KEY_USAGE) +int i2a_ACCESS_DESCRIPTION(BIO *bp, const ACCESS_DESCRIPTION *a); + +DECLARE_ASN1_ALLOC_FUNCTIONS(TLS_FEATURE) + +DECLARE_ASN1_FUNCTIONS(CERTIFICATEPOLICIES) +DECLARE_ASN1_FUNCTIONS(POLICYINFO) +DECLARE_ASN1_FUNCTIONS(POLICYQUALINFO) +DECLARE_ASN1_FUNCTIONS(USERNOTICE) +DECLARE_ASN1_FUNCTIONS(NOTICEREF) + +DECLARE_ASN1_FUNCTIONS(CRL_DIST_POINTS) +DECLARE_ASN1_FUNCTIONS(DIST_POINT) +DECLARE_ASN1_FUNCTIONS(DIST_POINT_NAME) +DECLARE_ASN1_FUNCTIONS(ISSUING_DIST_POINT) + +int DIST_POINT_set_dpname(DIST_POINT_NAME *dpn, X509_NAME *iname); + +int NAME_CONSTRAINTS_check(X509 *x, NAME_CONSTRAINTS *nc); +int NAME_CONSTRAINTS_check_CN(X509 *x, NAME_CONSTRAINTS *nc); + +DECLARE_ASN1_FUNCTIONS(ACCESS_DESCRIPTION) +DECLARE_ASN1_FUNCTIONS(AUTHORITY_INFO_ACCESS) + +DECLARE_ASN1_ITEM(POLICY_MAPPING) +DECLARE_ASN1_ALLOC_FUNCTIONS(POLICY_MAPPING) +DECLARE_ASN1_ITEM(POLICY_MAPPINGS) + +DECLARE_ASN1_ITEM(GENERAL_SUBTREE) +DECLARE_ASN1_ALLOC_FUNCTIONS(GENERAL_SUBTREE) + +DECLARE_ASN1_ITEM(NAME_CONSTRAINTS) +DECLARE_ASN1_ALLOC_FUNCTIONS(NAME_CONSTRAINTS) + +DECLARE_ASN1_ALLOC_FUNCTIONS(POLICY_CONSTRAINTS) +DECLARE_ASN1_ITEM(POLICY_CONSTRAINTS) + +GENERAL_NAME *a2i_GENERAL_NAME(GENERAL_NAME *out, + const X509V3_EXT_METHOD *method, + X509V3_CTX *ctx, int gen_type, + const char *value, int is_nc); + +# ifdef HEADER_CONF_H +GENERAL_NAME *v2i_GENERAL_NAME(const X509V3_EXT_METHOD *method, + X509V3_CTX *ctx, CONF_VALUE *cnf); +GENERAL_NAME *v2i_GENERAL_NAME_ex(GENERAL_NAME *out, + const X509V3_EXT_METHOD *method, + X509V3_CTX *ctx, CONF_VALUE *cnf, + int is_nc); +void X509V3_conf_free(CONF_VALUE *val); + +X509_EXTENSION *X509V3_EXT_nconf_nid(CONF *conf, X509V3_CTX *ctx, int ext_nid, + const char *value); +X509_EXTENSION *X509V3_EXT_nconf(CONF *conf, X509V3_CTX *ctx, const char *name, + const char *value); +int X509V3_EXT_add_nconf_sk(CONF *conf, X509V3_CTX *ctx, const char *section, + STACK_OF(X509_EXTENSION) **sk); +int X509V3_EXT_add_nconf(CONF *conf, X509V3_CTX *ctx, const char *section, + X509 *cert); +int X509V3_EXT_REQ_add_nconf(CONF *conf, X509V3_CTX *ctx, const char *section, + X509_REQ *req); +int X509V3_EXT_CRL_add_nconf(CONF *conf, X509V3_CTX *ctx, const char *section, + X509_CRL *crl); + +X509_EXTENSION *X509V3_EXT_conf_nid(LHASH_OF(CONF_VALUE) *conf, + X509V3_CTX *ctx, int ext_nid, + const char *value); +X509_EXTENSION *X509V3_EXT_conf(LHASH_OF(CONF_VALUE) *conf, X509V3_CTX *ctx, + const char *name, const char *value); +int X509V3_EXT_add_conf(LHASH_OF(CONF_VALUE) *conf, X509V3_CTX *ctx, + const char *section, X509 *cert); +int X509V3_EXT_REQ_add_conf(LHASH_OF(CONF_VALUE) *conf, X509V3_CTX *ctx, + const char *section, X509_REQ *req); +int X509V3_EXT_CRL_add_conf(LHASH_OF(CONF_VALUE) *conf, X509V3_CTX *ctx, + const char *section, X509_CRL *crl); + +int X509V3_add_value_bool_nf(const char *name, int asn1_bool, + STACK_OF(CONF_VALUE) **extlist); +int X509V3_get_value_bool(const CONF_VALUE *value, int *asn1_bool); +int X509V3_get_value_int(const CONF_VALUE *value, ASN1_INTEGER **aint); +void X509V3_set_nconf(X509V3_CTX *ctx, CONF *conf); +void X509V3_set_conf_lhash(X509V3_CTX *ctx, LHASH_OF(CONF_VALUE) *lhash); +# endif + +char *X509V3_get_string(X509V3_CTX *ctx, const char *name, const char *section); +STACK_OF(CONF_VALUE) *X509V3_get_section(X509V3_CTX *ctx, const char *section); +void X509V3_string_free(X509V3_CTX *ctx, char *str); +void X509V3_section_free(X509V3_CTX *ctx, STACK_OF(CONF_VALUE) *section); +void X509V3_set_ctx(X509V3_CTX *ctx, X509 *issuer, X509 *subject, + X509_REQ *req, X509_CRL *crl, int flags); + +int X509V3_add_value(const char *name, const char *value, + STACK_OF(CONF_VALUE) **extlist); +int X509V3_add_value_uchar(const char *name, const unsigned char *value, + STACK_OF(CONF_VALUE) **extlist); +int X509V3_add_value_bool(const char *name, int asn1_bool, + STACK_OF(CONF_VALUE) **extlist); +int X509V3_add_value_int(const char *name, const ASN1_INTEGER *aint, + STACK_OF(CONF_VALUE) **extlist); +char *i2s_ASN1_INTEGER(X509V3_EXT_METHOD *meth, const ASN1_INTEGER *aint); +ASN1_INTEGER *s2i_ASN1_INTEGER(X509V3_EXT_METHOD *meth, const char *value); +char *i2s_ASN1_ENUMERATED(X509V3_EXT_METHOD *meth, const ASN1_ENUMERATED *aint); +char *i2s_ASN1_ENUMERATED_TABLE(X509V3_EXT_METHOD *meth, + const ASN1_ENUMERATED *aint); +int X509V3_EXT_add(X509V3_EXT_METHOD *ext); +int X509V3_EXT_add_list(X509V3_EXT_METHOD *extlist); +int X509V3_EXT_add_alias(int nid_to, int nid_from); +void X509V3_EXT_cleanup(void); + +const X509V3_EXT_METHOD *X509V3_EXT_get(X509_EXTENSION *ext); +const X509V3_EXT_METHOD *X509V3_EXT_get_nid(int nid); +int X509V3_add_standard_extensions(void); +STACK_OF(CONF_VALUE) *X509V3_parse_list(const char *line); +void *X509V3_EXT_d2i(X509_EXTENSION *ext); +void *X509V3_get_d2i(const STACK_OF(X509_EXTENSION) *x, int nid, int *crit, + int *idx); + +X509_EXTENSION *X509V3_EXT_i2d(int ext_nid, int crit, void *ext_struc); +int X509V3_add1_i2d(STACK_OF(X509_EXTENSION) **x, int nid, void *value, + int crit, unsigned long flags); + +#if OPENSSL_API_COMPAT < 0x10100000L +/* The new declarations are in crypto.h, but the old ones were here. */ +# define hex_to_string OPENSSL_buf2hexstr +# define string_to_hex OPENSSL_hexstr2buf +#endif + +void X509V3_EXT_val_prn(BIO *out, STACK_OF(CONF_VALUE) *val, int indent, + int ml); +int X509V3_EXT_print(BIO *out, X509_EXTENSION *ext, unsigned long flag, + int indent); +#ifndef OPENSSL_NO_STDIO +int X509V3_EXT_print_fp(FILE *out, X509_EXTENSION *ext, int flag, int indent); +#endif +int X509V3_extensions_print(BIO *out, const char *title, + const STACK_OF(X509_EXTENSION) *exts, + unsigned long flag, int indent); + +int X509_check_ca(X509 *x); +int X509_check_purpose(X509 *x, int id, int ca); +int X509_supported_extension(X509_EXTENSION *ex); +int X509_PURPOSE_set(int *p, int purpose); +int X509_check_issued(X509 *issuer, X509 *subject); +int X509_check_akid(X509 *issuer, AUTHORITY_KEYID *akid); +void X509_set_proxy_flag(X509 *x); +void X509_set_proxy_pathlen(X509 *x, long l); +long X509_get_proxy_pathlen(X509 *x); + +uint32_t X509_get_extension_flags(X509 *x); +uint32_t X509_get_key_usage(X509 *x); +uint32_t X509_get_extended_key_usage(X509 *x); +const ASN1_OCTET_STRING *X509_get0_subject_key_id(X509 *x); + +int X509_PURPOSE_get_count(void); +X509_PURPOSE *X509_PURPOSE_get0(int idx); +int X509_PURPOSE_get_by_sname(const char *sname); +int X509_PURPOSE_get_by_id(int id); +int X509_PURPOSE_add(int id, int trust, int flags, + int (*ck) (const X509_PURPOSE *, const X509 *, int), + const char *name, const char *sname, void *arg); +char *X509_PURPOSE_get0_name(const X509_PURPOSE *xp); +char *X509_PURPOSE_get0_sname(const X509_PURPOSE *xp); +int X509_PURPOSE_get_trust(const X509_PURPOSE *xp); +void X509_PURPOSE_cleanup(void); +int X509_PURPOSE_get_id(const X509_PURPOSE *); + +STACK_OF(OPENSSL_STRING) *X509_get1_email(X509 *x); +STACK_OF(OPENSSL_STRING) *X509_REQ_get1_email(X509_REQ *x); +void X509_email_free(STACK_OF(OPENSSL_STRING) *sk); +STACK_OF(OPENSSL_STRING) *X509_get1_ocsp(X509 *x); +/* Flags for X509_check_* functions */ + +/* + * Always check subject name for host match even if subject alt names present + */ +# define X509_CHECK_FLAG_ALWAYS_CHECK_SUBJECT 0x1 +/* Disable wildcard matching for dnsName fields and common name. */ +# define X509_CHECK_FLAG_NO_WILDCARDS 0x2 +/* Wildcards must not match a partial label. */ +# define X509_CHECK_FLAG_NO_PARTIAL_WILDCARDS 0x4 +/* Allow (non-partial) wildcards to match multiple labels. */ +# define X509_CHECK_FLAG_MULTI_LABEL_WILDCARDS 0x8 +/* Constraint verifier subdomain patterns to match a single labels. */ +# define X509_CHECK_FLAG_SINGLE_LABEL_SUBDOMAINS 0x10 +/* Never check the subject CN */ +# define X509_CHECK_FLAG_NEVER_CHECK_SUBJECT 0x20 +/* + * Match reference identifiers starting with "." to any sub-domain. + * This is a non-public flag, turned on implicitly when the subject + * reference identity is a DNS name. + */ +# define _X509_CHECK_FLAG_DOT_SUBDOMAINS 0x8000 + +int X509_check_host(X509 *x, const char *chk, size_t chklen, + unsigned int flags, char **peername); +int X509_check_email(X509 *x, const char *chk, size_t chklen, + unsigned int flags); +int X509_check_ip(X509 *x, const unsigned char *chk, size_t chklen, + unsigned int flags); +int X509_check_ip_asc(X509 *x, const char *ipasc, unsigned int flags); + +ASN1_OCTET_STRING *a2i_IPADDRESS(const char *ipasc); +ASN1_OCTET_STRING *a2i_IPADDRESS_NC(const char *ipasc); +int X509V3_NAME_from_section(X509_NAME *nm, STACK_OF(CONF_VALUE) *dn_sk, + unsigned long chtype); + +void X509_POLICY_NODE_print(BIO *out, X509_POLICY_NODE *node, int indent); +DEFINE_STACK_OF(X509_POLICY_NODE) + +#ifndef OPENSSL_NO_RFC3779 +typedef struct ASRange_st { + ASN1_INTEGER *min, *max; +} ASRange; + +# define ASIdOrRange_id 0 +# define ASIdOrRange_range 1 + +typedef struct ASIdOrRange_st { + int type; + union { + ASN1_INTEGER *id; + ASRange *range; + } u; +} ASIdOrRange; + +typedef STACK_OF(ASIdOrRange) ASIdOrRanges; +DEFINE_STACK_OF(ASIdOrRange) + +# define ASIdentifierChoice_inherit 0 +# define ASIdentifierChoice_asIdsOrRanges 1 + +typedef struct ASIdentifierChoice_st { + int type; + union { + ASN1_NULL *inherit; + ASIdOrRanges *asIdsOrRanges; + } u; +} ASIdentifierChoice; + +typedef struct ASIdentifiers_st { + ASIdentifierChoice *asnum, *rdi; +} ASIdentifiers; + +DECLARE_ASN1_FUNCTIONS(ASRange) +DECLARE_ASN1_FUNCTIONS(ASIdOrRange) +DECLARE_ASN1_FUNCTIONS(ASIdentifierChoice) +DECLARE_ASN1_FUNCTIONS(ASIdentifiers) + +typedef struct IPAddressRange_st { + ASN1_BIT_STRING *min, *max; +} IPAddressRange; + +# define IPAddressOrRange_addressPrefix 0 +# define IPAddressOrRange_addressRange 1 + +typedef struct IPAddressOrRange_st { + int type; + union { + ASN1_BIT_STRING *addressPrefix; + IPAddressRange *addressRange; + } u; +} IPAddressOrRange; + +typedef STACK_OF(IPAddressOrRange) IPAddressOrRanges; +DEFINE_STACK_OF(IPAddressOrRange) + +# define IPAddressChoice_inherit 0 +# define IPAddressChoice_addressesOrRanges 1 + +typedef struct IPAddressChoice_st { + int type; + union { + ASN1_NULL *inherit; + IPAddressOrRanges *addressesOrRanges; + } u; +} IPAddressChoice; + +typedef struct IPAddressFamily_st { + ASN1_OCTET_STRING *addressFamily; + IPAddressChoice *ipAddressChoice; +} IPAddressFamily; + +typedef STACK_OF(IPAddressFamily) IPAddrBlocks; +DEFINE_STACK_OF(IPAddressFamily) + +DECLARE_ASN1_FUNCTIONS(IPAddressRange) +DECLARE_ASN1_FUNCTIONS(IPAddressOrRange) +DECLARE_ASN1_FUNCTIONS(IPAddressChoice) +DECLARE_ASN1_FUNCTIONS(IPAddressFamily) + +/* + * API tag for elements of the ASIdentifer SEQUENCE. + */ +# define V3_ASID_ASNUM 0 +# define V3_ASID_RDI 1 + +/* + * AFI values, assigned by IANA. It'd be nice to make the AFI + * handling code totally generic, but there are too many little things + * that would need to be defined for other address families for it to + * be worth the trouble. + */ +# define IANA_AFI_IPV4 1 +# define IANA_AFI_IPV6 2 + +/* + * Utilities to construct and extract values from RFC3779 extensions, + * since some of the encodings (particularly for IP address prefixes + * and ranges) are a bit tedious to work with directly. + */ +int X509v3_asid_add_inherit(ASIdentifiers *asid, int which); +int X509v3_asid_add_id_or_range(ASIdentifiers *asid, int which, + ASN1_INTEGER *min, ASN1_INTEGER *max); +int X509v3_addr_add_inherit(IPAddrBlocks *addr, + const unsigned afi, const unsigned *safi); +int X509v3_addr_add_prefix(IPAddrBlocks *addr, + const unsigned afi, const unsigned *safi, + unsigned char *a, const int prefixlen); +int X509v3_addr_add_range(IPAddrBlocks *addr, + const unsigned afi, const unsigned *safi, + unsigned char *min, unsigned char *max); +unsigned X509v3_addr_get_afi(const IPAddressFamily *f); +int X509v3_addr_get_range(IPAddressOrRange *aor, const unsigned afi, + unsigned char *min, unsigned char *max, + const int length); + +/* + * Canonical forms. + */ +int X509v3_asid_is_canonical(ASIdentifiers *asid); +int X509v3_addr_is_canonical(IPAddrBlocks *addr); +int X509v3_asid_canonize(ASIdentifiers *asid); +int X509v3_addr_canonize(IPAddrBlocks *addr); + +/* + * Tests for inheritance and containment. + */ +int X509v3_asid_inherits(ASIdentifiers *asid); +int X509v3_addr_inherits(IPAddrBlocks *addr); +int X509v3_asid_subset(ASIdentifiers *a, ASIdentifiers *b); +int X509v3_addr_subset(IPAddrBlocks *a, IPAddrBlocks *b); + +/* + * Check whether RFC 3779 extensions nest properly in chains. + */ +int X509v3_asid_validate_path(X509_STORE_CTX *); +int X509v3_addr_validate_path(X509_STORE_CTX *); +int X509v3_asid_validate_resource_set(STACK_OF(X509) *chain, + ASIdentifiers *ext, + int allow_inheritance); +int X509v3_addr_validate_resource_set(STACK_OF(X509) *chain, + IPAddrBlocks *ext, int allow_inheritance); + +#endif /* OPENSSL_NO_RFC3779 */ + +/* BEGIN ERROR CODES */ +/* + * The following lines are auto generated by the script mkerr.pl. Any changes + * made after this point may be overwritten when the script is next run. + */ + +int ERR_load_X509V3_strings(void); + +/* Error codes for the X509V3 functions. */ + +/* Function codes. */ +# define X509V3_F_A2I_GENERAL_NAME 164 +# define X509V3_F_ADDR_VALIDATE_PATH_INTERNAL 166 +# define X509V3_F_ASIDENTIFIERCHOICE_CANONIZE 161 +# define X509V3_F_ASIDENTIFIERCHOICE_IS_CANONICAL 162 +# define X509V3_F_COPY_EMAIL 122 +# define X509V3_F_COPY_ISSUER 123 +# define X509V3_F_DO_DIRNAME 144 +# define X509V3_F_DO_EXT_I2D 135 +# define X509V3_F_DO_EXT_NCONF 151 +# define X509V3_F_GNAMES_FROM_SECTNAME 156 +# define X509V3_F_I2S_ASN1_ENUMERATED 121 +# define X509V3_F_I2S_ASN1_IA5STRING 149 +# define X509V3_F_I2S_ASN1_INTEGER 120 +# define X509V3_F_I2V_AUTHORITY_INFO_ACCESS 138 +# define X509V3_F_NOTICE_SECTION 132 +# define X509V3_F_NREF_NOS 133 +# define X509V3_F_POLICY_SECTION 131 +# define X509V3_F_PROCESS_PCI_VALUE 150 +# define X509V3_F_R2I_CERTPOL 130 +# define X509V3_F_R2I_PCI 155 +# define X509V3_F_S2I_ASN1_IA5STRING 100 +# define X509V3_F_S2I_ASN1_INTEGER 108 +# define X509V3_F_S2I_ASN1_OCTET_STRING 112 +# define X509V3_F_S2I_SKEY_ID 115 +# define X509V3_F_SET_DIST_POINT_NAME 158 +# define X509V3_F_SXNET_ADD_ID_ASC 125 +# define X509V3_F_SXNET_ADD_ID_INTEGER 126 +# define X509V3_F_SXNET_ADD_ID_ULONG 127 +# define X509V3_F_SXNET_GET_ID_ASC 128 +# define X509V3_F_SXNET_GET_ID_ULONG 129 +# define X509V3_F_V2I_ASIDENTIFIERS 163 +# define X509V3_F_V2I_ASN1_BIT_STRING 101 +# define X509V3_F_V2I_AUTHORITY_INFO_ACCESS 139 +# define X509V3_F_V2I_AUTHORITY_KEYID 119 +# define X509V3_F_V2I_BASIC_CONSTRAINTS 102 +# define X509V3_F_V2I_CRLD 134 +# define X509V3_F_V2I_EXTENDED_KEY_USAGE 103 +# define X509V3_F_V2I_GENERAL_NAMES 118 +# define X509V3_F_V2I_GENERAL_NAME_EX 117 +# define X509V3_F_V2I_IDP 157 +# define X509V3_F_V2I_IPADDRBLOCKS 159 +# define X509V3_F_V2I_ISSUER_ALT 153 +# define X509V3_F_V2I_NAME_CONSTRAINTS 147 +# define X509V3_F_V2I_POLICY_CONSTRAINTS 146 +# define X509V3_F_V2I_POLICY_MAPPINGS 145 +# define X509V3_F_V2I_SUBJECT_ALT 154 +# define X509V3_F_V2I_TLS_FEATURE 165 +# define X509V3_F_V3_GENERIC_EXTENSION 116 +# define X509V3_F_X509V3_ADD1_I2D 140 +# define X509V3_F_X509V3_ADD_VALUE 105 +# define X509V3_F_X509V3_EXT_ADD 104 +# define X509V3_F_X509V3_EXT_ADD_ALIAS 106 +# define X509V3_F_X509V3_EXT_I2D 136 +# define X509V3_F_X509V3_EXT_NCONF 152 +# define X509V3_F_X509V3_GET_SECTION 142 +# define X509V3_F_X509V3_GET_STRING 143 +# define X509V3_F_X509V3_GET_VALUE_BOOL 110 +# define X509V3_F_X509V3_PARSE_LIST 109 +# define X509V3_F_X509_PURPOSE_ADD 137 +# define X509V3_F_X509_PURPOSE_SET 141 + +/* Reason codes. */ +# define X509V3_R_BAD_IP_ADDRESS 118 +# define X509V3_R_BAD_OBJECT 119 +# define X509V3_R_BN_DEC2BN_ERROR 100 +# define X509V3_R_BN_TO_ASN1_INTEGER_ERROR 101 +# define X509V3_R_DIRNAME_ERROR 149 +# define X509V3_R_DISTPOINT_ALREADY_SET 160 +# define X509V3_R_DUPLICATE_ZONE_ID 133 +# define X509V3_R_ERROR_CONVERTING_ZONE 131 +# define X509V3_R_ERROR_CREATING_EXTENSION 144 +# define X509V3_R_ERROR_IN_EXTENSION 128 +# define X509V3_R_EXPECTED_A_SECTION_NAME 137 +# define X509V3_R_EXTENSION_EXISTS 145 +# define X509V3_R_EXTENSION_NAME_ERROR 115 +# define X509V3_R_EXTENSION_NOT_FOUND 102 +# define X509V3_R_EXTENSION_SETTING_NOT_SUPPORTED 103 +# define X509V3_R_EXTENSION_VALUE_ERROR 116 +# define X509V3_R_ILLEGAL_EMPTY_EXTENSION 151 +# define X509V3_R_INCORRECT_POLICY_SYNTAX_TAG 152 +# define X509V3_R_INVALID_ASNUMBER 162 +# define X509V3_R_INVALID_ASRANGE 163 +# define X509V3_R_INVALID_BOOLEAN_STRING 104 +# define X509V3_R_INVALID_EXTENSION_STRING 105 +# define X509V3_R_INVALID_INHERITANCE 165 +# define X509V3_R_INVALID_IPADDRESS 166 +# define X509V3_R_INVALID_MULTIPLE_RDNS 161 +# define X509V3_R_INVALID_NAME 106 +# define X509V3_R_INVALID_NULL_ARGUMENT 107 +# define X509V3_R_INVALID_NULL_NAME 108 +# define X509V3_R_INVALID_NULL_VALUE 109 +# define X509V3_R_INVALID_NUMBER 140 +# define X509V3_R_INVALID_NUMBERS 141 +# define X509V3_R_INVALID_OBJECT_IDENTIFIER 110 +# define X509V3_R_INVALID_OPTION 138 +# define X509V3_R_INVALID_POLICY_IDENTIFIER 134 +# define X509V3_R_INVALID_PROXY_POLICY_SETTING 153 +# define X509V3_R_INVALID_PURPOSE 146 +# define X509V3_R_INVALID_SAFI 164 +# define X509V3_R_INVALID_SECTION 135 +# define X509V3_R_INVALID_SYNTAX 143 +# define X509V3_R_ISSUER_DECODE_ERROR 126 +# define X509V3_R_MISSING_VALUE 124 +# define X509V3_R_NEED_ORGANIZATION_AND_NUMBERS 142 +# define X509V3_R_NO_CONFIG_DATABASE 136 +# define X509V3_R_NO_ISSUER_CERTIFICATE 121 +# define X509V3_R_NO_ISSUER_DETAILS 127 +# define X509V3_R_NO_POLICY_IDENTIFIER 139 +# define X509V3_R_NO_PROXY_CERT_POLICY_LANGUAGE_DEFINED 154 +# define X509V3_R_NO_PUBLIC_KEY 114 +# define X509V3_R_NO_SUBJECT_DETAILS 125 +# define X509V3_R_OPERATION_NOT_DEFINED 148 +# define X509V3_R_OTHERNAME_ERROR 147 +# define X509V3_R_POLICY_LANGUAGE_ALREADY_DEFINED 155 +# define X509V3_R_POLICY_PATH_LENGTH 156 +# define X509V3_R_POLICY_PATH_LENGTH_ALREADY_DEFINED 157 +# define X509V3_R_POLICY_WHEN_PROXY_LANGUAGE_REQUIRES_NO_POLICY 159 +# define X509V3_R_SECTION_NOT_FOUND 150 +# define X509V3_R_UNABLE_TO_GET_ISSUER_DETAILS 122 +# define X509V3_R_UNABLE_TO_GET_ISSUER_KEYID 123 +# define X509V3_R_UNKNOWN_BIT_STRING_ARGUMENT 111 +# define X509V3_R_UNKNOWN_EXTENSION 129 +# define X509V3_R_UNKNOWN_EXTENSION_NAME 130 +# define X509V3_R_UNKNOWN_OPTION 120 +# define X509V3_R_UNSUPPORTED_OPTION 117 +# define X509V3_R_UNSUPPORTED_TYPE 167 +# define X509V3_R_USER_TOO_LONG 132 + +# ifdef __cplusplus +} +# endif +#endif diff --git a/mobile/ios/FxA/FxA/lib/libcrypto.a b/mobile/ios/FxA/FxA/lib/libcrypto.a new file mode 100644 index 0000000000..7da435ef1b Binary files /dev/null and b/mobile/ios/FxA/FxA/lib/libcrypto.a differ diff --git a/mobile/ios/FxA/FxA/lib/libssl.a b/mobile/ios/FxA/FxA/lib/libssl.a new file mode 100644 index 0000000000..d537897db4 Binary files /dev/null and b/mobile/ios/FxA/FxA/lib/libssl.a differ diff --git a/mobile/ios/FxA/FxATests/FxATests.m b/mobile/ios/FxA/FxATests/FxATests.m new file mode 100644 index 0000000000..47bccdb461 --- /dev/null +++ b/mobile/ios/FxA/FxATests/FxATests.m @@ -0,0 +1,37 @@ +// +// FxATests.m +// FxATests +// +// Created by Richard Newman on 2014-09-22. +// Copyright (c) 2014 Mozilla. All rights reserved. +// + +#import +#import + +#import + +@interface FxATests : XCTestCase + +@end + +@implementation FxATests + +- (void)setUp { + [super setUp]; + // Put setup code here. This method is called before the invocation of each test method in the class. +} + +- (void)tearDown { + // Put teardown code here. This method is called after the invocation of each test method in the class. + [super tearDown]; +} + +- (void)testPerformanceExample { + // This is an example of a performance test case. + [self measureBlock:^{ + // Put the code you want to measure the time of here. + }]; +} + +@end diff --git a/mobile/ios/FxA/FxATests/Info.plist b/mobile/ios/FxA/FxATests/Info.plist new file mode 100644 index 0000000000..ba72822e87 --- /dev/null +++ b/mobile/ios/FxA/FxATests/Info.plist @@ -0,0 +1,24 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + BNDL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1 + + diff --git a/mobile/ios/Gemfile b/mobile/ios/Gemfile new file mode 100644 index 0000000000..eab52f7235 --- /dev/null +++ b/mobile/ios/Gemfile @@ -0,0 +1,4 @@ +source 'https://rubygems.org' + +gem 'danger', :git => 'git@github.com:danger/danger.git', :branch => 'master' +gem 'danger-swiftlint' \ No newline at end of file diff --git a/mobile/ios/ISSUE_TEMPLATE.md b/mobile/ios/ISSUE_TEMPLATE.md new file mode 100644 index 0000000000..1b5c6a1c5c --- /dev/null +++ b/mobile/ios/ISSUE_TEMPLATE.md @@ -0,0 +1,7 @@ +Hey there! Thanks for using Firefox! + +We prefer to have bugs in Bugzilla. That way issues can be tracked and the rest of Mozilla can help us fix and verify issues as well. + +We would love if you have found a bug to report it in Bugzilla. [File a new bug](https://bugzilla.mozilla.org/enter_bug.cgi?bug_file_loc=http%3A%2F%2F&bug_ignored=0&op_sys=iOS%20&product=Firefox%20for%20iOS&rep_platform=All) + +If you are having an issue building or running the project message us on IRC. There are lots of people who are around to help. We are on [#mobile](https://wiki.mozilla.org/IRC) \ No newline at end of file diff --git a/mobile/ios/L10nSnapshotTests-Info.plist b/mobile/ios/L10nSnapshotTests-Info.plist new file mode 100644 index 0000000000..ba72822e87 --- /dev/null +++ b/mobile/ios/L10nSnapshotTests-Info.plist @@ -0,0 +1,24 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + BNDL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1 + + diff --git a/mobile/ios/L10nSnapshotTests/Info.plist b/mobile/ios/L10nSnapshotTests/Info.plist new file mode 100644 index 0000000000..9411e4a06e --- /dev/null +++ b/mobile/ios/L10nSnapshotTests/Info.plist @@ -0,0 +1,24 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + BNDL + CFBundleShortVersionString + 10.6 + CFBundleSignature + ???? + CFBundleVersion + 1 + + diff --git a/mobile/ios/L10nSnapshotTests/L10nBaseSnapshotTests.swift b/mobile/ios/L10nSnapshotTests/L10nBaseSnapshotTests.swift new file mode 100644 index 0000000000..0bf22ce1ef --- /dev/null +++ b/mobile/ios/L10nSnapshotTests/L10nBaseSnapshotTests.swift @@ -0,0 +1,74 @@ +/* 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 XCTest + +class L10nBaseSnapshotTests: XCTestCase { + var skipIntro: Bool { + return true + } + + override func setUp() { + super.setUp() + continueAfterFailure = false + let app = XCUIApplication() + setupSnapshot(app) + app.launchArguments = [LaunchArguments.Test, LaunchArguments.ClearProfile] + if skipIntro { + app.launchArguments.append(LaunchArguments.SkipIntro) + } + app.launch() + } + + func waitforExistence(_ element: XCUIElement) { + let exists = NSPredicate(format: "exists == true") + + expectation(for: exists, evaluatedWith: element, handler: nil) + waitForExpectations(timeout: 20, handler: nil) + } + + func waitforNoExistence(_ element: XCUIElement) { + let exists = NSPredicate(format: "exists != true") + + expectation(for: exists, evaluatedWith: element, handler: nil) + waitForExpectations(timeout: 20, handler: nil) + } + + func loadWebPage(url: String, waitForOtherElementWithAriaLabel ariaLabel: String) { + let app = XCUIApplication() + UIPasteboard.general.string = url + app.textFields["url"].press(forDuration: 2.0) + app.sheets.element(boundBy: 0).buttons.element(boundBy: 0).tap() + sleep(3) // TODO Otherwise we detect the body in the currently loaded document, before the new page has loaded + + let webView = app.webViews.element(boundBy: 0) + let element = webView.otherElements[ariaLabel] + expectation(for: NSPredicate(format: "exists == 1"), evaluatedWith: element, handler: nil) + + waitForExpectations(timeout: 5.0) { (error) -> Void in + if error != nil { + XCTFail("Failed to detect element with ariaLabel=\(ariaLabel) on \(url): \(error)") + } + } + } + + func loadWebPage(url: String, waitForLoadToFinish: Bool = true) { + let LoadingTimeout: TimeInterval = 60 + let exists = NSPredicate(format: "exists = true") + let loaded = NSPredicate(format: "value BEGINSWITH '100'") + + let app = XCUIApplication() + + UIPasteboard.general.string = url + app.textFields["url"].press(forDuration: 2.0) + app.sheets.element(boundBy: 0).buttons.element(boundBy: 0).tap() + + if waitForLoadToFinish { + let progressIndicator = app.progressIndicators.element(boundBy: 0) + expectation(for: exists, evaluatedWith: progressIndicator, handler: nil) + expectation(for: loaded, evaluatedWith: progressIndicator, handler: nil) + waitForExpectations(timeout: LoadingTimeout, handler: nil) + } + } +} diff --git a/mobile/ios/L10nSnapshotTests/L10nIntroSnapshotTests.swift b/mobile/ios/L10nSnapshotTests/L10nIntroSnapshotTests.swift new file mode 100644 index 0000000000..5d1be9a3da --- /dev/null +++ b/mobile/ios/L10nSnapshotTests/L10nIntroSnapshotTests.swift @@ -0,0 +1,28 @@ +/* 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 XCTest + +class L10nIntroSnapshotTests: L10nBaseSnapshotTests { + override var skipIntro: Bool { + return false + } + + func testIntro() { + let app = XCUIApplication() + snapshot("Intro-1") + app.scrollViews["IntroViewController.scrollView"].swipeLeft() + sleep(2) + snapshot("Intro-2") + app.scrollViews["IntroViewController.scrollView"].swipeLeft() + sleep(2) + snapshot("Intro-3") + app.scrollViews["IntroViewController.scrollView"].swipeLeft() + sleep(2) + snapshot("Intro-4") + app.scrollViews["IntroViewController.scrollView"].swipeLeft() + sleep(2) + snapshot("01Intro-5") + } +} diff --git a/mobile/ios/L10nSnapshotTests/L10nPermissionStringsSnapshotTests.swift b/mobile/ios/L10nSnapshotTests/L10nPermissionStringsSnapshotTests.swift new file mode 100644 index 0000000000..d079e637aa --- /dev/null +++ b/mobile/ios/L10nSnapshotTests/L10nPermissionStringsSnapshotTests.swift @@ -0,0 +1,13 @@ +/* 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 XCTest + +class L10nPermissionStringsSnapshotTests: L10nBaseSnapshotTests { + func testNSLocationWhenInUseUsageDescription() { + loadWebPage(url: "http://wopr.norad.org/~sarentz/fxios/testpages/geolocation.html", waitForOtherElementWithAriaLabel: "body") + snapshot("15LocationDialog-01") + loadWebPage(url: "http://wopr.norad.org/~sarentz/fxios/testpages/index.html", waitForOtherElementWithAriaLabel: "body") + } +} diff --git a/mobile/ios/L10nSnapshotTests/L10nSnapshotTests.swift b/mobile/ios/L10nSnapshotTests/L10nSnapshotTests.swift new file mode 100644 index 0000000000..471e203d61 --- /dev/null +++ b/mobile/ios/L10nSnapshotTests/L10nSnapshotTests.swift @@ -0,0 +1,268 @@ +/* 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 XCTest + +class L10nSnapshotTests: L10nBaseSnapshotTests { + func test02DefaultTopSites() { + snapshot("02DefaultTopSites-01") + } + + func test03MenuOnTopSites() { + let app = XCUIApplication() + app.buttons["TabToolbar.menuButton"].tap() + snapshot("03MenuOnTopSites-01") + app.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.25)).tap() + + loadWebPage(url: "about:blank", waitForLoadToFinish: false) + sleep(2) + app.buttons["TabToolbar.menuButton"].tap() + snapshot("10MenuOnWebPage-01") + app.otherElements["MenuViewController.menuView"].swipeLeft() + snapshot("10MenuOnWebPage-02") + app.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.25)).tap() + + app.buttons["URLBarView.tabsButton"].tap() + app.buttons["TabTrayController.menuButton"].tap() + snapshot("10MenuOnTabsTray-02") + } + + func test04Foo() { + let app = XCUIApplication() + loadWebPage(url: "http://wopr.norad.org/~sarentz/fxios/testpages/index.html", waitForOtherElementWithAriaLabel: "body") + app.buttons["TabToolbar.menuButton"].tap() + snapshot("10MenuOnWebPage-01") + app.otherElements["MenuViewController.menuView"].swipeLeft() + snapshot("10MenuOnWebPage-02") + } + + func test04Settings() { + let app = XCUIApplication() + app.buttons["TabToolbar.menuButton"].tap() + app.otherElements["MenuViewController.menuView"].swipeLeft() + app.cells["SettingsMenuItem"].tap() + snapshot("04SettingsTopLevel") + + // TODO Scroll through the settings and make a screenshot of every page + + // Screenshot all the settings that have a separate page + for cellName in ["Search", "NewTab", "Homepage", "Logins"] { + app.tables["AppSettingsTableViewController.tableView"].cells[cellName].tap() + snapshot("04Settings-\(cellName)") + app.navigationBars.element(boundBy: 0).buttons.element(boundBy: 0).tap() + } + + // Unclear why this is needed - without it the test framework finds two TouchIDPasscode cells + app.tables["AppSettingsTableViewController.tableView"].swipeUp() + + for cellName in ["TouchIDPasscode", "ClearPrivateData", "TrackingProtection"] { + app.tables["AppSettingsTableViewController.tableView"].cells[cellName].tap() + snapshot("04Settings-\(cellName)") + app.navigationBars.element(boundBy: 0).buttons.element(boundBy: 0).tap() + } + } + + func test05PrivateBrowsingTabsEmptyState() { + let app = XCUIApplication() + app.buttons["URLBarView.tabsButton"].tap() // Open tabs tray + app.buttons["TabTrayController.maskButton"].tap() // Switch to private mode + snapshot("05PrivateBrowsingTabsEmptyState-01") + } + + func test06PanelsEmptyState() { + let app = XCUIApplication() + app.textFields["url"].tap() + app.buttons["HomePanels.Bookmarks"].tap() + snapshot("06PanelsEmptyState-01") + app.buttons["HomePanels.History"].tap() + snapshot("06PanelsEmptyState-02") + app.cells["HistoryPanel.syncedDevicesCell"].tap() + snapshot("06PanelsEmptyState-03") + app.buttons["HomePanels.ReadingList"].tap() + snapshot("06PanelsEmptyState-04") + } + + // From here on it is fine to load pages + + func test07AddSearchProvider() { + let app = XCUIApplication() + loadWebPage(url: "http://wopr.norad.org/~sarentz/fxios/testpages/addSearchProvider.html", waitForOtherElementWithAriaLabel: "body") + app.webViews.element(boundBy: 0).buttons["focus"].tap() + snapshot("07AddSearchProvider-01", waitForLoadingIndicator: false) + app.buttons["BrowserViewController.customSearchEngineButton"].tap() + snapshot("07AddSearchProvider-02", waitForLoadingIndicator: false) + + let alert = XCUIApplication().alerts.element(boundBy: 0) + expectation(for: NSPredicate(format: "exists == 1"), evaluatedWith: alert, handler: nil) + waitForExpectations(timeout: 3, handler: nil) + alert.buttons.element(boundBy: 0).tap() + } + + func test08URLBar() { + let app = XCUIApplication() + app.textFields["url"].tap() + snapshot("08URLBar-01") + app.textFields["address"].typeText("moz") + snapshot("08URLBar-02") + } + + func test09URLBarContextMenu() { + let app = XCUIApplication() + // Long press with nothing on the clipboard + app.textFields["url"].press(forDuration: 2.0) + snapshot("09LocationBarContextMenu-01") + app.sheets.element(boundBy: 0).buttons.element(boundBy: 0).tap() + sleep(2) + + // Long press with a URL on the clipboard + UIPasteboard.general.string = "https://www.mozilla.com" + app.textFields["url"].press(forDuration: 2.0) + snapshot("09LocationBarContextMenu-02") + app.sheets.element(boundBy: 0).buttons.element(boundBy: 0).tap() + sleep(2) + } + + func test10MenuOnWebPage() { + let app = XCUIApplication() + loadWebPage(url: "http://wopr.norad.org/~sarentz/fxios/testpages/index.html", waitForOtherElementWithAriaLabel: "body") + app.buttons["TabToolbar.menuButton"].tap() + snapshot("10MenuOnWebPage-01") + app.otherElements["MenuViewController.menuView"].swipeLeft() + snapshot("10MenuOnWebPage-02") + } + + func test11WebViewContextMenu() { + let app = XCUIApplication() + + // Link + loadWebPage(url: "http://wopr.norad.org/~sarentz/fxios/testpages/link.html", waitForOtherElementWithAriaLabel: "body") + app.webViews.element(boundBy: 0).links["link"].press(forDuration: 2.0) + snapshot("11WebViewContextMenu-01") + app.sheets.element(boundBy: 0).buttons.element(boundBy: app.sheets.element(boundBy: 0).buttons.count-1).tap() + + // Image + loadWebPage(url: "http://wopr.norad.org/~sarentz/fxios/testpages/image.html", waitForOtherElementWithAriaLabel: "body") + app.webViews.element(boundBy: 0).images["image"].press(forDuration: 2.0) + snapshot("11WebViewContextMenu-02") + app.sheets.element(boundBy: 0).buttons.element(boundBy: app.sheets.element(boundBy: 0).buttons.count-1).tap() + + // Image inside Link + loadWebPage(url: "http://wopr.norad.org/~sarentz/fxios/testpages/imageWithLink.html", waitForOtherElementWithAriaLabel: "body") + app.webViews.element(boundBy: 0).links["link"].press(forDuration: 2.0) + snapshot("11WebViewContextMenu-03") + app.sheets.element(boundBy: 0).buttons.element(boundBy: app.sheets.element(boundBy: 0).buttons.count-1).tap() + } + + func test12WebViewAuthenticationDialog() { + loadWebPage(url: "http://wopr.norad.org/~sarentz/fxios/testpages/basicauth/index.html", waitForLoadToFinish: false) + let predicate = NSPredicate(format: "exists == 1") + let query = XCUIApplication().alerts.element(boundBy: 0) + expectation(for: predicate, evaluatedWith: query, handler: nil) + self.waitForExpectations(timeout: 3, handler: nil) + snapshot("12WebViewAuthenticationDialog-01", waitForLoadingIndicator: false) + } + + func test13ReloadButtonContextMenu() { + let app = XCUIApplication() + loadWebPage(url: "http://wopr.norad.org/~sarentz/fxios/testpages/index.html", waitForOtherElementWithAriaLabel: "body") + app.buttons["TabToolbar.stopReloadButton"].press(forDuration: 2.0) + snapshot("13ContextMenuReloadButton-01", waitForLoadingIndicator: false) + app.sheets.element(boundBy: 0).buttons.element(boundBy: 0).tap() + app.buttons["TabToolbar.stopReloadButton"].press(forDuration: 2.0) + snapshot("13ContextMenuReloadButton-02", waitForLoadingIndicator: false) + } + + func test14SetHompage() { + let app = XCUIApplication() + loadWebPage(url: "http://wopr.norad.org/~sarentz/fxios/testpages/index.html", waitForOtherElementWithAriaLabel: "body") + app.buttons["TabToolbar.menuButton"].tap() + app.cells["SetHomePageMenuItem"].tap() + snapshot("14SetHomepage-01", waitForLoadingIndicator: false) + } + + func test17PasswordSnackbar() { + let app = XCUIApplication() + loadWebPage(url: "http://wopr.norad.org/~sarentz/fxios/testpages/password.html", waitForOtherElementWithAriaLabel: "body") + app.webViews.element(boundBy: 0).buttons["submit"].tap() + snapshot("17PasswordSnackbar-01") + app.buttons["SaveLoginPrompt.saveLoginButton"].tap() + // The password is pre-filled with a random value so second this this will cause the update prompt + loadWebPage(url: "http://wopr.norad.org/~sarentz/fxios/testpages/password.html", waitForOtherElementWithAriaLabel: "body") + app.webViews.element(boundBy: 0).buttons["submit"].tap() + snapshot("17PasswordSnackbar-02") + } + + func test18TopSitesMenu() { + let app = XCUIApplication() + let topSites = app.cells["TopSitesCell"] + topSites.cells.matching(identifier: "TopSite").element(boundBy: 0).press(forDuration: 2.0) + snapshot("18TopSitesMenu-01") + } + + func test19HistoryTableContextMenu() { + let app = XCUIApplication() + loadWebPage(url: "http://wopr.norad.org/~sarentz/fxios/testpages/index.html", waitForOtherElementWithAriaLabel: "body") + app.buttons["TabToolbar.menuButton"].tap() + app.toolbars.buttons["HistoryMenuToolbarItem"].tap() + app.tables["History List"].cells.element(boundBy: 2).press(forDuration: 2.0) + snapshot("19HistoryTableContextMenu-01") + } + + func test20BookmarksTableContextMenu() { + let app = XCUIApplication() + loadWebPage(url: "http://wopr.norad.org/~sarentz/fxios/testpages/index.html", waitForOtherElementWithAriaLabel: "body") + app.buttons["TabToolbar.menuButton"].tap() + app.cells["AddBookmarkMenuItem"].tap() + waitforNoExistence(app.otherElements["MenuViewController.menuView"]) + app.textFields["url"].tap() + app.buttons["HomePanels.Bookmarks"].tap() + app.tables["Bookmarks List"].cells.element(boundBy: 0).press(forDuration: 2.0) + snapshot("20BookmarksTableContextMenu-01") + } + + func test21ReaderModeSettingsMenu() { + let app = XCUIApplication() + loadWebPage(url: "http://wopr.norad.org/~sarentz/fxios/testpages/index.html", waitForOtherElementWithAriaLabel: "body") + app.buttons["TabLocationView.readerModeButton"].tap() + app.buttons["ReaderModeBarView.settingsButton"].tap() + snapshot("21ReaderModeSettingsMenu-01") + } + + func test22ReadingListTableContextMenu() { + let app = XCUIApplication() + loadWebPage(url: "http://wopr.norad.org/~sarentz/fxios/testpages/index.html", waitForOtherElementWithAriaLabel: "body") + app.buttons["TabLocationView.readerModeButton"].tap() + app.buttons["ReaderModeBarView.listStatusButton"].tap() + app.textFields["url"].tap() + app.buttons["HomePanels.ReadingList"].tap() + app.tables["ReadingTable"].cells.element(boundBy: 0).press(forDuration: 2.0) + snapshot("22ReadingListTableContextMenu-01") + } + + func test23ReadingListTableRowMenu() { + let app = XCUIApplication() + loadWebPage(url: "http://wopr.norad.org/~sarentz/fxios/testpages/index.html", waitForOtherElementWithAriaLabel: "body") + app.buttons["TabLocationView.readerModeButton"].tap() + app.buttons["ReaderModeBarView.listStatusButton"].tap() + app.textFields["url"].tap() + app.buttons["HomePanels.ReadingList"].tap() + app.tables["ReadingTable"].cells.element(boundBy: 0).swipeLeft() + snapshot("23ReadingListTableRowMenu-01") + app.tables["ReadingTable"].cells.element(boundBy: 0).buttons.element(boundBy: 1).tap() + app.tables["ReadingTable"].cells.element(boundBy: 0).swipeLeft() + snapshot("23ReadingListTableRowMenu-02") + } + + func test24BookmarksListTableRowMenu() { + let app = XCUIApplication() + loadWebPage(url: "http://wopr.norad.org/~sarentz/fxios/testpages/index.html", waitForOtherElementWithAriaLabel: "body") + app.buttons["TabToolbar.menuButton"].tap() + app.cells["AddBookmarkMenuItem"].tap() + waitforNoExistence(app.otherElements["MenuViewController.menuView"]) + app.textFields["url"].tap() + app.buttons["HomePanels.Bookmarks"].tap() + app.tables["Bookmarks List"].cells.element(boundBy: 0).swipeLeft() + snapshot("24BookmarksListTableRowMenu-01") + } +} diff --git a/mobile/ios/LICENSE b/mobile/ios/LICENSE new file mode 100644 index 0000000000..e87a115e46 --- /dev/null +++ b/mobile/ios/LICENSE @@ -0,0 +1,363 @@ +Mozilla Public License, version 2.0 + +1. Definitions + +1.1. "Contributor" + + means each individual or legal entity that creates, contributes to the + creation of, or owns Covered Software. + +1.2. "Contributor Version" + + means the combination of the Contributions of others (if any) used by a + Contributor and that particular Contributor's Contribution. + +1.3. "Contribution" + + means Covered Software of a particular Contributor. + +1.4. "Covered Software" + + means Source Code Form to which the initial Contributor has attached the + notice in Exhibit A, the Executable Form of such Source Code Form, and + Modifications of such Source Code Form, in each case including portions + thereof. + +1.5. "Incompatible With Secondary Licenses" + means + + a. that the initial Contributor has attached the notice described in + Exhibit B to the Covered Software; or + + b. that the Covered Software was made available under the terms of + version 1.1 or earlier of the License, but not also under the terms of + a Secondary License. + +1.6. "Executable Form" + + means any form of the work other than Source Code Form. + +1.7. "Larger Work" + + means a work that combines Covered Software with other material, in a + separate file or files, that is not Covered Software. + +1.8. "License" + + means this document. + +1.9. "Licensable" + + means having the right to grant, to the maximum extent possible, whether + at the time of the initial grant or subsequently, any and all of the + rights conveyed by this License. + +1.10. "Modifications" + + means any of the following: + + a. any file in Source Code Form that results from an addition to, + deletion from, or modification of the contents of Covered Software; or + + b. any new file in Source Code Form that contains any Covered Software. + +1.11. "Patent Claims" of a Contributor + + means any patent claim(s), including without limitation, method, + process, and apparatus claims, in any patent Licensable by such + Contributor that would be infringed, but for the grant of the License, + by the making, using, selling, offering for sale, having made, import, + or transfer of either its Contributions or its Contributor Version. + +1.12. "Secondary License" + + means either the GNU General Public License, Version 2.0, the GNU Lesser + General Public License, Version 2.1, the GNU Affero General Public + License, Version 3.0, or any later versions of those licenses. + +1.13. "Source Code Form" + + means the form of the work preferred for making modifications. + +1.14. "You" (or "Your") + + means an individual or a legal entity exercising rights under this + License. For legal entities, "You" includes any entity that controls, is + controlled by, or is under common control with You. For purposes of this + definition, "control" means (a) the power, direct or indirect, to cause + the direction or management of such entity, whether by contract or + otherwise, or (b) ownership of more than fifty percent (50%) of the + outstanding shares or beneficial ownership of such entity. + + +2. License Grants and Conditions + +2.1. Grants + + Each Contributor hereby grants You a world-wide, royalty-free, + non-exclusive license: + + a. under intellectual property rights (other than patent or trademark) + Licensable by such Contributor to use, reproduce, make available, + modify, display, perform, distribute, and otherwise exploit its + Contributions, either on an unmodified basis, with Modifications, or + as part of a Larger Work; and + + b. under Patent Claims of such Contributor to make, use, sell, offer for + sale, have made, import, and otherwise transfer either its + Contributions or its Contributor Version. + +2.2. Effective Date + + The licenses granted in Section 2.1 with respect to any Contribution + become effective for each Contribution on the date the Contributor first + distributes such Contribution. + +2.3. Limitations on Grant Scope + + The licenses granted in this Section 2 are the only rights granted under + this License. No additional rights or licenses will be implied from the + distribution or licensing of Covered Software under this License. + Notwithstanding Section 2.1(b) above, no patent license is granted by a + Contributor: + + a. for any code that a Contributor has removed from Covered Software; or + + b. for infringements caused by: (i) Your and any other third party's + modifications of Covered Software, or (ii) the combination of its + Contributions with other software (except as part of its Contributor + Version); or + + c. under Patent Claims infringed by Covered Software in the absence of + its Contributions. + + This License does not grant any rights in the trademarks, service marks, + or logos of any Contributor (except as may be necessary to comply with + the notice requirements in Section 3.4). + +2.4. Subsequent Licenses + + No Contributor makes additional grants as a result of Your choice to + distribute the Covered Software under a subsequent version of this + License (see Section 10.2) or under the terms of a Secondary License (if + permitted under the terms of Section 3.3). + +2.5. Representation + + Each Contributor represents that the Contributor believes its + Contributions are its original creation(s) or it has sufficient rights to + grant the rights to its Contributions conveyed by this License. + +2.6. Fair Use + + This License is not intended to limit any rights You have under + applicable copyright doctrines of fair use, fair dealing, or other + equivalents. + +2.7. Conditions + + Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in + Section 2.1. + + +3. Responsibilities + +3.1. Distribution of Source Form + + All distribution of Covered Software in Source Code Form, including any + Modifications that You create or to which You contribute, must be under + the terms of this License. You must inform recipients that the Source + Code Form of the Covered Software is governed by the terms of this + License, and how they can obtain a copy of this License. You may not + attempt to alter or restrict the recipients' rights in the Source Code + Form. + +3.2. Distribution of Executable Form + + If You distribute Covered Software in Executable Form then: + + a. such Covered Software must also be made available in Source Code Form, + as described in Section 3.1, and You must inform recipients of the + Executable Form how they can obtain a copy of such Source Code Form by + reasonable means in a timely manner, at a charge no more than the cost + of distribution to the recipient; and + + b. You may distribute such Executable Form under the terms of this + License, or sublicense it under different terms, provided that the + license for the Executable Form does not attempt to limit or alter the + recipients' rights in the Source Code Form under this License. + +3.3. Distribution of a Larger Work + + You may create and distribute a Larger Work under terms of Your choice, + provided that You also comply with the requirements of this License for + the Covered Software. If the Larger Work is a combination of Covered + Software with a work governed by one or more Secondary Licenses, and the + Covered Software is not Incompatible With Secondary Licenses, this + License permits You to additionally distribute such Covered Software + under the terms of such Secondary License(s), so that the recipient of + the Larger Work may, at their option, further distribute the Covered + Software under the terms of either this License or such Secondary + License(s). + +3.4. Notices + + You may not remove or alter the substance of any license notices + (including copyright notices, patent notices, disclaimers of warranty, or + limitations of liability) contained within the Source Code Form of the + Covered Software, except that You may alter any license notices to the + extent required to remedy known factual inaccuracies. + +3.5. Application of Additional Terms + + You may choose to offer, and to charge a fee for, warranty, support, + indemnity or liability obligations to one or more recipients of Covered + Software. However, You may do so only on Your own behalf, and not on + behalf of any Contributor. You must make it absolutely clear that any + such warranty, support, indemnity, or liability obligation is offered by + You alone, and You hereby agree to indemnify every Contributor for any + liability incurred by such Contributor as a result of warranty, support, + indemnity or liability terms You offer. You may include additional + disclaimers of warranty and limitations of liability specific to any + jurisdiction. + +4. Inability to Comply Due to Statute or Regulation + + If it is impossible for You to comply with any of the terms of this License + with respect to some or all of the Covered Software due to statute, + judicial order, or regulation then You must: (a) comply with the terms of + this License to the maximum extent possible; and (b) describe the + limitations and the code they affect. Such description must be placed in a + text file included with all distributions of the Covered Software under + this License. Except to the extent prohibited by statute or regulation, + such description must be sufficiently detailed for a recipient of ordinary + skill to be able to understand it. + +5. Termination + +5.1. The rights granted under this License will terminate automatically if You + fail to comply with any of its terms. However, if You become compliant, + then the rights granted under this License from a particular Contributor + are reinstated (a) provisionally, unless and until such Contributor + explicitly and finally terminates Your grants, and (b) on an ongoing + basis, if such Contributor fails to notify You of the non-compliance by + some reasonable means prior to 60 days after You have come back into + compliance. Moreover, Your grants from a particular Contributor are + reinstated on an ongoing basis if such Contributor notifies You of the + non-compliance by some reasonable means, this is the first time You have + received notice of non-compliance with this License from such + Contributor, and You become compliant prior to 30 days after Your receipt + of the notice. + +5.2. If You initiate litigation against any entity by asserting a patent + infringement claim (excluding declaratory judgment actions, + counter-claims, and cross-claims) alleging that a Contributor Version + directly or indirectly infringes any patent, then the rights granted to + You by any and all Contributors for the Covered Software under Section + 2.1 of this License shall terminate. + +5.3. In the event of termination under Sections 5.1 or 5.2 above, all end user + license agreements (excluding distributors and resellers) which have been + validly granted by You or Your distributors under this License prior to + termination shall survive termination. + +6. Disclaimer of Warranty + + Covered Software is provided under this License on an "as is" basis, + without warranty of any kind, either expressed, implied, or statutory, + including, without limitation, warranties that the Covered Software is free + of defects, merchantable, fit for a particular purpose or non-infringing. + The entire risk as to the quality and performance of the Covered Software + is with You. Should any Covered Software prove defective in any respect, + You (not any Contributor) assume the cost of any necessary servicing, + repair, or correction. This disclaimer of warranty constitutes an essential + part of this License. No use of any Covered Software is authorized under + this License except under this disclaimer. + +7. Limitation of Liability + + Under no circumstances and under no legal theory, whether tort (including + negligence), contract, or otherwise, shall any Contributor, or anyone who + distributes Covered Software as permitted above, be liable to You for any + direct, indirect, special, incidental, or consequential damages of any + character including, without limitation, damages for lost profits, loss of + goodwill, work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses, even if such party shall have been + informed of the possibility of such damages. This limitation of liability + shall not apply to liability for death or personal injury resulting from + such party's negligence to the extent applicable law prohibits such + limitation. Some jurisdictions do not allow the exclusion or limitation of + incidental or consequential damages, so this exclusion and limitation may + not apply to You. + +8. Litigation + + Any litigation relating to this License may be brought only in the courts + of a jurisdiction where the defendant maintains its principal place of + business and such litigation shall be governed by laws of that + jurisdiction, without reference to its conflict-of-law provisions. Nothing + in this Section shall prevent a party's ability to bring cross-claims or + counter-claims. + +9. Miscellaneous + + This License represents the complete agreement concerning the subject + matter hereof. If any provision of this License is held to be + unenforceable, such provision shall be reformed only to the extent + necessary to make it enforceable. Any law or regulation which provides that + the language of a contract shall be construed against the drafter shall not + be used to construe this License against a Contributor. + + +10. Versions of the License + +10.1. New Versions + + Mozilla Foundation is the license steward. Except as provided in Section + 10.3, no one other than the license steward has the right to modify or + publish new versions of this License. Each version will be given a + distinguishing version number. + +10.2. Effect of New Versions + + You may distribute the Covered Software under the terms of the version + of the License under which You originally received the Covered Software, + or under the terms of any subsequent version published by the license + steward. + +10.3. Modified Versions + + If you create software not governed by this License, and you want to + create a new license for such software, you may create and use a + modified version of this License if you rename the license and remove + any references to the name of the license steward (except to note that + such modified license differs from this License). + +10.4. Distributing Source Code Form that is Incompatible With Secondary + Licenses If You choose to distribute Source Code Form that is + Incompatible With Secondary Licenses under the terms of this version of + the License, the notice described in Exhibit B of this License must be + attached. + +Exhibit A - Source Code Form License Notice + + 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/. + +If it is not possible or desirable to put the notice in a particular file, +then You may include the notice in a location (such as a LICENSE file in a +relevant directory) where a recipient would be likely to look for such a +notice. + +You may add additional accurate notices of copyright ownership. + +Exhibit B - "Incompatible With Secondary Licenses" Notice + + This Source Code Form is "Incompatible + With Secondary Licenses", as defined by + the Mozilla Public License, v. 2.0. + diff --git a/mobile/ios/Leanplum.json b/mobile/ios/Leanplum.json new file mode 100644 index 0000000000..be89fdf336 --- /dev/null +++ b/mobile/ios/Leanplum.json @@ -0,0 +1,4 @@ +{ + "2.0.1": "https://github.com/Leanplum/Leanplum-iOS-SDK/releases/download/2.0.1/Leanplum.framework.zip", + "2.0.2": "https://github.com/Leanplum/Leanplum-iOS-SDK/releases/download/2.0.2/Leanplum.framework.zip" +} diff --git a/mobile/ios/MMA.md b/mobile/ios/MMA.md new file mode 100644 index 0000000000..3973f2d884 --- /dev/null +++ b/mobile/ios/MMA.md @@ -0,0 +1,223 @@ + MMA (Mobile Marketing Automation) +================================ + +Mozilla wants to engage with users more. MMA is the project for this purpose. When a user performs a certain UI action (or set of UI actions), she will see a prompt and have a chance to interact with it. For example, if a user uses Firefox 10 times a week, but Firefox is not her default browser, we'll prompt the user the next time when she launches our app, and guide her to set us as default browser. + +Starting in Firefox 8.0 on iOS, Mozilla is using a third party framework called "Leanplum" in order to achieve this. Leanplum is a San Francisco company, founded in 2012. We put their SDK in our codebase via Carthage. + +The SDK is documented at https://www.leanplum.com/docs/ios + +There are three major component in Leanplum SDK. +1. Events : Events are fired when users perform certain actions. +2. User Attributes: User Attributes are set on a per-user basis, and inform us about an aspect of the user. +3. Deep Links: Actions that users can perform to interact with the Message. +4. Messages: User Interaction points that we want to engage with users that help them use Firefox better. + +An Event or a series of Events plus some User Attributes may trigger a Message, and when the user acts on the Message, a Deep Link may be processed. + + +Data collection +===================================================== + + +Who will have Leanplum enabled? +====================================================== + +Users who have a device locale listed in the following code snippet will have Leanplum enabled: https://github.com/mozilla-mobile/firefox-ios/blob/master/Client/Application/LeanplumIntegration.swift + + +Where does data sent to the Leanplum backend go? +============================================== + +The Leanplum SDK is hard-coded to send data to the endpoint https://www.leanplum.com. The endpoint is +defined by ``com.leanplum.internal.Constants.API_HOST_NAME`` at +https://searchfox.org/mozilla-central/rev/c49a70b53f67dd5550eec8a08793805f2aca8d42/mobile/android/thirdparty/com/leanplum/internal/Constants.java#32. + +The user is identified by Leanplum using a random UUID generated by Apple when Leanplum is initialized for the first time (see https://developer.apple.com/documentation/uikit/uidevice/1620059-identifierforvendor). +This unique identifier is only used by Leanplum and can't be tracked back to any Firefox users. + + +What data is collected and sent to the Leanplum backend? +====================================================== + +The Leanplum SDK collects and sends the following information at various times while the SDK is in use. + +Sent every time when an event is triggered: +~~~~~~~~~~~~~~~ + "action" -> "track" // track: an event is tracked. + "event" -> "Launch" // Used when an event is triggered. e.g. E_Saved_Bookmark. + "info" -> "" // Used when an event is triggered. Basic context associated with the event. + "value" -> 0.0 // Used when an event is triggered. Value of that event. + "messageId" -> 5111602214338560 // Used when an event is triggered. The ID of the message. +~~~~~~~~~~~~~~~ + +Sent when the app starts: +~~~~~~~~~~~~~~~ + "action" -> "start" // start: Leanplum SDK starts. heartbeat + "userAttributes" -> "{ // A set of key-value pairs used to describe the user. + "Focus Installed" -> true // If Focus for iOS is installed. + "Klar Installed" -> true // If Klar for iOS is installed. + "Signed In Sync" -> true // If the user signed into a Firefox Account. + "Mailto Is Default" -> true // If the user has not changed their default mailto handler. + "Telemetry Opt In" -> true // If the user has not disabled Sending Anonymous Product Usage. + }" + "appId" -> "app_6Ao...." // Leanplum App ID. + "clientKey" -> "dev_srwDUNZR...." // Leanplum client access key. + "systemName" -> "iOS" // Fixed String in SDK. + "locale" -> "zh_TW" // System Locale. + "timezone" -> "Asia/Taipei" // System Timezone. + "versionName" -> "55.0a1" // Fennec version. + "systemVersion" -> "10.3.1" // System version. + "deviceModel" -> "iPhone" // System device model. + "timezoneOffsetSeconds" -> "28800" // User timezone offset with PST. + "deviceName" -> "sdaswani-31710" // System device name. + "region" -> "(detect)" // Not used. We strip location so this is will be the default stub value in Leanplum SDK. + "city" -> "(detect)" // Same as above. + "country" -> "(detect)" // Same as above. + "location" -> "(detect)" // Same as above. + "newsfeedMessages" -> " size = 0" // Not used. New Leanplum Inbox message(Leanplum feature) count. + "includeDefaults" -> "false" // Not used. Always false. + ~~~~~~~~~~~~~~~ + +Sent every time a session is renewed or has a state change: +~~~~~~~~~~~~~~~ + "action" -> "heartbeat" // heartbeat: every 15 minutes when app is in the foreground + // pauseSession: when app goes to background + // resumeSession: when app goes to foreground +~~~~~~~~~~~~~~~ + +Sent for every Message: +~~~~~~~~~~~~~~~ + "userId" -> "b13b3c239d01aa7c" // Set by Fennec, we use random uuid so users are anonymous to Leanplum. + "deviceId" -> "b13b3c239d01aa7c" // Same as above. + "sdkVersion" -> "2.2.2-SNAPSHOT" // Leanplum SDK version. + "devMode" -> "true" // If the SDK is in developer mode. For official builds, it's false. + "time" -> "1.497595093902E9" // System time in second. + "token" -> "nksZ5pa0R5iegC7wj...." // Token come from Leanplum backend. +~~~~~~~~~~~~~~~ + +Notes on what data is collected +=============================== +User Identifier +--------------- +Since Device ID is a random UUID, Leanplum can't map the device to any know Client ID in Fennec nor Advertising ID. + +Events +------- +Most of the Leanplum events can be mapped to a single combination of Telemetry event (Event+Method+Extra). +Some events are not collected in Mozilla Telemetry. This will be addressed separately in each campaign review. +There are three elements that are used for each event. They are: event name, value(default: 0.0), and info(default: ""). +Default value for event value is 0.0. Default value for event info is empty string. + +Here is the list of current Events sent, which can be found here in the code base: https://github.com/mozilla-mobile/firefox-ios/blob/master/Client/Application/LeanplumIntegration.swift#L21 + +The first launch after install +~~~~ +{ + "event": "E_First_Run" +} +~~~~ + +The second launch after install +~~~~ +{ + "event": "E_Second_Run" +} +~~~~ + +Whenever the App is launched. +~~~~ +{ + "event": "E_Opened_App" +} +~~~~ + +The user loads a bookmark from home panel. +~~~~ +{ + "event": "E_Opened_Bookmark" +} +~~~~ + +The user opened a new tab. +~~~~ +{ + + "event": "E_Opened_New_Tab" +} +~~~~ + +The user opens a Pocket trending story +~~~~ +{ + + "event": "E_Opened_Pocket_Story" +} +~~~~ + +The user interacts with search url area. +~~~~ +{ + "event": "E_Interact_With_Search_URL_Area" +} +~~~~ + +The user saves a bookmark. +~~~~ +{ + "event": "E_Saved_Bookmark" +} +~~~~ + +The user opened a mailto link from a web page. +~~~~ +{ + "event": "E_Opened_Mailto_Link" +} +~~~~ + +The user cleared their private data. +~~~~ +{ + "event": "E_Cleared_Private_Data" +} +~~~~ + +Download videos or any other media +~~~~ +{ + "event": "E_Download_Media_Saved_Image" +} +~~~~ + +Save password and login from door hanger +~~~~ +{ + "event": "E_Saved_Login_And_Password" +} +~~~~ + +Deep Links +---------- +Deep links are actions that can point Firefox to open certain pages or load features such as `show bookmark list` or +`open Firefox Account Settings`. When users receives a Message, they can click the button(s) on it. These buttons can +trigger the following deep links: +* Link to sync signup/sign in (firefox://deep-link?url=settings/fxa) +* Link to default search engine settings (firefox://deep-link?url=settings/search) +* Link to bookmark list (firefox://deep-link?url=homepanel/bookmarks) +* Link to history list (firefox://deep-link?url=/history) +* Link to general preferences (firefox://deep-link?url=settings) +* Link to the mailto preferences (firefox://deep-link?url=settings/mailto) +* Link to open a new tab (firefox://deep-link?url=settings/newtab) +* Link to open the user's homepage (firefox://deep-link?url=settings/homepage) +* Link to open the settings page so the user can clear their private data (firefox://deep-link?url=settings/clear-private-data) +* Link to open a new private tab (firefox://deep-link?url=homepanel/new-private-tab) + + +Note these deep links can only be processed from within Firefox (i.e., if they are clicked from outside of Firefox, Firefox will open but nothing further will happen). + +Messages +----------- +Messages are in-app prompts to the user from Leanplum. The user interaction of that prompt will be sent to the Leanplum backend (such as "Accept" or "Show") to track overall engagement with the Message. The Message is downloaded from Leanplum when the Leanplum SDK is initialized at App start, assuming the fulfillment criteria for the Message is met. As mentioned before, the fulfillment criteria is a set of required Events and User Attributes. The fulfillment criteria are set in the Leanplum backend. + +The List of current Messages for iOS can be found here: https://wiki.mozilla.org/Leanplum_Contextual_Hints diff --git a/mobile/ios/MarketingUITests/Info.plist b/mobile/ios/MarketingUITests/Info.plist new file mode 100644 index 0000000000..9411e4a06e --- /dev/null +++ b/mobile/ios/MarketingUITests/Info.plist @@ -0,0 +1,24 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + BNDL + CFBundleShortVersionString + 10.6 + CFBundleSignature + ???? + CFBundleVersion + 1 + + diff --git a/mobile/ios/MarketingUITests/MarketingUITests.swift b/mobile/ios/MarketingUITests/MarketingUITests.swift new file mode 100644 index 0000000000..3c3dfc208b --- /dev/null +++ b/mobile/ios/MarketingUITests/MarketingUITests.swift @@ -0,0 +1,118 @@ +/* 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 XCTest + +class MarketingSnapshotTests: XCTestCase { + + let LoadingTimeout: NSTimeInterval = 60 + let exists = NSPredicate(format: "exists = true") + let loaded = NSPredicate(format: "value BEGINSWITH '100'") + + var sleep = false + + override func setUp() { + super.setUp() + + // Put setup code here. This method is called before the invocation of each test method in the class. + + // In UI tests it is usually best to stop immediately when a failure occurs. + continueAfterFailure = false + + let app = XCUIApplication() + setupSnapshot(app) + app.launch() + } + + override func tearDown() { + // Put teardown code here. This method is called after the invocation of each test method in the class. + super.tearDown() + } + + func loadWebPage(url: String, testForAutocompleteDialog: Bool = false) { + let app = XCUIApplication() + let addressTextField = app.textFields.elementBoundByIndex(0) + addressTextField.tap() + addressTextField.typeText(url) + if testForAutocompleteDialog { + // for var idx:UInt = 0; idx < app.buttons.count; idx++ { + // let button = app.buttons.elementBoundByIndex(idx) + // print("\(button.identifier): \(button.value)") + // } + app.buttons.elementBoundByIndex(8).tap() + } + let progressIndicator = app.progressIndicators.elementBoundByIndex(0) + expectationForPredicate(exists, evaluatedWithObject: progressIndicator, handler: nil) + expectationForPredicate(loaded, evaluatedWithObject: progressIndicator, handler: nil) + app.typeText("\n") + waitForExpectations(timeout: LoadingTimeout, handler: nil) + } + + func testTakeMarketingScreenshots() { + + let app = XCUIApplication() + + // dismiss the intro tour + app.buttons.elementBoundByIndex(1).tap() + snapshot("00TopSites") + + // go to synced tabs home screen + app.buttons.elementBoundByIndex(4).tap() + snapshot("03SyncedTabs") + + // return to top sites + app.buttons.elementBoundByIndex(1).tap() + + // create new tab + app.staticTexts.firstWithName("1").tap() + let addTabButton = app.buttons.elementBoundByIndex(3) + addTabButton.tap() + + // load some web pages in some new tabs + loadWebPage("http://twitter.com", testForAutocompleteDialog: true) + + app.staticTexts.firstWithName("2").tap() + addTabButton.tap() + loadWebPage("https://mozilla.org/firefox/desktop") + + app.staticTexts.firstWithName("3").tap() + addTabButton.tap() + loadWebPage("https://mozilla.org") + app.staticTexts.firstWithName("4").tap() + addTabButton.tap() + + loadWebPage("firefox") + loadWebPage("https://mozilla.org/firefox/new") + app.staticTexts.firstWithName("5").tap() + snapshot("02TabTray") + + // return to first tab + app.collectionViews.firstMatchingType(.Cell).tap() + let addressTextField = app.textFields.elementBoundByIndex(0) + addressTextField.tap() + + // perform a search but don't complete (we're testing autocomplete here) + addressTextField.typeText("firef") + snapshot("01SearchResults") + + // cancel search + app.buttons.elementBoundByIndex(1).tap() + } + +} + +extension XCUIElementQuery { + func firstWithName(name: String) -> XCUIElement { + let values = self.containingPredicate(NSPredicate(format: "label = '\(name)'")) + if values.count > 0 { + return values.elementBoundByIndex(0) + } + + return self.elementBoundByIndex(0) + } + + func firstMatchingType( type: XCUIElementType) -> XCUIElement { + return self.childrenMatchingType(type).elementBoundByIndex(0) + } +} diff --git a/mobile/ios/Mocking.swift b/mobile/ios/Mocking.swift new file mode 100644 index 0000000000..952af39cca --- /dev/null +++ b/mobile/ios/Mocking.swift @@ -0,0 +1,68 @@ +/* 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 SwiftyJSON +@testable import Sync + +class MockBackoffStorage: BackoffStorage { + var serverBackoffUntilLocalTimestamp: Timestamp? + + func clearServerBackoff() { + serverBackoffUntilLocalTimestamp = nil + } + + func isInBackoff(_ now: Timestamp) -> Timestamp? { + return nil + } +} + +class MockSyncCollectionClient: Sync15CollectionClient { + let uploader: BatchUploadFunction + let infoConfig: InfoConfiguration + + init(uploader: @escaping BatchUploadFunction, + infoConfig: InfoConfiguration, + collection: String, + encrypter: RecordEncrypter, + client: Sync15StorageClient = getClient(server: getServer(preStart: { _ in })), + serverURI: URL = URL(string: "http://localhost/collections")!) { + self.uploader = uploader + self.infoConfig = infoConfig + super.init(client: client, serverURI: serverURI, collection: collection, encrypter: encrypter) + } + + override func newBatch(ifUnmodifiedSince: Timestamp? = nil, onCollectionUploaded: @escaping (POSTResult, Timestamp?) -> DeferredTimestamp) -> Sync15BatchClient { + return Sync15BatchClient(config: self.infoConfig, + ifUnmodifiedSince: ifUnmodifiedSince, + serializeRecord: self.serializeRecord, + uploader: self.uploader, + onCollectionUploaded: onCollectionUploaded) + } +} + +// MARK: Various mocks + +// Non-encrypting 'encrypter'. +func getEncrypter() -> RecordEncrypter { + let serializer: (Record) -> JSON? = { $0.payload.json } + let factory: (String) -> CleartextPayloadJSON = { CleartextPayloadJSON($0) } + return RecordEncrypter(serializer: serializer, factory: factory) +} + +func getClient(server: MockSyncServer) -> Sync15StorageClient { + let url = server.baseURL.asURL! + let authorizer: Authorizer = identity + let queue = DispatchQueue.global(qos: DispatchQoS.background.qosClass) + + return Sync15StorageClient(serverURI: url, authorizer: authorizer, workQueue: queue, resultQueue: queue, backoff: MockBackoffStorage()) +} + +func getServer(username: String = "1234567", preStart: (MockSyncServer) -> Void) -> MockSyncServer { + let server = MockSyncServer(username: username) + preStart(server) + server.start() + return server +} diff --git a/mobile/ios/Providers/NSUserDefaultsPrefs.swift b/mobile/ios/Providers/NSUserDefaultsPrefs.swift new file mode 100644 index 0000000000..4cd30690db --- /dev/null +++ b/mobile/ios/Providers/NSUserDefaultsPrefs.swift @@ -0,0 +1,137 @@ +/* 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 + +open class NSUserDefaultsPrefs: Prefs { + + fileprivate let prefixWithDot: String + fileprivate let userDefaults: UserDefaults + + open func getBranchPrefix() -> String { + return self.prefixWithDot + } + + init(prefix: String, userDefaults: UserDefaults) { + self.prefixWithDot = prefix + (prefix.endsWith(".") ? "" : ".") + self.userDefaults = userDefaults + } + + init(prefix: String) { + self.prefixWithDot = prefix + (prefix.endsWith(".") ? "" : ".") + self.userDefaults = UserDefaults(suiteName: AppInfo.sharedContainerIdentifier)! + } + + open func branch(_ branch: String) -> Prefs { + let prefix = self.prefixWithDot + branch + "." + return NSUserDefaultsPrefs(prefix: prefix, userDefaults: self.userDefaults) + } + + // Preferences are qualified by the profile's local name. + // Connecting a profile to a Firefox Account, or changing to another, won't alter this. + fileprivate func qualifyKey(_ key: String) -> String { + return self.prefixWithDot + key + } + + open func setInt(_ value: Int32, forKey defaultName: String) { + // Why aren't you using userDefaults.setInteger? + // Because userDefaults.getInteger returns a non-optional; it's impossible + // to tell whether there's a value set, and you thus can't distinguish + // between "not present" and zero. + // Yeah, NSUserDefaults is meant to be used for storing "defaults", not data. + setObject(NSNumber(value: value), forKey: defaultName) + } + + open func setTimestamp(_ value: Timestamp, forKey defaultName: String) { + setLong(value, forKey: defaultName) + } + + open func setLong(_ value: UInt64, forKey defaultName: String) { + setObject(NSNumber(value: value), forKey: defaultName) + } + + open func setLong(_ value: Int64, forKey defaultName: String) { + setObject(NSNumber(value: value), forKey: defaultName) + } + + open func setString(_ value: String, forKey defaultName: String) { + setObject(value as AnyObject?, forKey: defaultName) + } + + open func setObject(_ value: Any?, forKey defaultName: String) { + userDefaults.set(value, forKey: qualifyKey(defaultName)) + } + + open func stringForKey(_ defaultName: String) -> String? { + // stringForKey converts numbers to strings, which is almost always a bug. + return userDefaults.object(forKey: qualifyKey(defaultName)) as? String + } + + open func setBool(_ value: Bool, forKey defaultName: String) { + setObject(NSNumber(value: value as Bool), forKey: defaultName) + } + + open func boolForKey(_ defaultName: String) -> Bool? { + // boolForKey just returns false if the key doesn't exist. We need to + // distinguish between false and non-existent keys, so use objectForKey + // and cast the result instead. + let number = userDefaults.object(forKey: qualifyKey(defaultName)) as? NSNumber + return number?.boolValue + } + + fileprivate func nsNumberForKey(_ defaultName: String) -> NSNumber? { + return userDefaults.object(forKey: qualifyKey(defaultName)) as? NSNumber + } + + open func unsignedLongForKey(_ defaultName: String) -> UInt64? { + return nsNumberForKey(defaultName)?.uint64Value + } + + open func timestampForKey(_ defaultName: String) -> Timestamp? { + return unsignedLongForKey(defaultName) + } + + open func longForKey(_ defaultName: String) -> Int64? { + return nsNumberForKey(defaultName)?.int64Value + } + + open func objectForKey(_ defaultName: String) -> T? { + return userDefaults.object(forKey: qualifyKey(defaultName)) as? T + } + + open func intForKey(_ defaultName: String) -> Int32? { + return nsNumberForKey(defaultName)?.int32Value + } + + open func stringArrayForKey(_ defaultName: String) -> [String]? { + let objects = userDefaults.stringArray(forKey: qualifyKey(defaultName)) + if let strings = objects { + return strings + } + return nil + } + + open func arrayForKey(_ defaultName: String) -> [Any]? { + return userDefaults.array(forKey: qualifyKey(defaultName)) as [Any]? + } + + open func dictionaryForKey(_ defaultName: String) -> [String: Any]? { + return userDefaults.dictionary(forKey: qualifyKey(defaultName)) as [String: Any]? + } + + open func removeObjectForKey(_ defaultName: String) { + userDefaults.removeObject(forKey: qualifyKey(defaultName)) + } + + open func clearAll() { + // TODO: userDefaults.removePersistentDomainForName() has no effect for app group suites. + // iOS Bug? Iterate and remove each manually for now. + for key in userDefaults.dictionaryRepresentation().keys { + if key.startsWith(prefixWithDot) { + userDefaults.removeObject(forKey: key) + } + } + } +} diff --git a/mobile/ios/Providers/PocketFeed.swift b/mobile/ios/Providers/PocketFeed.swift new file mode 100644 index 0000000000..415ccfa8ff --- /dev/null +++ b/mobile/ios/Providers/PocketFeed.swift @@ -0,0 +1,144 @@ +/* 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 Alamofire +import Shared +import Deferred +import Storage + +private let PocketEnvAPIKey = "PocketEnvironmentAPIKey" +private let PocketGlobalFeed = "https://getpocket.cdn.mozilla.net/v3/firefox/global-recs" +private let MaxCacheAge: Timestamp = OneMinuteInMilliseconds * 60 // 1 hour in milliseconds +private let SupportedLocales = ["en_US", "en_GB", "en_ZA", "de_DE", "de_AT", "de_CH"] + +/*s + The Pocket class is used to fetch stories from the Pocked API. + Right now this only supports the global feed + + For a sample feed item check ClientTests/pocketglobalfeed.json + */ +struct PocketStory { + let url: URL + let title: String + let storyDescription: String + let imageURL: URL + let domain: String + let dedupeURL: URL + + static func parseJSON(list: Array<[String: Any]>) -> [PocketStory] { + return list.flatMap({ (storyDict) -> PocketStory? in + guard let urlS = storyDict["url"] as? String, let domain = storyDict["domain"] as? String, + let dedupe_URL = storyDict["dedupe_url"] as? String, + let imageURLS = storyDict["image_src"] as? String, + let title = storyDict["title"] as? String, + let description = storyDict["excerpt"] as? String else { + return nil + } + guard let url = URL(string: urlS), let imageURL = URL(string: imageURLS), let dedupeURL = URL(string: dedupe_URL) else { + return nil + } + return PocketStory(url: url, title: title, storyDescription: description, imageURL: imageURL, domain: domain, dedupeURL: dedupeURL) + }) + } +} + +private class PocketError: MaybeErrorType { + var description = "Failed to load from API" +} + +class Pocket { + private let pocketGlobalFeed: String + static let MoreStoriesURL = URL(string: "https://getpocket.com/explore/trending?src=ff_ios&cdn=0")! + + // Allow endPoint to be overriden for testing + init(endPoint: String = PocketGlobalFeed) { + self.pocketGlobalFeed = endPoint + } + + lazy fileprivate var alamofire: SessionManager = { + let ua = UserAgent.defaultClientUserAgent + let configuration = URLSessionConfiguration.default + var defaultHeaders = SessionManager.default.session.configuration.httpAdditionalHeaders ?? [:] + defaultHeaders["User-Agent"] = ua + configuration.httpAdditionalHeaders = defaultHeaders + return SessionManager(configuration: configuration) + }() + + private func findCachedResponse(for request: URLRequest) -> [String: Any]? { + let cachedResponse = URLCache.shared.cachedResponse(for: request) + guard let cachedAtTime = cachedResponse?.userInfo?["cache-time"] as? Timestamp, (Date.now() - cachedAtTime) < MaxCacheAge else { + return nil + } + + guard let data = cachedResponse?.data, let json = try? JSONSerialization.jsonObject(with: data, options: []) else { + return nil + } + + return json as? [String: Any] + } + + private func cache(response: HTTPURLResponse?, for request: URLRequest, with data: Data?) { + guard let resp = response, let data = data else { + return + } + let metadata = ["cache-time": Date.now()] + let cachedResp = CachedURLResponse(response: resp, data: data, userInfo: metadata, storagePolicy: .allowed) + URLCache.shared.removeCachedResponse(for: request) + URLCache.shared.storeCachedResponse(cachedResp, for: request) + } + + // Fetch items from the global pocket feed + func globalFeed(items: Int = 2) -> Deferred> { + let deferred = Deferred>() + + guard let request = createGlobalFeedRequest(items: items) else { + deferred.fill([]) + return deferred + } + + if let cachedResponse = findCachedResponse(for: request), let items = cachedResponse["list"] as? Array<[String: Any]> { + deferred.fill(PocketStory.parseJSON(list: items)) + return deferred + } + + alamofire.request(request).validate(contentType: ["application/json"]).responseJSON { response in + guard response.error == nil, let result = response.result.value as? [String: Any] else { + return deferred.fill([]) + } + self.cache(response: response.response, for: request, with: response.data) + guard let items = result["list"] as? Array<[String: Any]> else { + return deferred.fill([]) + } + return deferred.fill(PocketStory.parseJSON(list: items)) + } + + return deferred + } + + // Returns nil if the locale is not supported + static func IslocaleSupported(_ locale: String) -> Bool { + return SupportedLocales.contains(locale) + } + + // Create the URL request to query the Pocket API. The max items that the query can return is 20 + private func createGlobalFeedRequest(items: Int = 2) -> URLRequest? { + guard items > 0 && items <= 20 else { + return nil + } + + let locale = Locale.current.identifier + let pocketLocale = locale.replacingOccurrences(of: "_", with: "-") + var params = [URLQueryItem(name: "count", value: String(items)), URLQueryItem(name: "locale_lang", value: pocketLocale)] + if let consumerKey = Bundle.main.object(forInfoDictionaryKey: PocketEnvAPIKey) as? String { + params.append(URLQueryItem(name: "consumer_key", value: consumerKey)) + } + + guard let feedURL = URL(string: pocketGlobalFeed)?.withQueryParams(params) else { + return nil + } + + return URLRequest(url: feedURL, cachePolicy: URLRequest.CachePolicy.reloadIgnoringCacheData, timeoutInterval: 5) + } +} diff --git a/mobile/ios/Providers/Profile.swift b/mobile/ios/Providers/Profile.swift new file mode 100644 index 0000000000..2151d9a6f6 --- /dev/null +++ b/mobile/ios/Providers/Profile.swift @@ -0,0 +1,1251 @@ +/* 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/. */ + +// IMPORTANT!: Please take into consideration when adding new imports to +// this file that it is utilized by external components besides the core +// application (i.e. App Extensions). Introducing new dependencies here +// may have unintended negative consequences for App Extensions such as +// increased startup times which may lead to termination by the OS. +import Account +import Shared +import Storage +import Sync +import XCGLogger +import SwiftKeychainWrapper +import Deferred + +// Import these dependencies ONLY for the main `Client` application target. +#if MOZ_TARGET_CLIENT + import SwiftyJSON + import SyncTelemetry +#endif + +// Import these dependencies for ALL targets *EXCEPT* `NotificationService`. +#if !MOZ_TARGET_NOTIFICATIONSERVICE + import ReadingList +#endif + +private let log = Logger.syncLogger + +public let NotificationProfileDidStartSyncing = Notification.Name("NotificationProfileDidStartSyncing") +public let NotificationProfileDidFinishSyncing = Notification.Name("NotificationProfileDidFinishSyncing") + +public let ProfileRemoteTabsSyncDelay: TimeInterval = 0.1 + +public protocol SyncManager { + var isSyncing: Bool { get } + var lastSyncFinishTime: Timestamp? { get set } + var syncDisplayState: SyncDisplayState? { get } + + func hasSyncedHistory() -> Deferred> + func hasSyncedLogins() -> Deferred> + + func syncClients() -> SyncResult + func syncClientsThenTabs() -> SyncResult + func syncHistory() -> SyncResult + func syncLogins() -> SyncResult + func mirrorBookmarks() -> SyncResult + @discardableResult func syncEverything(why: SyncReason) -> Success + func syncNamedCollections(why: SyncReason, names: [String]) -> Success + + // The simplest possible approach. + func beginTimedSyncs() + func endTimedSyncs() + func applicationDidEnterBackground() + func applicationDidBecomeActive() + + func onNewProfile() + @discardableResult func onRemovedAccount(_ account: FirefoxAccount?) -> Success + @discardableResult func onAddedAccount() -> Success +} + +typealias SyncFunction = (SyncDelegate, Prefs, Ready, SyncReason) -> SyncResult + +class ProfileFileAccessor: FileAccessor { + convenience init(profile: Profile) { + self.init(localName: profile.localName()) + } + + init(localName: String) { + let profileDirName = "profile.\(localName)" + + // Bug 1147262: First option is for device, second is for simulator. + var rootPath: String + let sharedContainerIdentifier = AppInfo.sharedContainerIdentifier + if let url = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: sharedContainerIdentifier) { + rootPath = url.path + } else { + log.error("Unable to find the shared container. Defaulting profile location to ~/Documents instead.") + rootPath = (NSSearchPathForDirectoriesInDomains(FileManager.SearchPathDirectory.documentDirectory, FileManager.SearchPathDomainMask.userDomainMask, true)[0]) + } + + super.init(rootPath: URL(fileURLWithPath: rootPath).appendingPathComponent(profileDirName).path) + } +} + +class CommandStoringSyncDelegate: SyncDelegate { + let profile: Profile + + init(profile: Profile) { + self.profile = profile + } + + public func displaySentTab(for url: URL, title: String, from deviceName: String?) { + let item = ShareItem(url: url.absoluteString, title: title, favicon: nil) + _ = self.profile.queue.addToQueue(item) + } +} + +/** + * This exists because the Sync code is extension-safe, and thus doesn't get + * direct access to UIApplication.sharedApplication, which it would need to + * display a notification. + * This will also likely be the extension point for wipes, resets, and + * getting access to data sources during a sync. + */ + +let TabSendURLKey = "TabSendURL" +let TabSendTitleKey = "TabSendTitle" +let TabSendCategory = "TabSendCategory" + +enum SentTabAction: String { + case view = "TabSendViewAction" + case bookmark = "TabSendBookmarkAction" + case readingList = "TabSendReadingListAction" +} + +/** + * A Profile manages access to the user's data. + */ +protocol Profile: class { + var bookmarks: BookmarksModelFactorySource & KeywordSearchSource & ShareToDestination & SyncableBookmarks & LocalItemSource & MirrorItemSource { get } + // var favicons: Favicons { get } + var prefs: Prefs { get } + var queue: TabQueue { get } + var searchEngines: SearchEngines { get } + var files: FileAccessor { get } + var history: BrowserHistory & SyncableHistory & ResettableSyncStorage { get } + var metadata: Metadata { get } + var recommendations: HistoryRecommendations { get } + var favicons: Favicons { get } + var logins: BrowserLogins & SyncableLogins & ResettableSyncStorage { get } + var certStore: CertStore { get } + var recentlyClosedTabs: ClosedTabsStore { get } + var panelDataObservers: PanelDataObservers { get } + + #if !MOZ_TARGET_NOTIFICATIONSERVICE + var readingList: ReadingListService? { get } + #endif + + var isShutdown: Bool { get } + + func shutdown() + func reopen() + + // I got really weird EXC_BAD_ACCESS errors on a non-null reference when I made this a getter. + // Similar to . + func localName() -> String + + // URLs and account configuration. + var accountConfiguration: FirefoxAccountConfiguration { get } + + // Do we have an account at all? + func hasAccount() -> Bool + + // Do we have an account that (as far as we know) is in a syncable state? + func hasSyncableAccount() -> Bool + + func getAccount() -> FirefoxAccount? + func removeAccount() + func setAccount(_ account: FirefoxAccount) + func flushAccount() + + func getClients() -> Deferred> + func getClientsAndTabs() -> Deferred> + func getCachedClientsAndTabs() -> Deferred> + + @discardableResult func storeTabs(_ tabs: [RemoteTab]) -> Deferred> + + func sendItems(_ items: [ShareItem], toClients clients: [RemoteClient]) -> Deferred> + + var syncManager: SyncManager! { get } + var isChinaEdition: Bool { get } +} + +fileprivate let PrefKeyClientID = "PrefKeyClientID" +extension Profile { + var clientID: String { + let clientID: String + if let id = prefs.stringForKey(PrefKeyClientID) { + clientID = id + } else { + clientID = UUID().uuidString + prefs.setString(clientID, forKey: PrefKeyClientID) + } + return clientID + } +} + +open class BrowserProfile: Profile { + fileprivate let name: String + fileprivate let keychain: KeychainWrapper + var isShutdown = false + + internal let files: FileAccessor + + let db: BrowserDB + let loginsDB: BrowserDB + var syncManager: SyncManager! + + private static var loginsKey: String? { + let key = "sqlcipher.key.logins.db" + let keychain = KeychainWrapper.sharedAppContainerKeychain + keychain.ensureStringItemAccessibility(.afterFirstUnlock, forKey: key) + if keychain.hasValue(forKey: key) { + return keychain.string(forKey: key) + } + + let Length: UInt = 256 + let secret = Bytes.generateRandomBytes(Length).base64EncodedString + keychain.set(secret, forKey: key, withAccessibility: .afterFirstUnlock) + return secret + } + + var syncDelegate: SyncDelegate? + + /** + * N.B., BrowserProfile is used from our extensions, often via a pattern like + * + * BrowserProfile(…).foo.saveSomething(…) + * + * This can break if BrowserProfile's initializer does async work that + * subsequently — and asynchronously — expects the profile to stick around: + * see Bug 1218833. Be sure to only perform synchronous actions here. + * + * A SyncDelegate can be provided in this initializer, or once the profile is initialized. + * However, if we provide it here, it's assumed that we're initializing it from the application, + * and initialize the logins.db. + */ + init(localName: String, syncDelegate: SyncDelegate? = nil, clear: Bool = false) { + log.debug("Initing profile \(localName) on thread \(Thread.current).") + self.name = localName + self.files = ProfileFileAccessor(localName: localName) + self.keychain = KeychainWrapper.sharedAppContainerKeychain + self.syncDelegate = syncDelegate + + if clear { + do { + // Remove the contents of the directory… + try self.files.removeFilesInDirectory() + // …then remove the directory itself. + try self.files.remove("") + } catch { + log.info("Cannot clear profile: \(error)") + } + } + + // If the profile dir doesn't exist yet, this is first run (for this profile). The check is made here + // since the DB handles will create new DBs under the new profile folder. + let isNewProfile = !files.exists("") + + // Set up our database handles. + self.loginsDB = BrowserDB(filename: "logins.db", secretKey: BrowserProfile.loginsKey, schema: LoginsSchema(), files: files) + self.db = BrowserDB(filename: "browser.db", schema: BrowserSchema(), files: files) + + // This has to happen prior to the databases being opened, because opening them can trigger + // events to which the SyncManager listens. + self.syncManager = BrowserSyncManager(profile: self) + + let notificationCenter = NotificationCenter.default + + notificationCenter.addObserver(self, selector: #selector(onLocationChange(notification:)), name: NotificationOnLocationChange, object: nil) + notificationCenter.addObserver(self, selector: #selector(onPageMetadataFetched(notification:)), name: NotificationOnPageMetadataFetched, object: nil) + + if isNewProfile { + log.info("New profile. Removing old account metadata.") + self.removeAccountMetadata() + self.syncManager.onNewProfile() + self.removeExistingAuthenticationInfo() + prefs.clearAll() + } + + // Always start by needing invalidation. + // This is the same as self.history.setTopSitesNeedsInvalidation, but without the + // side-effect of instantiating SQLiteHistory (and thus BrowserDB) on the main thread. + prefs.setBool(false, forKey: PrefsKeys.KeyTopSitesCacheIsValid) + + if isChinaEdition { + // Set the default homepage. + prefs.setString(PrefsDefaults.ChineseHomePageURL, forKey: PrefsKeys.KeyDefaultHomePageURL) + + if prefs.stringForKey(PrefsKeys.KeyNewTab) == nil { + prefs.setString(PrefsDefaults.ChineseNewTabDefault, forKey: PrefsKeys.KeyNewTab) + } + } else { + // Remove the default homepage. This does not change the user's preference, + // just the behaviour when there is no homepage. + prefs.removeObjectForKey(PrefsKeys.KeyDefaultHomePageURL) + } + } + + func reopen() { + log.debug("Reopening profile.") + isShutdown = false + + db.reopenIfClosed() + loginsDB.reopenIfClosed() + } + + func shutdown() { + log.debug("Shutting down profile.") + isShutdown = true + + db.forceClose() + loginsDB.forceClose() + } + + @objc + func onLocationChange(notification: NSNotification) { + if let v = notification.userInfo!["visitType"] as? Int, + let visitType = VisitType(rawValue: v), + let url = notification.userInfo!["url"] as? URL, !isIgnoredURL(url), + let title = notification.userInfo!["title"] as? NSString { + // Only record local vists if the change notification originated from a non-private tab + if !(notification.userInfo!["isPrivate"] as? Bool ?? false) { + // We don't record a visit if no type was specified -- that means "ignore me". + let site = Site(url: url.absoluteString, title: title as String) + let visit = SiteVisit(site: site, date: Date.nowMicroseconds(), type: visitType) + history.addLocalVisit(visit) + } + + history.setTopSitesNeedsInvalidation() + } else { + log.debug("Ignoring navigation.") + } + } + + @objc + func onPageMetadataFetched(notification: NSNotification) { + let isPrivate = notification.userInfo?["isPrivate"] as? Bool ?? true + guard !isPrivate else { + log.debug("Private mode - Ignoring page metadata.") + return + } + guard let pageURL = notification.userInfo?["tabURL"] as? URL, + let pageMetadata = notification.userInfo?["pageMetadata"] as? PageMetadata else { + log.debug("Metadata notification doesn't contain any metadata!") + return + } + let defaultMetadataTTL: UInt64 = 3 * 24 * 60 * 60 * 1000 // 3 days for the metadata to live + self.metadata.storeMetadata(pageMetadata, forPageURL: pageURL, expireAt: defaultMetadataTTL + Date.now()) + } + + deinit { + log.debug("Deiniting profile \(self.localName).") + self.syncManager.endTimedSyncs() + } + + func localName() -> String { + return name + } + + lazy var queue: TabQueue = { + withExtendedLifetime(self.history) { + return SQLiteQueue(db: self.db) + } + }() + + /** + * Favicons, history, and bookmarks are all stored in one intermeshed + * collection of tables. + * + * Any other class that needs to access any one of these should ensure + * that this is initialized first. + */ + fileprivate lazy var places: BrowserHistory & Favicons & SyncableHistory & ResettableSyncStorage & HistoryRecommendations = { + return SQLiteHistory(db: self.db, prefs: self.prefs) + }() + + var favicons: Favicons { + return self.places + } + + var history: BrowserHistory & SyncableHistory & ResettableSyncStorage { + return self.places + } + + lazy var panelDataObservers: PanelDataObservers = { + return PanelDataObservers(profile: self) + }() + + lazy var metadata: Metadata = { + return SQLiteMetadata(db: self.db) + }() + + var recommendations: HistoryRecommendations { + return self.places + } + + lazy var bookmarks: BookmarksModelFactorySource & KeywordSearchSource & ShareToDestination & SyncableBookmarks & LocalItemSource & MirrorItemSource = { + // Make sure the rest of our tables are initialized before we try to read them! + // This expression is for side-effects only. + withExtendedLifetime(self.places) { + return MergedSQLiteBookmarks(db: self.db) + } + }() + + lazy var mirrorBookmarks: BookmarkBufferStorage & BufferItemSource = { + // Yeah, this is lazy. Sorry. + return self.bookmarks as! MergedSQLiteBookmarks + }() + + lazy var searchEngines: SearchEngines = { + return SearchEngines(prefs: self.prefs, files: self.files) + }() + + func makePrefs() -> Prefs { + return NSUserDefaultsPrefs(prefix: self.localName()) + } + + lazy var prefs: Prefs = { + return self.makePrefs() + }() + + #if !MOZ_TARGET_NOTIFICATIONSERVICE + lazy var readingList: ReadingListService? = { + return ReadingListService(profileStoragePath: self.files.rootPath as String) + }() + #endif + + lazy var remoteClientsAndTabs: RemoteClientsAndTabs & ResettableSyncStorage & AccountRemovalDelegate & RemoteDevices = { + return SQLiteRemoteClientsAndTabs(db: self.db) + }() + + lazy var certStore: CertStore = { + return CertStore() + }() + + lazy var recentlyClosedTabs: ClosedTabsStore = { + return ClosedTabsStore(prefs: self.prefs) + }() + + open func getSyncDelegate() -> SyncDelegate { + return syncDelegate ?? CommandStoringSyncDelegate(profile: self) + } + + public func getClients() -> Deferred> { + return self.syncManager.syncClients() + >>> { self.remoteClientsAndTabs.getClients() } + } + + public func getClientsAndTabs() -> Deferred> { + return self.syncManager.syncClientsThenTabs() + >>> { self.remoteClientsAndTabs.getClientsAndTabs() } + } + + public func getCachedClientsAndTabs() -> Deferred> { + return self.remoteClientsAndTabs.getClientsAndTabs() + } + + func storeTabs(_ tabs: [RemoteTab]) -> Deferred> { + return self.remoteClientsAndTabs.insertOrUpdateTabs(tabs) + } + + public func sendItems(_ items: [ShareItem], toClients clients: [RemoteClient]) -> Deferred> { + let id = DeviceInfo.clientIdentifier(self.prefs) + let commands = items.map { item in + SyncCommand.displayURIFromShareItem(item, asClient: id) + } + + func notifyClients() { + let deviceIDs = clients.flatMap { $0.fxaDeviceId } + guard let account = self.getAccount() else { + return + } + + account.notify(deviceIDs: deviceIDs, collectionsChanged: ["clients"], reason: "sendtab") + } + + return self.remoteClientsAndTabs.insertCommands(commands, forClients: clients) >>> { + let syncStatus = self.syncManager.syncClients() + syncStatus >>> notifyClients + return syncStatus + } + } + + lazy var logins: BrowserLogins & SyncableLogins & ResettableSyncStorage = { + return SQLiteLogins(db: self.loginsDB) + }() + + lazy var isChinaEdition: Bool = { + return Locale.current.identifier == "zh_CN" + }() + + var accountConfiguration: FirefoxAccountConfiguration { + if prefs.boolForKey("useCustomSyncService") ?? false { + return CustomFirefoxAccountConfiguration(prefs: self.prefs) + } + if prefs.boolForKey("useChinaSyncService") ?? isChinaEdition { + return ChinaEditionFirefoxAccountConfiguration() + } + if prefs.boolForKey("useStageSyncService") ?? false { + return StageFirefoxAccountConfiguration() + } + return ProductionFirefoxAccountConfiguration() + } + + fileprivate lazy var account: FirefoxAccount? = { + let key = self.name + ".account" + self.keychain.ensureObjectItemAccessibility(.afterFirstUnlock, forKey: key) + if let dictionary = self.keychain.object(forKey: key) as? [String: AnyObject] { + let account = FirefoxAccount.fromDictionary(dictionary) + + // Check to see if the account configuration set is a custom service + // and update it to use the custom servers. + if let configuration = account?.configuration as? CustomFirefoxAccountConfiguration { + account?.configuration = CustomFirefoxAccountConfiguration(prefs: self.prefs) + } + + return account + } + return nil + }() + + func hasAccount() -> Bool { + return account != nil + } + + func hasSyncableAccount() -> Bool { + return account?.actionNeeded == FxAActionNeeded.none + } + + func getAccount() -> FirefoxAccount? { + return account + } + + func removeAccountMetadata() { + self.prefs.removeObjectForKey(PrefsKeys.KeyLastRemoteTabSyncTime) + self.keychain.removeObject(forKey: self.name + ".account") + } + + func removeExistingAuthenticationInfo() { + self.keychain.setAuthenticationInfo(nil) + } + + func removeAccount() { + let old = self.account + removeAccountMetadata() + self.account = nil + + // Tell any observers that our account has changed. + NotificationCenter.default.post(name: NotificationFirefoxAccountChanged, object: nil) + + // Trigger cleanup. Pass in the account in case we want to try to remove + // client-specific data from the server. + self.syncManager.onRemovedAccount(old) + } + + func setAccount(_ account: FirefoxAccount) { + self.account = account + + flushAccount() + + // tell any observers that our account has changed + DispatchQueue.main.async { + // Many of the observers for this notifications are on the main thread, + // so we should post the notification there, just in case we're not already + // on the main thread. + let userInfo = [NotificationUserInfoKeyHasSyncableAccount: self.hasSyncableAccount()] + NotificationCenter.default.post(name: NotificationFirefoxAccountChanged, object: nil, userInfo: userInfo) + } + + self.syncManager.onAddedAccount() + } + + func flushAccount() { + if let account = account { + self.keychain.set(account.dictionary() as NSCoding, forKey: name + ".account", withAccessibility: .afterFirstUnlock) + } + } + + // Extends NSObject so we can use timers. + public class BrowserSyncManager: NSObject, SyncManager, CollectionChangedNotifier { + // We shouldn't live beyond our containing BrowserProfile, either in the main app or in + // an extension. + // But it's possible that we'll finish a side-effect sync after we've ditched the profile + // as a whole, so we hold on to our Prefs, potentially for a little while longer. This is + // safe as a strong reference, because there's no cycle. + unowned fileprivate let profile: BrowserProfile + fileprivate let prefs: Prefs + + let FifteenMinutes = TimeInterval(60 * 15) + let OneMinute = TimeInterval(60) + + fileprivate var syncTimer: Timer? + + fileprivate var backgrounded: Bool = true + public func applicationDidEnterBackground() { + self.backgrounded = true + self.endTimedSyncs() + } + + public func applicationDidBecomeActive() { + self.backgrounded = false + + guard self.profile.hasSyncableAccount() else { + return + } + + self.beginTimedSyncs() + + // Sync now if it's been more than our threshold. + let now = Date.now() + let then = self.lastSyncFinishTime ?? 0 + guard now >= then else { + log.debug("Time was modified since last sync.") + self.syncEverythingSoon() + return + } + let since = now - then + log.debug("\(since)msec since last sync.") + if since > SyncConstants.SyncOnForegroundMinimumDelayMillis { + self.syncEverythingSoon() + } + } + + /** + * Locking is managed by syncSeveral. Make sure you take and release these + * whenever you do anything Sync-ey. + */ + fileprivate let syncLock = NSRecursiveLock() + + public var isSyncing: Bool { + syncLock.lock() + defer { syncLock.unlock() } + return syncDisplayState != nil && syncDisplayState! == .inProgress + } + + public var syncDisplayState: SyncDisplayState? + + // The dispatch queue for coordinating syncing and resetting the database. + fileprivate let syncQueue = DispatchQueue(label: "com.mozilla.firefox.sync") + + fileprivate typealias EngineResults = [(EngineIdentifier, SyncStatus)] + fileprivate typealias EngineTasks = [(EngineIdentifier, SyncFunction)] + + // Used as a task queue for syncing. + fileprivate var syncReducer: AsyncReducer? + + fileprivate func beginSyncing() { + notifySyncing(notification: NotificationProfileDidStartSyncing) + } + + fileprivate func endSyncing(_ result: SyncOperationResult) { + // loop through statuses and fill sync state + syncLock.lock() + defer { syncLock.unlock() } + log.info("Ending all queued syncs.") + + syncDisplayState = SyncStatusResolver(engineResults: result.engineResults).resolveResults() + + #if MOZ_TARGET_CLIENT + if let account = profile.account, canSendUsageData() { + SyncPing.from(result: result, + account: account, + remoteClientsAndTabs: profile.remoteClientsAndTabs, + prefs: prefs, + why: .schedule) >>== { SyncTelemetry.send(ping: $0, docType: .sync) } + } else { + log.debug("Profile isn't sending usage data. Not sending sync status event.") + } + #endif + + // Dont notify if we are performing a sync in the background. This prevents more db access from happening + if !self.backgrounded { + notifySyncing(notification: NotificationProfileDidFinishSyncing) + } + syncReducer = nil + } + + func canSendUsageData() -> Bool { + return profile.prefs.boolForKey(AppConstants.PrefSendUsageData) ?? true + } + + private func notifySyncing(notification: Notification.Name) { + NotificationCenter.default.post(name: notification, object: syncDisplayState?.asObject()) + } + + init(profile: BrowserProfile) { + self.profile = profile + self.prefs = profile.prefs + + super.init() + + let center = NotificationCenter.default + center.addObserver(self, selector: #selector(onDatabaseWasRecreated(notification:)), name: NotificationDatabaseWasRecreated, object: nil) + center.addObserver(self, selector: #selector(onLoginDidChange(_:)), name: NotificationDataLoginDidChange, object: nil) + center.addObserver(self, selector: #selector(onStartSyncing(_:)), name: NotificationProfileDidStartSyncing, object: nil) + center.addObserver(self, selector: #selector(onFinishSyncing(_:)), name: NotificationProfileDidFinishSyncing, object: nil) + center.addObserver(self, selector: #selector(onBookmarkBufferValidated(notification:)), name: NotificationBookmarkBufferValidated, object: nil) + } + + func onBookmarkBufferValidated(notification: NSNotification) { + #if MOZ_TARGET_CLIENT + // We don't send this ad hoc telemetry on the release channel. + guard AppConstants.BuildChannel != AppBuildChannel.release else { + return + } + + guard profile.prefs.boolForKey(AppConstants.PrefSendUsageData) ?? true else { + log.debug("Profile isn't sending usage data. Not sending bookmark event.") + return + } + + guard let validations = (notification.object as? Box<[String: Bool]>)?.value else { + log.warning("Notification didn't have validations.") + return + } + + let attempt: Int32 = self.prefs.intForKey("bookmarkvalidationattempt") ?? 1 + self.prefs.setInt(attempt + 1, forKey: "bookmarkvalidationattempt") + + // Capture the buffer count ASAP, not in the delayed op, because the merge could wipe it! + let bufferRows = (self.profile.bookmarks as? MergedSQLiteBookmarks)?.synchronousBufferCount() + + self.doInBackgroundAfter(300) { + self.profile.remoteClientsAndTabs.getClientGUIDs() >>== { clients in + // We would love to include the version and OS etc. of each remote client, + // but we don't store that information. For now, just do a count. + let clientCount = clients.count + + let id = DeviceInfo.clientIdentifier(self.prefs) + let ping = makeAdHocBookmarkMergePing(Bundle.main, clientID: id, attempt: attempt, bufferRows: bufferRows, valid: validations, clientCount: clientCount) + let payload = ping.stringValue + + log.debug("Payload is: \(payload)") + guard let body = payload.data(using: String.Encoding.utf8) else { + log.debug("Invalid JSON!") + return + } + + guard let url = URL(string: "https://mozilla-anonymous-sync-metrics.moo.mx/post/bookmarkvalidation") else { + return + } + + var request = URLRequest(url: url) + request.httpMethod = "POST" + request.httpBody = body + request.setValue("application/json", forHTTPHeaderField: "Content-Type") + + URLSession.shared.dataTask(with: request) { data, response, error in + let httpResponse = response as? HTTPURLResponse + log.debug("Bookmark validation upload response: \(httpResponse?.statusCode ?? -1).") + } + } + } + #endif + } + + private func handleRecreationOfDatabaseNamed(name: String?) -> Success { + let loginsCollections = ["passwords"] + let browserCollections = ["bookmarks", "history", "tabs"] + + let dbName = name ?? "" + switch dbName { + case "": + return self.locallyResetCollections(loginsCollections + browserCollections) + case "logins.db": + return self.locallyResetCollections(loginsCollections) + case "browser.db": + return self.locallyResetCollections(browserCollections) + default: + log.debug("Unknown database \(dbName).") + return succeed() + } + } + + func doInBackgroundAfter(_ millis: Int64, _ block: @escaping () -> Void) { + let queue = DispatchQueue.global(qos: DispatchQoS.background.qosClass) + //Pretty ambiguous here. I'm thinking .now was DispatchTime.now() and not Date.now() + queue.asyncAfter(deadline: DispatchTime.now() + DispatchTimeInterval.milliseconds(Int(millis)), execute: block) + } + + @objc + func onDatabaseWasRecreated(notification: NSNotification) { + log.debug("Database was recreated.") + let name = notification.object as? String + log.debug("Database was \(name ?? "nil").") + + // We run this in the background after a few hundred milliseconds; + // it doesn't really matter when it runs, so long as it doesn't + // happen in the middle of a sync. + + let resetDatabase = { + return self.handleRecreationOfDatabaseNamed(name: name) >>== { + log.debug("Reset of \(name ?? "nil") done") + } + } + + self.doInBackgroundAfter(300) { + self.syncLock.lock() + defer { self.syncLock.unlock() } + // If we're syncing already, then wait for sync to end, + // then reset the database on the same serial queue. + if let reducer = self.syncReducer, !reducer.isFilled { + reducer.terminal.upon { _ in + self.syncQueue.async(execute: resetDatabase) + } + } else { + // Otherwise, reset the database on the sync queue now + // Sync can't start while this is still going on. + self.syncQueue.async(execute: resetDatabase) + } + } + } + + // Simple in-memory rate limiting. + var lastTriggeredLoginSync: Timestamp = 0 + @objc func onLoginDidChange(_ notification: NSNotification) { + log.debug("Login did change.") + if (Date.now() - lastTriggeredLoginSync) > OneMinuteInMilliseconds { + lastTriggeredLoginSync = Date.now() + + // Give it a few seconds. + // Trigger on the main queue. The bulk of the sync work runs in the background. + let greenLight = self.greenLight() + DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(SyncConstants.SyncDelayTriggered)) { + if greenLight() { + self.syncLogins() + } + } + } + } + + public var lastSyncFinishTime: Timestamp? { + get { + return self.prefs.timestampForKey(PrefsKeys.KeyLastSyncFinishTime) + } + + set(value) { + if let value = value { + self.prefs.setTimestamp(value, forKey: PrefsKeys.KeyLastSyncFinishTime) + } else { + self.prefs.removeObjectForKey(PrefsKeys.KeyLastSyncFinishTime) + } + } + } + + @objc func onStartSyncing(_ notification: NSNotification) { + syncLock.lock() + defer { syncLock.unlock() } + syncDisplayState = .inProgress + } + + @objc func onFinishSyncing(_ notification: NSNotification) { + syncLock.lock() + defer { syncLock.unlock() } + if let syncState = syncDisplayState, syncState == .good { + self.lastSyncFinishTime = Date.now() + } + } + + var prefsForSync: Prefs { + return self.prefs.branch("sync") + } + + public func onAddedAccount() -> Success { + // Only sync if we're green lit. This makes sure that we don't sync unverified accounts. + guard self.profile.hasSyncableAccount() else { return succeed() } + + self.beginTimedSyncs() + return self.syncEverything(why: .didLogin) + } + + func locallyResetCollections(_ collections: [String]) -> Success { + return walk(collections, f: self.locallyResetCollection) + } + + func locallyResetCollection(_ collection: String) -> Success { + switch collection { + case "bookmarks": + return BufferingBookmarksSynchronizer.resetSynchronizerWithStorage(self.profile.bookmarks, basePrefs: self.prefsForSync, collection: "bookmarks") + + case "clients": + fallthrough + case "tabs": + // Because clients and tabs share storage, and thus we wipe data for both if we reset either, + // we reset the prefs for both at the same time. + return TabsSynchronizer.resetClientsAndTabsWithStorage(self.profile.remoteClientsAndTabs, basePrefs: self.prefsForSync) + + case "history": + return HistorySynchronizer.resetSynchronizerWithStorage(self.profile.history, basePrefs: self.prefsForSync, collection: "history") + case "passwords": + return LoginsSynchronizer.resetSynchronizerWithStorage(self.profile.logins, basePrefs: self.prefsForSync, collection: "passwords") + + case "forms": + log.debug("Requested reset for forms, but this client doesn't sync them yet.") + return succeed() + case "addons": + log.debug("Requested reset for addons, but this client doesn't sync them.") + return succeed() + case "prefs": + log.debug("Requested reset for prefs, but this client doesn't sync them.") + return succeed() + default: + log.warning("Asked to reset collection \(collection), which we don't know about.") + return succeed() + } + } + + public func onNewProfile() { + SyncStateMachine.clearStateFromPrefs(self.prefsForSync) + } + + public func onRemovedAccount(_ account: FirefoxAccount?) -> Success { + let profile = self.profile + + // Run these in order, because they might write to the same DB! + let remove = [ + profile.history.onRemovedAccount, + profile.remoteClientsAndTabs.onRemovedAccount, + profile.logins.onRemovedAccount, + profile.bookmarks.onRemovedAccount, + ] + + let clearPrefs: () -> Success = { + withExtendedLifetime(self) { + // Clear prefs after we're done clearing everything else -- just in case + // one of them needs the prefs and we race. Clear regardless of success + // or failure. + + // This will remove keys from the Keychain if they exist, as well + // as wiping the Sync prefs. + SyncStateMachine.clearStateFromPrefs(self.prefsForSync) + } + return succeed() + } + + return accumulate(remove) >>> clearPrefs + } + + fileprivate func repeatingTimerAtInterval(_ interval: TimeInterval, selector: Selector) -> Timer { + return Timer.scheduledTimer(timeInterval: interval, target: self, selector: selector, userInfo: nil, repeats: true) + } + + public func beginTimedSyncs() { + if self.syncTimer != nil { + log.debug("Already running sync timer.") + return + } + + let interval = FifteenMinutes + let selector = #selector(BrowserSyncManager.syncOnTimer) + log.debug("Starting sync timer.") + self.syncTimer = repeatingTimerAtInterval(interval, selector: selector) + } + + /** + * The caller is responsible for calling this on the same thread on which it called + * beginTimedSyncs. + */ + public func endTimedSyncs() { + if let t = self.syncTimer { + log.debug("Stopping sync timer.") + self.syncTimer = nil + t.invalidate() + } + } + + fileprivate func syncClientsWithDelegate(_ delegate: SyncDelegate, prefs: Prefs, ready: Ready, why: SyncReason) -> SyncResult { + log.debug("Syncing clients to storage.") + + let clientSynchronizer = ready.synchronizer(ClientsSynchronizer.self, delegate: delegate, prefs: prefs, why: why) + return clientSynchronizer.synchronizeLocalClients(self.profile.remoteClientsAndTabs, withServer: ready.client, info: ready.info, notifier: self) >>== { result in + guard case .completed = result else { + return deferMaybe(result) + } + guard let account = self.profile.account else { + return deferMaybe(result) + } + log.debug("Updating FxA devices list.") + return account.updateFxADevices(remoteDevices: self.profile.remoteClientsAndTabs).bind { _ in + return deferMaybe(result) + } + } + } + + fileprivate func syncTabsWithDelegate(_ delegate: SyncDelegate, prefs: Prefs, ready: Ready, why: SyncReason) -> SyncResult { + let storage = self.profile.remoteClientsAndTabs + let tabSynchronizer = ready.synchronizer(TabsSynchronizer.self, delegate: delegate, prefs: prefs, why: why) + return tabSynchronizer.synchronizeLocalTabs(storage, withServer: ready.client, info: ready.info) + } + + fileprivate func syncHistoryWithDelegate(_ delegate: SyncDelegate, prefs: Prefs, ready: Ready, why: SyncReason) -> SyncResult { + log.debug("Syncing history to storage.") + let historySynchronizer = ready.synchronizer(HistorySynchronizer.self, delegate: delegate, prefs: prefs, why: why) + return historySynchronizer.synchronizeLocalHistory(self.profile.history, withServer: ready.client, info: ready.info, greenLight: self.greenLight()) + } + + fileprivate func syncLoginsWithDelegate(_ delegate: SyncDelegate, prefs: Prefs, ready: Ready, why: SyncReason) -> SyncResult { + log.debug("Syncing logins to storage.") + let loginsSynchronizer = ready.synchronizer(LoginsSynchronizer.self, delegate: delegate, prefs: prefs, why: why) + return loginsSynchronizer.synchronizeLocalLogins(self.profile.logins, withServer: ready.client, info: ready.info) + } + + fileprivate func mirrorBookmarksWithDelegate(_ delegate: SyncDelegate, prefs: Prefs, ready: Ready, why: SyncReason) -> SyncResult { + log.debug("Synchronizing server bookmarks to storage.") + let bookmarksMirrorer = ready.synchronizer(BufferingBookmarksSynchronizer.self, delegate: delegate, prefs: prefs, why: why) + return bookmarksMirrorer.synchronizeBookmarksToStorage(self.profile.bookmarks, usingBuffer: self.profile.mirrorBookmarks, withServer: ready.client, info: ready.info, greenLight: self.greenLight(), remoteClientsAndTabs: self.profile.remoteClientsAndTabs) + } + + func takeActionsOnEngineStateChanges(_ changes: T) -> Deferred> { + var needReset = Set(changes.collectionsThatNeedLocalReset()) + needReset.formUnion(changes.enginesDisabled()) + needReset.formUnion(changes.enginesEnabled()) + if needReset.isEmpty { + log.debug("No collections need reset. Moving on.") + return deferMaybe(changes) + } + + // needReset needs at most one of clients and tabs, because we reset them + // both if either needs reset. This is strictly an optimization to avoid + // doing duplicate work. + if needReset.contains("clients") { + if needReset.remove("tabs") != nil { + log.debug("Already resetting clients (and tabs); not bothering to also reset tabs again.") + } + } + + return walk(Array(needReset), f: self.locallyResetCollection) + >>> effect(changes.clearLocalCommands) + >>> always(changes) + } + + /** + * Runs the single provided synchronization function and returns its status. + */ + fileprivate func sync(_ label: EngineIdentifier, function: @escaping SyncFunction) -> SyncResult { + return syncSeveral(why: .user, synchronizers: [(label, function)]) >>== { statuses in + let status = statuses.find { label == $0.0 }?.1 + return deferMaybe(status ?? .notStarted(.unknown)) + } + } + + /** + * Convenience method for syncSeveral([(EngineIdentifier, SyncFunction)]) + */ + private func syncSeveral(why: SyncReason, synchronizers: (EngineIdentifier, SyncFunction)...) -> Deferred> { + return syncSeveral(why: why, synchronizers: synchronizers) + } + + /** + * Runs each of the provided synchronization functions with the same inputs. + * Returns an array of IDs and SyncStatuses at least length as the input. + * The statuses returned will be a superset of the ones that are requested here. + * While a sync is ongoing, each engine from successive calls to this method will only be called once. + */ + fileprivate func syncSeveral(why: SyncReason, synchronizers: [(EngineIdentifier, SyncFunction)]) -> Deferred> { + syncLock.lock() + defer { syncLock.unlock() } + + guard let account = self.profile.account else { + log.info("No account to sync with.") + let statuses = synchronizers.map { + ($0.0, SyncStatus.notStarted(.noAccount)) + } + return deferMaybe(statuses) + } + + if !isSyncing { + // A sync isn't already going on, so start another one. + let statsSession = SyncOperationStatsSession(why: why, uid: account.uid, deviceID: account.deviceRegistration?.id) + let reducer = AsyncReducer(initialValue: [], queue: syncQueue) { (statuses, synchronizers) in + let done = Set(statuses.map { $0.0 }) + let remaining = synchronizers.filter { !done.contains($0.0) } + if remaining.isEmpty { + log.info("Nothing left to sync") + return deferMaybe(statuses) + } + + return self.syncWith(synchronizers: remaining, account: account, statsSession: statsSession, why: why) >>== { deferMaybe(statuses + $0) } + } + + reducer.terminal.upon { results in + let result = SyncOperationResult( + engineResults: results, + stats: statsSession.hasStarted() ? statsSession.end() : nil + ) + self.endSyncing(result) + } + + // The actual work of synchronizing doesn't start until we append + // the synchronizers to the reducer below. + self.syncReducer = reducer + self.beginSyncing() + } + + do { + return try syncReducer!.append(synchronizers) + } catch let error { + log.error("Synchronizers appended after sync was finished. This is a bug. \(error)") + let statuses = synchronizers.map { + ($0.0, SyncStatus.notStarted(.unknown)) + } + return deferMaybe(statuses) + } + } + + // This SHOULD NOT be called directly: use syncSeveral instead. + fileprivate func syncWith(synchronizers: [(EngineIdentifier, SyncFunction)], + account: FirefoxAccount, + statsSession: SyncOperationStatsSession, why: SyncReason) -> Deferred> { + log.info("Syncing \(synchronizers.map { $0.0 })") + let authState = account.syncAuthState + let delegate = self.profile.getSyncDelegate() + let readyDeferred = SyncStateMachine(prefs: self.prefsForSync).toReady(authState!) + + let function: (SyncDelegate, Prefs, Ready) -> Deferred> = { delegate, syncPrefs, ready in + let thunks = synchronizers.map { (i, f) in + return { () -> Deferred> in + log.debug("Syncing \(i)…") + return f(delegate, syncPrefs, ready, why) >>== { deferMaybe((i, $0)) } + } + } + return accumulate(thunks) + } + + return readyDeferred >>== self.takeActionsOnEngineStateChanges >>== { ready in + statsSession.start() + return function(delegate, self.prefsForSync, ready) + } + } + + @discardableResult public func syncEverything(why: SyncReason) -> Success { + return self.syncSeveral( + why: why, + synchronizers: + ("clients", self.syncClientsWithDelegate), + ("tabs", self.syncTabsWithDelegate), + ("logins", self.syncLoginsWithDelegate), + ("bookmarks", self.mirrorBookmarksWithDelegate), + ("history", self.syncHistoryWithDelegate)) >>> succeed + } + + func syncEverythingSoon() { + self.doInBackgroundAfter(SyncConstants.SyncOnForegroundAfterMillis) { + log.debug("Running delayed startup sync.") + self.syncEverything(why: .startup) + } + } + + /** + * Allows selective sync of different collections, for use by external APIs. + * Some help is given to callers who use different namespaces (specifically: `passwords` is mapped to `logins`) + * and to preserve some ordering rules. + */ + public func syncNamedCollections(why: SyncReason, names: [String]) -> Success { + // Massage the list of names into engine identifiers. + let engineIdentifiers = names.map { name -> [EngineIdentifier] in + switch name { + case "passwords": + return ["logins"] + case "tabs": + return ["clients", "tabs"] + default: + return [name] + } + }.flatMap { $0 } + + // By this time, `engineIdentifiers` may have duplicates in. We won't try and dedupe here + // because `syncSeveral` will do that for us. + + let synchronizers: [(EngineIdentifier, SyncFunction)] = engineIdentifiers.flatMap { + switch $0 { + case "clients": return ("clients", self.syncClientsWithDelegate) + case "tabs": return ("tabs", self.syncTabsWithDelegate) + case "logins": return ("logins", self.syncLoginsWithDelegate) + case "bookmarks": return ("bookmarks", self.mirrorBookmarksWithDelegate) + case "history": return ("history", self.syncHistoryWithDelegate) + default: return nil + } + } + return self.syncSeveral(why: why, synchronizers: synchronizers) >>> succeed + } + + @objc func syncOnTimer() { + self.syncEverything(why: .scheduled) + } + + public func hasSyncedHistory() -> Deferred> { + return self.profile.history.hasSyncedHistory() + } + + public func hasSyncedLogins() -> Deferred> { + return self.profile.logins.hasSyncedLogins() + } + + public func syncClients() -> SyncResult { + // TODO: recognize .NotStarted. + return self.sync("clients", function: syncClientsWithDelegate) + } + + public func syncClientsThenTabs() -> SyncResult { + return self.syncSeveral( + why: .user, + synchronizers: + ("clients", self.syncClientsWithDelegate), + ("tabs", self.syncTabsWithDelegate)) >>== { statuses in + let status = statuses.find { "tabs" == $0.0 } + return deferMaybe(status!.1) + } + } + + @discardableResult public func syncLogins() -> SyncResult { + return self.sync("logins", function: syncLoginsWithDelegate) + } + + public func syncHistory() -> SyncResult { + // TODO: recognize .NotStarted. + return self.sync("history", function: syncHistoryWithDelegate) + } + + public func mirrorBookmarks() -> SyncResult { + return self.sync("bookmarks", function: mirrorBookmarksWithDelegate) + } + + /** + * Return a thunk that continues to return true so long as an ongoing sync + * should continue. + */ + func greenLight() -> () -> Bool { + let start = Date.now() + + // Give it two minutes to run before we stop. + let stopBy = start + (2 * OneMinuteInMilliseconds) + log.debug("Checking green light. Backgrounded: \(self.backgrounded).") + return { + Date.now() < stopBy && + self.profile.hasSyncableAccount() + } + } + + class NoAccountError: MaybeErrorType { + var description = "No account." + } + + public func notify(deviceIDs: [GUID], collectionsChanged collections: [String], reason: String) -> Success { + guard let account = self.profile.account else { + return deferMaybe(NoAccountError()) + } + return account.notify(deviceIDs: deviceIDs, collectionsChanged: collections, reason: reason) + } + + public func notifyAll(collectionsChanged collections: [String], reason: String) -> Success { + guard let account = self.profile.account else { + return deferMaybe(NoAccountError()) + } + return account.notifyAll(collectionsChanged: collections, reason: reason) + } + } +} diff --git a/mobile/ios/Providers/SyncStatusResolver.swift b/mobile/ios/Providers/SyncStatusResolver.swift new file mode 100644 index 0000000000..fa8abff483 --- /dev/null +++ b/mobile/ios/Providers/SyncStatusResolver.swift @@ -0,0 +1,131 @@ +/* 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 Sync +import Shared +import Storage + +public enum SyncDisplayState { + case inProgress + case good + case bad(message: String?) + case warning(message: String) + + func asObject() -> [String: String]? { + switch self { + case .bad(let msg): + guard let message = msg else { + return ["state": "Error"] + } + return ["state": "Error", + "message": message] + case .warning(let message): + return ["state": "Warning", + "message": message] + default: + break + } + return nil + } +} + +public func ==(a: SyncDisplayState, b: SyncDisplayState) -> Bool { + switch (a, b) { + case (.inProgress, .inProgress): + return true + case (.good, .good): + return true + case (.bad(let a), .bad(let b)) where a == b: + return true + case (.warning(let a), .warning(let b)) where a == b: + return true + default: + return false + } +} + +/* + * Translates the fine-grained SyncStatuses of each sync engine into a more coarse-grained + * display-oriented state for displaying warnings/errors to the user. + */ +public struct SyncStatusResolver { + + let engineResults: Maybe + + public func resolveResults() -> SyncDisplayState { + guard let results = engineResults.successValue else { + switch engineResults.failureValue { + case _ as BookmarksMergeError, _ as BufferInvalidError: + return SyncDisplayState.warning(message: String(format: Strings.FirefoxSyncPartialTitle, Strings.localizedStringForSyncComponent("bookmarks") ?? "")) + default: + return SyncDisplayState.bad(message: nil) + } + } + + // Run through the engine results and produce a relevant display status for each one + let displayStates: [SyncDisplayState] = results.map { (engineIdentifier, syncStatus) in + print("Sync status for \(engineIdentifier): \(syncStatus)") + + // Explicitly call out each of the enum cases to let us lean on the compiler when + // we add new error states + switch syncStatus { + case .notStarted(let reason): + switch reason { + case .offline: + return .bad(message: Strings.FirefoxSyncOfflineTitle) + case .noAccount: + return .warning(message: Strings.FirefoxSyncOfflineTitle) + case .backoff(_): + return .good + case .engineRemotelyNotEnabled(_): + return .good + case .engineFormatOutdated(_): + return .good + case .engineFormatTooNew(_): + return .good + case .storageFormatOutdated(_): + return .good + case .storageFormatTooNew(_): + return .good + case .stateMachineNotReady: + return .good + case .redLight: + return .good + case .unknown: + return .good + } + case .completed: + return .good + case .partial: + return .good + } + } + + // TODO: Instead of finding the worst offender in a list of statuses, we should better surface + // what might have happened with a particular engine when syncing. + let aggregate: SyncDisplayState = displayStates.reduce(.good) { carried, displayState in + switch displayState { + + case .bad(_): + return displayState + + case .warning(_): + // If the state we're carrying is worse than the stale one, keep passing + // along the worst one + switch carried { + case .bad(_): + return carried + default: + return displayState + } + default: + // This one is good so just pass on what was being carried + return carried + } + } + + print("Resolved sync display state: \(aggregate)") + return aggregate + } +} diff --git a/mobile/ios/Push/PushClient.swift b/mobile/ios/Push/PushClient.swift new file mode 100644 index 0000000000..1d0b0359f9 --- /dev/null +++ b/mobile/ios/Push/PushClient.swift @@ -0,0 +1,187 @@ +/* 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 Alamofire +import Deferred +import Shared +import SwiftyJSON + +//public struct PushRemoteError { +// static let MissingNecessaryCryptoKeys: Int32 = 101 +// static let InvalidURLEndpoint: Int32 = 102 +// static let ExpiredURLEndpoint: Int32 = 103 +// static let DataPayloadTooLarge: Int32 = 104 +// static let EndpointBecameUnavailable: Int32 = 105 +// static let InvalidSubscription: Int32 = 106 +// static let RouterTypeIsInvalid: Int32 = 108 +// static let InvalidAuthentication: Int32 = 109 +// static let InvalidCryptoKeysSpecified: Int32 = 110 +// static let MissingRequiredHeader: Int32 = 111 +// static let InvalidTTLHeaderValue: Int32 = 112 +// static let UnknownError: Int32 = 999 +//} + +public let PushClientErrorDomain = "org.mozilla.push.error" +private let PushClientUnknownError = NSError(domain: PushClientErrorDomain, code: 999, + userInfo: [NSLocalizedDescriptionKey: "Invalid server response"]) +private let log = Logger.browserLogger + +/// Bug 1364403 – This is to be put into the push registration +private let apsEnvironment: [String: Any] = [ + "mutable-content": 1, + "alert": [ + "title": " ", + "body": " " + ], +] + +public struct PushRemoteError { + let code: Int + let errno: Int + let error: String + let message: String? + + public static func from(json: JSON) -> PushRemoteError? { + guard let code = json["code"].int, + let errno = json["errno"].int, + let error = json["error"].string else { + return nil + } + + let message = json["message"].string + return PushRemoteError(code: code, errno: errno, error: error, message: message) + } +} + +public enum PushClientError: MaybeErrorType { + case Remote(PushRemoteError) + case Local(Error) + + public var description: String { + switch self { + case let .Remote(error): + let errorString = error.error + let messageString = error.message ?? "" + return "" + case let .Local(error): + return "" + } + } +} + +public class PushClient { + let endpointURL: NSURL + let experimentalMode: Bool + + lazy fileprivate var alamofire: SessionManager = { + let ua = UserAgent.fxaUserAgent + let configuration = URLSessionConfiguration.ephemeral + var defaultHeaders = SessionManager.default.session.configuration.httpAdditionalHeaders ?? [:] + defaultHeaders["User-Agent"] = ua + configuration.httpAdditionalHeaders = defaultHeaders + return SessionManager(configuration: configuration) + }() + + public init(endpointURL: NSURL, experimentalMode: Bool = false) { + self.endpointURL = endpointURL + self.experimentalMode = experimentalMode + } +} + +public extension PushClient { + public func register(_ apnsToken: String) -> Deferred> { + // POST /v1/{type}/{app_id}/registration + let registerURL = endpointURL.appendingPathComponent("registration")! + + var mutableURLRequest = URLRequest(url: registerURL) + mutableURLRequest.httpMethod = HTTPMethod.post.rawValue + + mutableURLRequest.setValue("application/json", forHTTPHeaderField: "Content-Type") + let parameters: [String: Any] + if experimentalMode { + parameters = [ + "token": apnsToken, + "aps": apsEnvironment, + ] + } else { + parameters = ["token": apnsToken] + } + + mutableURLRequest.httpBody = JSON(parameters).stringValue()?.utf8EncodedData + + if experimentalMode { + log.info("curl -X POST \(registerURL.absoluteString) --data '\(JSON(parameters).stringValue()!)'") + } + + return send(request: mutableURLRequest) >>== { json in + guard let response = PushRegistration.from(json: json) else { + return deferMaybe(PushClientError.Local(PushClientUnknownError)) + } + + return deferMaybe(response) + } + } + + public func updateUAID(_ apnsToken: String, withRegistration creds: PushRegistration) -> Deferred> { + // PUT /v1/{type}/{app_id}/registration/{uaid} + let registerURL = endpointURL.appendingPathComponent("registration/\(creds.uaid)")! + var mutableURLRequest = URLRequest(url: registerURL) + + mutableURLRequest.httpMethod = HTTPMethod.put.rawValue + mutableURLRequest.addValue("Bearer \(creds.secret)", forHTTPHeaderField: "Authorization") + + mutableURLRequest.setValue("application/json", forHTTPHeaderField: "Content-Type") + let parameters = ["token": apnsToken] + mutableURLRequest.httpBody = JSON(parameters).stringValue()?.utf8EncodedData + + return send(request: mutableURLRequest) >>== { json in + return deferMaybe(creds) + } + } + + public func unregister(_ creds: PushRegistration) -> Success { + // DELETE /v1/{type}/{app_id}/registration/{uaid} + let unregisterURL = endpointURL.appendingPathComponent("registration/\(creds.uaid)") + + var mutableURLRequest = URLRequest(url: unregisterURL!) + mutableURLRequest.httpMethod = HTTPMethod.delete.rawValue + mutableURLRequest.addValue("Bearer \(creds.secret)", forHTTPHeaderField: "Authorization") + + return send(request: mutableURLRequest) >>> succeed + } +} + +/// Utilities +extension PushClient { + fileprivate func send(request: URLRequest) -> Deferred> { + log.info("\(request.httpMethod!) \(request.url?.absoluteString ?? "nil")") + let deferred = Deferred>() + alamofire.request(request) + .validate(contentType: ["application/json"]) + .responseJSON { response in + // Don't cancel requests just because our client is deallocated. + withExtendedLifetime(self.alamofire) { + let result = response.result + if let error = result.error { + return deferred.fill(Maybe(failure: PushClientError.Local(error))) + } + + guard let data = response.data else { + return deferred.fill(Maybe(failure: PushClientError.Local(PushClientUnknownError))) + } + + let json = JSON(data: data) + + if let remoteError = PushRemoteError.from(json: json) { + return deferred.fill(Maybe(failure: PushClientError.Remote(remoteError))) + } + + deferred.fill(Maybe(success: json)) + } + } + + return deferred + } +} diff --git a/mobile/ios/Push/PushConfiguration.swift b/mobile/ios/Push/PushConfiguration.swift new file mode 100644 index 0000000000..3ce5cb555b --- /dev/null +++ b/mobile/ios/Push/PushConfiguration.swift @@ -0,0 +1,82 @@ +/* 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 + +public enum PushConfigurationLabel: String { + case fennec = "Fennec" + case fennecEnterprise = "FennecEnterprise" + case firefoxBeta = "FirefoxBeta" + case firefoxNightlyEnterprise = "FirefoxNightly" + case firefox = "Firefox" + + public func toConfiguration() -> PushConfiguration { + switch self { + case .fennec: return FennecPushConfiguration() + case .fennecEnterprise: return FennecEnterprisePushConfiguration() + case .firefoxBeta: return FirefoxBetaPushConfiguration() + case .firefoxNightlyEnterprise: return FirefoxNightlyEnterprisePushConfiguration() + case .firefox: return FirefoxPushConfiguration() + } + } +} + +public protocol PushConfiguration { + var label: PushConfigurationLabel { get } + + /// The associated autopush server should speak the protocol documented at + /// http://autopush.readthedocs.io/en/latest/http.html#push-service-http-api + /// /v1/{type}/{app_id} + /// type == apns + /// app_id == the “platform” or “channel” of development (e.g. “firefox”, “beta”, “gecko”, etc.) + var endpointURL: NSURL { get } +} + +public struct FennecPushConfiguration: PushConfiguration { + public init() {} + public let label = PushConfigurationLabel.fennec + public let endpointURL = NSURL(string: "https://updates.push.services.mozilla.com/v1/apns/fennec")! +} + +public struct FennecStagingPushConfiguration: PushConfiguration { + public init() {} + public let label = PushConfigurationLabel.fennec + public let endpointURL = NSURL(string: "https://updates-autopush.stage.mozaws.net/v1/apns/fennec")! +} + +public struct FennecEnterprisePushConfiguration: PushConfiguration { + public init() {} + public let label = PushConfigurationLabel.fennecEnterprise + public let endpointURL = NSURL(string: "https://updates.push.services.mozilla.com/v1/apns/fennecenterprise")! +} + +public struct FirefoxBetaPushConfiguration: PushConfiguration { + public init() {} + public let label = PushConfigurationLabel.firefoxBeta + public let endpointURL = NSURL(string: "https://updates.push.services.mozilla.com/v1/apns/firefoxbeta")! +} + +public struct FirefoxBetaStagingPushConfiguration: PushConfiguration { + public init() {} + public let label = PushConfigurationLabel.firefoxBeta + public let endpointURL = NSURL(string: "https://updates-autopush.stage.mozaws.net/v1/apns/firefoxbeta")! +} + +public struct FirefoxNightlyEnterprisePushConfiguration: PushConfiguration { + public init() {} + public let label = PushConfigurationLabel.firefoxNightlyEnterprise + public let endpointURL = NSURL(string: "https://updates.push.services.mozilla.com/v1/apns/firefoxnightlyenterprise")! +} + +public struct FirefoxPushConfiguration: PushConfiguration { + public init() {} + public let label = PushConfigurationLabel.firefox + public let endpointURL = NSURL(string: "https://updates.push.services.mozilla.com/v1/apns/firefox")! +} + +public struct FirefoxStagingPushConfiguration: PushConfiguration { + public init() {} + public let label = PushConfigurationLabel.firefox + public let endpointURL = NSURL(string: "https://updates-autopush.stage.mozaws.net/v1/apns/firefox")! +} diff --git a/mobile/ios/Push/PushCrypto.swift b/mobile/ios/Push/PushCrypto.swift new file mode 100644 index 0000000000..541a770546 --- /dev/null +++ b/mobile/ios/Push/PushCrypto.swift @@ -0,0 +1,335 @@ +/* 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 FxA + +/// Class to wrap ecec which does the encryption, decryption and key generation with OpenSSL. +/// This supports aesgcm and the newer aes128gcm. +/// For each standard of decryption, two methods are supplied: one with Data parameters and return value, +/// and one with a String based one. +class PushCrypto { + // Stateless, we provide a singleton for convenience. + open static var sharedInstance = PushCrypto() +} + +// AES128GCM +extension PushCrypto { + func aes128gcm(payload data: String, decryptWith privateKey: String, authenticateWith authKey: String) throws -> String { + guard let authSecret = authKey.base64urlSafeDecodedData, + let rawRecvPrivKey = privateKey.base64urlSafeDecodedData, + let payload = data.base64urlSafeDecodedData else { + throw PushCryptoError.base64DecodeError + } + + let decrypted = try aes128gcm(payload: payload, + decryptWith: rawRecvPrivKey, + authenticateWith: authSecret) + + guard let plaintext = decrypted.utf8EncodedString else { + throw PushCryptoError.utf8EncodingError + } + + return plaintext + } + + func aes128gcm(payload: Data, decryptWith rawRecvPrivKey: Data, authenticateWith authSecret: Data) throws -> Data { + var plaintextLen = ece_aes128gcm_plaintext_max_length(payload.getBytes(), payload.count) + 1 + var plaintext = [UInt8](repeating: 0, count: plaintextLen) + + let err = ece_webpush_aes128gcm_decrypt( + rawRecvPrivKey.getBytes(), rawRecvPrivKey.count, + authSecret.getBytes(), authSecret.count, + payload.getBytes(), payload.count, + &plaintext, &plaintextLen) + + if err != ECE_OK { + throw PushCryptoError.decryptionError(errCode: err) + } + + return Data(bytes: plaintext, count: plaintextLen) + } + + func aes128gcm(plaintext: String, encryptWith rawRecvPubKey: String, authenticateWith authSecret: String, rs: Int, padLen: Int) throws -> String { + guard let rawRecvPubKey = rawRecvPubKey.base64urlSafeDecodedData, + let authSecret = authSecret.base64urlSafeDecodedData else { + throw PushCryptoError.base64DecodeError + } + + let plaintextData = plaintext.utf8EncodedData + + let payloadData = try aes128gcm(plaintext: plaintextData, + encryptWith: rawRecvPubKey, + authenticateWith: authSecret, + rs: rs, padLen: padLen) + + guard let payload = payloadData.base64urlSafeEncodedString else { + throw PushCryptoError.base64EncodeError + } + + return payload + } + + func aes128gcm(plaintext: Data, encryptWith rawRecvPubKey: Data, authenticateWith authSecret: Data, rs rsInt: Int, padLen: Int) throws -> Data { + let rs = UInt32(rsInt) + + // rs needs to be >= 18. + assert(rsInt >= Int(ECE_AES128GCM_MIN_RS)) + var payloadLen = ece_aes128gcm_payload_max_length(rs, padLen, plaintext.count) + 1 + var payload = [UInt8](repeating: 0, count: payloadLen) + + let err = ece_webpush_aes128gcm_encrypt(rawRecvPubKey.getBytes(), rawRecvPubKey.count, + authSecret.getBytes(), authSecret.count, + rs, padLen, + plaintext.getBytes(), plaintext.count, + &payload, &payloadLen) + if err != ECE_OK { + throw PushCryptoError.encryptionError(errCode: err) + } + + return Data(bytes: payload, count: payloadLen) + } +} + +// AESGCM +extension PushCrypto { + func aesgcm(ciphertext data: String, withHeaders headers: PushCryptoHeaders, decryptWith privateKey: String, authenticateWith authKey: String) throws -> String { + guard let authSecret = authKey.base64urlSafeDecodedData, + let rawRecvPrivKey = privateKey.base64urlSafeDecodedData, + let ciphertext = data.base64urlSafeDecodedData else { + throw PushCryptoError.base64DecodeError + } + + let decrypted = try aesgcm(ciphertext: ciphertext, + withHeaders: headers, + decryptWith: rawRecvPrivKey, + authenticateWith: authSecret) + + guard let plaintext = decrypted.utf8EncodedString else { + throw PushCryptoError.utf8EncodingError + } + + return plaintext + } + + func aesgcm(ciphertext: Data, withHeaders headers: PushCryptoHeaders, decryptWith rawRecvPrivKey: Data, authenticateWith authSecret: Data) throws -> Data { + let saltLength = Int(ECE_SALT_LENGTH) + var salt = [UInt8](repeating: 0, count: saltLength) + + let rawSenderPubKeyLength = Int(ECE_WEBPUSH_PUBLIC_KEY_LENGTH) + var rawSenderPubKey = [UInt8](repeating: 0, count: rawSenderPubKeyLength) + + var rs = UInt32(0) + + let paramsErr = ece_webpush_aesgcm_headers_extract_params( + headers.cryptoKey, headers.encryption, + &salt, saltLength, + &rawSenderPubKey, rawSenderPubKeyLength, + &rs) + if paramsErr != ECE_OK { + throw PushCryptoError.decryptionError(errCode: paramsErr) + } + + var plaintextLen = ece_aesgcm_plaintext_max_length(rs, ciphertext.count) + 1 + var plaintext = [UInt8](repeating: 0, count: plaintextLen) + + let decryptErr = ece_webpush_aesgcm_decrypt( + rawRecvPrivKey.getBytes(), rawRecvPrivKey.count, + authSecret.getBytes(), authSecret.count, + &salt, salt.count, + &rawSenderPubKey, rawSenderPubKey.count, + rs, + ciphertext.getBytes(), ciphertext.count, + &plaintext, &plaintextLen) + + if decryptErr != ECE_OK { + throw PushCryptoError.decryptionError(errCode: decryptErr) + } + + return Data(bytes: plaintext, count: plaintextLen) + } + + func aesgcm(plaintext: String, encryptWith rawRecvPubKey: String, authenticateWith authSecret: String, rs: Int, padLen: Int) throws -> (headers: PushCryptoHeaders, ciphertext: String) { + guard let rawRecvPubKey = rawRecvPubKey.base64urlSafeDecodedData, + let authSecret = authSecret.base64urlSafeDecodedData else { + throw PushCryptoError.base64DecodeError + } + + let plaintextData = plaintext.utf8EncodedData + + let (headers, messageData) = try aesgcm( + plaintext: plaintextData, + encryptWith: rawRecvPubKey, + authenticateWith: authSecret, + rs: rs, padLen: padLen) + + guard let message = messageData.base64urlSafeEncodedString else { + throw PushCryptoError.base64EncodeError + } + + return (headers, message) + } + + func aesgcm(plaintext: Data, encryptWith rawRecvPubKey: Data, authenticateWith authSecret: Data, rs rsInt: Int, padLen: Int) throws -> (headers: PushCryptoHeaders, data: Data) { + let rs = UInt32(rsInt) + + // rs needs to be >= 3. + assert(rsInt >= Int(ECE_AESGCM_MIN_RS)) + var ciphertextLength = ece_aesgcm_ciphertext_max_length(rs, padLen, plaintext.count) + 1 + var ciphertext = [UInt8](repeating: 0, count: ciphertextLength) + + let saltLength = Int(ECE_SALT_LENGTH) + var salt = [UInt8](repeating: 0, count: saltLength) + + let rawSenderPubKeyLength = Int(ECE_WEBPUSH_PUBLIC_KEY_LENGTH) + var rawSenderPubKey = [UInt8](repeating: 0, count: rawSenderPubKeyLength) + + let encryptErr = ece_webpush_aesgcm_encrypt(rawRecvPubKey.getBytes(), rawRecvPubKey.count, + authSecret.getBytes(), authSecret.count, + rs, padLen, + plaintext.getBytes(), plaintext.count, + &salt, saltLength, + &rawSenderPubKey, rawSenderPubKeyLength, + &ciphertext, &ciphertextLength) + if encryptErr != ECE_OK { + throw PushCryptoError.encryptionError(errCode: encryptErr) + } + + var cryptoKeyHeaderLength = 0 + var encryptionHeaderLength = 0 + + let paramsSizeErr = ece_webpush_aesgcm_headers_from_params( + salt, saltLength, + rawSenderPubKey, rawSenderPubKeyLength, + rs, + nil, &cryptoKeyHeaderLength, + nil, &encryptionHeaderLength) + if paramsSizeErr != ECE_OK { + throw PushCryptoError.encryptionError(errCode: paramsSizeErr) + } + + var cryptoKeyHeaderBytes = [CChar](repeating: 0, count: cryptoKeyHeaderLength) + var encryptionHeaderBytes = [CChar](repeating: 0, count: encryptionHeaderLength) + + let paramsErr = ece_webpush_aesgcm_headers_from_params( + salt, saltLength, + rawSenderPubKey, rawSenderPubKeyLength, + rs, + &cryptoKeyHeaderBytes, &cryptoKeyHeaderLength, + &encryptionHeaderBytes, &encryptionHeaderLength) + if paramsErr != ECE_OK { + throw PushCryptoError.encryptionError(errCode: paramsErr) + } + + guard let cryptoKeyHeader = String(data: Data(bytes: cryptoKeyHeaderBytes, count: cryptoKeyHeaderLength), + encoding: .ascii), + let encryptionHeader = String(data: Data(bytes: encryptionHeaderBytes, count: encryptionHeaderLength), + encoding: .ascii) else { + throw PushCryptoError.base64EncodeError + } + + let headers = PushCryptoHeaders(encryption: encryptionHeader, cryptoKey: cryptoKeyHeader) + return (headers, Data(bytes: ciphertext, count: ciphertextLength)) + } +} + +extension PushCrypto { + func generateKeys() throws -> PushKeys { + // The subscription private key. This key should never be sent to the app + // server. It should be persisted with the endpoint and auth secret, and used + // to decrypt all messages sent to the subscription. + let privateKeyLength = Int(ECE_WEBPUSH_PRIVATE_KEY_LENGTH) + var rawRecvPrivKey = [UInt8](repeating: 0, count: privateKeyLength) + + // The subscription public key. This key should be sent to the app server, + // and used to encrypt messages. The Push DOM API exposes the public key via + // `pushSubscription.getKey("p256dh")`. + let publicKeyLength = Int(ECE_WEBPUSH_PUBLIC_KEY_LENGTH) + var rawRecvPubKey = [UInt8](repeating: 0, count: publicKeyLength) + + // The shared auth secret. This secret should be persisted with the + // subscription information, and sent to the app server. The DOM API exposes + // the auth secret via `pushSubscription.getKey("auth")`. + let authSecretLength = Int(ECE_WEBPUSH_AUTH_SECRET_LENGTH) + var authSecret = [UInt8](repeating: 0, count: authSecretLength) + + let err = ece_webpush_generate_keys( + &rawRecvPrivKey, privateKeyLength, + &rawRecvPubKey, publicKeyLength, + &authSecret, authSecretLength) + + if err != ECE_OK { + throw PushCryptoError.keyGenerationError(errCode: err) + } + + guard let privKey = Data(bytes: rawRecvPrivKey, count: privateKeyLength).base64urlSafeEncodedString, + let pubKey = Data(bytes: rawRecvPubKey, count: publicKeyLength).base64urlSafeEncodedString, + let authKey = Data(bytes: authSecret, count: authSecretLength).base64urlSafeEncodedString else { + throw PushCryptoError.base64EncodeError + } + + return PushKeys(p256dhPrivateKey: privKey, p256dhPublicKey: pubKey, auth: authKey) + } +} + +struct PushKeys { + let p256dhPrivateKey: String + let p256dhPublicKey: String + let auth: String +} + +enum PushCryptoError: Error { + case base64DecodeError + case base64EncodeError + case decryptionError(errCode: Int32) + case encryptionError(errCode: Int32) + case keyGenerationError(errCode: Int32) + case utf8EncodingError +} + +struct PushCryptoHeaders { + let encryption: String + let cryptoKey: String +} + +extension String { + /// Returns a base64 url safe decoding of the given string. + /// The string is allowed to be padded + /// What is padding?: http://stackoverflow.com/a/26632221 + var base64urlSafeDecodedData: Data? { + // We call this method twice: once with the last two args as nil, 0 – this gets us the length + // of the decoded string. + let length = ece_base64url_decode(self, self.characters.count, ECE_BASE64URL_REJECT_PADDING, nil, 0) + guard length > 0 else { + return nil + } + + // The second time, we actually decode, and copy it into a made to measure byte array. + var bytes = [UInt8](repeating: 0, count: length) + let checkLength = ece_base64url_decode(self, self.characters.count, ECE_BASE64URL_REJECT_PADDING, &bytes, length) + guard checkLength == length else { + return nil + } + + return Data(bytes: bytes, count: length) + } +} + +extension Data { + /// Returns a base64 url safe encoding of the given data. + var base64urlSafeEncodedString: String? { + let length = ece_base64url_encode(self.getBytes(), self.count, ECE_BASE64URL_OMIT_PADDING, nil, 0) + guard length > 0 else { + return nil + } + + var bytes = [CChar](repeating: 0, count: length) + let checkLength = ece_base64url_encode(self.getBytes(), self.count, ECE_BASE64URL_OMIT_PADDING, &bytes, length) + guard checkLength == length else { + return nil + } + + return String(data: Data(bytes: bytes, count: length), encoding: .ascii) + } +} diff --git a/mobile/ios/Push/PushRegistration.swift b/mobile/ios/Push/PushRegistration.swift new file mode 100644 index 0000000000..4919a6a7d9 --- /dev/null +++ b/mobile/ios/Push/PushRegistration.swift @@ -0,0 +1,129 @@ +/* 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 FxA +import Shared +import SwiftyJSON + +public class PushRegistration: NSObject, NSCoding { + let uaid: String + let secret: String + // We don't need to have more than one subscription until WebPush is exposed to content Javascript + // however, if/when we do, it'll make migrating easier if we have been serializing it like this all along. + fileprivate var subscriptions: [String: PushSubscription] + + public var defaultSubscription: PushSubscription { + return subscriptions[defaultSubscriptionID]! + } + + public init(uaid: String, secret: String, subscriptions: [String: PushSubscription] = [:]) { + self.uaid = uaid + self.secret = secret + + self.subscriptions = subscriptions + } + + public convenience init(uaid: String, secret: String, subscription: PushSubscription) { + self.init(uaid: uaid, secret: secret, subscriptions: [defaultSubscriptionID: subscription]) + } + + @objc public convenience required init?(coder aDecoder: NSCoder) { + guard let uaid = aDecoder.decodeObject(forKey: "uaid") as? String, + let secret = aDecoder.decodeObject(forKey: "secret") as? String, + let subscriptions = aDecoder.decodeObject(forKey: "subscriptions") as? [String: PushSubscription] else { + fatalError("Cannot decode registration") + } + self.init(uaid: uaid, secret: secret, subscriptions: subscriptions) + } + + @objc public func encode(with aCoder: NSCoder) { + aCoder.encode(uaid, forKey: "uaid") + aCoder.encode(secret, forKey: "secret") + aCoder.encode(subscriptions, forKey: "subscriptions") + } + + public static func from(json: JSON) -> PushRegistration? { + guard let endpointString = json["endpoint"].stringValue(), + let endpoint = URL(string: endpointString), + let secret = json["secret"].stringValue(), + let uaid = json["uaid"].stringValue(), + let channelID = json["channelID"].stringValue() else { + return nil + } + guard let defaultSubscription = try? PushSubscription(channelID: channelID, endpoint: endpoint) else { + return nil + } + return PushRegistration(uaid: uaid, secret: secret, subscriptions: [defaultSubscriptionID: defaultSubscription]) + } +} + +fileprivate let defaultSubscriptionID = "defaultSubscription" +/// Small NSCodable class for persisting a channel subscription. +/// We use NSCoder because we expect it to be stored in the profile. +public class PushSubscription: NSObject, NSCoding { + let channelID: String + let endpoint: URL + + let p256dhPublicKey: String + let p256dhPrivateKey: String + let authKey: String + + init(channelID: String, endpoint: URL, p256dhPrivateKey: String, p256dhPublicKey: String, authKey: String) { + self.channelID = channelID + self.endpoint = endpoint + self.p256dhPrivateKey = p256dhPrivateKey + self.p256dhPublicKey = p256dhPublicKey + self.authKey = authKey + } + + convenience init(channelID: String, endpoint: URL, keys: PushKeys) { + self.init(channelID: channelID, + endpoint: endpoint, + p256dhPrivateKey: keys.p256dhPrivateKey, + p256dhPublicKey: keys.p256dhPublicKey, + authKey: keys.auth) + } + + convenience init(channelID: String, endpoint: URL) throws { + let keys = try PushCrypto.sharedInstance.generateKeys() + self.init(channelID: channelID, endpoint: endpoint, keys: keys) + } + + @objc public convenience required init?(coder aDecoder: NSCoder) { + guard let channelID = aDecoder.decodeObject(forKey: "channelID") as? String, + let urlString = aDecoder.decodeObject(forKey: "endpoint") as? String, + let endpoint = URL(string: urlString), + let p256dhPrivateKey = aDecoder.decodeObject(forKey: "p256dhPrivateKey") as? String, + let p256dhPublicKey = aDecoder.decodeObject(forKey: "p256dhPublicKey") as? String, + let authKey = aDecoder.decodeObject(forKey: "authKey") as? String else { + return nil + } + + self.init(channelID: channelID, + endpoint: endpoint, + p256dhPrivateKey: p256dhPrivateKey, + p256dhPublicKey: p256dhPublicKey, + authKey: authKey) + } + + @objc public func encode(with aCoder: NSCoder) { + aCoder.encode(channelID, forKey: "channelID") + aCoder.encode(endpoint.absoluteString, forKey: "endpoint") + aCoder.encode(p256dhPrivateKey, forKey: "p256dhPrivateKey") + aCoder.encode(p256dhPublicKey, forKey: "p256dhPublicKey") + aCoder.encode(authKey, forKey: "authKey") + } +} + +public extension PushSubscription { + public func aesgcm(payload: String, encryptionHeader: String, cryptoHeader: String) -> String? { + let headers = PushCryptoHeaders(encryption: encryptionHeader, cryptoKey: cryptoHeader) + return try? PushCrypto.sharedInstance.aesgcm(ciphertext: payload, withHeaders: headers, decryptWith: p256dhPrivateKey, authenticateWith: authKey) + } + + public func aes128gcm(payload: String) -> String? { + return try? PushCrypto.sharedInstance.aes128gcm(payload: payload, decryptWith: p256dhPrivateKey, authenticateWith: authKey) + } +} diff --git a/mobile/ios/PushTests/LivePushClientTests.swift b/mobile/ios/PushTests/LivePushClientTests.swift new file mode 100644 index 0000000000..60e476be22 --- /dev/null +++ b/mobile/ios/PushTests/LivePushClientTests.swift @@ -0,0 +1,60 @@ +/* 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/. */ + +@testable import Account +import Foundation +import Shared +import XCTest + +class LivePushClientTests: XCTestCase { + + var endpointURL: NSURL { + return FennecPushConfiguration().endpointURL + } + + func generateDeviceID() -> String { + let num = arc4random_uniform(1 << 31) + return "test-id-deadbeef-\(num)" + } + + func testClientRegistration() { + let deviceID = generateDeviceID() + let client = PushClient(endpointURL: endpointURL) + + let registrationExpectation = XCTestExpectation() + let unregistrationExpectation = XCTestExpectation() + + client.register(deviceID) >>== { registration in + registrationExpectation.fulfill() + client.unregister(registration) >>== { res in + unregistrationExpectation.fulfill() + } + } + + wait(for: [registrationExpectation, unregistrationExpectation], timeout: 2 * 60) + } + + func testClientUpdate() { + let client = PushClient(endpointURL: endpointURL) + + let registrationExpectation = XCTestExpectation() + let updateExpectation = XCTestExpectation() + let unregistrationExpectation = XCTestExpectation() + + client.register(generateDeviceID()) >>== { ogRegistration in + registrationExpectation.fulfill() + + client.updateUAID(self.generateDeviceID(), withRegistration: ogRegistration) >>== { registration in + updateExpectation.fulfill() + XCTAssertEqual(ogRegistration, registration) + + client.unregister(registration) >>== { res in + unregistrationExpectation.fulfill() + } + } + } + + wait(for: [registrationExpectation, updateExpectation, unregistrationExpectation], timeout: 2 * 60) + } +} diff --git a/mobile/ios/PushTests/PushCryptoTests.swift b/mobile/ios/PushTests/PushCryptoTests.swift new file mode 100644 index 0000000000..3a737651f4 --- /dev/null +++ b/mobile/ios/PushTests/PushCryptoTests.swift @@ -0,0 +1,258 @@ +/* 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/. */ + +@testable import Account +import XCTest + +class PushCryptoTests: XCTestCase { + + let push = PushCrypto.sharedInstance + + func testRoundtrip_base64urlSafe() { + let ogString = "I am the walrus! I am the eggman!" + let ogData = ogString.data(using: .utf8)! + + guard let encodedString = ogData.base64urlSafeEncodedString else { + return XCTFail("base64urlSafeEncodedString failed") + } + + guard let decodedData = encodedString.base64urlSafeDecodedData else { + return XCTFail("base64urlSafeDecodedData failed") + } + + guard let decodedString = decodedData.utf8EncodedString else { + return XCTFail("utf8EncodedString failed") + } + + XCTAssertEqual(decodedString, ogString) + } + + func testRoundtrip_base64urlSafe_withSpecialChars() { + let encoded = "mW-ti1CqLQK4PyZBKy4q7g" + + XCTAssertEqual(encoded.base64urlSafeDecodedData!.base64urlSafeEncodedString, encoded) + } + + struct Aes128GcmTest { + let payload: String + let recvPrivKey: String + let authSecret: String + let plaintext: String + } + + func testDecrypt_aes128gcm() { + let tests = [ + Aes128GcmTest( + payload: "SVzmyN6TpFOehi6GNJk8uwAAABhBBDwzeKLAq5VOFJhxjoXwi7cj-30l4TWmY_44WITrgZIza_kKVO1yDxwEXAtAXpu8OiFCsWyJCGc0w3Trr3CZ5kJ-LTLIraUBhwPFSxC0geECfXIJ2Ma0NVP6Ezr6WX8t3EWluoFAlE5kkLuNbZm6HQLmDZX0jOZER3wXIx2VuXpPld0", + recvPrivKey: "yJnRHTLit-b-dJh4b1DyO5is5Tl60mHeObpkSezBLK0", + authSecret: "mW-ti1CqLQK4PyZBKy4q7g", + plaintext: "I am the walrus" + ), + Aes128GcmTest( + payload: "DGv6ra1nlYgDCS1FRnbzlwAAEABBBP4z9KsN6nGRTbVYI_c7VJSPQTBtkgcy27mlmlMoZIIgDll6e3vCYLocInmYWAmS6TlzAC8wEqKK6PBru3jl7A_yl95bQpu6cVPTpK4Mqgkf1CXztLVBSt2Ks3oZwbuwXPXLWyouBWLVWGNWQexSgSxsj_Qulcy4a-fN", + recvPrivKey: "q1dXpw3UpT5VOmu_cf_v6ih07Aems3njxI-JWgLcM94", + authSecret: "BTBZMqHH6r4Tts7J_aSIgg", + plaintext: "When I grow up, I want to be a watermelon" + ), + ] + + for test in tests { + if let deciphered = try? push.aes128gcm(payload: test.payload, + decryptWith: test.recvPrivKey, + authenticateWith: test.authSecret) { + XCTAssertEqual(deciphered, test.plaintext) + } else { + XCTFail("Failed to decipher \(test.plaintext)") + } + } + } + + func testFail_aes128gcm() { + let tests = [ + Aes128GcmTest( + payload: "bad base 64 encoding", + recvPrivKey: "yJnRHTLit-b-dJh4b1DyO5is5Tl60mHeObpkSezBLK0", + authSecret: "mW-ti1CqLQK4PyZBKy4q7g", + plaintext: "bad base64" + ), + Aes128GcmTest( + payload: "DGv6ra1nlYgDCS1FRnbzlwAAEABBBP4z9KsN6nGRTbVYI_c7VJSPQTBtkgcy27mlmlMoZIIgDll6e3vCYLocInmYWAmS6TlzAC8wEqKK6PBru3jl7A_yl95bQpu6cVPTpK4Mqgkf1CXztLVBSt2Ks3oZwbuwXPXLWyouBWLVWGNWQexSgSxsj_Qulcy4a-fN", + recvPrivKey: "badkey", + authSecret: "BTBZMqHH6r4Tts7J_aSIgg", + plaintext: "bad key" + ), + ] + + for test in tests { + do { + _ = try push.aes128gcm(payload: test.payload, + decryptWith: test.recvPrivKey, + authenticateWith: test.authSecret) + + XCTFail("Somehow, deciphered \(test.plaintext)") + } catch PushCryptoError.base64DecodeError { + XCTAssertEqual(test.plaintext, tests[0].plaintext) + } catch PushCryptoError.decryptionError(_) { + XCTAssertEqual(test.plaintext, tests[1].plaintext) + } catch PushCryptoError.utf8EncodingError { + XCTFail("Not tested") + } catch { + XCTFail("Not tested but found \(error)") + } + } + } + + func testRoundTrip_aes128gcm() { + // The real reason for this test is to test generateKeys() + guard let keys = try? push.generateKeys() else { + return XCTFail("Generating keys should not fail") + } + + let plaintext = "I am the egg man!" + + guard let payload = try? push.aes128gcm(plaintext: plaintext, encryptWith: keys.p256dhPublicKey, authenticateWith: keys.auth, rs: 1024, padLen: 2) else { + return XCTFail("Encryption should not fail") + } + + guard let deciphered = try? push.aes128gcm(payload: payload, decryptWith: keys.p256dhPrivateKey, authenticateWith: keys.auth) else { + return XCTFail("Decryption should not fail") + } + + XCTAssertEqual(deciphered, plaintext) + } + + struct AesGcmTest { + let plaintext: String + let recvPrivKey: String + let authSecret: String + let ciphertext: String + let cryptoKey: String + let encryption: String + } + + func testDecrypt_aesgcm() { + let tests = [ + AesGcmTest( + plaintext: "I am the walrus", + recvPrivKey: "9FWl15_QUQAWDaD3k3l50ZBZQJ4au27F1V4F0uLSD_M", + authSecret: "R29vIGdvbyBnJyBqb29iIQ", + ciphertext: "6nqAQUME8hNqw5J3kl8cpVVJylXKYqZOeseZG8UueKpA", + cryptoKey: "keyid=\"dhkey\"; dh=\"BNoRDbb84JGm8g5Z5CFxurSqsXWJ11ItfXEWYVLE85Y7CYkDjXsIEc4aqxYaQ1G8BqkXCJ6DPpDrWtdWj_mugHU\"", + encryption: "keyid=\"dhkey\"; salt=\"lngarbyKfMoi9Z75xYXmkg\"" + ), + AesGcmTest( + plaintext: "Small record size", + recvPrivKey: "4h23G_KkXC9TvBSK2v0Q7ImpS2YAuRd8hQyN0rFAwBg", + authSecret: "g2rWVHUCpUxgcL9Tz7vyeQ", + ciphertext: "oY4e5eDatDVt2fpQylxbPJM-3vrfhDasfPc8Q1PWt4tPfMVbz_sDNL_cvr0DXXkdFzS1lxsJsj550USx4MMl01ihjImXCjrw9R5xFgFrCAqJD3GwXA1vzS4T5yvGVbUp3SndMDdT1OCcEofTn7VC6xZ-zP8rzSQfDCBBxmPU7OISzr8Z4HyzFCGJeBfqiZ7yUfNlKF1x5UaZ4X6iU_TXx5KlQy_toV1dXZ2eEAMHJUcSdArvB6zRpFdEIxdcHcJyo1BIYgAYTDdAIy__IJVCPY_b2CE5W_6ohlYKB7xDyH8giNuWWXAgBozUfScLUVjPC38yJTpAUi6w6pXgXUWffende5FreQpnMFL1L4G-38wsI_-ISIOzdO8QIrXHxmtc1S5xzYu8bMqSgCinvCEwdeGFCmighRjj8t1zRWo0D14rHbQLPR_b1P5SvEeJTtS9Nm3iibM", + cryptoKey: "dh=BCg6ZIGuE2ZNm2ti6Arf4CDVD_8--aLXAGLYhpghwjl1xxVjTLLpb7zihuEOGGbyt8Qj0_fYHBP4ObxwJNl56bk", + encryption: "salt=5LIDBXbvkBvvb7ZdD-T4PQ; rs=3" + ), + AesGcmTest( + plaintext: "Yet another message", + recvPrivKey: "4h23G_KkXC9TvBSK2v0Q7ImpS2YAuRd8hQyN0rFAwBg", + authSecret: "6plwZnSpVUbF7APDXus3UQ", + ciphertext: "uEC5B_tR-fuQ3delQcrzrDCp40W6ipMZjGZ78USDJ5sMj-6bAOVG3AK6JqFl9E6AoWiBYYvMZfwThVxmDnw6RHtVeLKFM5DWgl1EwkOohwH2EhiDD0gM3io-d79WKzOPZE9rDWUSv64JstImSfX_ADQfABrvbZkeaWxh53EG59QMOElFJqHue4dMURpsMXg", + cryptoKey: "dh=BEaA4gzA3i0JDuirGhiLgymS4hfFX7TNTdEhSk_HBlLpkjgCpjPL5c-GL9uBGIfa_fhGNKKFhXz1k9Kyens2ZpQ", + encryption: "salt=ZFhzj0S-n29g9P2p4-I7tA; rs=8" + ), + AesGcmTest( + plaintext: "Some message", + recvPrivKey: "4h23G_KkXC9TvBSK2v0Q7ImpS2YAuRd8hQyN0rFAwBg", + authSecret: "aTDc6JebzR6eScy2oLo4RQ", + ciphertext: "Oo34w2F9VVnTMFfKtdx48AZWQ9Li9M6DauWJVgXU", + cryptoKey: "dh=BCHFVrflyxibGLlgztLwKelsRZp4gqX3tNfAKFaxAcBhpvYeN1yIUMrxaDKiLh4LNKPtj0BOXGdr-IQ-QP82Wjo", + encryption: "salt=zCU18Rw3A5aB_Xi-vfixmA; rs=24" + ) + ] + + for test in tests { + if let deciphered = try? push.aesgcm(ciphertext: test.ciphertext, + withHeaders: PushCryptoHeaders(encryption: test.encryption, cryptoKey: test.cryptoKey), + decryptWith: test.recvPrivKey, + authenticateWith: test.authSecret) { + XCTAssertEqual(deciphered, test.plaintext) + } else { + XCTFail("Failed to decipher \(test.plaintext)") + } + } + } + + func testFail_aesgcm() { + let tests = [ + AesGcmTest( + plaintext: "bad base64", + recvPrivKey: "4h23G_KkXC9TvBSK2v0Q7ImpS2YAuRd8hQyN0rFAwBg", + authSecret: "aTDc6JebzR6eScy2oLo4RQ", + ciphertext: "non base64", + cryptoKey: "dh=BCHFVrflyxibGLlgztLwKelsRZp4gqX3tNfAKFaxAcBhpvYeN1yIUMrxaDKiLh4LNKPtj0BOXGdr-IQ-QP82Wjo", + encryption: "salt=zCU18Rw3A5aB_Xi-vfixmA; rs=24" + ), + + AesGcmTest( + plaintext: "bad key", + recvPrivKey: "badkey", + authSecret: "aTDc6JebzR6eScy2oLo4RQ", + ciphertext: "Oo34w2F9VVnTMFfKtdx48AZWQ9Li9M6DauWJVgXU", + cryptoKey: "dh=BCHFVrflyxibGLlgztLwKelsRZp4gqX3tNfAKFaxAcBhpvYeN1yIUMrxaDKiLh4LNKPtj0BOXGdr-IQ-QP82Wjo", + encryption: "salt=zCU18Rw3A5aB_Xi-vfixmA; rs=24" + ), + ] + + for test in tests { + do { + _ = try push.aesgcm(ciphertext: test.ciphertext, + withHeaders: PushCryptoHeaders(encryption: test.encryption, cryptoKey: test.cryptoKey), + decryptWith: test.recvPrivKey, + authenticateWith: test.authSecret) + + XCTFail("Somehow, deciphered \(test.plaintext)") + } catch PushCryptoError.base64DecodeError { + XCTAssertEqual(test.plaintext, tests[0].plaintext) + } catch PushCryptoError.decryptionError(_) { + XCTAssertEqual(test.plaintext, tests[1].plaintext) + } catch PushCryptoError.utf8EncodingError { + XCTFail("Not tested") + } catch { + XCTFail("Not tested but found \(error)") + } + } + } + + func testRoundTrip_aesgcm() { + guard let keys = try? push.generateKeys() else { + return XCTFail("Generating keys should not fail") + } + + let plaintext = "While I'm away, I'll write home every day" + + guard let (headers, ciphertext) = try? push.aesgcm(plaintext: plaintext, + encryptWith: keys.p256dhPublicKey, + authenticateWith: keys.auth, + rs: 28, + padLen: 16) else { + return XCTFail("Encryption should not fail") + } + + guard let deciphered = try? push.aesgcm(ciphertext: ciphertext, + withHeaders: headers, + decryptWith: keys.p256dhPrivateKey, + authenticateWith: keys.auth) else { + return XCTFail("Decryption should not fail") + } + + XCTAssertEqual(deciphered, plaintext) + } + + func testKeyGeneration() { + guard let keys = try? push.generateKeys() else { + return XCTFail("Keys can't be tested") + } + // We can't really test if the keys are random enough, but we can check if they + // roundtrip with the base64urlSafe encoding. + for string in [keys.p256dhPublicKey, keys.p256dhPrivateKey, keys.auth] { + XCTAssertEqual(string.base64urlSafeDecodedData?.base64urlSafeEncodedString, string) + } + } +} diff --git a/mobile/ios/README.md b/mobile/ios/README.md new file mode 100644 index 0000000000..b03454ed63 --- /dev/null +++ b/mobile/ios/README.md @@ -0,0 +1,25 @@ +# Dactyloidae iOS + +This code only works with Xcode 9.2 and supports iOS 10 and above. + +This code is written in Swift 3.2. + +## Building the code + +1. Install the latest [Xcode developer tools](https://developer.apple.com/xcode/downloads/) from Apple. +2. Install Carthage + ```shell + brew update + brew install carthage + ``` +3. Clone the repository: + ```shell + git clone https://repo.dactyloidae.xyz/dactyloidae/dactyloidae + ``` +4. Pull in the project dependencies: + ```shell + cd dactyloidae/mobile/ios + sh ./bootstrap.sh + ``` +5. Open `Client.xcodeproj` in Xcode. +6. Build the `Fennec` scheme in Xcode. diff --git a/mobile/ios/ReadingList/Info.plist b/mobile/ios/ReadingList/Info.plist new file mode 100644 index 0000000000..b3205ecaf4 --- /dev/null +++ b/mobile/ios/ReadingList/Info.plist @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + FMWK + CFBundleShortVersionString + 10.6 + CFBundleSignature + ???? + CFBundleVersion + 1 + NSPrincipalClass + + + diff --git a/mobile/ios/ReadingList/ReadingList-Bridging-Header.h b/mobile/ios/ReadingList/ReadingList-Bridging-Header.h new file mode 100644 index 0000000000..1df775e1a4 --- /dev/null +++ b/mobile/ios/ReadingList/ReadingList-Bridging-Header.h @@ -0,0 +1,13 @@ +// +// ReadingList-Bridging-Header.h +// Client +// +// Created by Emily Toop on 1/8/16. +// Copyright © 2016 Mozilla. All rights reserved. +// + +#ifndef ReadingList_Bridging_Header_h +#define ReadingList_Bridging_Header_h + + +#endif /* ReadingList_Bridging_Header_h */ diff --git a/mobile/ios/ReadingList/ReadingListAuthenticator.swift b/mobile/ios/ReadingList/ReadingListAuthenticator.swift new file mode 100644 index 0000000000..9d1e4449a0 --- /dev/null +++ b/mobile/ios/ReadingList/ReadingListAuthenticator.swift @@ -0,0 +1,9 @@ +/* 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 + +protocol ReadingListAuthenticator { + var headers: [String: String] { get } +} diff --git a/mobile/ios/ReadingList/ReadingListBasicAuthAuthenticator.swift b/mobile/ios/ReadingList/ReadingListBasicAuthAuthenticator.swift new file mode 100644 index 0000000000..a430fd6dc1 --- /dev/null +++ b/mobile/ios/ReadingList/ReadingListBasicAuthAuthenticator.swift @@ -0,0 +1,16 @@ +/* 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 + +class ReadingListBasicAuthAuthenticator: ReadingListAuthenticator { + var headers: [String: String] + + init(username: String, password: String) { + let credentials = "\(username):\(password)" + let credentialsData = credentials.data(using: String.Encoding.utf8)! + let encodedCredentials = credentialsData.base64EncodedString(options: NSData.Base64EncodingOptions()) + self.headers = ["Authorization": "Basic \(encodedCredentials)"] + } +} diff --git a/mobile/ios/ReadingList/ReadingListBatchRecordResponse.swift b/mobile/ios/ReadingList/ReadingListBatchRecordResponse.swift new file mode 100644 index 0000000000..dab8e3e89c --- /dev/null +++ b/mobile/ios/ReadingList/ReadingListBatchRecordResponse.swift @@ -0,0 +1,35 @@ +/* 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 + +class ReadingListBatchRecordResponse: ReadingListResponse { + var responses: [ReadingListRecordResponse] = [ReadingListRecordResponse]() + + override init?(response: HTTPURLResponse, json: [String: Any]) { + super.init(response: response, json: json) + guard let responses = json["responses"] as? [[String: Any]] else { + return nil + } + + for resp in responses { + guard let body = resp["body"] as? [String: Any], + let statusCode = resp["status"] as? Int, + let path = resp["path"] as? String, + let url = URL(string: path, relativeTo: self.response.url), + let headers = resp["headers"] as? [String: String], + let r = HTTPURLResponse(url: url, statusCode: statusCode, httpVersion: "1.1", headerFields: headers), + let recordResponse = ReadingListRecordResponse(response: r, json: body) else { + return nil + } + self.responses.append(recordResponse) + } + } + + var wasSuccessful: Bool { + get { + return response.statusCode == 200 + } + } +} diff --git a/mobile/ios/ReadingList/ReadingListChangeAccumulator.swift b/mobile/ios/ReadingList/ReadingListChangeAccumulator.swift new file mode 100644 index 0000000000..b8c5f695d1 --- /dev/null +++ b/mobile/ios/ReadingList/ReadingListChangeAccumulator.swift @@ -0,0 +1,14 @@ +/* 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 + +protocol ReadingListChangeAccumulator { + func addDeletedClientRecord(_ deletedRecord: ReadingListClientRecord) + func addDeletedServerRecord(_ deletedRecord: ReadingListServerRecord) + func addChangedRecord(_ changedRecord: ReadingListClientRecord) + func addUploadedRecord(_ uploadedRecord: ReadingListClientRecord, down: ReadingListServerRecord) + func addDownloadedRecord(_ downloadedRecord: ReadingListServerRecord) + func applyAccumulatedChanges() +} diff --git a/mobile/ios/ReadingList/ReadingListClient.swift b/mobile/ios/ReadingList/ReadingListClient.swift new file mode 100644 index 0000000000..abd685662d --- /dev/null +++ b/mobile/ios/ReadingList/ReadingListClient.swift @@ -0,0 +1,211 @@ +/* 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 Alamofire + +enum ReadingListDeleteRecordResult { + case success(ReadingListRecordResponse) + case preconditionFailed(ReadingListResponse) + case notFound(ReadingListResponse) + case failure(ReadingListResponse) + case error(NSError) +} + +enum ReadingListGetRecordResult { + case success(ReadingListRecordResponse) + case notModified(ReadingListResponse) // TODO Should really call this NotModified for clarity + case notFound(ReadingListResponse) + case failure(ReadingListResponse) + case error(NSError) +} + +enum ReadingListGetAllRecordsResult { + case success(ReadingListRecordsResponse) + case notModified(ReadingListResponse) // TODO Should really call this NotModified for clarity + case failure(ReadingListResponse) + case error(NSError) +} + +enum ReadingListPatchRecordResult { +} + +enum ReadingListAddRecordResult { + case success(ReadingListRecordResponse) + case failure(ReadingListResponse) + case conflict(ReadingListResponse) + case error(NSError) +} + +enum ReadingListBatchAddRecordsResult { + case success(ReadingListBatchRecordResponse) + case failure(ReadingListResponse) + case error(NSError) +} + +private let ReadingListClientUnknownError = NSError(domain: "org.mozilla.ios.Fennec.ReadingListClient", code: -1, userInfo: nil) + +class ReadingListClient { + var serviceURL: URL + var authenticator: ReadingListAuthenticator + + var articlesURL: URL! + var articlesBaseURL: URL! + var batchURL: URL! + + func getRecordWithGuid(_ guid: String, ifModifiedSince: ReadingListTimestamp?, completion: @escaping (ReadingListGetRecordResult) -> Void) { + if let url = URL(string: guid, relativeTo: articlesBaseURL) { + SessionManager.default.request(createRequest("GET", url, ifModifiedSince: ifModifiedSince)).responseJSON(options: [], completionHandler: { response -> Void in + if let json = response.result.value as? [String: Any], let response = response.response { + switch response.statusCode { + case 200: + completion(.success(ReadingListRecordResponse(response: response, json: json)!)) + case 304: + completion(.notModified(ReadingListResponse(response: response, json: json)!)) + case 404: + completion(.notFound(ReadingListResponse(response: response, json: json)!)) + default: + completion(.failure(ReadingListResponse(response: response, json: json)!)) + } + } else { + completion(.error(response.result.error as NSError? ?? ReadingListClientUnknownError)) + } + }) + } else { + // TODO ??? + } + } + + func getRecordWithGuid(_ guid: String, completion: @escaping (ReadingListGetRecordResult) -> Void) { + getRecordWithGuid(guid, ifModifiedSince: nil, completion: completion) + } + + func getAllRecordsWithFetchSpec(_ fetchSpec: ReadingListFetchSpec, ifModifiedSince: ReadingListTimestamp?, completion: @escaping (ReadingListGetAllRecordsResult) -> Void) { + if let url = fetchSpec.getURL(serviceURL: serviceURL, path: "/v1/articles") { + SessionManager.default.request(createRequest("GET", url)).responseJSON(options: [], completionHandler: { response -> Void in + if let json = response.result.value as? [String: Any], let response = response.response { + switch response.statusCode { + case 200: + completion(.success(ReadingListRecordsResponse(response: response, json: json)!)) + case 304: + completion(.notModified(ReadingListResponse(response: response, json: json)!)) + default: + completion(.failure(ReadingListResponse(response: response, json: json)!)) + } + } else { + completion(.error(response.result.error as NSError? ?? ReadingListClientUnknownError)) + } + }) + } else { + // TODO ??? + } + } + + func getAllRecordsWithFetchSpec(_ fetchSpec: ReadingListFetchSpec, completion: @escaping (ReadingListGetAllRecordsResult) -> Void) { + getAllRecordsWithFetchSpec(fetchSpec, ifModifiedSince: nil, completion: completion) + } + + func patchRecord(_ record: ReadingListClientRecord, completion: (ReadingListPatchRecordResult) -> Void) { + } + + func addRecord(_ record: ReadingListClientRecord, completion: @escaping (ReadingListAddRecordResult) -> Void) { + SessionManager.default.request(createRequest("POST", articlesURL, json: record.json)).responseJSON(options: [], completionHandler: { response in + if let json = response.result.value as? [String: Any], let response = response.response { + switch response.statusCode { + case 200, 201: // TODO Should we have different results for these? Do we care about 200 vs 201? + completion(.success(ReadingListRecordResponse(response: response, json: json)!)) + case 303: + completion(.conflict(ReadingListResponse(response: response, json: json)!)) + default: + completion(.failure(ReadingListResponse(response: response, json: json)!)) + } + } else { + completion(.error(response.result.error as NSError? ?? ReadingListClientUnknownError)) + } + }) + } + + /// Build the JSON body for POST /v1/batch { defaults: {}, request: [ {body: {} } ] } + fileprivate func recordsToBatchJSON(_ records: [ReadingListClientRecord]) -> AnyObject { + return [ + "defaults": ["method": "POST", "path": "/v1/articles", "headers": ["Content-Type": "application/json"]], + "requests": records.map { ["body": $0.json] } + ] as NSDictionary + } + + func batchAddRecords(_ records: [ReadingListClientRecord], completion: @escaping (ReadingListBatchAddRecordsResult) -> Void) { + SessionManager.default.request(createRequest("POST", batchURL, json: recordsToBatchJSON(records))).responseJSON(options: [], completionHandler: { response in + if let json = response.result.value as? [String: Any], let response = response.response { + switch response.statusCode { + case 200: + completion(.success(ReadingListBatchRecordResponse(response: response, json: json)!)) + default: + completion(.failure(ReadingListResponse(response: response, json: json)!)) + } + } else { + completion(.error(response.result.error as NSError? ?? ReadingListClientUnknownError)) + } + }) + } + + func deleteRecordWithGuid(_ guid: String, ifUnmodifiedSince: ReadingListTimestamp?, completion: @escaping (ReadingListDeleteRecordResult) -> Void) { + if let url = URL(string: guid, relativeTo: articlesBaseURL) { + SessionManager.default.request(createRequest("DELETE", url, ifUnmodifiedSince: ifUnmodifiedSince)).responseJSON(options: [], completionHandler: { response in + if let json = response.result.value as? [String: Any], let response = response.response { + switch response.statusCode { + case 200: + completion(.success(ReadingListRecordResponse(response: response, json: json)!)) + case 412: + completion(.preconditionFailed(ReadingListResponse(response: response, json: json)!)) + case 404: + completion(.notFound(ReadingListResponse(response: response, json: json)!)) + default: + completion(.failure(ReadingListResponse(response: response, json: json)!)) + } + } else { + completion(.error(response.result.error as NSError? ?? ReadingListClientUnknownError)) + } + }) + } else { + // TODO ??? + } + } + + func deleteRecordWithGuid(_ guid: String, completion: @escaping (ReadingListDeleteRecordResult) -> Void) { + deleteRecordWithGuid(guid, ifUnmodifiedSince: nil, completion: completion) + } + + func createRequest(_ method: String, _ url: URL, ifUnmodifiedSince: ReadingListTimestamp? = nil, ifModifiedSince: ReadingListTimestamp? = nil, json: AnyObject? = nil) -> URLRequest { + let request = NSMutableURLRequest(url: url) + request.httpMethod = method + if let ifUnmodifiedSince = ifUnmodifiedSince { + request.setValue(String(ifUnmodifiedSince), forHTTPHeaderField: "If-Unmodified-Since") + } + if let ifModifiedSince = ifModifiedSince { + request.setValue(String(ifModifiedSince), forHTTPHeaderField: "If-Modified-Since") + } + for (headerField, value) in authenticator.headers { + request.setValue(value, forHTTPHeaderField: headerField) + } + request.addValue("application/json", forHTTPHeaderField: "Accept") + if let json = json { + request.addValue("application/json", forHTTPHeaderField: "Content-Type") + do { + request.httpBody = try JSONSerialization.data(withJSONObject: json, options: JSONSerialization.WritingOptions.prettyPrinted) + } catch _ { + request.httpBody = nil + } // TODO Handle errors here + } + return request as URLRequest + } + + init(serviceURL: URL, authenticator: ReadingListAuthenticator) { + self.serviceURL = serviceURL + self.authenticator = authenticator + + self.articlesURL = URL(string: "/v1/articles", relativeTo: self.serviceURL) + self.articlesBaseURL = URL(string: "/v1/articles/", relativeTo: self.serviceURL) + self.batchURL = URL(string: "/v1/batch", relativeTo: self.serviceURL) + } +} diff --git a/mobile/ios/ReadingList/ReadingListClientMetadata.swift b/mobile/ios/ReadingList/ReadingListClientMetadata.swift new file mode 100644 index 0000000000..f3921ae10b --- /dev/null +++ b/mobile/ios/ReadingList/ReadingListClientMetadata.swift @@ -0,0 +1,25 @@ +/* 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 + +public struct ReadingListClientMetadata: Equatable { + /// The id of the record in the database + public var id: Int64 + /// A client timestamp + public var lastModified: ReadingListTimestamp + + public init?(row: [String: Any]) { + guard let id = row["client_id"] as? Int64, + let lastModified = row["client_last_modified"] as? Int64 else { + return nil + } + self.id = id + self.lastModified = lastModified + } +} + +public func ==(lhs: ReadingListClientMetadata, rhs: ReadingListClientMetadata) -> Bool { + return lhs.id == rhs.id && lhs.lastModified == rhs.lastModified +} diff --git a/mobile/ios/ReadingList/ReadingListClientRecord.swift b/mobile/ios/ReadingList/ReadingListClientRecord.swift new file mode 100644 index 0000000000..705343623d --- /dev/null +++ b/mobile/ios/ReadingList/ReadingListClientRecord.swift @@ -0,0 +1,67 @@ +/* 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 + +public struct ReadingListClientRecord: Equatable { + public let clientMetadata: ReadingListClientMetadata + public let serverMetadata: ReadingListServerMetadata? + + public let url: String + public let title: String + public let addedBy: String + public let unread: Bool + public let archived: Bool + public let favorite: Bool + + /// Initializer for when a record is loaded from a database row + public init?(row: [String: Any]) { + guard let clientMetadata = ReadingListClientMetadata(row: row) else { + return nil + } + + guard let url = row["url"] as? String, + let title = row["title"] as? String, + let addedBy = row["added_by"] as? String, + let unread = row["unread"] as? Bool else { + return nil + } + + self.clientMetadata = clientMetadata + self.serverMetadata = ReadingListServerMetadata(row: row) + + self.url = url + self.title = title + self.addedBy = addedBy + + self.unread = unread + + self.archived = row["archived"] as? Bool ?? false + self.favorite = row["favorite"] as? Bool ?? false + } + + public var json: AnyObject { + get { + let json = NSMutableDictionary() + json["url"] = url + json["title"] = title + json["added_by"] = addedBy + json["unread"] = unread + json["archived"] = archived + json["favorite"] = favorite + return json + } + } +} + +public func ==(lhs: ReadingListClientRecord, rhs: ReadingListClientRecord) -> Bool { + return lhs.clientMetadata == rhs.clientMetadata + && lhs.serverMetadata == rhs.serverMetadata + && lhs.url == rhs.url + && lhs.title == rhs.title + && lhs.addedBy == rhs.addedBy + && lhs.unread == rhs.unread + && lhs.archived == rhs.archived + && lhs.favorite == rhs.favorite +} diff --git a/mobile/ios/ReadingList/ReadingListError.swift b/mobile/ios/ReadingList/ReadingListError.swift new file mode 100644 index 0000000000..39fd68ddf7 --- /dev/null +++ b/mobile/ios/ReadingList/ReadingListError.swift @@ -0,0 +1,17 @@ +/* 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 + +// TODO This needs to encapsulate an NSError eventually +open class ReadingListError: MaybeErrorType { + var message: String + init(_ message: String) { + self.message = message + } + open var description: String { + return message + } +} diff --git a/mobile/ios/ReadingList/ReadingListFetchSpec.swift b/mobile/ios/ReadingList/ReadingListFetchSpec.swift new file mode 100644 index 0000000000..404aea34df --- /dev/null +++ b/mobile/ios/ReadingList/ReadingListFetchSpec.swift @@ -0,0 +1,87 @@ +/* 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 + +// TODO This should really use NSURLComponents to build up the query part. And not string ops. + +class ReadingListFetchSpec { + var queryString: String + + init(queryString: String) { + self.queryString = queryString + } + + func getURL(serviceURL: URL) -> URL? { + if var components = URLComponents(url: serviceURL, resolvingAgainstBaseURL: true) { + components.query = queryString + return components.url + } + return nil + } + + func getURL(serviceURL: URL, path: String) -> URL? { + if var components = URLComponents(url: serviceURL, resolvingAgainstBaseURL: true) { + components.path = path + components.query = queryString + return components.url + } + return nil + } + + // This should really generate a dictionary of values that we can pass to NSURLComponents instead of building the query string by hand + + class Builder { + var buffer: String = "" + var first = true + + func build() -> ReadingListFetchSpec { + return ReadingListFetchSpec(queryString: buffer) + } + + fileprivate func ampersand() { + if first { + first = false + return + } + buffer += "&" + } + + func setUnread(_ unread: Bool) -> Builder { + ampersand() + buffer += "unread=" + buffer += unread ? "true" : "false" + return self + } + + func setStatus(_ status: String, not: Bool) -> Builder { + ampersand() + if not { + buffer += "not_" + } + buffer += "status=" + buffer += status + return self + } + + fileprivate func qualifyAttribute(_ attribute: String, withQualifier qualifier: String, value: String) -> Builder { + ampersand() + buffer += qualifier + buffer += attribute + buffer += "=" + buffer += value + return self + } + + func setMinAttribute(_ attribute: String, value: String) -> Builder { + _ = qualifyAttribute(attribute, withQualifier: "min_", value: value) + return self + } + + func setMaxAttribute(_ attribute: String, value: String) -> Builder { + _ = qualifyAttribute(attribute, withQualifier: "max_", value: value) + return self + } + } +} diff --git a/mobile/ios/ReadingList/ReadingListOAuthAuthenticator.swift b/mobile/ios/ReadingList/ReadingListOAuthAuthenticator.swift new file mode 100644 index 0000000000..b312aa5ae0 --- /dev/null +++ b/mobile/ios/ReadingList/ReadingListOAuthAuthenticator.swift @@ -0,0 +1,15 @@ +/* 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 + +class ReadingListOAuthAuthAuthenticator: ReadingListAuthenticator { + var token: String + var headers: [String: String] + + init(token: String) { + self.token = token + self.headers = ["Authorization": "Bearer \(token)"] + } +} \ No newline at end of file diff --git a/mobile/ios/ReadingList/ReadingListRecord.swift b/mobile/ios/ReadingList/ReadingListRecord.swift new file mode 100644 index 0000000000..53ffa51788 --- /dev/null +++ b/mobile/ios/ReadingList/ReadingListRecord.swift @@ -0,0 +1,44 @@ +/* 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 + +// TODO This is not used anymore. Decide if we need to turn this into a protocol that the ClientRecord and ServerRecord implement. Not sure if we actually need that though. +class ReadingListRecord { + let serverMetadata: ReadingListServerMetadata? + + init(serverMetadata: ReadingListServerMetadata?) { + self.serverMetadata = serverMetadata + } + + var guid: String? { + get { + return serverMetadata?.guid + } + } + + var serverLastModified: Int64? { + get { + return serverMetadata?.lastModified + } + } + + var url: String { + get { + fatalError("Subclass Responsibility") + } + } + + var title: String { + get { + fatalError("Subclass Responsibility") + } + } + + var addedBy: String { + get { + fatalError("Subclass Responsibility") + } + } +} diff --git a/mobile/ios/ReadingList/ReadingListRecordResponse.swift b/mobile/ios/ReadingList/ReadingListRecordResponse.swift new file mode 100644 index 0000000000..206eb7cdb5 --- /dev/null +++ b/mobile/ios/ReadingList/ReadingListRecordResponse.swift @@ -0,0 +1,28 @@ +/* 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 + +class ReadingListRecordResponse: ReadingListResponse { + override init?(response: HTTPURLResponse, json: [String: Any]) { + super.init(response: response, json: json) + } + + var wasSuccessful: Bool { + get { + return response.statusCode == 200 || response.statusCode == 201 || response.statusCode == 204 + // TODO On Android we call super.wasSuccessful() .. is there another value that we consider a success? + } + } + + var record: ReadingListServerRecord? { + get { + if let json = self.json { + return ReadingListServerRecord(json: json) + } else { + return nil + } + } + } +} diff --git a/mobile/ios/ReadingList/ReadingListRecordsResponse.swift b/mobile/ios/ReadingList/ReadingListRecordsResponse.swift new file mode 100644 index 0000000000..97827df040 --- /dev/null +++ b/mobile/ios/ReadingList/ReadingListRecordsResponse.swift @@ -0,0 +1,36 @@ +/* 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 + +class ReadingListRecordsResponse: ReadingListResponse { + override init?(response: HTTPURLResponse, json: [String: Any]) { + super.init(response: response, json: json) + } + + var wasSuccessful: Bool { + get { + return response.statusCode == 200 || response.statusCode == 201 || response.statusCode == 204 + // TODO On Android we call super.wasSuccessful() .. is there another value that we consider a success? + } + } + + var records: [ReadingListServerRecord]? { + get { + if let json = self.json { + var records = [ReadingListServerRecord]() + if let items = json["items"] as? [[String: Any]] { + for item in items { + if let record = ReadingListServerRecord(json: item) { + records.append(record) + } + } + } + return records + } else { + return nil + } + } + } +} diff --git a/mobile/ios/ReadingList/ReadingListResponse.swift b/mobile/ios/ReadingList/ReadingListResponse.swift new file mode 100644 index 0000000000..fb9739a17a --- /dev/null +++ b/mobile/ios/ReadingList/ReadingListResponse.swift @@ -0,0 +1,28 @@ +/* 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 + +/// Wrapper around NSURLResponse and the response json object to easily pass this to delegates. Also contains some higher level functions to access service specific headers. +/// +/// TODO: In the Android code this is a subclass of MozResponse - Which has a bunch of other useful shortcuts. Maybe we should do that too? Or for the sake of simplicity, move some of those functions (which ones do we need?) into this class +class ReadingListResponse { + var response: HTTPURLResponse + var json: [String: Any]? + + init?(response: HTTPURLResponse, json: [String: Any]) { + self.response = response + self.json = json + } + + var lastModified: Int64? { + get { + if let lastModified = response.allHeaderFields["Last-Modified"] as? String { + return Int64(lastModified) + } else { + return nil + } + } + } +} diff --git a/mobile/ios/ReadingList/ReadingListSQLStorage.swift b/mobile/ios/ReadingList/ReadingListSQLStorage.swift new file mode 100644 index 0000000000..e0e5193a23 --- /dev/null +++ b/mobile/ios/ReadingList/ReadingListSQLStorage.swift @@ -0,0 +1,180 @@ +/* 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 SQLite +import Shared + +class ReadingListSQLStorage: ReadingListStorage { + var db: Connection! + let items: Table! + struct ItemColumns { + // Client Metadata + static let ClientId = Expression("client_id") + static let ClientLastModified = Expression("client_last_modified") + // Server Metadata + static let Id = Expression("id") + static let LastModified = Expression("last_modified") + // Properties + static let Url = Expression("url") + static let Title = Expression("title") + static let AddedBy = Expression("added_by") + static let Archived = Expression("archived") + static let Favorite = Expression("favorite") + static let Unread = Expression("unread") + } + + init(path: String) { + db = try! Connection(path) + + items = Table("items") + do { + try db.run(items.create(temporary: false, ifNotExists: true, block: { (t: SQLite.TableBuilder) in + // Client Metadata + t.column(ItemColumns.ClientId, primaryKey: .autoincrement) + t.column(ItemColumns.ClientLastModified) + // Server Metadata + t.column(ItemColumns.Id) // TODO Unique but may be null? + t.column(ItemColumns.LastModified) + // Properties + t.column(ItemColumns.Url, unique: true) + t.column(ItemColumns.Title) + t.column(ItemColumns.AddedBy) + t.column(ItemColumns.Archived, defaultValue: false) + t.column(ItemColumns.Favorite, defaultValue: false) + t.column(ItemColumns.Unread, defaultValue: true) + })) + } catch { + print("Unable to create items database '\(error)") + } + } + + func getAllRecords() -> Maybe<[ReadingListClientRecord]> { + do { + let preparedItems = try db.prepare(items) + return Maybe(success: Array(preparedItems).map {ReadingListClientRecord(row: self.rowToDictionary($0))!}) + } catch { + return Maybe(failure: ReadingListStorageError("Can't fetch all records: \(error)")) + } + } + + func getNewRecords() -> Maybe<[ReadingListClientRecord]> { + do { + let preparedItems = try db.prepare(items.filter(ItemColumns.Id == nil)) + return Maybe(success: Array(preparedItems).map {ReadingListClientRecord(row: self.rowToDictionary($0))!}) + } catch { + return Maybe(failure: ReadingListStorageError("Can't fetch all records: \(error)")) + } + } + + func getUnreadRecords() -> Maybe<[ReadingListClientRecord]> { + do { + let preparedItems = try db.prepare(items.filter(ItemColumns.Unread == true)) + return Maybe(success: Array(preparedItems).map {ReadingListClientRecord(row: self.rowToDictionary($0))!}) + } catch { + return Maybe(failure: ReadingListStorageError("Can't fetch all records: \(error)")) + } + } + + func getAvailableRecords() -> Maybe<[ReadingListClientRecord]> { + do { + let preparedItems = try db.prepare(items.order(ItemColumns.ClientLastModified.desc)) + return Maybe(success: Array(preparedItems).map {ReadingListClientRecord(row: self.rowToDictionary($0))!}) + } catch { + return Maybe(failure: ReadingListStorageError("Can't fetch all records: \(error)")) + } + } + + func deleteRecord(_ record: ReadingListClientRecord) -> Maybe { + print("Trying to delete record with id \(record.clientMetadata.id)\n") + let query = items.filter(ItemColumns.ClientId == record.clientMetadata.id) + do { + try db.run(query.delete()) + return Maybe(success: Void()) + } catch { + return Maybe(failure: ReadingListStorageError("Failed to delete")) + } + } + + func deleteAllRecords() -> Maybe { + do { + try db.run(self.items.delete()) + return Maybe(success: Void()) + } catch { + return Maybe(failure: ReadingListStorageError("Failed to delete")) + } + } + + func createRecordWithURL(_ url: String, title: String, addedBy: String) -> Maybe { + let insert = items.insert(ItemColumns.ClientLastModified <- ReadingListNow(), ItemColumns.Url <- url, ItemColumns.Title <- title, ItemColumns.AddedBy <- addedBy) + + do { + let id = try db.run(insert) + let preparedItems = try db.prepare(items.filter(ItemColumns.ClientId == id)) + if let item = Array(preparedItems).first { + if let record = ReadingListClientRecord(row: rowToDictionary(item)) { + return Maybe(success: record) + } else { + return Maybe(failure: ReadingListStorageError("Can't create RLCR from row")) + } + } else { + return Maybe(failure: ReadingListStorageError("Can't get first item from results")) + } + } catch { + return Maybe(failure: ReadingListStorageError("Can't insert: \(error)")) + } + } + + func getRecordWithURL(_ url: String) -> Maybe { + do { + let preparedItems = try db.prepare(items.filter(ItemColumns.Url == url)) + if let item = Array(preparedItems).first { + if let record = ReadingListClientRecord(row: rowToDictionary(item)) { + return Maybe(success: record) + } else { + return Maybe(failure: ReadingListStorageError("Can't create RLCR from row")) + } + } else { + return Maybe(success: nil) + } + } catch { + return Maybe(failure: ReadingListStorageError("Can't fetch: \(error)")) + } + } + + func updateRecord(_ record: ReadingListClientRecord, unread: Bool) -> Maybe { + let query = items.filter(ItemColumns.ClientId == record.clientMetadata.id).update(ItemColumns.Unread <- unread) + do { + try db.run(query) + let preparedItems = try db.prepare(items.filter(ItemColumns.ClientId == record.clientMetadata.id)) + if let item = Array(preparedItems).first { + if let record = ReadingListClientRecord(row: rowToDictionary(item)) { + return Maybe(success: record) + } else { + return Maybe(failure: ReadingListStorageError("Can't create RLCR from row")) + } + } else { + return Maybe(success: nil) + } + } catch { + return Maybe(success: nil) + } + } + + fileprivate func rowToDictionary(_ row: Row) -> [String: Any] { + var result: [String: Any] = [:] + result["client_id"] = NSNumber(value: row.get(ItemColumns.ClientId)) + result["client_last_modified"] = NSNumber(value: row.get(ItemColumns.ClientLastModified)) + result["id"] = row.get(ItemColumns.Id) + result["last_modified"] = NSNumber(value: row.get(ItemColumns.LastModified) ?? 0) + result["url"] = row.get(ItemColumns.Url) + result["title"] = row.get(ItemColumns.Title) + result["added_by"] = row.get(ItemColumns.AddedBy) + result["archived"] = row.get(ItemColumns.Archived) + result["favorite"] = row.get(ItemColumns.Favorite) + result["unread"] = row.get(ItemColumns.Unread) + return result + } +} diff --git a/mobile/ios/ReadingList/ReadingListServerMetadata.swift b/mobile/ios/ReadingList/ReadingListServerMetadata.swift new file mode 100644 index 0000000000..e307a09feb --- /dev/null +++ b/mobile/ios/ReadingList/ReadingListServerMetadata.swift @@ -0,0 +1,37 @@ +/* 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 + +public struct ReadingListServerMetadata: Equatable { + public var guid: String + public var lastModified: ReadingListTimestamp + + init(guid: String, lastModified: ReadingListTimestamp) { + self.guid = guid + self.lastModified = lastModified + } + + /// Initialize from server record. + init?(json: [String: Any]) { + self.init(data: json) + } + + init?(row: [String: Any]) { + self.init(data: row) + } + + private init?(data: [String: Any]) { + guard let guid = data["id"] as? String, + let lastModified = data["last_modified"] as? Int64 else { + return nil + } + self.guid = guid + self.lastModified = lastModified + } +} + +public func ==(lhs: ReadingListServerMetadata, rhs: ReadingListServerMetadata) -> Bool { + return lhs.guid == rhs.guid && lhs.lastModified == rhs.lastModified +} diff --git a/mobile/ios/ReadingList/ReadingListServerRecord.swift b/mobile/ios/ReadingList/ReadingListServerRecord.swift new file mode 100644 index 0000000000..700f9c2ec7 --- /dev/null +++ b/mobile/ios/ReadingList/ReadingListServerRecord.swift @@ -0,0 +1,41 @@ +/* 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 + +struct ReadingListServerRecord { + let serverMetadata: ReadingListServerMetadata? + + let url: String + let title: String + let addedBy: String + let unread: Bool + let archived: Bool + let favorite: Bool + + /// Initializer for when a record is loaded from server-sent json + init?(json: [String: Any]?) { + guard let json = json, + let serverMetadata = ReadingListServerMetadata(json: json), + let url = json["url"] as? String, + let title = json["title"] as? String, + let addedBy = json["added_by"] as? String, + let unread = json["unread"] as? Bool, + let archived = json["archived"] as? Bool, + let favorite = json["favorite"] as? Bool else { + return nil + } + + self.serverMetadata = serverMetadata + + self.url = url + self.title = title + self.addedBy = addedBy + + self.unread = unread + self.archived = archived + self.favorite = favorite + } + +} diff --git a/mobile/ios/ReadingList/ReadingListService.swift b/mobile/ios/ReadingList/ReadingListService.swift new file mode 100644 index 0000000000..ca3ff9cf3b --- /dev/null +++ b/mobile/ios/ReadingList/ReadingListService.swift @@ -0,0 +1,42 @@ +/* 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 + +/// This is the public API that the application and extension talk to. It exposes the bare minimum +/// functions that need to be public and hides details like storage and syncing. +open class ReadingListService { + var databasePath: String + var storage: ReadingListStorage + + public init?(profileStoragePath: String) { + databasePath = (profileStoragePath as NSString).appendingPathComponent("ReadingList.db") + storage = ReadingListSQLStorage(path: "\(profileStoragePath)/ReadingList.db") + } + + open func getAvailableRecords() -> Maybe<[ReadingListClientRecord]> { + return storage.getAvailableRecords() + } + + @discardableResult open func deleteRecord(_ record: ReadingListClientRecord) -> Maybe { + return storage.deleteRecord(record) + } + + open func deleteAllRecords() -> Maybe { + return storage.deleteAllRecords() + } + + @discardableResult open func createRecordWithURL(_ url: String, title: String, addedBy: String) -> Maybe { + return storage.createRecordWithURL(url, title: title, addedBy: addedBy) + } + + open func getRecordWithURL(_ url: String) -> Maybe { + return storage.getRecordWithURL(url) + } + + @discardableResult open func updateRecord(_ record: ReadingListClientRecord, unread: Bool) -> Maybe { + return storage.updateRecord(record, unread: unread) + } +} diff --git a/mobile/ios/ReadingList/ReadingListStorage.swift b/mobile/ios/ReadingList/ReadingListStorage.swift new file mode 100644 index 0000000000..5d7bb6ec6f --- /dev/null +++ b/mobile/ios/ReadingList/ReadingListStorage.swift @@ -0,0 +1,31 @@ +/* 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 + +class ReadingListStorageError: MaybeErrorType { + var message: String + init(_ message: String) { + self.message = message + } + var description: String { + return message + } +} + +/// Storage protocol. The only thing the client (application) communicates with is the storage. Adding, removing and updating items. +protocol ReadingListStorage { + func getAllRecords() -> Maybe<[ReadingListClientRecord]> + func getNewRecords() -> Maybe<[ReadingListClientRecord]> + + // These are the used by the application + func getUnreadRecords() -> Maybe<[ReadingListClientRecord]> + func getAvailableRecords() -> Maybe<[ReadingListClientRecord]> + func deleteRecord(_ record: ReadingListClientRecord) -> Maybe + func deleteAllRecords() -> Maybe + func createRecordWithURL(_ url: String, title: String, addedBy: String) -> Maybe + func getRecordWithURL(_ url: String) -> Maybe + func updateRecord(_ record: ReadingListClientRecord, unread: Bool) -> Maybe +} diff --git a/mobile/ios/ReadingList/ReadingListSyncMetadata.swift b/mobile/ios/ReadingList/ReadingListSyncMetadata.swift new file mode 100644 index 0000000000..40cf0fd2cc --- /dev/null +++ b/mobile/ios/ReadingList/ReadingListSyncMetadata.swift @@ -0,0 +1,25 @@ +/* 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 + +enum ReadingListSyncChanges { + +} + +enum ReadingListSyncStatus { + case synced + case new + case deleted + case modified +} + +enum ReadingListSyncChange { + case none, unread, favorite, resolved +} + +struct ReadingListSyncMetadata { + var changes: ReadingListSyncChanges + var status: ReadingListSyncStatus +} diff --git a/mobile/ios/ReadingList/ReadingListSynchronizer.swift b/mobile/ios/ReadingList/ReadingListSynchronizer.swift new file mode 100644 index 0000000000..0658d8755f --- /dev/null +++ b/mobile/ios/ReadingList/ReadingListSynchronizer.swift @@ -0,0 +1,69 @@ +/* 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 + +enum ReadingListSynchronizerResult { + case success + case failure + case error(NSError) +} + +enum ReadingListSyncType { + case uploadOnly + case full +} + +class ReadingListSynchronizer { + var storage: ReadingListStorage + var client: ReadingListClient + + init(storage: ReadingListStorage, client: ReadingListClient) { + self.storage = storage + self.client = client + } + + func synchronize(type: ReadingListSyncType, completion: (ReadingListSynchronizerResult) -> Void) { + // TODO Check if we are already syncing - Keep state somewhere + // TODO If this is a first time sync then we want to remember the account and server in storage, so that: + // TODO Check if the client is configured to use a different account or server + switch type { + case .uploadOnly: + let synchronizer = ReadingListUploadOnlySynchronizer(storage: storage, client: client) + synchronizer.synchronizeWithCompletion(completion) + case .full: + let synchronizer = ReadingListFullSynchronizer(storage: storage, client: client) + synchronizer.synchronizeWithCompletion(completion) + } + } +} + +// This is implemented in two different classes to make the design simpler. There will be some duplicate +// code but I prefer that instead of having two implementations in one class. + +private class ReadingListUploadOnlySynchronizer { + var storage: ReadingListStorage + var client: ReadingListClient + + init(storage: ReadingListStorage, client: ReadingListClient) { + self.storage = storage + self.client = client + } + + func synchronizeWithCompletion(_ completion: (ReadingListSynchronizerResult) -> Void) { + } +} + +private class ReadingListFullSynchronizer { + var storage: ReadingListStorage + var client: ReadingListClient + + init(storage: ReadingListStorage, client: ReadingListClient) { + self.storage = storage + self.client = client + } + + func synchronizeWithCompletion(_ completion: (ReadingListSynchronizerResult) -> Void) { + } +} diff --git a/mobile/ios/ReadingList/ReadingListUtils.swift b/mobile/ios/ReadingList/ReadingListUtils.swift new file mode 100644 index 0000000000..520145255f --- /dev/null +++ b/mobile/ios/ReadingList/ReadingListUtils.swift @@ -0,0 +1,16 @@ +/* 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 + +public typealias ReadingListRecordId = Int64 +public typealias ReadingListTimestamp = Int64 + +func ReadingListNow() -> ReadingListTimestamp { + return ReadingListTimestamp(Date.timeIntervalSinceReferenceDate * 1000.0) +} + +let ReadingListDefaultUnread: Bool = true +let ReadingListDefaultFavorite: Bool = false +let ReadingListDefaultArchived: Bool = false diff --git a/mobile/ios/ReadingListTests/Info.plist b/mobile/ios/ReadingListTests/Info.plist new file mode 100644 index 0000000000..9411e4a06e --- /dev/null +++ b/mobile/ios/ReadingListTests/Info.plist @@ -0,0 +1,24 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + BNDL + CFBundleShortVersionString + 10.6 + CFBundleSignature + ???? + CFBundleVersion + 1 + + diff --git a/mobile/ios/ReadingListTests/ReadingListClientRecordTestCase.swift b/mobile/ios/ReadingListTests/ReadingListClientRecordTestCase.swift new file mode 100644 index 0000000000..fddfbdbbde --- /dev/null +++ b/mobile/ios/ReadingListTests/ReadingListClientRecordTestCase.swift @@ -0,0 +1,126 @@ +/* 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/. */ + +@testable import ReadingList +import Foundation + +import XCTest + +class ReadingListClientRecordTestCase: XCTestCase { + + func testInitWithRow() { + let now = ReadingListNow() + let row: [String: Any] = [ + "client_id": Int64(1234), + "client_last_modified": now, + "id": "5DB5597A-8E60-454D-B1F5-51810C2A5FFE", + "last_modified": now, + "url": "http://localhost/article/1", + "title": "Article 1", + "added_by": "Stefan's iPhone", + "unread": true, + "archived": false, + "favorite": true + ] + + let record = ReadingListClientRecord(row: row) + XCTAssert(record != nil) + + if let record = record { + XCTAssertEqual(record.url, "http://localhost/article/1") + XCTAssertEqual(record.title, "Article 1") + XCTAssertEqual(record.addedBy, "Stefan's iPhone") + XCTAssertEqual(record.unread, true) + XCTAssertEqual(record.archived, false) + XCTAssertEqual(record.favorite, true) + + XCTAssert(record.clientMetadata.id == 1234) + XCTAssert(record.clientMetadata.lastModified == now) + + XCTAssert(record.serverMetadata != nil) + XCTAssertEqual(record.serverMetadata!.guid, "5DB5597A-8E60-454D-B1F5-51810C2A5FFE") + XCTAssertEqual(record.serverMetadata!.lastModified, now) + } + } + + func testInitWithRowWithoutServerMeta() { + let now = ReadingListNow() + let row: [String:Any] = [ + "client_id": Int64(1234), + "client_last_modified": now, + "url": "http://localhost/article/1", + "title": "Article 1", + "added_by": "Stefan's iPhone", + "unread": true, + "archived": false, + "favorite": true + ] + + let record = ReadingListClientRecord(row: row) + XCTAssert(record != nil) + + if let record = record { + XCTAssertEqual(record.url, "http://localhost/article/1") + XCTAssertEqual(record.title, "Article 1") + XCTAssertEqual(record.addedBy, "Stefan's iPhone") + XCTAssertEqual(record.unread, true) + XCTAssertEqual(record.archived, false) + XCTAssertEqual(record.favorite, true) + + XCTAssert(record.clientMetadata.id == 1234) + XCTAssert(record.clientMetadata.lastModified == now) + + XCTAssert(record.serverMetadata == nil) + } + } + + func testInitWithEmptyRow() { + let row = NSDictionary() + let record = ReadingListClientRecord(row: row as! [String : Any]) + XCTAssert(record == nil) + } + +// func testInit() { +// let record = ReadingListClientRecord(url: "http://localhost/some/article", title: "Some Article", addedBy: "Stefan's iPad Mini") +// XCTAssertNil(record.clientMetadata.id) +// XCTAssert(record.clientMetadata.lastModified != 0) +// XCTAssert(record.serverMetadata == nil) +// XCTAssertEqual(record.url, "http://localhost/some/article") +// XCTAssertEqual(record.title, "Some Article") +// XCTAssertEqual(record.addedBy, "Stefan's iPad Mini") +// XCTAssertEqual(record.unread, ReadingListDefaultUnread) +// XCTAssertEqual(record.favorite, ReadingListDefaultFavorite) +// XCTAssertEqual(record.archived, ReadingListDefaultArchived) +// } + + func testJSON() { + let now = ReadingListNow() + let row: [String:Any] = [ + "client_id": Int64(1234), + "client_last_modified": now, + "id": "5DB5597A-8E60-454D-B1F5-51810C2A5FFE", + "last_modified": now, + "url": "http://localhost/article/1", + "title": "Article 1", + "added_by": "Stefan's iPhone 3GS", + "unread": true, + "archived": false, + "favorite": true + ] + + let record = ReadingListClientRecord(row: row) + XCTAssert(record != nil) + + if let record = record { + let json: AnyObject = record.json + XCTAssertEqual(json.value(forKeyPath: "url") as? String, "http://localhost/article/1") + XCTAssertEqual(json.value(forKeyPath: "title") as? String, "Article 1") + XCTAssertEqual(json.value(forKeyPath: "added_by") as? String, "Stefan's iPhone 3GS") + + XCTAssertEqual(json.value(forKeyPath: "unread") as? Bool, true) + XCTAssertEqual(json.value(forKeyPath: "archived") as? Bool, false) + XCTAssertEqual(json.value(forKeyPath: "favorite") as? Bool, true) + } + } +} diff --git a/mobile/ios/ReadingListTests/ReadingListStorageTestCase.swift b/mobile/ios/ReadingListTests/ReadingListStorageTestCase.swift new file mode 100644 index 0000000000..507f91ecdf --- /dev/null +++ b/mobile/ios/ReadingListTests/ReadingListStorageTestCase.swift @@ -0,0 +1,195 @@ +/* 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/. */ + +@testable import ReadingList +import Foundation +import Shared + +import XCTest + +class ReadingListStorageTestCase: XCTestCase { + var storage: ReadingListStorage! + + override func setUp() { + let path = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true).first! + if FileManager.default.fileExists(atPath: "\(path)/ReadingList.db") { + do { + try FileManager.default.removeItem(atPath: "\(path)/ReadingList.db") + } catch _ { + XCTFail("Cannot remove old \(path)/ReadingList.db") + } + } + storage = ReadingListSQLStorage(path: "\(path)/ReadingList.db") + } + + func testCreateRecord() { + let result = storage.createRecordWithURL("http://www.anandtech.com/show/9117/analyzing-intel-core-m-performance", title: "Analyzing Intel Core M Performance: How 5Y10 can beat 5Y71 & the OEMs' Dilemma", addedBy: "Stefan's iPhone") + switch result { + case .failure(let error): + XCTFail(error.description) + case .success(let result): + XCTAssertEqual(result.value.url, "http://www.anandtech.com/show/9117/analyzing-intel-core-m-performance") + XCTAssertEqual(result.value.title, "Analyzing Intel Core M Performance: How 5Y10 can beat 5Y71 & the OEMs' Dilemma") + XCTAssertEqual(result.value.addedBy, "Stefan's iPhone") + XCTAssertEqual(result.value.unread, true) + XCTAssertEqual(result.value.archived, false) + XCTAssertEqual(result.value.favorite, false) + } + } + + func testGetRecordWithURL() { + let result1 = storage.createRecordWithURL("http://www.anandtech.com/show/9117/analyzing-intel-core-m-performance", title: "Analyzing Intel Core M Performance: How 5Y10 can beat 5Y71 & the OEMs' Dilemma", addedBy: "Stefan's iPhone") + switch result1 { + case .failure(let error): + XCTFail(error.description) + case .success( _): + break + } + + let result2 = storage.getRecordWithURL("http://www.anandtech.com/show/9117/analyzing-intel-core-m-performance") + switch result2 { + case .failure(let error): + XCTFail(error.description) + case .success( _): + XCTAssert(result1.successValue == result2.successValue!) + } + } + + func testGetUnreadRecords() { + // Create 3 records, mark the 2nd as read. + let _ = createRecordWithURL("http://localhost/article1", title: "Test 1", addedBy: "Stefan's iPhone") + let createResult2 = createRecordWithURL("http://localhost/article2", title: "Test 2", addedBy: "Stefan's iPhone") + let _ = createRecordWithURL("http://localhost/article3", title: "Test 3", addedBy: "Stefan's iPhone") + if let record = createResult2.successValue { + let _ = updateRecord(record, unread: false) + } + + // Get all unread records, make sure we only get the first and last + let getUnreadResult = storage.getUnreadRecords() + if let records = getUnreadResult.successValue { + XCTAssertEqual(2, records.count) + for record in records { + XCTAssert(record.title == "Test 1" || record.title == "Test 3") + XCTAssertEqual(record.unread, true) + } + } + } + + func testGetAllRecords() { + let _ = createRecordWithURL("http://localhost/article1", title: "Test 1", addedBy: "Stefan's iPhone") + let createResult2 = createRecordWithURL("http://localhost/article2", title: "Test 2", addedBy: "Stefan's iPhone") + let _ = createRecordWithURL("http://localhost/article3", title: "Test 3", addedBy: "Stefan's iPhone") + if let record = createResult2.successValue { + let _ = updateRecord(record, unread: false) + } + + let getAllResult = storage.getAllRecords() + if let records = getAllResult.successValue { + XCTAssertEqual(3, records.count) + } + } + + func testGetNewRecords() { + let _ = createRecordWithURL("http://localhost/article1", title: "Test 1", addedBy: "Stefan's iPhone") + let _ = createRecordWithURL("http://localhost/article2", title: "Test 2", addedBy: "Stefan's iPhone") + let _ = createRecordWithURL("http://localhost/article3", title: "Test 3", addedBy: "Stefan's iPhone") + let getAllResult = getAllRecords() + if let records = getAllResult.successValue { + XCTAssertEqual(3, records.count) + } + // TODO When we are able to create records coming from the server, we can extend this test to see if we query correctly + } + + func testDeleteRecord() { + let result1 = storage.createRecordWithURL("http://www.anandtech.com/show/9117/analyzing-intel-core-m-performance", title: "Analyzing Intel Core M Performance: How 5Y10 can beat 5Y71 & the OEMs' Dilemma", addedBy: "Stefan's iPhone") + switch result1 { + case .failure(let error): + XCTFail(error.description) + case .success(_): + break + } + + let result2 = storage.deleteRecord(result1.successValue!) + switch result2 { + case .failure(let error): + XCTFail(error.description) + case .success: + break + } + + let result3 = storage.getRecordWithURL("http://www.anandtech.com/show/9117/analyzing-intel-core-m-performance") + switch result3 { + case .failure(let error): + XCTFail(error.description) + case .success(let result): + XCTAssert(result.value == nil) + } + } + + func testDeleteAllRecords() { + let _ = createRecordWithURL("http://localhost/article1", title: "Test 1", addedBy: "Stefan's iPhone") + let _ = createRecordWithURL("http://localhost/article2", title: "Test 2", addedBy: "Stefan's iPhone") + let _ = createRecordWithURL("http://localhost/article3", title: "Test 3", addedBy: "Stefan's iPhone") + + let getAllResult1 = storage.getAllRecords() + if let records = getAllResult1.successValue { + XCTAssertEqual(3, records.count) + } + + let _ = deleteAllRecords() + + let getAllResult2 = getAllRecords() + if let records = getAllResult2.successValue { + XCTAssertEqual(0, records.count) + } + } + + func testUpdateRecord() { + let result = createRecordWithURL("http://www.anandtech.com/show/9117/analyzing-intel-core-m-performance", title: "Analyzing Intel Core M Performance: How 5Y10 can beat 5Y71 & the OEMs' Dilemma", addedBy: "Stefan's iPhone") + if let record = result.successValue { + XCTAssertEqual(record.url, "http://www.anandtech.com/show/9117/analyzing-intel-core-m-performance") + XCTAssertEqual(record.title, "Analyzing Intel Core M Performance: How 5Y10 can beat 5Y71 & the OEMs' Dilemma") + XCTAssertEqual(record.addedBy, "Stefan's iPhone") + XCTAssertEqual(record.unread, true) + XCTAssertEqual(record.archived, false) + XCTAssertEqual(record.favorite, false) + + let result = updateRecord(record, unread: false) + if let record = result.successValue! { + XCTAssertEqual(record.url, "http://www.anandtech.com/show/9117/analyzing-intel-core-m-performance") + XCTAssertEqual(record.title, "Analyzing Intel Core M Performance: How 5Y10 can beat 5Y71 & the OEMs' Dilemma") + XCTAssertEqual(record.addedBy, "Stefan's iPhone") + XCTAssertEqual(record.unread, false) + XCTAssertEqual(record.archived, false) + XCTAssertEqual(record.favorite, false) + } + } + } + + // Helpers that croak if the storage call was not successful + + func createRecordWithURL(_ url: String, title: String, addedBy: String) -> Maybe { + let result = storage.createRecordWithURL(url, title: title, addedBy: addedBy) + XCTAssertTrue(result.isSuccess) + return result + } + + func deleteAllRecords() -> Maybe { + let result = storage.deleteAllRecords() + XCTAssertTrue(result.isSuccess) + return result + } + + func getAllRecords() -> Maybe<[ReadingListClientRecord]> { + let result = storage.getAllRecords() + XCTAssertTrue(result.isSuccess) + return result + } + + func updateRecord(_ record: ReadingListClientRecord, unread: Bool) -> Maybe { + let result = storage.updateRecord(record, unread: unread) + XCTAssertTrue(result.isSuccess) + return result + } +} diff --git a/mobile/ios/Shared/Accessibility.swift b/mobile/ios/Shared/Accessibility.swift new file mode 100644 index 0000000000..8ee70a12fb --- /dev/null +++ b/mobile/ios/Shared/Accessibility.swift @@ -0,0 +1,41 @@ +/* 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 UIKit + +public protocol AccessibilityActionsSource: class { + func accessibilityCustomActionsForView(_ view: UIView) -> [UIAccessibilityCustomAction]? +} + +open class AccessibleAction { + open let name: String + open let handler: () -> Bool + + public init(name: String, handler: @escaping () -> Bool) { + self.name = name + self.handler = handler + } +} + +extension AccessibleAction { // UIAccessibilityCustomAction + @objc private func SELperformAccessibilityAction() -> Bool { + return handler() + } + + public var accessibilityCustomAction: UIAccessibilityCustomAction { + return UIAccessibilityCustomAction(name: name, target: self, selector: #selector(AccessibleAction.SELperformAccessibilityAction)) + } +} + +extension AccessibleAction { // UIAlertAction + private var alertActionHandler: (UIAlertAction!) -> Void { + return { (_: UIAlertAction!) -> Void in + _ = self.handler() + } + } + + public func alertAction(style: UIAlertActionStyle) -> UIAlertAction { + return UIAlertAction(title: name, style: style, handler: alertActionHandler) + } +} diff --git a/mobile/ios/Shared/AppConstants.swift b/mobile/ios/Shared/AppConstants.swift new file mode 100644 index 0000000000..495b2b51fe --- /dev/null +++ b/mobile/ios/Shared/AppConstants.swift @@ -0,0 +1,230 @@ +/* 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 UIKit + +public enum AppBuildChannel: String { + case release = "release" + case beta = "beta" + case developer = "developer" +} + +public enum KVOConstants: String { + case loading = "loading" + case estimatedProgress = "estimatedProgress" + case URL = "URL" + case title = "title" + case canGoBack = "canGoBack" + case canGoForward = "canGoForward" + case contentSize = "contentSize" +} + +public struct AppConstants { + public static let IsRunningTest = NSClassFromString("XCTestCase") != nil || ProcessInfo.processInfo.arguments.contains(LaunchArguments.Test) + + public static let FxAiOSClientId = "1b1a3e44c54fbb58" + + /// Build Channel. + public static let BuildChannel: AppBuildChannel = { + #if MOZ_CHANNEL_RELEASE + return AppBuildChannel.release + #elseif MOZ_CHANNEL_BETA + return AppBuildChannel.beta + #elseif MOZ_CHANNEL_FENNEC + return AppBuildChannel.developer + #endif + }() + + public static let scheme: String = { + guard let identifier = Bundle.main.bundleIdentifier else { + return "unknown" + } + + let scheme = identifier.replacingOccurrences(of: "org.mozilla.ios.", with: "") + if scheme == "FirefoxNightly.enterprise" { + return "FirefoxNightly" + } + return scheme + }() + + public static let PrefSendUsageData = "settings.sendUsageData" + + /// Whether we just mirror (false) or actively do a full bookmark merge and upload (true). + public static var shouldMergeBookmarks = false + + /// Should we try to sync (no merging) the Mobile Folder (if shouldMergeBookmarks is false). + public static let MOZ_SIMPLE_BOOKMARKS_SYNCING: Bool = { + #if MOZ_CHANNEL_RELEASE + return true + #elseif MOZ_CHANNEL_BETA + return true + #elseif MOZ_CHANNEL_FENNEC + return true + #else + return true + #endif + }() + + /// Should we send a repair request to other clients when the bookmarks buffer validation fails. + public static let MOZ_BOOKMARKS_REPAIR_REQUEST: Bool = { + #if MOZ_CHANNEL_RELEASE + return false + #elseif MOZ_CHANNEL_BETA + return true + #elseif MOZ_CHANNEL_FENNEC + return true + #else + return true + #endif + }() + + /// Flag indicating if we are running in Debug mode or not. + public static let isDebug: Bool = { + #if MOZ_CHANNEL_FENNEC + return true + #else + return false + #endif + }() + + /// Enables support for International Domain Names (IDN) + /// Disabled because of https://bugzilla.mozilla.org/show_bug.cgi?id=1312294 + public static let MOZ_PUNYCODE: Bool = { + #if MOZ_CHANNEL_RELEASE + return false + #elseif MOZ_CHANNEL_BETA + return false + #elseif MOZ_CHANNEL_FENNEC + return true + #else + return true + #endif + }() + + /// Enables/disables deep linking form fill for FxA + public static let MOZ_FXA_DEEP_LINK_FORM_FILL: Bool = { + #if MOZ_CHANNEL_RELEASE + return true + #elseif MOZ_CHANNEL_BETA + return true + #elseif MOZ_CHANNEL_FENNEC + return true + #else + return true + #endif + }() + + /// Toggles reporting our ad-hoc bookmark sync ping + public static let MOZ_ADHOC_SYNC_REPORTING: Bool = { + #if MOZ_CHANNEL_RELEASE + return false + #elseif MOZ_CHANNEL_BETA + return false + #elseif MOZ_CHANNEL_FENNEC + return true + #else + return true + #endif + }() + + /// Toggles the ability to add a custom search engine + public static let MOZ_CUSTOM_SEARCH_ENGINE: Bool = { + #if MOZ_CHANNEL_RELEASE + return true + #elseif MOZ_CHANNEL_BETA + return true + #elseif MOZ_CHANNEL_FENNEC + return true + #else + return true + #endif + }() + + /// Enables/disables push notificatuibs for FxA + public static let MOZ_FXA_PUSH: Bool = { + #if MOZ_CHANNEL_RELEASE + return true + #elseif MOZ_CHANNEL_BETA + return true + #elseif MOZ_CHANNEL_FENNEC + return true + #else + return true + #endif + }() + + /// Toggle the feature that shows the blue 'Open copied link' banner + public static let MOZ_CLIPBOARD_BAR: Bool = { + #if MOZ_CHANNEL_RELEASE + return true + #elseif MOZ_CHANNEL_BETA + return true + #elseif MOZ_CHANNEL_FENNEC + return true + #else + return true + #endif + }() + + /// Toggle pocket stories feature + public static let MOZ_POCKET_STORIES: Bool = { + #if MOZ_CHANNEL_RELEASE + return true + #elseif MOZ_CHANNEL_BETA + return true + #elseif MOZ_CHANNEL_FENNEC + return true + #else + return true + #endif + }() + + /// Toggle the use of Leanplum. + public static let MOZ_ENABLE_LEANPLUM: Bool = { + #if MOZ_CHANNEL_RELEASE + return true + #elseif MOZ_CHANNEL_BETA + return true + #elseif MOZ_CHANNEL_FENNEC + return true + #else + return false + #endif + }() + + /// Toggle the feature that shows updated FxA preferences cell + public static let MOZ_SHOW_FXA_AVATAR: Bool = { + #if MOZ_CHANNEL_RELEASE + return true + #elseif MOZ_CHANNEL_BETA + return true + #elseif MOZ_CHANNEL_FENNEC + return true + #else + return true + #endif + }() + + /// The maximum length of a URL stored by Firefox. Shared with Places on desktop. + public static let DB_URL_LENGTH_MAX = 65536 + + /// The maximum length of a page title stored by Firefox. Shared with Places on desktop. + public static let DB_TITLE_LENGTH_MAX = 4096 + + /// The maximum length of a bookmark description stored by Firefox. Shared with Places on desktop. + public static let DB_DESCRIPTION_LENGTH_MAX = 1024 + + /// Toggle FxA Leanplum A/B test for prompting push permissions + public static let MOZ_FXA_LEANPLUM_AB_PUSH_TEST: Bool = { + #if MOZ_CHANNEL_RELEASE + return false + #elseif MOZ_CHANNEL_BETA + return true + #elseif MOZ_CHANNEL_FENNEC + return true + #else + return false + #endif + }() +} diff --git a/mobile/ios/Shared/AppInfo.swift b/mobile/ios/Shared/AppInfo.swift new file mode 100644 index 0000000000..8dd7ef0dda --- /dev/null +++ b/mobile/ios/Shared/AppInfo.swift @@ -0,0 +1,86 @@ +/* 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 + +open class AppInfo { + /// Return the main application bundle. If this is called from an extension, the containing app bundle is returned. + open static var applicationBundle: Bundle { + let bundle = Bundle.main + switch bundle.bundleURL.pathExtension { + case "app": + return bundle + case "appex": + // .../Client.app/PlugIns/SendTo.appex + return Bundle(url: bundle.bundleURL.deletingLastPathComponent().deletingLastPathComponent())! + default: + fatalError("Unable to get application Bundle (Bundle.main.bundlePath=\(bundle.bundlePath))") + } + } + + open static var displayName: String { + return applicationBundle.object(forInfoDictionaryKey: "CFBundleDisplayName") as! String + } + + open static var appVersion: String { + return applicationBundle.object(forInfoDictionaryKey: "CFBundleShortVersionString") as! String + } + + open static var buildNumber: String { + return applicationBundle.object(forInfoDictionaryKey: String(kCFBundleVersionKey)) as! String + } + + open static var majorAppVersion: String { + return appVersion.components(separatedBy: ".").first! + } + + /// Return the shared container identifier (also known as the app group) to be used with for example background + /// http requests. It is the base bundle identifier with a "group." prefix. + open static var sharedContainerIdentifier: String { + var bundleIdentifier = baseBundleIdentifier + if bundleIdentifier == "org.mozilla.ios.FennecEnterprise" { + // Bug 1373726 - Base bundle identifier incorrectly generated for Nightly builds + // This can be removed when we are able to fix the app group in the developer portal + bundleIdentifier = "org.mozilla.ios.Fennec.enterprise" + } + return "group." + bundleIdentifier + } + + /// Return the keychain access group. + open static func keychainAccessGroupWithPrefix(_ prefix: String) -> String { + var bundleIdentifier = baseBundleIdentifier + if bundleIdentifier == "org.mozilla.ios.FennecEnterprise" { + // Bug 1373726 - Base bundle identifier incorrectly generated for Nightly builds + // This can be removed when we are able to fix the app group in the developer portal + bundleIdentifier = "org.mozilla.ios.Fennec.enterprise" + } + return prefix + "." + bundleIdentifier + } + + /// Return the base bundle identifier. + /// + /// This function is smart enough to find out if it is being called from an extension or the main application. In + /// case of the former, it will chop off the extension identifier from the bundle since that is a suffix not part + /// of the *base* bundle identifier. + open static var baseBundleIdentifier: String { + let bundle = Bundle.main + let packageType = bundle.object(forInfoDictionaryKey: "CFBundlePackageType") as! String + let baseBundleIdentifier = bundle.bundleIdentifier! + if packageType == "XPC!" { + let components = baseBundleIdentifier.components(separatedBy: ".") + return components[0..(_ initialValue: T, combine: @escaping (T, U) -> Deferred>) -> AsyncReducer { + return AsyncReducer(initialValue: initialValue, combine: combine) +} + +/** + * A appendable, async `reduce`. + * + * The reducer starts empty. New items need to be `append`ed. + * + * The constructor takes an `initialValue`, a `dispatch_queue_t`, and a `combine` function. + * + * The reduced value can be accessed via the `reducer.terminal` `Deferred>`, which is + * run once all items have been combined. + * + * The terminal will never be filled if no items have been appended. + * + * Once the terminal has been filled, no more items can be appended, and `append` methods will error. + */ +open class AsyncReducer { + // T is the accumulator. U is the input value. The returned T is the new accumulated value. + public typealias Combine = (T, U) -> Deferred> + fileprivate let lock = NSRecursiveLock() + + private let dispatchQueue: DispatchQueue + private let combine: Combine + + private let initialValueDeferred: Deferred> + open let terminal: Deferred> = Deferred() + + private var queuedItems: [U] = [] + + private var isStarted: Bool = false + + /** + * Has this task queue finished? + * Once the task queue has finished, it cannot have more tasks appended. + */ + open var isFilled: Bool { + lock.lock() + defer { lock.unlock() } + return terminal.isFilled + } + + public convenience init(initialValue: T, queue: DispatchQueue = DefaultDispatchQueue, combine: @escaping Combine) { + self.init(initialValue: deferMaybe(initialValue), queue: queue, combine: combine) + } + + public init(initialValue: Deferred>, queue: DispatchQueue = DefaultDispatchQueue, combine: @escaping Combine) { + self.dispatchQueue = queue + self.combine = combine + self.initialValueDeferred = initialValue + } + + // This is always protected by a lock, so we don't need to + // take another one. + fileprivate func ensureStarted() { + if self.isStarted { + return + } + + func queueNext(_ deferredValue: Deferred>) { + deferredValue.uponQueue(dispatchQueue, block: continueMaybe) + } + + func nextItem() -> U? { + // Because popFirst is only available on array slices. + // removeFirst is fine for range-replaceable collections. + return queuedItems.isEmpty ? nil : queuedItems.removeFirst() + } + + func continueMaybe(_ res: Maybe) { + lock.lock() + defer { lock.unlock() } + + if res.isFailure { + self.queuedItems.removeAll() + self.terminal.fill(Maybe(failure: res.failureValue!)) + return + } + + let accumulator = res.successValue! + + guard let item = nextItem() else { + self.terminal.fill(Maybe(success: accumulator)) + return + } + + let combineItem = deferDispatchAsync(dispatchQueue) { _ in + return self.combine(accumulator, item) + } + + queueNext(combineItem) + } + + queueNext(self.initialValueDeferred) + self.isStarted = true + } + + /** + * Append one or more tasks onto the end of the queue. + * + * @throws AlreadyFilled if the queue has finished already. + */ + open func append(_ items: U...) throws -> Deferred> { + return try append(items) + } + + /** + * Append a list of tasks onto the end of the queue. + * + * @throws AlreadyFilled if the queue has already finished. + */ + open func append(_ items: [U]) throws -> Deferred> { + lock.lock() + defer { lock.unlock() } + + if terminal.isFilled { + throw ReducerError.alreadyFilled + } + + queuedItems.append(contentsOf: items) + ensureStarted() + + return terminal + } +} + +enum ReducerError: Error { + case alreadyFilled +} diff --git a/mobile/ios/Shared/AuthenticationKeychainInfo.swift b/mobile/ios/Shared/AuthenticationKeychainInfo.swift new file mode 100644 index 0000000000..3bbc145262 --- /dev/null +++ b/mobile/ios/Shared/AuthenticationKeychainInfo.swift @@ -0,0 +1,157 @@ +/* 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 SwiftKeychainWrapper + +public let KeychainKeyAuthenticationInfo = "authenticationInfo" +public let AllowedPasscodeFailedAttempts = 3 + +// Passcode intervals with rawValue in seconds. +public enum PasscodeInterval: Int { + case immediately = 2 + case oneMinute = 60 + case fiveMinutes = 300 + case tenMinutes = 600 + case fifteenMinutes = 900 + case oneHour = 3600 +} + +// MARK: - Helper methods for accessing Authentication information from the Keychain +public extension KeychainWrapper { + func authenticationInfo() -> AuthenticationKeychainInfo? { + NSKeyedUnarchiver.setClass(AuthenticationKeychainInfo.self, forClassName: "AuthenticationKeychainInfo") + return object(forKey: KeychainKeyAuthenticationInfo) as? AuthenticationKeychainInfo + } + + func setAuthenticationInfo(_ info: AuthenticationKeychainInfo?) { + NSKeyedArchiver.setClassName("AuthenticationKeychainInfo", for: AuthenticationKeychainInfo.self) + if let info = info { + set(info, forKey: KeychainKeyAuthenticationInfo) + } else { + removeObject(forKey: KeychainKeyAuthenticationInfo) + } + } +} + +open class AuthenticationKeychainInfo: NSObject, NSCoding { + fileprivate(set) open var lastPasscodeValidationInterval: TimeInterval? + fileprivate(set) open var passcode: String? + fileprivate(set) open var requiredPasscodeInterval: PasscodeInterval? + fileprivate(set) open var lockOutInterval: TimeInterval? + fileprivate(set) open var failedAttempts: Int + open var useTouchID: Bool + + // Timeout period before user can retry entering passcodes + open var lockTimeInterval: TimeInterval = 15 * 60 + + public init(passcode: String) { + self.passcode = passcode + self.requiredPasscodeInterval = .immediately + self.failedAttempts = 0 + self.useTouchID = false + } + + open func encode(with aCoder: NSCoder) { + if let lastPasscodeValidationInterval = lastPasscodeValidationInterval { + let interval = NSNumber(value: lastPasscodeValidationInterval as Double) + aCoder.encode(interval, forKey: "lastPasscodeValidationInterval") + } + + if let lockOutInterval = lockOutInterval, isLocked() { + let interval = NSNumber(value: lockOutInterval as Double) + aCoder.encode(interval, forKey: "lockOutInterval") + } + + aCoder.encode(passcode, forKey: "passcode") + aCoder.encode(requiredPasscodeInterval?.rawValue, forKey: "requiredPasscodeInterval") + aCoder.encode(failedAttempts, forKey: "failedAttempts") + aCoder.encode(useTouchID, forKey: "useTouchID") + } + + public required init?(coder aDecoder: NSCoder) { + self.lastPasscodeValidationInterval = aDecoder.decodeAsDouble(forKey: "lastPasscodeValidationInterval") + if let lockOutInterval = aDecoder.decodeObject(forKey: "lockOutInterval") as? NSNumber { + self.lockOutInterval = lockOutInterval.doubleValue + } + self.passcode = aDecoder.decodeObject(forKey: "passcode") as? String + self.failedAttempts = aDecoder.decodeAsInt(forKey: "failedAttempts") + self.useTouchID = aDecoder.decodeAsBool(forKey: "useTouchID") + if var interval = aDecoder.decodeObject(forKey: "requiredPasscodeInterval") as? NSNumber { + // We have updated the immediate lockout value to 2 from 0 due to timing issues with systemUptime() + interval = interval == 0 ? 2 : interval + self.requiredPasscodeInterval = PasscodeInterval(rawValue: interval.intValue) + } + } +} + +// MARK: - API +public extension AuthenticationKeychainInfo { + private func resetLockoutState() { + self.failedAttempts = 0 + self.lockOutInterval = nil + } + + func updatePasscode(_ passcode: String) { + self.passcode = passcode + self.lastPasscodeValidationInterval = nil + } + + func updateRequiredPasscodeInterval(_ interval: PasscodeInterval) { + self.requiredPasscodeInterval = interval + self.lastPasscodeValidationInterval = nil + } + + func recordValidation() { + // Save the timestamp to remember the last time we successfully + // validated and clear out the failed attempts counter. + self.lastPasscodeValidationInterval = SystemUtils.systemUptime() + resetLockoutState() + } + + func lockOutUser() { + self.lockOutInterval = SystemUtils.systemUptime() + } + + func recordFailedAttempt() { + if self.failedAttempts >= AllowedPasscodeFailedAttempts { + //This is a failed attempt after a lockout period. Reset the lockout state + //This prevents failedAttemps from being higher than 3 + self.resetLockoutState() + } + self.failedAttempts += 1 + } + + func isLocked() -> Bool { + guard let lockOutInterval = self.lockOutInterval else { + return false + } + + if SystemUtils.systemUptime() < lockOutInterval { + // Unlock and require passcode input + resetLockoutState() + return false + } + return (SystemUtils.systemUptime() - (self.lockOutInterval ?? 0)) < lockTimeInterval + } + + func requiresValidation() -> Bool { + // If there isn't a passcode, don't need validation. + guard let _ = passcode else { + return false + } + + // Need to make sure we've validated in the past. If not, its a definite yes. + guard let lastValidationInterval = lastPasscodeValidationInterval, + let requireInterval = requiredPasscodeInterval + else { + return true + } + + // We've authenticated before so lets see how long since. If the uptime is less than the last validation stamp, + // we probably restarted which means we should require validation. + return SystemUtils.systemUptime() - lastValidationInterval > Double(requireInterval.rawValue) || + SystemUtils.systemUptime() < lastValidationInterval + } +} diff --git a/mobile/ios/Shared/Bytes.swift b/mobile/ios/Shared/Bytes.swift new file mode 100644 index 0000000000..b1e7c3f1e3 --- /dev/null +++ b/mobile/ios/Shared/Bytes.swift @@ -0,0 +1,49 @@ +/* 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 + +public typealias GUID = String + +/** + * Utilities for futzing with bytes and such. + */ +open class Bytes { + open class func generateRandomBytes(_ len: UInt) -> Data { + let len = Int(len) + var data = Data(count: len) + data.withUnsafeMutableBytes { (p: UnsafeMutablePointer) in + if (SecRandomCopyBytes(kSecRandomDefault, len, p) != errSecSuccess) { + fatalError("Random byte generation failed.") + } + } + return data + } + + open class func generateGUID() -> GUID { + // Turns the standard NSData encoding into the URL-safe variant that Sync expects. + return generateRandomBytes(9) + .base64EncodedString(options: NSData.Base64EncodingOptions()) + .replacingOccurrences(of: "/", with: "_", options: NSString.CompareOptions(), range: nil) + .replacingOccurrences(of: "+", with: "-", options: NSString.CompareOptions(), range: nil) + } + + open class func decodeBase64(_ b64: String) -> Data? { + return Data(base64Encoded: b64, + options: NSData.Base64DecodingOptions()) + } + + /** + * Turn a string of base64 characters into an NSData *without decoding*. + * This is to allow HMAC to be computed of the raw base64 string. + */ + open class func dataFromBase64(_ b64: String) -> Data? { + return b64.data(using: String.Encoding.ascii, allowLossyConversion: false) + } + + func fromHex(_ str: String) -> Data { + // TODO + return Data() + } +} diff --git a/mobile/ios/Shared/Cancellable.swift b/mobile/ios/Shared/Cancellable.swift new file mode 100644 index 0000000000..6cb1e240f1 --- /dev/null +++ b/mobile/ios/Shared/Cancellable.swift @@ -0,0 +1,9 @@ +/* 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/. */ + +public protocol Cancellable: class { + func cancel() + var cancelled: Bool { get } + var running: Bool { get set } +} diff --git a/mobile/ios/Shared/CrashSimulator.h b/mobile/ios/Shared/CrashSimulator.h new file mode 100644 index 0000000000..3258c7356b --- /dev/null +++ b/mobile/ios/Shared/CrashSimulator.h @@ -0,0 +1,11 @@ +/* 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 + +@interface CrashSimulator : NSObject + ++ (void)forceCrash; + +@end diff --git a/mobile/ios/Shared/CrashSimulator.m b/mobile/ios/Shared/CrashSimulator.m new file mode 100644 index 0000000000..ce182ac09e --- /dev/null +++ b/mobile/ios/Shared/CrashSimulator.m @@ -0,0 +1,14 @@ +/* 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 "CrashSimulator.h" + +@implementation CrashSimulator + ++ (void)forceCrash +{ + @throw [[NSException alloc] initWithName:@"Simulated Crash" reason:@"This is a simulated crash." userInfo:nil]; +} + +@end diff --git a/mobile/ios/Shared/DeferredUtils.swift b/mobile/ios/Shared/DeferredUtils.swift new file mode 100644 index 0000000000..e96d0e3168 --- /dev/null +++ b/mobile/ios/Shared/DeferredUtils.swift @@ -0,0 +1,199 @@ +/* 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 Deferred +// Haskell, baby. + +// Monadic bind/flatMap operator for Deferred. +precedencegroup MonadicBindPrecedence { + associativity: left + higherThan: MonadicDoPrecedence + lowerThan: BitwiseShiftPrecedence +} + +precedencegroup MonadicDoPrecedence { + associativity: left + higherThan: MultiplicationPrecedence +} + +infix operator >>== : MonadicBindPrecedence +infix operator >>> : MonadicDoPrecedence + +@discardableResult public func >>== (x: Deferred>, f: @escaping (T) -> Deferred>) -> Deferred> { + return chainDeferred(x, f: f) +} + +// A termination case. +public func >>== (x: Deferred>, f: @escaping (T) -> Void) { + return x.upon { result in + if let v = result.successValue { + f(v) + } + } +} + +// Monadic `do` for Deferred. +@discardableResult public func >>> (x: Deferred>, f: @escaping () -> Deferred>) -> Deferred> { + return x.bind { res in + if res.isSuccess { + return f() + } + return deferMaybe(res.failureValue!) + } +} + +// Another termination case. +public func >>> (x: Deferred>, f: @escaping () -> Void) { + return x.upon { res in + if res.isSuccess { + f() + } + } +} + +/** +* Returns a thunk that return a Deferred that resolves to the provided value. +*/ +public func always(_ t: T) -> () -> Deferred> { + return { deferMaybe(t) } +} + +public func deferMaybe(_ s: T) -> Deferred> { + return Deferred(value: Maybe(success: s)) +} + +public func deferMaybe(_ e: MaybeErrorType) -> Deferred> { + return Deferred(value: Maybe(failure: e)) +} + +public typealias Success = Deferred> + +@discardableResult public func succeed() -> Success { + return deferMaybe(()) +} + +/** + * Return a single Deferred that represents the sequential chaining + * of f over the provided items. + */ +public func walk(_ items: [T], f: @escaping (T) -> Success) -> Success { + return items.reduce(succeed()) { success, item -> Success in + success >>> { f(item) } + } +} + +/** + * Like `all`, but thanks to its taking thunks as input, each result is + * generated in strict sequence. Fails immediately if any result is failure. + */ +public func accumulate(_ thunks: [() -> Deferred>]) -> Deferred> { + if thunks.isEmpty { + return deferMaybe([]) + } + + let combined = Deferred>() + var results: [T] = [] + results.reserveCapacity(thunks.count) + + var onValue: ((T) -> Void)! + var onResult: ((Maybe) -> Void)! + + // onValue and onResult both hold references to each other niling them out before exiting breaks a reference cycle + // We also cannot use unowned here because the thunks are not class types. + onValue = { t in + results.append(t) + if results.count == thunks.count { + onResult = nil + combined.fill(Maybe(success: results)) + } else { + thunks[results.count]().upon(onResult) + } + } + + onResult = { r in + if r.isFailure { + onValue = nil + combined.fill(Maybe(failure: r.failureValue!)) + return + } + onValue(r.successValue!) + } + + thunks[0]().upon(onResult) + + return combined +} + +/** + * Take a function and turn it into a side-effect that can appear + * in a chain of async operations without producing its own value. + */ +public func effect(_ f: @escaping (T) -> U) -> (T) -> Deferred> { + return { t in + _ = f(t) + return deferMaybe(t) + } +} + +/** + * Return a single Deferred that represents the sequential chaining of + * f over the provided items, with the return value chained through. + */ +public func walk(_ items: S, start: Deferred>, f: @escaping (T, U) -> Deferred>) -> Deferred> where S.Iterator.Element == T { + let fs = items.map { item in + return { val in + f(item, val) + } + } + return fs.reduce(start, >>==) +} + +/** + * Like `all`, but doesn't accrue individual values. + */ +extension Array where Element: Success { + public func allSucceed() -> Success { + return all(self).bind { results -> Success in + if let failure = results.find({ $0.isFailure }) { + return deferMaybe(failure.failureValue!) + } + + return succeed() + } + } +} + +public func chainDeferred(_ a: Deferred>, f: @escaping (T) -> Deferred>) -> Deferred> { + return a.bind { res in + if let v = res.successValue { + return f(v) + } + return Deferred(value: Maybe(failure: res.failureValue!)) + } +} + +public func chainResult(_ a: Deferred>, f: @escaping (T) -> Maybe) -> Deferred> { + return a.map { res in + if let v = res.successValue { + return f(v) + } + return Maybe(failure: res.failureValue!) + } +} + +public func chain(_ a: Deferred>, f: @escaping (T) -> U) -> Deferred> { + return chainResult(a, f: { Maybe(success: f($0)) }) +} + +/// Defer-ifies a block to an async dispatch queue. +public func deferDispatchAsync(_ queue: DispatchQueue, f: @escaping () -> Deferred>) -> Deferred> { + let deferred = Deferred>() + queue.async(execute: { + f().upon { result in + deferred.fill(result) + } + }) + + return deferred +} diff --git a/mobile/ios/Shared/DeviceInfo.swift b/mobile/ios/Shared/DeviceInfo.swift new file mode 100644 index 0000000000..4306c20245 --- /dev/null +++ b/mobile/ios/Shared/DeviceInfo.swift @@ -0,0 +1,72 @@ +/* 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 UIKit + +open class DeviceInfo { + // List of device names that don't support advanced visual settings + static let lowGraphicsQualityModels = ["iPad", "iPad1,1", "iPhone1,1", "iPhone1,2", "iPhone2,1", "iPhone3,1", "iPhone3,2", "iPhone3,3", "iPod1,1", "iPod2,1", "iPod2,2", "iPod3,1", "iPod4,1", "iPad2,1", "iPad2,2", "iPad2,3", "iPad2,4", "iPad3,1", "iPad3,2", "iPad3,3"] + + open static var specificModelName: String { + var systemInfo = utsname() + uname(&systemInfo) + + let machine = systemInfo.machine + let mirror = Mirror(reflecting: machine) + var identifier = "" + + // Parses the string for the model name via NSUTF8StringEncoding, refer to + // http://stackoverflow.com/questions/26028918/ios-how-to-determine-iphone-model-in-swift + for child in mirror.children.enumerated() { + if let value = child.1.value as? Int8, value != 0 { + identifier.append(String(UnicodeScalar(UInt8(value)))) + } + } + return identifier + } + + /// Return the client name, which can be either "Fennec on Stefan's iPod" or simply "Stefan's iPod" if the application display name cannot be obtained. + open class func defaultClientName() -> String { + let format = NSLocalizedString("%@ on %@", tableName: "Shared", comment: "A brief descriptive name for this app on this device, used for Send Tab and Synced Tabs. The first argument is the app name. The second argument is the device name.") + return String(format: format, AppInfo.displayName, UIDevice.current.name) + } + + open class func clientIdentifier(_ prefs: Prefs) -> String { + if let id = prefs.stringForKey("clientIdentifier") { + return id + } + let id = UUID().uuidString + prefs.setString(id, forKey: "clientIdentifier") + return id + } + + open class func deviceModel() -> String { + return UIDevice.current.model + } + + open class func isSimulator() -> Bool { + return ProcessInfo.processInfo.environment["SIMULATOR_ROOT"] != nil + } + + open class func isBlurSupported() -> Bool { + // We've tried multiple ways to make this change visible on simulators, but we + // haven't found a solution that worked: + // 1. http://stackoverflow.com/questions/21603475/how-can-i-detect-if-the-iphone-my-app-is-on-is-going-to-use-a-simple-transparen + // 2. https://gist.github.com/conradev/8655650 + // Thus, testing has to take place on actual devices. + return !lowGraphicsQualityModels.contains(specificModelName) + } + + open class func hasConnectivity() -> Bool { + let status = Reach().connectionStatus() + switch status { + case .online(.wwan): + return true + case .online(.wiFi): + return true + default: + return false + } + } +} diff --git a/mobile/ios/Shared/Extensions/ArrayExtensions.swift b/mobile/ios/Shared/Extensions/ArrayExtensions.swift new file mode 100644 index 0000000000..06447d4aa9 --- /dev/null +++ b/mobile/ios/Shared/Extensions/ArrayExtensions.swift @@ -0,0 +1,70 @@ +/* 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 + +public extension Array where Element: Comparable { + func sameElements(_ arr: [Element]) -> Bool { + guard self.count == arr.count else { return false } + let sorted = self.sorted(by: <) + let arrSorted = arr.sorted(by: <) + for elements in sorted.zip(arrSorted) where elements.0 != elements.1 { + return false + } + return true + } +} + +public extension Array { + + func find(_ f: (Iterator.Element) -> Bool) -> Iterator.Element? { + for x in self { + if f(x) { + return x + } + } + return nil + } + + func contains(_ x: Element, f: (Element, Element) -> Bool) -> Bool { + for y in self { + if f(x, y) { + return true + } + } + return false + } + + // Performs a union operator using the result of f(Element) as the value to base uniqueness on. + func union(_ arr: [Element], f: ((Element) -> T)) -> [Element] { + let result = self + arr + return result.unique(f) + } + + // Returns unique values in an array using the result of f() + func unique(_ f: ((Element) -> T)) -> [Element] { + var map: [T: Element] = [T: Element]() + return self.flatMap { a in + let t = f(a) + if map[t] == nil { + map[t] = a + return a + } else { + return nil + } + } + } + +} + +public extension Sequence { + func every(_ f: (Self.Iterator.Element) -> Bool) -> Bool { + for x in self { + if !f(x) { + return false + } + } + return true + } +} diff --git a/mobile/ios/Shared/Extensions/DataExtensions.swift b/mobile/ios/Shared/Extensions/DataExtensions.swift new file mode 100644 index 0000000000..2293ddbc40 --- /dev/null +++ b/mobile/ios/Shared/Extensions/DataExtensions.swift @@ -0,0 +1,19 @@ +/* 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 + +public extension Data { + public mutating func appendBytes(fromData data: Data) { + var bytes = [UInt8](repeating: 0, count: data.count) + data.copyBytes(to: &bytes, count: data.count) + self.append(bytes, count: bytes.count) + } + + public func getBytes() -> [UInt8] { + var bytes = [UInt8](repeating: 0, count: self.count) + self.copyBytes(to: &bytes, count: self.count) + return bytes + } +} diff --git a/mobile/ios/Shared/Extensions/DictionaryExtensions.swift b/mobile/ios/Shared/Extensions/DictionaryExtensions.swift new file mode 100644 index 0000000000..3fd10ae3c7 --- /dev/null +++ b/mobile/ios/Shared/Extensions/DictionaryExtensions.swift @@ -0,0 +1,9 @@ +/* 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/. */ + +extension Dictionary { + public mutating func merge(with dictionary: Dictionary) { + dictionary.forEach { updateValue($1, forKey: $0) } + } +} diff --git a/mobile/ios/Shared/Extensions/HashExtensions.swift b/mobile/ios/Shared/Extensions/HashExtensions.swift new file mode 100644 index 0000000000..bfe2da4d3e --- /dev/null +++ b/mobile/ios/Shared/Extensions/HashExtensions.swift @@ -0,0 +1,74 @@ +/* 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 + +extension Data { + public var sha1: Data { + let len = Int(CC_SHA1_DIGEST_LENGTH) + let digest = UnsafeMutablePointer.allocate(capacity: len) + CC_SHA1((self as NSData).bytes, CC_LONG(self.count), digest) + return Data(bytes: UnsafePointer(digest), count: len) + } + + public var sha256: Data { + let len = Int(CC_SHA256_DIGEST_LENGTH) + let digest = UnsafeMutablePointer.allocate(capacity: len) + CC_SHA256((self as NSData).bytes, CC_LONG(self.count), digest) + return Data(bytes: UnsafePointer(digest), count: len) + } +} + +extension String { + public var sha1: Data { + let data = self.data(using: String.Encoding.utf8)! + return data.sha1 + } + + public var sha256: Data { + let data = self.data(using: String.Encoding.utf8)! + return data.sha256 + } +} + +extension Data { + public func hmacSha256WithKey(_ key: Data) -> Data { + let len = Int(CC_SHA256_DIGEST_LENGTH) + + let digest = UnsafeMutablePointer.allocate(capacity: len) + CCHmac(CCHmacAlgorithm(kCCHmacAlgSHA256), + (key as NSData).bytes, Int(key.count), + (self as NSData).bytes, Int(self.count), + digest) + return Data(bytes: UnsafePointer(digest), count: len) + } +} + +extension String { + public var utf8EncodedData: Data { + return self.data(using: String.Encoding.utf8, allowLossyConversion: false)! + } +} + +extension Data { + public var utf8EncodedString: String? { + return NSString(data: self, encoding: String.Encoding.utf8.rawValue) as String? + } +} + +extension Data { + public func xoredWith(_ other: Data) -> Data? { + if self.count != other.count { + return nil + } + var xoredBytes = [UInt8](repeating: 0, count: self.count) + let selfBytes = (self as NSData).bytes.bindMemory(to: UInt8.self, capacity: self.count) + let otherBytes = (other as NSData).bytes.bindMemory(to: UInt8.self, capacity: other.count) + for i in 0..(xoredBytes), count: self.count) + } + +} diff --git a/mobile/ios/Shared/Extensions/HexExtensions.swift b/mobile/ios/Shared/Extensions/HexExtensions.swift new file mode 100644 index 0000000000..5900ec58e9 --- /dev/null +++ b/mobile/ios/Shared/Extensions/HexExtensions.swift @@ -0,0 +1,70 @@ +/* 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 + +extension String { + public var hexDecodedData: Data { + // Convert to a CString and make sure it has an even number of characters (terminating 0 is included, so we + // check for uneven!) + guard let cString = self.cString(using: String.Encoding.ascii), (cString.count % 2) == 1 else { + return Data() + } + + var result = Data(capacity: (cString.count - 1) / 2) + for i in stride(from: 0, to: (cString.count - 1), by: 2) { + guard let l = hexCharToByte(cString[i]), let r = hexCharToByte(cString[i+1]) else { + return Data() + } + var value: UInt8 = (l << 4) | r + result.append(&value, count: MemoryLayout.size(ofValue: value)) + } + return result + } + + private func hexCharToByte(_ c: CChar) -> UInt8? { + if c >= 48 && c <= 57 { // 0 - 9 + return UInt8(c - 48) + } + if c >= 97 && c <= 102 { // a - f + return UInt8(10) + UInt8(c - 97) + } + if c >= 65 && c <= 70 { // A - F + return UInt8(10) + UInt8(c - 65) + } + return nil + } +} + +private let HexDigits: [String] = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "a", "b", "c", "d", "e", "f"] + +extension Data { + public var hexEncodedString: String { + var result = String() + result.reserveCapacity(count * 2) + withUnsafeBytes { (p: UnsafePointer) in + for i in 0..> 4)]) + result.append(HexDigits[Int(p[i] & 0x0f)]) + } + } + return String(result) + } + + public static func randomOfLength(_ length: UInt) -> Data? { + let length = Int(length) + var data = Data(count: length) + var result: Int32 = 0 + data.withUnsafeMutableBytes { (p: UnsafeMutablePointer) in + result = SecRandomCopyBytes(kSecRandomDefault, length, p) + } + return result == 0 ? data : nil + } +} + +extension Data { + public var base64EncodedString: String { + return self.base64EncodedString(options: NSData.Base64EncodingOptions()) + } +} diff --git a/mobile/ios/Shared/Extensions/JSONExtensions.swift b/mobile/ios/Shared/Extensions/JSONExtensions.swift new file mode 100644 index 0000000000..85c29de98f --- /dev/null +++ b/mobile/ios/Shared/Extensions/JSONExtensions.swift @@ -0,0 +1,65 @@ +/* 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 SwiftyJSON + +public extension JSON { + func isStringOrNull() -> Bool { + return self.isString() || + self.isNull() + } + + func isError() -> Bool { + return self.error != nil + } + + func isString() -> Bool { + // SwiftyJSON doesn't link values to types; it's possible for `self.type == .string` but + // `self.string` to return `nil`. Validate both. + return self.type == .string && + self.string != nil + } + + func isBool() -> Bool { + return self.type == .bool + } + + func isArray() -> Bool { + return self.type == .array + } + + func isDictionary() -> Bool { + return self.type == .dictionary + } + + // Bear in mind that for this function to work you need to set the value to NSNull: + // ``` + // var myObj = JSON(…) + // myObj["foo"] = someOptional ?? NSNull() + // ``` + // This is… easy to get wrong. + func isNull() -> Bool { + return self.type == .null + } + + func isInt() -> Bool { + return self.type == .number && self.int != nil + } + + func isNumber() -> Bool { + return self.type == .number && self.number != nil + } + + func isDouble() -> Bool { + return self.type == .number && self.double != nil + } + + // SwiftyJSON pretty prints the string value by default. Since all of our + // existing code required the string to not be pretty printed, this helper + // can be used as a shorthand for non-pretty printed strings. + func stringValue() -> String? { + return self.rawString(.utf8, options: []) + } +} diff --git a/mobile/ios/Shared/Extensions/KeychainWrapperExtensions.swift b/mobile/ios/Shared/Extensions/KeychainWrapperExtensions.swift new file mode 100644 index 0000000000..64b548940d --- /dev/null +++ b/mobile/ios/Shared/Extensions/KeychainWrapperExtensions.swift @@ -0,0 +1,62 @@ +/* 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 XCGLogger +import SwiftKeychainWrapper + +private let log = Logger.keychainLogger + +public extension KeychainWrapper { + static var sharedAppContainerKeychain: KeychainWrapper { + let baseBundleIdentifier = AppInfo.baseBundleIdentifier + let accessGroupPrefix = Bundle.main.object(forInfoDictionaryKey: "MozDevelopmentTeam") as! String + let accessGroupIdentifier = AppInfo.keychainAccessGroupWithPrefix(accessGroupPrefix) + return KeychainWrapper(serviceName: baseBundleIdentifier, accessGroup: accessGroupIdentifier) + } +} + +public extension KeychainWrapper { + func ensureStringItemAccessibility(_ accessibility: SwiftKeychainWrapper.KeychainItemAccessibility, forKey key: String) { + if self.hasValue(forKey: key) { + if self.accessibilityOfKey(key) != .afterFirstUnlock { + log.debug("updating item \(key) with \(accessibility)") + + guard let value = self.string(forKey: key) else { + log.error("failed to get item \(key)") + return + } + + if !self.removeObject(forKey: key) { + log.warning("failed to remove item \(key)") + } + + if !self.set(value, forKey: key, withAccessibility: accessibility) { + log.warning("failed to update item \(key)") + } + } + } + } + + func ensureObjectItemAccessibility(_ accessibility: SwiftKeychainWrapper.KeychainItemAccessibility, forKey key: String) { + if self.hasValue(forKey: key) { + if self.accessibilityOfKey(key) != .afterFirstUnlock { + log.debug("updating item \(key) with \(accessibility)") + + guard let value = self.object(forKey: key) else { + log.error("failed to get item \(key)") + return + } + + if !self.removeObject(forKey: key) { + log.warning("failed to remove item \(key)") + } + + if !self.set(value, forKey: key, withAccessibility: accessibility) { + log.warning("failed to update item \(key)") + } + } + } + } +} diff --git a/mobile/ios/Shared/Extensions/NSCharacterSetExtensions.swift b/mobile/ios/Shared/Extensions/NSCharacterSetExtensions.swift new file mode 100644 index 0000000000..dd90cc2b50 --- /dev/null +++ b/mobile/ios/Shared/Extensions/NSCharacterSetExtensions.swift @@ -0,0 +1,15 @@ +/* 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 + +extension CharacterSet { + public static func URLAllowedCharacterSet() -> CharacterSet { + return CharacterSet(charactersIn: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~:/?#[]@!$&'()*+,;=%") + } + + public static func SearchTermsAllowedCharacterSet() -> CharacterSet { + return CharacterSet(charactersIn: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789*-_.") + } +} diff --git a/mobile/ios/Shared/Extensions/NSCoderExtensions.swift b/mobile/ios/Shared/Extensions/NSCoderExtensions.swift new file mode 100644 index 0000000000..a891d27675 --- /dev/null +++ b/mobile/ios/Shared/Extensions/NSCoderExtensions.swift @@ -0,0 +1,41 @@ +/* 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 + +/** + * There are some oddnesses around the different ways that NSKeyedArchiver decodes objects based on whether or not they were + * originally encoded using Swift 2.x or Swift 3. + * If the object was encoded on Swift 2.x, then you need to use decodeObject to unwrap it. But that will return a nil if the object was encoded on Swift 3 + * For swift 3 encoded objects to you need to use decode + * These helper functions provide a unified way of achieving that + **/ +extension NSCoder { + /** + * Decode as Int regardless of which Swift version was used to encode it + **/ + open func decodeAsInt(forKey key: String) -> Int { + return self.decodeObject(forKey: key) as? Int ?? self.decodeInteger(forKey: key) + } + /** + * Decode as UInt64 regardless of which Swift version was used to encode it + **/ + open func decodeAsUInt64(forKey key: String) -> UInt64 { + return (self.decodeObject(forKey: key) as? NSNumber)?.uint64Value ?? UInt64(self.decodeInt64(forKey: key)) + } + + /** + * Decode as Bool regardless of which Swift version was used to encode it + **/ + open func decodeAsBool(forKey key: String) -> Bool { + return self.decodeObject(forKey: key) as? Bool ?? self.decodeBool(forKey: key) + } + + /** + * Decode as Double regardless of which Swift version was used to encode it + **/ + open func decodeAsDouble(forKey key: String) -> Double { + return (self.decodeObject(forKey: key) as? NSNumber)?.doubleValue ?? self.decodeDouble(forKey: key) + } +} diff --git a/mobile/ios/Shared/Extensions/NSFileManagerExtensions.swift b/mobile/ios/Shared/Extensions/NSFileManagerExtensions.swift new file mode 100644 index 0000000000..828cee6ca5 --- /dev/null +++ b/mobile/ios/Shared/Extensions/NSFileManagerExtensions.swift @@ -0,0 +1,112 @@ +/* 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/. */ + +/* Created and contributed by Nikolai Ruhe and rewritten in Swift. + * https://github.com/NikolaiRuhe/NRFoundation */ + +import Foundation + +public let NSFileManagerExtensionsDomain = "org.mozilla.NSFileManagerExtensions" + +public enum NSFileManagerExtensionsErrorCodes: Int { + case enumeratorFailure = 0 + case enumeratorElementNotURL = 1 + case errorEnumeratingDirectory = 2 +} + +public extension FileManager { + + private func directoryEnumeratorForURL(_ url: URL) throws -> FileManager.DirectoryEnumerator { + let prefetchedProperties = [ + URLResourceKey.isRegularFileKey, + URLResourceKey.fileAllocatedSizeKey, + URLResourceKey.totalFileAllocatedSizeKey + ] + + // If we run into an issue getting an enumerator for the given URL, capture the error and bail out later. + var enumeratorError: NSError? + let errorHandler: (URL, Error) -> Bool = { _, error in + enumeratorError = error as NSError + return false + } + + guard let directoryEnumerator = FileManager.default.enumerator(at: url, + includingPropertiesForKeys: prefetchedProperties, + options: [], + errorHandler: errorHandler) else { + throw errorWithCode(.enumeratorFailure) + } + + // Bail out if we encountered an issue getting the enumerator. + if let _ = enumeratorError { + throw errorWithCode(.errorEnumeratingDirectory, underlyingError: enumeratorError) + } + + return directoryEnumerator + } + + private func sizeForItemURL(_ url: Any, withPrefix prefix: String) throws -> Int64 { + guard let itemURL = url as? URL else { + throw errorWithCode(.enumeratorElementNotURL) + } + + // Skip files that are not regular and don't match our prefix + guard itemURL.isRegularFile && itemURL.lastComponentIsPrefixedBy(prefix) else { + return 0 + } + + return itemURL.allocatedFileSize() + } + + func allocatedSizeOfDirectoryAtURL(_ url: URL, forFilesPrefixedWith prefix: String, isLargerThanBytes threshold: Int64) throws -> Bool { + let directoryEnumerator = try directoryEnumeratorForURL(url) + var acc: Int64 = 0 + for item in directoryEnumerator { + acc += try sizeForItemURL(item as AnyObject, withPrefix: prefix) + if acc > threshold { + return true + } + } + return false + } + + /** + Returns the precise size of the given directory on disk. + + - parameter url: Directory URL + - parameter prefix: Prefix of files to check for size + + - throws: Error reading/operating on disk. + */ + func getAllocatedSizeOfDirectoryAtURL(_ url: URL, forFilesPrefixedWith prefix: String) throws -> Int64 { + let directoryEnumerator = try directoryEnumeratorForURL(url) + return try directoryEnumerator.reduce(0) { + let size = try sizeForItemURL($1 as AnyObject, withPrefix: prefix) + return $0 + size + } + } + + func contentsOfDirectoryAtPath(_ path: String, withFilenamePrefix prefix: String) throws -> [String] { + return try FileManager.default.contentsOfDirectory(atPath: path) + .filter { $0.hasPrefix("\(prefix).") } + .sorted { $0 < $1 } + } + + func removeItemInDirectory(_ directory: String, named: String) throws { + let file = URL(fileURLWithPath: directory).appendingPathComponent(named).path + try self.removeItem(atPath: file) + } + + private func errorWithCode(_ code: NSFileManagerExtensionsErrorCodes, underlyingError error: NSError? = nil) -> NSError { + var userInfo = [String: AnyObject]() + if let _ = error { + userInfo[NSUnderlyingErrorKey] = error + } + + return NSError( + domain: NSFileManagerExtensionsDomain, + code: code.rawValue, + userInfo: userInfo) + } +} diff --git a/mobile/ios/Shared/Extensions/NSMutableAttributedStringExtensions.swift b/mobile/ios/Shared/Extensions/NSMutableAttributedStringExtensions.swift new file mode 100644 index 0000000000..89d0be1357 --- /dev/null +++ b/mobile/ios/Shared/Extensions/NSMutableAttributedStringExtensions.swift @@ -0,0 +1,22 @@ +/* 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 UIKit + +extension NSMutableAttributedString { + public func colorSubstring(_ substring: String, withColor color: UIColor) { + self.attributeSubstring(substring, forAttribute: NSForegroundColorAttributeName, withValue: color) + } + + public func pitchSubstring(_ substring: String, withPitch pitch: Double) { + let pitchValue = NSNumber(value: pitch as Double) + self.attributeSubstring(substring, forAttribute: UIAccessibilitySpeechAttributePitch, withValue: pitchValue) + } + + private func attributeSubstring(_ substring: String, forAttribute attribute: String, withValue value: AnyObject) { + let nsString = self.string as NSString + let range = nsString.range(of: substring) + self.addAttribute(attribute, value: value, range: range) + } +} diff --git a/mobile/ios/Shared/Extensions/NSScannerExtensions.swift b/mobile/ios/Shared/Extensions/NSScannerExtensions.swift new file mode 100644 index 0000000000..ada72571e4 --- /dev/null +++ b/mobile/ios/Shared/Extensions/NSScannerExtensions.swift @@ -0,0 +1,31 @@ +/* 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 + +extension Scanner { + public func scanUnsignedLongLong() -> UInt64? { + var value: UInt64 = 0 + if scanUnsignedLongLong(&value) { + return value + } + return nil + } + + public func scanLongLong() -> Int64? { + var value: Int64 = 0 + if scanInt64(&value) { + return value + } + return nil + } + + public func scanDouble() -> Double? { + var value: Double = 0 + if scanDouble(&value) { + return value + } + return nil + } +} diff --git a/mobile/ios/Shared/Extensions/NSStringExtensions.swift b/mobile/ios/Shared/Extensions/NSStringExtensions.swift new file mode 100644 index 0000000000..3938f040c0 --- /dev/null +++ b/mobile/ios/Shared/Extensions/NSStringExtensions.swift @@ -0,0 +1,20 @@ +/* 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 + +extension String { + public static func contentsOfFileWithResourceName(_ name: String, ofType type: String, fromBundle bundle: Bundle, encoding: String.Encoding, error: NSErrorPointer) -> String? { + if let path = bundle.path(forResource: name, ofType: type) { + do { + return try String(contentsOfFile: path, encoding: encoding) + } catch { + return nil + } + } else { + return nil + } + } +} + diff --git a/mobile/ios/Shared/Extensions/NSURLExtensions.swift b/mobile/ios/Shared/Extensions/NSURLExtensions.swift new file mode 100644 index 0000000000..64654a8684 --- /dev/null +++ b/mobile/ios/Shared/Extensions/NSURLExtensions.swift @@ -0,0 +1,498 @@ +/* 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 UIKit + +private struct ETLDEntry: CustomStringConvertible { + let entry: String + + var isNormal: Bool { return isWild || !isException } + var isWild: Bool = false + var isException: Bool = false + + init(entry: String) { + self.entry = entry + self.isWild = entry.hasPrefix("*") + self.isException = entry.hasPrefix("!") + } + + fileprivate var description: String { + return "{ Entry: \(entry), isWildcard: \(isWild), isException: \(isException) }" + } +} + +private typealias TLDEntryMap = [String: ETLDEntry] + +private func loadEntriesFromDisk() -> TLDEntryMap? { + if let data = String.contentsOfFileWithResourceName("effective_tld_names", ofType: "dat", fromBundle: Bundle(identifier: "org.mozilla.Shared")!, encoding: String.Encoding.utf8, error: nil) { + let lines = data.components(separatedBy: "\n") + let trimmedLines = lines.filter { !$0.hasPrefix("//") && $0 != "\n" && $0 != "" } + + var entries = TLDEntryMap() + for line in trimmedLines { + let entry = ETLDEntry(entry: line) + let key: String + if entry.isWild { + // Trim off the '*.' part of the line + key = line.substring(from: line.characters.index(line.startIndex, offsetBy: 2)) + } else if entry.isException { + // Trim off the '!' part of the line + key = line.substring(from: line.characters.index(line.startIndex, offsetBy: 1)) + } else { + key = line + } + entries[key] = entry + } + return entries + } + return nil +} + +private var etldEntries: TLDEntryMap? = { + return loadEntriesFromDisk() +}() + +// MARK: - Local Resource URL Extensions +extension URL { + + public func allocatedFileSize() -> Int64 { + // First try to get the total allocated size and in failing that, get the file allocated size + return getResourceLongLongForKey(URLResourceKey.totalFileAllocatedSizeKey.rawValue) + ?? getResourceLongLongForKey(URLResourceKey.fileAllocatedSizeKey.rawValue) + ?? 0 + } + + public func getResourceValueForKey(_ key: String) -> Any? { + let resourceKey = URLResourceKey(key) + let keySet = Set([resourceKey]) + + var val: Any? + do { + let values = try resourceValues(forKeys: keySet) + val = values.allValues[resourceKey] + } catch _ { + return nil + } + return val + } + + public func getResourceLongLongForKey(_ key: String) -> Int64? { + return (getResourceValueForKey(key) as? NSNumber)?.int64Value + } + + public func getResourceBoolForKey(_ key: String) -> Bool? { + return getResourceValueForKey(key) as? Bool + } + + public var isRegularFile: Bool { + return getResourceBoolForKey(URLResourceKey.isRegularFileKey.rawValue) ?? false + } + + public func lastComponentIsPrefixedBy(_ prefix: String) -> Bool { + return (pathComponents.last?.hasPrefix(prefix) ?? false) + } +} + +// The list of permanent URI schemes has been taken from http://www.iana.org/assignments/uri-schemes/uri-schemes.xhtml +private let permanentURISchemes = ["aaa", "aaas", "about", "acap", "acct", "cap", "cid", "coap", "coaps", "crid", "data", "dav", "dict", "dns", "example", "file", "ftp", "geo", "go", "gopher", "h323", "http", "https", "iax", "icap", "im", "imap", "info", "ipp", "ipps", "iris", "iris.beep", "iris.lwz", "iris.xpc", "iris.xpcs", "jabber", "ldap", "mailto", "mid", "msrp", "msrps", "mtqp", "mupdate", "news", "nfs", "ni", "nih", "nntp", "opaquelocktoken", "pkcs11", "pop", "pres", "reload", "rtsp", "rtsps", "rtspu", "service", "session", "shttp", "sieve", "sip", "sips", "sms", "snmp", "soap.beep", "soap.beeps", "stun", "stuns", "tag", "tel", "telnet", "tftp", "thismessage", "tip", "tn3270", "turn", "turns", "tv", "urn", "vemmi", "vnc", "ws", "wss", "xcon", "xcon-userid", "xmlrpc.beep", "xmlrpc.beeps", "xmpp", "z39.50r", "z39.50s"] + +extension URL { + + public func withQueryParams(_ params: [URLQueryItem]) -> URL { + var components = URLComponents(url: self, resolvingAgainstBaseURL: false)! + var items = (components.queryItems ?? []) + for param in params { + items.append(param) + } + components.queryItems = items + return components.url! + } + + public func withQueryParam(_ name: String, value: String) -> URL { + var components = URLComponents(url: self, resolvingAgainstBaseURL: false)! + let item = URLQueryItem(name: name, value: value) + components.queryItems = (components.queryItems ?? []) + [item] + return components.url! + } + + public func getQuery() -> [String: String] { + var results = [String: String]() + let keyValues = self.query?.components(separatedBy: "&") + + if keyValues?.count ?? 0 > 0 { + for pair in keyValues! { + let kv = pair.components(separatedBy: "=") + if kv.count > 1 { + results[kv[0]] = kv[1] + } + } + } + + return results + } + + public var hostPort: String? { + if let host = self.host { + if let port = (self as NSURL).port?.int32Value { + return "\(host):\(port)" + } + return host + } + return nil + } + + public var origin: String? { + guard isWebPage(includeDataURIs: false), let hostPort = self.hostPort, let scheme = scheme else { + return nil + } + return "\(scheme)://\(hostPort)" + } + + /** + * Returns the second level domain (SLD) of a url. It removes any subdomain/TLD + * + * E.g., https://m.foo.com/bar/baz?noo=abc#123 => foo + **/ + public var hostSLD: String { + guard let publicSuffix = self.publicSuffix, let baseDomain = self.baseDomain else { + return self.normalizedHost ?? self.absoluteString + } + return baseDomain.replacingOccurrences(of: ".\(publicSuffix)", with: "") + } + + public var normalizedHostAndPath: String? { + if let normalizedHost = self.normalizedHost { + return normalizedHost + self.path + } + return nil + } + + public var absoluteDisplayString: String { + var urlString = self.absoluteString + // For http URLs, get rid of the trailing slash if the path is empty or '/' + if (self.scheme == "http" || self.scheme == "https") && (self.path == "/") && urlString.endsWith("/") { + urlString = urlString.substring(to: urlString.characters.index(urlString.endIndex, offsetBy: -1)) + } + // If it's basic http, strip out the string but leave anything else in + if urlString.hasPrefix("http://") { + return urlString.substring(from: urlString.characters.index(urlString.startIndex, offsetBy: 7)) + } else { + return urlString + } + } + + /// String suitable for displaying outside of the app, for example in notifications, were Data Detectors will + /// linkify the text and make it into a openable-in-Safari link. + public var absoluteDisplayExternalString: String { + return self.absoluteDisplayString.replacingOccurrences(of: ".", with: "\u{2024}") + } + + public var displayURL: URL? { + if self.isReaderModeURL { + return self.decodeReaderModeURL?.havingRemovedAuthorisationComponents() + } + + if self.isErrorPageURL { + if let decodedURL = self.originalURLFromErrorURL { + return decodedURL.displayURL + } else { + return nil + } + } + + if !self.isAboutURL { + return self.havingRemovedAuthorisationComponents() + } + + return nil + } + + /** + Returns the base domain from a given hostname. The base domain name is defined as the public domain suffix + with the base private domain attached to the front. For example, for the URL www.bbc.co.uk, the base domain + would be bbc.co.uk. The base domain includes the public suffix (co.uk) + one level down (bbc). + + :returns: The base domain string for the given host name. + */ + public var baseDomain: String? { + guard !isIPv6, let host = host else { return nil } + + // If this is just a hostname and not a FQDN, use the entire hostname. + if !host.contains(".") { + return host + } + + return publicSuffixFromHost(host, withAdditionalParts: 1) + } + + /** + * Returns just the domain, but with the same scheme, and a trailing '/'. + * + * E.g., https://m.foo.com/bar/baz?noo=abc#123 => https://foo.com/ + * + * Any failure? Return this URL. + */ + public var domainURL: URL { + if let normalized = self.normalizedHost { + // Use NSURLComponents instead of NSURL since the former correctly preserves + // brackets for IPv6 hosts, whereas the latter escapes them. + var components = URLComponents() + components.scheme = self.scheme + components.host = normalized + components.path = "/" + return components.url ?? self + } + return self + } + + public var normalizedHost: String? { + // Use components.host instead of self.host since the former correctly preserves + // brackets for IPv6 hosts, whereas the latter strips them. + guard let components = URLComponents(url: self, resolvingAgainstBaseURL: false), var host = components.host, host != "" else { + return nil + } + + if let range = host.range(of: "^(www|mobile|m)\\.", options: .regularExpression) { + host.replaceSubrange(range, with: "") + } + + return host + } + + /** + Returns the public portion of the host name determined by the public suffix list found here: https://publicsuffix.org/list/. + For example for the url www.bbc.co.uk, based on the entries in the TLD list, the public suffix would return co.uk. + + :returns: The public suffix for within the given hostname. + */ + public var publicSuffix: String? { + if let host = self.host { + return publicSuffixFromHost(host, withAdditionalParts: 0) + } else { + return nil + } + } + + public func isWebPage(includeDataURIs: Bool = true) -> Bool { + let schemes = includeDataURIs ? ["http", "https", "data"] : ["http", "https"] + if let scheme = scheme, schemes.contains(scheme) { + return true + } + + return false + } + + // This helps find local urls that we do not want to show loading bars on. + // These utility pages should be invisible to the user + public var isLocalUtility: Bool { + guard self.isLocal else { + return false + } + let utilityURLs = ["/errors", "/about/sessionrestore", "/about/home", "/reader-mode"] + return utilityURLs.contains { self.path.startsWith($0) } + } + + public var isLocal: Bool { + guard isWebPage(includeDataURIs: false) else { + return false + } + // iOS forwards hostless URLs (e.g., http://:6571) to localhost. + guard let host = host, !host.isEmpty else { + return true + } + + return host.lowercased() == "localhost" || host == "127.0.0.1" + } + + public var isIPv6: Bool { + return host?.contains(":") ?? false + } + + /** + Returns whether the URL's scheme is one of those listed on the official list of URI schemes. + This only accepts permanent schemes: historical and provisional schemes are not accepted. + */ + public var schemeIsValid: Bool { + guard let scheme = scheme else { return false } + return permanentURISchemes.contains(scheme.lowercased()) + } + + public func havingRemovedAuthorisationComponents() -> URL { + guard var urlComponents = URLComponents(url: self, resolvingAgainstBaseURL: false) else { + return self + } + urlComponents.user = nil + urlComponents.password = nil + if let url = urlComponents.url { + return url + } + return self + } +} + +// Extensions to deal with ReaderMode URLs + +extension URL { + public var isReaderModeURL: Bool { + let scheme = self.scheme, host = self.host, path = self.path + return scheme == "http" && host == "localhost" && path == "/reader-mode/page" + } + + public var decodeReaderModeURL: URL? { + if self.isReaderModeURL { + if let components = URLComponents(url: self, resolvingAgainstBaseURL: false), let queryItems = components.queryItems, queryItems.count == 1 { + if let queryItem = queryItems.first, let value = queryItem.value { + return URL(string: value) + } + } + } + return nil + } + + public func encodeReaderModeURL(_ baseReaderModeURL: String) -> URL? { + if let encodedURL = absoluteString.addingPercentEncoding(withAllowedCharacters: CharacterSet.alphanumerics) { + if let aboutReaderURL = URL(string: "\(baseReaderModeURL)?url=\(encodedURL)") { + return aboutReaderURL + } + } + return nil + } +} + +// Helpers to deal with ErrorPage URLs + +extension URL { + public var isErrorPageURL: Bool { + if let host = self.host { + return self.scheme == "http" && host == "localhost" && path == "/errors/error.html" + } + return false + } + + public var originalURLFromErrorURL: URL? { + let components = URLComponents(url: self, resolvingAgainstBaseURL: false) + if let queryURL = components?.queryItems?.find({ $0.name == "url" })?.value { + return URL(string: queryURL) + } + return nil + } +} + +// Helpers to deal with About URLs +extension URL { + public var isAboutHomeURL: Bool { + if let urlString = self.getQuery()["url"]?.unescape(), isErrorPageURL { + let url = URL(string: urlString) ?? self + return url.aboutComponent == "home" + } + return self.aboutComponent == "home" + } + + public var isAboutURL: Bool { + return self.aboutComponent != nil + } + + /// If the URI is an about: URI, return the path after "about/" in the URI. + /// For example, return "home" for "http://localhost:1234/about/home/#panel=0". + public var aboutComponent: String? { + let aboutPath = "/about/" + guard let scheme = self.scheme, let host = self.host else { + return nil + } + if scheme == "http" && host == "localhost" && path.startsWith(aboutPath) { + return path.substring(from: aboutPath.endIndex) + } + return nil + } + +} + +//MARK: Private Helpers +private extension URL { + func publicSuffixFromHost( _ host: String, withAdditionalParts additionalPartCount: Int) -> String? { + if host.isEmpty { + return nil + } + + // Check edge case where the host is either a single or double '.'. + if host.isEmpty || NSString(string: host).lastPathComponent == "." { + return "" + } + + /** + * The following algorithm breaks apart the domain and checks each sub domain against the effective TLD + * entries from the effective_tld_names.dat file. It works like this: + * + * Example Domain: test.bbc.co.uk + * TLD Entry: bbc + * + * 1. Start off by checking the current domain (test.bbc.co.uk) + * 2. Also store the domain after the next dot (bbc.co.uk) + * 3. If we find an entry that matches the current domain (test.bbc.co.uk), perform the following checks: + * i. If the domain is a wildcard AND the previous entry is not nil, then the current domain matches + * since it satisfies the wildcard requirement. + * ii. If the domain is normal (no wildcard) and we don't have anything after the next dot, then + * currentDomain is a valid TLD + * iii. If the entry we matched is an exception case, then the base domain is the part after the next dot + * + * On the next run through the loop, we set the new domain to check as the part after the next dot, + * update the next dot reference to be the string after the new next dot, and check the TLD entries again. + * If we reach the end of the host (nextDot = nil) and we haven't found anything, then we've hit the + * top domain level so we use it by default. + */ + + let tokens = host.components(separatedBy: ".") + let tokenCount = tokens.count + var suffix: String? + var previousDomain: String? = nil + var currentDomain: String = host + + for offset in 0.. 0 { + if let suffix = suffix { + // Take out the public suffixed and add in the additional parts we want. + let literalFromEnd: NSString.CompareOptions = [NSString.CompareOptions.literal, // Match the string exactly. + NSString.CompareOptions.backwards, // Search from the end. + NSString.CompareOptions.anchored] // Stick to the end. + let suffixlessHost = host.replacingOccurrences(of: suffix, with: "", options: literalFromEnd, range: nil) + let suffixlessTokens = suffixlessHost.components(separatedBy: ".").filter { $0 != "" } + let maxAdditionalCount = max(0, suffixlessTokens.count - additionalPartCount) + let additionalParts = suffixlessTokens[maxAdditionalCount.. String { + // If our host is empty, return nothing since it doesn't make sense to add the scheme or port. + guard !host.isEmpty else { + return "" + } + + var urlString: String + if let p = `protocol` { + urlString = "\(p)://\(host)" + } else { + urlString = host + } + + // Check for non-standard ports + if port != 0 && port != 443 && port != 80 { + urlString += ":\(port)" + } + + return urlString + } +} diff --git a/mobile/ios/Shared/Extensions/OptionalExtensions.swift b/mobile/ios/Shared/Extensions/OptionalExtensions.swift new file mode 100644 index 0000000000..460f8d3027 --- /dev/null +++ b/mobile/ios/Shared/Extensions/OptionalExtensions.swift @@ -0,0 +1,18 @@ +/* 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 + +/* A smarter ?? operator which allows the left hand/right hand arguments to not be + * the same type. This is useful where we want to print the string representation + * of an optional value that is not a string but want to return a string value when + * a value is absent. + * + * For more informatin, check out Oleb's post: + * https://oleb.net/blog/2016/12/optionals-string-interpolation/ */ + +infix operator ???: NilCoalescingPrecedence +public func ???(optional: T?, defaultValue: @autoclosure () -> String) -> String { + return optional.map { String(describing: $0) } ?? defaultValue() +} diff --git a/mobile/ios/Shared/Extensions/SetExtensions.swift b/mobile/ios/Shared/Extensions/SetExtensions.swift new file mode 100644 index 0000000000..810b7766f3 --- /dev/null +++ b/mobile/ios/Shared/Extensions/SetExtensions.swift @@ -0,0 +1,84 @@ +/* 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 + +public extension SetIterator { + mutating func take(_ n: Int) -> [Element]? { + precondition(n >= 0) + + if n == 0 { + return [] + } + + var count: Int = 0 + var out: [Element] = [] + + while count < n { + count += 1 + guard let val = self.next() else { + if out.isEmpty { + return nil + } + return out + } + out.append(val) + } + return out + } +} + +public extension Set { + func withSubsetsOfSize(_ n: Int, f: (Set) throws -> Void) rethrows { + precondition(n > 0) + + if self.isEmpty { + return + } + + if n > self.count { + try f(self) + return + } + + if n == 1 { + try self.forEach { try f(Set([$0])) } + return + } + + var generator = self.makeIterator() + while let next = generator.take(n) { + if !next.isEmpty { + try f(Set(next)) + } + } + } + + func subsetsOfSize(_ n: Int) -> [Set] { + precondition(n > 0) + + if self.isEmpty { + return [] + } + + if n > self.count { + return [self] + } + + if n == 1 { + // Special case. + return self.map({ Set([$0]) }) + } + + var generator = self.makeIterator() + var out: [Set] = [] + out.reserveCapacity(self.count / n) + while let next = generator.take(n) { + if !next.isEmpty { + out.append(Set(next)) + } + } + return out + } +} diff --git a/mobile/ios/Shared/Extensions/StringExtensions.swift b/mobile/ios/Shared/Extensions/StringExtensions.swift new file mode 100644 index 0000000000..9b717d4510 --- /dev/null +++ b/mobile/ios/Shared/Extensions/StringExtensions.swift @@ -0,0 +1,107 @@ +/* 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 + +public extension String { + public func startsWith(_ other: String) -> Bool { + // rangeOfString returns nil if other is empty, destroying the analogy with (ordered) sets. + if other.isEmpty { + return true + } + if let range = self.range(of: other, + options: NSString.CompareOptions.anchored) { + return range.lowerBound == self.startIndex + } + return false + } + + public func endsWith(_ other: String) -> Bool { + // rangeOfString returns nil if other is empty, destroying the analogy with (ordered) sets. + if other.isEmpty { + return true + } + if let range = self.range(of: other, + options: [NSString.CompareOptions.anchored, NSString.CompareOptions.backwards]) { + return range.upperBound == self.endIndex + } + return false + } + + func escape() -> String? { + // We can't guaruntee that strings have a valid string encoding, as this is an entry point for tainted data, + // we should be very careful about forcefully dereferencing optional types. + // https://stackoverflow.com/questions/33558933/why-is-the-return-value-of-string-addingpercentencoding-optional#33558934 + let queryItemDividers = CharacterSet(charactersIn: "?=&") + let allowedEscapes = CharacterSet.urlQueryAllowed.symmetricDifference(queryItemDividers) + return self.addingPercentEncoding(withAllowedCharacters: allowedEscapes) + } + + func unescape() -> String? { + return self.removingPercentEncoding + } + + /** + Ellipsizes a String only if it's longer than `maxLength` + + "ABCDEF".ellipsize(4) + // "AB…EF" + + :param: maxLength The maximum length of the String. + + :returns: A String with `maxLength` characters or less + */ + func ellipsize(maxLength: Int) -> String { + if (maxLength >= 2) && (self.characters.count > maxLength) { + let index1 = self.characters.index(self.startIndex, offsetBy: (maxLength + 1) / 2) // `+ 1` has the same effect as an int ceil + let index2 = self.characters.index(self.endIndex, offsetBy: maxLength / -2) + + return self.substring(to: index1) + "…\u{2060}" + self.substring(from: index2) + } + return self + } + + private var stringWithAdditionalEscaping: String { + return self.replacingOccurrences(of: "|", with: "%7C", options: NSString.CompareOptions(), range: nil) + } + + public var asURL: URL? { + // Firefox and NSURL disagree about the valid contents of a URL. + // Let's escape | for them. + // We'd love to use one of the more sophisticated CFURL* or NSString.* functions, but + // none seem to be quite suitable. + return URL(string: self) ?? + URL(string: self.stringWithAdditionalEscaping) + } + + /// Returns a new string made by removing the leading String characters contained + /// in a given character set. + public func stringByTrimmingLeadingCharactersInSet(_ set: CharacterSet) -> String { + var trimmed = self + while trimmed.rangeOfCharacter(from: set)?.lowerBound == trimmed.startIndex { + trimmed.remove(at: trimmed.startIndex) + } + return trimmed + } + + /// Adds a newline at the closest space from the middle of a string. + /// Example turning "Mark as Read" into "Mark as\n Read" + public func stringSplitWithNewline() -> String { + let mid = self.characters.count/2 + + let arr: [Int] = self.characters.indices.flatMap { + if self.characters[$0] == " " { + return self.distance(from: startIndex, to: $0) + } + + return nil + } + guard let closest = arr.enumerated().min(by: { abs($0.1 - mid) < abs($1.1 - mid) }) else { + return self + } + var newString = self + newString.insert("\n", at: newString.characters.index(newString.characters.startIndex, offsetBy: closest.element)) + return newString + } +} diff --git a/mobile/ios/Shared/Extensions/UIColorExtensions.swift b/mobile/ios/Shared/Extensions/UIColorExtensions.swift new file mode 100644 index 0000000000..33118b575d --- /dev/null +++ b/mobile/ios/Shared/Extensions/UIColorExtensions.swift @@ -0,0 +1,31 @@ +/* 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 UIKit + +private struct Color { + var red: CGFloat + var green: CGFloat + var blue: CGFloat +} + +extension UIColor { + /** + * Initializes and returns a color object for the given RGB hex integer. + */ + public convenience init(rgb: Int) { + self.init( + red: CGFloat((rgb & 0xFF0000) >> 16) / 255.0, + green: CGFloat((rgb & 0x00FF00) >> 8) / 255.0, + blue: CGFloat((rgb & 0x0000FF) >> 0) / 255.0, + alpha: 1) + } + + public convenience init(colorString: String) { + var colorInt: UInt32 = 0 + Scanner(string: colorString).scanHexInt32(&colorInt) + self.init(rgb: (Int) (colorInt)) + } +} diff --git a/mobile/ios/Shared/Extensions/UIImageExtensions.swift b/mobile/ios/Shared/Extensions/UIImageExtensions.swift new file mode 100644 index 0000000000..4c616ca2fb --- /dev/null +++ b/mobile/ios/Shared/Extensions/UIImageExtensions.swift @@ -0,0 +1,83 @@ +/* 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 UIKit +import SDWebImage + +private let imageLock = NSLock() + +extension UIImage { + /// Despite docs that say otherwise, UIImage(data: NSData) isn't thread-safe (see bug 1223132). + /// As a workaround, synchronize access to this initializer. + /// This fix requires that you *always* use this over UIImage(data: NSData)! + public static func imageFromDataThreadSafe(_ data: Data) -> UIImage? { + imageLock.lock() + let image = UIImage(data: data) + imageLock.unlock() + return image + } + + /// Generates a UIImage from GIF data by calling out to SDWebImage. The latter in turn uses UIImage(data: NSData) + /// in certain cases so we have to synchronize calls (see bug 1223132). + public static func imageFromGIFDataThreadSafe(_ data: Data) -> UIImage? { + imageLock.lock() + let image = UIImage.sd_animatedGIF(with: data) + imageLock.unlock() + return image + } + + public static func dataIsGIF(_ data: Data) -> Bool { + guard data.count > 3 else { + return false + } + + // Look for "GIF" header to identify GIF images + var header = [UInt8](repeating: 0, count: 3) + data.copyBytes(to: &header, count: 3 * MemoryLayout.size) + return header == [0x47, 0x49, 0x46] + } + + public static func createWithColor(_ size: CGSize, color: UIColor) -> UIImage { + UIGraphicsBeginImageContextWithOptions(size, false, 0.0) + let context = UIGraphicsGetCurrentContext() + let rect = CGRect(origin: CGPoint.zero, size: size) + color.setFill() + context!.fill(rect) + let image = UIGraphicsGetImageFromCurrentImageContext() + UIGraphicsEndImageContext() + return image! + } + + public func createScaled(_ size: CGSize) -> UIImage { + UIGraphicsBeginImageContextWithOptions(size, false, 0) + draw(in: CGRect(origin: CGPoint(x: 0, y: 0), size: size)) + let scaledImage = UIGraphicsGetImageFromCurrentImageContext() + UIGraphicsEndImageContext() + return scaledImage! + } + + public static func templateImageNamed(_ name: String) -> UIImage? { + return UIImage(named: name)?.withRenderingMode(.alwaysTemplate) + } + + // TESTING ONLY: not for use in release/production code. + // PNG comparison can return false negatives, be very careful using for non-equal comparison. + // PNG comparison requires UIImages to be constructed the same way in order for the metadata block to match, + // this function ensures that. + // + // This can be verified with this code: + // let image = UIImage(named: "fxLogo")! + // let data = UIImagePNGRepresentation(image)! + // assert(data != UIImagePNGRepresentation(UIImage(data: data)!)) + @available(*, deprecated, message: "use only in testing code") + public func isStrictlyEqual(to other: UIImage) -> Bool { + // Must use same constructor for PNG metadata block to be the same. + let imageA = UIImage(data: UIImagePNGRepresentation(self)!)! + let imageB = UIImage(data: UIImagePNGRepresentation(other)!)! + let dataA = UIImagePNGRepresentation(imageA)! + let dataB = UIImagePNGRepresentation(imageB)! + return dataA == dataB + } +} diff --git a/mobile/ios/Shared/Extensions/URLRequestExtensions.swift b/mobile/ios/Shared/Extensions/URLRequestExtensions.swift new file mode 100644 index 0000000000..4a45bd48a8 --- /dev/null +++ b/mobile/ios/Shared/Extensions/URLRequestExtensions.swift @@ -0,0 +1,14 @@ +/* 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 + +public extension URLRequest { + public enum Method: String { + case get = "GET" + case post = "POST" + case delete = "DELETE" + case put = "PUT" + } +} diff --git a/mobile/ios/Shared/FSUtils.h b/mobile/ios/Shared/FSUtils.h new file mode 100644 index 0000000000..cbec7f5e38 --- /dev/null +++ b/mobile/ios/Shared/FSUtils.h @@ -0,0 +1,17 @@ +/* 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; + +@interface FSUtils : NSObject + +/** + * Grabs all open file descriptions and returns them in a key-value dictionary where + * the key is the descriptor # and the value being the filename. + * + * @return Dictionary of open file descriptors. + */ ++ (NSDictionary * _Nonnull)openFileDescriptors; + +@end diff --git a/mobile/ios/Shared/FSUtils.m b/mobile/ios/Shared/FSUtils.m new file mode 100644 index 0000000000..1d2e136c2e --- /dev/null +++ b/mobile/ios/Shared/FSUtils.m @@ -0,0 +1,42 @@ +/* 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 "FSUtils.h" + +#import +#import +#import +#import + +@implementation FSUtils + ++ (NSDictionary * _Nonnull)openFileDescriptors +{ + int flags; + int fd; + char buf[MAXPATHLEN+1] ; + int n = 1 ; + NSMutableDictionary *dict = [@{} mutableCopy]; + + for (fd = 0; fd < (int) FD_SETSIZE; fd++) { + errno = 0; + flags = fcntl(fd, F_GETFD, 0); + if (flags == -1 && errno) { + if (errno != EBADF) { + return @{}; + } else { + continue; + } + } + fcntl(fd , F_GETPATH, buf ) ; + + dict[@(fd)] = [NSString stringWithCString:buf encoding:NSUTF8StringEncoding]; + ++n ; + } + + return dict; +} + +@end + diff --git a/mobile/ios/Shared/FeatureSwitch.swift b/mobile/ios/Shared/FeatureSwitch.swift new file mode 100644 index 0000000000..996b1be3e7 --- /dev/null +++ b/mobile/ios/Shared/FeatureSwitch.swift @@ -0,0 +1,83 @@ +/* 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 + +/// Steadily growing set of feature switches controlling access to features by populations of Release users. +open class FeatureSwitches { +} + +/// Small class to allow a percentage of users to access a given feature. +/// It is deliberately low tech, and is not remotely changeable. +/// Randomized bucketing is only applied when the app's release build channel. +open class FeatureSwitch { + let featureID: String + let buildChannel: AppBuildChannel + let nonChannelValue: Bool + let percentage: Int + fileprivate let switchKey: String + init(named featureID: String, _ value: Bool = true, allowPercentage percentage: Int, buildChannel: AppBuildChannel = .release) { + self.featureID = featureID + self.percentage = percentage + self.buildChannel = buildChannel + self.nonChannelValue = value + self.switchKey = "feature_switches.\(self.featureID)" + } + + /// Is this user a member of the bucket that is allowed to use this feature. + /// Bucketing is decided with the hash of a UUID, which is randomly generated and cached + /// in the preferences. + /// This gives us stable properties across restarts and new releases. + open func isMember(_ prefs: Prefs) -> Bool { + // Only use bucketing if we're in the correct build channel, and feature flag is true. + guard buildChannel == AppConstants.BuildChannel, nonChannelValue else { + return nonChannelValue + } + + // Check if this feature has been enabled by the user + let key = "\(self.switchKey).enabled" + if let isEnabled = prefs.boolForKey(key) { + return isEnabled + } + + return lowerCaseS(prefs) < self.percentage + } + + /// Is this user always a member of the test set, whatever the percentage probability? + /// This _only_ tests the probabilities, not the other conditions. + open func alwaysMembership(_ prefs: Prefs) -> Bool { + return lowerCaseS(prefs) == 99 + } + + /// Reset the random component of this switch (`lowerCaseS`). This is primarily useful for testing. + open func resetMembership(_ prefs: Prefs) { + let uuidKey = "\(self.switchKey).uuid" + prefs.removeObjectForKey(uuidKey) + } + + // If the set of all possible values the switch can be in is `S` (integers between 0 and 99) + // then the specific value is `s`. + // We use this to compare with the probability of membership. + fileprivate func lowerCaseS(_ prefs: Prefs) -> Int { + // Use a branch of the prefs. + let uuidKey = "\(self.switchKey).uuid" + + let uuidString: String + if let string = prefs.stringForKey(uuidKey) { + uuidString = string + } else { + uuidString = UUID().uuidString + prefs.setString(uuidString, forKey: uuidKey) + } + + let hash = abs(uuidString.hashValue) + + return hash % 100 + } + + open func setMembership(_ isEnabled: Bool, for prefs: Prefs) { + let key = "\(self.switchKey).enabled" + prefs.setBool(isEnabled, forKey: key) + } +} diff --git a/mobile/ios/Shared/Functions.swift b/mobile/ios/Shared/Functions.swift new file mode 100644 index 0000000000..6b81c7c6d8 --- /dev/null +++ b/mobile/ios/Shared/Functions.swift @@ -0,0 +1,253 @@ +/* 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 SwiftyJSON + +// Pipelining. +precedencegroup PipelinePrecedence { + associativity: left +} +infix operator |> : PipelinePrecedence + +public func |> (x: T, f: (T) -> U) -> U { + return f(x) +} + +// Basic currying. +public func curry(_ f: @escaping (A) -> B) -> (A) -> B { + return { a in + return f(a) + } +} + +public func curry(_ f: @escaping (A, B) -> C) -> (A) -> (B) -> C { + return { a in + return { b in + return f(a, b) + } + } +} + +public func curry(_ f: @escaping (A, B, C) -> D) -> (A) -> (B) -> (C) -> D { + return { a in + return { b in + return { c in + return f(a, b, c) + } + } + } +} + +public func curry(_ f: @escaping (A, B, C, D) -> E) -> (A, B, C) -> (D) -> E { + return { (a, b, c) in + return { d in + return f(a, b, c, d) + } + } +} + +// Function composition. +infix operator • + +public func •(f: @escaping (T) -> U, g: @escaping (U) -> V) -> (T) -> V { + return { t in + return g(f(t)) + } +} +public func •(f: @escaping (T) -> Void, g: @escaping () -> V) -> (T) -> V { + return { t in + f(t) + return g() + } +} +public func •(f: @escaping () -> Void, g: @escaping () -> V) -> () -> V { + return { + f() + return g() + } +} + +// Why not simply provide an override for ==? Well, that's scary, and can accidentally recurse. +// This is enough to catch arrays, which Swift will delegate to element-==. +public func optArrayEqual(_ lhs: [T]?, rhs: [T]?) -> Bool { + switch (lhs, rhs) { + case (.none, .none): + return true + case (.none, _): + return false + case (_, .none): + return false + default: + // This delegates to Swift's own array '==', which calls T's == on each element. + return lhs! == rhs! + } +} + +/** + * Given an array, return an array of slices of size `by` (possibly excepting the last slice). + * + * If `by` is longer than the input, returns a single chunk. + * If `by` is less than 1, acts as if `by` is 1. + * If the length of the array isn't a multiple of `by`, the final slice will + * be smaller than `by`, but never empty. + * + * If the input array is empty, returns an empty array. + */ + +public func chunk(_ arr: [T], by: Int) -> [ArraySlice] { + var result = [ArraySlice]() + var chunk = -1 + let size = max(1, by) + for (index, elem) in arr.enumerated() { + if index % size == 0 { + result.append(ArraySlice()) + chunk += 1 + } + result[chunk].append(elem) + } + return result +} + +public func chunkCollection(_ items: T, by: Int, f: ([E]) -> [X]) -> [X] where T.Iterator.Element == E { + assert(by >= 0) + let max = by > 0 ? by : 1 + var i = 0 + var acc: [E] = [] + var results: [X] = [] + var iter = items.makeIterator() + + while let item = iter.next() { + if i >= max { + results.append(contentsOf: f(acc)) + acc = [] + i = 0 + } + acc.append(item) + i += 1 + } + + if !acc.isEmpty { + results.append(contentsOf: f(acc)) + } + + return results +} + +public extension Sequence { + // [T] -> (T -> K) -> [K: [T]] + // As opposed to `groupWith` (to follow Haskell's naming), which would be + // [T] -> (T -> K) -> [[T]] + func groupBy(_ selector: (Self.Iterator.Element) -> Key, transformer: (Self.Iterator.Element) -> Value) -> [Key: [Value]] { + var acc: [Key: [Value]] = [:] + for x in self { + let k = selector(x) + var a = acc[k] ?? [] + a.append(transformer(x)) + acc[k] = a + } + return acc + } + + func zip(_ elems: S) -> [(Self.Iterator.Element, S.Iterator.Element)] { + var rights = elems.makeIterator() + return self.flatMap { lhs in + guard let rhs = rights.next() else { + return nil + } + return (lhs, rhs) + } + } +} + +public func optDictionaryEqual(_ lhs: [K: V]?, rhs: [K: V]?) -> Bool { + switch (lhs, rhs) { + case (.none, .none): + return true + case (.none, _): + return false + case (_, .none): + return false + default: + return lhs! == rhs! + } +} + +/** + * Return members of `a` that aren't nil, changing the type of the sequence accordingly. + */ +public func optFilter(_ a: [T?]) -> [T] { + return a.flatMap { $0 } +} + +/** + * Return a new map with only key-value pairs that have a non-nil value. + */ +public func optFilter(_ source: [K: V?]) -> [K: V] { + var m = [K: V]() + for (k, v) in source { + if let v = v { + m[k] = v + } + } + return m +} + +/** + * Map a function over the values of a map. + */ +public func mapValues(_ source: [K: T], f: ((T) -> U)) -> [K: U] { + var m = [K: U]() + for (k, v) in source { + m[k] = f(v) + } + return m +} + +public func findOneValue(_ map: [K: V], f: (V) -> Bool) -> V? { + for v in map.values { + if f(v) { + return v + } + } + return nil +} + +/** + * Take a JSON array, returning the String elements as an array. + * It's usually convenient for this to accept an optional. + */ +public func jsonsToStrings(_ arr: [JSON]?) -> [String]? { + return arr?.flatMap { $0.stringValue } +} + +// Encapsulate a callback in a way that we can use it with NSTimer. +private class Callback { + private let handler:() -> Void + + init(handler:@escaping () -> Void) { + self.handler = handler + } + + @objc + func go() { + handler() + } +} + +/** + * Taken from http://stackoverflow.com/questions/27116684/how-can-i-debounce-a-method-call + * Allows creating a block that will fire after a delay. Resets the timer if called again before the delay expires. + **/ +public func debounce(_ delay: TimeInterval, action:@escaping () -> Void) -> () -> Void { + let callback = Callback(handler: action) + var timer: Timer? + + return { + // If calling again, invalidate the last timer. + if let timer = timer { + timer.invalidate() + } + timer = Timer(timeInterval: delay, target: callback, selector: #selector(Callback.go), userInfo: nil, repeats: false) + RunLoop.current.add(timer!, forMode: RunLoopMode.defaultRunLoopMode) + } +} diff --git a/mobile/ios/Shared/KeyboardHelper.swift b/mobile/ios/Shared/KeyboardHelper.swift new file mode 100644 index 0000000000..009c50d16f --- /dev/null +++ b/mobile/ios/Shared/KeyboardHelper.swift @@ -0,0 +1,126 @@ +/* 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 UIKit +import Foundation + +/** + * The keyboard state at the time of notification. + */ +public struct KeyboardState { + public let animationDuration: Double + public let animationCurve: UIViewAnimationCurve + private let userInfo: [AnyHashable: Any] + + fileprivate init(_ userInfo: [AnyHashable: Any]) { + self.userInfo = userInfo + animationDuration = userInfo[UIKeyboardAnimationDurationUserInfoKey] as! Double + // HACK: UIViewAnimationCurve doesn't expose the keyboard animation used (curveValue = 7), + // so UIViewAnimationCurve(rawValue: curveValue) returns nil. As a workaround, get a + // reference to an EaseIn curve, then change the underlying pointer data with that ref. + var curve = UIViewAnimationCurve.easeIn + if let curveValue = userInfo[UIKeyboardAnimationCurveUserInfoKey] as? Int { + NSNumber(value: curveValue as Int).getValue(&curve) + } + self.animationCurve = curve + } + + /// Return the height of the keyboard that overlaps with the specified view. This is more + /// accurate than simply using the height of UIKeyboardFrameBeginUserInfoKey since for example + /// on iPad the overlap may be partial or if an external keyboard is attached, the intersection + /// height will be zero. (Even if the height of the *invisible* keyboard will look normal!) + public func intersectionHeightForView(_ view: UIView) -> CGFloat { + if let keyboardFrameValue = userInfo[UIKeyboardFrameEndUserInfoKey] as? NSValue { + let keyboardFrame = keyboardFrameValue.cgRectValue + let convertedKeyboardFrame = view.convert(keyboardFrame, from: nil) + let intersection = convertedKeyboardFrame.intersection(view.bounds) + return intersection.size.height + } + return 0 + } +} + +public protocol KeyboardHelperDelegate: class { + func keyboardHelper(_ keyboardHelper: KeyboardHelper, keyboardWillShowWithState state: KeyboardState) + func keyboardHelper(_ keyboardHelper: KeyboardHelper, keyboardDidShowWithState state: KeyboardState) + func keyboardHelper(_ keyboardHelper: KeyboardHelper, keyboardWillHideWithState state: KeyboardState) +} + +/** + * Convenience class for observing keyboard state. + */ +open class KeyboardHelper: NSObject { + open var currentState: KeyboardState? + + fileprivate var delegates = [WeakKeyboardDelegate]() + + open class var defaultHelper: KeyboardHelper { + struct Singleton { + static let instance = KeyboardHelper() + } + return Singleton.instance + } + + /** + * Starts monitoring the keyboard state. + */ + open func startObserving() { + NotificationCenter.default.addObserver(self, selector: #selector(KeyboardHelper.SELkeyboardWillShow(_:)), name: NSNotification.Name.UIKeyboardWillShow, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(KeyboardHelper.SELkeyboardDidShow(_:)), name: NSNotification.Name.UIKeyboardDidShow, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(KeyboardHelper.SELkeyboardWillHide(_:)), name: NSNotification.Name.UIKeyboardWillHide, object: nil) + } + + deinit { + NotificationCenter.default.removeObserver(self) + } + + /** + * Adds a delegate to the helper. + * Delegates are weakly held. + */ + open func addDelegate(_ delegate: KeyboardHelperDelegate) { + // Reuse any existing slots that have been deallocated. + for weakDelegate in delegates where weakDelegate.delegate == nil { + weakDelegate.delegate = delegate + return + } + + delegates.append(WeakKeyboardDelegate(delegate)) + } + + func SELkeyboardWillShow(_ notification: Notification) { + if let userInfo = notification.userInfo { + currentState = KeyboardState(userInfo) + for weakDelegate in delegates { + weakDelegate.delegate?.keyboardHelper(self, keyboardWillShowWithState: currentState!) + } + } + } + + func SELkeyboardDidShow(_ notification: Notification) { + if let userInfo = notification.userInfo { + currentState = KeyboardState(userInfo) + for weakDelegate in delegates { + weakDelegate.delegate?.keyboardHelper(self, keyboardDidShowWithState: currentState!) + } + } + } + + func SELkeyboardWillHide(_ notification: Notification) { + if let userInfo = notification.userInfo { + currentState = KeyboardState(userInfo) + for weakDelegate in delegates { + weakDelegate.delegate?.keyboardHelper(self, keyboardWillHideWithState: currentState!) + } + } + } +} + +private class WeakKeyboardDelegate { + weak var delegate: KeyboardHelperDelegate? + + init(_ delegate: KeyboardHelperDelegate) { + self.delegate = delegate + } +} diff --git a/mobile/ios/Shared/KeychainCache.swift b/mobile/ios/Shared/KeychainCache.swift new file mode 100644 index 0000000000..dce3789aeb --- /dev/null +++ b/mobile/ios/Shared/KeychainCache.swift @@ -0,0 +1,65 @@ +/* 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 XCGLogger +import SwiftKeychainWrapper +import SwiftyJSON + +private let log = Logger.keychainLogger + +public protocol JSONLiteralConvertible { + func asJSON() -> JSON +} + +open class KeychainCache { + open let branch: String + open let label: String + + open var value: T? { + didSet { + checkpoint() + } + } + + public init(branch: String, label: String, value: T?) { + self.branch = branch + self.label = label + self.value = value + } + + open class func fromBranch(_ branch: String, withLabel label: String?, withDefault defaultValue: T? = nil, factory: (JSON) -> T?) -> KeychainCache { + if let l = label { + let key = "\(branch).\(l)" + KeychainWrapper.sharedAppContainerKeychain.ensureStringItemAccessibility(.afterFirstUnlock, forKey: key) + if let s = KeychainWrapper.sharedAppContainerKeychain.string(forKey: key) { + if let t = factory(JSON(parseJSON: s)) { + log.info("Read \(branch) from Keychain with label \(branch).\(l).") + return KeychainCache(branch: branch, label: l, value: t) + } else { + log.warning("Found \(branch) in Keychain with label \(branch).\(l), but could not parse it.") + } + } else { + log.warning("Did not find \(branch) in Keychain with label \(branch).\(l).") + } + } else { + log.warning("Did not find \(branch) label in Keychain.") + } + // Fall through to missing. + log.warning("Failed to read \(branch) from Keychain.") + let label = label ?? Bytes.generateGUID() + return KeychainCache(branch: branch, label: label, value: defaultValue) + } + + open func checkpoint() { + log.info("Storing \(self.branch) in Keychain with label \(self.branch).\(self.label).") + // TODO: PII logging. + if let value = value, + let jsonString = value.asJSON().stringValue() { + KeychainWrapper.sharedAppContainerKeychain.set(jsonString, forKey: "\(branch).\(label)", withAccessibility: .afterFirstUnlock) + } else { + KeychainWrapper.sharedAppContainerKeychain.removeObject(forKey: "\(branch).\(label)") + } + } +} diff --git a/mobile/ios/Shared/LaunchArguments.swift b/mobile/ios/Shared/LaunchArguments.swift new file mode 100644 index 0000000000..5a4679fcc3 --- /dev/null +++ b/mobile/ios/Shared/LaunchArguments.swift @@ -0,0 +1,12 @@ +/* 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 + +public struct LaunchArguments { + public static let Test = "FIREFOX_TEST" + public static let SkipIntro = "FIREFOX_SKIP_INTRO" + public static let SkipWhatsNew = "FIREFOX_SKIP_WHATS_NEW" + public static let ClearProfile = "FIREFOX_CLEAR_PROFILE" +} diff --git a/mobile/ios/Shared/Loader.swift b/mobile/ios/Shared/Loader.swift new file mode 100644 index 0000000000..b73a2a0589 --- /dev/null +++ b/mobile/ios/Shared/Loader.swift @@ -0,0 +1,33 @@ +/* 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 + +/** + * Interface for listening to Loader updates. + */ +public protocol LoaderListener: class { + associatedtype T + func loader(dataLoaded data: T) +} + +/** + * Base implementation for a "push" data model. + * Interested clients add themselves as listeners for data changes. + */ +open class Loader where T == ListenerType.T { + private let listeners = WeakList() + + public init() {} + + open func addListener(_ listener: ListenerType) { + listeners.insert(listener) + } + + open func load(_ data: T) { + for listener in listeners { + listener.loader(dataLoaded: data) + } + } +} diff --git a/mobile/ios/Shared/Logger.swift b/mobile/ios/Shared/Logger.swift new file mode 100644 index 0000000000..44740ad95e --- /dev/null +++ b/mobile/ios/Shared/Logger.swift @@ -0,0 +1,96 @@ +/* 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 XCGLogger + +public struct Logger {} + +// MARK: - Singleton Logger Instances +public extension Logger { + static let logPII = false + + /// Logger used for recording happenings with Sync, Accounts, Providers, Storage, and Profiles + static let syncLogger = RollingFileLogger(filenameRoot: "sync", logDirectoryPath: Logger.logFileDirectoryPath()) + + /// Logger used for recording frontend/browser happenings + static let browserLogger = RollingFileLogger(filenameRoot: "browser", logDirectoryPath: Logger.logFileDirectoryPath()) + + /// Logger used for recording interactions with the keychain + static let keychainLogger: XCGLogger = Logger.fileLoggerWithName("keychain") + + /// Logger used for logging database errors such as corruption + static let corruptLogger: RollingFileLogger = { + let logger = RollingFileLogger(filenameRoot: "corruptLogger", logDirectoryPath: Logger.logFileDirectoryPath()) + logger.newLogWithDate(Date()) + return logger + }() + + /** + Return the log file directory path. If the directory doesn't exist, make sure it exist first before returning the path. + + :returns: Directory path where log files are stored + */ + static func logFileDirectoryPath() -> String? { + if let cacheDir = NSSearchPathForDirectoriesInDomains(.cachesDirectory, .userDomainMask, true).first { + let logDir = "\(cacheDir)/Logs" + if !FileManager.default.fileExists(atPath: logDir) { + do { + try FileManager.default.createDirectory(atPath: logDir, withIntermediateDirectories: false, attributes: nil) + return logDir + } catch _ as NSError { + return nil + } + } else { + return logDir + } + } + + return nil + } + + static private func fileLoggerWithName(_ name: String) -> XCGLogger { + let log = XCGLogger() + if let logFileURL = urlForLogNamed(name) { + let fileDestination = FileDestination( + owner: log, + writeToFile: logFileURL.absoluteString, + identifier: "com.mozilla.firefox.filelogger.\(name)" + ) + log.add(destination: fileDestination) + } + return log + } + + static private func urlForLogNamed(_ name: String) -> URL? { + guard let logDir = Logger.logFileDirectoryPath() else { + return nil + } + + return URL(string: "\(logDir)/\(name).log") + } + + /** + Grabs all of the configured logs that write to disk and returns them in NSData format along with their + associated filename. + + - returns: Tuples of filenames to each file's contexts in a NSData object + */ + static func diskLogFilenamesAndData() throws -> [(String, Data?)] { + var filenamesAndURLs = [(String, URL)]() + filenamesAndURLs.append(("browser", urlForLogNamed("browser")!)) + filenamesAndURLs.append(("keychain", urlForLogNamed("keychain")!)) + + // Grab all sync log files + do { + filenamesAndURLs += try syncLogger.logFilenamesAndURLs() + filenamesAndURLs += try corruptLogger.logFilenamesAndURLs() + filenamesAndURLs += try browserLogger.logFilenamesAndURLs() + } catch _ { + } + + return filenamesAndURLs.map { ($0, try? Data(contentsOf: URL(fileURLWithPath: $1.absoluteString))) } + } +} + diff --git a/mobile/ios/Shared/NotificationConstants.swift b/mobile/ios/Shared/NotificationConstants.swift new file mode 100644 index 0000000000..456f966e71 --- /dev/null +++ b/mobile/ios/Shared/NotificationConstants.swift @@ -0,0 +1,31 @@ +/* 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/. */ + +public let NotificationDataLoginDidChange = Notification.Name("Data:Login:DidChange") + +// add a property to allow the observation of firefox accounts +public let NotificationFirefoxAccountChanged = Notification.Name("FirefoxAccountChangedNotification") + +public let NotificationFirefoxAccountProfileChanged = Notification.Name("NotificationFirefoxAccountProfileChanged") + +public let NotificationFirefoxAccountDeviceRegistrationUpdated = Notification.Name("FirefoxAccountDeviceRegistrationUpdated") + +public let NotificationPrivateDataClearedHistory = Notification.Name("PrivateDataClearedHistoryNotification") + +// Fired when the user finishes navigating to a page and the location has changed +public let NotificationOnLocationChange = Notification.Name("OnLocationChange") + +// Fired when a the page metadata extraction script has completed and is being passed back to the native client +public let NotificationOnPageMetadataFetched = Notification.Name("OnPageMetadataFetched") + +// Fired when the login synchronizer has finished applying remote changes +public let NotificationDataRemoteLoginChangesWereApplied = Notification.Name("NotificationDataRemoteLoginChangesWereApplied") + +// Fired when the FxA account has been verified. This should only be fired by the FxALoginStateMachine. +public let NotificationFirefoxAccountVerified = Notification.Name("FirefoxAccountVerifiedNotification") + +// MARK: Notification UserInfo Keys +public let NotificationUserInfoKeyHasSyncableAccount = Notification.Name("NotificationUserInfoKeyHasSyncableAccount") + +public let NotificationDidRestoreSession = Notification.Name("NotificationDidRestoreSession") diff --git a/mobile/ios/Shared/Prefs.swift b/mobile/ios/Shared/Prefs.swift new file mode 100644 index 0000000000..39a12be94b --- /dev/null +++ b/mobile/ios/Shared/Prefs.swift @@ -0,0 +1,193 @@ +/* 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 + +public struct PrefsKeys { + public static let KeyLastRemoteTabSyncTime = "lastRemoteTabSyncTime" + public static let KeyLastSyncFinishTime = "lastSyncFinishTime" + public static let KeyDefaultHomePageURL = "KeyDefaultHomePageURL" + public static let KeyNoImageModeStatus = "NoImageModeStatus" + public static let KeyNightModeButtonIsInMenu = "NightModeButtonIsInMenuPrefKey" + public static let KeyNightModeStatus = "NightModeStatus" + public static let KeyMailToOption = "MailToOption" + public static let HasFocusInstalled = "HasFocusInstalled" + public static let HasPocketInstalled = "HasPocketInstalled" + + //Activity Stream + public static let KeyTopSitesCacheIsValid = "topSitesCacheIsValid" + public static let KeyTopSitesCacheSize = "topSitesCacheSize" + public static let KeyNewTab = "NewTabPrefKey" + public static let ASPocketStoriesVisible = "ASPocketStoriesVisible" + public static let ASRecentHighlightsVisible = "ASRecentHighlightsVisible" + public static let ASBookmarkHighlightsVisible = "ASBookmarkHighlightsVisible" + public static let ASLastInvalidation = "ASLastInvalidation" + + public static let KeyUseCustomSyncService = "useCustomSyncService" + public static let KeyCustomSyncToken = "customSyncTokenServer" + public static let KeyCustomSyncProfile = "customSyncProfileServer" + public static let KeyCustomSyncOauth = "customSyncOauthServer" + public static let KeyCustomSyncAuth = "customSyncAuthServer" + public static let KeyCustomSyncWeb = "customSyncWebServer" + +} + +public struct PrefsDefaults { + public static let ChineseHomePageURL = "http://mobile.firefoxchina.cn/" + public static let ChineseNewTabDefault = "HomePage" +} + +public protocol Prefs { + func getBranchPrefix() -> String + func branch(_ branch: String) -> Prefs + func setTimestamp(_ value: Timestamp, forKey defaultName: String) + func setLong(_ value: UInt64, forKey defaultName: String) + func setLong(_ value: Int64, forKey defaultName: String) + func setInt(_ value: Int32, forKey defaultName: String) + func setString(_ value: String, forKey defaultName: String) + func setBool(_ value: Bool, forKey defaultName: String) + func setObject(_ value: Any?, forKey defaultName: String) + func stringForKey(_ defaultName: String) -> String? + func objectForKey(_ defaultName: String) -> T? + func boolForKey(_ defaultName: String) -> Bool? + func intForKey(_ defaultName: String) -> Int32? + func timestampForKey(_ defaultName: String) -> Timestamp? + func longForKey(_ defaultName: String) -> Int64? + func unsignedLongForKey(_ defaultName: String) -> UInt64? + func stringArrayForKey(_ defaultName: String) -> [String]? + func arrayForKey(_ defaultName: String) -> [Any]? + func dictionaryForKey(_ defaultName: String) -> [String: Any]? + func removeObjectForKey(_ defaultName: String) + func clearAll() +} + +open class MockProfilePrefs: Prefs { + let prefix: String + + open func getBranchPrefix() -> String { + return self.prefix + } + + // Public for testing. + open var things: NSMutableDictionary = NSMutableDictionary() + + public init(things: NSMutableDictionary, prefix: String) { + self.things = things + self.prefix = prefix + } + + public init() { + self.prefix = "" + } + + open func branch(_ branch: String) -> Prefs { + return MockProfilePrefs(things: self.things, prefix: self.prefix + branch + ".") + } + + private func name(_ name: String) -> String { + return self.prefix + name + } + + open func setTimestamp(_ value: Timestamp, forKey defaultName: String) { + self.setLong(value, forKey: defaultName) + } + + open func setLong(_ value: UInt64, forKey defaultName: String) { + setObject(NSNumber(value: value as UInt64), forKey: defaultName) + } + + open func setLong(_ value: Int64, forKey defaultName: String) { + setObject(NSNumber(value: value as Int64), forKey: defaultName) + } + + open func setInt(_ value: Int32, forKey defaultName: String) { + things[name(defaultName)] = NSNumber(value: value as Int32) + } + + open func setString(_ value: String, forKey defaultName: String) { + things[name(defaultName)] = value + } + + open func setBool(_ value: Bool, forKey defaultName: String) { + things[name(defaultName)] = value + } + + open func setObject(_ value: Any?, forKey defaultName: String) { + things[name(defaultName)] = value + } + + open func stringForKey(_ defaultName: String) -> String? { + return things[name(defaultName)] as? String + } + + open func boolForKey(_ defaultName: String) -> Bool? { + return things[name(defaultName)] as? Bool + } + + open func objectForKey(_ defaultName: String) -> T? { + return things[name(defaultName)] as? T + } + + open func timestampForKey(_ defaultName: String) -> Timestamp? { + return unsignedLongForKey(defaultName) + } + + open func unsignedLongForKey(_ defaultName: String) -> UInt64? { + let num = things[name(defaultName)] as? UInt64 + if let num = num { + return num + } + return nil + } + + open func longForKey(_ defaultName: String) -> Int64? { + let num = things[name(defaultName)] as? Int64 + if let num = num { + return num + } + return nil + } + + open func intForKey(_ defaultName: String) -> Int32? { + let num = things[name(defaultName)] as? Int32 + if let num = num { + return num + } + return nil + } + + open func stringArrayForKey(_ defaultName: String) -> [String]? { + if let arr = self.arrayForKey(defaultName) { + if let arr = arr as? [String] { + return arr + } + } + return nil + } + + open func arrayForKey(_ defaultName: String) -> [Any]? { + let r: Any? = things.object(forKey: name(defaultName)) as Any? + if r == nil { + return nil + } + if let arr = r as? [Any] { + return arr + } + return nil + } + + open func dictionaryForKey(_ defaultName: String) -> [String: Any]? { + return things.object(forKey: name(defaultName)) as? [String: Any] + } + + open func removeObjectForKey(_ defaultName: String) { + self.things.removeObject(forKey: name(defaultName)) + } + + open func clearAll() { + let dictionary = things as! [String: Any] + let keysToDelete: [String] = dictionary.keys.filter { $0.startsWith(self.prefix) } + things.removeObjects(forKeys: keysToDelete) + } +} diff --git a/mobile/ios/Shared/RemoteDevices.swift b/mobile/ios/Shared/RemoteDevices.swift new file mode 100644 index 0000000000..d8bcac60cb --- /dev/null +++ b/mobile/ios/Shared/RemoteDevices.swift @@ -0,0 +1,23 @@ +/* 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/. */ + +public protocol RemoteDevices { + func replaceRemoteDevices(_ remoteDevices: [RemoteDevice]) -> Success +} + +open class RemoteDevice { + public let id: String? + public let name: String + public let type: String? + public let isCurrentDevice: Bool + public let lastAccessTime: Timestamp? + + public init(id: String?, name: String, type: String?, isCurrentDevice: Bool, lastAccessTime: Timestamp?) { + self.id = id + self.name = name + self.type = type + self.isCurrentDevice = isCurrentDevice + self.lastAccessTime = lastAccessTime + } +} diff --git a/mobile/ios/Shared/RollingFileLogger.swift b/mobile/ios/Shared/RollingFileLogger.swift new file mode 100644 index 0000000000..a47f5ac713 --- /dev/null +++ b/mobile/ios/Shared/RollingFileLogger.swift @@ -0,0 +1,115 @@ +/* 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 XCGLogger + +//// A rolling file logger that saves to a different log file based on given timestamp. +open class RollingFileLogger: XCGLogger { + + fileprivate static let TwoMBsInBytes: Int64 = 2 * 100000 + fileprivate let sizeLimit: Int64 + fileprivate let logDirectoryPath: String? + + let fileLogIdentifierPrefix = "com.mozilla.firefox.filelogger." + + fileprivate static let DateFormatter: DateFormatter = { + let formatter = Foundation.DateFormatter() + formatter.dateFormat = "yyyyMMdd'T'HHmmssZ" + return formatter + }() + + let root: String + + public init(filenameRoot: String, logDirectoryPath: String?, sizeLimit: Int64 = TwoMBsInBytes) { + root = filenameRoot + self.sizeLimit = sizeLimit + self.logDirectoryPath = logDirectoryPath + super.init() + } + + /** + Create a new log file with the given timestamp to log events into + + :param: date Date for with to start and mark the new log file + */ + open func newLogWithDate(_ date: Date) { + // Don't start a log if we don't have a valid log directory path + if logDirectoryPath == nil { + return + } + + if let filename = filenameWithRoot(root, withDate: date) { + remove(destinationWithIdentifier: fileLogIdentifierWithRoot(root)) + add(destination: FileDestination(owner: self, writeToFile: filename, identifier: fileLogIdentifierWithRoot(root))) + info("Created file destination for logger with root: \(self.root) and timestamp: \(date)") + } else { + error("Failed to create a new log with root name: \(self.root) and timestamp: \(date)") + } + } + + open func deleteOldLogsDownToSizeLimit() { + // Check to see we haven't hit our size limit and if we did, clear out some logs to make room. + while sizeOfAllLogFilesWithPrefix(self.root, exceedsSizeInBytes: sizeLimit) { + deleteOldestLogWithPrefix(self.root) + } + } + + open func logFilenamesAndURLs() throws -> [(String, URL)] { + guard let logPath = logDirectoryPath else { + return [] + } + + let files = try FileManager.default.contentsOfDirectoryAtPath(logPath, withFilenamePrefix: root) + return files.flatMap { filename in + if let url = URL(string: "\(logPath)/\(filename)") { + return (filename, url) + } + return nil + } + } + + fileprivate func deleteOldestLogWithPrefix(_ prefix: String) { + if logDirectoryPath == nil { + return + } + + do { + let logFiles = try FileManager.default.contentsOfDirectoryAtPath(logDirectoryPath!, withFilenamePrefix: prefix) + if let oldestLogFilename = logFiles.first { + try FileManager.default.removeItem(atPath: "\(logDirectoryPath!)/\(oldestLogFilename)") + } + } catch _ as NSError { + error("Shouldn't get here") + return + } + } + + fileprivate func sizeOfAllLogFilesWithPrefix(_ prefix: String, exceedsSizeInBytes threshold: Int64) -> Bool { + guard let path = logDirectoryPath else { + return false + } + + let logDirURL = URL(fileURLWithPath: path) + do { + return try FileManager.default.allocatedSizeOfDirectoryAtURL(logDirURL, forFilesPrefixedWith: prefix, isLargerThanBytes: threshold) + } catch let errorValue as NSError { + error("Error determining log directory size: \(errorValue)") + } + + return false + } + + fileprivate func filenameWithRoot(_ root: String, withDate date: Date) -> String? { + if let dir = logDirectoryPath { + return "\(dir)/\(root).\(RollingFileLogger.DateFormatter.string(from: date)).log" + } + + return nil + } + + fileprivate func fileLogIdentifierWithRoot(_ root: String) -> String { + return "\(fileLogIdentifierPrefix).\(root)" + } +} diff --git a/mobile/ios/Shared/SentryIntegration.swift b/mobile/ios/Shared/SentryIntegration.swift new file mode 100644 index 0000000000..763c11fbc1 --- /dev/null +++ b/mobile/ios/Shared/SentryIntegration.swift @@ -0,0 +1,163 @@ +/* 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 Sentry + +public enum SentryTag: String { + case swiftData = "SwiftData" + case browserDB = "BrowserDB" + case notificationService = "NotificationService" + case unifiedTelemetry = "UnifiedTelemetry" + case general = "General" + case tabManager = "TabManager" + case bookmarks = "Bookmarks" +} + +public class Sentry { + public static let shared = Sentry() + + public static var crashedLastLaunch: Bool { + return Client.shared?.crashedLastLaunch() ?? false + } + + private let SentryDSNKey = "SentryDSN" + private let SentryDeviceAppHashKey = "SentryDeviceAppHash" + private let DefaultDeviceAppHash = "0000000000000000000000000000000000000000" + private let DeviceAppHashLength = UInt(20) + + private var enabled = false + + private var attributes: [String: Any] = [:] + + public func setup(sendUsageData: Bool) { + assert(!enabled, "Sentry.setup() should only be called once") + + if DeviceInfo.isSimulator() { + Logger.browserLogger.debug("Not enabling Sentry; Running in Simulator") + return + } + + if !sendUsageData { + Logger.browserLogger.debug("Not enabling Sentry; Not enabled by user choice") + return + } + + guard let dsn = Bundle.main.object(forInfoDictionaryKey: SentryDSNKey) as? String, !dsn.isEmpty else { + Logger.browserLogger.debug("Not enabling Sentry; Not configured in Info.plist") + return + } + + Logger.browserLogger.debug("Enabling Sentry crash handler") + + do { + Client.shared = try Client(dsn: dsn) + try Client.shared?.startCrashHandler() + enabled = true + + // If we have not already for this install, generate a completely random identifier + // for this device. It is stored in the app group so that the same value will + // be used for both the main application and the app extensions. + if let defaults = UserDefaults(suiteName: AppInfo.sharedContainerIdentifier), defaults.string(forKey: SentryDeviceAppHashKey) == nil { + defaults.set(Bytes.generateRandomBytes(DeviceAppHashLength).hexEncodedString, forKey: SentryDeviceAppHashKey) + defaults.synchronize() + } + + // For all outgoing reports, override the default device identifier with our own random + // version. Default to a blank (zero) identifier in case of errors. + Client.shared?.beforeSerializeEvent = { event in + let deviceAppHash = UserDefaults(suiteName: AppInfo.sharedContainerIdentifier)?.string(forKey: self.SentryDeviceAppHashKey) + event.context?.appContext?["device_app_hash"] = deviceAppHash ?? self.DefaultDeviceAppHash + + var attributes = event.extra ?? [:] + attributes.merge(with: self.attributes) + event.extra = attributes + } + } catch let error { + Logger.browserLogger.error("Failed to initialize Sentry: \(error)") + } + + } + + public func crash() { + Client.shared?.crash() + } + + /* + This is the behaviour we want for Sentry logging + .info .error .severe + Debug y y y + Beta y y y + Relase n n y + */ + private func shouldNotSendEventFor(_ severity: SentrySeverity) -> Bool { + return !enabled || (AppConstants.BuildChannel == .release && severity != .fatal) + } + + private func makeEvent(message: String, tag: String, severity: SentrySeverity, extra: [String: Any]?) -> Event { + let event = Event(level: severity) + event.message = message + event.tags = ["tag": tag] + if let extra = extra { + event.extra = extra + } + return event + } + + public func send(message: String, tag: SentryTag = .general, severity: SentrySeverity = .info, extra: [String: Any]? = nil, description: String? = nil, completion: SentryRequestFinished? = nil) { + // Build the dictionary + var extraEvents: [String: Any] = [:] + if let paramEvents = extra { + extraEvents.merge(with: paramEvents) + } + if let extraString = description { + extraEvents.merge(with: ["errorDescription": extraString]) + } + printMessage(message: message, extra: extraEvents) + + // Only report fatal errors on release + if shouldNotSendEventFor(severity) { + completion?(nil) + return + } + + let event = makeEvent(message: message, tag: tag.rawValue, severity: severity, extra: extraEvents) + Client.shared?.send(event: event, completion: completion) + } + + public func sendWithStacktrace(message: String, tag: SentryTag = .general, severity: SentrySeverity = .info, extra: [String: Any]? = nil, description: String? = nil, completion: SentryRequestFinished? = nil) { + var extraEvents: [String: Any] = [:] + if let paramEvents = extra { + extraEvents.merge(with: paramEvents) + } + if let extraString = description { + extraEvents.merge(with: ["errorDescription": extraString]) + } + printMessage(message: message, extra: extraEvents) + + // Do not send messages to Sentry if disabled OR if we are not on beta and the severity isnt severe + if shouldNotSendEventFor(severity) { + completion?(nil) + return + } + Client.shared?.snapshotStacktrace { + let event = self.makeEvent(message: message, tag: tag.rawValue, severity: severity, extra: extraEvents) + Client.shared?.appendStacktrace(to: event) + event.debugMeta = nil + Client.shared?.send(event: event, completion: completion) + } + } + + public func addAttributes(_ attributes: [String: Any]) { + self.attributes.merge(with: attributes) + } + + private func printMessage(message: String, extra: [String: Any]? = nil) { + let string = extra?.reduce("") { (result: String, arg1) in + let (key, value) = arg1 + return "\(result), \(key): \(value)" + } + Logger.browserLogger.debug("Sentry: \(message) \(string ??? "")") + } +} diff --git a/mobile/ios/Shared/Shared-Bridging-Header.h b/mobile/ios/Shared/Shared-Bridging-Header.h new file mode 100644 index 0000000000..3dafa01863 --- /dev/null +++ b/mobile/ios/Shared/Shared-Bridging-Header.h @@ -0,0 +1,9 @@ +#ifndef Client_Shared_Bridging_Header_h +#define Client_Shared_Bridging_Header_h + +#import +#import +#import "FSUtils.h" +#import "CrashSimulator.h" + +#endif diff --git a/mobile/ios/Shared/SupportUtils.swift b/mobile/ios/Shared/SupportUtils.swift new file mode 100644 index 0000000000..ef8f29d912 --- /dev/null +++ b/mobile/ios/Shared/SupportUtils.swift @@ -0,0 +1,21 @@ +/* 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 + +/// Utility functions related to SUMO. +public struct SupportUtils { + /// Construct a NSURL pointing to a specific topic on SUMO. The topic should be a non-escaped string. It will + /// be properly escaped by this function. + /// + /// The resulting NSURL will include the app version, operating system and locale code. For example, a topic + /// "cheese" will be turned into a link that looks like https://support.mozilla.org/1/mobile/2.0/iOS/en-US/cheese + public static func URLForTopic(_ topic: String) -> URL? { + guard let escapedTopic = topic.addingPercentEncoding(withAllowedCharacters: CharacterSet.urlPathAllowed), + let languageIdentifier = Locale.preferredLanguages.first else { + return nil + } + return URL(string: "https://support.mozilla.org/1/mobile/\(AppInfo.appVersion)/iOS/\(languageIdentifier)/\(escapedTopic)") + } +} diff --git a/mobile/ios/Shared/Supporting Files/Info.plist b/mobile/ios/Shared/Supporting Files/Info.plist new file mode 100644 index 0000000000..b3205ecaf4 --- /dev/null +++ b/mobile/ios/Shared/Supporting Files/Info.plist @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + FMWK + CFBundleShortVersionString + 10.6 + CFBundleSignature + ???? + CFBundleVersion + 1 + NSPrincipalClass + + + diff --git a/mobile/ios/Shared/SystemUtils.swift b/mobile/ios/Shared/SystemUtils.swift new file mode 100644 index 0000000000..e7621e2626 --- /dev/null +++ b/mobile/ios/Shared/SystemUtils.swift @@ -0,0 +1,69 @@ +/* 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 + +/** + * System helper methods written in Swift. + */ +public struct SystemUtils { + + /** + Returns an accurate version of the system uptime even while the device is asleep. + http://stackoverflow.com/questions/12488481/getting-ios-system-uptime-that-doesnt-pause-when-asleep + + - returns: Time interval since last reboot. + */ + public static func systemUptime() -> TimeInterval { + var boottime = timeval() + var mib = [CTL_KERN, KERN_BOOTTIME] + var size = MemoryLayout.stride + var now = time_t() + time(&now) + + sysctl(&mib, u_int(mib.count), &boottime, &size, nil, 0) + let tv_sec: time_t = withUnsafePointer(to: &boottime.tv_sec) { $0.pointee } + return TimeInterval(now - tv_sec) + } +} + +extension SystemUtils { + // This should be run on first run of the application. + // It shouldn't be run from an extension. + // Its function is to write a lock file that is only accessible from the application, + // and not accessible from extension when the device is locked. Thus, we can tell if an extension is being run + // when the device is locked. + public static func onFirstRun() { + guard let lockFileURL = lockedDeviceURL else { + return + } + + let lockFile = lockFileURL.path + let fm = FileManager.default + if fm.fileExists(atPath: lockFile) { + return + } + let contents = "Device is unlocked".data(using: String.Encoding.utf8) + fm.createFile(atPath: lockFile, contents: contents, attributes: [FileAttributeKey.protectionKey.rawValue: FileProtectionType.complete]) + } + + private static var lockedDeviceURL: URL? { + let directoryURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: AppInfo.sharedContainerIdentifier) + return directoryURL?.appendingPathComponent("security.dummy") + } + + public static func isDeviceLocked() -> Bool { + guard let lockFileURL = lockedDeviceURL else { + return true + } + do { + _ = try Data(contentsOf: lockFileURL, options: .mappedIfSafe) + return false + } catch let err as NSError { + return err.code == 257 + } catch _ { + return true + } + } +} diff --git a/mobile/ios/Shared/TimeConstants.swift b/mobile/ios/Shared/TimeConstants.swift new file mode 100644 index 0000000000..0c1c81afd4 --- /dev/null +++ b/mobile/ios/Shared/TimeConstants.swift @@ -0,0 +1,172 @@ +/* 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 + +public typealias Timestamp = UInt64 +public typealias MicrosecondTimestamp = UInt64 + +public let ThreeWeeksInSeconds = 3 * 7 * 24 * 60 * 60 + +public let OneYearInMilliseconds = 12 * OneMonthInMilliseconds +public let OneMonthInMilliseconds = 30 * OneDayInMilliseconds +public let OneWeekInMilliseconds = 7 * OneDayInMilliseconds +public let OneDayInMilliseconds = 24 * OneHourInMilliseconds +public let OneHourInMilliseconds = 60 * OneMinuteInMilliseconds +public let OneMinuteInMilliseconds = 60 * OneSecondInMilliseconds +public let OneSecondInMilliseconds: UInt64 = 1000 + +fileprivate let rfc822DateFormatter: DateFormatter = { + let dateFormatter = DateFormatter() + dateFormatter.timeZone = TimeZone(abbreviation: "GMT") + dateFormatter.dateFormat = "EEE',' dd MMM yyyy HH':'mm':'ss 'GMT'" + dateFormatter.locale = Locale(identifier: "en_US") + return dateFormatter +}() + +extension Timestamp { + public static func uptimeInMilliseconds() -> Timestamp { + return Timestamp(DispatchTime.now().uptimeNanoseconds) / 1000000 + } +} + +extension Date { + public static func now() -> Timestamp { + return UInt64(1000 * Date().timeIntervalSince1970) + } + + public static func nowNumber() -> NSNumber { + return NSNumber(value: now() as UInt64) + } + + public static func nowMicroseconds() -> MicrosecondTimestamp { + return UInt64(1000000 * Date().timeIntervalSince1970) + } + + public static func fromTimestamp(_ timestamp: Timestamp) -> Date { + return Date(timeIntervalSince1970: Double(timestamp) / 1000) + } + + public static func fromMicrosecondTimestamp(_ microsecondTimestamp: MicrosecondTimestamp) -> Date { + return Date(timeIntervalSince1970: Double(microsecondTimestamp) / 1000000) + } + + public func toRelativeTimeString() -> String { + let now = Date() + + let units: NSCalendar.Unit = [NSCalendar.Unit.second, NSCalendar.Unit.minute, NSCalendar.Unit.day, NSCalendar.Unit.weekOfYear, NSCalendar.Unit.month, NSCalendar.Unit.year, NSCalendar.Unit.hour] + + let components = (Calendar.current as NSCalendar).components(units, + from: self, + to: now, + options: []) + + if components.year! > 0 { + return String(format: DateFormatter.localizedString(from: self, dateStyle: DateFormatter.Style.short, timeStyle: DateFormatter.Style.short)) + } + + if components.month == 1 { + return String(format: NSLocalizedString("more than a month ago", comment: "Relative date for dates older than a month and less than two months.")) + } + + if components.month! > 1 { + return String(format: DateFormatter.localizedString(from: self, dateStyle: DateFormatter.Style.short, timeStyle: DateFormatter.Style.short)) + } + + if components.weekOfYear! > 0 { + return String(format: NSLocalizedString("more than a week ago", comment: "Description for a date more than a week ago, but less than a month ago.")) + } + + if components.day == 1 { + return String(format: NSLocalizedString("yesterday", comment: "Relative date for yesterday.")) + } + + if components.day! > 1 { + return String(format: NSLocalizedString("this week", comment: "Relative date for date in past week."), String(describing: components.day)) + } + + if components.hour! > 0 || components.minute! > 0 { + let absoluteTime = DateFormatter.localizedString(from: self, dateStyle: DateFormatter.Style.none, timeStyle: DateFormatter.Style.short) + let format = NSLocalizedString("today at %@", comment: "Relative date for date older than a minute.") + return String(format: format, absoluteTime) + } + + return String(format: NSLocalizedString("just now", comment: "Relative time for a tab that was visited within the last few moments.")) + } + + public func toRFC822String() -> String { + return rfc822DateFormatter.string(from: self) + } +} + +let MaxTimestampAsDouble: Double = Double(UInt64.max) + +/** This is just like decimalSecondsStringToTimestamp, but it looks for values that seem to be + * milliseconds and fixes them. That's necessary because Firefox for iOS <= 7.3 uploaded millis + * when seconds were expected. + */ +public func someKindOfTimestampStringToTimestamp(_ input: String) -> Timestamp? { + var double = 0.0 + if Scanner(string: input).scanDouble(&double) { + // This should never happen. Hah! + if double.isNaN || double.isInfinite { + return nil + } + + // `double` will be either huge or negatively huge on overflow, and 0 on underflow. + // We clamp to reasonable ranges. + if double < 0 { + return nil + } + + if double >= MaxTimestampAsDouble { + // Definitely not representable as a timestamp if the seconds are this large! + return nil + } + + if double > 1000000000000 { + // Oh, this was in milliseconds. + return Timestamp(double) + } + + let millis = double * 1000 + if millis >= MaxTimestampAsDouble { + // Not representable as a timestamp. + return nil + } + + return Timestamp(millis) + } + return nil +} + +public func decimalSecondsStringToTimestamp(_ input: String) -> Timestamp? { + var double = 0.0 + if Scanner(string: input).scanDouble(&double) { + // This should never happen. Hah! + if double.isNaN || double.isInfinite { + return nil + } + + // `double` will be either huge or negatively huge on overflow, and 0 on underflow. + // We clamp to reasonable ranges. + if double < 0 { + return nil + } + + let millis = double * 1000 + if millis >= MaxTimestampAsDouble { + // Not representable as a timestamp. + return nil + } + + return Timestamp(millis) + } + return nil +} + +public func millisecondsToDecimalSeconds(_ input: Timestamp) -> String { + let val: Double = Double(input) / 1000 + return String(format: "%.2F", val) +} diff --git a/mobile/ios/Shared/UserAgent.swift b/mobile/ios/Shared/UserAgent.swift new file mode 100644 index 0000000000..2459e5e987 --- /dev/null +++ b/mobile/ios/Shared/UserAgent.swift @@ -0,0 +1,131 @@ +/* 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 AVFoundation +import UIKit + +open class UserAgent { + private static var defaults = UserDefaults(suiteName: AppInfo.sharedContainerIdentifier)! + + private static func clientUserAgent(prefix: String) -> String { + return "\(prefix)/\(AppInfo.appVersion)b\(AppInfo.buildNumber) (\(DeviceInfo.deviceModel()); iPhone OS \(UIDevice.current.systemVersion)) (\(AppInfo.displayName))" + } + + open static var syncUserAgent: String { + return clientUserAgent(prefix: "Firefox-iOS-Sync") + } + + open static var tokenServerClientUserAgent: String { + return clientUserAgent(prefix: "Firefox-iOS-Token") + } + + open static var fxaUserAgent: String { + return clientUserAgent(prefix: "Firefox-iOS-FxA") + } + + open static var defaultClientUserAgent: String { + return clientUserAgent(prefix: "Firefox-iOS") + } + + /** + * Use this if you know that a value must have been computed before your + * code runs, or you don't mind failure. + */ + open static func cachedUserAgent(checkiOSVersion: Bool = true, + checkFirefoxVersion: Bool = true, + checkFirefoxBuildNumber: Bool = true) -> String? { + let currentiOSVersion = UIDevice.current.systemVersion + let lastiOSVersion = defaults.string(forKey: "LastDeviceSystemVersionNumber") + + let currentFirefoxBuildNumber = AppInfo.buildNumber + let currentFirefoxVersion = AppInfo.appVersion + let lastFirefoxVersion = defaults.string(forKey: "LastFirefoxVersionNumber") + let lastFirefoxBuildNumber = defaults.string(forKey: "LastFirefoxBuildNumber") + + if let firefoxUA = defaults.string(forKey: "UserAgent") { + if (!checkiOSVersion || (lastiOSVersion == currentiOSVersion)) + && (!checkFirefoxVersion || (lastFirefoxVersion == currentFirefoxVersion) + && (!checkFirefoxBuildNumber || (lastFirefoxBuildNumber == currentFirefoxBuildNumber))) { + return firefoxUA + } + } + + return nil + } + + /** + * This will typically return quickly, but can require creation of a UIWebView. + * As a result, it must be called on the UI thread. + */ + open static func defaultUserAgent() -> String { + assert(Thread.current.isMainThread, "This method must be called on the main thread.") + + if let firefoxUA = UserAgent.cachedUserAgent(checkiOSVersion: true) { + return firefoxUA + } + + let webView = UIWebView() + + let appVersion = AppInfo.appVersion + let buildNumber = AppInfo.buildNumber + let currentiOSVersion = UIDevice.current.systemVersion + defaults.set(currentiOSVersion, forKey: "LastDeviceSystemVersionNumber") + defaults.set(appVersion, forKey: "LastFirefoxVersionNumber") + defaults.set(buildNumber, forKey: "LastFirefoxBuildNumber") + + let userAgent = webView.stringByEvaluatingJavaScript(from: "navigator.userAgent")! + + // Extract the WebKit version and use it as the Safari version. + let webKitVersionRegex = try! NSRegularExpression(pattern: "AppleWebKit/([^ ]+) ", options: []) + + let match = webKitVersionRegex.firstMatch(in: userAgent, options: [], + range: NSRange(location: 0, length: userAgent.characters.count)) + + if match == nil { + print("Error: Unable to determine WebKit version in UA.") + return userAgent // Fall back to Safari's. + } + + let webKitVersion = (userAgent as NSString).substring(with: match!.rangeAt(1)) + + // Insert "FxiOS/" before the Mobile/ section. + let mobileRange = (userAgent as NSString).range(of: "Mobile/") + if mobileRange.location == NSNotFound { + print("Error: Unable to find Mobile section in UA.") + return userAgent // Fall back to Safari's. + } + + let mutableUA = NSMutableString(string: userAgent) + mutableUA.insert("FxiOS/\(appVersion)b\(AppInfo.buildNumber) ", at: mobileRange.location) + + let firefoxUA = "\(mutableUA) Safari/\(webKitVersion)" + + defaults.set(firefoxUA, forKey: "UserAgent") + + return firefoxUA + } + + open static func desktopUserAgent() -> String { + let userAgent = NSMutableString(string: defaultUserAgent()) + + // Spoof platform section + let platformRegex = try! NSRegularExpression(pattern: "\\([^\\)]+\\)", options: []) + guard let platformMatch = platformRegex.firstMatch(in: userAgent as String, options: [], range: NSRange(location: 0, length: userAgent.length)) else { + print("Error: Unable to determine platform in UA.") + return String(userAgent) + } + userAgent.replaceCharacters(in: platformMatch.range, with: "(Macintosh; Intel Mac OS X 10_11_1)") + + // Strip mobile section + let mobileRegex = try! NSRegularExpression(pattern: " FxiOS/[^ ]+ Mobile/[^ ]+", options: []) + + guard let mobileMatch = mobileRegex.firstMatch(in: userAgent as String, options: [], range: NSRange(location: 0, length: userAgent.length)) else { + print("Error: Unable to find Mobile section in UA.") + return String(userAgent) + } + userAgent.replaceCharacters(in: mobileMatch.range, with: "") + + return String(userAgent) + } +} diff --git a/mobile/ios/Shared/WeakList.swift b/mobile/ios/Shared/WeakList.swift new file mode 100644 index 0000000000..fb34cc3439 --- /dev/null +++ b/mobile/ios/Shared/WeakList.swift @@ -0,0 +1,64 @@ +/* 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 + +/** + * A list that weakly holds references to its items. + * Note that while the references themselves are cleared, their wrapper objects + * are not (though they are reused). Also note that since slots are reused, + * order is not preserved. + * + * This class crashes at runtime with EXC_BAD_ACCESS if a protocol is given as + * the type T. Make sure to use a class type. + */ +open class WeakList: Sequence { + private var items = [WeakRef]() + + public init() {} + + /** + * Adds an item to the list. + * Note that every insertion iterates through the list to find any "holes" (items that have + * been deallocated) to reuse them, so this class may not be appropriate in situations where + * insertion is frequent. + */ + open func insert(_ item: T) { + // Reuse any existing slots that have been deallocated. + for wrapper in items where wrapper.value == nil { + wrapper.value = item + return + } + + items.append(WeakRef(item)) + } + + open func makeIterator() -> AnyIterator { + var index = 0 + + return AnyIterator() { + if index >= self.items.count { + return nil + } + + for i in index.. { + public weak var value: T? + + public init(_ value: T) { + self.value = value + } +} diff --git a/mobile/ios/Shared/effective_tld_names.dat b/mobile/ios/Shared/effective_tld_names.dat new file mode 100644 index 0000000000..d5c9924ed5 --- /dev/null +++ b/mobile/ios/Shared/effective_tld_names.dat @@ -0,0 +1,10309 @@ +// 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/. + +// ===BEGIN ICANN DOMAINS=== + +// ac : http://en.wikipedia.org/wiki/.ac +ac +com.ac +edu.ac +gov.ac +net.ac +mil.ac +org.ac + +// ad : http://en.wikipedia.org/wiki/.ad +ad +nom.ad + +// ae : http://en.wikipedia.org/wiki/.ae +// see also: "Domain Name Eligibility Policy" at http://www.aeda.ae/eng/aepolicy.php +ae +co.ae +net.ae +org.ae +sch.ae +ac.ae +gov.ae +mil.ae + +// aero : see http://www.information.aero/index.php?id=66 +aero +accident-investigation.aero +accident-prevention.aero +aerobatic.aero +aeroclub.aero +aerodrome.aero +agents.aero +aircraft.aero +airline.aero +airport.aero +air-surveillance.aero +airtraffic.aero +air-traffic-control.aero +ambulance.aero +amusement.aero +association.aero +author.aero +ballooning.aero +broker.aero +caa.aero +cargo.aero +catering.aero +certification.aero +championship.aero +charter.aero +civilaviation.aero +club.aero +conference.aero +consultant.aero +consulting.aero +control.aero +council.aero +crew.aero +design.aero +dgca.aero +educator.aero +emergency.aero +engine.aero +engineer.aero +entertainment.aero +equipment.aero +exchange.aero +express.aero +federation.aero +flight.aero +freight.aero +fuel.aero +gliding.aero +government.aero +groundhandling.aero +group.aero +hanggliding.aero +homebuilt.aero +insurance.aero +journal.aero +journalist.aero +leasing.aero +logistics.aero +magazine.aero +maintenance.aero +marketplace.aero +media.aero +microlight.aero +modelling.aero +navigation.aero +parachuting.aero +paragliding.aero +passenger-association.aero +pilot.aero +press.aero +production.aero +recreation.aero +repbody.aero +res.aero +research.aero +rotorcraft.aero +safety.aero +scientist.aero +services.aero +show.aero +skydiving.aero +software.aero +student.aero +taxi.aero +trader.aero +trading.aero +trainer.aero +union.aero +workinggroup.aero +works.aero + +// af : http://www.nic.af/help.jsp +af +gov.af +com.af +org.af +net.af +edu.af + +// ag : http://www.nic.ag/prices.htm +ag +com.ag +org.ag +net.ag +co.ag +nom.ag + +// ai : http://nic.com.ai/ +ai +off.ai +com.ai +net.ai +org.ai + +// al : http://www.ert.gov.al/ert_alb/faq_det.html?Id=31 +al +com.al +edu.al +gov.al +mil.al +net.al +org.al + +// am : http://en.wikipedia.org/wiki/.am +am + +// an : http://www.una.an/an_domreg/default.asp +an +com.an +net.an +org.an +edu.an + +// ao : http://en.wikipedia.org/wiki/.ao +// http://www.dns.ao/REGISTR.DOC +ao +ed.ao +gv.ao +og.ao +co.ao +pb.ao +it.ao + +// aq : http://en.wikipedia.org/wiki/.aq +aq + +// ar : https://nic.ar/normativa-vigente.xhtml +ar +com.ar +edu.ar +gob.ar +gov.ar +int.ar +mil.ar +net.ar +org.ar +tur.ar + +// arpa : http://en.wikipedia.org/wiki/.arpa +// Confirmed by registry 2008-06-18 +arpa +e164.arpa +in-addr.arpa +ip6.arpa +iris.arpa +uri.arpa +urn.arpa + +// as : http://en.wikipedia.org/wiki/.as +as +gov.as + +// asia : http://en.wikipedia.org/wiki/.asia +asia + +// at : http://en.wikipedia.org/wiki/.at +// Confirmed by registry 2008-06-17 +at +ac.at +co.at +gv.at +or.at + +// au : http://en.wikipedia.org/wiki/.au +// http://www.auda.org.au/ +au +// 2LDs +com.au +net.au +org.au +edu.au +gov.au +asn.au +id.au +// Historic 2LDs (closed to new registration, but sites still exist) +info.au +conf.au +oz.au +// CGDNs - http://www.cgdn.org.au/ +act.au +nsw.au +nt.au +qld.au +sa.au +tas.au +vic.au +wa.au +// 3LDs +act.edu.au +nsw.edu.au +nt.edu.au +qld.edu.au +sa.edu.au +tas.edu.au +vic.edu.au +wa.edu.au +// act.gov.au Bug 984824 - Removed at request of Greg Tankard +// nsw.gov.au Bug 547985 - Removed at request of +// nt.gov.au Bug 940478 - Removed at request of Greg Connors +qld.gov.au +sa.gov.au +tas.gov.au +vic.gov.au +wa.gov.au + +// aw : http://en.wikipedia.org/wiki/.aw +aw +com.aw + +// ax : http://en.wikipedia.org/wiki/.ax +ax + +// az : http://en.wikipedia.org/wiki/.az +az +com.az +net.az +int.az +gov.az +org.az +edu.az +info.az +pp.az +mil.az +name.az +pro.az +biz.az + +// ba : http://en.wikipedia.org/wiki/.ba +ba +org.ba +net.ba +edu.ba +gov.ba +mil.ba +unsa.ba +unbi.ba +co.ba +com.ba +rs.ba + +// bb : http://en.wikipedia.org/wiki/.bb +bb +biz.bb +co.bb +com.bb +edu.bb +gov.bb +info.bb +net.bb +org.bb +store.bb +tv.bb + +// bd : http://en.wikipedia.org/wiki/.bd +*.bd + +// be : http://en.wikipedia.org/wiki/.be +// Confirmed by registry 2008-06-08 +be +ac.be + +// bf : http://en.wikipedia.org/wiki/.bf +bf +gov.bf + +// bg : http://en.wikipedia.org/wiki/.bg +// https://www.register.bg/user/static/rules/en/index.html +bg +a.bg +b.bg +c.bg +d.bg +e.bg +f.bg +g.bg +h.bg +i.bg +j.bg +k.bg +l.bg +m.bg +n.bg +o.bg +p.bg +q.bg +r.bg +s.bg +t.bg +u.bg +v.bg +w.bg +x.bg +y.bg +z.bg +0.bg +1.bg +2.bg +3.bg +4.bg +5.bg +6.bg +7.bg +8.bg +9.bg + +// bh : http://en.wikipedia.org/wiki/.bh +bh +com.bh +edu.bh +net.bh +org.bh +gov.bh + +// bi : http://en.wikipedia.org/wiki/.bi +// http://whois.nic.bi/ +bi +co.bi +com.bi +edu.bi +or.bi +org.bi + +// biz : http://en.wikipedia.org/wiki/.biz +biz + +// bj : http://en.wikipedia.org/wiki/.bj +bj +asso.bj +barreau.bj +gouv.bj + +// bm : http://www.bermudanic.bm/dnr-text.txt +bm +com.bm +edu.bm +gov.bm +net.bm +org.bm + +// bn : http://en.wikipedia.org/wiki/.bn +*.bn + +// bo : http://www.nic.bo/ +bo +com.bo +edu.bo +gov.bo +gob.bo +int.bo +org.bo +net.bo +mil.bo +tv.bo + +// br : http://registro.br/dominio/categoria.html +// Submitted by registry 2014-08-11 +br +adm.br +adv.br +agr.br +am.br +arq.br +art.br +ato.br +b.br +bio.br +blog.br +bmd.br +cim.br +cng.br +cnt.br +com.br +coop.br +ecn.br +eco.br +edu.br +emp.br +eng.br +esp.br +etc.br +eti.br +far.br +flog.br +fm.br +fnd.br +fot.br +fst.br +g12.br +ggf.br +gov.br +imb.br +ind.br +inf.br +jor.br +jus.br +leg.br +lel.br +mat.br +med.br +mil.br +mp.br +mus.br +net.br +*.nom.br +not.br +ntr.br +odo.br +org.br +ppg.br +pro.br +psc.br +psi.br +qsl.br +radio.br +rec.br +slg.br +srv.br +taxi.br +teo.br +tmp.br +trd.br +tur.br +tv.br +vet.br +vlog.br +wiki.br +zlg.br + +// bs : http://www.nic.bs/rules.html +bs +com.bs +net.bs +org.bs +edu.bs +gov.bs + +// bt : http://en.wikipedia.org/wiki/.bt +bt +com.bt +edu.bt +gov.bt +net.bt +org.bt + +// bv : No registrations at this time. +// Submitted by registry 2006-06-16 +bv + +// bw : http://en.wikipedia.org/wiki/.bw +// http://www.gobin.info/domainname/bw.doc +// list of other 2nd level tlds ? +bw +co.bw +org.bw + +// by : http://en.wikipedia.org/wiki/.by +// http://tld.by/rules_2006_en.html +// list of other 2nd level tlds ? +by +gov.by +mil.by +// Official information does not indicate that com.by is a reserved +// second-level domain, but it's being used as one (see www.google.com.by and +// www.yahoo.com.by, for example), so we list it here for safety's sake. +com.by + +// http://hoster.by/ +of.by + +// bz : http://en.wikipedia.org/wiki/.bz +// http://www.belizenic.bz/ +bz +com.bz +net.bz +org.bz +edu.bz +gov.bz + +// ca : http://en.wikipedia.org/wiki/.ca +ca +// ca geographical names +ab.ca +bc.ca +mb.ca +nb.ca +nf.ca +nl.ca +ns.ca +nt.ca +nu.ca +on.ca +pe.ca +qc.ca +sk.ca +yk.ca +// gc.ca: http://en.wikipedia.org/wiki/.gc.ca +// see also: http://registry.gc.ca/en/SubdomainFAQ +gc.ca + +// cat : http://en.wikipedia.org/wiki/.cat +cat + +// cc : http://en.wikipedia.org/wiki/.cc +cc + +// cd : http://en.wikipedia.org/wiki/.cd +// see also: https://www.nic.cd/domain/insertDomain_2.jsp?act=1 +cd +gov.cd + +// cf : http://en.wikipedia.org/wiki/.cf +cf + +// cg : http://en.wikipedia.org/wiki/.cg +cg + +// ch : http://en.wikipedia.org/wiki/.ch +ch + +// ci : http://en.wikipedia.org/wiki/.ci +// http://www.nic.ci/index.php?page=charte +ci +org.ci +or.ci +com.ci +co.ci +edu.ci +ed.ci +ac.ci +net.ci +go.ci +asso.ci +aéroport.ci +int.ci +presse.ci +md.ci +gouv.ci + +// ck : http://en.wikipedia.org/wiki/.ck +*.ck +!www.ck + +// cl : http://en.wikipedia.org/wiki/.cl +cl +gov.cl +gob.cl +co.cl +mil.cl + +// cm : http://en.wikipedia.org/wiki/.cm plus bug 981927 +cm +co.cm +com.cm +gov.cm +net.cm + +// cn : http://en.wikipedia.org/wiki/.cn +// Submitted by registry 2008-06-11 +cn +ac.cn +com.cn +edu.cn +gov.cn +net.cn +org.cn +mil.cn +公司.cn +网络.cn +網絡.cn +// cn geographic names +ah.cn +bj.cn +cq.cn +fj.cn +gd.cn +gs.cn +gz.cn +gx.cn +ha.cn +hb.cn +he.cn +hi.cn +hl.cn +hn.cn +jl.cn +js.cn +jx.cn +ln.cn +nm.cn +nx.cn +qh.cn +sc.cn +sd.cn +sh.cn +sn.cn +sx.cn +tj.cn +xj.cn +xz.cn +yn.cn +zj.cn +hk.cn +mo.cn +tw.cn + +// co : http://en.wikipedia.org/wiki/.co +// Submitted by registry 2008-06-11 +co +arts.co +com.co +edu.co +firm.co +gov.co +info.co +int.co +mil.co +net.co +nom.co +org.co +rec.co +web.co + +// com : http://en.wikipedia.org/wiki/.com +com + +// coop : http://en.wikipedia.org/wiki/.coop +coop + +// cr : http://www.nic.cr/niccr_publico/showRegistroDominiosScreen.do +cr +ac.cr +co.cr +ed.cr +fi.cr +go.cr +or.cr +sa.cr + +// cu : http://en.wikipedia.org/wiki/.cu +cu +com.cu +edu.cu +org.cu +net.cu +gov.cu +inf.cu + +// cv : http://en.wikipedia.org/wiki/.cv +cv + +// cw : http://www.una.cw/cw_registry/ +// Confirmed by registry 2013-03-26 +cw +com.cw +edu.cw +net.cw +org.cw + +// cx : http://en.wikipedia.org/wiki/.cx +// list of other 2nd level tlds ? +cx +gov.cx + +// cy : http://en.wikipedia.org/wiki/.cy +*.cy + +// cz : http://en.wikipedia.org/wiki/.cz +cz + +// de : http://en.wikipedia.org/wiki/.de +// Confirmed by registry (with technical +// reservations) 2008-07-01 +de + +// dj : http://en.wikipedia.org/wiki/.dj +dj + +// dk : http://en.wikipedia.org/wiki/.dk +// Confirmed by registry 2008-06-17 +dk + +// dm : http://en.wikipedia.org/wiki/.dm +dm +com.dm +net.dm +org.dm +edu.dm +gov.dm + +// do : http://en.wikipedia.org/wiki/.do +do +art.do +com.do +edu.do +gob.do +gov.do +mil.do +net.do +org.do +sld.do +web.do + +// dz : http://en.wikipedia.org/wiki/.dz +dz +com.dz +org.dz +net.dz +gov.dz +edu.dz +asso.dz +pol.dz +art.dz + +// ec : http://www.nic.ec/reg/paso1.asp +// Submitted by registry 2008-07-04 +ec +com.ec +info.ec +net.ec +fin.ec +k12.ec +med.ec +pro.ec +org.ec +edu.ec +gov.ec +gob.ec +mil.ec + +// edu : http://en.wikipedia.org/wiki/.edu +edu + +// ee : http://www.eenet.ee/EENet/dom_reeglid.html#lisa_B +ee +edu.ee +gov.ee +riik.ee +lib.ee +med.ee +com.ee +pri.ee +aip.ee +org.ee +fie.ee + +// eg : http://en.wikipedia.org/wiki/.eg +eg +com.eg +edu.eg +eun.eg +gov.eg +mil.eg +name.eg +net.eg +org.eg +sci.eg + +// er : http://en.wikipedia.org/wiki/.er +*.er + +// es : https://www.nic.es/site_ingles/ingles/dominios/index.html +es +com.es +nom.es +org.es +gob.es +edu.es + +// et : http://en.wikipedia.org/wiki/.et +et +com.et +gov.et +org.et +edu.et +biz.et +name.et +info.et +net.et + +// eu : http://en.wikipedia.org/wiki/.eu +eu + +// fi : http://en.wikipedia.org/wiki/.fi +fi +// aland.fi : http://en.wikipedia.org/wiki/.ax +// This domain is being phased out in favor of .ax. As there are still many +// domains under aland.fi, we still keep it on the list until aland.fi is +// completely removed. +// TODO: Check for updates (expected to be phased out around Q1/2009) +aland.fi + +// fj : http://en.wikipedia.org/wiki/.fj +*.fj + +// fk : http://en.wikipedia.org/wiki/.fk +*.fk + +// fm : http://en.wikipedia.org/wiki/.fm +fm + +// fo : http://en.wikipedia.org/wiki/.fo +fo + +// fr : http://www.afnic.fr/ +// domaines descriptifs : http://www.afnic.fr/obtenir/chartes/nommage-fr/annexe-descriptifs +fr +com.fr +asso.fr +nom.fr +prd.fr +presse.fr +tm.fr +// domaines sectoriels : http://www.afnic.fr/obtenir/chartes/nommage-fr/annexe-sectoriels +aeroport.fr +assedic.fr +avocat.fr +avoues.fr +cci.fr +chambagri.fr +chirurgiens-dentistes.fr +experts-comptables.fr +geometre-expert.fr +gouv.fr +greta.fr +huissier-justice.fr +medecin.fr +notaires.fr +pharmacien.fr +port.fr +veterinaire.fr + +// ga : http://en.wikipedia.org/wiki/.ga +ga + +// gb : This registry is effectively dormant +// Submitted by registry 2008-06-12 +gb + +// gd : http://en.wikipedia.org/wiki/.gd +gd + +// ge : http://www.nic.net.ge/policy_en.pdf +ge +com.ge +edu.ge +gov.ge +org.ge +mil.ge +net.ge +pvt.ge + +// gf : http://en.wikipedia.org/wiki/.gf +gf + +// gg : http://www.channelisles.net/register-domains/ +// Confirmed by registry 2013-11-28 +gg +co.gg +net.gg +org.gg + +// gh : http://en.wikipedia.org/wiki/.gh +// see also: http://www.nic.gh/reg_now.php +// Although domains directly at second level are not possible at the moment, +// they have been possible for some time and may come back. +gh +com.gh +edu.gh +gov.gh +org.gh +mil.gh + +// gi : http://www.nic.gi/rules.html +gi +com.gi +ltd.gi +gov.gi +mod.gi +edu.gi +org.gi + +// gl : http://en.wikipedia.org/wiki/.gl +// http://nic.gl +gl +co.gl +com.gl +edu.gl +net.gl +org.gl + +// gm : http://www.nic.gm/htmlpages%5Cgm-policy.htm +gm + +// gn : http://psg.com/dns/gn/gn.txt +// Submitted by registry 2008-06-17 +gn +ac.gn +com.gn +edu.gn +gov.gn +org.gn +net.gn + +// gov : http://en.wikipedia.org/wiki/.gov +gov + +// gp : http://www.nic.gp/index.php?lang=en +gp +com.gp +net.gp +mobi.gp +edu.gp +org.gp +asso.gp + +// gq : http://en.wikipedia.org/wiki/.gq +gq + +// gr : https://grweb.ics.forth.gr/english/1617-B-2005.html +// Submitted by registry 2008-06-09 +gr +com.gr +edu.gr +net.gr +org.gr +gov.gr + +// gs : http://en.wikipedia.org/wiki/.gs +gs + +// gt : http://www.gt/politicas_de_registro.html +gt +com.gt +edu.gt +gob.gt +ind.gt +mil.gt +net.gt +org.gt + +// gu : http://gadao.gov.gu/registration.txt +*.gu + +// gw : http://en.wikipedia.org/wiki/.gw +gw + +// gy : http://en.wikipedia.org/wiki/.gy +// http://registry.gy/ +gy +co.gy +com.gy +net.gy + +// hk : https://www.hkdnr.hk +// Submitted by registry 2008-06-11 +hk +com.hk +edu.hk +gov.hk +idv.hk +net.hk +org.hk +公司.hk +教育.hk +敎育.hk +政府.hk +個人.hk +个人.hk +箇人.hk +網络.hk +网络.hk +组織.hk +網絡.hk +网絡.hk +组织.hk +組織.hk +組织.hk + +// hm : http://en.wikipedia.org/wiki/.hm +hm + +// hn : http://www.nic.hn/politicas/ps02,,05.html +hn +com.hn +edu.hn +org.hn +net.hn +mil.hn +gob.hn + +// hr : http://www.dns.hr/documents/pdf/HRTLD-regulations.pdf +hr +iz.hr +from.hr +name.hr +com.hr + +// ht : http://www.nic.ht/info/charte.cfm +ht +com.ht +shop.ht +firm.ht +info.ht +adult.ht +net.ht +pro.ht +org.ht +med.ht +art.ht +coop.ht +pol.ht +asso.ht +edu.ht +rel.ht +gouv.ht +perso.ht + +// hu : http://www.domain.hu/domain/English/sld.html +// Confirmed by registry 2008-06-12 +hu +co.hu +info.hu +org.hu +priv.hu +sport.hu +tm.hu +2000.hu +agrar.hu +bolt.hu +casino.hu +city.hu +erotica.hu +erotika.hu +film.hu +forum.hu +games.hu +hotel.hu +ingatlan.hu +jogasz.hu +konyvelo.hu +lakas.hu +media.hu +news.hu +reklam.hu +sex.hu +shop.hu +suli.hu +szex.hu +tozsde.hu +utazas.hu +video.hu + +// id : https://register.pandi.or.id/ +id +ac.id +biz.id +co.id +desa.id +go.id +mil.id +my.id +net.id +or.id +sch.id +web.id + +// ie : http://en.wikipedia.org/wiki/.ie +ie +gov.ie + +// il : http://en.wikipedia.org/wiki/.il +*.il + +// im : https://www.nic.im/ +// Submitted by registry 2013-11-15 +im +ac.im +co.im +com.im +ltd.co.im +net.im +org.im +plc.co.im +tt.im +tv.im + +// in : http://en.wikipedia.org/wiki/.in +// see also: https://registry.in/Policies +// Please note, that nic.in is not an offical eTLD, but used by most +// government institutions. +in +co.in +firm.in +net.in +org.in +gen.in +ind.in +nic.in +ac.in +edu.in +res.in +gov.in +mil.in + +// info : http://en.wikipedia.org/wiki/.info +info + +// int : http://en.wikipedia.org/wiki/.int +// Confirmed by registry 2008-06-18 +int +eu.int + +// io : http://www.nic.io/rules.html +// list of other 2nd level tlds ? +io +com.io + +// iq : http://www.cmc.iq/english/iq/iqregister1.htm +iq +gov.iq +edu.iq +mil.iq +com.iq +org.iq +net.iq + +// ir : http://www.nic.ir/Terms_and_Conditions_ir,_Appendix_1_Domain_Rules +// Also see http://www.nic.ir/Internationalized_Domain_Names +// Two .ir entries added at request of , 2010-04-16 +ir +ac.ir +co.ir +gov.ir +id.ir +net.ir +org.ir +sch.ir +// xn--mgba3a4f16a.ir (.ir, Persian YEH) +ایران.ir +// xn--mgba3a4fra.ir (.ir, Arabic YEH) +ايران.ir + +// is : http://www.isnic.is/domain/rules.php +// Confirmed by registry 2008-12-06 +is +net.is +com.is +edu.is +gov.is +org.is +int.is + +// it : http://en.wikipedia.org/wiki/.it +it +gov.it +edu.it +// Reserved geo-names: +// http://www.nic.it/documenti/regolamenti-e-linee-guida/regolamento-assegnazione-versione-6.0.pdf +// There is also a list of reserved geo-names corresponding to Italian municipalities +// http://www.nic.it/documenti/appendice-c.pdf, but it is not included here. +// Regions +abr.it +abruzzo.it +aosta-valley.it +aostavalley.it +bas.it +basilicata.it +cal.it +calabria.it +cam.it +campania.it +emilia-romagna.it +emiliaromagna.it +emr.it +friuli-v-giulia.it +friuli-ve-giulia.it +friuli-vegiulia.it +friuli-venezia-giulia.it +friuli-veneziagiulia.it +friuli-vgiulia.it +friuliv-giulia.it +friulive-giulia.it +friulivegiulia.it +friulivenezia-giulia.it +friuliveneziagiulia.it +friulivgiulia.it +fvg.it +laz.it +lazio.it +lig.it +liguria.it +lom.it +lombardia.it +lombardy.it +lucania.it +mar.it +marche.it +mol.it +molise.it +piedmont.it +piemonte.it +pmn.it +pug.it +puglia.it +sar.it +sardegna.it +sardinia.it +sic.it +sicilia.it +sicily.it +taa.it +tos.it +toscana.it +trentino-a-adige.it +trentino-aadige.it +trentino-alto-adige.it +trentino-altoadige.it +trentino-s-tirol.it +trentino-stirol.it +trentino-sud-tirol.it +trentino-sudtirol.it +trentino-sued-tirol.it +trentino-suedtirol.it +trentinoa-adige.it +trentinoaadige.it +trentinoalto-adige.it +trentinoaltoadige.it +trentinos-tirol.it +trentinostirol.it +trentinosud-tirol.it +trentinosudtirol.it +trentinosued-tirol.it +trentinosuedtirol.it +tuscany.it +umb.it +umbria.it +val-d-aosta.it +val-daosta.it +vald-aosta.it +valdaosta.it +valle-aosta.it +valle-d-aosta.it +valle-daosta.it +valleaosta.it +valled-aosta.it +valledaosta.it +vallee-aoste.it +valleeaoste.it +vao.it +vda.it +ven.it +veneto.it +// Provinces +ag.it +agrigento.it +al.it +alessandria.it +alto-adige.it +altoadige.it +an.it +ancona.it +andria-barletta-trani.it +andria-trani-barletta.it +andriabarlettatrani.it +andriatranibarletta.it +ao.it +aosta.it +aoste.it +ap.it +aq.it +aquila.it +ar.it +arezzo.it +ascoli-piceno.it +ascolipiceno.it +asti.it +at.it +av.it +avellino.it +ba.it +balsan.it +bari.it +barletta-trani-andria.it +barlettatraniandria.it +belluno.it +benevento.it +bergamo.it +bg.it +bi.it +biella.it +bl.it +bn.it +bo.it +bologna.it +bolzano.it +bozen.it +br.it +brescia.it +brindisi.it +bs.it +bt.it +bz.it +ca.it +cagliari.it +caltanissetta.it +campidano-medio.it +campidanomedio.it +campobasso.it +carbonia-iglesias.it +carboniaiglesias.it +carrara-massa.it +carraramassa.it +caserta.it +catania.it +catanzaro.it +cb.it +ce.it +cesena-forli.it +cesenaforli.it +ch.it +chieti.it +ci.it +cl.it +cn.it +co.it +como.it +cosenza.it +cr.it +cremona.it +crotone.it +cs.it +ct.it +cuneo.it +cz.it +dell-ogliastra.it +dellogliastra.it +en.it +enna.it +fc.it +fe.it +fermo.it +ferrara.it +fg.it +fi.it +firenze.it +florence.it +fm.it +foggia.it +forli-cesena.it +forlicesena.it +fr.it +frosinone.it +ge.it +genoa.it +genova.it +go.it +gorizia.it +gr.it +grosseto.it +iglesias-carbonia.it +iglesiascarbonia.it +im.it +imperia.it +is.it +isernia.it +kr.it +la-spezia.it +laquila.it +laspezia.it +latina.it +lc.it +le.it +lecce.it +lecco.it +li.it +livorno.it +lo.it +lodi.it +lt.it +lu.it +lucca.it +macerata.it +mantova.it +massa-carrara.it +massacarrara.it +matera.it +mb.it +mc.it +me.it +medio-campidano.it +mediocampidano.it +messina.it +mi.it +milan.it +milano.it +mn.it +mo.it +modena.it +monza-brianza.it +monza-e-della-brianza.it +monza.it +monzabrianza.it +monzaebrianza.it +monzaedellabrianza.it +ms.it +mt.it +na.it +naples.it +napoli.it +no.it +novara.it +nu.it +nuoro.it +og.it +ogliastra.it +olbia-tempio.it +olbiatempio.it +or.it +oristano.it +ot.it +pa.it +padova.it +padua.it +palermo.it +parma.it +pavia.it +pc.it +pd.it +pe.it +perugia.it +pesaro-urbino.it +pesarourbino.it +pescara.it +pg.it +pi.it +piacenza.it +pisa.it +pistoia.it +pn.it +po.it +pordenone.it +potenza.it +pr.it +prato.it +pt.it +pu.it +pv.it +pz.it +ra.it +ragusa.it +ravenna.it +rc.it +re.it +reggio-calabria.it +reggio-emilia.it +reggiocalabria.it +reggioemilia.it +rg.it +ri.it +rieti.it +rimini.it +rm.it +rn.it +ro.it +roma.it +rome.it +rovigo.it +sa.it +salerno.it +sassari.it +savona.it +si.it +siena.it +siracusa.it +so.it +sondrio.it +sp.it +sr.it +ss.it +suedtirol.it +sv.it +ta.it +taranto.it +te.it +tempio-olbia.it +tempioolbia.it +teramo.it +terni.it +tn.it +to.it +torino.it +tp.it +tr.it +trani-andria-barletta.it +trani-barletta-andria.it +traniandriabarletta.it +tranibarlettaandria.it +trapani.it +trentino.it +trento.it +treviso.it +trieste.it +ts.it +turin.it +tv.it +ud.it +udine.it +urbino-pesaro.it +urbinopesaro.it +va.it +varese.it +vb.it +vc.it +ve.it +venezia.it +venice.it +verbania.it +vercelli.it +verona.it +vi.it +vibo-valentia.it +vibovalentia.it +vicenza.it +viterbo.it +vr.it +vs.it +vt.it +vv.it + +// je : http://www.channelisles.net/register-domains/ +// Confirmed by registry 2013-11-28 +je +co.je +net.je +org.je + +// jm : http://www.com.jm/register.html +*.jm + +// jo : http://www.dns.jo/Registration_policy.aspx +jo +com.jo +org.jo +net.jo +edu.jo +sch.jo +gov.jo +mil.jo +name.jo + +// jobs : http://en.wikipedia.org/wiki/.jobs +jobs + +// jp : http://en.wikipedia.org/wiki/.jp +// http://jprs.co.jp/en/jpdomain.html +// Submitted by registry 2014-10-30 +jp +// jp organizational type names +ac.jp +ad.jp +co.jp +ed.jp +go.jp +gr.jp +lg.jp +ne.jp +or.jp +// jp prefecture type names +aichi.jp +akita.jp +aomori.jp +chiba.jp +ehime.jp +fukui.jp +fukuoka.jp +fukushima.jp +gifu.jp +gunma.jp +hiroshima.jp +hokkaido.jp +hyogo.jp +ibaraki.jp +ishikawa.jp +iwate.jp +kagawa.jp +kagoshima.jp +kanagawa.jp +kochi.jp +kumamoto.jp +kyoto.jp +mie.jp +miyagi.jp +miyazaki.jp +nagano.jp +nagasaki.jp +nara.jp +niigata.jp +oita.jp +okayama.jp +okinawa.jp +osaka.jp +saga.jp +saitama.jp +shiga.jp +shimane.jp +shizuoka.jp +tochigi.jp +tokushima.jp +tokyo.jp +tottori.jp +toyama.jp +wakayama.jp +yamagata.jp +yamaguchi.jp +yamanashi.jp +栃木.jp +愛知.jp +愛媛.jp +兵庫.jp +熊本.jp +茨城.jp +北海道.jp +千葉.jp +和歌山.jp +長崎.jp +長野.jp +新潟.jp +青森.jp +静岡.jp +東京.jp +石川.jp +埼玉.jp +三重.jp +京都.jp +佐賀.jp +大分.jp +大阪.jp +奈良.jp +宮城.jp +宮崎.jp +富山.jp +山口.jp +山形.jp +山梨.jp +岩手.jp +岐阜.jp +岡山.jp +島根.jp +広島.jp +徳島.jp +沖縄.jp +滋賀.jp +神奈川.jp +福井.jp +福岡.jp +福島.jp +秋田.jp +群馬.jp +香川.jp +高知.jp +鳥取.jp +鹿児島.jp +// jp geographic type names +// http://jprs.jp/doc/rule/saisoku-1.html +*.kawasaki.jp +*.kitakyushu.jp +*.kobe.jp +*.nagoya.jp +*.sapporo.jp +*.sendai.jp +*.yokohama.jp +!city.kawasaki.jp +!city.kitakyushu.jp +!city.kobe.jp +!city.nagoya.jp +!city.sapporo.jp +!city.sendai.jp +!city.yokohama.jp +// 4th level registration +aisai.aichi.jp +ama.aichi.jp +anjo.aichi.jp +asuke.aichi.jp +chiryu.aichi.jp +chita.aichi.jp +fuso.aichi.jp +gamagori.aichi.jp +handa.aichi.jp +hazu.aichi.jp +hekinan.aichi.jp +higashiura.aichi.jp +ichinomiya.aichi.jp +inazawa.aichi.jp +inuyama.aichi.jp +isshiki.aichi.jp +iwakura.aichi.jp +kanie.aichi.jp +kariya.aichi.jp +kasugai.aichi.jp +kira.aichi.jp +kiyosu.aichi.jp +komaki.aichi.jp +konan.aichi.jp +kota.aichi.jp +mihama.aichi.jp +miyoshi.aichi.jp +nishio.aichi.jp +nisshin.aichi.jp +obu.aichi.jp +oguchi.aichi.jp +oharu.aichi.jp +okazaki.aichi.jp +owariasahi.aichi.jp +seto.aichi.jp +shikatsu.aichi.jp +shinshiro.aichi.jp +shitara.aichi.jp +tahara.aichi.jp +takahama.aichi.jp +tobishima.aichi.jp +toei.aichi.jp +togo.aichi.jp +tokai.aichi.jp +tokoname.aichi.jp +toyoake.aichi.jp +toyohashi.aichi.jp +toyokawa.aichi.jp +toyone.aichi.jp +toyota.aichi.jp +tsushima.aichi.jp +yatomi.aichi.jp +akita.akita.jp +daisen.akita.jp +fujisato.akita.jp +gojome.akita.jp +hachirogata.akita.jp +happou.akita.jp +higashinaruse.akita.jp +honjo.akita.jp +honjyo.akita.jp +ikawa.akita.jp +kamikoani.akita.jp +kamioka.akita.jp +katagami.akita.jp +kazuno.akita.jp +kitaakita.akita.jp +kosaka.akita.jp +kyowa.akita.jp +misato.akita.jp +mitane.akita.jp +moriyoshi.akita.jp +nikaho.akita.jp +noshiro.akita.jp +odate.akita.jp +oga.akita.jp +ogata.akita.jp +semboku.akita.jp +yokote.akita.jp +yurihonjo.akita.jp +aomori.aomori.jp +gonohe.aomori.jp +hachinohe.aomori.jp +hashikami.aomori.jp +hiranai.aomori.jp +hirosaki.aomori.jp +itayanagi.aomori.jp +kuroishi.aomori.jp +misawa.aomori.jp +mutsu.aomori.jp +nakadomari.aomori.jp +noheji.aomori.jp +oirase.aomori.jp +owani.aomori.jp +rokunohe.aomori.jp +sannohe.aomori.jp +shichinohe.aomori.jp +shingo.aomori.jp +takko.aomori.jp +towada.aomori.jp +tsugaru.aomori.jp +tsuruta.aomori.jp +abiko.chiba.jp +asahi.chiba.jp +chonan.chiba.jp +chosei.chiba.jp +choshi.chiba.jp +chuo.chiba.jp +funabashi.chiba.jp +futtsu.chiba.jp +hanamigawa.chiba.jp +ichihara.chiba.jp +ichikawa.chiba.jp +ichinomiya.chiba.jp +inzai.chiba.jp +isumi.chiba.jp +kamagaya.chiba.jp +kamogawa.chiba.jp +kashiwa.chiba.jp +katori.chiba.jp +katsuura.chiba.jp +kimitsu.chiba.jp +kisarazu.chiba.jp +kozaki.chiba.jp +kujukuri.chiba.jp +kyonan.chiba.jp +matsudo.chiba.jp +midori.chiba.jp +mihama.chiba.jp +minamiboso.chiba.jp +mobara.chiba.jp +mutsuzawa.chiba.jp +nagara.chiba.jp +nagareyama.chiba.jp +narashino.chiba.jp +narita.chiba.jp +noda.chiba.jp +oamishirasato.chiba.jp +omigawa.chiba.jp +onjuku.chiba.jp +otaki.chiba.jp +sakae.chiba.jp +sakura.chiba.jp +shimofusa.chiba.jp +shirako.chiba.jp +shiroi.chiba.jp +shisui.chiba.jp +sodegaura.chiba.jp +sosa.chiba.jp +tako.chiba.jp +tateyama.chiba.jp +togane.chiba.jp +tohnosho.chiba.jp +tomisato.chiba.jp +urayasu.chiba.jp +yachimata.chiba.jp +yachiyo.chiba.jp +yokaichiba.chiba.jp +yokoshibahikari.chiba.jp +yotsukaido.chiba.jp +ainan.ehime.jp +honai.ehime.jp +ikata.ehime.jp +imabari.ehime.jp +iyo.ehime.jp +kamijima.ehime.jp +kihoku.ehime.jp +kumakogen.ehime.jp +masaki.ehime.jp +matsuno.ehime.jp +matsuyama.ehime.jp +namikata.ehime.jp +niihama.ehime.jp +ozu.ehime.jp +saijo.ehime.jp +seiyo.ehime.jp +shikokuchuo.ehime.jp +tobe.ehime.jp +toon.ehime.jp +uchiko.ehime.jp +uwajima.ehime.jp +yawatahama.ehime.jp +echizen.fukui.jp +eiheiji.fukui.jp +fukui.fukui.jp +ikeda.fukui.jp +katsuyama.fukui.jp +mihama.fukui.jp +minamiechizen.fukui.jp +obama.fukui.jp +ohi.fukui.jp +ono.fukui.jp +sabae.fukui.jp +sakai.fukui.jp +takahama.fukui.jp +tsuruga.fukui.jp +wakasa.fukui.jp +ashiya.fukuoka.jp +buzen.fukuoka.jp +chikugo.fukuoka.jp +chikuho.fukuoka.jp +chikujo.fukuoka.jp +chikushino.fukuoka.jp +chikuzen.fukuoka.jp +chuo.fukuoka.jp +dazaifu.fukuoka.jp +fukuchi.fukuoka.jp +hakata.fukuoka.jp +higashi.fukuoka.jp +hirokawa.fukuoka.jp +hisayama.fukuoka.jp +iizuka.fukuoka.jp +inatsuki.fukuoka.jp +kaho.fukuoka.jp +kasuga.fukuoka.jp +kasuya.fukuoka.jp +kawara.fukuoka.jp +keisen.fukuoka.jp +koga.fukuoka.jp +kurate.fukuoka.jp +kurogi.fukuoka.jp +kurume.fukuoka.jp +minami.fukuoka.jp +miyako.fukuoka.jp +miyama.fukuoka.jp +miyawaka.fukuoka.jp +mizumaki.fukuoka.jp +munakata.fukuoka.jp +nakagawa.fukuoka.jp +nakama.fukuoka.jp +nishi.fukuoka.jp +nogata.fukuoka.jp +ogori.fukuoka.jp +okagaki.fukuoka.jp +okawa.fukuoka.jp +oki.fukuoka.jp +omuta.fukuoka.jp +onga.fukuoka.jp +onojo.fukuoka.jp +oto.fukuoka.jp +saigawa.fukuoka.jp +sasaguri.fukuoka.jp +shingu.fukuoka.jp +shinyoshitomi.fukuoka.jp +shonai.fukuoka.jp +soeda.fukuoka.jp +sue.fukuoka.jp +tachiarai.fukuoka.jp +tagawa.fukuoka.jp +takata.fukuoka.jp +toho.fukuoka.jp +toyotsu.fukuoka.jp +tsuiki.fukuoka.jp +ukiha.fukuoka.jp +umi.fukuoka.jp +usui.fukuoka.jp +yamada.fukuoka.jp +yame.fukuoka.jp +yanagawa.fukuoka.jp +yukuhashi.fukuoka.jp +aizubange.fukushima.jp +aizumisato.fukushima.jp +aizuwakamatsu.fukushima.jp +asakawa.fukushima.jp +bandai.fukushima.jp +date.fukushima.jp +fukushima.fukushima.jp +furudono.fukushima.jp +futaba.fukushima.jp +hanawa.fukushima.jp +higashi.fukushima.jp +hirata.fukushima.jp +hirono.fukushima.jp +iitate.fukushima.jp +inawashiro.fukushima.jp +ishikawa.fukushima.jp +iwaki.fukushima.jp +izumizaki.fukushima.jp +kagamiishi.fukushima.jp +kaneyama.fukushima.jp +kawamata.fukushima.jp +kitakata.fukushima.jp +kitashiobara.fukushima.jp +koori.fukushima.jp +koriyama.fukushima.jp +kunimi.fukushima.jp +miharu.fukushima.jp +mishima.fukushima.jp +namie.fukushima.jp +nango.fukushima.jp +nishiaizu.fukushima.jp +nishigo.fukushima.jp +okuma.fukushima.jp +omotego.fukushima.jp +ono.fukushima.jp +otama.fukushima.jp +samegawa.fukushima.jp +shimogo.fukushima.jp +shirakawa.fukushima.jp +showa.fukushima.jp +soma.fukushima.jp +sukagawa.fukushima.jp +taishin.fukushima.jp +tamakawa.fukushima.jp +tanagura.fukushima.jp +tenei.fukushima.jp +yabuki.fukushima.jp +yamato.fukushima.jp +yamatsuri.fukushima.jp +yanaizu.fukushima.jp +yugawa.fukushima.jp +anpachi.gifu.jp +ena.gifu.jp +gifu.gifu.jp +ginan.gifu.jp +godo.gifu.jp +gujo.gifu.jp +hashima.gifu.jp +hichiso.gifu.jp +hida.gifu.jp +higashishirakawa.gifu.jp +ibigawa.gifu.jp +ikeda.gifu.jp +kakamigahara.gifu.jp +kani.gifu.jp +kasahara.gifu.jp +kasamatsu.gifu.jp +kawaue.gifu.jp +kitagata.gifu.jp +mino.gifu.jp +minokamo.gifu.jp +mitake.gifu.jp +mizunami.gifu.jp +motosu.gifu.jp +nakatsugawa.gifu.jp +ogaki.gifu.jp +sakahogi.gifu.jp +seki.gifu.jp +sekigahara.gifu.jp +shirakawa.gifu.jp +tajimi.gifu.jp +takayama.gifu.jp +tarui.gifu.jp +toki.gifu.jp +tomika.gifu.jp +wanouchi.gifu.jp +yamagata.gifu.jp +yaotsu.gifu.jp +yoro.gifu.jp +annaka.gunma.jp +chiyoda.gunma.jp +fujioka.gunma.jp +higashiagatsuma.gunma.jp +isesaki.gunma.jp +itakura.gunma.jp +kanna.gunma.jp +kanra.gunma.jp +katashina.gunma.jp +kawaba.gunma.jp +kiryu.gunma.jp +kusatsu.gunma.jp +maebashi.gunma.jp +meiwa.gunma.jp +midori.gunma.jp +minakami.gunma.jp +naganohara.gunma.jp +nakanojo.gunma.jp +nanmoku.gunma.jp +numata.gunma.jp +oizumi.gunma.jp +ora.gunma.jp +ota.gunma.jp +shibukawa.gunma.jp +shimonita.gunma.jp +shinto.gunma.jp +showa.gunma.jp +takasaki.gunma.jp +takayama.gunma.jp +tamamura.gunma.jp +tatebayashi.gunma.jp +tomioka.gunma.jp +tsukiyono.gunma.jp +tsumagoi.gunma.jp +ueno.gunma.jp +yoshioka.gunma.jp +asaminami.hiroshima.jp +daiwa.hiroshima.jp +etajima.hiroshima.jp +fuchu.hiroshima.jp +fukuyama.hiroshima.jp +hatsukaichi.hiroshima.jp +higashihiroshima.hiroshima.jp +hongo.hiroshima.jp +jinsekikogen.hiroshima.jp +kaita.hiroshima.jp +kui.hiroshima.jp +kumano.hiroshima.jp +kure.hiroshima.jp +mihara.hiroshima.jp +miyoshi.hiroshima.jp +naka.hiroshima.jp +onomichi.hiroshima.jp +osakikamijima.hiroshima.jp +otake.hiroshima.jp +saka.hiroshima.jp +sera.hiroshima.jp +seranishi.hiroshima.jp +shinichi.hiroshima.jp +shobara.hiroshima.jp +takehara.hiroshima.jp +abashiri.hokkaido.jp +abira.hokkaido.jp +aibetsu.hokkaido.jp +akabira.hokkaido.jp +akkeshi.hokkaido.jp +asahikawa.hokkaido.jp +ashibetsu.hokkaido.jp +ashoro.hokkaido.jp +assabu.hokkaido.jp +atsuma.hokkaido.jp +bibai.hokkaido.jp +biei.hokkaido.jp +bifuka.hokkaido.jp +bihoro.hokkaido.jp +biratori.hokkaido.jp +chippubetsu.hokkaido.jp +chitose.hokkaido.jp +date.hokkaido.jp +ebetsu.hokkaido.jp +embetsu.hokkaido.jp +eniwa.hokkaido.jp +erimo.hokkaido.jp +esan.hokkaido.jp +esashi.hokkaido.jp +fukagawa.hokkaido.jp +fukushima.hokkaido.jp +furano.hokkaido.jp +furubira.hokkaido.jp +haboro.hokkaido.jp +hakodate.hokkaido.jp +hamatonbetsu.hokkaido.jp +hidaka.hokkaido.jp +higashikagura.hokkaido.jp +higashikawa.hokkaido.jp +hiroo.hokkaido.jp +hokuryu.hokkaido.jp +hokuto.hokkaido.jp +honbetsu.hokkaido.jp +horokanai.hokkaido.jp +horonobe.hokkaido.jp +ikeda.hokkaido.jp +imakane.hokkaido.jp +ishikari.hokkaido.jp +iwamizawa.hokkaido.jp +iwanai.hokkaido.jp +kamifurano.hokkaido.jp +kamikawa.hokkaido.jp +kamishihoro.hokkaido.jp +kamisunagawa.hokkaido.jp +kamoenai.hokkaido.jp +kayabe.hokkaido.jp +kembuchi.hokkaido.jp +kikonai.hokkaido.jp +kimobetsu.hokkaido.jp +kitahiroshima.hokkaido.jp +kitami.hokkaido.jp +kiyosato.hokkaido.jp +koshimizu.hokkaido.jp +kunneppu.hokkaido.jp +kuriyama.hokkaido.jp +kuromatsunai.hokkaido.jp +kushiro.hokkaido.jp +kutchan.hokkaido.jp +kyowa.hokkaido.jp +mashike.hokkaido.jp +matsumae.hokkaido.jp +mikasa.hokkaido.jp +minamifurano.hokkaido.jp +mombetsu.hokkaido.jp +moseushi.hokkaido.jp +mukawa.hokkaido.jp +muroran.hokkaido.jp +naie.hokkaido.jp +nakagawa.hokkaido.jp +nakasatsunai.hokkaido.jp +nakatombetsu.hokkaido.jp +nanae.hokkaido.jp +nanporo.hokkaido.jp +nayoro.hokkaido.jp +nemuro.hokkaido.jp +niikappu.hokkaido.jp +niki.hokkaido.jp +nishiokoppe.hokkaido.jp +noboribetsu.hokkaido.jp +numata.hokkaido.jp +obihiro.hokkaido.jp +obira.hokkaido.jp +oketo.hokkaido.jp +okoppe.hokkaido.jp +otaru.hokkaido.jp +otobe.hokkaido.jp +otofuke.hokkaido.jp +otoineppu.hokkaido.jp +oumu.hokkaido.jp +ozora.hokkaido.jp +pippu.hokkaido.jp +rankoshi.hokkaido.jp +rebun.hokkaido.jp +rikubetsu.hokkaido.jp +rishiri.hokkaido.jp +rishirifuji.hokkaido.jp +saroma.hokkaido.jp +sarufutsu.hokkaido.jp +shakotan.hokkaido.jp +shari.hokkaido.jp +shibecha.hokkaido.jp +shibetsu.hokkaido.jp +shikabe.hokkaido.jp +shikaoi.hokkaido.jp +shimamaki.hokkaido.jp +shimizu.hokkaido.jp +shimokawa.hokkaido.jp +shinshinotsu.hokkaido.jp +shintoku.hokkaido.jp +shiranuka.hokkaido.jp +shiraoi.hokkaido.jp +shiriuchi.hokkaido.jp +sobetsu.hokkaido.jp +sunagawa.hokkaido.jp +taiki.hokkaido.jp +takasu.hokkaido.jp +takikawa.hokkaido.jp +takinoue.hokkaido.jp +teshikaga.hokkaido.jp +tobetsu.hokkaido.jp +tohma.hokkaido.jp +tomakomai.hokkaido.jp +tomari.hokkaido.jp +toya.hokkaido.jp +toyako.hokkaido.jp +toyotomi.hokkaido.jp +toyoura.hokkaido.jp +tsubetsu.hokkaido.jp +tsukigata.hokkaido.jp +urakawa.hokkaido.jp +urausu.hokkaido.jp +uryu.hokkaido.jp +utashinai.hokkaido.jp +wakkanai.hokkaido.jp +wassamu.hokkaido.jp +yakumo.hokkaido.jp +yoichi.hokkaido.jp +aioi.hyogo.jp +akashi.hyogo.jp +ako.hyogo.jp +amagasaki.hyogo.jp +aogaki.hyogo.jp +asago.hyogo.jp +ashiya.hyogo.jp +awaji.hyogo.jp +fukusaki.hyogo.jp +goshiki.hyogo.jp +harima.hyogo.jp +himeji.hyogo.jp +ichikawa.hyogo.jp +inagawa.hyogo.jp +itami.hyogo.jp +kakogawa.hyogo.jp +kamigori.hyogo.jp +kamikawa.hyogo.jp +kasai.hyogo.jp +kasuga.hyogo.jp +kawanishi.hyogo.jp +miki.hyogo.jp +minamiawaji.hyogo.jp +nishinomiya.hyogo.jp +nishiwaki.hyogo.jp +ono.hyogo.jp +sanda.hyogo.jp +sannan.hyogo.jp +sasayama.hyogo.jp +sayo.hyogo.jp +shingu.hyogo.jp +shinonsen.hyogo.jp +shiso.hyogo.jp +sumoto.hyogo.jp +taishi.hyogo.jp +taka.hyogo.jp +takarazuka.hyogo.jp +takasago.hyogo.jp +takino.hyogo.jp +tamba.hyogo.jp +tatsuno.hyogo.jp +toyooka.hyogo.jp +yabu.hyogo.jp +yashiro.hyogo.jp +yoka.hyogo.jp +yokawa.hyogo.jp +ami.ibaraki.jp +asahi.ibaraki.jp +bando.ibaraki.jp +chikusei.ibaraki.jp +daigo.ibaraki.jp +fujishiro.ibaraki.jp +hitachi.ibaraki.jp +hitachinaka.ibaraki.jp +hitachiomiya.ibaraki.jp +hitachiota.ibaraki.jp +ibaraki.ibaraki.jp +ina.ibaraki.jp +inashiki.ibaraki.jp +itako.ibaraki.jp +iwama.ibaraki.jp +joso.ibaraki.jp +kamisu.ibaraki.jp +kasama.ibaraki.jp +kashima.ibaraki.jp +kasumigaura.ibaraki.jp +koga.ibaraki.jp +miho.ibaraki.jp +mito.ibaraki.jp +moriya.ibaraki.jp +naka.ibaraki.jp +namegata.ibaraki.jp +oarai.ibaraki.jp +ogawa.ibaraki.jp +omitama.ibaraki.jp +ryugasaki.ibaraki.jp +sakai.ibaraki.jp +sakuragawa.ibaraki.jp +shimodate.ibaraki.jp +shimotsuma.ibaraki.jp +shirosato.ibaraki.jp +sowa.ibaraki.jp +suifu.ibaraki.jp +takahagi.ibaraki.jp +tamatsukuri.ibaraki.jp +tokai.ibaraki.jp +tomobe.ibaraki.jp +tone.ibaraki.jp +toride.ibaraki.jp +tsuchiura.ibaraki.jp +tsukuba.ibaraki.jp +uchihara.ibaraki.jp +ushiku.ibaraki.jp +yachiyo.ibaraki.jp +yamagata.ibaraki.jp +yawara.ibaraki.jp +yuki.ibaraki.jp +anamizu.ishikawa.jp +hakui.ishikawa.jp +hakusan.ishikawa.jp +kaga.ishikawa.jp +kahoku.ishikawa.jp +kanazawa.ishikawa.jp +kawakita.ishikawa.jp +komatsu.ishikawa.jp +nakanoto.ishikawa.jp +nanao.ishikawa.jp +nomi.ishikawa.jp +nonoichi.ishikawa.jp +noto.ishikawa.jp +shika.ishikawa.jp +suzu.ishikawa.jp +tsubata.ishikawa.jp +tsurugi.ishikawa.jp +uchinada.ishikawa.jp +wajima.ishikawa.jp +fudai.iwate.jp +fujisawa.iwate.jp +hanamaki.iwate.jp +hiraizumi.iwate.jp +hirono.iwate.jp +ichinohe.iwate.jp +ichinoseki.iwate.jp +iwaizumi.iwate.jp +iwate.iwate.jp +joboji.iwate.jp +kamaishi.iwate.jp +kanegasaki.iwate.jp +karumai.iwate.jp +kawai.iwate.jp +kitakami.iwate.jp +kuji.iwate.jp +kunohe.iwate.jp +kuzumaki.iwate.jp +miyako.iwate.jp +mizusawa.iwate.jp +morioka.iwate.jp +ninohe.iwate.jp +noda.iwate.jp +ofunato.iwate.jp +oshu.iwate.jp +otsuchi.iwate.jp +rikuzentakata.iwate.jp +shiwa.iwate.jp +shizukuishi.iwate.jp +sumita.iwate.jp +tanohata.iwate.jp +tono.iwate.jp +yahaba.iwate.jp +yamada.iwate.jp +ayagawa.kagawa.jp +higashikagawa.kagawa.jp +kanonji.kagawa.jp +kotohira.kagawa.jp +manno.kagawa.jp +marugame.kagawa.jp +mitoyo.kagawa.jp +naoshima.kagawa.jp +sanuki.kagawa.jp +tadotsu.kagawa.jp +takamatsu.kagawa.jp +tonosho.kagawa.jp +uchinomi.kagawa.jp +utazu.kagawa.jp +zentsuji.kagawa.jp +akune.kagoshima.jp +amami.kagoshima.jp +hioki.kagoshima.jp +isa.kagoshima.jp +isen.kagoshima.jp +izumi.kagoshima.jp +kagoshima.kagoshima.jp +kanoya.kagoshima.jp +kawanabe.kagoshima.jp +kinko.kagoshima.jp +kouyama.kagoshima.jp +makurazaki.kagoshima.jp +matsumoto.kagoshima.jp +minamitane.kagoshima.jp +nakatane.kagoshima.jp +nishinoomote.kagoshima.jp +satsumasendai.kagoshima.jp +soo.kagoshima.jp +tarumizu.kagoshima.jp +yusui.kagoshima.jp +aikawa.kanagawa.jp +atsugi.kanagawa.jp +ayase.kanagawa.jp +chigasaki.kanagawa.jp +ebina.kanagawa.jp +fujisawa.kanagawa.jp +hadano.kanagawa.jp +hakone.kanagawa.jp +hiratsuka.kanagawa.jp +isehara.kanagawa.jp +kaisei.kanagawa.jp +kamakura.kanagawa.jp +kiyokawa.kanagawa.jp +matsuda.kanagawa.jp +minamiashigara.kanagawa.jp +miura.kanagawa.jp +nakai.kanagawa.jp +ninomiya.kanagawa.jp +odawara.kanagawa.jp +oi.kanagawa.jp +oiso.kanagawa.jp +sagamihara.kanagawa.jp +samukawa.kanagawa.jp +tsukui.kanagawa.jp +yamakita.kanagawa.jp +yamato.kanagawa.jp +yokosuka.kanagawa.jp +yugawara.kanagawa.jp +zama.kanagawa.jp +zushi.kanagawa.jp +aki.kochi.jp +geisei.kochi.jp +hidaka.kochi.jp +higashitsuno.kochi.jp +ino.kochi.jp +kagami.kochi.jp +kami.kochi.jp +kitagawa.kochi.jp +kochi.kochi.jp +mihara.kochi.jp +motoyama.kochi.jp +muroto.kochi.jp +nahari.kochi.jp +nakamura.kochi.jp +nankoku.kochi.jp +nishitosa.kochi.jp +niyodogawa.kochi.jp +ochi.kochi.jp +okawa.kochi.jp +otoyo.kochi.jp +otsuki.kochi.jp +sakawa.kochi.jp +sukumo.kochi.jp +susaki.kochi.jp +tosa.kochi.jp +tosashimizu.kochi.jp +toyo.kochi.jp +tsuno.kochi.jp +umaji.kochi.jp +yasuda.kochi.jp +yusuhara.kochi.jp +amakusa.kumamoto.jp +arao.kumamoto.jp +aso.kumamoto.jp +choyo.kumamoto.jp +gyokuto.kumamoto.jp +hitoyoshi.kumamoto.jp +kamiamakusa.kumamoto.jp +kashima.kumamoto.jp +kikuchi.kumamoto.jp +kosa.kumamoto.jp +kumamoto.kumamoto.jp +mashiki.kumamoto.jp +mifune.kumamoto.jp +minamata.kumamoto.jp +minamioguni.kumamoto.jp +nagasu.kumamoto.jp +nishihara.kumamoto.jp +oguni.kumamoto.jp +ozu.kumamoto.jp +sumoto.kumamoto.jp +takamori.kumamoto.jp +uki.kumamoto.jp +uto.kumamoto.jp +yamaga.kumamoto.jp +yamato.kumamoto.jp +yatsushiro.kumamoto.jp +ayabe.kyoto.jp +fukuchiyama.kyoto.jp +higashiyama.kyoto.jp +ide.kyoto.jp +ine.kyoto.jp +joyo.kyoto.jp +kameoka.kyoto.jp +kamo.kyoto.jp +kita.kyoto.jp +kizu.kyoto.jp +kumiyama.kyoto.jp +kyotamba.kyoto.jp +kyotanabe.kyoto.jp +kyotango.kyoto.jp +maizuru.kyoto.jp +minami.kyoto.jp +minamiyamashiro.kyoto.jp +miyazu.kyoto.jp +muko.kyoto.jp +nagaokakyo.kyoto.jp +nakagyo.kyoto.jp +nantan.kyoto.jp +oyamazaki.kyoto.jp +sakyo.kyoto.jp +seika.kyoto.jp +tanabe.kyoto.jp +uji.kyoto.jp +ujitawara.kyoto.jp +wazuka.kyoto.jp +yamashina.kyoto.jp +yawata.kyoto.jp +asahi.mie.jp +inabe.mie.jp +ise.mie.jp +kameyama.mie.jp +kawagoe.mie.jp +kiho.mie.jp +kisosaki.mie.jp +kiwa.mie.jp +komono.mie.jp +kumano.mie.jp +kuwana.mie.jp +matsusaka.mie.jp +meiwa.mie.jp +mihama.mie.jp +minamiise.mie.jp +misugi.mie.jp +miyama.mie.jp +nabari.mie.jp +shima.mie.jp +suzuka.mie.jp +tado.mie.jp +taiki.mie.jp +taki.mie.jp +tamaki.mie.jp +toba.mie.jp +tsu.mie.jp +udono.mie.jp +ureshino.mie.jp +watarai.mie.jp +yokkaichi.mie.jp +furukawa.miyagi.jp +higashimatsushima.miyagi.jp +ishinomaki.miyagi.jp +iwanuma.miyagi.jp +kakuda.miyagi.jp +kami.miyagi.jp +kawasaki.miyagi.jp +kesennuma.miyagi.jp +marumori.miyagi.jp +matsushima.miyagi.jp +minamisanriku.miyagi.jp +misato.miyagi.jp +murata.miyagi.jp +natori.miyagi.jp +ogawara.miyagi.jp +ohira.miyagi.jp +onagawa.miyagi.jp +osaki.miyagi.jp +rifu.miyagi.jp +semine.miyagi.jp +shibata.miyagi.jp +shichikashuku.miyagi.jp +shikama.miyagi.jp +shiogama.miyagi.jp +shiroishi.miyagi.jp +tagajo.miyagi.jp +taiwa.miyagi.jp +tome.miyagi.jp +tomiya.miyagi.jp +wakuya.miyagi.jp +watari.miyagi.jp +yamamoto.miyagi.jp +zao.miyagi.jp +aya.miyazaki.jp +ebino.miyazaki.jp +gokase.miyazaki.jp +hyuga.miyazaki.jp +kadogawa.miyazaki.jp +kawaminami.miyazaki.jp +kijo.miyazaki.jp +kitagawa.miyazaki.jp +kitakata.miyazaki.jp +kitaura.miyazaki.jp +kobayashi.miyazaki.jp +kunitomi.miyazaki.jp +kushima.miyazaki.jp +mimata.miyazaki.jp +miyakonojo.miyazaki.jp +miyazaki.miyazaki.jp +morotsuka.miyazaki.jp +nichinan.miyazaki.jp +nishimera.miyazaki.jp +nobeoka.miyazaki.jp +saito.miyazaki.jp +shiiba.miyazaki.jp +shintomi.miyazaki.jp +takaharu.miyazaki.jp +takanabe.miyazaki.jp +takazaki.miyazaki.jp +tsuno.miyazaki.jp +achi.nagano.jp +agematsu.nagano.jp +anan.nagano.jp +aoki.nagano.jp +asahi.nagano.jp +azumino.nagano.jp +chikuhoku.nagano.jp +chikuma.nagano.jp +chino.nagano.jp +fujimi.nagano.jp +hakuba.nagano.jp +hara.nagano.jp +hiraya.nagano.jp +iida.nagano.jp +iijima.nagano.jp +iiyama.nagano.jp +iizuna.nagano.jp +ikeda.nagano.jp +ikusaka.nagano.jp +ina.nagano.jp +karuizawa.nagano.jp +kawakami.nagano.jp +kiso.nagano.jp +kisofukushima.nagano.jp +kitaaiki.nagano.jp +komagane.nagano.jp +komoro.nagano.jp +matsukawa.nagano.jp +matsumoto.nagano.jp +miasa.nagano.jp +minamiaiki.nagano.jp +minamimaki.nagano.jp +minamiminowa.nagano.jp +minowa.nagano.jp +miyada.nagano.jp +miyota.nagano.jp +mochizuki.nagano.jp +nagano.nagano.jp +nagawa.nagano.jp +nagiso.nagano.jp +nakagawa.nagano.jp +nakano.nagano.jp +nozawaonsen.nagano.jp +obuse.nagano.jp +ogawa.nagano.jp +okaya.nagano.jp +omachi.nagano.jp +omi.nagano.jp +ookuwa.nagano.jp +ooshika.nagano.jp +otaki.nagano.jp +otari.nagano.jp +sakae.nagano.jp +sakaki.nagano.jp +saku.nagano.jp +sakuho.nagano.jp +shimosuwa.nagano.jp +shinanomachi.nagano.jp +shiojiri.nagano.jp +suwa.nagano.jp +suzaka.nagano.jp +takagi.nagano.jp +takamori.nagano.jp +takayama.nagano.jp +tateshina.nagano.jp +tatsuno.nagano.jp +togakushi.nagano.jp +togura.nagano.jp +tomi.nagano.jp +ueda.nagano.jp +wada.nagano.jp +yamagata.nagano.jp +yamanouchi.nagano.jp +yasaka.nagano.jp +yasuoka.nagano.jp +chijiwa.nagasaki.jp +futsu.nagasaki.jp +goto.nagasaki.jp +hasami.nagasaki.jp +hirado.nagasaki.jp +iki.nagasaki.jp +isahaya.nagasaki.jp +kawatana.nagasaki.jp +kuchinotsu.nagasaki.jp +matsuura.nagasaki.jp +nagasaki.nagasaki.jp +obama.nagasaki.jp +omura.nagasaki.jp +oseto.nagasaki.jp +saikai.nagasaki.jp +sasebo.nagasaki.jp +seihi.nagasaki.jp +shimabara.nagasaki.jp +shinkamigoto.nagasaki.jp +togitsu.nagasaki.jp +tsushima.nagasaki.jp +unzen.nagasaki.jp +ando.nara.jp +gose.nara.jp +heguri.nara.jp +higashiyoshino.nara.jp +ikaruga.nara.jp +ikoma.nara.jp +kamikitayama.nara.jp +kanmaki.nara.jp +kashiba.nara.jp +kashihara.nara.jp +katsuragi.nara.jp +kawai.nara.jp +kawakami.nara.jp +kawanishi.nara.jp +koryo.nara.jp +kurotaki.nara.jp +mitsue.nara.jp +miyake.nara.jp +nara.nara.jp +nosegawa.nara.jp +oji.nara.jp +ouda.nara.jp +oyodo.nara.jp +sakurai.nara.jp +sango.nara.jp +shimoichi.nara.jp +shimokitayama.nara.jp +shinjo.nara.jp +soni.nara.jp +takatori.nara.jp +tawaramoto.nara.jp +tenkawa.nara.jp +tenri.nara.jp +uda.nara.jp +yamatokoriyama.nara.jp +yamatotakada.nara.jp +yamazoe.nara.jp +yoshino.nara.jp +aga.niigata.jp +agano.niigata.jp +gosen.niigata.jp +itoigawa.niigata.jp +izumozaki.niigata.jp +joetsu.niigata.jp +kamo.niigata.jp +kariwa.niigata.jp +kashiwazaki.niigata.jp +minamiuonuma.niigata.jp +mitsuke.niigata.jp +muika.niigata.jp +murakami.niigata.jp +myoko.niigata.jp +nagaoka.niigata.jp +niigata.niigata.jp +ojiya.niigata.jp +omi.niigata.jp +sado.niigata.jp +sanjo.niigata.jp +seiro.niigata.jp +seirou.niigata.jp +sekikawa.niigata.jp +shibata.niigata.jp +tagami.niigata.jp +tainai.niigata.jp +tochio.niigata.jp +tokamachi.niigata.jp +tsubame.niigata.jp +tsunan.niigata.jp +uonuma.niigata.jp +yahiko.niigata.jp +yoita.niigata.jp +yuzawa.niigata.jp +beppu.oita.jp +bungoono.oita.jp +bungotakada.oita.jp +hasama.oita.jp +hiji.oita.jp +himeshima.oita.jp +hita.oita.jp +kamitsue.oita.jp +kokonoe.oita.jp +kuju.oita.jp +kunisaki.oita.jp +kusu.oita.jp +oita.oita.jp +saiki.oita.jp +taketa.oita.jp +tsukumi.oita.jp +usa.oita.jp +usuki.oita.jp +yufu.oita.jp +akaiwa.okayama.jp +asakuchi.okayama.jp +bizen.okayama.jp +hayashima.okayama.jp +ibara.okayama.jp +kagamino.okayama.jp +kasaoka.okayama.jp +kibichuo.okayama.jp +kumenan.okayama.jp +kurashiki.okayama.jp +maniwa.okayama.jp +misaki.okayama.jp +nagi.okayama.jp +niimi.okayama.jp +nishiawakura.okayama.jp +okayama.okayama.jp +satosho.okayama.jp +setouchi.okayama.jp +shinjo.okayama.jp +shoo.okayama.jp +soja.okayama.jp +takahashi.okayama.jp +tamano.okayama.jp +tsuyama.okayama.jp +wake.okayama.jp +yakage.okayama.jp +aguni.okinawa.jp +ginowan.okinawa.jp +ginoza.okinawa.jp +gushikami.okinawa.jp +haebaru.okinawa.jp +higashi.okinawa.jp +hirara.okinawa.jp +iheya.okinawa.jp +ishigaki.okinawa.jp +ishikawa.okinawa.jp +itoman.okinawa.jp +izena.okinawa.jp +kadena.okinawa.jp +kin.okinawa.jp +kitadaito.okinawa.jp +kitanakagusuku.okinawa.jp +kumejima.okinawa.jp +kunigami.okinawa.jp +minamidaito.okinawa.jp +motobu.okinawa.jp +nago.okinawa.jp +naha.okinawa.jp +nakagusuku.okinawa.jp +nakijin.okinawa.jp +nanjo.okinawa.jp +nishihara.okinawa.jp +ogimi.okinawa.jp +okinawa.okinawa.jp +onna.okinawa.jp +shimoji.okinawa.jp +taketomi.okinawa.jp +tarama.okinawa.jp +tokashiki.okinawa.jp +tomigusuku.okinawa.jp +tonaki.okinawa.jp +urasoe.okinawa.jp +uruma.okinawa.jp +yaese.okinawa.jp +yomitan.okinawa.jp +yonabaru.okinawa.jp +yonaguni.okinawa.jp +zamami.okinawa.jp +abeno.osaka.jp +chihayaakasaka.osaka.jp +chuo.osaka.jp +daito.osaka.jp +fujiidera.osaka.jp +habikino.osaka.jp +hannan.osaka.jp +higashiosaka.osaka.jp +higashisumiyoshi.osaka.jp +higashiyodogawa.osaka.jp +hirakata.osaka.jp +ibaraki.osaka.jp +ikeda.osaka.jp +izumi.osaka.jp +izumiotsu.osaka.jp +izumisano.osaka.jp +kadoma.osaka.jp +kaizuka.osaka.jp +kanan.osaka.jp +kashiwara.osaka.jp +katano.osaka.jp +kawachinagano.osaka.jp +kishiwada.osaka.jp +kita.osaka.jp +kumatori.osaka.jp +matsubara.osaka.jp +minato.osaka.jp +minoh.osaka.jp +misaki.osaka.jp +moriguchi.osaka.jp +neyagawa.osaka.jp +nishi.osaka.jp +nose.osaka.jp +osakasayama.osaka.jp +sakai.osaka.jp +sayama.osaka.jp +sennan.osaka.jp +settsu.osaka.jp +shijonawate.osaka.jp +shimamoto.osaka.jp +suita.osaka.jp +tadaoka.osaka.jp +taishi.osaka.jp +tajiri.osaka.jp +takaishi.osaka.jp +takatsuki.osaka.jp +tondabayashi.osaka.jp +toyonaka.osaka.jp +toyono.osaka.jp +yao.osaka.jp +ariake.saga.jp +arita.saga.jp +fukudomi.saga.jp +genkai.saga.jp +hamatama.saga.jp +hizen.saga.jp +imari.saga.jp +kamimine.saga.jp +kanzaki.saga.jp +karatsu.saga.jp +kashima.saga.jp +kitagata.saga.jp +kitahata.saga.jp +kiyama.saga.jp +kouhoku.saga.jp +kyuragi.saga.jp +nishiarita.saga.jp +ogi.saga.jp +omachi.saga.jp +ouchi.saga.jp +saga.saga.jp +shiroishi.saga.jp +taku.saga.jp +tara.saga.jp +tosu.saga.jp +yoshinogari.saga.jp +arakawa.saitama.jp +asaka.saitama.jp +chichibu.saitama.jp +fujimi.saitama.jp +fujimino.saitama.jp +fukaya.saitama.jp +hanno.saitama.jp +hanyu.saitama.jp +hasuda.saitama.jp +hatogaya.saitama.jp +hatoyama.saitama.jp +hidaka.saitama.jp +higashichichibu.saitama.jp +higashimatsuyama.saitama.jp +honjo.saitama.jp +ina.saitama.jp +iruma.saitama.jp +iwatsuki.saitama.jp +kamiizumi.saitama.jp +kamikawa.saitama.jp +kamisato.saitama.jp +kasukabe.saitama.jp +kawagoe.saitama.jp +kawaguchi.saitama.jp +kawajima.saitama.jp +kazo.saitama.jp +kitamoto.saitama.jp +koshigaya.saitama.jp +kounosu.saitama.jp +kuki.saitama.jp +kumagaya.saitama.jp +matsubushi.saitama.jp +minano.saitama.jp +misato.saitama.jp +miyashiro.saitama.jp +miyoshi.saitama.jp +moroyama.saitama.jp +nagatoro.saitama.jp +namegawa.saitama.jp +niiza.saitama.jp +ogano.saitama.jp +ogawa.saitama.jp +ogose.saitama.jp +okegawa.saitama.jp +omiya.saitama.jp +otaki.saitama.jp +ranzan.saitama.jp +ryokami.saitama.jp +saitama.saitama.jp +sakado.saitama.jp +satte.saitama.jp +sayama.saitama.jp +shiki.saitama.jp +shiraoka.saitama.jp +soka.saitama.jp +sugito.saitama.jp +toda.saitama.jp +tokigawa.saitama.jp +tokorozawa.saitama.jp +tsurugashima.saitama.jp +urawa.saitama.jp +warabi.saitama.jp +yashio.saitama.jp +yokoze.saitama.jp +yono.saitama.jp +yorii.saitama.jp +yoshida.saitama.jp +yoshikawa.saitama.jp +yoshimi.saitama.jp +aisho.shiga.jp +gamo.shiga.jp +higashiomi.shiga.jp +hikone.shiga.jp +koka.shiga.jp +konan.shiga.jp +kosei.shiga.jp +koto.shiga.jp +kusatsu.shiga.jp +maibara.shiga.jp +moriyama.shiga.jp +nagahama.shiga.jp +nishiazai.shiga.jp +notogawa.shiga.jp +omihachiman.shiga.jp +otsu.shiga.jp +ritto.shiga.jp +ryuoh.shiga.jp +takashima.shiga.jp +takatsuki.shiga.jp +torahime.shiga.jp +toyosato.shiga.jp +yasu.shiga.jp +akagi.shimane.jp +ama.shimane.jp +gotsu.shimane.jp +hamada.shimane.jp +higashiizumo.shimane.jp +hikawa.shimane.jp +hikimi.shimane.jp +izumo.shimane.jp +kakinoki.shimane.jp +masuda.shimane.jp +matsue.shimane.jp +misato.shimane.jp +nishinoshima.shimane.jp +ohda.shimane.jp +okinoshima.shimane.jp +okuizumo.shimane.jp +shimane.shimane.jp +tamayu.shimane.jp +tsuwano.shimane.jp +unnan.shimane.jp +yakumo.shimane.jp +yasugi.shimane.jp +yatsuka.shimane.jp +arai.shizuoka.jp +atami.shizuoka.jp +fuji.shizuoka.jp +fujieda.shizuoka.jp +fujikawa.shizuoka.jp +fujinomiya.shizuoka.jp +fukuroi.shizuoka.jp +gotemba.shizuoka.jp +haibara.shizuoka.jp +hamamatsu.shizuoka.jp +higashiizu.shizuoka.jp +ito.shizuoka.jp +iwata.shizuoka.jp +izu.shizuoka.jp +izunokuni.shizuoka.jp +kakegawa.shizuoka.jp +kannami.shizuoka.jp +kawanehon.shizuoka.jp +kawazu.shizuoka.jp +kikugawa.shizuoka.jp +kosai.shizuoka.jp +makinohara.shizuoka.jp +matsuzaki.shizuoka.jp +minamiizu.shizuoka.jp +mishima.shizuoka.jp +morimachi.shizuoka.jp +nishiizu.shizuoka.jp +numazu.shizuoka.jp +omaezaki.shizuoka.jp +shimada.shizuoka.jp +shimizu.shizuoka.jp +shimoda.shizuoka.jp +shizuoka.shizuoka.jp +susono.shizuoka.jp +yaizu.shizuoka.jp +yoshida.shizuoka.jp +ashikaga.tochigi.jp +bato.tochigi.jp +haga.tochigi.jp +ichikai.tochigi.jp +iwafune.tochigi.jp +kaminokawa.tochigi.jp +kanuma.tochigi.jp +karasuyama.tochigi.jp +kuroiso.tochigi.jp +mashiko.tochigi.jp +mibu.tochigi.jp +moka.tochigi.jp +motegi.tochigi.jp +nasu.tochigi.jp +nasushiobara.tochigi.jp +nikko.tochigi.jp +nishikata.tochigi.jp +nogi.tochigi.jp +ohira.tochigi.jp +ohtawara.tochigi.jp +oyama.tochigi.jp +sakura.tochigi.jp +sano.tochigi.jp +shimotsuke.tochigi.jp +shioya.tochigi.jp +takanezawa.tochigi.jp +tochigi.tochigi.jp +tsuga.tochigi.jp +ujiie.tochigi.jp +utsunomiya.tochigi.jp +yaita.tochigi.jp +aizumi.tokushima.jp +anan.tokushima.jp +ichiba.tokushima.jp +itano.tokushima.jp +kainan.tokushima.jp +komatsushima.tokushima.jp +matsushige.tokushima.jp +mima.tokushima.jp +minami.tokushima.jp +miyoshi.tokushima.jp +mugi.tokushima.jp +nakagawa.tokushima.jp +naruto.tokushima.jp +sanagochi.tokushima.jp +shishikui.tokushima.jp +tokushima.tokushima.jp +wajiki.tokushima.jp +adachi.tokyo.jp +akiruno.tokyo.jp +akishima.tokyo.jp +aogashima.tokyo.jp +arakawa.tokyo.jp +bunkyo.tokyo.jp +chiyoda.tokyo.jp +chofu.tokyo.jp +chuo.tokyo.jp +edogawa.tokyo.jp +fuchu.tokyo.jp +fussa.tokyo.jp +hachijo.tokyo.jp +hachioji.tokyo.jp +hamura.tokyo.jp +higashikurume.tokyo.jp +higashimurayama.tokyo.jp +higashiyamato.tokyo.jp +hino.tokyo.jp +hinode.tokyo.jp +hinohara.tokyo.jp +inagi.tokyo.jp +itabashi.tokyo.jp +katsushika.tokyo.jp +kita.tokyo.jp +kiyose.tokyo.jp +kodaira.tokyo.jp +koganei.tokyo.jp +kokubunji.tokyo.jp +komae.tokyo.jp +koto.tokyo.jp +kouzushima.tokyo.jp +kunitachi.tokyo.jp +machida.tokyo.jp +meguro.tokyo.jp +minato.tokyo.jp +mitaka.tokyo.jp +mizuho.tokyo.jp +musashimurayama.tokyo.jp +musashino.tokyo.jp +nakano.tokyo.jp +nerima.tokyo.jp +ogasawara.tokyo.jp +okutama.tokyo.jp +ome.tokyo.jp +oshima.tokyo.jp +ota.tokyo.jp +setagaya.tokyo.jp +shibuya.tokyo.jp +shinagawa.tokyo.jp +shinjuku.tokyo.jp +suginami.tokyo.jp +sumida.tokyo.jp +tachikawa.tokyo.jp +taito.tokyo.jp +tama.tokyo.jp +toshima.tokyo.jp +chizu.tottori.jp +hino.tottori.jp +kawahara.tottori.jp +koge.tottori.jp +kotoura.tottori.jp +misasa.tottori.jp +nanbu.tottori.jp +nichinan.tottori.jp +sakaiminato.tottori.jp +tottori.tottori.jp +wakasa.tottori.jp +yazu.tottori.jp +yonago.tottori.jp +asahi.toyama.jp +fuchu.toyama.jp +fukumitsu.toyama.jp +funahashi.toyama.jp +himi.toyama.jp +imizu.toyama.jp +inami.toyama.jp +johana.toyama.jp +kamiichi.toyama.jp +kurobe.toyama.jp +nakaniikawa.toyama.jp +namerikawa.toyama.jp +nanto.toyama.jp +nyuzen.toyama.jp +oyabe.toyama.jp +taira.toyama.jp +takaoka.toyama.jp +tateyama.toyama.jp +toga.toyama.jp +tonami.toyama.jp +toyama.toyama.jp +unazuki.toyama.jp +uozu.toyama.jp +yamada.toyama.jp +arida.wakayama.jp +aridagawa.wakayama.jp +gobo.wakayama.jp +hashimoto.wakayama.jp +hidaka.wakayama.jp +hirogawa.wakayama.jp +inami.wakayama.jp +iwade.wakayama.jp +kainan.wakayama.jp +kamitonda.wakayama.jp +katsuragi.wakayama.jp +kimino.wakayama.jp +kinokawa.wakayama.jp +kitayama.wakayama.jp +koya.wakayama.jp +koza.wakayama.jp +kozagawa.wakayama.jp +kudoyama.wakayama.jp +kushimoto.wakayama.jp +mihama.wakayama.jp +misato.wakayama.jp +nachikatsuura.wakayama.jp +shingu.wakayama.jp +shirahama.wakayama.jp +taiji.wakayama.jp +tanabe.wakayama.jp +wakayama.wakayama.jp +yuasa.wakayama.jp +yura.wakayama.jp +asahi.yamagata.jp +funagata.yamagata.jp +higashine.yamagata.jp +iide.yamagata.jp +kahoku.yamagata.jp +kaminoyama.yamagata.jp +kaneyama.yamagata.jp +kawanishi.yamagata.jp +mamurogawa.yamagata.jp +mikawa.yamagata.jp +murayama.yamagata.jp +nagai.yamagata.jp +nakayama.yamagata.jp +nanyo.yamagata.jp +nishikawa.yamagata.jp +obanazawa.yamagata.jp +oe.yamagata.jp +oguni.yamagata.jp +ohkura.yamagata.jp +oishida.yamagata.jp +sagae.yamagata.jp +sakata.yamagata.jp +sakegawa.yamagata.jp +shinjo.yamagata.jp +shirataka.yamagata.jp +shonai.yamagata.jp +takahata.yamagata.jp +tendo.yamagata.jp +tozawa.yamagata.jp +tsuruoka.yamagata.jp +yamagata.yamagata.jp +yamanobe.yamagata.jp +yonezawa.yamagata.jp +yuza.yamagata.jp +abu.yamaguchi.jp +hagi.yamaguchi.jp +hikari.yamaguchi.jp +hofu.yamaguchi.jp +iwakuni.yamaguchi.jp +kudamatsu.yamaguchi.jp +mitou.yamaguchi.jp +nagato.yamaguchi.jp +oshima.yamaguchi.jp +shimonoseki.yamaguchi.jp +shunan.yamaguchi.jp +tabuse.yamaguchi.jp +tokuyama.yamaguchi.jp +toyota.yamaguchi.jp +ube.yamaguchi.jp +yuu.yamaguchi.jp +chuo.yamanashi.jp +doshi.yamanashi.jp +fuefuki.yamanashi.jp +fujikawa.yamanashi.jp +fujikawaguchiko.yamanashi.jp +fujiyoshida.yamanashi.jp +hayakawa.yamanashi.jp +hokuto.yamanashi.jp +ichikawamisato.yamanashi.jp +kai.yamanashi.jp +kofu.yamanashi.jp +koshu.yamanashi.jp +kosuge.yamanashi.jp +minami-alps.yamanashi.jp +minobu.yamanashi.jp +nakamichi.yamanashi.jp +nanbu.yamanashi.jp +narusawa.yamanashi.jp +nirasaki.yamanashi.jp +nishikatsura.yamanashi.jp +oshino.yamanashi.jp +otsuki.yamanashi.jp +showa.yamanashi.jp +tabayama.yamanashi.jp +tsuru.yamanashi.jp +uenohara.yamanashi.jp +yamanakako.yamanashi.jp +yamanashi.yamanashi.jp + +// ke : http://www.kenic.or.ke/index.php?option=com_content&task=view&id=117&Itemid=145 +*.ke + +// kg : http://www.domain.kg/dmn_n.html +kg +org.kg +net.kg +com.kg +edu.kg +gov.kg +mil.kg + +// kh : http://www.mptc.gov.kh/dns_registration.htm +*.kh + +// ki : http://www.ki/dns/index.html +ki +edu.ki +biz.ki +net.ki +org.ki +gov.ki +info.ki +com.ki + +// km : http://en.wikipedia.org/wiki/.km +// http://www.domaine.km/documents/charte.doc +km +org.km +nom.km +gov.km +prd.km +tm.km +edu.km +mil.km +ass.km +com.km +// These are only mentioned as proposed suggestions at domaine.km, but +// http://en.wikipedia.org/wiki/.km says they're available for registration: +coop.km +asso.km +presse.km +medecin.km +notaires.km +pharmaciens.km +veterinaire.km +gouv.km + +// kn : http://en.wikipedia.org/wiki/.kn +// http://www.dot.kn/domainRules.html +kn +net.kn +org.kn +edu.kn +gov.kn + +// kp : http://www.kcce.kp/en_index.php +kp +com.kp +edu.kp +gov.kp +org.kp +rep.kp +tra.kp + +// kr : http://en.wikipedia.org/wiki/.kr +// see also: http://domain.nida.or.kr/eng/registration.jsp +kr +ac.kr +co.kr +es.kr +go.kr +hs.kr +kg.kr +mil.kr +ms.kr +ne.kr +or.kr +pe.kr +re.kr +sc.kr +// kr geographical names +busan.kr +chungbuk.kr +chungnam.kr +daegu.kr +daejeon.kr +gangwon.kr +gwangju.kr +gyeongbuk.kr +gyeonggi.kr +gyeongnam.kr +incheon.kr +jeju.kr +jeonbuk.kr +jeonnam.kr +seoul.kr +ulsan.kr + +// kw : http://en.wikipedia.org/wiki/.kw +*.kw + +// ky : http://www.icta.ky/da_ky_reg_dom.php +// Confirmed by registry 2008-06-17 +ky +edu.ky +gov.ky +com.ky +org.ky +net.ky + +// kz : http://en.wikipedia.org/wiki/.kz +// see also: http://www.nic.kz/rules/index.jsp +kz +org.kz +edu.kz +net.kz +gov.kz +mil.kz +com.kz + +// la : http://en.wikipedia.org/wiki/.la +// Submitted by registry 2008-06-10 +la +int.la +net.la +info.la +edu.la +gov.la +per.la +com.la +org.la + +// lb : http://en.wikipedia.org/wiki/.lb +// Submitted by registry 2008-06-17 +lb +com.lb +edu.lb +gov.lb +net.lb +org.lb + +// lc : http://en.wikipedia.org/wiki/.lc +// see also: http://www.nic.lc/rules.htm +lc +com.lc +net.lc +co.lc +org.lc +edu.lc +gov.lc + +// li : http://en.wikipedia.org/wiki/.li +li + +// lk : http://www.nic.lk/seclevpr.html +lk +gov.lk +sch.lk +net.lk +int.lk +com.lk +org.lk +edu.lk +ngo.lk +soc.lk +web.lk +ltd.lk +assn.lk +grp.lk +hotel.lk +ac.lk + +// lr : http://psg.com/dns/lr/lr.txt +// Submitted by registry 2008-06-17 +lr +com.lr +edu.lr +gov.lr +org.lr +net.lr + +// ls : http://en.wikipedia.org/wiki/.ls +ls +co.ls +org.ls + +// lt : http://en.wikipedia.org/wiki/.lt +lt +// gov.lt : http://www.gov.lt/index_en.php +gov.lt + +// lu : http://www.dns.lu/en/ +lu + +// lv : http://www.nic.lv/DNS/En/generic.php +lv +com.lv +edu.lv +gov.lv +org.lv +mil.lv +id.lv +net.lv +asn.lv +conf.lv + +// ly : http://www.nic.ly/regulations.php +ly +com.ly +net.ly +gov.ly +plc.ly +edu.ly +sch.ly +med.ly +org.ly +id.ly + +// ma : http://en.wikipedia.org/wiki/.ma +// http://www.anrt.ma/fr/admin/download/upload/file_fr782.pdf +ma +co.ma +net.ma +gov.ma +org.ma +ac.ma +press.ma + +// mc : http://www.nic.mc/ +mc +tm.mc +asso.mc + +// md : http://en.wikipedia.org/wiki/.md +md + +// me : http://en.wikipedia.org/wiki/.me +me +co.me +net.me +org.me +edu.me +ac.me +gov.me +its.me +priv.me + +// mg : http://www.nic.mg/tarif.htm +mg +org.mg +nom.mg +gov.mg +prd.mg +tm.mg +edu.mg +mil.mg +com.mg + +// mh : http://en.wikipedia.org/wiki/.mh +mh + +// mil : http://en.wikipedia.org/wiki/.mil +mil + +// mk : http://en.wikipedia.org/wiki/.mk +// see also: http://dns.marnet.net.mk/postapka.php +mk +com.mk +org.mk +net.mk +edu.mk +gov.mk +inf.mk +name.mk + +// ml : http://www.gobin.info/domainname/ml-template.doc +// see also: http://en.wikipedia.org/wiki/.ml +ml +com.ml +edu.ml +gouv.ml +gov.ml +net.ml +org.ml +presse.ml + +// mm : http://en.wikipedia.org/wiki/.mm +*.mm + +// mn : http://en.wikipedia.org/wiki/.mn +mn +gov.mn +edu.mn +org.mn + +// mo : http://www.monic.net.mo/ +mo +com.mo +net.mo +org.mo +edu.mo +gov.mo + +// mobi : http://en.wikipedia.org/wiki/.mobi +mobi + +// mp : http://www.dot.mp/ +// Confirmed by registry 2008-06-17 +mp + +// mq : http://en.wikipedia.org/wiki/.mq +mq + +// mr : http://en.wikipedia.org/wiki/.mr +mr +gov.mr + +// ms : http://www.nic.ms/pdf/MS_Domain_Name_Rules.pdf +ms +com.ms +edu.ms +gov.ms +net.ms +org.ms + +// mt : https://www.nic.org.mt/go/policy +// Submitted by registry 2013-11-19 +mt +com.mt +edu.mt +net.mt +org.mt + +// mu : http://en.wikipedia.org/wiki/.mu +mu +com.mu +net.mu +org.mu +gov.mu +ac.mu +co.mu +or.mu + +// museum : http://about.museum/naming/ +// http://index.museum/ +museum +academy.museum +agriculture.museum +air.museum +airguard.museum +alabama.museum +alaska.museum +amber.museum +ambulance.museum +american.museum +americana.museum +americanantiques.museum +americanart.museum +amsterdam.museum +and.museum +annefrank.museum +anthro.museum +anthropology.museum +antiques.museum +aquarium.museum +arboretum.museum +archaeological.museum +archaeology.museum +architecture.museum +art.museum +artanddesign.museum +artcenter.museum +artdeco.museum +arteducation.museum +artgallery.museum +arts.museum +artsandcrafts.museum +asmatart.museum +assassination.museum +assisi.museum +association.museum +astronomy.museum +atlanta.museum +austin.museum +australia.museum +automotive.museum +aviation.museum +axis.museum +badajoz.museum +baghdad.museum +bahn.museum +bale.museum +baltimore.museum +barcelona.museum +baseball.museum +basel.museum +baths.museum +bauern.museum +beauxarts.museum +beeldengeluid.museum +bellevue.museum +bergbau.museum +berkeley.museum +berlin.museum +bern.museum +bible.museum +bilbao.museum +bill.museum +birdart.museum +birthplace.museum +bonn.museum +boston.museum +botanical.museum +botanicalgarden.museum +botanicgarden.museum +botany.museum +brandywinevalley.museum +brasil.museum +bristol.museum +british.museum +britishcolumbia.museum +broadcast.museum +brunel.museum +brussel.museum +brussels.museum +bruxelles.museum +building.museum +burghof.museum +bus.museum +bushey.museum +cadaques.museum +california.museum +cambridge.museum +can.museum +canada.museum +capebreton.museum +carrier.museum +cartoonart.museum +casadelamoneda.museum +castle.museum +castres.museum +celtic.museum +center.museum +chattanooga.museum +cheltenham.museum +chesapeakebay.museum +chicago.museum +children.museum +childrens.museum +childrensgarden.museum +chiropractic.museum +chocolate.museum +christiansburg.museum +cincinnati.museum +cinema.museum +circus.museum +civilisation.museum +civilization.museum +civilwar.museum +clinton.museum +clock.museum +coal.museum +coastaldefence.museum +cody.museum +coldwar.museum +collection.museum +colonialwilliamsburg.museum +coloradoplateau.museum +columbia.museum +columbus.museum +communication.museum +communications.museum +community.museum +computer.museum +computerhistory.museum +comunicações.museum +contemporary.museum +contemporaryart.museum +convent.museum +copenhagen.museum +corporation.museum +correios-e-telecomunicações.museum +corvette.museum +costume.museum +countryestate.museum +county.museum +crafts.museum +cranbrook.museum +creation.museum +cultural.museum +culturalcenter.museum +culture.museum +cyber.museum +cymru.museum +dali.museum +dallas.museum +database.museum +ddr.museum +decorativearts.museum +delaware.museum +delmenhorst.museum +denmark.museum +depot.museum +design.museum +detroit.museum +dinosaur.museum +discovery.museum +dolls.museum +donostia.museum +durham.museum +eastafrica.museum +eastcoast.museum +education.museum +educational.museum +egyptian.museum +eisenbahn.museum +elburg.museum +elvendrell.museum +embroidery.museum +encyclopedic.museum +england.museum +entomology.museum +environment.museum +environmentalconservation.museum +epilepsy.museum +essex.museum +estate.museum +ethnology.museum +exeter.museum +exhibition.museum +family.museum +farm.museum +farmequipment.museum +farmers.museum +farmstead.museum +field.museum +figueres.museum +filatelia.museum +film.museum +fineart.museum +finearts.museum +finland.museum +flanders.museum +florida.museum +force.museum +fortmissoula.museum +fortworth.museum +foundation.museum +francaise.museum +frankfurt.museum +franziskaner.museum +freemasonry.museum +freiburg.museum +fribourg.museum +frog.museum +fundacio.museum +furniture.museum +gallery.museum +garden.museum +gateway.museum +geelvinck.museum +gemological.museum +geology.museum +georgia.museum +giessen.museum +glas.museum +glass.museum +gorge.museum +grandrapids.museum +graz.museum +guernsey.museum +halloffame.museum +hamburg.museum +handson.museum +harvestcelebration.museum +hawaii.museum +health.museum +heimatunduhren.museum +hellas.museum +helsinki.museum +hembygdsforbund.museum +heritage.museum +histoire.museum +historical.museum +historicalsociety.museum +historichouses.museum +historisch.museum +historisches.museum +history.museum +historyofscience.museum +horology.museum +house.museum +humanities.museum +illustration.museum +imageandsound.museum +indian.museum +indiana.museum +indianapolis.museum +indianmarket.museum +intelligence.museum +interactive.museum +iraq.museum +iron.museum +isleofman.museum +jamison.museum +jefferson.museum +jerusalem.museum +jewelry.museum +jewish.museum +jewishart.museum +jfk.museum +journalism.museum +judaica.museum +judygarland.museum +juedisches.museum +juif.museum +karate.museum +karikatur.museum +kids.museum +koebenhavn.museum +koeln.museum +kunst.museum +kunstsammlung.museum +kunstunddesign.museum +labor.museum +labour.museum +lajolla.museum +lancashire.museum +landes.museum +lans.museum +läns.museum +larsson.museum +lewismiller.museum +lincoln.museum +linz.museum +living.museum +livinghistory.museum +localhistory.museum +london.museum +losangeles.museum +louvre.museum +loyalist.museum +lucerne.museum +luxembourg.museum +luzern.museum +mad.museum +madrid.museum +mallorca.museum +manchester.museum +mansion.museum +mansions.museum +manx.museum +marburg.museum +maritime.museum +maritimo.museum +maryland.museum +marylhurst.museum +media.museum +medical.museum +medizinhistorisches.museum +meeres.museum +memorial.museum +mesaverde.museum +michigan.museum +midatlantic.museum +military.museum +mill.museum +miners.museum +mining.museum +minnesota.museum +missile.museum +missoula.museum +modern.museum +moma.museum +money.museum +monmouth.museum +monticello.museum +montreal.museum +moscow.museum +motorcycle.museum +muenchen.museum +muenster.museum +mulhouse.museum +muncie.museum +museet.museum +museumcenter.museum +museumvereniging.museum +music.museum +national.museum +nationalfirearms.museum +nationalheritage.museum +nativeamerican.museum +naturalhistory.museum +naturalhistorymuseum.museum +naturalsciences.museum +nature.museum +naturhistorisches.museum +natuurwetenschappen.museum +naumburg.museum +naval.museum +nebraska.museum +neues.museum +newhampshire.museum +newjersey.museum +newmexico.museum +newport.museum +newspaper.museum +newyork.museum +niepce.museum +norfolk.museum +north.museum +nrw.museum +nuernberg.museum +nuremberg.museum +nyc.museum +nyny.museum +oceanographic.museum +oceanographique.museum +omaha.museum +online.museum +ontario.museum +openair.museum +oregon.museum +oregontrail.museum +otago.museum +oxford.museum +pacific.museum +paderborn.museum +palace.museum +paleo.museum +palmsprings.museum +panama.museum +paris.museum +pasadena.museum +pharmacy.museum +philadelphia.museum +philadelphiaarea.museum +philately.museum +phoenix.museum +photography.museum +pilots.museum +pittsburgh.museum +planetarium.museum +plantation.museum +plants.museum +plaza.museum +portal.museum +portland.museum +portlligat.museum +posts-and-telecommunications.museum +preservation.museum +presidio.museum +press.museum +project.museum +public.museum +pubol.museum +quebec.museum +railroad.museum +railway.museum +research.museum +resistance.museum +riodejaneiro.museum +rochester.museum +rockart.museum +roma.museum +russia.museum +saintlouis.museum +salem.museum +salvadordali.museum +salzburg.museum +sandiego.museum +sanfrancisco.museum +santabarbara.museum +santacruz.museum +santafe.museum +saskatchewan.museum +satx.museum +savannahga.museum +schlesisches.museum +schoenbrunn.museum +schokoladen.museum +school.museum +schweiz.museum +science.museum +scienceandhistory.museum +scienceandindustry.museum +sciencecenter.museum +sciencecenters.museum +science-fiction.museum +sciencehistory.museum +sciences.museum +sciencesnaturelles.museum +scotland.museum +seaport.museum +settlement.museum +settlers.museum +shell.museum +sherbrooke.museum +sibenik.museum +silk.museum +ski.museum +skole.museum +society.museum +sologne.museum +soundandvision.museum +southcarolina.museum +southwest.museum +space.museum +spy.museum +square.museum +stadt.museum +stalbans.museum +starnberg.museum +state.museum +stateofdelaware.museum +station.museum +steam.museum +steiermark.museum +stjohn.museum +stockholm.museum +stpetersburg.museum +stuttgart.museum +suisse.museum +surgeonshall.museum +surrey.museum +svizzera.museum +sweden.museum +sydney.museum +tank.museum +tcm.museum +technology.museum +telekommunikation.museum +television.museum +texas.museum +textile.museum +theater.museum +time.museum +timekeeping.museum +topology.museum +torino.museum +touch.museum +town.museum +transport.museum +tree.museum +trolley.museum +trust.museum +trustee.museum +uhren.museum +ulm.museum +undersea.museum +university.museum +usa.museum +usantiques.museum +usarts.museum +uscountryestate.museum +usculture.museum +usdecorativearts.museum +usgarden.museum +ushistory.museum +ushuaia.museum +uslivinghistory.museum +utah.museum +uvic.museum +valley.museum +vantaa.museum +versailles.museum +viking.museum +village.museum +virginia.museum +virtual.museum +virtuel.museum +vlaanderen.museum +volkenkunde.museum +wales.museum +wallonie.museum +war.museum +washingtondc.museum +watchandclock.museum +watch-and-clock.museum +western.museum +westfalen.museum +whaling.museum +wildlife.museum +williamsburg.museum +windmill.museum +workshop.museum +york.museum +yorkshire.museum +yosemite.museum +youth.museum +zoological.museum +zoology.museum +ירושלים.museum +иком.museum + +// mv : http://en.wikipedia.org/wiki/.mv +// "mv" included because, contra Wikipedia, google.mv exists. +mv +aero.mv +biz.mv +com.mv +coop.mv +edu.mv +gov.mv +info.mv +int.mv +mil.mv +museum.mv +name.mv +net.mv +org.mv +pro.mv + +// mw : http://www.registrar.mw/ +mw +ac.mw +biz.mw +co.mw +com.mw +coop.mw +edu.mw +gov.mw +int.mw +museum.mw +net.mw +org.mw + +// mx : http://www.nic.mx/ +// Submitted by registry 2008-06-19 +mx +com.mx +org.mx +gob.mx +edu.mx +net.mx + +// my : http://www.mynic.net.my/ +my +com.my +net.my +org.my +gov.my +edu.my +mil.my +name.my + +// mz : http://www.gobin.info/domainname/mz-template.doc +*.mz +!teledata.mz + +// na : http://www.na-nic.com.na/ +// http://www.info.na/domain/ +na +info.na +pro.na +name.na +school.na +or.na +dr.na +us.na +mx.na +ca.na +in.na +cc.na +tv.na +ws.na +mobi.na +co.na +com.na +org.na + +// name : has 2nd-level tlds, but there's no list of them +name + +// nc : http://www.cctld.nc/ +nc +asso.nc + +// ne : http://en.wikipedia.org/wiki/.ne +ne + +// net : http://en.wikipedia.org/wiki/.net +net + +// nf : http://en.wikipedia.org/wiki/.nf +nf +com.nf +net.nf +per.nf +rec.nf +web.nf +arts.nf +firm.nf +info.nf +other.nf +store.nf + +// ng : http://psg.com/dns/ng/ +ng +com.ng +edu.ng +name.ng +net.ng +org.ng +sch.ng +gov.ng +mil.ng +mobi.ng + +// ni : http://www.nic.ni/dominios.htm +*.ni + +// nl : http://en.wikipedia.org/wiki/.nl +// https://www.sidn.nl/ +// ccTLD for the Netherlands +nl + +// BV.nl will be a registry for dutch BV's (besloten vennootschap) +bv.nl + +// no : http://www.norid.no/regelverk/index.en.html +// The Norwegian registry has declined to notify us of updates. The web pages +// referenced below are the official source of the data. There is also an +// announce mailing list: +// https://postlister.uninett.no/sympa/info/norid-diskusjon +no +// Norid generic domains : http://www.norid.no/regelverk/vedlegg-c.en.html +fhs.no +vgs.no +fylkesbibl.no +folkebibl.no +museum.no +idrett.no +priv.no +// Non-Norid generic domains : http://www.norid.no/regelverk/vedlegg-d.en.html +mil.no +stat.no +dep.no +kommune.no +herad.no +// no geographical names : http://www.norid.no/regelverk/vedlegg-b.en.html +// counties +aa.no +ah.no +bu.no +fm.no +hl.no +hm.no +jan-mayen.no +mr.no +nl.no +nt.no +of.no +ol.no +oslo.no +rl.no +sf.no +st.no +svalbard.no +tm.no +tr.no +va.no +vf.no +// primary and lower secondary schools per county +gs.aa.no +gs.ah.no +gs.bu.no +gs.fm.no +gs.hl.no +gs.hm.no +gs.jan-mayen.no +gs.mr.no +gs.nl.no +gs.nt.no +gs.of.no +gs.ol.no +gs.oslo.no +gs.rl.no +gs.sf.no +gs.st.no +gs.svalbard.no +gs.tm.no +gs.tr.no +gs.va.no +gs.vf.no +// cities +akrehamn.no +åkrehamn.no +algard.no +ålgård.no +arna.no +brumunddal.no +bryne.no +bronnoysund.no +brønnøysund.no +drobak.no +drøbak.no +egersund.no +fetsund.no +floro.no +florø.no +fredrikstad.no +hokksund.no +honefoss.no +hønefoss.no +jessheim.no +jorpeland.no +jørpeland.no +kirkenes.no +kopervik.no +krokstadelva.no +langevag.no +langevåg.no +leirvik.no +mjondalen.no +mjøndalen.no +mo-i-rana.no +mosjoen.no +mosjøen.no +nesoddtangen.no +orkanger.no +osoyro.no +osøyro.no +raholt.no +råholt.no +sandnessjoen.no +sandnessjøen.no +skedsmokorset.no +slattum.no +spjelkavik.no +stathelle.no +stavern.no +stjordalshalsen.no +stjørdalshalsen.no +tananger.no +tranby.no +vossevangen.no +// communities +afjord.no +åfjord.no +agdenes.no +al.no +ål.no +alesund.no +ålesund.no +alstahaug.no +alta.no +áltá.no +alaheadju.no +álaheadju.no +alvdal.no +amli.no +åmli.no +amot.no +åmot.no +andebu.no +andoy.no +andøy.no +andasuolo.no +ardal.no +årdal.no +aremark.no +arendal.no +ås.no +aseral.no +åseral.no +asker.no +askim.no +askvoll.no +askoy.no +askøy.no +asnes.no +åsnes.no +audnedaln.no +aukra.no +aure.no +aurland.no +aurskog-holand.no +aurskog-høland.no +austevoll.no +austrheim.no +averoy.no +averøy.no +balestrand.no +ballangen.no +balat.no +bálát.no +balsfjord.no +bahccavuotna.no +báhccavuotna.no +bamble.no +bardu.no +beardu.no +beiarn.no +bajddar.no +bájddar.no +baidar.no +báidár.no +berg.no +bergen.no +berlevag.no +berlevåg.no +bearalvahki.no +bearalváhki.no +bindal.no +birkenes.no +bjarkoy.no +bjarkøy.no +bjerkreim.no +bjugn.no +bodo.no +bodø.no +badaddja.no +bådåddjå.no +budejju.no +bokn.no +bremanger.no +bronnoy.no +brønnøy.no +bygland.no +bykle.no +barum.no +bærum.no +bo.telemark.no +bø.telemark.no +bo.nordland.no +bø.nordland.no +bievat.no +bievát.no +bomlo.no +bømlo.no +batsfjord.no +båtsfjord.no +bahcavuotna.no +báhcavuotna.no +dovre.no +drammen.no +drangedal.no +dyroy.no +dyrøy.no +donna.no +dønna.no +eid.no +eidfjord.no +eidsberg.no +eidskog.no +eidsvoll.no +eigersund.no +elverum.no +enebakk.no +engerdal.no +etne.no +etnedal.no +evenes.no +evenassi.no +evenášši.no +evje-og-hornnes.no +farsund.no +fauske.no +fuossko.no +fuoisku.no +fedje.no +fet.no +finnoy.no +finnøy.no +fitjar.no +fjaler.no +fjell.no +flakstad.no +flatanger.no +flekkefjord.no +flesberg.no +flora.no +fla.no +flå.no +folldal.no +forsand.no +fosnes.no +frei.no +frogn.no +froland.no +frosta.no +frana.no +fræna.no +froya.no +frøya.no +fusa.no +fyresdal.no +forde.no +førde.no +gamvik.no +gangaviika.no +gáŋgaviika.no +gaular.no +gausdal.no +gildeskal.no +gildeskål.no +giske.no +gjemnes.no +gjerdrum.no +gjerstad.no +gjesdal.no +gjovik.no +gjøvik.no +gloppen.no +gol.no +gran.no +grane.no +granvin.no +gratangen.no +grimstad.no +grong.no +kraanghke.no +kråanghke.no +grue.no +gulen.no +hadsel.no +halden.no +halsa.no +hamar.no +hamaroy.no +habmer.no +hábmer.no +hapmir.no +hápmir.no +hammerfest.no +hammarfeasta.no +hámmárfeasta.no +haram.no +hareid.no +harstad.no +hasvik.no +aknoluokta.no +ákŋoluokta.no +hattfjelldal.no +aarborte.no +haugesund.no +hemne.no +hemnes.no +hemsedal.no +heroy.more-og-romsdal.no +herøy.møre-og-romsdal.no +heroy.nordland.no +herøy.nordland.no +hitra.no +hjartdal.no +hjelmeland.no +hobol.no +hobøl.no +hof.no +hol.no +hole.no +holmestrand.no +holtalen.no +holtålen.no +hornindal.no +horten.no +hurdal.no +hurum.no +hvaler.no +hyllestad.no +hagebostad.no +hægebostad.no +hoyanger.no +høyanger.no +hoylandet.no +høylandet.no +ha.no +hå.no +ibestad.no +inderoy.no +inderøy.no +iveland.no +jevnaker.no +jondal.no +jolster.no +jølster.no +karasjok.no +karasjohka.no +kárášjohka.no +karlsoy.no +galsa.no +gálsá.no +karmoy.no +karmøy.no +kautokeino.no +guovdageaidnu.no +klepp.no +klabu.no +klæbu.no +kongsberg.no +kongsvinger.no +kragero.no +kragerø.no +kristiansand.no +kristiansund.no +krodsherad.no +krødsherad.no +kvalsund.no +rahkkeravju.no +ráhkkerávju.no +kvam.no +kvinesdal.no +kvinnherad.no +kviteseid.no +kvitsoy.no +kvitsøy.no +kvafjord.no +kvæfjord.no +giehtavuoatna.no +kvanangen.no +kvænangen.no +navuotna.no +návuotna.no +kafjord.no +kåfjord.no +gaivuotna.no +gáivuotna.no +larvik.no +lavangen.no +lavagis.no +loabat.no +loabát.no +lebesby.no +davvesiida.no +leikanger.no +leirfjord.no +leka.no +leksvik.no +lenvik.no +leangaviika.no +leaŋgaviika.no +lesja.no +levanger.no +lier.no +lierne.no +lillehammer.no +lillesand.no +lindesnes.no +lindas.no +lindås.no +lom.no +loppa.no +lahppi.no +láhppi.no +lund.no +lunner.no +luroy.no +lurøy.no +luster.no +lyngdal.no +lyngen.no +ivgu.no +lardal.no +lerdal.no +lærdal.no +lodingen.no +lødingen.no +lorenskog.no +lørenskog.no +loten.no +løten.no +malvik.no +masoy.no +måsøy.no +muosat.no +muosát.no +mandal.no +marker.no +marnardal.no +masfjorden.no +meland.no +meldal.no +melhus.no +meloy.no +meløy.no +meraker.no +meråker.no +moareke.no +moåreke.no +midsund.no +midtre-gauldal.no +modalen.no +modum.no +molde.no +moskenes.no +moss.no +mosvik.no +malselv.no +målselv.no +malatvuopmi.no +málatvuopmi.no +namdalseid.no +aejrie.no +namsos.no +namsskogan.no +naamesjevuemie.no +nååmesjevuemie.no +laakesvuemie.no +nannestad.no +narvik.no +narviika.no +naustdal.no +nedre-eiker.no +nes.akershus.no +nes.buskerud.no +nesna.no +nesodden.no +nesseby.no +unjarga.no +unjárga.no +nesset.no +nissedal.no +nittedal.no +nord-aurdal.no +nord-fron.no +nord-odal.no +norddal.no +nordkapp.no +davvenjarga.no +davvenjárga.no +nordre-land.no +nordreisa.no +raisa.no +ráisa.no +nore-og-uvdal.no +notodden.no +naroy.no +nærøy.no +notteroy.no +nøtterøy.no +odda.no +oksnes.no +øksnes.no +oppdal.no +oppegard.no +oppegård.no +orkdal.no +orland.no +ørland.no +orskog.no +ørskog.no +orsta.no +ørsta.no +os.hedmark.no +os.hordaland.no +osen.no +osteroy.no +osterøy.no +ostre-toten.no +østre-toten.no +overhalla.no +ovre-eiker.no +øvre-eiker.no +oyer.no +øyer.no +oygarden.no +øygarden.no +oystre-slidre.no +øystre-slidre.no +porsanger.no +porsangu.no +porsáŋgu.no +porsgrunn.no +radoy.no +radøy.no +rakkestad.no +rana.no +ruovat.no +randaberg.no +rauma.no +rendalen.no +rennebu.no +rennesoy.no +rennesøy.no +rindal.no +ringebu.no +ringerike.no +ringsaker.no +rissa.no +risor.no +risør.no +roan.no +rollag.no +rygge.no +ralingen.no +rælingen.no +rodoy.no +rødøy.no +romskog.no +rømskog.no +roros.no +røros.no +rost.no +røst.no +royken.no +røyken.no +royrvik.no +røyrvik.no +rade.no +råde.no +salangen.no +siellak.no +saltdal.no +salat.no +sálát.no +sálat.no +samnanger.no +sande.more-og-romsdal.no +sande.møre-og-romsdal.no +sande.vestfold.no +sandefjord.no +sandnes.no +sandoy.no +sandøy.no +sarpsborg.no +sauda.no +sauherad.no +sel.no +selbu.no +selje.no +seljord.no +sigdal.no +siljan.no +sirdal.no +skaun.no +skedsmo.no +ski.no +skien.no +skiptvet.no +skjervoy.no +skjervøy.no +skierva.no +skiervá.no +skjak.no +skjåk.no +skodje.no +skanland.no +skånland.no +skanit.no +skánit.no +smola.no +smøla.no +snillfjord.no +snasa.no +snåsa.no +snoasa.no +snaase.no +snåase.no +sogndal.no +sokndal.no +sola.no +solund.no +songdalen.no +sortland.no +spydeberg.no +stange.no +stavanger.no +steigen.no +steinkjer.no +stjordal.no +stjørdal.no +stokke.no +stor-elvdal.no +stord.no +stordal.no +storfjord.no +omasvuotna.no +strand.no +stranda.no +stryn.no +sula.no +suldal.no +sund.no +sunndal.no +surnadal.no +sveio.no +svelvik.no +sykkylven.no +sogne.no +søgne.no +somna.no +sømna.no +sondre-land.no +søndre-land.no +sor-aurdal.no +sør-aurdal.no +sor-fron.no +sør-fron.no +sor-odal.no +sør-odal.no +sor-varanger.no +sør-varanger.no +matta-varjjat.no +mátta-várjjat.no +sorfold.no +sørfold.no +sorreisa.no +sørreisa.no +sorum.no +sørum.no +tana.no +deatnu.no +time.no +tingvoll.no +tinn.no +tjeldsund.no +dielddanuorri.no +tjome.no +tjøme.no +tokke.no +tolga.no +torsken.no +tranoy.no +tranøy.no +tromso.no +tromsø.no +tromsa.no +romsa.no +trondheim.no +troandin.no +trysil.no +trana.no +træna.no +trogstad.no +trøgstad.no +tvedestrand.no +tydal.no +tynset.no +tysfjord.no +divtasvuodna.no +divttasvuotna.no +tysnes.no +tysvar.no +tysvær.no +tonsberg.no +tønsberg.no +ullensaker.no +ullensvang.no +ulvik.no +utsira.no +vadso.no +vadsø.no +cahcesuolo.no +čáhcesuolo.no +vaksdal.no +valle.no +vang.no +vanylven.no +vardo.no +vardø.no +varggat.no +várggát.no +vefsn.no +vaapste.no +vega.no +vegarshei.no +vegårshei.no +vennesla.no +verdal.no +verran.no +vestby.no +vestnes.no +vestre-slidre.no +vestre-toten.no +vestvagoy.no +vestvågøy.no +vevelstad.no +vik.no +vikna.no +vindafjord.no +volda.no +voss.no +varoy.no +værøy.no +vagan.no +vågan.no +voagat.no +vagsoy.no +vågsøy.no +vaga.no +vågå.no +valer.ostfold.no +våler.østfold.no +valer.hedmark.no +våler.hedmark.no + +// np : http://www.mos.com.np/register.html +*.np + +// nr : http://cenpac.net.nr/dns/index.html +// Confirmed by registry 2008-06-17 +nr +biz.nr +info.nr +gov.nr +edu.nr +org.nr +net.nr +com.nr + +// nu : http://en.wikipedia.org/wiki/.nu +nu + +// nz : http://en.wikipedia.org/wiki/.nz +// Confirmed by registry 2014-05-19 +nz +ac.nz +co.nz +cri.nz +geek.nz +gen.nz +govt.nz +health.nz +iwi.nz +kiwi.nz +maori.nz +mil.nz +māori.nz +net.nz +org.nz +parliament.nz +school.nz + +// om : http://en.wikipedia.org/wiki/.om +om +co.om +com.om +edu.om +gov.om +med.om +museum.om +net.om +org.om +pro.om + +// org : http://en.wikipedia.org/wiki/.org +org + +// pa : http://www.nic.pa/ +// Some additional second level "domains" resolve directly as hostnames, such as +// pannet.pa, so we add a rule for "pa". +pa +ac.pa +gob.pa +com.pa +org.pa +sld.pa +edu.pa +net.pa +ing.pa +abo.pa +med.pa +nom.pa + +// pe : https://www.nic.pe/InformeFinalComision.pdf +pe +edu.pe +gob.pe +nom.pe +mil.pe +org.pe +com.pe +net.pe + +// pf : http://www.gobin.info/domainname/formulaire-pf.pdf +pf +com.pf +org.pf +edu.pf + +// pg : http://en.wikipedia.org/wiki/.pg +*.pg + +// ph : http://www.domains.ph/FAQ2.asp +// Submitted by registry 2008-06-13 +ph +com.ph +net.ph +org.ph +gov.ph +edu.ph +ngo.ph +mil.ph +i.ph + +// pk : http://pk5.pknic.net.pk/pk5/msgNamepk.PK +pk +com.pk +net.pk +edu.pk +org.pk +fam.pk +biz.pk +web.pk +gov.pk +gob.pk +gok.pk +gon.pk +gop.pk +gos.pk +info.pk + +// pl http://www.dns.pl/english/index.html +// updated by .PL registry on 2015-04-28 +pl +com.pl +net.pl +org.pl +// pl functional domains (http://www.dns.pl/english/index.html) +aid.pl +agro.pl +atm.pl +auto.pl +biz.pl +edu.pl +gmina.pl +gsm.pl +info.pl +mail.pl +miasta.pl +media.pl +mil.pl +nieruchomosci.pl +nom.pl +pc.pl +powiat.pl +priv.pl +realestate.pl +rel.pl +sex.pl +shop.pl +sklep.pl +sos.pl +szkola.pl +targi.pl +tm.pl +tourism.pl +travel.pl +turystyka.pl +// Government domains +gov.pl +ap.gov.pl +ic.gov.pl +is.gov.pl +us.gov.pl +kmpsp.gov.pl +kppsp.gov.pl +kwpsp.gov.pl +psp.gov.pl +wskr.gov.pl +kwp.gov.pl +mw.gov.pl +ug.gov.pl +um.gov.pl +umig.gov.pl +ugim.gov.pl +upow.gov.pl +uw.gov.pl +starostwo.gov.pl +pa.gov.pl +po.gov.pl +psse.gov.pl +pup.gov.pl +rzgw.gov.pl +sa.gov.pl +so.gov.pl +sr.gov.pl +wsa.gov.pl +sko.gov.pl +uzs.gov.pl +wiih.gov.pl +winb.gov.pl +pinb.gov.pl +wios.gov.pl +witd.gov.pl +wzmiuw.gov.pl +piw.gov.pl +wiw.gov.pl +griw.gov.pl +wif.gov.pl +oum.gov.pl +sdn.gov.pl +zp.gov.pl +uppo.gov.pl +mup.gov.pl +wuoz.gov.pl +konsulat.gov.pl +oirm.gov.pl +// pl regional domains (http://www.dns.pl/english/index.html) +augustow.pl +babia-gora.pl +bedzin.pl +beskidy.pl +bialowieza.pl +bialystok.pl +bielawa.pl +bieszczady.pl +boleslawiec.pl +bydgoszcz.pl +bytom.pl +cieszyn.pl +czeladz.pl +czest.pl +dlugoleka.pl +elblag.pl +elk.pl +glogow.pl +gniezno.pl +gorlice.pl +grajewo.pl +ilawa.pl +jaworzno.pl +jelenia-gora.pl +jgora.pl +kalisz.pl +kazimierz-dolny.pl +karpacz.pl +kartuzy.pl +kaszuby.pl +katowice.pl +kepno.pl +ketrzyn.pl +klodzko.pl +kobierzyce.pl +kolobrzeg.pl +konin.pl +konskowola.pl +kutno.pl +lapy.pl +lebork.pl +legnica.pl +lezajsk.pl +limanowa.pl +lomza.pl +lowicz.pl +lubin.pl +lukow.pl +malbork.pl +malopolska.pl +mazowsze.pl +mazury.pl +mielec.pl +mielno.pl +mragowo.pl +naklo.pl +nowaruda.pl +nysa.pl +olawa.pl +olecko.pl +olkusz.pl +olsztyn.pl +opoczno.pl +opole.pl +ostroda.pl +ostroleka.pl +ostrowiec.pl +ostrowwlkp.pl +pila.pl +pisz.pl +podhale.pl +podlasie.pl +polkowice.pl +pomorze.pl +pomorskie.pl +prochowice.pl +pruszkow.pl +przeworsk.pl +pulawy.pl +radom.pl +rawa-maz.pl +rybnik.pl +rzeszow.pl +sanok.pl +sejny.pl +slask.pl +slupsk.pl +sosnowiec.pl +stalowa-wola.pl +skoczow.pl +starachowice.pl +stargard.pl +suwalki.pl +swidnica.pl +swiebodzin.pl +swinoujscie.pl +szczecin.pl +szczytno.pl +tarnobrzeg.pl +tgory.pl +turek.pl +tychy.pl +ustka.pl +walbrzych.pl +warmia.pl +warszawa.pl +waw.pl +wegrow.pl +wielun.pl +wlocl.pl +wloclawek.pl +wodzislaw.pl +wolomin.pl +wroclaw.pl +zachpomor.pl +zagan.pl +zarow.pl +zgora.pl +zgorzelec.pl + +// pm : http://www.afnic.fr/medias/documents/AFNIC-naming-policy2012.pdf +pm + +// pn : http://www.government.pn/PnRegistry/policies.htm +pn +gov.pn +co.pn +org.pn +edu.pn +net.pn + +// post : http://en.wikipedia.org/wiki/.post +post + +// pr : http://www.nic.pr/index.asp?f=1 +pr +com.pr +net.pr +org.pr +gov.pr +edu.pr +isla.pr +pro.pr +biz.pr +info.pr +name.pr +// these aren't mentioned on nic.pr, but on http://en.wikipedia.org/wiki/.pr +est.pr +prof.pr +ac.pr + +// pro : http://www.nic.pro/support_faq.htm +pro +aca.pro +bar.pro +cpa.pro +jur.pro +law.pro +med.pro +eng.pro + +// ps : http://en.wikipedia.org/wiki/.ps +// http://www.nic.ps/registration/policy.html#reg +ps +edu.ps +gov.ps +sec.ps +plo.ps +com.ps +org.ps +net.ps + +// pt : http://online.dns.pt/dns/start_dns +pt +net.pt +gov.pt +org.pt +edu.pt +int.pt +publ.pt +com.pt +nome.pt + +// pw : http://en.wikipedia.org/wiki/.pw +pw +co.pw +ne.pw +or.pw +ed.pw +go.pw +belau.pw + +// py : http://www.nic.py/pautas.html#seccion_9 +// Confirmed by registry 2012-10-03 +py +com.py +coop.py +edu.py +gov.py +mil.py +net.py +org.py + +// qa : http://domains.qa/en/ +qa +com.qa +edu.qa +gov.qa +mil.qa +name.qa +net.qa +org.qa +sch.qa + +// re : http://www.afnic.re/obtenir/chartes/nommage-re/annexe-descriptifs +re +com.re +asso.re +nom.re + +// ro : http://www.rotld.ro/ +ro +com.ro +org.ro +tm.ro +nt.ro +nom.ro +info.ro +rec.ro +arts.ro +firm.ro +store.ro +www.ro + +// rs : http://en.wikipedia.org/wiki/.rs +rs +co.rs +org.rs +edu.rs +ac.rs +gov.rs +in.rs + +// ru : http://www.cctld.ru/ru/docs/aktiv_8.php +// Industry domains +ru +ac.ru +com.ru +edu.ru +int.ru +net.ru +org.ru +pp.ru +// Geographical domains +adygeya.ru +altai.ru +amur.ru +arkhangelsk.ru +astrakhan.ru +bashkiria.ru +belgorod.ru +bir.ru +bryansk.ru +buryatia.ru +cbg.ru +chel.ru +chelyabinsk.ru +chita.ru +chukotka.ru +chuvashia.ru +dagestan.ru +dudinka.ru +e-burg.ru +grozny.ru +irkutsk.ru +ivanovo.ru +izhevsk.ru +jar.ru +joshkar-ola.ru +kalmykia.ru +kaluga.ru +kamchatka.ru +karelia.ru +kazan.ru +kchr.ru +kemerovo.ru +khabarovsk.ru +khakassia.ru +khv.ru +kirov.ru +koenig.ru +komi.ru +kostroma.ru +krasnoyarsk.ru +kuban.ru +kurgan.ru +kursk.ru +lipetsk.ru +magadan.ru +mari.ru +mari-el.ru +marine.ru +mordovia.ru +// mosreg.ru Bug 1090800 - removed at request of Aleksey Konstantinov +msk.ru +murmansk.ru +nalchik.ru +nnov.ru +nov.ru +novosibirsk.ru +nsk.ru +omsk.ru +orenburg.ru +oryol.ru +palana.ru +penza.ru +perm.ru +ptz.ru +rnd.ru +ryazan.ru +sakhalin.ru +samara.ru +saratov.ru +simbirsk.ru +smolensk.ru +spb.ru +stavropol.ru +stv.ru +surgut.ru +tambov.ru +tatarstan.ru +tom.ru +tomsk.ru +tsaritsyn.ru +tsk.ru +tula.ru +tuva.ru +tver.ru +tyumen.ru +udm.ru +udmurtia.ru +ulan-ude.ru +vladikavkaz.ru +vladimir.ru +vladivostok.ru +volgograd.ru +vologda.ru +voronezh.ru +vrn.ru +vyatka.ru +yakutia.ru +yamal.ru +yaroslavl.ru +yekaterinburg.ru +yuzhno-sakhalinsk.ru +// More geographical domains +amursk.ru +baikal.ru +cmw.ru +fareast.ru +jamal.ru +kms.ru +k-uralsk.ru +kustanai.ru +kuzbass.ru +magnitka.ru +mytis.ru +nakhodka.ru +nkz.ru +norilsk.ru +oskol.ru +pyatigorsk.ru +rubtsovsk.ru +snz.ru +syzran.ru +vdonsk.ru +zgrad.ru +// State domains +gov.ru +mil.ru +// Technical domains +test.ru + +// rw : http://www.nic.rw/cgi-bin/policy.pl +rw +gov.rw +net.rw +edu.rw +ac.rw +com.rw +co.rw +int.rw +mil.rw +gouv.rw + +// sa : http://www.nic.net.sa/ +sa +com.sa +net.sa +org.sa +gov.sa +med.sa +pub.sa +edu.sa +sch.sa + +// sb : http://www.sbnic.net.sb/ +// Submitted by registry 2008-06-08 +sb +com.sb +edu.sb +gov.sb +net.sb +org.sb + +// sc : http://www.nic.sc/ +sc +com.sc +gov.sc +net.sc +org.sc +edu.sc + +// sd : http://www.isoc.sd/sudanic.isoc.sd/billing_pricing.htm +// Submitted by registry 2008-06-17 +sd +com.sd +net.sd +org.sd +edu.sd +med.sd +tv.sd +gov.sd +info.sd + +// se : http://en.wikipedia.org/wiki/.se +// Submitted by registry 2014-03-18 +se +a.se +ac.se +b.se +bd.se +brand.se +c.se +d.se +e.se +f.se +fh.se +fhsk.se +fhv.se +g.se +h.se +i.se +k.se +komforb.se +kommunalforbund.se +komvux.se +l.se +lanbib.se +m.se +n.se +naturbruksgymn.se +o.se +org.se +p.se +parti.se +pp.se +press.se +r.se +s.se +t.se +tm.se +u.se +w.se +x.se +y.se +z.se + +// sg : http://www.nic.net.sg/page/registration-policies-procedures-and-guidelines +sg +com.sg +net.sg +org.sg +gov.sg +edu.sg +per.sg + +// sh : http://www.nic.sh/registrar.html +sh +com.sh +net.sh +gov.sh +org.sh +mil.sh + +// si : http://en.wikipedia.org/wiki/.si +si + +// sj : No registrations at this time. +// Submitted by registry 2008-06-16 +sj + +// sk : http://en.wikipedia.org/wiki/.sk +// list of 2nd level domains ? +sk + +// sl : http://www.nic.sl +// Submitted by registry 2008-06-12 +sl +com.sl +net.sl +edu.sl +gov.sl +org.sl + +// sm : http://en.wikipedia.org/wiki/.sm +sm + +// sn : http://en.wikipedia.org/wiki/.sn +sn +art.sn +com.sn +edu.sn +gouv.sn +org.sn +perso.sn +univ.sn + +// so : http://www.soregistry.com/ +so +com.so +net.so +org.so + +// sr : http://en.wikipedia.org/wiki/.sr +sr + +// st : http://www.nic.st/html/policyrules/ +st +co.st +com.st +consulado.st +edu.st +embaixada.st +gov.st +mil.st +net.st +org.st +principe.st +saotome.st +store.st + +// su : http://en.wikipedia.org/wiki/.su +su +adygeya.su +arkhangelsk.su +balashov.su +bashkiria.su +bryansk.su +dagestan.su +grozny.su +ivanovo.su +kalmykia.su +kaluga.su +karelia.su +khakassia.su +krasnodar.su +kurgan.su +lenug.su +mordovia.su +msk.su +murmansk.su +nalchik.su +nov.su +obninsk.su +penza.su +pokrovsk.su +sochi.su +spb.su +togliatti.su +troitsk.su +tula.su +tuva.su +vladikavkaz.su +vladimir.su +vologda.su + +// sv : http://www.svnet.org.sv/niveldos.pdf +sv +com.sv +edu.sv +gob.sv +org.sv +red.sv + +// sx : http://en.wikipedia.org/wiki/.sx +// Confirmed by registry 2012-05-31 +sx +gov.sx + +// sy : http://en.wikipedia.org/wiki/.sy +// see also: http://www.gobin.info/domainname/sy.doc +sy +edu.sy +gov.sy +net.sy +mil.sy +com.sy +org.sy + +// sz : http://en.wikipedia.org/wiki/.sz +// http://www.sispa.org.sz/ +sz +co.sz +ac.sz +org.sz + +// tc : http://en.wikipedia.org/wiki/.tc +tc + +// td : http://en.wikipedia.org/wiki/.td +td + +// tel: http://en.wikipedia.org/wiki/.tel +// http://www.telnic.org/ +tel + +// tf : http://en.wikipedia.org/wiki/.tf +tf + +// tg : http://en.wikipedia.org/wiki/.tg +// http://www.nic.tg/ +tg + +// th : http://en.wikipedia.org/wiki/.th +// Submitted by registry 2008-06-17 +th +ac.th +co.th +go.th +in.th +mi.th +net.th +or.th + +// tj : http://www.nic.tj/policy.html +tj +ac.tj +biz.tj +co.tj +com.tj +edu.tj +go.tj +gov.tj +int.tj +mil.tj +name.tj +net.tj +nic.tj +org.tj +test.tj +web.tj + +// tk : http://en.wikipedia.org/wiki/.tk +tk + +// tl : http://en.wikipedia.org/wiki/.tl +tl +gov.tl + +// tm : http://www.nic.tm/local.html +tm +com.tm +co.tm +org.tm +net.tm +nom.tm +gov.tm +mil.tm +edu.tm + +// tn : http://en.wikipedia.org/wiki/.tn +// http://whois.ati.tn/ +tn +com.tn +ens.tn +fin.tn +gov.tn +ind.tn +intl.tn +nat.tn +net.tn +org.tn +info.tn +perso.tn +tourism.tn +edunet.tn +rnrt.tn +rns.tn +rnu.tn +mincom.tn +agrinet.tn +defense.tn +turen.tn + +// to : http://en.wikipedia.org/wiki/.to +// Submitted by registry 2008-06-17 +to +com.to +gov.to +net.to +org.to +edu.to +mil.to + +// tp : No registrations at this time. +// Submitted by Ryan Sleevi 2014-01-03 +tp + +// subTLDs: https://www.nic.tr/forms/eng/policies.pdf +// and: https://www.nic.tr/forms/politikalar.pdf +// Submitted by 2014-07-19 +tr +com.tr +info.tr +biz.tr +net.tr +org.tr +web.tr +gen.tr +tv.tr +av.tr +dr.tr +bbs.tr +name.tr +tel.tr +gov.tr +bel.tr +pol.tr +mil.tr +k12.tr +edu.tr +kep.tr + +// Used by Northern Cyprus +nc.tr + +// Used by government agencies of Northern Cyprus +gov.nc.tr + +// travel : http://en.wikipedia.org/wiki/.travel +travel + +// tt : http://www.nic.tt/ +tt +co.tt +com.tt +org.tt +net.tt +biz.tt +info.tt +pro.tt +int.tt +coop.tt +jobs.tt +mobi.tt +travel.tt +museum.tt +aero.tt +name.tt +gov.tt +edu.tt + +// tv : http://en.wikipedia.org/wiki/.tv +// Not listing any 2LDs as reserved since none seem to exist in practice, +// Wikipedia notwithstanding. +tv + +// tw : http://en.wikipedia.org/wiki/.tw +tw +edu.tw +gov.tw +mil.tw +com.tw +net.tw +org.tw +idv.tw +game.tw +ebiz.tw +club.tw +網路.tw +組織.tw +商業.tw + +// tz : http://www.tznic.or.tz/index.php/domains +// Confirmed by registry 2013-01-22 +tz +ac.tz +co.tz +go.tz +hotel.tz +info.tz +me.tz +mil.tz +mobi.tz +ne.tz +or.tz +sc.tz +tv.tz + +// ua : https://hostmaster.ua/policy/?ua +// Submitted by registry 2012-04-27 +ua +// ua 2LD +com.ua +edu.ua +gov.ua +in.ua +net.ua +org.ua +// ua geographic names +// https://hostmaster.ua/2ld/ +cherkassy.ua +cherkasy.ua +chernigov.ua +chernihiv.ua +chernivtsi.ua +chernovtsy.ua +ck.ua +cn.ua +cr.ua +crimea.ua +cv.ua +dn.ua +dnepropetrovsk.ua +dnipropetrovsk.ua +dominic.ua +donetsk.ua +dp.ua +if.ua +ivano-frankivsk.ua +kh.ua +kharkiv.ua +kharkov.ua +kherson.ua +khmelnitskiy.ua +khmelnytskyi.ua +kiev.ua +kirovograd.ua +km.ua +kr.ua +krym.ua +ks.ua +kv.ua +kyiv.ua +lg.ua +lt.ua +lugansk.ua +lutsk.ua +lv.ua +lviv.ua +mk.ua +mykolaiv.ua +nikolaev.ua +od.ua +odesa.ua +odessa.ua +pl.ua +poltava.ua +rivne.ua +rovno.ua +rv.ua +sb.ua +sebastopol.ua +sevastopol.ua +sm.ua +sumy.ua +te.ua +ternopil.ua +uz.ua +uzhgorod.ua +vinnica.ua +vinnytsia.ua +vn.ua +volyn.ua +yalta.ua +zaporizhzhe.ua +zaporizhzhia.ua +zhitomir.ua +zhytomyr.ua +zp.ua +zt.ua + +// Private registries in .ua +co.ua +pp.ua + +// ug : https://www.registry.co.ug/ +ug +co.ug +or.ug +ac.ug +sc.ug +go.ug +ne.ug +com.ug +org.ug + +// uk : http://en.wikipedia.org/wiki/.uk +// Submitted by registry +uk +ac.uk +co.uk +gov.uk +ltd.uk +me.uk +net.uk +nhs.uk +org.uk +plc.uk +police.uk +*.sch.uk + +// us : http://en.wikipedia.org/wiki/.us +us +dni.us +fed.us +isa.us +kids.us +nsn.us +// us geographic names +ak.us +al.us +ar.us +as.us +az.us +ca.us +co.us +ct.us +dc.us +de.us +fl.us +ga.us +gu.us +hi.us +ia.us +id.us +il.us +in.us +ks.us +ky.us +la.us +ma.us +md.us +me.us +mi.us +mn.us +mo.us +ms.us +mt.us +nc.us +nd.us +ne.us +nh.us +nj.us +nm.us +nv.us +ny.us +oh.us +ok.us +or.us +pa.us +pr.us +ri.us +sc.us +sd.us +tn.us +tx.us +ut.us +vi.us +vt.us +va.us +wa.us +wi.us +wv.us +wy.us +// The registrar notes several more specific domains available in each state, +// such as state.*.us, dst.*.us, etc., but resolution of these is somewhat +// haphazard; in some states these domains resolve as addresses, while in others +// only subdomains are available, or even nothing at all. We include the +// most common ones where it's clear that different sites are different +// entities. +k12.ak.us +k12.al.us +k12.ar.us +k12.as.us +k12.az.us +k12.ca.us +k12.co.us +k12.ct.us +k12.dc.us +k12.de.us +k12.fl.us +k12.ga.us +k12.gu.us +// k12.hi.us Bug 614565 - Hawaii has a state-wide DOE login +k12.ia.us +k12.id.us +k12.il.us +k12.in.us +k12.ks.us +k12.ky.us +k12.la.us +k12.ma.us +k12.md.us +k12.me.us +k12.mi.us +k12.mn.us +k12.mo.us +k12.ms.us +k12.mt.us +k12.nc.us +// k12.nd.us Bug 1028347 - Removed at request of Travis Rosso +k12.ne.us +k12.nh.us +k12.nj.us +k12.nm.us +k12.nv.us +k12.ny.us +k12.oh.us +k12.ok.us +k12.or.us +k12.pa.us +k12.pr.us +k12.ri.us +k12.sc.us +// k12.sd.us Bug 934131 - Removed at request of James Booze +k12.tn.us +k12.tx.us +k12.ut.us +k12.vi.us +k12.vt.us +k12.va.us +k12.wa.us +k12.wi.us +// k12.wv.us Bug 947705 - Removed at request of Verne Britton +k12.wy.us +cc.ak.us +cc.al.us +cc.ar.us +cc.as.us +cc.az.us +cc.ca.us +cc.co.us +cc.ct.us +cc.dc.us +cc.de.us +cc.fl.us +cc.ga.us +cc.gu.us +cc.hi.us +cc.ia.us +cc.id.us +cc.il.us +cc.in.us +cc.ks.us +cc.ky.us +cc.la.us +cc.ma.us +cc.md.us +cc.me.us +cc.mi.us +cc.mn.us +cc.mo.us +cc.ms.us +cc.mt.us +cc.nc.us +cc.nd.us +cc.ne.us +cc.nh.us +cc.nj.us +cc.nm.us +cc.nv.us +cc.ny.us +cc.oh.us +cc.ok.us +cc.or.us +cc.pa.us +cc.pr.us +cc.ri.us +cc.sc.us +cc.sd.us +cc.tn.us +cc.tx.us +cc.ut.us +cc.vi.us +cc.vt.us +cc.va.us +cc.wa.us +cc.wi.us +cc.wv.us +cc.wy.us +lib.ak.us +lib.al.us +lib.ar.us +lib.as.us +lib.az.us +lib.ca.us +lib.co.us +lib.ct.us +lib.dc.us +lib.de.us +lib.fl.us +lib.ga.us +lib.gu.us +lib.hi.us +lib.ia.us +lib.id.us +lib.il.us +lib.in.us +lib.ks.us +lib.ky.us +lib.la.us +lib.ma.us +lib.md.us +lib.me.us +lib.mi.us +lib.mn.us +lib.mo.us +lib.ms.us +lib.mt.us +lib.nc.us +lib.nd.us +lib.ne.us +lib.nh.us +lib.nj.us +lib.nm.us +lib.nv.us +lib.ny.us +lib.oh.us +lib.ok.us +lib.or.us +lib.pa.us +lib.pr.us +lib.ri.us +lib.sc.us +lib.sd.us +lib.tn.us +lib.tx.us +lib.ut.us +lib.vi.us +lib.vt.us +lib.va.us +lib.wa.us +lib.wi.us +// lib.wv.us Bug 941670 - Removed at request of Larry W Arnold +lib.wy.us +// k12.ma.us contains school districts in Massachusetts. The 4LDs are +// managed indepedently except for private (PVT), charter (CHTR) and +// parochial (PAROCH) schools. Those are delegated dorectly to the +// 5LD operators. +pvt.k12.ma.us +chtr.k12.ma.us +paroch.k12.ma.us + +// uy : http://www.nic.org.uy/ +uy +com.uy +edu.uy +gub.uy +mil.uy +net.uy +org.uy + +// uz : http://www.reg.uz/ +uz +co.uz +com.uz +net.uz +org.uz + +// va : http://en.wikipedia.org/wiki/.va +va + +// vc : http://en.wikipedia.org/wiki/.vc +// Submitted by registry 2008-06-13 +vc +com.vc +net.vc +org.vc +gov.vc +mil.vc +edu.vc + +// ve : https://registro.nic.ve/ +// Confirmed by registry 2012-10-04 +// Updated 2014-05-20 - Bug 940478 +ve +arts.ve +co.ve +com.ve +e12.ve +edu.ve +firm.ve +gob.ve +gov.ve +info.ve +int.ve +mil.ve +net.ve +org.ve +rec.ve +store.ve +tec.ve +web.ve + +// vg : http://en.wikipedia.org/wiki/.vg +vg + +// vi : http://www.nic.vi/newdomainform.htm +// http://www.nic.vi/Domain_Rules/body_domain_rules.html indicates some other +// TLDs are "reserved", such as edu.vi and gov.vi, but doesn't actually say they +// are available for registration (which they do not seem to be). +vi +co.vi +com.vi +k12.vi +net.vi +org.vi + +// vn : https://www.dot.vn/vnnic/vnnic/domainregistration.jsp +vn +com.vn +net.vn +org.vn +edu.vn +gov.vn +int.vn +ac.vn +biz.vn +info.vn +name.vn +pro.vn +health.vn + +// vu : http://en.wikipedia.org/wiki/.vu +// http://www.vunic.vu/ +vu +com.vu +edu.vu +net.vu +org.vu + +// wf : http://www.afnic.fr/medias/documents/AFNIC-naming-policy2012.pdf +wf + +// ws : http://en.wikipedia.org/wiki/.ws +// http://samoanic.ws/index.dhtml +ws +com.ws +net.ws +org.ws +gov.ws +edu.ws + +// yt : http://www.afnic.fr/medias/documents/AFNIC-naming-policy2012.pdf +yt + +// IDN ccTLDs +// When submitting patches, please maintain a sort by ISO 3166 ccTLD, then +// U-label, and follow this format: +// // A-Label ("", [, variant info]) : +// // [sponsoring org] +// U-Label + +// xn--mgbaam7a8h ("Emerat", Arabic) : AE +// http://nic.ae/english/arabicdomain/rules.jsp +امارات + +// xn--y9a3aq ("hye", Armenian) : AM +// ISOC AM (operated by .am Registry) +հայ + +// xn--54b7fta0cc ("Bangla", Bangla) : BD +বাংলা + +// xn--90ais ("bel", Belarusian/Russian Cyrillic) : BY +// Operated by .by registry +бел + +// xn--fiqs8s ("Zhongguo/China", Chinese, Simplified) : CN +// CNNIC +// http://cnnic.cn/html/Dir/2005/10/11/3218.htm +中国 + +// xn--fiqz9s ("Zhongguo/China", Chinese, Traditional) : CN +// CNNIC +// http://cnnic.cn/html/Dir/2005/10/11/3218.htm +中國 + +// xn--lgbbat1ad8j ("Algeria/Al Jazair", Arabic) : DZ +الجزائر + +// xn--wgbh1c ("Egypt/Masr", Arabic) : EG +// http://www.dotmasr.eg/ +مصر + +// xn--node ("ge", Georgian Mkhedruli) : GE +გე + +// xn--qxam ("el", Greek) : GR +// Hellenic Ministry of Infrastructure, Transport, and Networks +ελ + +// xn--j6w193g ("Hong Kong", Chinese) : HK +// https://www2.hkirc.hk/register/rules.jsp +香港 + +// xn--h2brj9c ("Bharat", Devanagari) : IN +// India +भारत + +// xn--mgbbh1a71e ("Bharat", Arabic) : IN +// India +بھارت + +// xn--fpcrj9c3d ("Bharat", Telugu) : IN +// India +భారత్ + +// xn--gecrj9c ("Bharat", Gujarati) : IN +// India +ભારત + +// xn--s9brj9c ("Bharat", Gurmukhi) : IN +// India +ਭਾਰਤ + +// xn--45brj9c ("Bharat", Bengali) : IN +// India +ভারত + +// xn--xkc2dl3a5ee0h ("India", Tamil) : IN +// India +இந்தியா + +// xn--mgba3a4f16a ("Iran", Persian) : IR +ایران + +// xn--mgba3a4fra ("Iran", Arabic) : IR +ايران + +// xn--mgbtx2b ("Iraq", Arabic) : IQ +// Communications and Media Commission +عراق + +// xn--mgbayh7gpa ("al-Ordon", Arabic) : JO +// National Information Technology Center (NITC) +// Royal Scientific Society, Al-Jubeiha +الاردن + +// xn--3e0b707e ("Republic of Korea", Hangul) : KR +한국 + +// xn--80ao21a ("Kaz", Kazakh) : KZ +қаз + +// xn--fzc2c9e2c ("Lanka", Sinhalese-Sinhala) : LK +// http://nic.lk +ලංකා + +// xn--xkc2al3hye2a ("Ilangai", Tamil) : LK +// http://nic.lk +இலங்கை + +// xn--mgbc0a9azcg ("Morocco/al-Maghrib", Arabic) : MA +المغرب + +// xn--d1alf ("mkd", Macedonian) : MK +// MARnet +мкд + +// xn--l1acc ("mon", Mongolian) : MN +мон + +// xn--mix891f ("Macao", Chinese, Traditional) : MO +// MONIC / HNET Asia (Registry Operator for .mo) +澳門 + +// xn--mix082f ("Macao", Chinese, Simplified) : MO +澳门 + +// xn--mgbx4cd0ab ("Malaysia", Malay) : MY +مليسيا + +// xn--mgb9awbf ("Oman", Arabic) : OM +عمان + +// xn--mgbai9azgqp6j ("Pakistan", Urdu/Arabic) : PK +پاکستان + +// xn--mgbai9a5eva00b ("Pakistan", Urdu/Arabic, variant) : PK +پاكستان + +// xn--ygbi2ammx ("Falasteen", Arabic) : PS +// The Palestinian National Internet Naming Authority (PNINA) +// http://www.pnina.ps +فلسطين + +// xn--90a3ac ("srb", Cyrillic) : RS +// http://www.rnids.rs/en/the-.срб-domain +срб +пр.срб +орг.срб +обр.срб +од.срб +упр.срб +ак.срб + +// xn--p1ai ("rf", Russian-Cyrillic) : RU +// http://www.cctld.ru/en/docs/rulesrf.php +рф + +// xn--wgbl6a ("Qatar", Arabic) : QA +// http://www.ict.gov.qa/ +قطر + +// xn--mgberp4a5d4ar ("AlSaudiah", Arabic) : SA +// http://www.nic.net.sa/ +السعودية + +// xn--mgberp4a5d4a87g ("AlSaudiah", Arabic, variant) : SA +السعودیة + +// xn--mgbqly7c0a67fbc ("AlSaudiah", Arabic, variant) : SA +السعودیۃ + +// xn--mgbqly7cvafr ("AlSaudiah", Arabic, variant) : SA +السعوديه + +// xn--mgbpl2fh ("sudan", Arabic) : SD +// Operated by .sd registry +سودان + +// xn--yfro4i67o Singapore ("Singapore", Chinese) : SG +新加坡 + +// xn--clchc0ea0b2g2a9gcd ("Singapore", Tamil) : SG +சிங்கப்பூர் + +// xn--ogbpf8fl ("Syria", Arabic) : SY +سورية + +// xn--mgbtf8fl ("Syria", Arabic, variant) : SY +سوريا + +// xn--o3cw4h ("Thai", Thai) : TH +// http://www.thnic.co.th +ไทย + +// xn--pgbs0dh ("Tunisia", Arabic) : TN +// http://nic.tn +تونس + +// xn--kpry57d ("Taiwan", Chinese, Traditional) : TW +// http://www.twnic.net/english/dn/dn_07a.htm +台灣 + +// xn--kprw13d ("Taiwan", Chinese, Simplified) : TW +// http://www.twnic.net/english/dn/dn_07a.htm +台湾 + +// xn--nnx388a ("Taiwan", Chinese, variant) : TW +臺灣 + +// xn--j1amh ("ukr", Cyrillic) : UA +укр + +// xn--mgb2ddes ("AlYemen", Arabic) : YE +اليمن + +// xxx : http://icmregistry.com +xxx + +// ye : http://www.y.net.ye/services/domain_name.htm +*.ye + +// za : http://www.zadna.org.za/slds.html +*.za + +// zm : http://en.wikipedia.org/wiki/.zm +*.zm + +// zw : http://en.wikipedia.org/wiki/.zw +*.zw + + +// List of new gTLDs imported from https://newgtlds.icann.org/newgtlds.csv on 2015-05-06T09:31:08Z + +// aaa : 2015-02-26 American Automobile Association, Inc. +aaa + +// abb : 2014-10-24 ABB Ltd +abb + +// abbott : 2014-07-24 Abbott Laboratories, Inc. +abbott + +// abogado : 2014-04-24 Top Level Domain Holdings Limited +abogado + +// academy : 2013-11-07 Half Oaks, LLC +academy + +// accenture : 2014-08-15 Accenture plc +accenture + +// accountant : 2014-11-20 dot Accountant Limited +accountant + +// accountants : 2014-03-20 Knob Town, LLC +accountants + +// aco : 2015-01-08 ACO Severin Ahlmann GmbH & Co. KG +aco + +// active : 2014-05-01 The Active Network, Inc +active + +// actor : 2013-12-12 United TLD Holdco Ltd. +actor + +// ads : 2014-12-04 Charleston Road Registry Inc. +ads + +// adult : 2014-10-16 ICM Registry AD LLC +adult + +// aeg : 2015-03-19 Aktiebolaget Electrolux +aeg + +// afl : 2014-10-02 Australian Football League +afl + +// africa : 2014-03-24 ZA Central Registry NPC trading as Registry.Africa +africa + +// africamagic : 2015-03-05 Electronic Media Network (Pty) Ltd +africamagic + +// agakhan : 2015-04-23 Fondation Aga Khan (Aga Khan Foundation) +agakhan + +// agency : 2013-11-14 Steel Falls, LLC +agency + +// aig : 2014-12-18 American International Group, Inc. +aig + +// airforce : 2014-03-06 United TLD Holdco Ltd. +airforce + +// airtel : 2014-10-24 Bharti Airtel Limited +airtel + +// akdn : 2015-04-23 Fondation Aga Khan (Aga Khan Foundation) +akdn + +// alibaba : 2015-01-15 Alibaba Group Holding Limited +alibaba + +// alipay : 2015-01-15 Alibaba Group Holding Limited +alipay + +// allfinanz : 2014-07-03 Allfinanz Deutsche Vermögensberatung Aktiengesellschaft +allfinanz + +// alsace : 2014-07-02 REGION D ALSACE +alsace + +// amsterdam : 2014-07-24 Gemeente Amsterdam +amsterdam + +// analytics : 2014-12-18 Campus IP LLC +analytics + +// android : 2014-08-07 Charleston Road Registry Inc. +android + +// anquan : 2015-01-08 QIHOO 360 TECHNOLOGY CO. LTD. +anquan + +// apartments : 2014-12-11 June Maple, LLC +apartments + +// aquarelle : 2014-07-24 Aquarelle.com +aquarelle + +// aramco : 2014-11-20 Aramco Services Company +aramco + +// archi : 2014-02-06 STARTING DOT LIMITED +archi + +// army : 2014-03-06 United TLD Holdco Ltd. +army + +// arte : 2014-12-11 Association Relative à la Télévision Européenne G.E.I.E. +arte + +// associates : 2014-03-06 Baxter Hill, LLC +associates + +// attorney : 2014-03-20 +attorney + +// auction : 2014-03-20 +auction + +// audio : 2014-03-20 Uniregistry, Corp. +audio + +// author : 2014-12-18 Amazon EU S.à r.l. +author + +// auto : 2014-11-13 Uniregistry, Corp. +auto + +// autos : 2014-01-09 DERAutos, LLC +autos + +// avianca : 2015-01-08 Aerovias del Continente Americano S.A. Avianca +avianca + +// axa : 2013-12-19 AXA SA +axa + +// azure : 2014-12-18 Microsoft Corporation +azure + +// baby : 2015-04-09 Johnson & Johnson Services, Inc. +baby + +// baidu : 2015-01-08 Baidu, Inc. +baidu + +// band : 2014-06-12 +band + +// bank : 2014-09-25 fTLD Registry Services LLC +bank + +// bar : 2013-12-12 Punto 2012 Sociedad Anonima Promotora de Inversion de Capital Variable +bar + +// barcelona : 2014-07-24 Municipi de Barcelona +barcelona + +// barclaycard : 2014-11-20 Barclays Bank PLC +barclaycard + +// barclays : 2014-11-20 Barclays Bank PLC +barclays + +// bargains : 2013-11-14 Half Hallow, LLC +bargains + +// bauhaus : 2014-04-17 Werkhaus GmbH +bauhaus + +// bayern : 2014-01-23 Bayern Connect GmbH +bayern + +// bbc : 2014-12-18 British Broadcasting Corporation +bbc + +// bbva : 2014-10-02 BANCO BILBAO VIZCAYA ARGENTARIA, S.A. +bbva + +// bcg : 2015-04-02 The Boston Consulting Group, Inc. +bcg + +// bcn : 2014-07-24 Municipi de Barcelona +bcn + +// beer : 2014-01-09 Top Level Domain Holdings Limited +beer + +// bentley : 2014-12-18 Bentley Motors Limited +bentley + +// berlin : 2013-10-31 dotBERLIN GmbH & Co. KG +berlin + +// best : 2013-12-19 BestTLD Pty Ltd +best + +// bharti : 2014-01-09 Bharti Enterprises (Holding) Private Limited +bharti + +// bible : 2014-06-19 American Bible Society +bible + +// bid : 2013-12-19 dot Bid Limited +bid + +// bike : 2013-08-27 Grand Hollow, LLC +bike + +// bing : 2014-12-18 Microsoft Corporation +bing + +// bingo : 2014-12-04 Sand Cedar, LLC +bingo + +// bio : 2014-03-06 STARTING DOT LIMITED +bio + +// black : 2014-01-16 Afilias Limited +black + +// blackfriday : 2014-01-16 Uniregistry, Corp. +blackfriday + +// bloomberg : 2014-07-17 Bloomberg IP Holdings LLC +bloomberg + +// blue : 2013-11-07 Afilias Limited +blue + +// bms : 2014-10-30 Bristol-Myers Squibb Company +bms + +// bmw : 2014-01-09 Bayerische Motoren Werke Aktiengesellschaft +bmw + +// bnl : 2014-07-24 Banca Nazionale del Lavoro +bnl + +// bnpparibas : 2014-05-29 BNP Paribas +bnpparibas + +// boats : 2014-12-04 DERBoats, LLC +boats + +// bom : 2014-10-16 Núcleo de Informação e Coordenação do Ponto BR - NIC.br +bom + +// bond : 2014-06-05 Bond University Limited +bond + +// boo : 2014-01-30 Charleston Road Registry Inc. +boo + +// boots : 2015-01-08 THE BOOTS COMPANY PLC +boots + +// bot : 2014-12-18 Amazon EU S.à r.l. +bot + +// boutique : 2013-11-14 Over Galley, LLC +boutique + +// bradesco : 2014-12-18 Banco Bradesco S.A. +bradesco + +// bridgestone : 2014-12-18 Bridgestone Corporation +bridgestone + +// broadway : 2014-12-22 Celebrate Broadway, Inc. +broadway + +// broker : 2014-12-11 IG Group Holdings PLC +broker + +// brother : 2015-01-29 Brother Industries, Ltd. +brother + +// brussels : 2014-02-06 DNS.be vzw +brussels + +// budapest : 2013-11-21 Top Level Domain Holdings Limited +budapest + +// build : 2013-11-07 Plan Bee LLC +build + +// builders : 2013-11-07 Atomic Madison, LLC +builders + +// business : 2013-11-07 Spring Cross, LLC +business + +// buy : 2014-12-18 Amazon EU S.à r.l. +buy + +// buzz : 2013-10-02 DOTSTRATEGY CO. +buzz + +// bzh : 2014-02-27 Association www.bzh +bzh + +// cab : 2013-10-24 Half Sunset, LLC +cab + +// cafe : 2015-02-11 Pioneer Canyon, LLC +cafe + +// cal : 2014-07-24 Charleston Road Registry Inc. +cal + +// call : 2014-12-18 Amazon EU S.à r.l. +call + +// camera : 2013-08-27 Atomic Maple, LLC +camera + +// camp : 2013-11-07 Delta Dynamite, LLC +camp + +// cancerresearch : 2014-05-15 Australian Cancer Research Foundation +cancerresearch + +// canon : 2014-09-12 Canon Inc. +canon + +// capetown : 2014-03-24 ZA Central Registry NPC trading as ZA Central Registry +capetown + +// capital : 2014-03-06 Delta Mill, LLC +capital + +// car : 2015-01-22 Charleston Road Registry Inc. +car + +// caravan : 2013-12-12 Caravan International, Inc. +caravan + +// cards : 2013-12-05 Foggy Hollow, LLC +cards + +// care : 2014-03-06 Goose Cross +care + +// career : 2013-10-09 dotCareer LLC +career + +// careers : 2013-10-02 Wild Corner, LLC +careers + +// cars : 2014-11-13 Uniregistry, Corp. +cars + +// cartier : 2014-06-23 Richemont DNS Inc. +cartier + +// casa : 2013-11-21 Top Level Domain Holdings Limited +casa + +// cash : 2014-03-06 Delta Lake, LLC +cash + +// casino : 2014-12-18 Binky Sky, LLC +casino + +// catering : 2013-12-05 New Falls. LLC +catering + +// cba : 2014-06-26 COMMONWEALTH BANK OF AUSTRALIA +cba + +// cbn : 2014-08-22 The Christian Broadcasting Network, Inc. +cbn + +// ceb : 2015-04-09 The Corporate Executive Board Company +ceb + +// center : 2013-11-07 Tin Mill, LLC +center + +// ceo : 2013-11-07 CEOTLD Pty Ltd +ceo + +// cern : 2014-06-05 European Organization for Nuclear Research ("CERN") +cern + +// cfa : 2014-08-28 CFA Institute +cfa + +// cfd : 2014-12-11 IG Group Holdings PLC +cfd + +// chanel : 2015-04-09 Chanel International B.V. +chanel + +// channel : 2014-05-08 Charleston Road Registry Inc. +channel + +// chase : 2015-04-30 JPMorgan Chase & Co. +chase + +// chat : 2014-12-04 Sand Fields, LLC +chat + +// cheap : 2013-11-14 Sand Cover, LLC +cheap + +// chloe : 2014-10-16 Richemont DNS Inc. +chloe + +// christmas : 2013-11-21 Uniregistry, Corp. +christmas + +// chrome : 2014-07-24 Charleston Road Registry Inc. +chrome + +// church : 2014-02-06 Holly Fields, LLC +church + +// cipriani : 2015-02-19 Hotel Cipriani Srl +cipriani + +// circle : 2014-12-18 Amazon EU S.à r.l. +circle + +// cisco : 2014-12-22 Cisco Technology, Inc. +cisco + +// citic : 2014-01-09 CITIC Group Corporation +citic + +// city : 2014-05-29 Snow Sky, LLC +city + +// cityeats : 2014-12-11 Lifestyle Domain Holdings, Inc. +cityeats + +// claims : 2014-03-20 Black Corner, LLC +claims + +// cleaning : 2013-12-05 Fox Shadow, LLC +cleaning + +// click : 2014-06-05 Uniregistry, Corp. +click + +// clinic : 2014-03-20 Goose Park, LLC +clinic + +// clothing : 2013-08-27 Steel Lake, LLC +clothing + +// cloud : 2015-04-16 ARUBA S.p.A. +cloud + +// club : 2013-11-08 .CLUB DOMAINS, LLC +club + +// coach : 2014-10-09 Koko Island, LLC +coach + +// codes : 2013-10-31 Puff Willow, LLC +codes + +// coffee : 2013-10-17 Trixy Cover, LLC +coffee + +// college : 2014-01-16 XYZ.COM LLC +college + +// cologne : 2014-02-05 NetCologne Gesellschaft für Telekommunikation mbH +cologne + +// commbank : 2014-06-26 COMMONWEALTH BANK OF AUSTRALIA +commbank + +// community : 2013-12-05 Fox Orchard, LLC +community + +// company : 2013-11-07 Silver Avenue, LLC +company + +// computer : 2013-10-24 Pine Mill, LLC +computer + +// comsec : 2015-01-08 VeriSign, Inc. +comsec + +// condos : 2013-12-05 Pine House, LLC +condos + +// construction : 2013-09-16 Fox Dynamite, LLC +construction + +// consulting : 2013-12-05 +consulting + +// contact : 2015-01-08 Top Level Spectrum, Inc. +contact + +// contractors : 2013-09-10 Magic Woods, LLC +contractors + +// cooking : 2013-11-21 Top Level Domain Holdings Limited +cooking + +// cool : 2013-11-14 Koko Lake, LLC +cool + +// corsica : 2014-09-25 Collectivité Territoriale de Corse +corsica + +// country : 2013-12-19 Top Level Domain Holdings Limited +country + +// coupon : 2015-02-26 Amazon EU S.à r.l. +coupon + +// coupons : 2015-03-26 Black Island, LLC +coupons + +// courses : 2014-12-04 OPEN UNIVERSITIES AUSTRALIA PTY LTD +courses + +// credit : 2014-03-20 Snow Shadow, LLC +credit + +// creditcard : 2014-03-20 Binky Frostbite, LLC +creditcard + +// creditunion : 2015-01-22 CUNA Performance Resources, LLC +creditunion + +// cricket : 2014-10-09 dot Cricket Limited +cricket + +// crown : 2014-10-24 Crown Equipment Corporation +crown + +// crs : 2014-04-03 Federated Co-operatives Limited +crs + +// cruises : 2013-12-05 Spring Way, LLC +cruises + +// csc : 2014-09-25 Alliance-One Services, Inc. +csc + +// cuisinella : 2014-04-03 SALM S.A.S. +cuisinella + +// cymru : 2014-05-08 Nominet UK +cymru + +// cyou : 2015-01-22 Beijing Gamease Age Digital Technology Co., Ltd. +cyou + +// dabur : 2014-02-06 Dabur India Limited +dabur + +// dad : 2014-01-23 Charleston Road Registry Inc. +dad + +// dance : 2013-10-24 United TLD Holdco Ltd. +dance + +// date : 2014-11-20 dot Date Limited +date + +// dating : 2013-12-05 Pine Fest, LLC +dating + +// datsun : 2014-03-27 NISSAN MOTOR CO., LTD. +datsun + +// day : 2014-01-30 Charleston Road Registry Inc. +day + +// dclk : 2014-11-20 Charleston Road Registry Inc. +dclk + +// dealer : 2014-12-22 Dealer Dot Com, Inc. +dealer + +// deals : 2014-05-22 Sand Sunset, LLC +deals + +// degree : 2014-03-06 +degree + +// delivery : 2014-09-11 Steel Station, LLC +delivery + +// dell : 2014-10-24 Dell Inc. +dell + +// delta : 2015-02-19 Delta Air Lines, Inc. +delta + +// democrat : 2013-10-24 United TLD Holdco Ltd. +democrat + +// dental : 2014-03-20 Tin Birch, LLC +dental + +// dentist : 2014-03-20 +dentist + +// desi : 2013-11-14 Desi Networks LLC +desi + +// design : 2014-11-07 Top Level Design, LLC +design + +// dev : 2014-10-16 Charleston Road Registry Inc. +dev + +// diamonds : 2013-09-22 John Edge, LLC +diamonds + +// diet : 2014-06-26 Uniregistry, Corp. +diet + +// digital : 2014-03-06 Dash Park, LLC +digital + +// direct : 2014-04-10 Half Trail, LLC +direct + +// directory : 2013-09-20 Extra Madison, LLC +directory + +// discount : 2014-03-06 Holly Hill, LLC +discount + +// dnp : 2013-12-13 Dai Nippon Printing Co., Ltd. +dnp + +// docs : 2014-10-16 Charleston Road Registry Inc. +docs + +// dog : 2014-12-04 Koko Mill, LLC +dog + +// doha : 2014-09-18 Communications Regulatory Authority (CRA) +doha + +// domains : 2013-10-17 Sugar Cross, LLC +domains + +// doosan : 2014-04-03 Doosan Corporation +doosan + +// download : 2014-11-20 dot Support Limited +download + +// drive : 2015-03-05 Charleston Road Registry Inc. +drive + +// dstv : 2015-03-12 MultiChoice (Proprietary) Limited +dstv + +// dubai : 2015-01-01 Dubai Smart Government Department +dubai + +// durban : 2014-03-24 ZA Central Registry NPC trading as ZA Central Registry +durban + +// dvag : 2014-06-23 Deutsche Vermögensberatung Aktiengesellschaft DVAG +dvag + +// earth : 2014-12-04 Interlink Co., Ltd. +earth + +// eat : 2014-01-23 Charleston Road Registry Inc. +eat + +// edeka : 2014-12-18 EDEKA Verband kaufmännischer Genossenschaften e.V. +edeka + +// education : 2013-11-07 Brice Way, LLC +education + +// email : 2013-10-31 Spring Madison, LLC +email + +// emerck : 2014-04-03 Merck KGaA +emerck + +// energy : 2014-09-11 Binky Birch, LLC +energy + +// engineer : 2014-03-06 United TLD Holdco Ltd. +engineer + +// engineering : 2014-03-06 Romeo Canyon +engineering + +// enterprises : 2013-09-20 Snow Oaks, LLC +enterprises + +// epson : 2014-12-04 Seiko Epson Corporation +epson + +// equipment : 2013-08-27 Corn Station, LLC +equipment + +// erni : 2014-04-03 ERNI Group Holding AG +erni + +// esq : 2014-05-08 Charleston Road Registry Inc. +esq + +// estate : 2013-08-27 Trixy Park, LLC +estate + +// eurovision : 2014-04-24 European Broadcasting Union (EBU) +eurovision + +// eus : 2013-12-12 Puntueus Fundazioa +eus + +// events : 2013-12-05 Pioneer Maple, LLC +events + +// everbank : 2014-05-15 EverBank +everbank + +// exchange : 2014-03-06 Spring Falls, LLC +exchange + +// expert : 2013-11-21 Magic Pass, LLC +expert + +// exposed : 2013-12-05 Victor Beach, LLC +exposed + +// express : 2015-02-11 Sea Sunset, LLC +express + +// fage : 2014-12-18 Fage International S.A. +fage + +// fail : 2014-03-06 Atomic Pipe, LLC +fail + +// fairwinds : 2014-11-13 FairWinds Partners, LLC +fairwinds + +// faith : 2014-11-20 dot Faith Limited +faith + +// family : 2015-04-02 Bitter Galley, LLC +family + +// fan : 2014-03-06 +fan + +// fans : 2014-11-07 Asiamix Digital Limited +fans + +// farm : 2013-11-07 Just Maple, LLC +farm + +// fashion : 2014-07-03 Top Level Domain Holdings Limited +fashion + +// fast : 2014-12-18 Amazon EU S.à r.l. +fast + +// feedback : 2013-12-19 Top Level Spectrum, Inc. +feedback + +// ferrero : 2014-12-18 Ferrero Trading Lux S.A. +ferrero + +// film : 2015-01-08 Motion Picture Domain Registry Pty Ltd +film + +// final : 2014-10-16 Núcleo de Informação e Coordenação do Ponto BR - NIC.br +final + +// finance : 2014-03-20 Cotton Cypress, LLC +finance + +// financial : 2014-03-06 Just Cover, LLC +financial + +// firestone : 2014-12-18 Bridgestone Corporation +firestone + +// firmdale : 2014-03-27 Firmdale Holdings Limited +firmdale + +// fish : 2013-12-12 Fox Woods, LLC +fish + +// fishing : 2013-11-21 Top Level Domain Holdings Limited +fishing + +// fit : 2014-11-07 Top Level Domain Holdings Limited +fit + +// fitness : 2014-03-06 Brice Orchard, LLC +fitness + +// flickr : 2015-04-02 Yahoo! Domain Services Inc. +flickr + +// flights : 2013-12-05 Fox Station, LLC +flights + +// florist : 2013-11-07 Half Cypress, LLC +florist + +// flowers : 2014-10-09 Uniregistry, Corp. +flowers + +// flsmidth : 2014-07-24 FLSmidth A/S +flsmidth + +// fly : 2014-05-08 Charleston Road Registry Inc. +fly + +// foo : 2014-01-23 Charleston Road Registry Inc. +foo + +// football : 2014-12-18 Foggy Farms, LLC +football + +// ford : 2014-11-13 Ford Motor Company +ford + +// forex : 2014-12-11 IG Group Holdings PLC +forex + +// forsale : 2014-05-22 +forsale + +// forum : 2015-04-02 Fegistry, LLC +forum + +// foundation : 2013-12-05 John Dale, LLC +foundation + +// frl : 2014-05-15 FRLregistry B.V. +frl + +// frogans : 2013-12-19 OP3FT +frogans + +// frontier : 2015-02-05 Frontier Communications Corporation +frontier + +// fund : 2014-03-20 John Castle, LLC +fund + +// furniture : 2014-03-20 Lone Fields, LLC +furniture + +// futbol : 2013-09-20 +futbol + +// fyi : 2015-04-02 Silver Tigers, LLC +fyi + +// gal : 2013-11-07 Asociación puntoGAL +gal + +// gallery : 2013-09-13 Sugar House, LLC +gallery + +// gallup : 2015-02-19 Gallup, Inc. +gallup + +// garden : 2014-06-26 Top Level Domain Holdings Limited +garden + +// gbiz : 2014-07-17 Charleston Road Registry Inc. +gbiz + +// gdn : 2014-07-31 Joint Stock Company "Navigation-information systems" +gdn + +// gea : 2014-12-04 GEA Group Aktiengesellschaft +gea + +// gent : 2014-01-23 COMBELL GROUP NV/SA +gent + +// genting : 2015-03-12 Resorts World Inc Pte. Ltd. +genting + +// ggee : 2014-01-09 GMO Internet, Inc. +ggee + +// gift : 2013-10-17 Uniregistry, Corp. +gift + +// gifts : 2014-07-03 Goose Sky, LLC +gifts + +// gives : 2014-03-06 United TLD Holdco Ltd. +gives + +// giving : 2014-11-13 Giving Limited +giving + +// glass : 2013-11-07 Black Cover, LLC +glass + +// gle : 2014-07-24 Charleston Road Registry Inc. +gle + +// global : 2014-04-17 Dot GLOBAL AS +global + +// globo : 2013-12-19 Globo Comunicação e Participações S.A +globo + +// gmail : 2014-05-01 Charleston Road Registry Inc. +gmail + +// gmo : 2014-01-09 GMO Internet, Inc. +gmo + +// gmx : 2014-04-24 1&1 Mail & Media GmbH +gmx + +// gold : 2015-01-22 June Edge, LLC +gold + +// goldpoint : 2014-11-20 YODOBASHI CAMERA CO.,LTD. +goldpoint + +// golf : 2014-12-18 Lone falls, LLC +golf + +// goo : 2014-12-18 NTT Resonant Inc. +goo + +// goog : 2014-11-20 Charleston Road Registry Inc. +goog + +// google : 2014-07-24 Charleston Road Registry Inc. +google + +// gop : 2014-01-16 Republican State Leadership Committee, Inc. +gop + +// got : 2014-12-18 Amazon EU S.à r.l. +got + +// gotv : 2015-03-12 MultiChoice (Proprietary) Limited +gotv + +// graphics : 2013-09-13 Over Madison, LLC +graphics + +// gratis : 2014-03-20 Pioneer Tigers, LLC +gratis + +// green : 2014-05-08 Afilias Limited +green + +// gripe : 2014-03-06 Corn Sunset, LLC +gripe + +// group : 2014-08-15 Romeo Town, LLC +group + +// gucci : 2014-11-13 Guccio Gucci S.p.a. +gucci + +// guge : 2014-08-28 Charleston Road Registry Inc. +guge + +// guide : 2013-09-13 Snow Moon, LLC +guide + +// guitars : 2013-11-14 Uniregistry, Corp. +guitars + +// guru : 2013-08-27 Pioneer Cypress, LLC +guru + +// hamburg : 2014-02-20 Hamburg Top-Level-Domain GmbH +hamburg + +// hangout : 2014-11-13 Charleston Road Registry Inc. +hangout + +// haus : 2013-12-05 +haus + +// hdfcbank : 2015-02-12 HDFC Bank Limited +hdfcbank + +// health : 2015-02-11 DotHealth, LLC +health + +// healthcare : 2014-06-12 Silver Glen, LLC +healthcare + +// help : 2014-06-26 Uniregistry, Corp. +help + +// helsinki : 2015-02-05 City of Helsinki +helsinki + +// here : 2014-02-06 Charleston Road Registry Inc. +here + +// hermes : 2014-07-10 HERMES INTERNATIONAL +hermes + +// hiphop : 2014-03-06 Uniregistry, Corp. +hiphop + +// hitachi : 2014-10-31 Hitachi, Ltd. +hitachi + +// hiv : 2014-03-13 dotHIV gemeinnuetziger e.V. +hiv + +// hockey : 2015-03-19 Half Willow, LLC +hockey + +// holdings : 2013-08-27 John Madison, LLC +holdings + +// holiday : 2013-11-07 Goose Woods, LLC +holiday + +// homedepot : 2015-04-02 Homer TLC, Inc. +homedepot + +// homes : 2014-01-09 DERHomes, LLC +homes + +// honda : 2014-12-18 Honda Motor Co., Ltd. +honda + +// horse : 2013-11-21 Top Level Domain Holdings Limited +horse + +// host : 2014-04-17 DotHost Inc. +host + +// hosting : 2014-05-29 Uniregistry, Corp. +hosting + +// hoteles : 2015-03-05 Travel Reservations SRL +hoteles + +// hotmail : 2014-12-18 Microsoft Corporation +hotmail + +// house : 2013-11-07 Sugar Park, LLC +house + +// how : 2014-01-23 Charleston Road Registry Inc. +how + +// hsbc : 2014-10-24 HSBC Holdings PLC +hsbc + +// htc : 2015-04-02 HTC corporation +htc + +// ibm : 2014-07-31 International Business Machines Corporation +ibm + +// icbc : 2015-02-19 Industrial and Commercial Bank of China Limited +icbc + +// ice : 2014-10-30 IntercontinentalExchange, Inc. +ice + +// icu : 2015-01-08 One.com A/S +icu + +// ifm : 2014-01-30 ifm electronic gmbh +ifm + +// iinet : 2014-07-03 Connect West Pty. Ltd. +iinet + +// immo : 2014-07-10 Auburn Bloom, LLC +immo + +// immobilien : 2013-11-07 United TLD Holdco Ltd. +immobilien + +// industries : 2013-12-05 Outer House, LLC +industries + +// infiniti : 2014-03-27 NISSAN MOTOR CO., LTD. +infiniti + +// ing : 2014-01-23 Charleston Road Registry Inc. +ing + +// ink : 2013-12-05 Top Level Design, LLC +ink + +// institute : 2013-11-07 Outer Maple, LLC +institute + +// insurance : 2015-02-19 fTLD Registry Services LLC +insurance + +// insure : 2014-03-20 Pioneer Willow, LLC +insure + +// international : 2013-11-07 Wild Way, LLC +international + +// investments : 2014-03-20 Holly Glen, LLC +investments + +// ipiranga : 2014-08-28 Ipiranga Produtos de Petroleo S.A. +ipiranga + +// irish : 2014-08-07 Dot-Irish LLC +irish + +// iselect : 2015-02-11 iSelect Ltd +iselect + +// ist : 2014-08-28 Istanbul Metropolitan Municipality +ist + +// istanbul : 2014-08-28 Istanbul Metropolitan Municipality +istanbul + +// itau : 2014-10-02 Itau Unibanco Holding S.A. +itau + +// iwc : 2014-06-23 Richemont DNS Inc. +iwc + +// jaguar : 2014-11-13 Jaguar Land Rover Ltd +jaguar + +// java : 2014-06-19 Oracle Corporation +java + +// jcb : 2014-11-20 JCB Co., Ltd. +jcb + +// jcp : 2015-04-23 JCP Media, Inc. +jcp + +// jetzt : 2014-01-09 New TLD Company AB +jetzt + +// jewelry : 2015-03-05 Wild Bloom, LLC +jewelry + +// jio : 2015-04-02 Affinity Names, Inc. +jio + +// jlc : 2014-12-04 Richemont DNS Inc. +jlc + +// jll : 2015-04-02 Jones Lang LaSalle Incorporated +jll + +// jmp : 2015-03-26 Matrix IP LLC +jmp + +// joburg : 2014-03-24 ZA Central Registry NPC trading as ZA Central Registry +joburg + +// jot : 2014-12-18 Amazon EU S.à r.l. +jot + +// joy : 2014-12-18 Amazon EU S.à r.l. +joy + +// jpmorgan : 2015-04-30 JPMorgan Chase & Co. +jpmorgan + +// jprs : 2014-09-18 Japan Registry Services Co., Ltd. +jprs + +// juegos : 2014-03-20 Uniregistry, Corp. +juegos + +// kaufen : 2013-11-07 United TLD Holdco Ltd. +kaufen + +// kddi : 2014-09-12 KDDI CORPORATION +kddi + +// kerryhotels : 2015-04-30 Kerry Trading Co. Limited +kerryhotels + +// kerrylogistics : 2015-04-09 Kerry Trading Co. Limited +kerrylogistics + +// kerryproperties : 2015-04-09 Kerry Trading Co. Limited +kerryproperties + +// kfh : 2014-12-04 Kuwait Finance House +kfh + +// kim : 2013-09-23 Afilias Limited +kim + +// kinder : 2014-11-07 Ferrero Trading Lux S.A. +kinder + +// kitchen : 2013-09-20 Just Goodbye, LLC +kitchen + +// kiwi : 2013-09-20 DOT KIWI LIMITED +kiwi + +// koeln : 2014-01-09 NetCologne Gesellschaft für Telekommunikation mbH +koeln + +// komatsu : 2015-01-08 Komatsu Ltd. +komatsu + +// kpmg : 2015-04-23 KPMG International Cooperative (KPMG International Genossenschaft) +kpmg + +// kpn : 2015-01-08 Koninklijke KPN N.V. +kpn + +// krd : 2013-12-05 KRG Department of Information Technology +krd + +// kred : 2013-12-19 KredTLD Pty Ltd +kred + +// kuokgroup : 2015-04-09 Kerry Trading Co. Limited +kuokgroup + +// kyknet : 2015-03-05 Electronic Media Network (Pty) Ltd +kyknet + +// kyoto : 2014-11-07 Academic Institution: Kyoto Jyoho Gakuen +kyoto + +// lacaixa : 2014-01-09 CAIXA D'ESTALVIS I PENSIONS DE BARCELONA +lacaixa + +// lancaster : 2015-02-12 LANCASTER +lancaster + +// land : 2013-09-10 Pine Moon, LLC +land + +// landrover : 2014-11-13 Jaguar Land Rover Ltd +landrover + +// lasalle : 2015-04-02 Jones Lang LaSalle Incorporated +lasalle + +// lat : 2014-10-16 ECOM-LAC Federaciòn de Latinoamèrica y el Caribe para Internet y el Comercio Electrònico +lat + +// latrobe : 2014-06-16 La Trobe University +latrobe + +// law : 2015-01-22 Minds + Machines Group Limited +law + +// lawyer : 2014-03-20 +lawyer + +// lds : 2014-03-20 IRI Domain Management, LLC ("Applicant") +lds + +// lease : 2014-03-06 Victor Trail, LLC +lease + +// leclerc : 2014-08-07 A.C.D. LEC Association des Centres Distributeurs Edouard Leclerc +leclerc + +// legal : 2014-10-16 Blue Falls, LLC +legal + +// lexus : 2015-04-23 TOYOTA MOTOR CORPORATION +lexus + +// lgbt : 2014-05-08 Afilias Limited +lgbt + +// liaison : 2014-10-02 Liaison Technologies, Incorporated +liaison + +// lidl : 2014-09-18 Schwarz Domains und Services GmbH & Co. KG +lidl + +// life : 2014-02-06 Trixy Oaks, LLC +life + +// lifeinsurance : 2015-01-15 American Council of Life Insurers +lifeinsurance + +// lifestyle : 2014-12-11 Lifestyle Domain Holdings, Inc. +lifestyle + +// lighting : 2013-08-27 John McCook, LLC +lighting + +// like : 2014-12-18 Amazon EU S.à r.l. +like + +// limited : 2014-03-06 Big Fest, LLC +limited + +// limo : 2013-10-17 Hidden Frostbite, LLC +limo + +// lincoln : 2014-11-13 Ford Motor Company +lincoln + +// linde : 2014-12-04 Linde Aktiengesellschaft +linde + +// link : 2013-11-14 Uniregistry, Corp. +link + +// live : 2014-12-04 Half Woods, LLC +live + +// lixil : 2015-03-19 LIXIL Group Corporation +lixil + +// loan : 2014-11-20 dot Loan Limited +loan + +// loans : 2014-03-20 June Woods, LLC +loans + +// lol : 2015-01-30 Uniregistry, Corp. +lol + +// london : 2013-11-14 Dot London Domains Limited +london + +// lotte : 2014-11-07 Lotte Holdings Co., Ltd. +lotte + +// lotto : 2014-04-10 Afilias Limited +lotto + +// love : 2014-12-22 Merchant Law Group LLP +love + +// ltd : 2014-09-25 Over Corner, LLC +ltd + +// ltda : 2014-04-17 DOMAIN ROBOT SERVICOS DE HOSPEDAGEM NA INTERNET LTDA +ltda + +// lupin : 2014-11-07 LUPIN LIMITED +lupin + +// luxe : 2014-01-09 Top Level Domain Holdings Limited +luxe + +// luxury : 2013-10-17 Luxury Partners, LLC +luxury + +// madrid : 2014-05-01 Comunidad de Madrid +madrid + +// maif : 2014-10-02 Mutuelle Assurance Instituteur France (MAIF) +maif + +// maison : 2013-12-05 Victor Frostbite, LLC +maison + +// makeup : 2015-01-15 L'Oréal +makeup + +// man : 2014-12-04 MAN SE +man + +// management : 2013-11-07 John Goodbye, LLC +management + +// mango : 2013-10-24 PUNTO FA S.L. +mango + +// market : 2014-03-06 +market + +// marketing : 2013-11-07 Fern Pass, LLC +marketing + +// markets : 2014-12-11 IG Group Holdings PLC +markets + +// marriott : 2014-10-09 Marriott Worldwide Corporation +marriott + +// mba : 2015-04-02 Lone Hollow, LLC +mba + +// media : 2014-03-06 Grand Glen, LLC +media + +// meet : 2014-01-16 +meet + +// melbourne : 2014-05-29 The Crown in right of the State of Victoria, represented by its Department of State Development, Business and Innovation +melbourne + +// meme : 2014-01-30 Charleston Road Registry Inc. +meme + +// memorial : 2014-10-16 Dog Beach, LLC +memorial + +// men : 2015-02-26 Exclusive Registry Limited +men + +// menu : 2013-09-11 Wedding TLD2, LLC +menu + +// meo : 2014-11-07 PT Comunicacoes S.A. +meo + +// miami : 2013-12-19 Top Level Domain Holdings Limited +miami + +// microsoft : 2014-12-18 Microsoft Corporation +microsoft + +// mini : 2014-01-09 Bayerische Motoren Werke Aktiengesellschaft +mini + +// mls : 2015-04-23 The Canadian Real Estate Association +mls + +// mma : 2014-11-07 MMA IARD +mma + +// mnet : 2015-03-05 Electronic Media Network (Pty) Ltd +mnet + +// mobily : 2014-12-18 GreenTech Consultancy Company W.L.L. +mobily + +// moda : 2013-11-07 United TLD Holdco Ltd. +moda + +// moe : 2013-11-13 Interlink Co., Ltd. +moe + +// moi : 2014-12-18 Amazon EU S.à r.l. +moi + +// mom : 2015-04-16 Uniregistry, Corp. +mom + +// monash : 2013-09-30 Monash University +monash + +// money : 2014-10-16 Outer McCook, LLC +money + +// montblanc : 2014-06-23 Richemont DNS Inc. +montblanc + +// mormon : 2013-12-05 IRI Domain Management, LLC ("Applicant") +mormon + +// mortgage : 2014-03-20 +mortgage + +// moscow : 2013-12-19 Foundation for Assistance for Internet Technologies and Infrastructure Development (FAITID) +moscow + +// motorcycles : 2014-01-09 DERMotorcycles, LLC +motorcycles + +// mov : 2014-01-30 Charleston Road Registry Inc. +mov + +// movie : 2015-02-05 New Frostbite, LLC +movie + +// movistar : 2014-10-16 Telefónica S.A. +movistar + +// mtn : 2014-12-04 MTN Dubai Limited +mtn + +// mtpc : 2014-11-20 Mitsubishi Tanabe Pharma Corporation +mtpc + +// mtr : 2015-03-12 MTR Corporation Limited +mtr + +// multichoice : 2015-03-12 MultiChoice (Proprietary) Limited +multichoice + +// mutual : 2015-04-02 Northwestern Mutual MU TLD Registry, LLC +mutual + +// mzansimagic : 2015-03-05 Electronic Media Network (Pty) Ltd +mzansimagic + +// nadex : 2014-12-11 IG Group Holdings PLC +nadex + +// nagoya : 2013-10-24 GMO Registry, Inc. +nagoya + +// naspers : 2015-02-12 Intelprop (Proprietary) Limited +naspers + +// natura : 2015-03-12 NATURA COSMÉTICOS S.A. +natura + +// navy : 2014-03-06 United TLD Holdco Ltd. +navy + +// nec : 2015-01-08 NEC Corporation +nec + +// netbank : 2014-06-26 COMMONWEALTH BANK OF AUSTRALIA +netbank + +// network : 2013-11-14 Trixy Manor, LLC +network + +// neustar : 2013-12-05 NeuStar, Inc. +neustar + +// new : 2014-01-30 Charleston Road Registry Inc. +new + +// news : 2014-12-18 +news + +// nexus : 2014-07-24 Charleston Road Registry Inc. +nexus + +// ngo : 2014-03-06 Public Interest Registry +ngo + +// nhk : 2014-02-13 Japan Broadcasting Corporation (NHK) +nhk + +// nico : 2014-12-04 DWANGO Co., Ltd. +nico + +// ninja : 2013-11-07 United TLD Holdco Ltd. +ninja + +// nissan : 2014-03-27 NISSAN MOTOR CO., LTD. +nissan + +// nokia : 2015-01-08 Nokia Corporation +nokia + +// norton : 2014-12-04 Symantec Corporation +norton + +// nowruz : 2014-09-04 Asia Green IT System Bilgisayar San. ve Tic. Ltd. Sti. +nowruz + +// nra : 2014-05-22 NRA Holdings Company, INC. +nra + +// nrw : 2013-11-21 Minds + Machines GmbH +nrw + +// ntt : 2014-10-31 NIPPON TELEGRAPH AND TELEPHONE CORPORATION +ntt + +// nyc : 2014-01-23 The City of New York by and through the New York City Department of Information Technology & Telecommunications +nyc + +// obi : 2014-09-25 OBI Group Holding SE & Co. KGaA +obi + +// observer : 2015-04-30 Guardian News and Media Limited +observer + +// office : 2015-03-12 Microsoft Corporation +office + +// okinawa : 2013-12-05 BusinessRalliart Inc. +okinawa + +// omega : 2015-01-08 The Swatch Group Ltd +omega + +// one : 2014-11-07 One.com A/S +one + +// ong : 2014-03-06 Public Interest Registry +ong + +// onl : 2013-09-16 I-Registry Ltd. +onl + +// online : 2015-01-15 DotOnline Inc. +online + +// ooo : 2014-01-09 INFIBEAM INCORPORATION LIMITED +ooo + +// oracle : 2014-06-19 Oracle Corporation +oracle + +// orange : 2015-03-12 Orange Brand Services Limited +orange + +// organic : 2014-03-27 Afilias Limited +organic + +// orientexpress : 2015-02-05 Belmond Ltd. +orientexpress + +// osaka : 2014-09-04 Interlink Co., Ltd. +osaka + +// otsuka : 2013-10-11 Otsuka Holdings Co., Ltd. +otsuka + +// ovh : 2014-01-16 OVH SAS +ovh + +// page : 2014-12-04 Charleston Road Registry Inc. +page + +// pamperedchef : 2015-02-05 The Pampered Chef, Ltd. +pamperedchef + +// panerai : 2014-11-07 Richemont DNS Inc. +panerai + +// paris : 2014-01-30 City of Paris +paris + +// pars : 2014-09-04 Asia Green IT System Bilgisayar San. ve Tic. Ltd. Sti. +pars + +// partners : 2013-12-05 Magic Glen, LLC +partners + +// parts : 2013-12-05 Sea Goodbye, LLC +parts + +// party : 2014-09-11 Blue Sky Registry Limited +party + +// passagens : 2015-03-05 Travel Reservations SRL +passagens + +// payu : 2015-02-12 MIH PayU B.V. +payu + +// pharmacy : 2014-06-19 National Association of Boards of Pharmacy +pharmacy + +// philips : 2014-11-07 Koninklijke Philips N.V. +philips + +// photo : 2013-11-14 Uniregistry, Corp. +photo + +// photography : 2013-09-20 Sugar Glen, LLC +photography + +// photos : 2013-10-17 Sea Corner, LLC +photos + +// physio : 2014-05-01 PhysBiz Pty Ltd +physio + +// piaget : 2014-10-16 Richemont DNS Inc. +piaget + +// pics : 2013-11-14 Uniregistry, Corp. +pics + +// pictet : 2014-06-26 Pictet Europe S.A. +pictet + +// pictures : 2014-03-06 Foggy Sky, LLC +pictures + +// pid : 2015-01-08 Top Level Spectrum, Inc. +pid + +// pin : 2014-12-18 Amazon EU S.à r.l. +pin + +// pink : 2013-10-01 Afilias Limited +pink + +// pizza : 2014-06-26 Foggy Moon, LLC +pizza + +// place : 2014-04-24 Snow Galley, LLC +place + +// play : 2015-03-05 Charleston Road Registry Inc. +play + +// plumbing : 2013-09-10 Spring Tigers, LLC +plumbing + +// plus : 2015-02-05 Sugar Mill, LLC +plus + +// pohl : 2014-06-23 Deutsche Vermögensberatung Aktiengesellschaft DVAG +pohl + +// poker : 2014-07-03 Afilias Domains No. 5 Limited +poker + +// porn : 2014-10-16 ICM Registry PN LLC +porn + +// praxi : 2013-12-05 Praxi S.p.A. +praxi + +// press : 2014-04-03 DotPress Inc. +press + +// prod : 2014-01-23 Charleston Road Registry Inc. +prod + +// productions : 2013-12-05 Magic Birch, LLC +productions + +// prof : 2014-07-24 Charleston Road Registry Inc. +prof + +// promo : 2014-12-18 Play.PROMO Oy +promo + +// properties : 2013-12-05 Big Pass, LLC +properties + +// property : 2014-05-22 Uniregistry, Corp. +property + +// protection : 2015-04-23 Symantec Corporation +protection + +// pub : 2013-12-12 United TLD Holdco Ltd. +pub + +// qpon : 2013-11-14 dotCOOL, Inc. +qpon + +// quebec : 2013-12-19 PointQuébec Inc +quebec + +// quest : 2015-03-26 Quest ION Limited +quest + +// racing : 2014-12-04 Premier Registry Limited +racing + +// read : 2014-12-18 Amazon EU S.à r.l. +read + +// realtor : 2014-05-29 Real Estate Domains LLC +realtor + +// realty : 2015-03-19 Fegistry, LLC +realty + +// recipes : 2013-10-17 Grand Island, LLC +recipes + +// red : 2013-11-07 Afilias Limited +red + +// redstone : 2014-10-31 Redstone Haute Couture Co., Ltd. +redstone + +// redumbrella : 2015-03-26 Travelers TLD, LLC +redumbrella + +// rehab : 2014-03-06 United TLD Holdco Ltd. +rehab + +// reise : 2014-03-13 dotreise GmbH +reise + +// reisen : 2014-03-06 New Cypress, LLC +reisen + +// reit : 2014-09-04 National Association of Real Estate Investment Trusts, Inc. +reit + +// reliance : 2015-04-02 Reliance Industries Limited +reliance + +// ren : 2013-12-12 Beijing Qianxiang Wangjing Technology Development Co., Ltd. +ren + +// rent : 2014-12-04 DERRent, LLC +rent + +// rentals : 2013-12-05 Big Hollow,LLC +rentals + +// repair : 2013-11-07 Lone Sunset, LLC +repair + +// report : 2013-12-05 Binky Glen, LLC +report + +// republican : 2014-03-20 United TLD Holdco Ltd. +republican + +// rest : 2013-12-19 Punto 2012 Sociedad Anonima Promotora de Inversion de Capital Variable +rest + +// restaurant : 2014-07-03 Snow Avenue, LLC +restaurant + +// review : 2014-11-20 dot Review Limited +review + +// reviews : 2013-09-13 +reviews + +// rich : 2013-11-21 I-Registry Ltd. +rich + +// ricoh : 2014-11-20 Ricoh Company, Ltd. +ricoh + +// ril : 2015-04-02 Reliance Industries Limited +ril + +// rio : 2014-02-27 Empresa Municipal de Informática SA - IPLANRIO +rio + +// rip : 2014-07-10 United TLD Holdco Ltd. +rip + +// rocher : 2014-12-18 Ferrero Trading Lux S.A. +rocher + +// rocks : 2013-11-14 +rocks + +// rodeo : 2013-12-19 Top Level Domain Holdings Limited +rodeo + +// room : 2014-12-18 Amazon EU S.à r.l. +room + +// rsvp : 2014-05-08 Charleston Road Registry Inc. +rsvp + +// ruhr : 2013-10-02 regiodot GmbH & Co. KG +ruhr + +// run : 2015-03-19 Snow Park, LLC +run + +// rwe : 2015-04-02 RWE AG +rwe + +// ryukyu : 2014-01-09 BusinessRalliart Inc. +ryukyu + +// saarland : 2013-12-12 dotSaarland GmbH +saarland + +// safe : 2014-12-18 Amazon EU S.à r.l. +safe + +// safety : 2015-01-08 Safety Registry Services, LLC. +safety + +// sakura : 2014-12-18 SAKURA Internet Inc. +sakura + +// sale : 2014-10-16 +sale + +// salon : 2014-12-11 Outer Orchard, LLC +salon + +// samsung : 2014-04-03 SAMSUNG SDS CO., LTD +samsung + +// sandvik : 2014-11-13 Sandvik AB +sandvik + +// sandvikcoromant : 2014-11-07 Sandvik AB +sandvikcoromant + +// sanofi : 2014-10-09 Sanofi +sanofi + +// sap : 2014-03-27 SAP AG +sap + +// sapo : 2014-11-07 PT Comunicacoes S.A. +sapo + +// sarl : 2014-07-03 Delta Orchard, LLC +sarl + +// sas : 2015-04-02 Research IP LLC +sas + +// saxo : 2014-10-31 Saxo Bank A/S +saxo + +// sbi : 2015-03-12 STATE BANK OF INDIA +sbi + +// sbs : 2014-11-07 SPECIAL BROADCASTING SERVICE CORPORATION +sbs + +// sca : 2014-03-13 SVENSKA CELLULOSA AKTIEBOLAGET SCA (publ) +sca + +// scb : 2014-02-20 The Siam Commercial Bank Public Company Limited ("SCB") +scb + +// schmidt : 2014-04-03 SALM S.A.S. +schmidt + +// scholarships : 2014-04-24 Scholarships.com, LLC +scholarships + +// school : 2014-12-18 Little Galley, LLC +school + +// schule : 2014-03-06 Outer Moon, LLC +schule + +// schwarz : 2014-09-18 Schwarz Domains und Services GmbH & Co. KG +schwarz + +// science : 2014-09-11 dot Science Limited +science + +// scor : 2014-10-31 SCOR SE +scor + +// scot : 2014-01-23 Dot Scot Registry Limited +scot + +// seat : 2014-05-22 SEAT, S.A. (Sociedad Unipersonal) +seat + +// seek : 2014-12-04 Seek Limited +seek + +// sener : 2014-10-24 Sener Ingeniería y Sistemas, S.A. +sener + +// services : 2014-02-27 Fox Castle, LLC +services + +// sew : 2014-07-17 SEW-EURODRIVE GmbH & Co KG +sew + +// sex : 2014-11-13 ICM Registry SX LLC +sex + +// sexy : 2013-09-11 Uniregistry, Corp. +sexy + +// sharp : 2014-05-01 Sharp Corporation +sharp + +// shaw : 2015-04-23 Shaw Cablesystems G.P. +shaw + +// shia : 2014-09-04 Asia Green IT System Bilgisayar San. ve Tic. Ltd. Sti. +shia + +// shiksha : 2013-11-14 Afilias Limited +shiksha + +// shoes : 2013-10-02 Binky Galley, LLC +shoes + +// shouji : 2015-01-08 QIHOO 360 TECHNOLOGY CO. LTD. +shouji + +// show : 2015-03-05 Snow Beach, LLC +show + +// shriram : 2014-01-23 Shriram Capital Ltd. +shriram + +// sina : 2015-03-12 Sina Corporation +sina + +// singles : 2013-08-27 Fern Madison, LLC +singles + +// site : 2015-01-15 DotSite Inc. +site + +// ski : 2015-04-09 STARTING DOT LIMITED +ski + +// skin : 2015-01-15 L'Oréal +skin + +// sky : 2014-06-19 Sky IP International Ltd, a company incorporated in England and Wales, operating via its registered Swiss branch +sky + +// skype : 2014-12-18 Microsoft Corporation +skype + +// smile : 2014-12-18 Amazon EU S.à r.l. +smile + +// sncf : 2015-02-19 Société Nationale des Chemins de fer Francais S N C F +sncf + +// soccer : 2015-03-26 Foggy Shadow, LLC +soccer + +// social : 2013-11-07 United TLD Holdco Ltd. +social + +// software : 2014-03-20 +software + +// sohu : 2013-12-19 Sohu.com Limited +sohu + +// solar : 2013-11-07 Ruby Town, LLC +solar + +// solutions : 2013-11-07 Silver Cover, LLC +solutions + +// song : 2015-02-26 Amazon EU S.à r.l. +song + +// sony : 2015-01-08 Sony Corporation +sony + +// soy : 2014-01-23 Charleston Road Registry Inc. +soy + +// space : 2014-04-03 DotSpace Inc. +space + +// spiegel : 2014-02-05 SPIEGEL-Verlag Rudolf Augstein GmbH & Co. KG +spiegel + +// spot : 2015-02-26 Amazon EU S.à r.l. +spot + +// spreadbetting : 2014-12-11 IG Group Holdings PLC +spreadbetting + +// stada : 2014-11-13 STADA Arzneimittel AG +stada + +// star : 2015-01-08 Star India Private Limited +star + +// starhub : 2015-02-05 StarHub Limited +starhub + +// statebank : 2015-03-12 STATE BANK OF INDIA +statebank + +// statoil : 2014-12-04 Statoil ASA +statoil + +// stc : 2014-10-09 Saudi Telecom Company +stc + +// stcgroup : 2014-10-09 Saudi Telecom Company +stcgroup + +// stockholm : 2014-12-18 Stockholms kommun +stockholm + +// storage : 2014-12-22 Self Storage Company LLC +storage + +// store : 2015-04-09 DotStore Inc. +store + +// studio : 2015-02-11 Spring Goodbye, LLC +studio + +// study : 2014-12-11 OPEN UNIVERSITIES AUSTRALIA PTY LTD +study + +// style : 2014-12-04 Binky Moon, LLC +style + +// sucks : 2014-12-22 Vox Populi Registry Inc. +sucks + +// supersport : 2015-03-05 SuperSport International Holdings Proprietary Limited +supersport + +// supplies : 2013-12-19 Atomic Fields, LLC +supplies + +// supply : 2013-12-19 Half Falls, LLC +supply + +// support : 2013-10-24 Grand Orchard, LLC +support + +// surf : 2014-01-09 Top Level Domain Holdings Limited +surf + +// surgery : 2014-03-20 Tin Avenue, LLC +surgery + +// suzuki : 2014-02-20 SUZUKI MOTOR CORPORATION +suzuki + +// swatch : 2015-01-08 The Swatch Group Ltd +swatch + +// swiss : 2014-10-16 Swiss Confederation +swiss + +// sydney : 2014-09-18 State of New South Wales, Department of Premier and Cabinet +sydney + +// symantec : 2014-12-04 Symantec Corporation +symantec + +// systems : 2013-11-07 Dash Cypress, LLC +systems + +// tab : 2014-12-04 Tabcorp Holdings Limited +tab + +// taipei : 2014-07-10 Taipei City Government +taipei + +// talk : 2015-04-09 Amazon EU S.à r.l. +talk + +// taobao : 2015-01-15 Alibaba Group Holding Limited +taobao + +// tatamotors : 2015-03-12 Tata Motors Ltd +tatamotors + +// tatar : 2014-04-24 Limited Liability Company "Coordination Center of Regional Domain of Tatarstan Republic" +tatar + +// tattoo : 2013-08-30 Uniregistry, Corp. +tattoo + +// tax : 2014-03-20 Storm Orchard, LLC +tax + +// taxi : 2015-03-19 Pine Falls, LLC +taxi + +// tci : 2014-09-12 Asia Green IT System Bilgisayar San. ve Tic. Ltd. Sti. +tci + +// team : 2015-03-05 Atomic Lake, LLC +team + +// tech : 2015-01-30 Dot Tech LLC +tech + +// technology : 2013-09-13 Auburn Falls +technology + +// telecity : 2015-02-19 TelecityGroup International Limited +telecity + +// telefonica : 2014-10-16 Telefónica S.A. +telefonica + +// temasek : 2014-08-07 Temasek Holdings (Private) Limited +temasek + +// tennis : 2014-12-04 Cotton Bloom, LLC +tennis + +// thd : 2015-04-02 Homer TLC, Inc. +thd + +// theater : 2015-03-19 Blue Tigers, LLC +theater + +// theguardian : 2015-04-30 Guardian News and Media Limited +theguardian + +// tickets : 2015-02-05 Accent Media Limited +tickets + +// tienda : 2013-11-14 Victor Manor, LLC +tienda + +// tiffany : 2015-01-30 Tiffany and Company +tiffany + +// tips : 2013-09-20 Corn Willow, LLC +tips + +// tires : 2014-11-07 Dog Edge, LLC +tires + +// tirol : 2014-04-24 punkt Tirol GmbH +tirol + +// tmall : 2015-01-15 Alibaba Group Holding Limited +tmall + +// today : 2013-09-20 Pearl Woods, LLC +today + +// tokyo : 2013-11-13 GMO Registry, Inc. +tokyo + +// tools : 2013-11-21 Pioneer North, LLC +tools + +// top : 2014-03-20 Jiangsu Bangning Science & Technology Co.,Ltd. +top + +// toray : 2014-12-18 Toray Industries, Inc. +toray + +// toshiba : 2014-04-10 TOSHIBA Corporation +toshiba + +// tours : 2015-01-22 Sugar Station, LLC +tours + +// town : 2014-03-06 Koko Moon, LLC +town + +// toyota : 2015-04-23 TOYOTA MOTOR CORPORATION +toyota + +// toys : 2014-03-06 Pioneer Orchard, LLC +toys + +// trade : 2014-01-23 Elite Registry Limited +trade + +// trading : 2014-12-11 IG Group Holdings PLC +trading + +// training : 2013-11-07 Wild Willow, LLC +training + +// travelers : 2015-03-26 Travelers TLD, LLC +travelers + +// travelersinsurance : 2015-03-26 Travelers TLD, LLC +travelersinsurance + +// trust : 2014-10-16 +trust + +// trv : 2015-03-26 Travelers TLD, LLC +trv + +// tui : 2014-07-03 TUI AG +tui + +// tunes : 2015-02-26 Amazon EU S.à r.l. +tunes + +// tushu : 2014-12-18 Amazon EU S.à r.l. +tushu + +// tvs : 2015-02-19 T V SUNDRAM IYENGAR & SONS LIMITED +tvs + +// ubs : 2014-12-11 UBS AG +ubs + +// university : 2014-03-06 Little Station, LLC +university + +// uno : 2013-09-11 Dot Latin LLC +uno + +// uol : 2014-05-01 UBN INTERNET LTDA. +uol + +// vacations : 2013-12-05 Atomic Tigers, LLC +vacations + +// vana : 2014-12-11 Lifestyle Domain Holdings, Inc. +vana + +// vegas : 2014-01-16 Dot Vegas, Inc. +vegas + +// ventures : 2013-08-27 Binky Lake, LLC +ventures + +// versicherung : 2014-03-20 dotversicherung-registry GmbH +versicherung + +// vet : 2014-03-06 +vet + +// viajes : 2013-10-17 Black Madison, LLC +viajes + +// video : 2014-10-16 +video + +// viking : 2015-04-02 Viking River Cruises (Bermuda) Ltd. +viking + +// villas : 2013-12-05 New Sky, LLC +villas + +// vip : 2015-01-22 Minds + Machines Group Limited +vip + +// virgin : 2014-09-25 Virgin Enterprises Limited +virgin + +// vision : 2013-12-05 Koko Station, LLC +vision + +// vista : 2014-09-18 Vistaprint Limited +vista + +// vistaprint : 2014-09-18 Vistaprint Limited +vistaprint + +// viva : 2014-11-07 Saudi Telecom Company +viva + +// vlaanderen : 2014-02-06 DNS.be vzw +vlaanderen + +// vodka : 2013-12-19 Top Level Domain Holdings Limited +vodka + +// vote : 2013-11-21 Monolith Registry LLC +vote + +// voting : 2013-11-13 Valuetainment Corp. +voting + +// voto : 2013-11-21 Monolith Registry LLC +voto + +// voyage : 2013-08-27 Ruby House, LLC +voyage + +// vuelos : 2015-03-05 Travel Reservations SRL +vuelos + +// wales : 2014-05-08 Nominet UK +wales + +// walter : 2014-11-13 Sandvik AB +walter + +// wang : 2013-10-24 Zodiac Leo Limited +wang + +// wanggou : 2014-12-18 Amazon EU S.à r.l. +wanggou + +// watch : 2013-11-14 Sand Shadow, LLC +watch + +// watches : 2014-12-22 Richemont DNS Inc. +watches + +// weather : 2015-01-08 The Weather Channel, LLC +weather + +// weatherchannel : 2015-03-12 The Weather Channel, LLC +weatherchannel + +// webcam : 2014-01-23 dot Webcam Limited +webcam + +// website : 2014-04-03 DotWebsite Inc. +website + +// wed : 2013-10-01 Atgron, Inc. +wed + +// wedding : 2014-04-24 Top Level Domain Holdings Limited +wedding + +// weibo : 2015-03-05 Sina Corporation +weibo + +// weir : 2015-01-29 Weir Group IP Limited +weir + +// whoswho : 2014-02-20 Who's Who Registry +whoswho + +// wien : 2013-10-28 punkt.wien GmbH +wien + +// wiki : 2013-11-07 Top Level Design, LLC +wiki + +// williamhill : 2014-03-13 William Hill Organization Limited +williamhill + +// win : 2014-11-20 First Registry Limited +win + +// windows : 2014-12-18 Microsoft Corporation +windows + +// wme : 2014-02-13 William Morris Endeavor Entertainment, LLC +wme + +// work : 2013-12-19 Top Level Domain Holdings Limited +work + +// works : 2013-11-14 Little Dynamite, LLC +works + +// world : 2014-06-12 Bitter Fields, LLC +world + +// wtc : 2013-12-19 World Trade Centers Association, Inc. +wtc + +// wtf : 2014-03-06 Hidden Way, LLC +wtf + +// xbox : 2014-12-18 Microsoft Corporation +xbox + +// xerox : 2014-10-24 Xerox DNHC LLC +xerox + +// xihuan : 2015-01-08 QIHOO 360 TECHNOLOGY CO. LTD. +xihuan + +// xin : 2014-12-11 Elegant Leader Limited +xin + +// xn--11b4c3d : 2015-01-15 VeriSign Sarl +कॉम + +// xn--1ck2e1b : 2015-02-26 Amazon EU S.à r.l. +セール + +// xn--1qqw23a : 2014-01-09 Guangzhou YU Wei Information Technology Co., Ltd. +佛山 + +// xn--30rr7y : 2014-06-12 Excellent First Limited +慈善 + +// xn--3bst00m : 2013-09-13 Eagle Horizon Limited +集团 + +// xn--3ds443g : 2013-09-08 TLD REGISTRY LIMITED +在线 + +// xn--3pxu8k : 2015-01-15 VeriSign Sarl +点看 + +// xn--42c2d9a : 2015-01-15 VeriSign Sarl +คอม + +// xn--45q11c : 2013-11-21 Zodiac Scorpio Limited +八卦 + +// xn--4gbrim : 2013-10-04 Suhub Electronic Establishment +موقع + +// xn--55qw42g : 2013-11-08 China Organizational Name Administration Center +公益 + +// xn--55qx5d : 2013-11-14 Computer Network Information Center of Chinese Academy of Sciences (China Internet Network Information Center) +公司 + +// xn--5tzm5g : 2014-12-22 Global Website TLD Asia Limited +网站 + +// xn--6frz82g : 2013-09-23 Afilias Limited +移动 + +// xn--6qq986b3xl : 2013-09-13 Tycoon Treasure Limited +我爱你 + +// xn--80adxhks : 2013-12-19 Foundation for Assistance for Internet Technologies and Infrastructure Development (FAITID) +москва + +// xn--80asehdb : 2013-07-14 CORE Association +онлайн + +// xn--80aswg : 2013-07-14 CORE Association +сайт + +// xn--8y0a063a : 2015-03-26 China United Network Communications Corporation Limited +联通 + +// xn--9dbq2a : 2015-01-15 VeriSign Sarl +קום + +// xn--9et52u : 2014-06-12 RISE VICTORY LIMITED +时尚 + +// xn--9krt00a : 2015-03-12 Sina Corporation +微博 + +// xn--b4w605ferd : 2014-08-07 Temasek Holdings (Private) Limited +淡马锡 + +// xn--bck1b9a5dre4c : 2015-02-26 Amazon EU S.à r.l. +ファッション + +// xn--c1avg : 2013-11-14 Public Interest Registry +орг + +// xn--c2br7g : 2015-01-15 VeriSign Sarl +नेट + +// xn--cck2b3b : 2015-02-26 Amazon EU S.à r.l. +ストア + +// xn--cg4bki : 2013-09-27 SAMSUNG SDS CO., LTD +삼성 + +// xn--czr694b : 2014-01-16 HU YI GLOBAL INFORMATION RESOURCES (HOLDING) COMPANY.HONGKONG LIMITED +商标 + +// xn--czrs0t : 2013-12-19 Wild Island, LLC +商店 + +// xn--czru2d : 2013-11-21 Zodiac Capricorn Limited +商城 + +// xn--d1acj3b : 2013-11-20 The Foundation for Network Initiatives “The Smart Internet” +дети + +// xn--eckvdtc9d : 2014-12-18 Amazon EU S.à r.l. +ポイント + +// xn--efvy88h : 2014-08-22 Xinhua News Agency Guangdong Branch 新华通讯社广东分社 +新闻 + +// xn--estv75g : 2015-02-19 Industrial and Commercial Bank of China Limited +工行 + +// xn--fct429k : 2015-04-09 Amazon EU S.à r.l. +家電 + +// xn--fhbei : 2015-01-15 VeriSign Sarl +كوم + +// xn--fiq228c5hs : 2013-09-08 TLD REGISTRY LIMITED +中文网 + +// xn--fiq64b : 2013-10-14 CITIC Group Corporation +中信 + +// xn--fjq720a : 2014-05-22 Will Bloom, LLC +娱乐 + +// xn--flw351e : 2014-07-31 Charleston Road Registry Inc. +谷歌 + +// xn--g2xx48c : 2015-01-30 Minds + Machines Group Limited +购物 + +// xn--gckr3f0f : 2015-02-26 Amazon EU S.à r.l. +クラウド + +// xn--hxt814e : 2014-05-15 Zodiac Libra Limited +网店 + +// xn--i1b6b1a6a2e : 2013-11-14 Public Interest Registry +संगठन + +// xn--imr513n : 2014-12-11 HU YI GLOBAL INFORMATION RESOURCES (HOLDING) COMPANY. HONGKONG LIMITED +餐厅 + +// xn--io0a7i : 2013-11-14 Computer Network Information Center of Chinese Academy of Sciences (China Internet Network Information Center) +网络 + +// xn--j1aef : 2015-01-15 VeriSign Sarl +ком + +// xn--jlq61u9w7b : 2015-01-08 Nokia Corporation +诺基亚 + +// xn--jvr189m : 2015-02-26 Amazon EU S.à r.l. +食品 + +// xn--kcrx77d1x4a : 2014-11-07 Koninklijke Philips N.V. +飞利浦 + +// xn--kpu716f : 2014-12-22 Richemont DNS Inc. +手表 + +// xn--kput3i : 2014-02-13 Beijing RITT-Net Technology Development Co., Ltd +手机 + +// xn--mgba3a3ejt : 2014-11-20 Aramco Services Company +ارامكو + +// xn--mgbab2bd : 2013-10-31 CORE Association +بازار + +// xn--mgbb9fbpob : 2014-12-18 GreenTech Consultancy Company W.L.L. +موبايلي + +// xn--mgbt3dhd : 2014-09-04 Asia Green IT System Bilgisayar San. ve Tic. Ltd. Sti. +همراه + +// xn--mk1bu44c : 2015-01-15 VeriSign Sarl +닷컴 + +// xn--mxtq1m : 2014-03-06 Net-Chinese Co., Ltd. +政府 + +// xn--ngbc5azd : 2013-07-13 International Domain Registry Pty. Ltd. +شبكة + +// xn--ngbe9e0a : 2014-12-04 Kuwait Finance House +بيتك + +// xn--nqv7f : 2013-11-14 Public Interest Registry +机构 + +// xn--nqv7fs00ema : 2013-11-14 Public Interest Registry +组织机构 + +// xn--nyqy26a : 2014-11-07 Stable Tone Limited +健康 + +// xn--p1acf : 2013-12-12 Rusnames Limited +рус + +// xn--pbt977c : 2014-12-22 Richemont DNS Inc. +珠宝 + +// xn--pssy2u : 2015-01-15 VeriSign Sarl +大拿 + +// xn--q9jyb4c : 2013-09-17 Charleston Road Registry Inc. +みんな + +// xn--qcka1pmc : 2014-07-31 Charleston Road Registry Inc. +グーグル + +// xn--rhqv96g : 2013-09-11 Stable Tone Limited +世界 + +// xn--rovu88b : 2015-02-26 Amazon EU S.à r.l. +書籍 + +// xn--ses554g : 2014-01-16 +网址 + +// xn--t60b56a : 2015-01-15 VeriSign Sarl +닷넷 + +// xn--tckwe : 2015-01-15 VeriSign Sarl +コム + +// xn--unup4y : 2013-07-14 Spring Fields, LLC +游戏 + +// xn--vermgensberater-ctb : 2014-06-23 Deutsche Vermögensberatung Aktiengesellschaft DVAG +vermögensberater + +// xn--vermgensberatung-pwb : 2014-06-23 Deutsche Vermögensberatung Aktiengesellschaft DVAG +vermögensberatung + +// xn--vhquv : 2013-08-27 Dash McCook, LLC +企业 + +// xn--vuq861b : 2014-10-16 Beijing Tele-info Network Technology Co., Ltd. +信息 + +// xn--w4r85el8fhu5dnra : 2015-04-30 Kerry Trading Co. Limited +嘉里大酒店 + +// xn--xhq521b : 2013-11-14 Guangzhou YU Wei Information Technology Co., Ltd. +广东 + +// xn--zfr164b : 2013-11-08 China Organizational Name Administration Center +政务 + +// xyz : 2013-12-05 XYZ.COM LLC +xyz + +// yachts : 2014-01-09 DERYachts, LLC +yachts + +// yahoo : 2015-04-02 Yahoo! Domain Services Inc. +yahoo + +// yamaxun : 2014-12-18 Amazon EU S.à r.l. +yamaxun + +// yandex : 2014-04-10 YANDEX, LLC +yandex + +// yodobashi : 2014-11-20 YODOBASHI CAMERA CO.,LTD. +yodobashi + +// yoga : 2014-05-29 Top Level Domain Holdings Limited +yoga + +// yokohama : 2013-12-12 GMO Registry, Inc. +yokohama + +// you : 2015-04-09 Amazon EU S.à r.l. +you + +// youtube : 2014-05-01 Charleston Road Registry Inc. +youtube + +// yun : 2015-01-08 QIHOO 360 TECHNOLOGY CO. LTD. +yun + +// zara : 2014-11-07 Industria de Diseño Textil, S.A. (INDITEX, S.A.) +zara + +// zero : 2014-12-18 Amazon EU S.à r.l. +zero + +// zip : 2014-05-08 Charleston Road Registry Inc. +zip + +// zone : 2013-11-14 Outer Falls, LLC +zone + +// zuerich : 2014-11-07 Kanton Zürich (Canton of Zurich) +zuerich + + +// ===END ICANN DOMAINS=== +// ===BEGIN PRIVATE DOMAINS=== +// (Note: these are in alphabetical order by company name) + +// Amazon CloudFront : https://aws.amazon.com/cloudfront/ +// Submitted by Donavan Miller 2013-03-22 +cloudfront.net + +// Amazon Elastic Compute Cloud: https://aws.amazon.com/ec2/ +// Submitted by Osman Surkatty 2014-12-16 +ap-northeast-1.compute.amazonaws.com +ap-southeast-1.compute.amazonaws.com +ap-southeast-2.compute.amazonaws.com +cn-north-1.compute.amazonaws.cn +compute.amazonaws.cn +compute.amazonaws.com +compute-1.amazonaws.com +eu-west-1.compute.amazonaws.com +eu-central-1.compute.amazonaws.com +sa-east-1.compute.amazonaws.com +us-east-1.amazonaws.com +us-gov-west-1.compute.amazonaws.com +us-west-1.compute.amazonaws.com +us-west-2.compute.amazonaws.com +z-1.compute-1.amazonaws.com +z-2.compute-1.amazonaws.com + +// Amazon Elastic Beanstalk : https://aws.amazon.com/elasticbeanstalk/ +// Submitted by Adam Stein 2013-04-02 +elasticbeanstalk.com + +// Amazon Elastic Load Balancing : https://aws.amazon.com/elasticloadbalancing/ +// Submitted by Scott Vidmar 2013-03-27 +elb.amazonaws.com + +// Amazon S3 : https://aws.amazon.com/s3/ +// Submitted by Courtney Eckhardt 2013-03-22 +s3.amazonaws.com +s3-us-west-2.amazonaws.com +s3-us-west-1.amazonaws.com +s3-eu-west-1.amazonaws.com +s3-ap-southeast-1.amazonaws.com +s3-ap-southeast-2.amazonaws.com +s3-ap-northeast-1.amazonaws.com +s3-sa-east-1.amazonaws.com +s3-us-gov-west-1.amazonaws.com +s3-fips-us-gov-west-1.amazonaws.com +s3-website-us-east-1.amazonaws.com +s3-website-us-west-2.amazonaws.com +s3-website-us-west-1.amazonaws.com +s3-website-eu-west-1.amazonaws.com +s3-website-ap-southeast-1.amazonaws.com +s3-website-ap-southeast-2.amazonaws.com +s3-website-ap-northeast-1.amazonaws.com +s3-website-sa-east-1.amazonaws.com +s3-website-us-gov-west-1.amazonaws.com + +// BetaInABox +// Submitted by adrian@betainabox.com 2012-09-13 +betainabox.com + +// CentralNic : http://www.centralnic.com/names/domains +// Submitted by registry 2012-09-27 +ae.org +ar.com +br.com +cn.com +com.de +com.se +de.com +eu.com +gb.com +gb.net +hu.com +hu.net +jp.net +jpn.com +kr.com +mex.com +no.com +qc.com +ru.com +sa.com +se.com +se.net +uk.com +uk.net +us.com +uy.com +za.bz +za.com + +// Africa.com Web Solutions Ltd : https://registry.africa.com +// Submitted by Gavin Brown 2014-02-04 +africa.com + +// iDOT Services Limited : http://www.domain.gr.com +// Submitted by Gavin Brown 2014-02-04 +gr.com + +// Radix FZC : http://domains.in.net +// Submitted by Gavin Brown 2014-02-04 +in.net + +// US REGISTRY LLC : http://us.org +// Submitted by Gavin Brown 2014-02-04 +us.org + +// co.com Registry, LLC : https://registry.co.com +// Submitted by Gavin Brown 2014-02-04 +co.com + +// c.la : http://www.c.la/ +c.la + +// cloudControl : https://www.cloudcontrol.com/ +// Submitted by Tobias Wilken 2013-07-23 +cloudcontrolled.com +cloudcontrolapp.com + +// co.ca : http://registry.co.ca/ +co.ca + +// CoDNS B.V. +co.nl +co.no + +// Commerce Guys, SAS +// Submitted by Damien Tournoud 2015-01-22 +*.platform.sh + +// Cupcake : https://cupcake.io/ +// Submitted by Jonathan Rudenberg 2013-10-08 +cupcake.is + +// DreamHost : http://www.dreamhost.com/ +// Submitted by Andrew Farmer 2012-10-02 +dreamhosters.com + +// DynDNS.com : http://www.dyndns.com/services/dns/dyndns/ +dyndns-at-home.com +dyndns-at-work.com +dyndns-blog.com +dyndns-free.com +dyndns-home.com +dyndns-ip.com +dyndns-mail.com +dyndns-office.com +dyndns-pics.com +dyndns-remote.com +dyndns-server.com +dyndns-web.com +dyndns-wiki.com +dyndns-work.com +dyndns.biz +dyndns.info +dyndns.org +dyndns.tv +at-band-camp.net +ath.cx +barrel-of-knowledge.info +barrell-of-knowledge.info +better-than.tv +blogdns.com +blogdns.net +blogdns.org +blogsite.org +boldlygoingnowhere.org +broke-it.net +buyshouses.net +cechire.com +dnsalias.com +dnsalias.net +dnsalias.org +dnsdojo.com +dnsdojo.net +dnsdojo.org +does-it.net +doesntexist.com +doesntexist.org +dontexist.com +dontexist.net +dontexist.org +doomdns.com +doomdns.org +dvrdns.org +dyn-o-saur.com +dynalias.com +dynalias.net +dynalias.org +dynathome.net +dyndns.ws +endofinternet.net +endofinternet.org +endoftheinternet.org +est-a-la-maison.com +est-a-la-masion.com +est-le-patron.com +est-mon-blogueur.com +for-better.biz +for-more.biz +for-our.info +for-some.biz +for-the.biz +forgot.her.name +forgot.his.name +from-ak.com +from-al.com +from-ar.com +from-az.net +from-ca.com +from-co.net +from-ct.com +from-dc.com +from-de.com +from-fl.com +from-ga.com +from-hi.com +from-ia.com +from-id.com +from-il.com +from-in.com +from-ks.com +from-ky.com +from-la.net +from-ma.com +from-md.com +from-me.org +from-mi.com +from-mn.com +from-mo.com +from-ms.com +from-mt.com +from-nc.com +from-nd.com +from-ne.com +from-nh.com +from-nj.com +from-nm.com +from-nv.com +from-ny.net +from-oh.com +from-ok.com +from-or.com +from-pa.com +from-pr.com +from-ri.com +from-sc.com +from-sd.com +from-tn.com +from-tx.com +from-ut.com +from-va.com +from-vt.com +from-wa.com +from-wi.com +from-wv.com +from-wy.com +ftpaccess.cc +fuettertdasnetz.de +game-host.org +game-server.cc +getmyip.com +gets-it.net +go.dyndns.org +gotdns.com +gotdns.org +groks-the.info +groks-this.info +ham-radio-op.net +here-for-more.info +hobby-site.com +hobby-site.org +home.dyndns.org +homedns.org +homeftp.net +homeftp.org +homeip.net +homelinux.com +homelinux.net +homelinux.org +homeunix.com +homeunix.net +homeunix.org +iamallama.com +in-the-band.net +is-a-anarchist.com +is-a-blogger.com +is-a-bookkeeper.com +is-a-bruinsfan.org +is-a-bulls-fan.com +is-a-candidate.org +is-a-caterer.com +is-a-celticsfan.org +is-a-chef.com +is-a-chef.net +is-a-chef.org +is-a-conservative.com +is-a-cpa.com +is-a-cubicle-slave.com +is-a-democrat.com +is-a-designer.com +is-a-doctor.com +is-a-financialadvisor.com +is-a-geek.com +is-a-geek.net +is-a-geek.org +is-a-green.com +is-a-guru.com +is-a-hard-worker.com +is-a-hunter.com +is-a-knight.org +is-a-landscaper.com +is-a-lawyer.com +is-a-liberal.com +is-a-libertarian.com +is-a-linux-user.org +is-a-llama.com +is-a-musician.com +is-a-nascarfan.com +is-a-nurse.com +is-a-painter.com +is-a-patsfan.org +is-a-personaltrainer.com +is-a-photographer.com +is-a-player.com +is-a-republican.com +is-a-rockstar.com +is-a-socialist.com +is-a-soxfan.org +is-a-student.com +is-a-teacher.com +is-a-techie.com +is-a-therapist.com +is-an-accountant.com +is-an-actor.com +is-an-actress.com +is-an-anarchist.com +is-an-artist.com +is-an-engineer.com +is-an-entertainer.com +is-by.us +is-certified.com +is-found.org +is-gone.com +is-into-anime.com +is-into-cars.com +is-into-cartoons.com +is-into-games.com +is-leet.com +is-lost.org +is-not-certified.com +is-saved.org +is-slick.com +is-uberleet.com +is-very-bad.org +is-very-evil.org +is-very-good.org +is-very-nice.org +is-very-sweet.org +is-with-theband.com +isa-geek.com +isa-geek.net +isa-geek.org +isa-hockeynut.com +issmarterthanyou.com +isteingeek.de +istmein.de +kicks-ass.net +kicks-ass.org +knowsitall.info +land-4-sale.us +lebtimnetz.de +leitungsen.de +likes-pie.com +likescandy.com +merseine.nu +mine.nu +misconfused.org +mypets.ws +myphotos.cc +neat-url.com +office-on-the.net +on-the-web.tv +podzone.net +podzone.org +readmyblog.org +saves-the-whales.com +scrapper-site.net +scrapping.cc +selfip.biz +selfip.com +selfip.info +selfip.net +selfip.org +sells-for-less.com +sells-for-u.com +sells-it.net +sellsyourhome.org +servebbs.com +servebbs.net +servebbs.org +serveftp.net +serveftp.org +servegame.org +shacknet.nu +simple-url.com +space-to-rent.com +stuff-4-sale.org +stuff-4-sale.us +teaches-yoga.com +thruhere.net +traeumtgerade.de +webhop.biz +webhop.info +webhop.net +webhop.org +worse-than.tv +writesthisblog.com + +// EU.org https://eu.org/ +// Submitted by Pierre Beyssac 2015-04-17 + +eu.org +al.eu.org +asso.eu.org +at.eu.org +au.eu.org +be.eu.org +bg.eu.org +ca.eu.org +cd.eu.org +ch.eu.org +cn.eu.org +cy.eu.org +cz.eu.org +de.eu.org +dk.eu.org +edu.eu.org +ee.eu.org +es.eu.org +fi.eu.org +fr.eu.org +gr.eu.org +hr.eu.org +hu.eu.org +ie.eu.org +il.eu.org +in.eu.org +int.eu.org +is.eu.org +it.eu.org +jp.eu.org +kr.eu.org +lt.eu.org +lu.eu.org +lv.eu.org +mc.eu.org +me.eu.org +mk.eu.org +mt.eu.org +my.eu.org +net.eu.org +ng.eu.org +nl.eu.org +no.eu.org +nz.eu.org +paris.eu.org +pl.eu.org +pt.eu.org +q-a.eu.org +ro.eu.org +ru.eu.org +se.eu.org +si.eu.org +sk.eu.org +tr.eu.org +uk.eu.org +us.eu.org + +// Fastly Inc. http://www.fastly.com/ +// Submitted by Vladimir Vuksan 2013-05-31 +a.ssl.fastly.net +b.ssl.fastly.net +global.ssl.fastly.net +a.prod.fastly.net +global.prod.fastly.net + +// Firebase, Inc. +// Submitted by Chris Raynor 2014-01-21 +firebaseapp.com + +// Flynn : https://flynn.io +// Submitted by Jonathan Rudenberg 2014-07-12 +flynnhub.com + +// GDS : https://www.gov.uk/service-manual/operations/operating-servicegovuk-subdomains +// Submitted by David Illsley 2014-08-28 +service.gov.uk + +// GitHub, Inc. +// Submitted by Ben Toews 2014-02-06 +github.io +githubusercontent.com + +// GlobeHosting, Inc. +// Submitted by Zoltan Egresi 2013-07-12 +ro.com + +// Google, Inc. +// Submitted by Eduardo Vela 2014-12-19 +appspot.com +blogspot.ae +blogspot.be +blogspot.bj +blogspot.ca +blogspot.cf +blogspot.ch +blogspot.co.at +blogspot.co.il +blogspot.co.nz +blogspot.co.uk +blogspot.com +blogspot.com.ar +blogspot.com.au +blogspot.com.br +blogspot.com.es +blogspot.com.tr +blogspot.cv +blogspot.cz +blogspot.de +blogspot.dk +blogspot.fi +blogspot.fr +blogspot.gr +blogspot.hk +blogspot.hu +blogspot.ie +blogspot.in +blogspot.it +blogspot.jp +blogspot.kr +blogspot.mr +blogspot.mx +blogspot.nl +blogspot.no +blogspot.pt +blogspot.re +blogspot.ro +blogspot.ru +blogspot.se +blogspot.sg +blogspot.sk +blogspot.td +blogspot.tw +codespot.com +googleapis.com +googlecode.com +pagespeedmobilizer.com +withgoogle.com + +// Heroku : https://www.heroku.com/ +// Submitted by Tom Maher 2013-05-02 +herokuapp.com +herokussl.com + +// iki.fi +// Submitted by Hannu Aronsson 2009-11-05 +iki.fi + +// info.at : http://www.info.at/ +biz.at +info.at + +// Michau Enterprises Limited : http://www.co.pl/ +co.pl + +// Microsoft : http://microsoft.com +// Submitted by Barry Dorrans 2014-01-24 +azurewebsites.net +azure-mobile.net +cloudapp.net + +// Neustar Inc. +// Submitted by Trung Tran 2015-04-23 +4u.com + +// NFSN, Inc. : https://www.NearlyFreeSpeech.NET/ +// Submitted by Jeff Wheelhouse 2014-02-02 +nfshost.com + +// NYC.mn : http://www.information.nyc.mn +// Submitted by Matthew Brown 2013-03-11 +nyc.mn + +// One Fold Media : http://www.onefoldmedia.com/ +// Submitted by Eddie Jones 2014-06-10 +nid.io + +// Opera Software, A.S.A. +// Submitted by Yngve Pettersen 2009-11-26 +operaunite.com + +// OutSystems +// Submitted by Duarte Santos 2014-03-11 +outsystemscloud.com + +// .pl domains (grandfathered) +art.pl +gliwice.pl +krakow.pl +poznan.pl +wroc.pl +zakopane.pl + +// priv.at : http://www.nic.priv.at/ +// Submitted by registry 2008-06-09 +priv.at + +// Red Hat, Inc. OpenShift : https://openshift.redhat.com/ +// Submitted by Tim Kramer 2012-10-24 +rhcloud.com + +// SinaAppEngine : http://sae.sina.com.cn/ +// Submitted by SinaAppEngine 2015-02-02 +sinaapp.com +vipsinaapp.com +1kapp.com + +// TASK geographical domains (www.task.gda.pl/uslugi/dns) +gda.pl +gdansk.pl +gdynia.pl +med.pl +sopot.pl + +// UDR Limited : http://www.udr.hk.com +// Submitted by registry 2014-11-07 +hk.com +hk.org +ltd.hk +inc.hk + +// Yola : https://www.yola.com/ +// Submitted by Stefano Rivera 2014-07-09 +yolasite.com + +// ZaNiC : http://www.za.net/ +// Submitted by registry 2009-10-03 +za.net +za.org + +// ===END PRIVATE DOMAINS=== diff --git a/mobile/ios/SharedTests/ArrayExtensionTests.swift b/mobile/ios/SharedTests/ArrayExtensionTests.swift new file mode 100644 index 0000000000..7ff01378d8 --- /dev/null +++ b/mobile/ios/SharedTests/ArrayExtensionTests.swift @@ -0,0 +1,62 @@ +/* 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 XCTest + +class ArrayExtensionTests: XCTestCase { + override func setUp() { + super.setUp() + } + + override func tearDown() { + super.tearDown() + } + + func testUnique() { + let a = [1, 2, 3, 4, 5, 6, 1, 2] + let result = a.unique { return $0 } + XCTAssertEqual(result, [1, 2, 3, 4, 5, 6]) + + let b = [1, 2, 3] + let resultB = b.unique { return $0 } + XCTAssertEqual(resultB, [1, 2, 3]) + } + + func testUnion() { + let a = [1, 2, 3, 4, 5, 6] + let b = [7, 8, 9, 10] + XCTAssertEqual(a.union(b) { return $0 }, + [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]) + + let c = [1, 2, 3, 4, 5, 6] + let d = [4, 5, 6, 7, 8, 9, 10] + XCTAssertEqual(c.union(d) { return $0 }, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]) + + let e = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] + let f = [4, 5, 6, 7, 8, 9, 10] + XCTAssertEqual(e.union(f) { return $0 }, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]) + + let g = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] + let h = [Int]() + XCTAssertEqual(g.union(h) { return $0 }, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]) + + let i = [Int]() + let j = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] + XCTAssertEqual(i.union(j) { return $0 }, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]) + } + + func testSameElements() { + let k = [1, 2, 3, 4, 5] + let l = [1, 2, 3, 4, 5] + let m = [2, 4, 6, 8, 10] + let n: [Int]? + n = k + XCTAssertTrue(k.sameElements(l)) + XCTAssertFalse(l.sameElements(m)) + XCTAssertTrue((n?.sameElements(k))!) + } + +} diff --git a/mobile/ios/SharedTests/AsyncReducerTests.swift b/mobile/ios/SharedTests/AsyncReducerTests.swift new file mode 100644 index 0000000000..94cb1c728b --- /dev/null +++ b/mobile/ios/SharedTests/AsyncReducerTests.swift @@ -0,0 +1,167 @@ +/* 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 XCTest +import Deferred +@testable import Shared + +private let timeoutPeriod: TimeInterval = 600 + +class AsyncReducerTests: XCTestCase { + override func setUp() { + super.setUp() + // Put setup code here. This method is called before the invocation of each test method in the class. + } + + override func tearDown() { + // Put teardown code here. This method is called after the invocation of each test method in the class. + super.tearDown() + } + + func testSimpleBehaviour() { + let expectation = self.expectation(description: #function) + happyCase(expectation, combine: simpleAdder) + } + + func testWaitingFillerBehaviour() { + let expectation = self.expectation(description: #function) + happyCase(expectation, combine: waitingFillingAdder) + } + + func testWaitingFillerAppendingBehaviour() { + let expectation = self.expectation(description: #function) + appendingCase(expectation, combine: waitingFillingAdder) + } + + func testFailingCombine() { + let expectation = self.expectation(description: #function) + let combine = { (a: Int, b: Int) -> Deferred> in + if a >= 6 { + return deferMaybe(TestError()) + } + return deferMaybe(a + b) + } + let reducer = AsyncReducer(initialValue: 0, combine: combine) + reducer.terminal.upon { res in + XCTAssert(res.isFailure) + expectation.fulfill() + } + + self.append(reducer, items: 1, 2, 3, 4, 5) + waitForExpectations(timeout: timeoutPeriod, handler: nil) + } + + func testFailingAppend() { + let expectation = self.expectation(description: #function) + + let reducer = AsyncReducer(initialValue: 0, combine: simpleAdder) + reducer.terminal.upon { res in + XCTAssert(res.isSuccess) + XCTAssertEqual(res.successValue!, 15) + } + + self.append(reducer, items: 1, 2, 3, 4, 5) + + delay(0.1) { + do { + let _ = try reducer.append(6, 7, 8) + XCTFail("Can't append to a reducer that's already finished") + } catch let error { + XCTAssert(true, "Properly received error on finished reducer \(error)") + } + expectation.fulfill() + } + + waitForExpectations(timeout: timeoutPeriod, handler: nil) + } + + func testAccumulation() { + var addDuring: [String] = ["bar", "baz"] + var reducer: AsyncReducer<[String: Bool], String>! + + func combine(_ t: [String: Bool], u: String) -> Deferred> { + var out = t + out[u] = true + + // Pretend that some new work arrived while we were handling this. + if let nextUp = addDuring.popLast() { + let _ = try! reducer.append(nextUp) + } + + return deferMaybe(out) + } + + // Start with 'foo'. + reducer = AsyncReducer(initialValue: deferMaybe([:]), combine: combine) + let _ = try! reducer.append("foo") + + // Wait for the result. We should have handled all three by the time this returns. + let result = reducer.terminal.value + XCTAssertTrue(result.isSuccess) + XCTAssertEqual(["foo": true, "bar": true, "baz": true], result.successValue!) + } +} + +extension AsyncReducerTests { + func happyCase(_ expectation: XCTestExpectation, combine: @escaping (Int, Int) -> Deferred>) { + let reducer = AsyncReducer(initialValue: 0, combine: combine) + reducer.terminal.upon { res in + XCTAssert(res.isSuccess) + XCTAssertEqual(res.successValue!, 15) + expectation.fulfill() + } + + self.append(reducer, items: 1, 2, 3, 4, 5) + waitForExpectations(timeout: timeoutPeriod, handler: nil) + } + + func appendingCase(_ expectation: XCTestExpectation, combine: @escaping (Int, Int) -> Deferred>) { + let reducer = AsyncReducer(initialValue: 0, combine: combine) + reducer.terminal.upon { res in + XCTAssert(res.isSuccess) + XCTAssertEqual(res.successValue!, 15) + expectation.fulfill() + } + + self.append(reducer, items: 1, 2) + + delay(0.1) { + self.append(reducer, items: 3, 4, 5) + } + waitForExpectations(timeout: timeoutPeriod, handler: nil) + } + + func append(_ reducer: AsyncReducer, items: Int...) { + do { + let _ = try reducer.append(items) + } catch let error { + XCTFail("Append failed with \(error)") + } + } +} + +class TestError: MaybeErrorType { + var description = "Error" +} + +private let serialQueue = DispatchQueue(label: "com.mozilla.test.serial", attributes: []) +private let concurrentQueue = DispatchQueue(label: "com.mozilla.test.concurrent", attributes: DispatchQueue.Attributes.concurrent) + +func delay(_ delay: Double, closure:@escaping () -> Void) { + concurrentQueue.asyncAfter( + deadline: DispatchTime.now() + Double(Int64(delay * Double(NSEC_PER_SEC))) / Double(NSEC_PER_SEC), execute: closure) +} + +private func simpleAdder(_ a: Int, b: Int) -> Deferred> { + return deferMaybe(a + b) +} + +private func waitingFillingAdder(_ a: Int, b: Int) -> Deferred> { + let deferred = Deferred>() + delay(0.1) { + deferred.fill(Maybe(success: a + b)) + } + return deferred +} + diff --git a/mobile/ios/SharedTests/AuthenticationKeychainInfoTests.swift b/mobile/ios/SharedTests/AuthenticationKeychainInfoTests.swift new file mode 100644 index 0000000000..176dd613a7 --- /dev/null +++ b/mobile/ios/SharedTests/AuthenticationKeychainInfoTests.swift @@ -0,0 +1,45 @@ +/* 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 XCTest +import SwiftKeychainWrapper + +class AuthenticationKeychainInfoTests: XCTestCase { + + func testEncodingAndDecoding() { + let passcode = "1234" + let authInfo = AuthenticationKeychainInfo(passcode: passcode) + authInfo.updateRequiredPasscodeInterval(.fiveMinutes) + authInfo.recordValidation() + authInfo.recordFailedAttempt() // failed attempt should be 1 + authInfo.lockOutUser() //lock out a user so a lockoutInterval is set. + authInfo.useTouchID = true + + let savedInterval = authInfo.lockOutInterval + let savedValidation = authInfo.lastPasscodeValidationInterval + + KeychainWrapper.sharedAppContainerKeychain.setAuthenticationInfo(authInfo) //Save to disk + let decodedAuthInfo = KeychainWrapper.sharedAppContainerKeychain.authenticationInfo()! //Fetch from disk + + XCTAssertEqual(savedInterval, decodedAuthInfo.lockOutInterval) + XCTAssertEqual(passcode, decodedAuthInfo.passcode) + XCTAssertEqual(1, decodedAuthInfo.failedAttempts, "We performed a recordFailedAttempt. This should be 1.") + XCTAssertTrue(decodedAuthInfo.useTouchID) + XCTAssertEqual(savedValidation, decodedAuthInfo.lastPasscodeValidationInterval) + XCTAssertEqual(PasscodeInterval.fiveMinutes, decodedAuthInfo.requiredPasscodeInterval) + } + + func testNilIntervalsArentZero() { + let passcode = "1234" + let authInfo = AuthenticationKeychainInfo(passcode: passcode) + + KeychainWrapper.sharedAppContainerKeychain.setAuthenticationInfo(authInfo) //Save to disk + let decodedAuthInfo = KeychainWrapper.sharedAppContainerKeychain.authenticationInfo()! //Fetch from disk + + XCTAssertNil(decodedAuthInfo.lockOutInterval, "The lockoutInterval was never used. It should be nil") + } + +} diff --git a/mobile/ios/SharedTests/DeferredTests.swift b/mobile/ios/SharedTests/DeferredTests.swift new file mode 100644 index 0000000000..402735697c --- /dev/null +++ b/mobile/ios/SharedTests/DeferredTests.swift @@ -0,0 +1,127 @@ +/* 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/. */ + +@testable import Shared +import Deferred +import XCTest + +// Trivial test for using Deferred. + +class DeferredTests: XCTestCase { + func testDeferred() { + let d = Deferred() + XCTAssertNil(d.peek(), "Value not yet filled.") + + let expectation = self.expectation(description: "Waiting on value.") + d.upon({ x in + expectation.fulfill() + }) + + d.fill(5) + waitForExpectations(timeout: 10) { (error) in + XCTAssertNil(error, "\(error.debugDescription)") + } + + XCTAssertEqual(5, d.peek()!, "Value is filled.") + } + + func testMultipleUponBlocks() { + let e1 = self.expectation(description: "First.") + let e2 = self.expectation(description: "Second.") + let d = Deferred() + d.upon { x in + XCTAssertEqual(x, 5) + e1.fulfill() + } + d.upon { x in + XCTAssertEqual(x, 5) + e2.fulfill() + } + d.fill(5) + waitForExpectations(timeout: 10, handler: nil) + } + + func testOperators() { + let e1 = self.expectation(description: "First.") + let e2 = self.expectation(description: "Second.") + + let f1: () -> Deferred> = { + return deferMaybe(5) + } + + let f2: (_ x: Int) -> Deferred> = { + if $0 == 5 { + e1.fulfill() + } + return deferMaybe("Hello!") + } + + // Type signatures: + let combined: () -> Deferred> = { f1() >>== f2 } + let result: Deferred> = combined() + + result.upon { + XCTAssertEqual("Hello!", $0.successValue!) + e2.fulfill() + } + + waitForExpectations(timeout: 10, handler: nil) + } + + func testPassAccumulate() { + let leak = self.expectation(description: "deinit") + + class TestClass { + let end: XCTestExpectation + init(e: XCTestExpectation) { + end = e + accumulate([self.aSimpleFunction]).upon { _ in + + } + } + + func aSimpleFunction() -> Success { + return succeed() + } + deinit { + end.fulfill() + } + } + + var myclass: TestClass? = TestClass(e: leak) + myclass = nil + waitForExpectations(timeout: 3, handler: nil) + } + + + func testFailAccumulate() { + let leak = self.expectation(description: "deinit") + + class TestError: MaybeErrorType { + var description = "Error" + } + + class TestClass { + let end: XCTestExpectation + init(e: XCTestExpectation) { + end = e + accumulate([self.aSimpleFunction]).upon { _ in + + } + } + + func aSimpleFunction() -> Success { + return Deferred(value: Maybe(failure: TestError())) + } + deinit { + end.fulfill() + } + } + + var myclass: TestClass? = TestClass(e: leak) + myclass = nil + waitForExpectations(timeout: 3, handler: nil) + } + +} diff --git a/mobile/ios/SharedTests/FeatureSwitchTests.swift b/mobile/ios/SharedTests/FeatureSwitchTests.swift new file mode 100644 index 0000000000..d2cef34493 --- /dev/null +++ b/mobile/ios/SharedTests/FeatureSwitchTests.swift @@ -0,0 +1,147 @@ +/* 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/. */ + +@testable import Shared +import XCTest + +class FeatureSwitchTests: XCTestCase { + let buildChannel = AppConstants.BuildChannel + + func testPersistent() { + let featureSwitch = FeatureSwitch(named: "test-persistent-over-restarts", allowPercentage: 50, buildChannel: buildChannel) + let prefs = MockProfilePrefs() + var membership = featureSwitch.isMember(prefs) + var changed = 0 + for _ in 0..<100 { + if featureSwitch.isMember(prefs) != membership { + membership = !membership + changed += 1 + } + } + + XCTAssertEqual(changed, 0, "Users should get and keep the feature over restarts") + } + + func testConsistentWhenChangingPercentage() { + let featureID = "test-persistent-over-releases" + let prefs = MockProfilePrefs() + let guardFeatureSwitch = FeatureSwitch(named: featureID, allowPercentage: 50, buildChannel: buildChannel) + while guardFeatureSwitch.alwaysMembership(prefs) { + guardFeatureSwitch.resetMembership(prefs) + } + var membership = false + var changed = 0 + for percent in 0..<100 { + let featureSwitch = FeatureSwitch(named: featureID, allowPercentage: percent, buildChannel: buildChannel) + if featureSwitch.isMember(prefs) != membership { + membership = !membership + changed += 1 + } + } + + XCTAssertEqual(changed, 1, "Users should get and keep the feature if the feature is becoming successful") + } + + func testReallyConsistentWhenChangingPercentage() { + for _ in 0..<1000 { + testConsistentWhenChangingPercentage() + } + } + + func testUserEnabled() { + let prefs = MockProfilePrefs() + let featureSwitch = FeatureSwitch(named: "test-user-enabled", allowPercentage: 0, buildChannel: buildChannel) + XCTAssertFalse(featureSwitch.isMember(prefs), "The feature should be disabled") + featureSwitch.setMembership(true, for: prefs) // enable the feature + XCTAssertTrue(featureSwitch.isMember(prefs), "The feature should be enabled") + featureSwitch.setMembership(false, for: prefs) // disable the feature + XCTAssertFalse(featureSwitch.isMember(prefs), "The feature should be disabled again") + } + + func testForceDisabled() { + let prefs = MockProfilePrefs() + let featureSwitch = FeatureSwitch(named: "test-user-disabled", allowPercentage: 100, buildChannel: buildChannel) + XCTAssertTrue(featureSwitch.isMember(prefs), "The feature should be enabled") + featureSwitch.setMembership(false, for: prefs) // disable the feature + XCTAssertFalse(featureSwitch.isMember(prefs), "The feature should be disabled again") + } +} + +extension FeatureSwitchTests { + func test0Percent() { + let featureSwitch = FeatureSwitch(named: "test-never", allowPercentage: 0, buildChannel: buildChannel) + testExactly(featureSwitch, expected: 0) + testApprox(featureSwitch, expected: 0) + } + + func test100Percent() { + let featureSwitch = FeatureSwitch(named: "test-always", allowPercentage: 100, buildChannel: buildChannel) + testExactly(featureSwitch, expected: 100) + testApprox(featureSwitch, expected: 100) + } + + func test50Percent() { + let featureSwitch = FeatureSwitch(named: "test-half-the-population", allowPercentage: 50, buildChannel: buildChannel) + testApprox(featureSwitch, expected: 50) + } + + func test30Percent() { + let featureSwitch = FeatureSwitch(named: "test-30%-population", allowPercentage: 30, buildChannel: buildChannel) + testApprox(featureSwitch, expected: 30) + } + + func testPerformance() { + let featureSwitch = FeatureSwitch(named: "test-30%-population", allowPercentage: 30, buildChannel: buildChannel) + let prefs = MockProfilePrefs() + measure { + for _ in 0..<1000 { + _ = featureSwitch.isMember(prefs) + } + } + } + + func testAppConstantsWin() { + // simulate in release channel, but switched off in AppConstants. + let featureFlaggedOff = FeatureSwitch(named: "test-release-flagged-off", false, allowPercentage: 100, buildChannel: buildChannel) + testExactly(featureFlaggedOff, expected: 0) + + // simulate in non-release channel, but switched on in AppConstants. + let buildChannelAndFlaggedOn = FeatureSwitch(named: "test-flagged-on", true, allowPercentage: 0) + testExactly(buildChannelAndFlaggedOn, expected: 100) + + // simulate in non-release channel, but switched off in AppConstants. + let buildChannelAndFlaggedOff = FeatureSwitch(named: "test-flagged-off", false, allowPercentage: 100) + testExactly(buildChannelAndFlaggedOff, expected: 0) + } +} + +private extension FeatureSwitchTests { + func sampleN(_ featureSwitch: FeatureSwitch, testCount: Int = 1000) -> Int { + var count = 0 + for _ in 0.. + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + BNDL + CFBundleShortVersionString + 10.6 + CFBundleSignature + ???? + CFBundleVersion + 1 + + diff --git a/mobile/ios/SharedTests/NSMutableAttributedStringExtensionsTests.swift b/mobile/ios/SharedTests/NSMutableAttributedStringExtensionsTests.swift new file mode 100644 index 0000000000..dad2b16f6d --- /dev/null +++ b/mobile/ios/SharedTests/NSMutableAttributedStringExtensionsTests.swift @@ -0,0 +1,49 @@ +/* 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 UIKit +import XCTest +@testable import Shared + +class NSMutableAttributedStringExtensionsTests: XCTestCase { + fileprivate func checkCharacterAtPosition(_ position: Int, isColored color: UIColor, inString string: NSAttributedString) -> Bool { + let attributes = string.attributes(at: position, effectiveRange: nil) + if let foregroundColor = attributes[NSForegroundColorAttributeName] as? UIColor { + if foregroundColor == color { + return true + } + } + return false + } + + func testColorsSubstring() { + let substring = "bc" + let example = NSMutableAttributedString(string: "abcd") + example.colorSubstring(substring, withColor: UIColor.red) + + XCTAssertFalse(checkCharacterAtPosition(0, isColored: UIColor.red, inString: example)) + for position in 1..<3 { + XCTAssertTrue(checkCharacterAtPosition(position, isColored: UIColor.red, inString: example)) + } + XCTAssertFalse(checkCharacterAtPosition(3, isColored: UIColor.red, inString: example)) + } + + func testDoesNothingWithEmptySubstring() { + let substring = "" + let example = NSMutableAttributedString(string: "abcd") + example.colorSubstring(substring, withColor: UIColor.red) + for position in 0.. 0 else { + print("\(url) doesn't match as a URL") + continue + } + + let modified = url.absoluteDisplayExternalString + XCTAssertNotEqual(original, modified) + + let newMatches = detector.matches(in: modified, options: [], range: NSMakeRange(0, modified.count)) + + XCTAssertEqual(0, newMatches.count, "\(modified) is not a valid URL") + } + + } + +} diff --git a/mobile/ios/SharedTests/ResultTests.swift b/mobile/ios/SharedTests/ResultTests.swift new file mode 100644 index 0000000000..508d1164e1 --- /dev/null +++ b/mobile/ios/SharedTests/ResultTests.swift @@ -0,0 +1,18 @@ +/* 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 Shared +import XCTest + +// Trivial test for using Result. +class ResultTests: XCTestCase { + func testResult() { + let r = Maybe(success: 5) + if let i = r.successValue { + XCTAssertEqual(5, i) + } else { + XCTFail("Expected success.") + } + } +} diff --git a/mobile/ios/SharedTests/RollingFileLoggerTests.swift b/mobile/ios/SharedTests/RollingFileLoggerTests.swift new file mode 100644 index 0000000000..cec27a414b --- /dev/null +++ b/mobile/ios/SharedTests/RollingFileLoggerTests.swift @@ -0,0 +1,119 @@ +/* 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 XCTest +import XCGLogger +@testable import Shared + +class RollingFileLoggerTests: XCTestCase { + var logger: RollingFileLogger! + var logDir: String = "" + var sizeLimit: Int = 5000 + + fileprivate lazy var formatter: DateFormatter = { + let formatter = DateFormatter() + formatter.dateFormat = "yyyyMMdd'T'HHmmssZ" + return formatter + }() + + override func setUp() { + super.setUp() + logDir = (NSSearchPathForDirectoriesInDomains(.cachesDirectory, .userDomainMask, true).first!) + "/Logs" + do { + try FileManager.default.createDirectory(atPath: logDir, withIntermediateDirectories: false, attributes: nil) + } catch _ { + } + logger = RollingFileLogger(filenameRoot: "test", logDirectoryPath: logDir, sizeLimit: Int64(sizeLimit)) + } + + func testNewLogCreatesLogFileWithTimestamp() { + let date = Date() + let expected = "test.\(formatter.string(from: date)).log" + let expectedPath = "\(logDir)/\(expected)" + logger.newLogWithDate(date) + XCTAssertTrue(FileManager.default.fileExists(atPath: expectedPath), "Log file should exist") + + let testMessage = "Logging some text" + logger.info(testMessage) + let logData = try? Data(contentsOf: URL(fileURLWithPath: expectedPath)) + XCTAssertNotNil(logData, "Log data should not be nil") + let logString = NSString(data: logData!, encoding: String.Encoding.utf8.rawValue) + XCTAssertTrue(logString!.contains(testMessage), "Log should contain our test message that we wrote") + } + + func testNewLogDeletesPreviousLogIfItsTooLarge() { + let manager = FileManager.default + let dirURL = URL(fileURLWithPath: logDir) + let prefix = "test" + let expectedPath = createNewLogFileWithSize(sizeLimit + 1) + + let directorySize = try! manager.getAllocatedSizeOfDirectoryAtURL(dirURL, forFilesPrefixedWith: prefix) + + // Pre-condition: Folder needs to be larger than the size limit + XCTAssertGreaterThan(directorySize, Int64(sizeLimit), "Log folder should be larger than size limit") + + let exceedsSmaller = try! manager.allocatedSizeOfDirectoryAtURL(dirURL, forFilesPrefixedWith: prefix, isLargerThanBytes: directorySize - 1) + let doesNotExceedLarger = try! manager.allocatedSizeOfDirectoryAtURL(dirURL, forFilesPrefixedWith: prefix, isLargerThanBytes: Int64(sizeLimit + 2)) + XCTAssertTrue(exceedsSmaller) + XCTAssertTrue(doesNotExceedLarger) + + let newDate = Date().addingTimeInterval(60*60) // Create a log file using a date an hour ahead + let newExpected = "\(prefix).\(formatter.string(from: newDate)).log" + let newExpectedPath = "\(logDir)/\(newExpected)" + logger.newLogWithDate(newDate) + + XCTAssertTrue(manager.fileExists(atPath: newExpectedPath), "New log file should exist") + XCTAssertTrue(manager.fileExists(atPath: expectedPath), "Old log file exists until pruned") + logger.deleteOldLogsDownToSizeLimit() + XCTAssertFalse(manager.fileExists(atPath: expectedPath), "Old log file should NOT exist") + } + + func testNewLogDeletesOldestLogFileToMakeRoomForNewFile() { + let manager = FileManager.default + let dirURL = URL(fileURLWithPath: logDir) + let prefix = "test" + + // Create 5 log files with spread out over 5 hours and reorder paths so oldest is first + let logFilePaths = [0, 1, 2, 3, 4].map { self.createNewLogFileWithSize(200, withDate: Date().addingTimeInterval(60 * 60 * $0)) } + .sorted { $0 < $1 } + + let directorySize = try! manager.getAllocatedSizeOfDirectoryAtURL(dirURL, forFilesPrefixedWith: prefix) + + // Pre-condition: Folder needs to be larger than the size limit + XCTAssertGreaterThan(directorySize, Int64(sizeLimit), "Log folder should be larger than size limit") + + let newDate = Date().addingTimeInterval(60*60*5) // Create a log file using a date an hour ahead + let newExpected = "\(prefix).\(formatter.string(from: newDate)).log" + let newExpectedPath = "\(logDir)/\(newExpected)" + logger.newLogWithDate(newDate) + + XCTAssertTrue(manager.fileExists(atPath: newExpectedPath), "New log file should exist") + XCTAssertTrue(manager.fileExists(atPath: logFilePaths.first!), "Old log file exists until pruned") + logger.deleteOldLogsDownToSizeLimit() + XCTAssertFalse(manager.fileExists(atPath: logFilePaths.first!), "Oldest log file should NOT exist") + } + + /** + Create a log file using the test logger and returns the path to that log file + + - parameter size: Size to make the log file + + - returns: Path to log file + */ + fileprivate func createNewLogFileWithSize(_ size: Int, withDate date: Date = Date()) -> String { + let expected = "test.\(formatter.string(from: date)).log" + let expectedPath = "\(logDir)/\(expected)" + logger.newLogWithDate(date) + XCTAssertTrue(FileManager.default.fileExists(atPath: expectedPath), "Log file should exist") + + let logFileHandle = FileHandle(forWritingAtPath: expectedPath) + XCTAssertNotNil(logFileHandle, "File should exist") + let garbageBytes = malloc(size) + let blankData = Data(bytes: garbageBytes!, count: size) + logFileHandle!.write(blankData) + logFileHandle!.closeFile() + return expectedPath + } +} diff --git a/mobile/ios/SharedTests/SupportUtilsTests.swift b/mobile/ios/SharedTests/SupportUtilsTests.swift new file mode 100644 index 0000000000..c56472acd6 --- /dev/null +++ b/mobile/ios/SharedTests/SupportUtilsTests.swift @@ -0,0 +1,17 @@ +/* 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 XCTest + +class SupportUtilsTests: XCTestCase { + func testURLForTopic() { + let appVersion = AppInfo.appVersion + let languageIdentifier = Locale.preferredLanguages.first! + XCTAssertEqual(SupportUtils.URLForTopic("Bacon")?.absoluteString, "https://support.mozilla.org/1/mobile/\(appVersion)/iOS/\(languageIdentifier)/Bacon") + XCTAssertEqual(SupportUtils.URLForTopic("Cheese & Crackers")?.absoluteString, "https://support.mozilla.org/1/mobile/\(appVersion)/iOS/\(languageIdentifier)/Cheese%20&%20Crackers") + XCTAssertEqual(SupportUtils.URLForTopic("Möbelträgerfüße")?.absoluteString, "https://support.mozilla.org/1/mobile/\(appVersion)/iOS/\(languageIdentifier)/M%C3%B6beltr%C3%A4gerf%C3%BC%C3%9Fe") + } +} diff --git a/mobile/ios/SharedTests/UtilsTests.swift b/mobile/ios/SharedTests/UtilsTests.swift new file mode 100644 index 0000000000..054e2cb630 --- /dev/null +++ b/mobile/ios/SharedTests/UtilsTests.swift @@ -0,0 +1,108 @@ +/* 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 XCTest + +/** + * Test for our own utils. + */ +class UtilsTests: XCTestCase { + func testMapUtils() { + let m: [String: Int] = ["foo": 123, "bar": 456] + let f: (Int) -> Int? = { v in + return (v > 200) ? 999 : nil + } + + let o = mapValues(m, f: f) + XCTAssertEqual(2, o.count) + XCTAssertTrue(o["foo"]! == nil) + XCTAssertTrue(o["bar"]! == 999) + + let filtered = optFilter(o) + XCTAssertEqual(1, filtered.count) + XCTAssertTrue(filtered["bar"] == 999) + } + + func testOptFilter() { + let a: [Int?] = [nil, 1, nil, 2, 3, 4] + let b = optFilter(a) + XCTAssertEqual(4, b.count) + XCTAssertEqual([1, 2, 3, 4], b) + } + + func testOptArrayEqual() { + let x: [String] = ["a", "b", "c"] + let y: [String]? = ["a", "b", "c"] + let z: [String]? = nil + + XCTAssertTrue(optArrayEqual(x, rhs: y)) + XCTAssertTrue(optArrayEqual(x, rhs: x)) + XCTAssertTrue(optArrayEqual(y, rhs: y)) + XCTAssertTrue(optArrayEqual(z, rhs: z)) + XCTAssertFalse(optArrayEqual(x, rhs: z)) + XCTAssertFalse(optArrayEqual(z, rhs: y)) + } + + func testChunk() { + let examples: [([Int], Int, [[Int]])] = [ + ([], 2, []), + ([1, 2], 0, [[1], [2]]), + ([1, 2], 1, [[1], [2]]), + ([1, 2, 3], 2, [[1, 2], [3]]), + ([1, 2], 3, [[1, 2]]), + ([1, 2, 3], 1, [[1], [2], [3]]), + ] + for (arr, by, expected) in examples { + // Turn the ArraySlices back into Arrays for comparison. + let actual = chunk(arr as [Int], by: by).map { Array($0) } + XCTAssertEqual(expected as NSArray, actual as NSArray) //wtf. why is XCTAssert being so weeird + } + } + + func testChunkCollection() { + let examples: [([Int], Int, [[Int]])] = [ + ([], 2, []), + ([1, 2], 0, [[1], [2]]), + ([1, 2], 1, [[1], [2]]), + ([1, 2, 3], 2, [[1, 2], [3]]), + ([1, 2], 3, [[1, 2]]), + ([1, 2, 3], 1, [[1], [2], [3]]), + ] + for (arr, by, expected) in examples { + let actual = chunkCollection(arr, by: by) { xs in [xs] } + XCTAssertEqual(expected as NSArray, actual as NSArray) + } + } + + func testParseTimestamps() { + let millis = "1492316843992" // Firefox for iOS produced millisecond timestamps. Oops. + let decimal = "1492316843.99" + let truncated = "1492316843" + let huge = "1844674407370955161512" + + XCTAssertNil(decimalSecondsStringToTimestamp("")) + XCTAssertNil(decimalSecondsStringToTimestamp(huge)) + XCTAssertNil(decimalSecondsStringToTimestamp("foo")) + + XCTAssertNil(someKindOfTimestampStringToTimestamp("")) + XCTAssertNil(someKindOfTimestampStringToTimestamp(huge)) + XCTAssertNil(someKindOfTimestampStringToTimestamp("foo")) + + let ts1: Timestamp = 1492316843990 + XCTAssertEqual(decimalSecondsStringToTimestamp(decimal) ?? 0, ts1) + XCTAssertEqual(someKindOfTimestampStringToTimestamp(decimal) ?? 0, ts1) + + let ts2: Timestamp = 1492316843000 + XCTAssertEqual(decimalSecondsStringToTimestamp(truncated) ?? 0, ts2) + XCTAssertEqual(someKindOfTimestampStringToTimestamp(truncated) ?? 0, ts2) + + let ts3: Timestamp = 1492316843992000 + XCTAssertEqual(decimalSecondsStringToTimestamp(millis) ?? 0, ts3) // Oops. + + let ts4: Timestamp = 1492316843992 + XCTAssertEqual(someKindOfTimestampStringToTimestamp(millis) ?? 0, ts4) + } +} diff --git a/mobile/ios/Storage/Bookmarks/Bookmarks.swift b/mobile/ios/Storage/Bookmarks/Bookmarks.swift new file mode 100644 index 0000000000..5bc6f45047 --- /dev/null +++ b/mobile/ios/Storage/Bookmarks/Bookmarks.swift @@ -0,0 +1,529 @@ +/* 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 UIKit +import Shared +import Deferred +import SwiftyJSON + +private let log = Logger.syncLogger + +public protocol SearchableBookmarks: class { + func bookmarksByURL(_ url: URL) -> Deferred>> +} + +public protocol SyncableBookmarks: class, ResettableSyncStorage, AccountRemovalDelegate { + // TODO + func isUnchanged() -> Deferred> + func getLocalBookmarksModifications(limit: Int) -> Deferred> + func getLocalDeletions() -> Deferred> + func treesForEdges() -> Deferred> + func treeForMirror() -> Deferred> + func applyLocalOverrideCompletionOp(_ op: LocalOverrideCompletionOp, itemSources: ItemSources) -> Success + func applyBufferUpdatedCompletionOp(_ op: BufferUpdatedCompletionOp) -> Success +} + +public let NotificationBookmarkBufferValidated = Notification.Name("NotificationBookmarkBufferValidated") + +public protocol BookmarkBufferStorage: class { + func isEmpty() -> Deferred> + func applyRecords(_ records: [BookmarkMirrorItem]) -> Success + func doneApplyingRecordsAfterDownload() -> Success + + func validate() -> Success + func getBufferedDeletions() -> Deferred> + func applyBufferCompletionOp(_ op: BufferCompletionOp, itemSources: ItemSources) -> Success + + // Only use for diagnostics. + func synchronousBufferCount() -> Int? + func getUpstreamRecordCount() -> Deferred +} + +public protocol MirrorItemSource: class { + func getMirrorItemWithGUID(_ guid: GUID) -> Deferred> + func getMirrorItemsWithGUIDs(_ guids: T) -> Deferred> where T.Iterator.Element == GUID + func prefetchMirrorItemsWithGUIDs(_ guids: T) -> Success where T.Iterator.Element == GUID +} + +public protocol BufferItemSource: class { + func getBufferItemWithGUID(_ guid: GUID) -> Deferred> + func getBufferItemsWithGUIDs(_ guids: T) -> Deferred> where T.Iterator.Element == GUID + func getBufferChildrenGUIDsForParent(_ guid: GUID) -> Deferred> + func prefetchBufferItemsWithGUIDs(_ guids: T) -> Success where T.Iterator.Element == GUID +} + +public protocol LocalItemSource: class { + func getLocalItemWithGUID(_ guid: GUID) -> Deferred> + func getLocalItemsWithGUIDs(_ guids: T) -> Deferred> where T.Iterator.Element == GUID + func prefetchLocalItemsWithGUIDs(_ guids: T) -> Success where T.Iterator.Element == GUID +} + +open class ItemSources { + open let local: LocalItemSource + open let mirror: MirrorItemSource + open let buffer: BufferItemSource + + public init(local: LocalItemSource, mirror: MirrorItemSource, buffer: BufferItemSource) { + self.local = local + self.mirror = mirror + self.buffer = buffer + } + + open func prefetchWithGUIDs(_ guids: T) -> Success where T.Iterator.Element == GUID { + return self.local.prefetchLocalItemsWithGUIDs(guids) + >>> { self.mirror.prefetchMirrorItemsWithGUIDs(guids) } + >>> { self.buffer.prefetchBufferItemsWithGUIDs(guids) } + } +} + +public struct BookmarkRoots { + // These match Places on desktop. + public static let RootGUID = "root________" + public static let MobileFolderGUID = "mobile______" + public static let MenuFolderGUID = "menu________" + public static let ToolbarFolderGUID = "toolbar_____" + public static let UnfiledFolderGUID = "unfiled_____" + + public static let FakeDesktopFolderGUID = "desktop_____" // Pseudo. Never mentioned in a real record. + + // This is the order we use. + public static let RootChildren: [GUID] = [ + BookmarkRoots.MenuFolderGUID, + BookmarkRoots.ToolbarFolderGUID, + BookmarkRoots.UnfiledFolderGUID, + BookmarkRoots.MobileFolderGUID, + ] + + public static let DesktopRoots: [GUID] = [ + BookmarkRoots.MenuFolderGUID, + BookmarkRoots.ToolbarFolderGUID, + BookmarkRoots.UnfiledFolderGUID, + ] + + public static let Real = Set([ + BookmarkRoots.RootGUID, + BookmarkRoots.MobileFolderGUID, + BookmarkRoots.MenuFolderGUID, + BookmarkRoots.ToolbarFolderGUID, + BookmarkRoots.UnfiledFolderGUID, + ]) + + public static let All = Set([ + BookmarkRoots.RootGUID, + BookmarkRoots.MobileFolderGUID, + BookmarkRoots.MenuFolderGUID, + BookmarkRoots.ToolbarFolderGUID, + BookmarkRoots.UnfiledFolderGUID, + BookmarkRoots.FakeDesktopFolderGUID, + ]) + + /** + * Sync records are a horrible mess of Places-native GUIDs and Sync-native IDs. + * For example: + * {"id":"places", + * "type":"folder", + * "title":"", + * "description":null, + * "children":["menu________","toolbar_____", + * "tags________","unfiled_____", + * "jKnyPDrBQSDg","T6XK5oJMU8ih"], + * "parentid":"2hYxKgBwvkEH"}" + * + * We thus normalize on the extended Places IDs (with underscores) for + * local storage, and translate to the Sync IDs when creating an outbound + * record. + * We translate the record's ID and also its parent. Evidence suggests that + * we don't need to translate children IDs. + * + * TODO: We don't create outbound records yet, so that's why there's no + * translation in that direction yet! + */ + public static func translateIncomingRootGUID(_ guid: GUID) -> GUID { + return [ + "places": RootGUID, + "root": RootGUID, + "mobile": MobileFolderGUID, + "menu": MenuFolderGUID, + "toolbar": ToolbarFolderGUID, + "unfiled": UnfiledFolderGUID + ][guid] ?? guid + } + + public static func translateOutgoingRootGUID(_ guid: GUID) -> GUID { + return [ + RootGUID: "places", + MobileFolderGUID: "mobile", + MenuFolderGUID: "menu", + ToolbarFolderGUID: "toolbar", + UnfiledFolderGUID: "unfiled" + ][guid] ?? guid + } + + /* + public static let TagsFolderGUID = "tags________" + public static let PinnedFolderGUID = "pinned______" + */ + + static let RootID = 0 + static let MobileID = 1 + static let MenuID = 2 + static let ToolbarID = 3 + static let UnfiledID = 4 +} + +/** + * This partly matches Places's nsINavBookmarksService, just for sanity. + * + * It is further extended to support the types that exist in Sync, so we can use + * this to store mirrored rows. + * + * These are only used at the DB layer. + */ +public enum BookmarkNodeType: Int { + case bookmark = 1 + case folder = 2 + case separator = 3 + case dynamicContainer = 4 + + case livemark = 5 + case query = 6 + + // No microsummary: those turn into bookmarks. +} + +public func == (lhs: BookmarkMirrorItem, rhs: BookmarkMirrorItem) -> Bool { + if lhs.type != rhs.type || + lhs.guid != rhs.guid || + lhs.dateAdded != rhs.dateAdded || + lhs.serverModified != rhs.serverModified || + lhs.isDeleted != rhs.isDeleted || + lhs.hasDupe != rhs.hasDupe || + lhs.pos != rhs.pos || + lhs.faviconID != rhs.faviconID || + lhs.localModified != rhs.localModified || + lhs.parentID != rhs.parentID || + lhs.parentName != rhs.parentName || + lhs.feedURI != rhs.feedURI || + lhs.siteURI != rhs.siteURI || + lhs.title != rhs.title || + lhs.description != rhs.description || + lhs.bookmarkURI != rhs.bookmarkURI || + lhs.tags != rhs.tags || + lhs.keyword != rhs.keyword || + lhs.folderName != rhs.folderName || + lhs.queryID != rhs.queryID { + return false + } + + if let lhsChildren = lhs.children, let rhsChildren = rhs.children { + return lhsChildren == rhsChildren + } + return lhs.children == nil && rhs.children == nil +} + +public struct BookmarkMirrorItem: Equatable { + public let guid: GUID + public let type: BookmarkNodeType + public let dateAdded: Timestamp? + public var serverModified: Timestamp + public let isDeleted: Bool + public let hasDupe: Bool + public let parentID: GUID? + public let parentName: String? + + // Livemarks. + public let feedURI: String? + public let siteURI: String? + + // Separators. + let pos: Int? + + // Folders, livemarks, bookmarks and queries. + public let title: String? + let description: String? + + // Bookmarks and queries. + let bookmarkURI: String? + let tags: String? + let keyword: String? + + // Queries. + let folderName: String? + let queryID: String? + + // Folders. + public let children: [GUID]? + + // Internal stuff. + let faviconID: Int? + public let localModified: Timestamp? + let syncStatus: SyncStatus? + + public func copyWithDateAdded(_ dateAdded: Timestamp) -> BookmarkMirrorItem { + return BookmarkMirrorItem( + guid: self.guid, + type: self.type, + dateAdded: dateAdded, + serverModified: self.serverModified, + isDeleted: self.isDeleted, + hasDupe: self.hasDupe, + parentID: self.parentID, + parentName: self.parentName, + feedURI: self.feedURI, + siteURI: self.siteURI, + pos: self.pos, + title: self.title, + description: self.description, + bookmarkURI: self.bookmarkURI, + tags: self.tags, + keyword: self.keyword, + folderName: self.folderName, + queryID: self.queryID, + children: self.children, + faviconID: self.faviconID, + localModified: self.localModified, + syncStatus: self.syncStatus) + } + + public func copyWithParentID(_ parentID: GUID, parentName: String?) -> BookmarkMirrorItem { + return BookmarkMirrorItem( + guid: self.guid, + type: self.type, + dateAdded: self.dateAdded, + serverModified: self.serverModified, + isDeleted: self.isDeleted, + hasDupe: self.hasDupe, + parentID: parentID, + parentName: parentName, + feedURI: self.feedURI, + siteURI: self.siteURI, + pos: self.pos, + title: self.title, + description: self.description, + bookmarkURI: self.bookmarkURI, + tags: self.tags, + keyword: self.keyword, + folderName: self.folderName, + queryID: self.queryID, + children: self.children, + faviconID: self.faviconID, + localModified: self.localModified, + syncStatus: self.syncStatus) + } + + // Ignores internal metadata and GUID; a pure value comparison. + // Does compare child GUIDs! + public func sameAs(_ rhs: BookmarkMirrorItem) -> Bool { + if self.type != rhs.type || + self.dateAdded != rhs.dateAdded || + self.isDeleted != rhs.isDeleted || + self.pos != rhs.pos || + self.parentID != rhs.parentID || + self.parentName != rhs.parentName || + self.feedURI != rhs.feedURI || + self.siteURI != rhs.siteURI || + self.title != rhs.title || + (self.description ?? "") != (rhs.description ?? "") || + self.bookmarkURI != rhs.bookmarkURI || + self.tags != rhs.tags || + self.keyword != rhs.keyword || + self.folderName != rhs.folderName || + self.queryID != rhs.queryID { + return false + } + + if let lhsChildren = self.children, let rhsChildren = rhs.children { + return lhsChildren == rhsChildren + } + return self.children == nil && rhs.children == nil + } + + public func asJSON() -> JSON { + return self.asJSONWithChildren(self.children) + } + + public func asJSONWithChildren(_ children: [GUID]?) -> JSON { + var out: [String: Any] = [:] + + out["id"] = BookmarkRoots.translateOutgoingRootGUID(self.guid) + + func take(_ key: String, _ val: String?) { + guard let val = val else { + return + } + out[key] = val + } + + if self.isDeleted { + out["deleted"] = true + return JSON(out) + } + + out["dateAdded"] = self.dateAdded + out["hasDupe"] = self.hasDupe + + // TODO: this should never be nil! + if let parentID = self.parentID { + out["parentid"] = BookmarkRoots.translateOutgoingRootGUID(parentID) + take("parentName", titleForSpecialGUID(parentID) ?? self.parentName ?? "") + } + + func takeBookmarkFields() { + take("title", self.title) + take("bmkUri", self.bookmarkURI) + take("description", self.description) + if let tags = self.tags { + let tagsJSON = JSON(parseJSON: tags) + if let tagsArray = tagsJSON.array, tagsArray.every({ $0.type == SwiftyJSON.Type.string }) { + out["tags"] = tagsArray + } else { + out["tags"] = [] + } + } else { + out["tags"] = [] + } + take("keyword", self.keyword) + } + + func takeFolderFields() { + take("title", titleForSpecialGUID(self.guid) ?? self.title) + take("description", self.description) + if let children = children { + if BookmarkRoots.RootGUID == self.guid { + // Only the root contains roots, and so only its children + // need to be translated. + out["children"] = children.map(BookmarkRoots.translateOutgoingRootGUID) + } else { + out["children"] = children + } + } + } + + switch self.type { + + case .query: + out["type"] = "query" + take("folderName", self.folderName) + take("queryId", self.queryID) + takeBookmarkFields() + + case .bookmark: + out["type"] = "bookmark" + takeBookmarkFields() + + case .livemark: + out["type"] = "livemark" + take("siteUri", self.siteURI) + take("feedUri", self.feedURI) + takeFolderFields() + + case .folder: + out["type"] = "folder" + takeFolderFields() + + case .separator: + out["type"] = "separator" + if let pos = self.pos { + out["pos"] = pos + } + + case .dynamicContainer: + // Sigh. + preconditionFailure("DynamicContainer not supported.") + } + + return JSON(out) + } + + // The places root is a folder but has no parentName. + public static func folder(_ guid: GUID, dateAdded: Timestamp?, modified: Timestamp, hasDupe: Bool, parentID: GUID, parentName: String?, title: String, description: String?, children: [GUID]) -> BookmarkMirrorItem { + let id = BookmarkRoots.translateIncomingRootGUID(guid) + let parent = BookmarkRoots.translateIncomingRootGUID(parentID) + + return BookmarkMirrorItem(guid: id, type: .folder, dateAdded: dateAdded, serverModified: modified, + isDeleted: false, hasDupe: hasDupe, parentID: parent, parentName: parentName, + feedURI: nil, siteURI: nil, + pos: nil, + title: title, description: description, + bookmarkURI: nil, tags: nil, keyword: nil, + folderName: nil, queryID: nil, + children: children, + faviconID: nil, localModified: nil, syncStatus: nil) + } + + public static func livemark(_ guid: GUID, dateAdded: Timestamp?, modified: Timestamp, hasDupe: Bool, parentID: GUID, parentName: String?, title: String?, description: String?, feedURI: String, siteURI: String) -> BookmarkMirrorItem { + let id = BookmarkRoots.translateIncomingRootGUID(guid) + let parent = BookmarkRoots.translateIncomingRootGUID(parentID) + + return BookmarkMirrorItem(guid: id, type: .livemark, dateAdded: dateAdded, serverModified: modified, + isDeleted: false, hasDupe: hasDupe, parentID: parent, parentName: parentName, + feedURI: feedURI, siteURI: siteURI, + pos: nil, + title: title, description: description, + bookmarkURI: nil, tags: nil, keyword: nil, + folderName: nil, queryID: nil, + children: nil, + faviconID: nil, localModified: nil, syncStatus: nil) + } + + public static func separator(_ guid: GUID, dateAdded: Timestamp?, modified: Timestamp, hasDupe: Bool, parentID: GUID, parentName: String?, pos: Int) -> BookmarkMirrorItem { + let id = BookmarkRoots.translateIncomingRootGUID(guid) + let parent = BookmarkRoots.translateIncomingRootGUID(parentID) + + return BookmarkMirrorItem(guid: id, type: .separator, dateAdded: dateAdded, serverModified: modified, + isDeleted: false, hasDupe: hasDupe, parentID: parent, parentName: parentName, + feedURI: nil, siteURI: nil, + pos: pos, + title: nil, description: nil, + bookmarkURI: nil, tags: nil, keyword: nil, + folderName: nil, queryID: nil, + children: nil, + faviconID: nil, localModified: nil, syncStatus: nil) + } + + public static func bookmark(_ guid: GUID, dateAdded: Timestamp?, modified: Timestamp, hasDupe: Bool, parentID: GUID, parentName: String?, title: String, description: String?, URI: String, tags: String, keyword: String?) -> BookmarkMirrorItem { + let id = BookmarkRoots.translateIncomingRootGUID(guid) + let parent = BookmarkRoots.translateIncomingRootGUID(parentID) + + return BookmarkMirrorItem(guid: id, type: .bookmark, dateAdded: dateAdded, serverModified: modified, + isDeleted: false, hasDupe: hasDupe, parentID: parent, parentName: parentName, + feedURI: nil, siteURI: nil, + pos: nil, + title: title, description: description, + bookmarkURI: URI, tags: tags, keyword: keyword, + folderName: nil, queryID: nil, + children: nil, + faviconID: nil, localModified: nil, syncStatus: nil) + } + + public static func query(_ guid: GUID, dateAdded: Timestamp?, modified: Timestamp, hasDupe: Bool, parentID: GUID, parentName: String?, title: String, description: String?, URI: String, tags: String, keyword: String?, folderName: String?, queryID: String?) -> BookmarkMirrorItem { + let id = BookmarkRoots.translateIncomingRootGUID(guid) + let parent = BookmarkRoots.translateIncomingRootGUID(parentID) + + return BookmarkMirrorItem(guid: id, type: .query, dateAdded: dateAdded, serverModified: modified, + isDeleted: false, hasDupe: hasDupe, parentID: parent, parentName: parentName, + feedURI: nil, siteURI: nil, + pos: nil, + title: title, description: description, + bookmarkURI: URI, tags: tags, keyword: keyword, + folderName: folderName, queryID: queryID, + children: nil, + faviconID: nil, localModified: nil, syncStatus: nil) + } + + public static func deleted(_ type: BookmarkNodeType, guid: GUID, modified: Timestamp) -> BookmarkMirrorItem { + let id = BookmarkRoots.translateIncomingRootGUID(guid) + + return BookmarkMirrorItem(guid: id, type: type, dateAdded: nil, serverModified: modified, + isDeleted: true, hasDupe: false, parentID: nil, parentName: nil, + feedURI: nil, siteURI: nil, + pos: nil, + title: nil, description: nil, + bookmarkURI: nil, tags: nil, keyword: nil, + folderName: nil, queryID: nil, + children: nil, + faviconID: nil, localModified: nil, syncStatus: nil) + } +} diff --git a/mobile/ios/Storage/Bookmarks/BookmarksModel.swift b/mobile/ios/Storage/Bookmarks/BookmarksModel.swift new file mode 100644 index 0000000000..179e230011 --- /dev/null +++ b/mobile/ios/Storage/Bookmarks/BookmarksModel.swift @@ -0,0 +1,417 @@ +/* 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 Deferred +import Foundation +import Shared + +private let log = Logger.syncLogger + +/** + * The kinda-immutable base interface for bookmarks and folders. + */ +open class BookmarkNode { + open var id: Int? + open let guid: GUID + open let title: String + open let isEditable: Bool + open var favicon: Favicon? + + init(guid: GUID, title: String, isEditable: Bool=false) { + self.guid = guid + self.title = title + self.isEditable = isEditable + } + + open var canDelete: Bool { + return self.isEditable + } +} + +open class BookmarkSeparator: BookmarkNode { + init(guid: GUID) { + super.init(guid: guid, title: "—") + } +} + +/** + * An immutable item representing a bookmark. + * + * To modify this, issue changes against the backing store and get an updated model. + */ +open class BookmarkItem: BookmarkNode { + open let url: String! + + public init(guid: String, title: String, url: String, isEditable: Bool=false) { + self.url = url + super.init(guid: guid, title: title, isEditable: isEditable) + } +} + +/** + * A folder is an immutable abstraction over a named + * thing that can return its child nodes by index. + */ +open class BookmarkFolder: BookmarkNode { + open var count: Int { return 0 } + open subscript(index: Int) -> BookmarkNode? { return nil } + + open func itemIsEditableAtIndex(_ index: Int) -> Bool { + return self[index]?.canDelete ?? false + } + + override open var canDelete: Bool { + return false + } + + open func removeItemWithGUID(_ guid: GUID) -> BookmarkFolder? { + return nil + } +} + +/** + * A model is a snapshot of the bookmarks store, suitable for backing a table view. + * + * Navigation through the folder hierarchy produces a sequence of models. + * + * Changes to the backing store implicitly invalidates a subset of models. + * + * 'Refresh' means requesting a new model from the store. + */ +open class BookmarksModel: BookmarksModelFactorySource { + fileprivate let factory: BookmarksModelFactory + open let modelFactory: Deferred> + open let current: BookmarkFolder + + public init(modelFactory: BookmarksModelFactory, root: BookmarkFolder) { + self.factory = modelFactory + self.modelFactory = deferMaybe(modelFactory) + self.current = root + } + + /** + * Produce a new model rooted at the appropriate folder. Fails if the folder doesn't exist. + */ + open func selectFolder(_ folder: BookmarkFolder) -> Deferred> { + return self.factory.modelForFolder(folder) + } + + /** + * Produce a new model rooted at the appropriate folder. Fails if the folder doesn't exist. + */ + open func selectFolder(_ guid: String) -> Deferred> { + return self.factory.modelForFolder(guid) + } + + /** + * Produce a new model rooted at the base of the hierarchy. Should never fail. + */ + open func selectRoot() -> Deferred> { + return self.factory.modelForRoot() + } + + /** + * Produce a new model with a memory-backed root with the given GUID removed from the current folder + */ + open func removeGUIDFromCurrent(_ guid: GUID) -> BookmarksModel { + if let removedRoot = self.current.removeItemWithGUID(guid) { + return BookmarksModel(modelFactory: self.factory, root: removedRoot) + } + log.warning("BookmarksModel.removeGUIDFromCurrent did not remove anything. Check to make sure you're not using the abstract BookmarkFolder class.") + return self + } + + /** + * Produce a new model rooted at the same place as this model. Can fail if + * the folder has been deleted from the backing store. + */ + open func reloadData() -> Deferred> { + return self.factory.modelForFolder(current) + } + + open var canDelete: Bool { + return false + } +} + +public protocol BookmarksModelFactorySource { + var modelFactory: Deferred> { get } +} + +public protocol BookmarksModelFactory { + func factoryForIndex(_ index: Int, inFolder folder: BookmarkFolder) -> BookmarksModelFactory + func modelForFolder(_ folder: BookmarkFolder) -> Deferred> + func modelForFolder(_ guid: GUID) -> Deferred> + func modelForFolder(_ guid: GUID, title: String) -> Deferred> + + func modelForRoot() -> Deferred> + + // Whenever async construction is necessary, we fall into a pattern of needing + // a placeholder that behaves correctly for the period between kickoff and set. + var nullModel: BookmarksModel { get } + + func isBookmarked(_ url: String) -> Deferred> + func removeByGUID(_ guid: GUID) -> Success + @discardableResult func removeByURL(_ url: String) -> Success +} + +/* + * A folder that contains an array of children. + */ +open class MemoryBookmarkFolder: BookmarkFolder, Sequence { + let children: [BookmarkNode] + + public init(guid: GUID, title: String, children: [BookmarkNode]) { + self.children = children + super.init(guid: guid, title: title) + } + + public struct BookmarkNodeGenerator: IteratorProtocol { + public typealias Element = BookmarkNode + let children: [BookmarkNode] + var index: Int = 0 + + init(children: [BookmarkNode]) { + self.children = children + } + + public mutating func next() -> BookmarkNode? { + if index < children.count { + defer { index += 1 } + return children[index] + } + return nil + } + } + + override open var favicon: Favicon? { + get { + if let path = Bundle.main.path(forResource: "bookmarkFolder", ofType: "png") { + let url = URL(fileURLWithPath: path) + return Favicon(url: url.absoluteString, date: Date(), type: IconType.local) + } + return nil + } + set { + } + } + + override open var count: Int { + return children.count + } + + override open subscript(index: Int) -> BookmarkNode { + get { + return children[index] + } + } + + override open func itemIsEditableAtIndex(_ index: Int) -> Bool { + return true + } + + override open func removeItemWithGUID(_ guid: GUID) -> BookmarkFolder? { + let without = children.filter { $0.guid != guid } + return MemoryBookmarkFolder(guid: self.guid, title: self.title, children: without) + } + + open func makeIterator() -> BookmarkNodeGenerator { + return BookmarkNodeGenerator(children: self.children) + } + + /** + * Return a new immutable folder that's just like this one, + * but also contains the new items. + */ + func append(_ items: [BookmarkNode]) -> MemoryBookmarkFolder { + if items.isEmpty { + return self + } + return MemoryBookmarkFolder(guid: self.guid, title: self.title, children: self.children + items) + } +} + +open class MemoryBookmarksSink: ShareToDestination { + var queue: [BookmarkNode] = [] + public init() { } + open func shareItem(_ item: ShareItem) -> Success { + let title = item.title == nil ? "Untitled" : item.title! + func exists(_ e: BookmarkNode) -> Bool { + if let bookmark = e as? BookmarkItem { + return bookmark.url == item.url + } + + return false + } + + // Don't create duplicates. + if !queue.contains(where: exists) { + queue.append(BookmarkItem(guid: Bytes.generateGUID(), title: title, url: item.url)) + } + + return succeed() + } +} + +private extension SuggestedSite { + func asBookmark() -> BookmarkNode { + let b = BookmarkItem(guid: self.guid ?? Bytes.generateGUID(), title: self.title, url: self.url) + b.favicon = self.icon + return b + } +} + +open class PrependedBookmarkFolder: BookmarkFolder { + let main: BookmarkFolder + fileprivate let prepend: BookmarkNode + + init(main: BookmarkFolder, prepend: BookmarkNode) { + self.main = main + self.prepend = prepend + super.init(guid: main.guid, title: main.guid) + } + + override open var count: Int { + return self.main.count + 1 + } + + override open subscript(index: Int) -> BookmarkNode? { + if index == 0 { + return self.prepend + } + + return self.main[index - 1] + } + + override open func itemIsEditableAtIndex(_ index: Int) -> Bool { + return index > 0 && self.main.itemIsEditableAtIndex(index - 1) + } + + override open func removeItemWithGUID(_ guid: GUID) -> BookmarkFolder? { + guard let removedFolder = main.removeItemWithGUID(guid) else { + log.warning("Failed to remove child item from prepended folder. Check that main folder overrides removeItemWithGUID.") + return nil + } + return PrependedBookmarkFolder(main: removedFolder, prepend: prepend) + } +} + +open class ConcatenatedBookmarkFolder: BookmarkFolder { + fileprivate let main: BookmarkFolder + fileprivate let append: BookmarkFolder + + init(main: BookmarkFolder, append: BookmarkFolder) { + self.main = main + self.append = append + super.init(guid: main.guid, title: main.title) + } + + var pivot: Int { + return main.count + } + + override open var count: Int { + return main.count + append.count + } + + override open subscript(index: Int) -> BookmarkNode? { + return index < main.count ? main[index] : append[index - main.count] + } + + override open func itemIsEditableAtIndex(_ index: Int) -> Bool { + return index < main.count ? main.itemIsEditableAtIndex(index) : append.itemIsEditableAtIndex(index - main.count) + } + + override open func removeItemWithGUID(_ guid: GUID) -> BookmarkFolder? { + let newMain = main.removeItemWithGUID(guid) ?? main + let newAppend = append.removeItemWithGUID(guid) ?? append + return ConcatenatedBookmarkFolder(main: newMain, append: newAppend) + } +} + +/** + * A trivial offline model factory that represents a simple hierarchy. + */ +open class MockMemoryBookmarksStore: BookmarksModelFactory, ShareToDestination { + let mobile: MemoryBookmarkFolder + let root: MemoryBookmarkFolder + var unsorted: MemoryBookmarkFolder + + let sink: MemoryBookmarksSink + + public init() { + let res = [BookmarkItem]() + + mobile = MemoryBookmarkFolder(guid: BookmarkRoots.MobileFolderGUID, title: "Mobile Bookmarks", children: res) + + unsorted = MemoryBookmarkFolder(guid: BookmarkRoots.UnfiledFolderGUID, title: "Unsorted Bookmarks", children: []) + sink = MemoryBookmarksSink() + + root = MemoryBookmarkFolder(guid: BookmarkRoots.RootGUID, title: "Root", children: [mobile, unsorted]) + } + + public func factoryForIndex(_ index: Int, inFolder folder: BookmarkFolder) -> BookmarksModelFactory { + return self + } + + open func modelForFolder(_ folder: BookmarkFolder) -> Deferred> { + return self.modelForFolder(folder.guid, title: folder.title) + } + + open func modelForFolder(_ guid: GUID) -> Deferred> { + return self.modelForFolder(guid, title: "") + } + + open func modelForFolder(_ guid: GUID, title: String) -> Deferred> { + var m: BookmarkFolder + switch guid { + case BookmarkRoots.MobileFolderGUID: + // Transparently merges in any queued items. + m = self.mobile.append(self.sink.queue) + break + case BookmarkRoots.RootGUID: + m = self.root + break + case BookmarkRoots.UnfiledFolderGUID: + m = self.unsorted + break + default: + return deferMaybe(DatabaseError(description: "No such folder \(guid).")) + } + + return deferMaybe(BookmarksModel(modelFactory: self, root: m)) + } + + open func modelForRoot() -> Deferred> { + return deferMaybe(BookmarksModel(modelFactory: self, root: self.root)) + } + + /** + * This class could return the full data immediately. We don't, because real DB-backed code won't. + */ + open var nullModel: BookmarksModel { + let f = MemoryBookmarkFolder(guid: BookmarkRoots.RootGUID, title: "Root", children: []) + return BookmarksModel(modelFactory: self, root: f) + } + + open func shareItem(_ item: ShareItem) -> Success { + return self.sink.shareItem(item) + } + + open func isBookmarked(_ url: String) -> Deferred> { + return deferMaybe(DatabaseError(description: "Not implemented")) + } + + open func removeByGUID(_ guid: GUID) -> Success { + return deferMaybe(DatabaseError(description: "Not implemented")) + } + + open func removeByURL(_ url: String) -> Success { + return deferMaybe(DatabaseError(description: "Not implemented")) + } + + open func clearBookmarks() -> Success { + return succeed() + } +} diff --git a/mobile/ios/Storage/Bookmarks/CachingItemSource.swift b/mobile/ios/Storage/Bookmarks/CachingItemSource.swift new file mode 100644 index 0000000000..40ec28ccb0 --- /dev/null +++ b/mobile/ios/Storage/Bookmarks/CachingItemSource.swift @@ -0,0 +1,192 @@ +/* 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 Deferred +import Foundation +import Shared + +private let log = Logger.syncLogger + +private class CachedSource { + // We track not just mappings between values and non-nil items, but also whether we've tried + // to look up a value at all. This allows us to distinguish between a cache miss and a + // cache hit that didn't find an item in the backing store. + // We expect, given our prefetching, that cache misses will be rare. + fileprivate var cache: [GUID: BookmarkMirrorItem] = [:] + fileprivate var seen: Set = Set() + + subscript(guid: GUID) -> BookmarkMirrorItem? { + get { + return self.cache[guid] + } + + set(value) { + self.cache[guid] = value + } + } + + func lookup(_ guid: GUID) -> Deferred>? { + guard self.seen.contains(guid) else { + log.warning("Cache miss for \(guid).") + return nil + } + + guard let found = self.cache[guid] else { + log.verbose("Cache hit, but no record found for \(guid).") + return deferMaybe(NoSuchRecordError(guid: guid)) + } + + log.verbose("Cache hit for \(guid).") + return deferMaybe(found) + } + + var isEmpty: Bool { + return self.cache.isEmpty + } + + // fill and seen are separate: we won't find every item in the DB. + func fill(_ items: [GUID: BookmarkMirrorItem]) -> Success { + for (x, y) in items { + self.cache[x] = y + } + return succeed() + } + + func markSeen(_ guid: GUID) { + self.seen.insert(guid) + } + + func markSeen(_ guids: T) where T.Iterator.Element == GUID { + self.seen.formUnion(guids) + } + + func takingGUIDs(_ guids: T) -> Deferred> where T.Iterator.Element == GUID { + var out: [GUID: BookmarkMirrorItem] = [:] + guids.forEach { + if let v = self.cache[$0] { + out[$0] = v + } + } + return deferMaybe(out) + } +} + +// Sorry about the boilerplate. +// These are separate protocols so that the method names don't collide when implemented +// by the same class, but that means extracting more base implementation is more trouble than +// it's worth. +open class CachingLocalItemSource: LocalItemSource { + fileprivate let cache: CachedSource + fileprivate let source: LocalItemSource + + public init(source: LocalItemSource) { + self.cache = CachedSource() + self.source = source + } + + open func getLocalItemWithGUID(_ guid: GUID) -> Deferred> { + if let found = self.cache.lookup(guid) { + return found + } + + return self.source.getLocalItemWithGUID(guid) >>== effect { + self.cache.markSeen(guid) + self.cache[guid] = $0 + } + } + + open func getLocalItemsWithGUIDs(_ guids: T) -> Deferred> where T.Iterator.Element == GUID { + return self.prefetchLocalItemsWithGUIDs(guids) >>> { self.cache.takingGUIDs(guids) } + } + + open func prefetchLocalItemsWithGUIDs(_ guids: T) -> Success where T.Iterator.Element == GUID { + log.debug("Prefetching \(guids.count) local items: \(guids.prefix(10))….") + if guids.isEmpty { + return succeed() + } + + return self.source.getLocalItemsWithGUIDs(guids) >>== { + self.cache.markSeen(guids) + return self.cache.fill($0) + } + } +} + +open class CachingMirrorItemSource: MirrorItemSource { + fileprivate let cache: CachedSource + fileprivate let source: MirrorItemSource + + public init(source: MirrorItemSource) { + self.cache = CachedSource() + self.source = source + } + + open func getMirrorItemWithGUID(_ guid: GUID) -> Deferred> { + if let found = self.cache.lookup(guid) { + return found + } + + return self.source.getMirrorItemWithGUID(guid) >>== effect { + self.cache.markSeen(guid) + self.cache[guid] = $0 + } + } + + open func getMirrorItemsWithGUIDs(_ guids: T) -> Deferred> where T.Iterator.Element == GUID { + return self.prefetchMirrorItemsWithGUIDs(guids) >>> { self.cache.takingGUIDs(guids) } + } + + open func prefetchMirrorItemsWithGUIDs(_ guids: T) -> Success where T.Iterator.Element == GUID { + log.debug("Prefetching \(guids.count) mirror items: \(guids.prefix(10))….") + if guids.isEmpty { + return succeed() + } + + return self.source.getMirrorItemsWithGUIDs(guids) >>== { + self.cache.markSeen(guids) + return self.cache.fill($0) + } + } +} + +open class CachingBufferItemSource: BufferItemSource { + fileprivate let cache: CachedSource + fileprivate let source: BufferItemSource + + public init(source: BufferItemSource) { + self.cache = CachedSource() + self.source = source + } + + open func getBufferItemWithGUID(_ guid: GUID) -> Deferred> { + if let found = self.cache.lookup(guid) { + return found + } + + return self.source.getBufferItemWithGUID(guid) >>== effect { + self.cache.markSeen(guid) + self.cache[guid] = $0 + } + } + + open func getBufferItemsWithGUIDs(_ guids: T) -> Deferred> where T.Iterator.Element == GUID { + return self.prefetchBufferItemsWithGUIDs(guids) >>> { self.cache.takingGUIDs(guids) } + } + + public func getBufferChildrenGUIDsForParent(_ guid: GUID) -> Deferred> { + return self.source.getBufferChildrenGUIDsForParent(guid) + } + + open func prefetchBufferItemsWithGUIDs(_ guids: T) -> Success where T.Iterator.Element == GUID { + log.debug("Prefetching \(guids.count) buffer items: \(guids.prefix(10))….") + if guids.isEmpty { + return succeed() + } + + return self.source.getBufferItemsWithGUIDs(guids) >>== { + self.cache.markSeen(guids) + return self.cache.fill($0) + } + } +} diff --git a/mobile/ios/Storage/Bookmarks/Trees.swift b/mobile/ios/Storage/Bookmarks/Trees.swift new file mode 100644 index 0000000000..ac3aad43d3 --- /dev/null +++ b/mobile/ios/Storage/Bookmarks/Trees.swift @@ -0,0 +1,319 @@ +/* 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 + +private let log = Logger.syncLogger + +// MARK: - Defining a tree structure for syncability. +public enum BookmarkTreeNode: Comparable { + indirect case folder(guid: GUID, children: [BookmarkTreeNode]) + case nonFolder(guid: GUID) + case unknown(guid: GUID) + + // Because shared associated values between enum cases aren't possible. + public var recordGUID: GUID { + switch self { + case let .folder(guid, _): + return guid + case let .nonFolder(guid): + return guid + case let .unknown(guid): + return guid + } + } + + public var isRoot: Bool { + return BookmarkRoots.All.contains(self.recordGUID) + } + + public var isUnknown: Bool { + if case .unknown = self { + return true + } + return false + } + + public var children: [BookmarkTreeNode]? { + if case let .folder(_, children) = self { + return children + } + return nil + } + + public func hasChildList(_ nodes: [BookmarkTreeNode]) -> Bool { + if case let .folder(_, ours) = self { + return ours.elementsEqual(nodes, by: { $0.recordGUID == $1.recordGUID }) + } + return false + } + + public func hasSameChildListAs(_ other: BookmarkTreeNode) -> Bool { + if case let .folder(_, ours) = self { + if case let .folder(_, theirs) = other { + return ours.elementsEqual(theirs, by: { $0.recordGUID == $1.recordGUID }) + } + } + return false + } + + // Returns false for unknowns. + public func isSameTypeAs(_ other: BookmarkTreeNode) -> Bool { + switch self { + case .folder: + if case .folder = other { + return true + } + case .nonFolder: + if case .nonFolder = other { + return true + } + default: + return false + } + return false + } +} + +public func == (lhs: BookmarkTreeNode, rhs: BookmarkTreeNode) -> Bool { + switch lhs { + case let .folder(guid, children): + if case let .folder(rguid, rchildren) = rhs { + return guid == rguid && children == rchildren + } + return false + case let .nonFolder(guid): + if case let .nonFolder(rguid) = rhs { + return guid == rguid + } + return false + case let .unknown(guid): + if case let .unknown(rguid) = rhs { + return guid == rguid + } + return false + } +} + +public func < (lhs: BookmarkTreeNode, rhs: BookmarkTreeNode) -> Bool { + return lhs.recordGUID < rhs.recordGUID +} + +typealias StructureRow = (parent: GUID, child: GUID, type: BookmarkNodeType?) + +// This is really a forest, not a tree: it can have multiple 'subtrees' +// and carries a collection of associated values. +public struct BookmarkTree { + // Records with no parents. + public let subtrees: [BookmarkTreeNode] + + // Record GUID -> record. + public let lookup: [GUID: BookmarkTreeNode] + + // Child GUID -> parent GUID. + public let parents: [GUID: GUID] + + // Records that appear in 'lookup' because they're modified, but aren't present + // in 'subtrees' because their parent didn't change. + public let orphans: Set + + // Records that have been deleted. + public let deleted: Set + + // Every record that's changed but not deleted. + public let modified: Set + + // Nodes that are present in this tree but aren't present in the source. + // In practical terms, this will be roots that we pretend exist in + // the mirror for purposes of three-way merging. + public let virtual: Set + + // Accessor for all top-level folders' GUIDs. + public var subtreeGUIDs: Set { + return Set(self.subtrees.map { $0.recordGUID }) + } + + public var isEmpty: Bool { + return self.subtrees.isEmpty && self.deleted.isEmpty + } + + public static func emptyTree() -> BookmarkTree { + return BookmarkTree(subtrees: [], lookup: [:], parents: [:], orphans: Set(), deleted: Set(), modified: Set(), virtual: Set()) + } + + public static func emptyMirrorTree() -> BookmarkTree { + return mappingsToTreeForStructureRows([], withNonFoldersAndEmptyFolders: [], withDeletedRecords: Set(), modifiedRecords: Set(), alwaysIncludeRoots: true) + } + + public func includesOrDeletesNode(_ node: BookmarkTreeNode) -> Bool { + return self.includesOrDeletesGUID(node.recordGUID) + } + + public func includesNode(_ node: BookmarkTreeNode) -> Bool { + return self.includesGUID(node.recordGUID) + } + + public func includesOrDeletesGUID(_ guid: GUID) -> Bool { + return self.includesGUID(guid) || self.deleted.contains(guid) + } + + public func includesGUID(_ guid: GUID) -> Bool { + return self.lookup[guid] != nil + } + + public func find(_ guid: GUID) -> BookmarkTreeNode? { + return self.lookup[guid] + } + + public func find(_ node: BookmarkTreeNode) -> BookmarkTreeNode? { + return self.find(node.recordGUID) + } + + /** + * True if there is one subtree, and it's the Root, when overlayed. + * We assume that the mirror will always be consistent, so what + * this really means is that every subtree in this tree is *present* + * in the comparison tree, or is itself rooted in a known root. + * + * In a fully rooted tree there can be no orphans; if our partial tree + * includes orphans, they must be known by the comparison tree. + */ + public func isFullyRootedIn(_ tree: BookmarkTree) -> Bool { + // We don't compare against tree.deleted, because you can't *undelete*. + return self.orphans.every(tree.includesGUID) && + self.subtrees.every { subtree in + tree.includesNode(subtree) || subtree.isRoot + } + } + + // If this tree contains the root, return it. + public var root: BookmarkTreeNode? { + return self.find(BookmarkRoots.RootGUID) + } + + // Recursively process an input set of structure pairs to yield complete subtrees, + // assembling those subtrees to make a minimal set of trees. + static func mappingsToTreeForStructureRows(_ mappings: [StructureRow], withNonFoldersAndEmptyFolders nonFoldersAndEmptyFolders: [BookmarkTreeNode], withDeletedRecords deleted: Set, modifiedRecords modified: Set, alwaysIncludeRoots: Bool) -> BookmarkTree { + // Accumulate. + var nodes: [GUID: BookmarkTreeNode] = [:] + var parents: [GUID: GUID] = [:] + var remainingFolders = Set() + + // `tops` is the collection of things that we think are the roots of subtrees (until + // we're proved wrong). We add GUIDs here when we don't know their parents; if we get to + // the end and they're still here, they're roots. + var tops = Set() + var notTops = Set() + var orphans = Set() + var virtual = Set() + + // We can't immediately build the final tree, because we need to do it bottom-up! + // So store structure, which we can figure out flat. + var pseudoTree: [GUID: [GUID]] = mappings.groupBy({ $0.parent }, transformer: { $0.child }) + + // Deal with the ones that are non-structural first. + nonFoldersAndEmptyFolders.forEach { node in + let guid = node.recordGUID + nodes[guid] = node + + switch node { + case .folder: + // If we end up here, it's because this folder is empty, and it won't + // appear in structure. Assert to make sure that's true! + assert(pseudoTree[guid] == nil) + pseudoTree[guid] = [] + + // It'll be a top unless we find it as a child in the structure somehow. + tops.insert(guid) + default: + orphans.insert(guid) + } + } + + // Precompute every leaf node. + mappings.forEach { row in + parents[row.child] = row.parent + remainingFolders.insert(row.parent) + tops.insert(row.parent) + + // None of the children we've seen can be top, so remove them. + notTops.insert(row.child) + + if let type = row.type { + switch type { + case .folder: + // The child is itself a folder. + remainingFolders.insert(row.child) + default: + nodes[row.child] = BookmarkTreeNode.nonFolder(guid: row.child) + } + } else { + // This will be the case if we've shadowed a folder; we indirectly reference the original rows. + nodes[row.child] = BookmarkTreeNode.unknown(guid: row.child) + } + } + + // When we build the mirror, we always want to pretend it has our stock roots. + // This gives us our shared basis from which to merge. + // Doing it here means we don't need to protect the mirror database table. + if alwaysIncludeRoots { + func setVirtual(_ guid: GUID) { + if !remainingFolders.contains(guid) && nodes[guid] == nil { + virtual.insert(guid) + } + } + + // Note that we don't check whether the input already contained the roots; we + // never change them, so it's safe to do this unconditionally. + setVirtual(BookmarkRoots.RootGUID) + BookmarkRoots.RootChildren.forEach { + setVirtual($0) + } + + pseudoTree[BookmarkRoots.RootGUID] = BookmarkRoots.RootChildren + tops.insert(BookmarkRoots.RootGUID) + notTops.formUnion(Set(BookmarkRoots.RootChildren)) + remainingFolders.formUnion(BookmarkRoots.All) + BookmarkRoots.RootChildren.forEach { + parents[$0] = BookmarkRoots.RootGUID + } + } + + tops.subtract(notTops) + orphans.subtract(notTops) + + // Recursive. (Not tail recursive, but trees shouldn't be deep enough to blow the stack….) + @discardableResult func nodeForGUID(_ guid: GUID) -> BookmarkTreeNode { + if let already = nodes[guid] { + return already + } + + if !remainingFolders.contains(guid) { + let node = BookmarkTreeNode.unknown(guid: guid) + nodes[guid] = node + return node + } + + // Removing these eagerly prevents infinite recursion in the case of a cycle. + let childGUIDs = pseudoTree[guid] ?? [] + pseudoTree.removeValue(forKey: guid) + remainingFolders.remove(guid) + + let node = BookmarkTreeNode.folder(guid: guid, children: childGUIDs.map(nodeForGUID)) + nodes[guid] = node + return node + } + + // Process every record. + // Do the not-tops first: shallower recursion. + notTops.forEach({ nodeForGUID($0) }) + + let subtrees = tops.map(nodeForGUID) // These will all be folders. + + // Whatever we're left with in `tops` is the set of records for which we + // didn't process a parent. + return BookmarkTree(subtrees: subtrees, lookup: nodes, parents: parents, orphans: orphans, deleted: deleted, modified: modified, virtual: virtual) + } +} diff --git a/mobile/ios/Storage/CertStore.swift b/mobile/ios/Storage/CertStore.swift new file mode 100644 index 0000000000..13ec695b77 --- /dev/null +++ b/mobile/ios/Storage/CertStore.swift @@ -0,0 +1,31 @@ +/* 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 UIKit +import Deferred + +/// In-memory certificate store. +open class CertStore { + fileprivate var keys = Set() + + public init() {} + + open func addCertificate(_ cert: SecCertificate, forOrigin origin: String) { + let data: Data = SecCertificateCopyData(cert) as Data + let key = keyForData(data, origin: origin) + keys.insert(key) + } + + open func containsCertificate(_ cert: SecCertificate, forOrigin origin: String) -> Bool { + let data: Data = SecCertificateCopyData(cert) as Data + let key = keyForData(data, origin: origin) + return keys.contains(key) + } + + fileprivate func keyForData(_ data: Data, origin: String) -> String { + return "\(origin)/\(data.sha256.hexEncodedString)" + } +} diff --git a/mobile/ios/Storage/Clients.swift b/mobile/ios/Storage/Clients.swift new file mode 100644 index 0000000000..401e15f736 --- /dev/null +++ b/mobile/ios/Storage/Clients.swift @@ -0,0 +1,75 @@ +/* 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 Shared +import SwiftyJSON + +public struct RemoteClient: Equatable { + public let guid: GUID? + public let modified: Timestamp + + public let name: String + public let type: String? + public let os: String? + public let version: String? + public let fxaDeviceId: String? + + let protocols: [String]? + + let appPackage: String? + let application: String? + let formfactor: String? + let device: String? + + // Requires a valid ClientPayload (: CleartextPayloadJSON: JSON). + public init(json: JSON, modified: Timestamp) { + self.guid = json["id"].string + self.modified = modified + self.name = json["name"].stringValue + self.type = json["type"].string + + self.version = json["version"].string + self.protocols = jsonsToStrings(json["protocols"].array) + self.os = json["os"].string + self.appPackage = json["appPackage"].string + self.application = json["application"].string + self.formfactor = json["formfactor"].string + self.device = json["device"].string + self.fxaDeviceId = json["fxaDeviceId"].string + } + + public init(guid: GUID?, name: String, modified: Timestamp, type: String?, formfactor: String?, os: String?, version: String?, fxaDeviceId: String?) { + self.guid = guid + self.name = name + self.modified = modified + self.type = type + self.formfactor = formfactor + self.os = os + self.version = version + self.fxaDeviceId = fxaDeviceId + + self.device = nil + self.appPackage = nil + self.application = nil + self.protocols = nil + } +} + +// TODO: should this really compare tabs? +public func ==(lhs: RemoteClient, rhs: RemoteClient) -> Bool { + return lhs.guid == rhs.guid && + lhs.name == rhs.name && + lhs.modified == rhs.modified && + lhs.type == rhs.type && + lhs.formfactor == rhs.formfactor && + lhs.os == rhs.os && + lhs.version == rhs.version && + lhs.fxaDeviceId == rhs.fxaDeviceId +} + +extension RemoteClient: CustomStringConvertible { + public var description: String { + return "" + } +} diff --git a/mobile/ios/Storage/CompletionOps.swift b/mobile/ios/Storage/CompletionOps.swift new file mode 100644 index 0000000000..2e4d804c1e --- /dev/null +++ b/mobile/ios/Storage/CompletionOps.swift @@ -0,0 +1,89 @@ +/* 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 XCGLogger + +private let log = Logger.syncLogger + +public protocol PerhapsNoOp { + var isNoOp: Bool { get } +} + +open class LocalOverrideCompletionOp: PerhapsNoOp { + open var processedLocalChanges: Set = Set() // These can be deleted when we're run. Mark mirror as non-overridden, too. + + open var mirrorItemsToDelete: Set = Set() // These were locally or remotely deleted. + open var mirrorItemsToInsert: [GUID: BookmarkMirrorItem] = [:] // These were locally or remotely added. + open var mirrorItemsToUpdate: [GUID: BookmarkMirrorItem] = [:] // These were already in the mirror, but changed. + open var mirrorStructures: [GUID: [GUID]] = [:] // New or changed structure. + + open var mirrorValuesToCopyFromBuffer: Set = Set() // No need to synthesize BookmarkMirrorItem instances in memory. + open var mirrorValuesToCopyFromLocal: Set = Set() + open var modifiedTimes: [Timestamp: [GUID]] = [:] // Only for copy. + + open var isNoOp: Bool { + return processedLocalChanges.isEmpty && + mirrorValuesToCopyFromBuffer.isEmpty && + mirrorValuesToCopyFromLocal.isEmpty && + mirrorItemsToDelete.isEmpty && + mirrorItemsToInsert.isEmpty && + mirrorItemsToUpdate.isEmpty && + mirrorStructures.isEmpty + } + + open func setModifiedTime(_ time: Timestamp, guids: [GUID]) { + var forCopy: [GUID] = self.modifiedTimes[time] ?? [] + for guid in guids { + // This saves us doing an UPDATE on these items. + if var item = self.mirrorItemsToInsert[guid] { + item.serverModified = time + } else if var item = self.mirrorItemsToUpdate[guid] { + item.serverModified = time + } else { + forCopy.append(guid) + } + } + + if !forCopy.isEmpty { + modifiedTimes[time] = forCopy + } + } + + public init() { + } +} + +open class BufferCompletionOp: PerhapsNoOp { + open var processedBufferChanges: Set = Set() // These can be deleted when we're run. + + open var isNoOp: Bool { + return self.processedBufferChanges.isEmpty + } + + public init() { + } +} + +// This supports the "simple" bookmark syncing scenario where 3-way-merging is disabled: +// After upload, we first remove from the buffer the deleted records, +// then we move new local records to the buffer. +open class BufferUpdatedCompletionOp: PerhapsNoOp { + internal let bufferValuesToMoveFromLocal: Set + internal let deletedValues: Set + internal let mobileRoot: BookmarkMirrorItem + internal let modifiedTime: Timestamp + + open var isNoOp: Bool { + return false + } + + public init(bufferValuesToMoveFromLocal: Set, deletedValues: Set, mobileRoot: BookmarkMirrorItem, modifiedTime: Timestamp) { + self.bufferValuesToMoveFromLocal = bufferValuesToMoveFromLocal + self.deletedValues = deletedValues + self.mobileRoot = mobileRoot + self.modifiedTime = modifiedTime + } +} diff --git a/mobile/ios/Storage/Cursor.swift b/mobile/ios/Storage/Cursor.swift new file mode 100644 index 0000000000..e5286b5cd0 --- /dev/null +++ b/mobile/ios/Storage/Cursor.swift @@ -0,0 +1,124 @@ +/* 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 + +/** + * Status results for a Cursor + */ +public enum CursorStatus { + case success + case failure + case closed +} + +public protocol TypedCursor: Sequence { + associatedtype T + var count: Int { get } + var status: CursorStatus { get } + var statusMessage: String { get } + subscript(index: Int) -> T? { get } + func asArray() -> [T] +} + +/** + * Provides a generic method of returning some data and status information about a request. + */ +open class Cursor: TypedCursor { + open var count: Int { + get { return 0 } + } + + // Extra status information + open var status: CursorStatus + public var statusMessage: String + + init(err: NSError) { + self.status = .failure + self.statusMessage = err.description + } + + public init(status: CursorStatus = .success, msg: String = "") { + self.statusMessage = msg + self.status = status + } + + // Collection iteration and access functions + open subscript(index: Int) -> T? { + get { return nil } + } + + open func asArray() -> [T] { + var acc = [T]() + acc.reserveCapacity(self.count) + for row in self { + // Shouldn't ever be nil -- that's to allow the generator or subscript to be + // out of range. + if let row = row { + acc.append(row) + } + } + return acc + } + + open func makeIterator() -> AnyIterator { + var nextIndex = 0 + return AnyIterator() { + if nextIndex >= self.count || self.status != CursorStatus.success { + return nil + } + + defer { nextIndex += 1 } + return self[nextIndex] + } + } + + open func close() { + status = .closed + statusMessage = "Closed" + } + + deinit { + if status != CursorStatus.closed { + close() + } + } +} + +/* + * A cursor implementation that wraps an array. + */ +open class ArrayCursor : Cursor { + fileprivate var data: [T] + + open override var count: Int { + if status != .success { + return 0 + } + return data.count + } + + public init(data: [T], status: CursorStatus, statusMessage: String) { + self.data = data + super.init(status: status, msg: statusMessage) + } + + public convenience init(data: [T]) { + self.init(data: data, status: CursorStatus.success, statusMessage: "Success") + } + + open override subscript(index: Int) -> T? { + get { + if index >= data.count || index < 0 || status != .success { + return nil + } + return data[index] + } + } + + override open func close() { + data = [T]() + super.close() + } +} diff --git a/mobile/ios/Storage/DatabaseError.swift b/mobile/ios/Storage/DatabaseError.swift new file mode 100644 index 0000000000..6039ca4607 --- /dev/null +++ b/mobile/ios/Storage/DatabaseError.swift @@ -0,0 +1,24 @@ +/* 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 Shared + +/** + * Used to bridge the NSErrors we get here into something that Result is happy with. + */ +open class DatabaseError: MaybeErrorType { + let err: NSError? + + open var description: String { + return err?.localizedDescription ?? "Unknown database error." + } + + public init(description: String) { + self.err = NSError(domain: "mozilla", code: 0, userInfo: [NSLocalizedDescriptionKey: description]) + } + + public init(err: NSError?) { + self.err = err + } +} diff --git a/mobile/ios/Storage/DefaultSuggestedSites.swift b/mobile/ios/Storage/DefaultSuggestedSites.swift new file mode 100644 index 0000000000..40dec5994e --- /dev/null +++ b/mobile/ios/Storage/DefaultSuggestedSites.swift @@ -0,0 +1,130 @@ +/* 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 + +open class DefaultSuggestedSites { + open static let urlMap = [ + "https://www.amazon.com/": [ + "as": "https://www.amazon.in", + "cy": "https://www.amazon.co.uk", + "da": "https://www.amazon.co.uk", + "de": "https://www.amazon.de", + "dsb": "https://www.amazon.de", + "en_GB": "https://www.amazon.co.uk", + "et": "https://www.amazon.co.uk", + "ff": "https://www.amazon.fr", + "ga_IE": "https://www.amazon.co.uk", + "gu_IN": "https://www.amazon.in", + "hi_IN": "https://www.amazon.in", + "hr": "https://www.amazon.co.uk", + "hsb": "https://www.amazon.de", + "ja": "https://www.amazon.co.jp", + "kn": "https://www.amazon.in", + "mr": "https://www.amazon.in", + "or": "https://www.amazon.in", + "sq": "https://www.amazon.co.uk", + "ta": "https://www.amazon.in", + "te": "https://www.amazon.in", + "ur": "https://www.amazon.in", + "en_CA": "https://www.amazon.ca", + "fr_CA": "https://www.amazon.ca" + ] + ] + + open static let sites = [ + "default": [ + SuggestedSiteData( + url: "https://m.facebook.com/", + bgColor: "0x385185", + imageUrl: "asset://suggestedsites_facebook", + faviconUrl: "asset://defaultFavicon", + trackingId: 632, + title: NSLocalizedString("Facebook", comment: "Tile title for Facebook") + ), + SuggestedSiteData( + url: "https://m.youtube.com/", + bgColor: "0xcd201f", + imageUrl: "asset://suggestedsites_youtube", + faviconUrl: "asset://defaultFavicon", + trackingId: 631, + title: NSLocalizedString("YouTube", comment: "Tile title for YouTube") + ), + SuggestedSiteData( + url: "https://www.amazon.com/", + bgColor: "0x000000", + imageUrl: "asset://suggestedsites_amazon", + faviconUrl: "asset://defaultFavicon", + trackingId: 630, + title: NSLocalizedString("Amazon", comment: "Tile title for Amazon") + ), + SuggestedSiteData( + url: "https://www.wikipedia.org/", + bgColor: "0x000000", + imageUrl: "asset://suggestedsites_wikipedia", + faviconUrl: "asset://defaultFavicon", + trackingId: 629, + title: NSLocalizedString("Wikipedia", comment: "Tile title for Wikipedia") + ), + SuggestedSiteData( + url: "https://mobile.twitter.com/", + bgColor: "0x55acee", + imageUrl: "asset://suggestedsites_twitter", + faviconUrl: "asset://defaultFavicon", + trackingId: 628, + title: NSLocalizedString("Twitter", comment: "Tile title for Twitter") + ) + ], + "zh_CN": [ + SuggestedSiteData( + url: "http://mozilla.com.cn", + bgColor: "0xbc3326", + imageUrl: "asset://suggestedsites_mozchina", + faviconUrl: "asset://mozChinaLogo", + trackingId: 700, + title: "火狐社区" + ), + SuggestedSiteData( + url: "https://m.baidu.com/?from=1000969b", + bgColor: "0x00479d", + imageUrl: "asset://suggestedsites_baidu", + faviconUrl: "asset://baiduLogo", + trackingId: 701, + title: "百度" + ), + SuggestedSiteData( + url: "http://sina.cn", + bgColor: "0xe60012", + imageUrl: "asset://suggestedsites_sina", + faviconUrl: "asset://sinaLogo", + trackingId: 702, + title: "新浪" + ), + SuggestedSiteData( + url: "http://info.3g.qq.com/g/s?aid=index&g_f=23946&g_ut=3", + bgColor: "0x028cca", + imageUrl: "asset://suggestedsites_qq", + faviconUrl: "asset://qqLogo", + trackingId: 703, + title: "腾讯" + ), + SuggestedSiteData( + url: "http://m.taobao.com", + bgColor: "0xee5900", + imageUrl: "asset://suggestedsites_taobao", + faviconUrl: "asset://taobaoLogo", + trackingId: 704, + title: "淘宝" + ), + SuggestedSiteData( + url: "http://union.click.jd.com/jdc?e=0&p=AyIHVCtaJQMiQwpDBUoyS0IQWlALHE4YDk5ER1xONwdJKVxASgI%2BeDkWfGJ6HEAOUmkbcjUXVyUBEQZRG1IXARQ3VhhaEQETBVweayVkbzcedVolBxIEUBxdFAoQN1UeXRQLGwFXHlsUABs3UisnS0lKWghLWBQCFzdlK2s%3D&t=W1dCFBBFC14NXAAECUte", + bgColor: "0xc71622", + imageUrl: "asset://suggestedsites_jd", + faviconUrl: "asset://jdLogo", + trackingId: 705, + title: "京东" + ) + ] + ] +} diff --git a/mobile/ios/Storage/DiskImageStore.swift b/mobile/ios/Storage/DiskImageStore.swift new file mode 100644 index 0000000000..42ca16645b --- /dev/null +++ b/mobile/ios/Storage/DiskImageStore.swift @@ -0,0 +1,105 @@ +/* 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 UIKit +import Deferred +import XCGLogger + +private var log = XCGLogger.default + +private class DiskImageStoreErrorType: MaybeErrorType { + let description: String + init(description: String) { + self.description = description + } +} + +/** + * Disk-backed key-value image store. + */ +open class DiskImageStore { + fileprivate let files: FileAccessor + fileprivate let filesDir: String + fileprivate let queue = DispatchQueue(label: "DiskImageStore") + fileprivate let quality: CGFloat + fileprivate var keys: Set + + required public init(files: FileAccessor, namespace: String, quality: Float) { + self.files = files + self.filesDir = try! files.getAndEnsureDirectory(namespace) + self.quality = CGFloat(quality) + + // Build an in-memory set of keys from the existing images on disk. + var keys = [String]() + if let fileEnumerator = FileManager.default.enumerator(atPath: filesDir) { + for file in fileEnumerator { + keys.append(file as! String) + } + } + self.keys = Set(keys) + } + + /// Gets an image for the given key if it is in the store. + open func get(_ key: String) -> Deferred> { + return deferDispatchAsync(queue) { () -> Deferred> in + if !self.keys.contains(key) { + return deferMaybe(DiskImageStoreErrorType(description: "Image key not found")) + } + + let imagePath = URL(fileURLWithPath: self.filesDir).appendingPathComponent(key) + if let data = try? Data(contentsOf: imagePath), + let image = UIImage.imageFromDataThreadSafe(data) { + return deferMaybe(image) + } + + return deferMaybe(DiskImageStoreErrorType(description: "Invalid image data")) + } + } + + /// Adds an image for the given key. + /// This put is asynchronous; the image is not recorded in the cache until the write completes. + /// Does nothing if this key already exists in the store. + @discardableResult open func put(_ key: String, image: UIImage) -> Success { + return deferDispatchAsync(queue) { () -> Success in + if self.keys.contains(key) { + return deferMaybe(DiskImageStoreErrorType(description: "Key already in store")) + } + + let imageURL = URL(fileURLWithPath: self.filesDir).appendingPathComponent(key) + if let data = UIImageJPEGRepresentation(image, self.quality) { + do { + try data.write(to: imageURL, options: .noFileProtection) + self.keys.insert(key) + return succeed() + } catch { + log.error("Unable to write image to disk: \(error)") + } + } + + return deferMaybe(DiskImageStoreErrorType(description: "Could not write image to file")) + } + } + + /// Clears all images from the cache, excluding the given set of keys. + open func clearExcluding(_ keys: Set) -> Success { + return deferDispatchAsync(queue) { () -> Success in + let keysToDelete = self.keys.subtracting(keys) + + for key in keysToDelete { + let url = URL(fileURLWithPath: self.filesDir).appendingPathComponent(key) + do { + try FileManager.default.removeItem(at: url) + } catch { + log.warning("Failed to remove DiskImageStore item at \(url.absoluteString): \(error)") + } + } + + self.keys = self.keys.intersection(keys) + + return succeed() + } + } +} diff --git a/mobile/ios/Storage/ExtensionUtils.swift b/mobile/ios/Storage/ExtensionUtils.swift new file mode 100644 index 0000000000..75bfb7d9ff --- /dev/null +++ b/mobile/ios/Storage/ExtensionUtils.swift @@ -0,0 +1,47 @@ +/* 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 UIKit +import MobileCoreServices + +public struct ExtensionUtils { + /// Look through the extensionContext for a url and title. Walks over all inputItems and then over all the attachments. + /// Has a completionHandler because ultimately an XPC call to the sharing application is done. + /// We can always extract a URL and sometimes a title. The favicon is currently just a placeholder, but + /// future code can possibly interact with a web page to find a proper icon. + public static func extractSharedItemFromExtensionContext(_ extensionContext: NSExtensionContext?, completionHandler: @escaping (ShareItem?, NSError?) -> Void) { + guard let extensionContext = extensionContext, + let inputItems = extensionContext.inputItems as? [NSExtensionItem] else { + completionHandler(nil, nil) + return + } + + for inputItem in inputItems { + guard let attachments = inputItem.attachments as? [NSItemProvider] else { continue } + + for attachment in attachments { + if attachment.hasItemConformingToTypeIdentifier(kUTTypeURL as String) { + attachment.loadItem(forTypeIdentifier: kUTTypeURL as String, options: nil) { obj, err in + guard err == nil else { + completionHandler(nil, err as NSError!) + return + } + + guard let url = obj as? URL else { + completionHandler(nil, NSError(domain: "org.mozilla.fennec", code: 999, userInfo: ["Problem": "Non-URL result."])) + return + } + + let title = inputItem.attributedContentText?.string + completionHandler(ShareItem(url: url.absoluteString, title: title, favicon: nil), nil) + } + + return + } + } + } + + completionHandler(nil, nil) + } +} diff --git a/mobile/ios/Storage/Favicons.swift b/mobile/ios/Storage/Favicons.swift new file mode 100644 index 0000000000..f3c9e54d41 --- /dev/null +++ b/mobile/ios/Storage/Favicons.swift @@ -0,0 +1,22 @@ +/* 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 Shared +import UIKit +import Deferred + +/* The base favicons protocol */ +public protocol Favicons { + func clearAllFavicons() -> Success + + /** + * Returns the ID of the added favicon. + */ + func addFavicon(_ icon: Favicon) -> Deferred> + + /** + * Returns the ID of the added favicon. + */ + @discardableResult func addFavicon(_ icon: Favicon, forSite site: Site) -> Deferred> +} diff --git a/mobile/ios/Storage/FileAccessor.swift b/mobile/ios/Storage/FileAccessor.swift new file mode 100644 index 0000000000..8122163591 --- /dev/null +++ b/mobile/ios/Storage/FileAccessor.swift @@ -0,0 +1,117 @@ +/* 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 + +/** + * A convenience class for file operations under a given root directory. + * Note that while this class is intended to be used to operate only on files + * under the root, this is not strictly enforced: clients can go outside + * the path using ".." or symlinks. + */ +open class FileAccessor { + open let rootPath: String + + public init(rootPath: String) { + self.rootPath = rootPath + } + + /** + * Gets the absolute directory path at the given relative path, creating it if it does not exist. + */ + open func getAndEnsureDirectory(_ relativeDir: String? = nil) throws -> String { + var absolutePath = rootPath + if let relativeDir = relativeDir { + absolutePath = URL(fileURLWithPath: absolutePath).appendingPathComponent(relativeDir).path + } + + try createDir(absolutePath) + return absolutePath + } + + /** + * Removes the file or directory at the given path, relative to the root. + */ + open func remove(_ relativePath: String) throws { + let path = URL(fileURLWithPath: rootPath).appendingPathComponent(relativePath).path + try FileManager.default.removeItem(atPath: path) + } + + /** + * Removes the contents of the directory without removing the directory itself. + */ + open func removeFilesInDirectory(_ relativePath: String = "") throws { + let fileManager = FileManager.default + let path = URL(fileURLWithPath: rootPath).appendingPathComponent(relativePath).path + let files = try fileManager.contentsOfDirectory(atPath: path) + for file in files { + try remove(URL(fileURLWithPath: relativePath).appendingPathComponent(file).path) + } + return + } + + /** + * Determines whether a file exists at the given path, relative to the root. + */ + open func exists(_ relativePath: String) -> Bool { + let path = URL(fileURLWithPath: rootPath).appendingPathComponent(relativePath).path + return FileManager.default.fileExists(atPath: path) + } + + open func attributesForFileAt(relativePath: String) throws -> [FileAttributeKey: Any] { + return try FileManager.default.attributesOfItem(atPath: URL(fileURLWithPath: rootPath).appendingPathComponent(relativePath).path) + } + + /** + * Moves the file or directory to the given destination, with both paths relative to the root. + * The destination directory is created if it does not exist. + */ + open func move(_ fromRelativePath: String, toRelativePath: String) throws { + let rootPathURL = URL(fileURLWithPath: rootPath) + let fromPath = rootPathURL.appendingPathComponent(fromRelativePath).path + let toPath = rootPathURL.appendingPathComponent(toRelativePath) + let toDir = toPath.deletingLastPathComponent() + let toDirPath = toDir.path + try createDir(toDirPath) + + try FileManager.default.moveItem(atPath: fromPath, toPath: toPath.path) + } + + open func copyMatching(fromRelativeDirectory relativePath: String, toAbsoluteDirectory absolutePath: String, matching: (String) -> Bool) throws { + let fileManager = FileManager.default + let pathURL = URL(fileURLWithPath: rootPath).appendingPathComponent(relativePath) + let path = pathURL.path + let destURL = URL(fileURLWithPath: absolutePath, isDirectory: true) + + let files = try fileManager.contentsOfDirectory(atPath: path) + for file in files { + if !matching(file) { + continue + } + + let from = pathURL.appendingPathComponent(file, isDirectory: false).path + let to = destURL.appendingPathComponent(file, isDirectory: false).path + do { + try fileManager.copyItem(atPath: from, toPath: to) + } catch { + } + } + } + + open func copy(_ fromRelativePath: String, toAbsolutePath: String) throws -> Bool { + let fromPath = URL(fileURLWithPath: rootPath).appendingPathComponent(fromRelativePath).path + let dest = URL(fileURLWithPath: toAbsolutePath).deletingLastPathComponent().path + try createDir(dest) + try FileManager.default.copyItem(atPath: fromPath, toPath: toAbsolutePath) + return true + } + + /** + * Creates a directory with the given path, including any intermediate directories. + * Does nothing if the directory already exists. + */ + fileprivate func createDir(_ absolutePath: String) throws { + try FileManager.default.createDirectory(atPath: absolutePath, withIntermediateDirectories: true, attributes: nil) + } +} diff --git a/mobile/ios/Storage/History.swift b/mobile/ios/Storage/History.swift new file mode 100644 index 0000000000..1e3a4c5374 --- /dev/null +++ b/mobile/ios/Storage/History.swift @@ -0,0 +1,108 @@ +/* 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 Shared +import Deferred + +open class IgnoredSiteError: MaybeErrorType { + open var description: String { + return "Ignored site." + } +} + +/** + * The base history protocol for front-end code. + * + * Note that the implementation of these methods might be complicated if + * the implementing class also implements SyncableHistory -- for example, + * `clear` might or might not need to set a bunch of flags to upload deletions. + */ +public protocol BrowserHistory { + @discardableResult func addLocalVisit(_ visit: SiteVisit) -> Success + func clearHistory() -> Success + @discardableResult func removeHistoryForURL(_ url: String) -> Success + func removeSiteFromTopSites(_ site: Site) -> Success + func removeHostFromTopSites(_ host: String) -> Success + func getFrecentHistory() -> FrecentHistory + func getSitesByLastVisit(_ limit: Int) -> Deferred>> + func getTopSitesWithLimit(_ limit: Int) -> Deferred>> + func setTopSitesNeedsInvalidation() + func setTopSitesCacheSize(_ size: Int32) + func clearTopSitesCache() -> Success + + // Pinning top sites + func removeFromPinnedTopSites(_ site: Site) -> Success + func addPinnedTopSite(_ site: Site) -> Success + func getPinnedTopSites() -> Deferred>> +} + +/** + * An interface for fast repeated frecency queries. + */ +public protocol FrecentHistory { + func getSites(whereURLContains filter: String?, historyLimit limit: Int, bookmarksLimit: Int) -> Deferred>> + func updateTopSitesCacheQuery() -> (String, Args?) +} + +/** + * An interface for accessing recommendation content from Storage + */ +public protocol HistoryRecommendations { + func getHighlights() -> Deferred>> + func getRecentBookmarks(_ limit: Int) -> Deferred>> + + func removeHighlightForURL(_ url: String) -> Success + func repopulate(invalidateTopSites shouldInvalidateTopSites: Bool, invalidateHighlights shouldInvalidateHighlights: Bool) -> Success +} + +/** + * The interface that history storage needs to provide in order to be + * synced by a `HistorySynchronizer`. + */ +public protocol SyncableHistory: AccountRemovalDelegate { + /** + * Make sure that the local place with the provided URL has the provided GUID. + * Succeeds if no place exists with that URL. + */ + func ensurePlaceWithURL(_ url: String, hasGUID guid: GUID) -> Success + + /** + * Delete the place with the provided GUID, and all of its visits. Succeeds if the GUID is unknown. + */ + func deleteByGUID(_ guid: GUID, deletedAt: Timestamp) -> Success + + func storeRemoteVisits(_ visits: [Visit], forGUID guid: GUID) -> Success + func insertOrUpdatePlace(_ place: Place, modified: Timestamp) -> Deferred> + + func getModifiedHistoryToUpload() -> Deferred> + func getDeletedHistoryToUpload() -> Deferred> + + /** + * Chains through the provided timestamp. + */ + func markAsSynchronized(_: [GUID], modified: Timestamp) -> Deferred> + func markAsDeleted(_ guids: [GUID]) -> Success + + func doneApplyingRecordsAfterDownload() -> Success + func doneUpdatingMetadataAfterUpload() -> Success + + /** + * For inspecting whether we're an active participant in history sync. + */ + func hasSyncedHistory() -> Deferred> +} + +// TODO: integrate Site with this. + +open class Place { + open let guid: GUID + open let url: String + open let title: String + + public init(guid: GUID, url: String, title: String) { + self.guid = guid + self.url = url + self.title = title + } +} diff --git a/mobile/ios/Storage/Info.plist b/mobile/ios/Storage/Info.plist new file mode 100644 index 0000000000..b3205ecaf4 --- /dev/null +++ b/mobile/ios/Storage/Info.plist @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + FMWK + CFBundleShortVersionString + 10.6 + CFBundleSignature + ???? + CFBundleVersion + 1 + NSPrincipalClass + + + diff --git a/mobile/ios/Storage/Logins.swift b/mobile/ios/Storage/Logins.swift new file mode 100644 index 0000000000..faccc18397 --- /dev/null +++ b/mobile/ios/Storage/Logins.swift @@ -0,0 +1,621 @@ +/* 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 WebKit +import Shared +import Deferred +import XCGLogger + +private var log = Logger.syncLogger + +enum SyncStatus: Int { + // Ordinarily not needed; synced items are removed from the overlay. But they start here when cloned. + case synced = 0 + + // A material change that we want to upload on next sync. + case changed = 1 + + // Created locally. + case new = 2 +} + +public enum CommutativeLoginField { + case timesUsed(increment: Int) +} + +public protocol Indexable { + var index: Int { get } +} + +public enum NonCommutativeLoginField: Indexable { + case hostname(to: String) + case password(to: String) + case username(to: String?) + case httpRealm(to: String?) + case formSubmitURL(to: String?) + case timeCreated(to: MicrosecondTimestamp) // Should be immutable. + case timeLastUsed(to: MicrosecondTimestamp) + case timePasswordChanged(to: MicrosecondTimestamp) + + public var index: Int { + switch self { + case .hostname: + return 0 + case .password: + return 1 + case .username: + return 2 + case .httpRealm: + return 3 + case .formSubmitURL: + return 4 + case .timeCreated: + return 5 + case .timeLastUsed: + return 6 + case .timePasswordChanged: + return 7 + } + } + + static let Entries: Int = 8 +} + +// We don't care about these, because they're slated for removal at some point -- +// we don't really use them for form fill. +// We handle them in the same way as NonCommutative, just broken out to allow us +// flexibility in removing them or reconciling them differently. +public enum NonConflictingLoginField: Indexable { + case usernameField(to: String?) + case passwordField(to: String?) + + public var index: Int { + switch self { + case .usernameField: + return 0 + case .passwordField: + return 1 + } + } + + static let Entries: Int = 2 +} + +public typealias LoginDeltas = ( + commutative: [CommutativeLoginField], + nonCommutative: [NonCommutativeLoginField], + nonConflicting: [NonConflictingLoginField] +) + +public typealias TimestampedLoginDeltas = (at: Timestamp, changed: LoginDeltas) + +/** + * LoginData is a wrapper around NSURLCredential and NSURLProtectionSpace to allow us to add extra fields where needed. + **/ +public protocol LoginData: class { + var guid: String { get set } // It'd be nice if this were read-only. + var credentials: URLCredential { get } + var protectionSpace: URLProtectionSpace { get } + var hostname: String { get } + var username: String? { get } + var password: String { get } + var httpRealm: String? { get set } + var formSubmitURL: String? { get set } + var usernameField: String? { get set } + var passwordField: String? { get set } + var isValid: Maybe<()> { get } + + // https://bugzilla.mozilla.org/show_bug.cgi?id=1238103 + var hasMalformedHostname: Bool { get set } + + func toDict() -> [String: String] + + func isSignificantlyDifferentFrom(_ login: LoginData) -> Bool +} + +public protocol LoginUsageData { + var timesUsed: Int { get set } + var timeCreated: MicrosecondTimestamp { get set } + var timeLastUsed: MicrosecondTimestamp { get set } + var timePasswordChanged: MicrosecondTimestamp { get set } +} + +open class Login: CustomStringConvertible, LoginData, LoginUsageData, Equatable { + open var guid: String + + open fileprivate(set) var credentials: URLCredential + open let protectionSpace: URLProtectionSpace + + open var hostname: String { + if let _ = protectionSpace.`protocol` { + return protectionSpace.urlString() + } + return protectionSpace.host + } + + open var hasMalformedHostname: Bool = false + + open var username: String? { return credentials.user } + open var password: String { return credentials.password ?? "" } + open var usernameField: String? + open var passwordField: String? + + fileprivate var _httpRealm: String? + open var httpRealm: String? { + get { return self._httpRealm ?? protectionSpace.realm } + set { self._httpRealm = newValue } + } + + fileprivate var _formSubmitURL: String? + open var formSubmitURL: String? { + get { + return self._formSubmitURL + } + set(value) { + guard let value = value, !value.isEmpty else { + self._formSubmitURL = nil + return + } + + let url2 = URL(string: self.hostname) + let url1 = URL(string: value) + + if url1?.host != url2?.host { + log.warning("Form submit URL domain doesn't match login's domain.") + } + + self._formSubmitURL = value + } + } + + // LoginUsageData. These defaults only apply to locally created records. + open var timesUsed = 0 + open var timeCreated = Date.nowMicroseconds() + open var timeLastUsed = Date.nowMicroseconds() + open var timePasswordChanged = Date.nowMicroseconds() + + // Printable + open var description: String { + return "Login for \(hostname)" + } + + open var isValid: Maybe<()> { + // Referenced from https://mxr.mozilla.org/mozilla-central/source/toolkit/components/passwordmgr/nsLoginManager.js?rev=f76692f0fcf8&mark=280-281#271 + + // Logins with empty hostnames are not valid. + if hostname.isEmpty { + return Maybe(failure: LoginDataError(description: "Can't add a login with an empty hostname.")) + } + + // Logins with empty passwords are not valid. + if password.isEmpty { + return Maybe(failure: LoginDataError(description: "Can't add a login with an empty password.")) + } + + // Logins with both a formSubmitURL and httpRealm are not valid. + if let _ = formSubmitURL, let _ = httpRealm { + return Maybe(failure: LoginDataError(description: "Can't add a login with both a httpRealm and formSubmitURL.")) + } + + // Login must have at least a formSubmitURL or httpRealm. + if (formSubmitURL == nil) && (httpRealm == nil) { + return Maybe(failure: LoginDataError(description: "Can't add a login without a httpRealm or formSubmitURL.")) + } + + // All good. + return Maybe(success: ()) + } + + open func update(password: String, username: String) { + self.credentials = + URLCredential(user: username, password: password, persistence: credentials.persistence) + } + + // Essentially: should we sync a change? + // Desktop ignores usernameField and hostnameField. + open func isSignificantlyDifferentFrom(_ login: LoginData) -> Bool { + return login.password != self.password || + login.hostname != self.hostname || + login.username != self.username || + login.formSubmitURL != self.formSubmitURL || + login.httpRealm != self.httpRealm + } + + /* Used for testing purposes since formSubmitURL should be given back to use from the Logins.js script */ + open class func createWithHostname(_ hostname: String, username: String, password: String, formSubmitURL: String?) -> LoginData { + let loginData = Login(hostname: hostname, username: username, password: password) as LoginData + loginData.formSubmitURL = formSubmitURL + return loginData + } + + open class func createWithHostname(_ hostname: String, username: String, password: String) -> LoginData { + return Login(hostname: hostname, username: username, password: password) as LoginData + } + + open class func createWithCredential(_ credential: URLCredential, protectionSpace: URLProtectionSpace) -> LoginData { + return Login(credential: credential, protectionSpace: protectionSpace) as LoginData + } + + public init(guid: String, hostname: String, username: String, password: String) { + self.guid = guid + self.credentials = URLCredential(user: username, password: password, persistence: URLCredential.Persistence.none) + + // Break down the full url hostname into its scheme/protocol and host components + let hostnameURL = hostname.asURL + let host = hostnameURL?.host ?? hostname + let scheme = hostnameURL?.scheme ?? "" + + // We should ignore any SSL or normal web ports in the URL. + var port = hostnameURL?.port ?? 0 + if port == 443 || port == 80 { + port = 0 + } + + self.protectionSpace = URLProtectionSpace(host: host, port: port, protocol: scheme, realm: nil, authenticationMethod: nil) + } + + convenience init(hostname: String, username: String, password: String) { + self.init(guid: Bytes.generateGUID(), hostname: hostname, username: username, password: password) + } + + // Why do we need this initializer to be marked as required? Because otherwise we can't + // use this type in our factory for MirrorLogin and LocalLogin. + // SO: http://stackoverflow.com/questions/26280176/swift-generics-not-preserving-type + // Playground: https://gist.github.com/rnewman/3fb0c4dbd25e7fda7e3d + // Conversation: https://twitter.com/rnewman/status/611332618412359680 + required public init(credential: URLCredential, protectionSpace: URLProtectionSpace) { + self.guid = Bytes.generateGUID() + self.credentials = credential + self.protectionSpace = protectionSpace + } + + open func toDict() -> [String: String] { + return [ + "hostname": hostname, + "formSubmitURL": formSubmitURL ?? "", + "httpRealm": httpRealm ?? "", + "username": username ?? "", + "password": password, + "usernameField": usernameField ?? "", + "passwordField": passwordField ?? "" + ] + } + + open class func fromScript(_ url: URL, script: [String: Any]) -> LoginData? { + guard let username = script["username"] as? String, + let password = script["password"] as? String else { + return nil + } + + guard let origin = getPasswordOrigin(url.absoluteString) else { + return nil + } + + let login = Login(hostname: origin, username: username, password: password) + + if let formSubmit = script["formSubmitURL"] as? String { + login.formSubmitURL = formSubmit + } + + if let passwordField = script["passwordField"] as? String { + login.passwordField = passwordField + } + + if let userField = script["usernameField"] as? String { + login.usernameField = userField + } + + return login as LoginData + } + + fileprivate class func getPasswordOrigin(_ uriString: String, allowJS: Bool = false) -> String? { + var realm: String? = nil + if let uri = URL(string: uriString), + let scheme = uri.scheme, !scheme.isEmpty, + let host = uri.host { + if allowJS && scheme == "javascript" { + return "javascript:" + } + + realm = "\(scheme)://\(host)" + + // If the URI explicitly specified a port, only include it when + // it's not the default. (We never want "http://foo.com:80") + if let port = uri.port { + realm? += ":\(port)" + } + } else { + // bug 159484 - disallow url types that don't support a hostPort. + // (although we handle "javascript:..." as a special case above.) + log.debug("Couldn't parse origin for \(uriString)") + realm = nil + } + return realm + } + + /** + * Produce a delta stream by comparing this record to a source. + * Note that the source might be missing the timestamp and counter fields + * introduced in Bug 555755, so we pay special attention to those, checking for + * and ignoring transitions to zero. + * + * TODO: it's possible that we'll have, say, two iOS clients working with a desktop. + * Each time the desktop changes the password fields, it'll upload a record without + * these extra timestamp fields. We need to make sure the right thing happens. + * + * There are three phases in this process: + * 1. Producing deltas. There is no intrinsic ordering here, but we yield ordered + * arrays for convenience and ease of debugging. + * 2. Comparing deltas. This is done through a kind of array-based Perlish Schwartzian + * transform, where each field has a known index in space to allow for trivial + * comparison; this, of course, is ordered. + * 3. Applying a merged delta stream to a record. Again, this is unordered, but we + * use arrays for convenience. + */ + open func deltas(from: Login) -> LoginDeltas { + let commutative: [CommutativeLoginField] + + if self.timesUsed > 0 && self.timesUsed != from.timesUsed { + commutative = [CommutativeLoginField.timesUsed(increment: self.timesUsed - from.timesUsed)] + } else { + commutative = [] + } + + var nonCommutative = [NonCommutativeLoginField]() + + if self.hostname != from.hostname { + nonCommutative.append(NonCommutativeLoginField.hostname(to: self.hostname)) + } + if self.password != from.password { + nonCommutative.append(NonCommutativeLoginField.password(to: self.password)) + } + if self.username != from.username { + nonCommutative.append(NonCommutativeLoginField.username(to: self.username)) + } + if self.httpRealm != from.httpRealm { + nonCommutative.append(NonCommutativeLoginField.httpRealm(to: self.httpRealm)) + } + if self.formSubmitURL != from.formSubmitURL { + nonCommutative.append(NonCommutativeLoginField.formSubmitURL(to: self.formSubmitURL)) + } + if self.timeCreated > 0 && self.timeCreated != from.timeCreated { + nonCommutative.append(NonCommutativeLoginField.timeCreated(to: self.timeCreated)) + } + if self.timeLastUsed > 0 && self.timeLastUsed != from.timeLastUsed { + nonCommutative.append(NonCommutativeLoginField.timeLastUsed(to: self.timeLastUsed)) + } + if self.timeLastUsed > 0 && self.timePasswordChanged != from.timePasswordChanged { + nonCommutative.append(NonCommutativeLoginField.timePasswordChanged(to: self.timePasswordChanged)) + } + + var nonConflicting = [NonConflictingLoginField]() + + if self.passwordField != from.passwordField { + nonConflicting.append(NonConflictingLoginField.passwordField(to: self.passwordField)) + } + if self.usernameField != from.usernameField { + nonConflicting.append(NonConflictingLoginField.usernameField(to: self.usernameField)) + } + + return (commutative, nonCommutative, nonConflicting) + } + + fileprivate class func mergeDeltaFields(_ count: Int, a: [T], b: [T], preferBToA: Bool) -> [T] { + var deltas = Array(repeating: nil, count: count) + + // Let's start with the 'a's. + for f in a { + deltas[f.index] = f + } + + // Then detect any conflicts and fill out the rest. + for f in b { + let index = f.index + if deltas[index] != nil { + log.warning("Collision in \(T.self) \(f.index). Using latest.") + if preferBToA { + deltas[index] = f + } + } else { + deltas[index] = f + } + } + + return optFilter(deltas) + } + + open class func mergeDeltas(a: TimestampedLoginDeltas, b: TimestampedLoginDeltas) -> LoginDeltas { + let (aAt, aChanged) = a + let (bAt, bChanged) = b + let (aCommutative, aNonCommutative, aNonConflicting) = aChanged + let (bCommutative, bNonCommutative, bNonConflicting) = bChanged + + // If the timestamps are exactly the same -- an exceedingly rare occurrence -- we default + // to 'b', which is the remote record by convention. + let bLatest = aAt <= bAt + + let commutative = aCommutative + bCommutative + let nonCommutative: [NonCommutativeLoginField] + let nonConflicting: [NonConflictingLoginField] + + if aNonCommutative.isEmpty { + nonCommutative = bNonCommutative + } else if bNonCommutative.isEmpty { + nonCommutative = aNonCommutative + } else { + nonCommutative = mergeDeltaFields(NonCommutativeLoginField.Entries, a: aNonCommutative, b: bNonCommutative, preferBToA: bLatest) + } + + if aNonConflicting.isEmpty { + nonConflicting = bNonConflicting + } else if bNonCommutative.isEmpty { + nonConflicting = aNonConflicting + } else { + nonConflicting = mergeDeltaFields(NonConflictingLoginField.Entries, a: aNonConflicting, b: bNonConflicting, preferBToA: bLatest) + } + + return ( + commutative: commutative, + nonCommutative: nonCommutative, + nonConflicting: nonConflicting + ) + } + + /** + * Apply the provided changes to yield a new login. + */ + open func applyDeltas(_ deltas: LoginDeltas) -> Login { + let guid = self.guid + var hostname = self.hostname + var username = self.username + var password = self.password + var usernameField = self.usernameField + var passwordField = self.passwordField + var timesUsed = self.timesUsed + var httpRealm = self.httpRealm + var formSubmitURL = self.formSubmitURL + var timeCreated = self.timeCreated + var timeLastUsed = self.timeLastUsed + var timePasswordChanged = self.timePasswordChanged + + for delta in deltas.commutative { + switch delta { + case let .timesUsed(increment): + timesUsed += increment + } + } + + for delta in deltas.nonCommutative { + switch delta { + case let .hostname(to): + hostname = to + break + case let .password(to): + password = to + break + case let .username(to): + username = to + break + case let .httpRealm(to): + httpRealm = to + break + case let .formSubmitURL(to): + formSubmitURL = to + break + case let .timeCreated(to): + timeCreated = to + break + case let .timeLastUsed(to): + timeLastUsed = to + break + case let .timePasswordChanged(to): + timePasswordChanged = to + break + } + } + + for delta in deltas.nonConflicting { + switch delta { + case let .usernameField(to): + usernameField = to + break + case let .passwordField(to): + passwordField = to + break + } + } + + let out = Login(guid: guid, hostname: hostname, username: username ?? "", password: password) + out.timesUsed = timesUsed + out.httpRealm = httpRealm + out.formSubmitURL = formSubmitURL + out.timeCreated = timeCreated + out.timeLastUsed = timeLastUsed + out.timePasswordChanged = timePasswordChanged + out.usernameField = usernameField + out.passwordField = passwordField + + return out + } +} + +public func ==(lhs: Login, rhs: Login) -> Bool { + return lhs.credentials == rhs.credentials && lhs.protectionSpace == rhs.protectionSpace +} + +open class ServerLogin: Login { + var serverModified: Timestamp = 0 + + public init(guid: String, hostname: String, username: String, password: String, modified: Timestamp) { + self.serverModified = modified + super.init(guid: guid, hostname: hostname, username: username, password: password) + } + + required public init(credential: URLCredential, protectionSpace: URLProtectionSpace) { + super.init(credential: credential, protectionSpace: protectionSpace) + } +} + +class MirrorLogin: ServerLogin { + var isOverridden: Bool = false +} + +class LocalLogin: Login { + var syncStatus: SyncStatus = .synced + var isDeleted: Bool = false + var localModified: Timestamp = 0 +} + +public protocol BrowserLogins { + func getUsageDataForLoginByGUID(_ guid: GUID) -> Deferred> + func getLoginDataForGUID(_ guid: GUID) -> Deferred> + func getLoginsForProtectionSpace(_ protectionSpace: URLProtectionSpace) -> Deferred>> + func getLoginsForProtectionSpace(_ protectionSpace: URLProtectionSpace, withUsername username: String?) -> Deferred>> + func getAllLogins() -> Deferred>> + func searchLoginsWithQuery(_ query: String?) -> Deferred>> + + // Add a new login regardless of whether other logins might match some fields. Callers + // are responsible for querying first if they care. + @discardableResult func addLogin(_ login: LoginData) -> Success + + @discardableResult func updateLoginByGUID(_ guid: GUID, new: LoginData, significant: Bool) -> Success + + // Add the use of a login by GUID. + @discardableResult func addUseOfLoginByGUID(_ guid: GUID) -> Success + func removeLoginByGUID(_ guid: GUID) -> Success + func removeLoginsWithGUIDs(_ guids: [GUID]) -> Success + + func removeAll() -> Success +} + +public protocol SyncableLogins: AccountRemovalDelegate { + /** + * Delete the login with the provided GUID. Succeeds if the GUID is unknown. + */ + func deleteByGUID(_ guid: GUID, deletedAt: Timestamp) -> Success + + func applyChangedLogin(_ upstream: ServerLogin) -> Success + + func getModifiedLoginsToUpload() -> Deferred> + func getDeletedLoginsToUpload() -> Deferred> + + /** + * Chains through the provided timestamp. + */ + func markAsSynchronized(_: T, modified: Timestamp) -> Deferred> where T.Iterator.Element == GUID + func markAsDeleted(_ guids: T) -> Success where T.Iterator.Element == GUID + + /** + * For inspecting whether we're an active participant in login sync. + */ + func hasSyncedLogins() -> Deferred> +} + +open class LoginDataError: MaybeErrorType { + open let description: String + public init(description: String) { + self.description = description + } +} diff --git a/mobile/ios/Storage/Metadata.swift b/mobile/ios/Storage/Metadata.swift new file mode 100644 index 0000000000..1f4e68f6d4 --- /dev/null +++ b/mobile/ios/Storage/Metadata.swift @@ -0,0 +1,13 @@ +/* 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 Deferred +import Shared + +/// Interface for saving and retrieving metadata web content +public protocol Metadata { + @discardableResult func storeMetadata(_ metadata: PageMetadata, forPageURL: URL, expireAt: UInt64) -> Success + func deleteExpiredMetadata() -> Success +} diff --git a/mobile/ios/Storage/MockLogins.swift b/mobile/ios/Storage/MockLogins.swift new file mode 100644 index 0000000000..5e44b59c2e --- /dev/null +++ b/mobile/ios/Storage/MockLogins.swift @@ -0,0 +1,154 @@ +/* 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 Deferred + +open class MockLogins: BrowserLogins, SyncableLogins { + fileprivate var cache = [Login]() + + public init(files: FileAccessor) { + } + + open func getLoginsForProtectionSpace(_ protectionSpace: URLProtectionSpace) -> Deferred>> { + let cursor = ArrayCursor(data: cache.filter({ login in + return login.protectionSpace.host == protectionSpace.host + }).sorted(by: { (loginA, loginB) -> Bool in + return loginA.timeLastUsed > loginB.timeLastUsed + }).map({ login in + return login as LoginData + })) + return Deferred(value: Maybe(success: cursor)) + } + + open func getLoginsForProtectionSpace(_ protectionSpace: URLProtectionSpace, withUsername username: String?) -> Deferred>> { + let cursor = ArrayCursor(data: cache.filter({ login in + return login.protectionSpace.host == protectionSpace.host && + login.username == username + }).sorted(by: { (loginA, loginB) -> Bool in + return loginA.timeLastUsed > loginB.timeLastUsed + }).map({ login in + return login as LoginData + })) + return Deferred(value: Maybe(success: cursor)) + } + + open func getLoginDataForGUID(_ guid: GUID) -> Deferred> { + if let login = (cache.filter { $0.guid == guid }).first { + return deferMaybe(login) + } else { + return deferMaybe(LoginDataError(description: "Login for GUID \(guid) not found")) + } + } + + open func getAllLogins() -> Deferred>> { + let cursor = ArrayCursor(data: cache.sorted(by: { (loginA, loginB) -> Bool in + return loginA.hostname > loginB.hostname + })) + return Deferred(value: Maybe(success: cursor)) + } + + open func searchLoginsWithQuery(_ query: String?) -> Deferred>> { + let cursor = ArrayCursor(data: cache.filter({ login in + var checks = [Bool]() + if let query = query { + checks.append(login.username?.contains(query) ?? false) + checks.append(login.password.contains(query)) + checks.append(login.hostname.contains(query)) + } + return checks.contains(true) + }).sorted(by: { (loginA, loginB) -> Bool in + return loginA.hostname > loginB.hostname + })) + return Deferred(value: Maybe(success: cursor)) + } + + // This method is only here for testing + open func getUsageDataForLoginByGUID(_ guid: GUID) -> Deferred> { + let res = cache.filter({ login in + return login.guid == guid + }).sorted(by: { (loginA, loginB) -> Bool in + return loginA.timeLastUsed > loginB.timeLastUsed + })[0] as LoginUsageData + + return Deferred(value: Maybe(success: res)) + } + + open func addLogin(_ login: LoginData) -> Success { + if let _ = cache.index(of: login as! Login) { + return deferMaybe(LoginDataError(description: "Already in the cache")) + } + cache.append(login as! Login) + return succeed() + } + + open func updateLoginByGUID(_ guid: GUID, new: LoginData, significant: Bool) -> Success { + // TODO + return succeed() + } + + open func getModifiedLoginsToUpload() -> Deferred> { + // TODO + return deferMaybe([]) + } + + open func getDeletedLoginsToUpload() -> Deferred> { + // TODO + return deferMaybe([]) + } + + open func updateLogin(_ login: LoginData) -> Success { + if let index = cache.index(of: login as! Login) { + cache[index].timePasswordChanged = Date.nowMicroseconds() + return succeed() + } + return deferMaybe(LoginDataError(description: "Password wasn't cached yet. Can't update")) + } + + open func addUseOfLoginByGUID(_ guid: GUID) -> Success { + if let login = cache.filter({ $0.guid == guid }).first { + login.timeLastUsed = Date.nowMicroseconds() + return succeed() + } + return deferMaybe(LoginDataError(description: "Password wasn't cached yet. Can't update")) + } + + open func removeLoginByGUID(_ guid: GUID) -> Success { + let filtered = cache.filter { $0.guid != guid } + if filtered.count == cache.count { + return deferMaybe(LoginDataError(description: "Can not remove a password that wasn't stored")) + } + cache = filtered + return succeed() + } + + open func removeLoginsWithGUIDs(_ guids: [GUID]) -> Success { + return walk(guids) { guid in + self.removeLoginByGUID(guid) + } + } + + open func removeAll() -> Success { + cache.removeAll(keepingCapacity: false) + return succeed() + } + + open func hasSyncedLogins() -> Deferred> { + return deferMaybe(true) + } + + // TODO + open func deleteByGUID(_ guid: GUID, deletedAt: Timestamp) -> Success { return succeed() } + open func applyChangedLogin(_ upstream: ServerLogin) -> Success { return succeed() } + open func markAsSynchronized(_: T, modified: Timestamp) -> Deferred> where T.Iterator.Element == GUID { return deferMaybe(0) } + open func markAsDeleted(_ guids: T) -> Success where T.Iterator.Element == GUID { return succeed() } + open func onRemovedAccount() -> Success { return succeed() } +} + +extension MockLogins: ResettableSyncStorage { + public func resetClient() -> Success { + return succeed() + } +} diff --git a/mobile/ios/Storage/PageMetadata.swift b/mobile/ios/Storage/PageMetadata.swift new file mode 100644 index 0000000000..48f93f911a --- /dev/null +++ b/mobile/ios/Storage/PageMetadata.swift @@ -0,0 +1,106 @@ +/* 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 UIKit +import SDWebImage + +enum MetadataKeys: String { + case imageURL = "image" + case imageDataURI = "image_data_uri" + case pageURL = "url" + case title = "title" + case description = "description" + case type = "type" + case provider = "provider" + case favicon = "icon" + case keywords = "keywords" +} + +/* + * Value types representing a page's metadata + */ +public struct PageMetadata { + public let id: Int? + public let siteURL: String + public let mediaURL: String? + public let title: String? + public let description: String? + public let type: String? + public let providerName: String? + public let faviconURL: String? + public let keywordsString: String? + public var keywords: Set { + guard let string = keywordsString else { + return Set() + } + + let strings = string.split(separator: ",", omittingEmptySubsequences: true).map(String.init) + return Set(strings) + } + + public init(id: Int?, siteURL: String, mediaURL: String?, title: String?, description: String?, type: String?, providerName: String?, mediaDataURI: String?, faviconURL: String? = nil, keywords: String? = nil, cacheImages: Bool = true) { + self.id = id + self.siteURL = siteURL + self.mediaURL = mediaURL + self.title = title + self.description = description + self.type = type + self.providerName = providerName + self.faviconURL = faviconURL + self.keywordsString = keywords + + if let urlString = mediaURL, let url = URL(string: urlString), cacheImages { + self.cacheImage(fromDataURI: mediaDataURI, forURL: url) + } + } + + public static func fromDictionary(_ dict: [String: Any]) -> PageMetadata? { + guard let siteURL = dict[MetadataKeys.pageURL.rawValue] as? String else { + return nil + } + + return PageMetadata(id: nil, siteURL: siteURL, mediaURL: dict[MetadataKeys.imageURL.rawValue] as? String, + title: dict[MetadataKeys.title.rawValue] as? String, description: dict[MetadataKeys.description.rawValue] as? String, + type: dict[MetadataKeys.type.rawValue] as? String, providerName: dict[MetadataKeys.provider.rawValue] as? String, mediaDataURI: dict[MetadataKeys.imageDataURI.rawValue] as? String, faviconURL: dict[MetadataKeys.favicon.rawValue] as? String, keywords: dict[MetadataKeys.keywords.rawValue] as? String) + } + + fileprivate func cacheImage(fromDataURI dataURI: String?, forURL url: URL) { + let manager = SDWebImageManager.shared() + + func cacheUsingURLOnly() { + manager.cachedImageExists(for: url) { exists in + if !exists { + self.downloadAndCache(fromURL: url) + } + } + } + + guard let dataURI = dataURI, let dataURL = URL(string: dataURI) else { + cacheUsingURLOnly() + return + } + + manager.cachedImageExists(for: dataURL) { exists in + if let data = try? Data(contentsOf: dataURL), let image = UIImage(data: data), !exists { + self.cache(image: image, forURL: url) + } else { + cacheUsingURLOnly() + } + } + } + + fileprivate func downloadAndCache(fromURL webUrl: URL) { + let manager = SDWebImageManager.shared() + manager.loadImage(with: webUrl, options: .continueInBackground, progress: nil) { (image, _, _, _, _, _) in + if let image = image { + self.cache(image: image, forURL: webUrl) + } + } + } + + fileprivate func cache(image: UIImage, forURL url: URL) { + SDWebImageManager.shared().saveImage(toCache: image, for: url) + } +} diff --git a/mobile/ios/Storage/Queue.swift b/mobile/ios/Storage/Queue.swift new file mode 100644 index 0000000000..882e3142bc --- /dev/null +++ b/mobile/ios/Storage/Queue.swift @@ -0,0 +1,13 @@ +/* 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 Deferred + +public protocol TabQueue { + func addToQueue(_ tab: ShareItem) -> Success + func getQueuedTabs() -> Deferred>> + @discardableResult func clearQueuedTabs() -> Success +} diff --git a/mobile/ios/Storage/RecentlyClosedTabs.swift b/mobile/ios/Storage/RecentlyClosedTabs.swift new file mode 100644 index 0000000000..fbffabbbbd --- /dev/null +++ b/mobile/ios/Storage/RecentlyClosedTabs.swift @@ -0,0 +1,76 @@ +/* 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 + +open class ClosedTabsStore { + let prefs: Prefs + + lazy open var tabs: [ClosedTab] = { + guard let tabsArray: Data = self.prefs.objectForKey("recentlyClosedTabs") as Any? as? Data, + let unarchivedArray = NSKeyedUnarchiver.unarchiveObject(with: tabsArray) as? [ClosedTab] else { + return [] + } + return unarchivedArray + }() + + public init(prefs: Prefs) { + self.prefs = prefs + } + + open func addTab(_ url: URL, title: String?, faviconURL: String?) { + let recentlyClosedTab = ClosedTab(url: url, title: title ?? "", faviconURL: faviconURL ?? "") + tabs.insert(recentlyClosedTab, at: 0) + if tabs.count > 5 { + tabs.removeLast() + } + let archivedTabsArray = NSKeyedArchiver.archivedData(withRootObject: tabs) + prefs.setObject(archivedTabsArray, forKey: "recentlyClosedTabs") + } + + open func clearTabs() { + prefs.removeObjectForKey("recentlyClosedTabs") + tabs = [] + } +} + +open class ClosedTab: NSObject, NSCoding { + open let url: URL + open let title: String? + open let faviconURL: String? + + var jsonDictionary: [String: Any] { + let title = (self.title ?? "") + let faviconURL = (self.faviconURL ?? "") + let json: [String: Any] = ["title": title, "url": url, "faviconURL": faviconURL] + return json + } + + init(url: URL, title: String?, faviconURL: String?) { + assert(Thread.isMainThread) + self.title = title + self.url = url + self.faviconURL = faviconURL + super.init() + } + + required convenience public init?(coder: NSCoder) { + guard let url = coder.decodeObject(forKey: "url") as? URL, + let faviconURL = coder.decodeObject(forKey: "faviconURL") as? String, + let title = coder.decodeObject(forKey: "title") as? String else { return nil } + + self.init( + url: url, + title: title, + faviconURL: faviconURL + ) + } + + open func encode(with coder: NSCoder) { + coder.encode(url, forKey: "url") + coder.encode(faviconURL, forKey: "faviconURL") + coder.encode(title, forKey: "title") + } +} diff --git a/mobile/ios/Storage/RemoteTabs.swift b/mobile/ios/Storage/RemoteTabs.swift new file mode 100644 index 0000000000..59cf906bad --- /dev/null +++ b/mobile/ios/Storage/RemoteTabs.swift @@ -0,0 +1,109 @@ +/* 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 Deferred + +public struct ClientAndTabs: Equatable, CustomStringConvertible { + public let client: RemoteClient + public let tabs: [RemoteTab] + + public var description: String { + return "" + } + + // See notes in RemoteTabsPanel.swift. + public func approximateLastSyncTime() -> Timestamp { + if tabs.isEmpty { + return client.modified + } + + return tabs.reduce(Timestamp(0), { m, tab in + return max(m, tab.lastUsed) + }) + } +} + +public func ==(lhs: ClientAndTabs, rhs: ClientAndTabs) -> Bool { + return (lhs.client == rhs.client) && + (lhs.tabs == rhs.tabs) +} + +public protocol RemoteClientsAndTabs: SyncCommands { + func wipeClients() -> Deferred> + func wipeRemoteTabs() -> Deferred> + func wipeTabs() -> Deferred> + func getClientGUIDs() -> Deferred>> + func getClients() -> Deferred> + func getClient(guid: GUID) -> Deferred> + func getClient(fxaDeviceId: String) -> Deferred> + @available(*, deprecated, message: "use getClient(guid:) instead") + func getClientWithId(_ clientID: GUID) -> Deferred> + func getClientsAndTabs() -> Deferred> + func getTabsForClientWithGUID(_ guid: GUID?) -> Deferred> + func insertOrUpdateClient(_ client: RemoteClient) -> Deferred> + func insertOrUpdateClients(_ clients: [RemoteClient]) -> Deferred> + + // Returns number of tabs inserted. + func insertOrUpdateTabs(_ tabs: [RemoteTab]) -> Deferred> // Insert into the local client. + func insertOrUpdateTabsForClientGUID(_ clientGUID: String?, tabs: [RemoteTab]) -> Deferred> + + func deleteClient(guid: GUID) -> Success +} + +public struct RemoteTab: Equatable { + public let clientGUID: String? + public let URL: Foundation.URL + public let title: String + public let history: [Foundation.URL] + public let lastUsed: Timestamp + public let icon: Foundation.URL? + + public static func shouldIncludeURL(_ url: Foundation.URL) -> Bool { + let scheme = url.scheme + if scheme == "about" { + return false + } + if scheme == "javascript" { + return false + } + + if let hostname = url.host?.lowercased() { + if hostname == "localhost" { + return false + } + return true + } + return false + } + + public init(clientGUID: String?, URL: Foundation.URL, title: String, history: [Foundation.URL], lastUsed: Timestamp, icon: Foundation.URL?) { + self.clientGUID = clientGUID + self.URL = URL + self.title = title + self.history = history + self.lastUsed = lastUsed + self.icon = icon + } + + public func withClientGUID(_ clientGUID: String?) -> RemoteTab { + return RemoteTab(clientGUID: clientGUID, URL: URL, title: title, history: history, lastUsed: lastUsed, icon: icon) + } +} + +public func ==(lhs: RemoteTab, rhs: RemoteTab) -> Bool { + return lhs.clientGUID == rhs.clientGUID && + lhs.URL == rhs.URL && + lhs.title == rhs.title && + lhs.history == rhs.history && + lhs.lastUsed == rhs.lastUsed && + lhs.icon == rhs.icon +} + +extension RemoteTab: CustomStringConvertible { + public var description: String { + return "" + } +} diff --git a/mobile/ios/Storage/SQL/BrowserDB.swift b/mobile/ios/Storage/SQL/BrowserDB.swift new file mode 100644 index 0000000000..0a4f29d901 --- /dev/null +++ b/mobile/ios/Storage/SQL/BrowserDB.swift @@ -0,0 +1,226 @@ +/* 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 XCGLogger +import Deferred +import Shared + +public let NotificationDatabaseWasRecreated = Notification.Name("NotificationDatabaseWasRecreated") + +private let log = Logger.syncLogger + +public typealias Args = [Any?] + +open class BrowserDB { + fileprivate let db: SwiftData + + // SQLITE_MAX_VARIABLE_NUMBER = 999 by default. This controls how many ?s can + // appear in a query string. + open static let MaxVariableNumber = 999 + + public init(filename: String, secretKey: String? = nil, schema: Schema, files: FileAccessor) { + log.debug("Initializing BrowserDB: \(filename).") + + let file = URL(fileURLWithPath: (try! files.getAndEnsureDirectory())).appendingPathComponent(filename).path + + if AppConstants.BuildChannel == .developer && secretKey != nil { + log.debug("Will attempt to use encrypted DB: \(file) with secret = \(secretKey ?? "nil")") + } + + self.db = SwiftData(filename: file, key: secretKey, prevKey: nil, schema: schema, files: files) + } + + // Remove the DB op from the queue (by marking it cancelled), and if it is already running tell sqlite to cancel it. + // At any point the operation could complete on another thread, so it is held weakly. + // Swift compiler bug: failing to compile WeakRef here. + public func cancel(databaseOperation: WeakRef) { + weak var databaseOperation = databaseOperation.value as? Cancellable + + db.suspendQueue() + defer { + db.resumeQueue() + } + + databaseOperation?.cancel() + if databaseOperation?.running ?? false { + db.cancel() + } + } + + // For testing purposes or other cases where we want to ensure that this `BrowserDB` + // instance has been initialized (schema is created/updated). + public func touch() -> Success { + return withConnection { connection -> Void in + guard let _ = connection as? ConcreteSQLiteDBConnection else { + throw DatabaseError(description: "Could not establish a database connection") + } + } + } + + /* + * Opening a WAL-using database with a hot journal cannot complete in read-only mode. + * The supported mechanism for a read-only query against a WAL-using SQLite database is to use PRAGMA query_only, + * but this isn't all that useful for us, because we have a mixed read/write workload. + */ + @discardableResult func withConnection(flags: SwiftData.Flags = .readWriteCreate, _ callback: @escaping (_ connection: SQLiteDBConnection) throws -> T) -> Deferred> { + return db.withConnection(flags, callback) + } + + func transaction(_ callback: @escaping (_ connection: SQLiteDBConnection) throws -> T) -> Deferred> { + return db.transaction(callback) + } + + @discardableResult func vacuum() -> Success { + log.debug("Vacuuming a BrowserDB.") + + return withConnection({ connection -> Void in + try connection.vacuum() + }) + } + + @discardableResult func checkpoint() -> Success { + log.debug("Checkpointing a BrowserDB.") + + return transaction { connection in + connection.checkpoint() + } + } + + public class func varlist(_ count: Int) -> String { + return "(" + Array(repeating: "?", count: count).joined(separator: ", ") + ")" + } + + enum InsertOperation: String { + case Insert = "INSERT" + case Replace = "REPLACE" + case InsertOrIgnore = "INSERT OR IGNORE" + case InsertOrReplace = "INSERT OR REPLACE" + case InsertOrRollback = "INSERT OR ROLLBACK" + case InsertOrAbort = "INSERT OR ABORT" + case InsertOrFail = "INSERT OR FAIL" + } + + /** + * Insert multiple sets of values into the given table. + * + * Assumptions: + * 1. The table exists and contains the provided columns. + * 2. Every item in `values` is the same length. + * 3. That length is the same as the length of `columns`. + * 4. Every value in each element of `values` is non-nil. + * + * If there are too many items to insert, multiple individual queries will run + * in sequence. + * + * A failure anywhere in the sequence will cause immediate return of failure, but + * will not roll back — use a transaction if you need one. + */ + func bulkInsert(_ table: String, op: InsertOperation, columns: [String], values: [Args]) -> Success { + // Note that there's a limit to how many ?s can be in a single query! + // So here we execute 999 / (columns * rows) insertions per query. + // Note that we can't use variables for the column names, so those don't affect the count. + if values.isEmpty { + log.debug("No values to insert.") + return succeed() + } + + let variablesPerRow = columns.count + + // Sanity check. + assert(values[0].count == variablesPerRow) + + let cols = columns.joined(separator: ", ") + let queryStart = "\(op.rawValue) INTO \(table) (\(cols)) VALUES " + + let varString = BrowserDB.varlist(variablesPerRow) + + let insertChunk: ([Args]) -> Success = { vals -> Success in + let valuesString = Array(repeating: varString, count: vals.count).joined(separator: ", ") + let args: Args = vals.flatMap { $0 } + return self.run(queryStart + valuesString, withArgs: args) + } + + let rowCount = values.count + if (variablesPerRow * rowCount) < BrowserDB.MaxVariableNumber { + return insertChunk(values) + } + + log.debug("Splitting bulk insert across multiple runs. I hope you started a transaction!") + let rowsPerInsert = (999 / variablesPerRow) + let chunks = chunk(values, by: rowsPerInsert) + log.debug("Inserting in \(chunks.count) chunks.") + + // There's no real reason why we can't pass the ArraySlice here, except that I don't + // want to keep fighting Swift. + return walk(chunks, f: { insertChunk(Array($0)) }) + } + + func write(_ sql: String, withArgs args: Args? = nil) -> Deferred> { + return withConnection { connection -> Int in + try connection.executeChange(sql, withArgs: args) + + let modified = connection.numberOfRowsModified + log.debug("Modified rows: \(modified).") + return modified + } + } + + public func forceClose() { + db.forceClose() + } + + public func reopenIfClosed() { + db.reopenIfClosed() + } + + func run(_ sql: String, withArgs args: Args? = nil) -> Success { + return run([(sql, args)]) + } + + func run(_ commands: [String]) -> Success { + return run(commands.map { (sql: $0, args: nil) }) + } + + /** + * Runs an array of SQL commands. Note: These will all run in order in a transaction and will block + * the caller's thread until they've finished. If any of them fail the operation will abort (no more + * commands will be run) and the transaction will roll back, returning a DatabaseError. + */ + func run(_ commands: [(sql: String, args: Args?)]) -> Success { + if commands.isEmpty { + return succeed() + } + + return transaction { connection -> Void in + for (sql, args) in commands { + try connection.executeChange(sql, withArgs: args) + } + } + } + + func runQuery(_ sql: String, args: Args?, factory: @escaping (SDRow) -> T) -> Deferred>> { + return withConnection { connection -> Cursor in + connection.executeQuery(sql, factory: factory, withArgs: args) + } + } + + func runQueryUnsafe(_ sql: String, args: Args?, factory: @escaping (SDRow) -> T, block: @escaping (Cursor) throws -> U) -> Deferred> { + return withConnection { connection -> U in + let cursor = connection.executeQueryUnsafe(sql, factory: factory, withArgs: args) + defer { cursor.close() } + return try block(cursor) + } + } + + func queryReturnsResults(_ sql: String, args: Args? = nil) -> Deferred> { + return runQuery(sql, args: args, factory: { _ in true }) + >>== { deferMaybe($0[0] ?? false) } + } + + func queryReturnsNoResults(_ sql: String, args: Args? = nil) -> Deferred> { + return runQuery(sql, args: nil, factory: { _ in false }) + >>== { deferMaybe($0[0] ?? true) } + } +} diff --git a/mobile/ios/Storage/SQL/BrowserSchema.swift b/mobile/ios/Storage/SQL/BrowserSchema.swift new file mode 100644 index 0000000000..976c488072 --- /dev/null +++ b/mobile/ios/Storage/SQL/BrowserSchema.swift @@ -0,0 +1,1379 @@ +/* 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 XCGLogger + +let BookmarksFolderTitleMobile: String = NSLocalizedString("Mobile Bookmarks", tableName: "Storage", comment: "The title of the folder that contains mobile bookmarks. This should match bookmarks.folder.mobile.label on Android.") +let BookmarksFolderTitleMenu: String = NSLocalizedString("Bookmarks Menu", tableName: "Storage", comment: "The name of the folder that contains desktop bookmarks in the menu. This should match bookmarks.folder.menu.label on Android.") +let BookmarksFolderTitleToolbar: String = NSLocalizedString("Bookmarks Toolbar", tableName: "Storage", comment: "The name of the folder that contains desktop bookmarks in the toolbar. This should match bookmarks.folder.toolbar.label on Android.") +let BookmarksFolderTitleUnsorted: String = NSLocalizedString("Unsorted Bookmarks", tableName: "Storage", comment: "The name of the folder that contains unsorted desktop bookmarks. This should match bookmarks.folder.unfiled.label on Android.") + +let _TableBookmarks = "bookmarks" // Removed in v12. Kept for migration. +let TableBookmarksMirror = "bookmarksMirror" // Added in v9. +let TableBookmarksMirrorStructure = "bookmarksMirrorStructure" // Added in v10. + +let TableBookmarksBuffer = "bookmarksBuffer" // Added in v12. bookmarksMirror is renamed to bookmarksBuffer. +let TableBookmarksBufferStructure = "bookmarksBufferStructure" // Added in v12. +let TableBookmarksLocal = "bookmarksLocal" // Added in v12. Supersedes 'bookmarks'. +let TableBookmarksLocalStructure = "bookmarksLocalStructure" // Added in v12. + +let TablePendingBookmarksDeletions = "pending_deletions" // Added in v28. + +let TableFavicons = "favicons" +let TableHistory = "history" +let TableCachedTopSites = "cached_top_sites" +let TablePinnedTopSites = "pinned_top_sites" +let TableDomains = "domains" +let TableVisits = "visits" +let TableFaviconSites = "favicon_sites" +let TableQueuedTabs = "queue" +let TableSyncCommands = "commands" +let TableClients = "clients" +let TableTabs = "tabs" + +let TableActivityStreamBlocklist = "activity_stream_blocklist" +let TablePageMetadata = "page_metadata" +let TableHighlights = "highlights" + +let TableRemoteDevices = "remote_devices" // Added in v29. + +let ViewBookmarksBufferOnMirror = "view_bookmarksBuffer_on_mirror" +let ViewBookmarksBufferWithDeletionsOnMirror = "view_bookmarksBuffer_with_deletions_on_mirror" +let ViewBookmarksBufferStructureOnMirror = "view_bookmarksBufferStructure_on_mirror" +let ViewBookmarksLocalOnMirror = "view_bookmarksLocal_on_mirror" +let ViewBookmarksLocalStructureOnMirror = "view_bookmarksLocalStructure_on_mirror" +let ViewAllBookmarks = "view_all_bookmarks" +let ViewAwesomebarBookmarks = "view_awesomebar_bookmarks" +let TempTableAwesomebarBookmarks = "awesomebar_bookmarks_temp_table" +let ViewAwesomebarBookmarksWithIcons = "view_awesomebar_bookmarks_with_favicons" + +let ViewHistoryVisits = "view_history_visits" +let ViewWidestFaviconsForSites = "view_favicons_widest" +let ViewHistoryIDsWithWidestFavicons = "view_history_id_favicon" +let ViewIconForURL = "view_icon_for_url" + +let IndexHistoryShouldUpload = "idx_history_should_upload" +let IndexVisitsSiteIDDate = "idx_visits_siteID_date" // Removed in v6. +let IndexVisitsSiteIDIsLocalDate = "idx_visits_siteID_is_local_date" // Added in v6. +let IndexBookmarksMirrorStructureParentIdx = "idx_bookmarksMirrorStructure_parent_idx" // Added in v10. +let IndexBookmarksLocalStructureParentIdx = "idx_bookmarksLocalStructure_parent_idx" // Added in v12. +let IndexBookmarksBufferStructureParentIdx = "idx_bookmarksBufferStructure_parent_idx" // Added in v12. +let IndexBookmarksMirrorStructureChild = "idx_bookmarksMirrorStructure_child" // Added in v14. +let IndexPageMetadataCacheKey = "idx_page_metadata_cache_key_uniqueindex" // Added in v19 +let IndexPageMetadataSiteURL = "idx_page_metadata_site_url_uniqueindex" // Added in v21 + +private let AllTables: [String] = [ + TableDomains, + TableFavicons, + TableFaviconSites, + + TableHistory, + TableVisits, + TableCachedTopSites, + + TableBookmarksBuffer, + TableBookmarksBufferStructure, + TableBookmarksLocal, + TableBookmarksLocalStructure, + TableBookmarksMirror, + TableBookmarksMirrorStructure, + TablePendingBookmarksDeletions, + TableQueuedTabs, + + TableActivityStreamBlocklist, + TablePageMetadata, + TableHighlights, + TablePinnedTopSites, + TableRemoteDevices, + + TableSyncCommands, + TableClients, + TableTabs, +] + +private let AllViews: [String] = [ + ViewHistoryIDsWithWidestFavicons, + ViewWidestFaviconsForSites, + ViewIconForURL, + ViewBookmarksBufferOnMirror, + ViewBookmarksBufferWithDeletionsOnMirror, + ViewBookmarksBufferStructureOnMirror, + ViewBookmarksLocalOnMirror, + ViewBookmarksLocalStructureOnMirror, + ViewAllBookmarks, + ViewAwesomebarBookmarks, + ViewAwesomebarBookmarksWithIcons, + ViewHistoryVisits, +] + +private let AllIndices: [String] = [ + IndexHistoryShouldUpload, + IndexVisitsSiteIDIsLocalDate, + IndexBookmarksBufferStructureParentIdx, + IndexBookmarksLocalStructureParentIdx, + IndexBookmarksMirrorStructureParentIdx, + IndexBookmarksMirrorStructureChild, + IndexPageMetadataCacheKey, + IndexPageMetadataSiteURL, +] + +private let AllTablesIndicesAndViews: [String] = AllViews + AllIndices + AllTables + +private let log = Logger.syncLogger + +/** + * The monolithic class that manages the inter-related history etc. tables. + * We rely on SQLiteHistory having initialized the favicon table first. + */ +open class BrowserSchema: Schema { + static let DefaultVersion = 34 // Bug 1409777. + + public var name: String { return "BROWSER" } + public var version: Int { return BrowserSchema.DefaultVersion } + + let sqliteVersion: Int32 + let supportsPartialIndices: Bool + + public init() { + let v = sqlite3_libversion_number() + self.sqliteVersion = v + self.supportsPartialIndices = v >= 3008000 // 3.8.0. + let ver = String(cString: sqlite3_libversion()) + log.info("SQLite version: \(ver) (\(v)).") + } + + func run(_ db: SQLiteDBConnection, sql: String, args: Args? = nil) -> Bool { + do { + try db.executeChange(sql, withArgs: args) + } catch let err as NSError { + log.error("Error running SQL in BrowserSchema: \(err.localizedDescription)") + log.error("SQL was \(sql)") + return false + } + + return true + } + + // TODO: transaction. + func run(_ db: SQLiteDBConnection, queries: [(String, Args?)]) -> Bool { + for (sql, args) in queries { + if !run(db, sql: sql, args: args) { + return false + } + } + return true + } + + func run(_ db: SQLiteDBConnection, queries: [String]) -> Bool { + for sql in queries { + if !run(db, sql: sql) { + return false + } + } + return true + } + + func runValidQueries(_ db: SQLiteDBConnection, queries: [(String?, Args?)]) -> Bool { + for (sql, args) in queries { + if let sql = sql { + if !run(db, sql: sql, args: args) { + return false + } + } + } + return true + } + + func runValidQueries(_ db: SQLiteDBConnection, queries: [String?]) -> Bool { + return self.run(db, queries: optFilter(queries)) + } + + func prepopulateRootFolders(_ db: SQLiteDBConnection) -> Bool { + let type = BookmarkNodeType.folder.rawValue + let now = Date.nowNumber() + let status = SyncStatus.new.rawValue + + let localArgs: Args = [ + BookmarkRoots.RootID, BookmarkRoots.RootGUID, type, now, BookmarkRoots.RootGUID, status, now, + BookmarkRoots.MobileID, BookmarkRoots.MobileFolderGUID, type, now, BookmarkRoots.RootGUID, status, now, + BookmarkRoots.MenuID, BookmarkRoots.MenuFolderGUID, type, now, BookmarkRoots.RootGUID, status, now, + BookmarkRoots.ToolbarID, BookmarkRoots.ToolbarFolderGUID, type, now, BookmarkRoots.RootGUID, status, now, + BookmarkRoots.UnfiledID, BookmarkRoots.UnfiledFolderGUID, type, now, BookmarkRoots.RootGUID, status, now, + ] + + // Compute these args using the sequence in RootChildren, rather than hard-coding. + var idx = 0 + var structureArgs = Args() + structureArgs.reserveCapacity(BookmarkRoots.RootChildren.count * 3) + BookmarkRoots.RootChildren.forEach { guid in + structureArgs.append(BookmarkRoots.RootGUID) + structureArgs.append(guid) + structureArgs.append(idx) + idx += 1 + } + + // Note that we specify an empty title and parentName for these records. We should + // never need a parentName -- we don't use content-based reconciling or + // reparent these -- and we'll use the current locale's string, retrieved + // via titleForSpecialGUID, if necessary. + + let local = + "INSERT INTO \(TableBookmarksLocal) " + + "(id, guid, type, date_added, parentid, title, parentName, sync_status, local_modified) VALUES " + + Array(repeating: "(?, ?, ?, ?, ?, '', '', ?, ?)", count: BookmarkRoots.RootChildren.count + 1).joined(separator: ", ") + + let structure = + "INSERT INTO \(TableBookmarksLocalStructure) (parent, child, idx) VALUES " + + Array(repeating: "(?, ?, ?)", count: BookmarkRoots.RootChildren.count).joined(separator: ", ") + + return self.run(db, queries: [(local, localArgs), (structure, structureArgs)]) + } + + let topSitesTableCreate = + "CREATE TABLE IF NOT EXISTS \(TableCachedTopSites) (" + + "historyID INTEGER, " + + "url TEXT NOT NULL, " + + "title TEXT NOT NULL, " + + "guid TEXT NOT NULL UNIQUE, " + + "domain_id INTEGER, " + + "domain TEXT NO NULL, " + + "localVisitDate REAL, " + + "remoteVisitDate REAL, " + + "localVisitCount INTEGER, " + + "remoteVisitCount INTEGER, " + + "iconID INTEGER, " + + "iconURL TEXT, " + + "iconDate REAL, " + + "iconType INTEGER, " + + "iconWidth INTEGER, " + + "frecencies REAL" + + ")" + + let pinnedTopSitesTableCreate = + "CREATE TABLE IF NOT EXISTS \(TablePinnedTopSites) (" + + "historyID INTEGER, " + + "url TEXT NOT NULL UNIQUE, " + + "title TEXT, " + + "guid TEXT, " + + "pinDate REAL, " + + "domain TEXT NOT NULL " + + ")" + + let domainsTableCreate = + "CREATE TABLE IF NOT EXISTS \(TableDomains) (" + + "id INTEGER PRIMARY KEY AUTOINCREMENT, " + + "domain TEXT NOT NULL UNIQUE, " + + "showOnTopSites TINYINT NOT NULL DEFAULT 1" + + ")" + + let queueTableCreate = + "CREATE TABLE IF NOT EXISTS \(TableQueuedTabs) (" + + "url TEXT NOT NULL UNIQUE, " + + "title TEXT" + + ") " + + let syncCommandsTableCreate = + "CREATE TABLE IF NOT EXISTS \(TableSyncCommands) (" + + "id INTEGER PRIMARY KEY AUTOINCREMENT, " + + "client_guid TEXT NOT NULL, " + + "value TEXT NOT NULL" + + ")" + + let clientsTableCreate = + "CREATE TABLE IF NOT EXISTS \(TableClients) (" + + "guid TEXT PRIMARY KEY, " + + "name TEXT NOT NULL, " + + "modified INTEGER NOT NULL, " + + "type TEXT, " + + "formfactor TEXT, " + + "os TEXT, " + + "version TEXT, " + + "fxaDeviceId TEXT" + + ")" + + let tabsTableCreate = + "CREATE TABLE IF NOT EXISTS \(TableTabs) (" + + "id INTEGER PRIMARY KEY AUTOINCREMENT, " + + "client_guid TEXT REFERENCES clients(guid) ON DELETE CASCADE, " + + "url TEXT NOT NULL, " + + "title TEXT, " + + "history TEXT, " + + "last_used INTEGER" + + ")" + + let activityStreamBlocklistCreate = + "CREATE TABLE IF NOT EXISTS \(TableActivityStreamBlocklist) (" + + "id INTEGER PRIMARY KEY AUTOINCREMENT, " + + "url TEXT NOT NULL UNIQUE, " + + "created_at DATETIME DEFAULT CURRENT_TIMESTAMP " + + ") " + + let pageMetadataCreate = + "CREATE TABLE IF NOT EXISTS \(TablePageMetadata) (" + + "id INTEGER PRIMARY KEY, " + + "cache_key LONGVARCHAR UNIQUE, " + + "site_url TEXT, " + + "media_url LONGVARCHAR, " + + "title TEXT, " + + "type VARCHAR(32), " + + "description TEXT, " + + "provider_name TEXT, " + + "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " + + "expired_at LONG" + + ") " + + let highlightsCreate = + "CREATE TABLE IF NOT EXISTS \(TableHighlights) (" + + "historyID INTEGER PRIMARY KEY," + + "cache_key LONGVARCHAR," + + "url TEXT," + + "title TEXT," + + "guid TEXT," + + "visitCount INTEGER," + + "visitDate DATETIME," + + "is_bookmarked INTEGER" + + ") " + + let indexPageMetadataCacheKeyCreate = + "CREATE UNIQUE INDEX IF NOT EXISTS \(IndexPageMetadataCacheKey) ON page_metadata (cache_key)" + + let indexPageMetadataSiteURLCreate = + "CREATE UNIQUE INDEX IF NOT EXISTS \(IndexPageMetadataSiteURL) ON page_metadata (site_url)" + + let iconColumns = ", faviconID INTEGER REFERENCES \(TableFavicons)(id) ON DELETE SET NULL" + let mirrorColumns = ", is_overridden TINYINT NOT NULL DEFAULT 0" + + let serverColumns = ", server_modified INTEGER NOT NULL" + // Milliseconds. + ", hasDupe TINYINT NOT NULL DEFAULT 0" // Boolean, 0 (false) if deleted. + + let localColumns = ", local_modified INTEGER" + // Can be null. Client clock. In extremis only. + ", sync_status TINYINT NOT NULL" // SyncStatus enum. Set when changed or created. + + func getBookmarksTableCreationStringForTable(_ table: String, withAdditionalColumns: String="") -> String { + // The stupid absence of naming conventions here is thanks to pre-Sync Weave. Sorry. + // For now we have the simplest possible schema: everything in one. + let sql = + "CREATE TABLE IF NOT EXISTS \(table) " + + + // Shared fields. + "( id INTEGER PRIMARY KEY AUTOINCREMENT" + + ", guid TEXT NOT NULL UNIQUE" + + ", type TINYINT NOT NULL" + // Type enum. + ", date_added INTEGER" + + + // Record/envelope metadata that'll allow us to do merges. + ", is_deleted TINYINT NOT NULL DEFAULT 0" + // Boolean + + ", parentid TEXT" + // GUID + ", parentName TEXT" + + + // Type-specific fields. These should be NOT NULL in many cases, but we're going + // for a sparse schema, so this'll do for now. Enforce these in the application code. + ", feedUri TEXT, siteUri TEXT" + // LIVEMARKS + ", pos INT" + // SEPARATORS + ", title TEXT, description TEXT" + // FOLDERS, BOOKMARKS, QUERIES + ", bmkUri TEXT, tags TEXT, keyword TEXT" + // BOOKMARKS, QUERIES + ", folderName TEXT, queryId TEXT" + // QUERIES + withAdditionalColumns + + ", CONSTRAINT parentidOrDeleted CHECK (parentid IS NOT NULL OR is_deleted = 1)" + + ", CONSTRAINT parentNameOrDeleted CHECK (parentName IS NOT NULL OR is_deleted = 1)" + + ")" + + return sql + } + + /** + * We need to explicitly store what's provided by the server, because we can't rely on + * referenced child nodes to exist yet! + */ + func getBookmarksStructureTableCreationStringForTable(_ table: String, referencingMirror mirror: String) -> String { + let sql = + "CREATE TABLE IF NOT EXISTS \(table) " + + "( parent TEXT NOT NULL REFERENCES \(mirror)(guid) ON DELETE CASCADE" + + ", child TEXT NOT NULL" + // Should be the GUID of a child. + ", idx INTEGER NOT NULL" + // Should advance from 0. + ")" + + return sql + } + + fileprivate let bufferBookmarksView = + "CREATE VIEW \(ViewBookmarksBufferOnMirror) AS " + + "SELECT" + + " -1 AS id" + + ", mirror.guid AS guid" + + ", mirror.type AS type" + + ", mirror.date_added AS date_added" + + ", mirror.is_deleted AS is_deleted" + + ", mirror.parentid AS parentid" + + ", mirror.parentName AS parentName" + + ", mirror.feedUri AS feedUri" + + ", mirror.siteUri AS siteUri" + + ", mirror.pos AS pos" + + ", mirror.title AS title" + + ", mirror.description AS description" + + ", mirror.bmkUri AS bmkUri" + + ", mirror.keyword AS keyword" + + ", mirror.folderName AS folderName" + + ", null AS faviconID" + + ", 0 AS is_overridden" + + + // LEFT EXCLUDING JOIN to get mirror records that aren't in the buffer. + // We don't have an is_overridden flag to help us here. + " FROM \(TableBookmarksMirror) mirror LEFT JOIN" + + " \(TableBookmarksBuffer) buffer ON mirror.guid = buffer.guid" + + " WHERE buffer.guid IS NULL" + + " UNION ALL " + + "SELECT" + + " -1 AS id" + + ", guid" + + ", type" + + ", date_added" + + ", is_deleted" + + ", parentid" + + ", parentName" + + ", feedUri" + + ", siteUri" + + ", pos" + + ", title" + + ", description" + + ", bmkUri" + + ", keyword" + + ", folderName" + + ", null AS faviconID" + + ", 1 AS is_overridden" + + " FROM \(TableBookmarksBuffer) WHERE is_deleted IS 0" + + fileprivate let bufferBookmarksWithDeletionsView = + "CREATE VIEW \(ViewBookmarksBufferWithDeletionsOnMirror) AS " + + "SELECT" + + " -1 AS id" + + ", mirror.guid AS guid" + + ", mirror.type AS type" + + ", mirror.date_added AS date_added" + + ", mirror.is_deleted AS is_deleted" + + ", mirror.parentid AS parentid" + + ", mirror.parentName AS parentName" + + ", mirror.feedUri AS feedUri" + + ", mirror.siteUri AS siteUri" + + ", mirror.pos AS pos" + + ", mirror.title AS title" + + ", mirror.description AS description" + + ", mirror.bmkUri AS bmkUri" + + ", mirror.keyword AS keyword" + + ", mirror.folderName AS folderName" + + ", null AS faviconID" + + ", 0 AS is_overridden" + + + // LEFT EXCLUDING JOIN to get mirror records that aren't in the buffer. + // We don't have an is_overridden flag to help us here. + " FROM \(TableBookmarksMirror) mirror LEFT JOIN" + + " \(TableBookmarksBuffer) buffer ON mirror.guid = buffer.guid" + + " WHERE buffer.guid IS NULL" + + " UNION ALL " + + "SELECT" + + " -1 AS id" + + ", guid" + + ", type" + + ", date_added" + + ", is_deleted" + + ", parentid" + + ", parentName" + + ", feedUri" + + ", siteUri" + + ", pos" + + ", title" + + ", description" + + ", bmkUri" + + ", keyword" + + ", folderName" + + ", null AS faviconID" + + ", 1 AS is_overridden" + + " FROM \(TableBookmarksBuffer) WHERE is_deleted IS 0" + + " AND NOT EXISTS (SELECT 1 FROM \(TablePendingBookmarksDeletions) deletions WHERE deletions.id = guid)" + + // TODO: phrase this without the subselect… + fileprivate let bufferBookmarksStructureView = + // We don't need to exclude deleted parents, because we drop those from the structure + // table when we see them. + "CREATE VIEW \(ViewBookmarksBufferStructureOnMirror) AS " + + "SELECT parent, child, idx, 1 AS is_overridden FROM \(TableBookmarksBufferStructure) " + + "UNION ALL " + + + // Exclude anything from the mirror that's present in the buffer -- dynamic is_overridden. + "SELECT parent, child, idx, 0 AS is_overridden FROM \(TableBookmarksMirrorStructure) " + + "LEFT JOIN \(TableBookmarksBuffer) ON parent = guid WHERE guid IS NULL" + + fileprivate let localBookmarksView = + "CREATE VIEW \(ViewBookmarksLocalOnMirror) AS " + + "SELECT -1 AS id, guid, type, date_added, is_deleted, parentid, parentName, feedUri," + + " siteUri, pos, title, description, bmkUri, folderName, faviconID, NULL AS local_modified, server_modified, 0 AS is_overridden " + + "FROM \(TableBookmarksMirror) WHERE is_overridden IS NOT 1 " + + "UNION ALL " + + "SELECT -1 AS id, guid, type, date_added, is_deleted, parentid, parentName, feedUri, siteUri, pos, title, description, bmkUri, folderName, faviconID," + + " local_modified, NULL AS server_modified, 1 AS is_overridden " + + "FROM \(TableBookmarksLocal) WHERE is_deleted IS NOT 1" + + // TODO: phrase this without the subselect… + fileprivate let localBookmarksStructureView = + "CREATE VIEW \(ViewBookmarksLocalStructureOnMirror) AS " + + "SELECT parent, child, idx, 1 AS is_overridden FROM \(TableBookmarksLocalStructure) " + + "WHERE " + + "((SELECT is_deleted FROM \(TableBookmarksLocal) WHERE guid = parent) IS NOT 1) " + + "UNION ALL " + + "SELECT parent, child, idx, 0 AS is_overridden FROM \(TableBookmarksMirrorStructure) " + + "WHERE " + + "((SELECT is_overridden FROM \(TableBookmarksMirror) WHERE guid = parent) IS NOT 1) " + + // This view exists only to allow for text searching of URLs and titles in the awesomebar. + // As such, we cheat a little: we include buffer, non-overridden mirror, and local. + // Usually this will be indistinguishable from a more sophisticated approach, and it's way + // easier. + fileprivate let allBookmarksView = + "CREATE VIEW \(ViewAllBookmarks) AS " + + "SELECT guid, bmkUri AS url, title, description, faviconID FROM " + + "\(TableBookmarksMirror) WHERE " + + "type = \(BookmarkNodeType.bookmark.rawValue) AND is_overridden IS 0 AND is_deleted IS 0 " + + "UNION ALL " + + "SELECT guid, bmkUri AS url, title, description, faviconID FROM " + + "\(TableBookmarksLocal) WHERE " + + "type = \(BookmarkNodeType.bookmark.rawValue) AND is_deleted IS 0 " + + "UNION ALL " + + "SELECT guid, bmkUri AS url, title, description, -1 AS faviconID FROM " + + "\(TableBookmarksBuffer) bb WHERE " + + "bb.type = \(BookmarkNodeType.bookmark.rawValue) AND bb.is_deleted IS 0 " + + + // Exclude pending bookmark deletions. + "AND NOT EXISTS (SELECT 1 FROM \(TablePendingBookmarksDeletions) AS pd WHERE pd.id = bb.guid)" + + // This exists only to allow upgrade from old versions. We have view dependencies, so + // we can't simply skip creating ViewAllBookmarks. Here's a stub. + fileprivate let oldAllBookmarksView = + "CREATE VIEW \(ViewAllBookmarks) AS " + + "SELECT guid, bmkUri AS url, title, description, faviconID FROM " + + "\(TableBookmarksMirror) WHERE " + + "type = \(BookmarkNodeType.bookmark.rawValue) AND is_overridden IS 0 AND is_deleted IS 0" + + // This smushes together remote and local visits. So it goes. + fileprivate let historyVisitsView = + "CREATE VIEW \(ViewHistoryVisits) AS " + + "SELECT h.url AS url, MAX(v.date) AS visitDate, h.domain_id AS domain_id FROM " + + "\(TableHistory) h JOIN \(TableVisits) v ON v.siteID = h.id " + + "GROUP BY h.id" + + // Join all bookmarks against history to find the most recent visit. + // visits. + fileprivate let awesomebarBookmarksView = + "CREATE VIEW \(ViewAwesomebarBookmarks) AS " + + "SELECT b.guid AS guid, b.url AS url, b.title AS title, " + + "b.description AS description, b.faviconID AS faviconID, " + + "h.visitDate AS visitDate " + + "FROM \(ViewAllBookmarks) b " + + "LEFT JOIN " + + "\(ViewHistoryVisits) h ON b.url = h.url" + + fileprivate let awesomebarBookmarksWithIconsView = + "CREATE VIEW \(ViewAwesomebarBookmarksWithIcons) AS " + + "SELECT b.guid AS guid, b.url AS url, b.title AS title, " + + "b.description AS description, b.visitDate AS visitDate, " + + "f.id AS iconID, f.url AS iconURL, f.date AS iconDate, " + + "f.type AS iconType, f.width AS iconWidth " + + "FROM \(ViewAwesomebarBookmarks) b " + + "LEFT JOIN " + + "\(TableFavicons) f ON f.id = b.faviconID" + + fileprivate let pendingBookmarksDeletions = + "CREATE TABLE IF NOT EXISTS \(TablePendingBookmarksDeletions) (" + + "id TEXT PRIMARY KEY REFERENCES \(TableBookmarksBuffer)(guid) ON DELETE CASCADE" + + ")" + + fileprivate let remoteDevices = + "CREATE TABLE IF NOT EXISTS \(TableRemoteDevices) (" + + "id INTEGER PRIMARY KEY AUTOINCREMENT, " + + "guid TEXT UNIQUE NOT NULL, " + + "name TEXT NOT NULL, " + + "type TEXT NOT NULL, " + + "is_current_device INTEGER NOT NULL, " + + "date_created INTEGER NOT NULL, " + // Timestamps in ms. + "date_modified INTEGER NOT NULL, " + + "last_access_time INTEGER" + + ")" + + public func create(_ db: SQLiteDBConnection) -> Bool { + let favicons = + "CREATE TABLE IF NOT EXISTS \(TableFavicons) (" + + "id INTEGER PRIMARY KEY AUTOINCREMENT, " + + "url TEXT NOT NULL UNIQUE, " + + "width INTEGER, " + + "height INTEGER, " + + "type INTEGER NOT NULL, " + + "date REAL NOT NULL" + + ") " + + let history = + "CREATE TABLE IF NOT EXISTS \(TableHistory) (" + + "id INTEGER PRIMARY KEY AUTOINCREMENT, " + + "guid TEXT NOT NULL UNIQUE, " + // Not null, but the value might be replaced by the server's. + "url TEXT UNIQUE, " + // May only be null for deleted records. + "title TEXT NOT NULL, " + + "server_modified INTEGER, " + // Can be null. Integer milliseconds. + "local_modified INTEGER, " + // Can be null. Client clock. In extremis only. + "is_deleted TINYINT NOT NULL, " + // Boolean. Locally deleted. + "should_upload TINYINT NOT NULL, " + // Boolean. Set when changed or visits added. + "domain_id INTEGER REFERENCES \(TableDomains)(id) ON DELETE CASCADE, " + + "CONSTRAINT urlOrDeleted CHECK (url IS NOT NULL OR is_deleted = 1)" + + ")" + + // Right now we don't need to track per-visit deletions: Sync can't + // represent them! See Bug 1157553 Comment 6. + // We flip the should_upload flag on the history item when we add a visit. + // If we ever want to support logic like not bothering to sync if we added + // and then rapidly removed a visit, then we need an 'is_new' flag on each visit. + let visits = + "CREATE TABLE IF NOT EXISTS \(TableVisits) (" + + "id INTEGER PRIMARY KEY AUTOINCREMENT, " + + "siteID INTEGER NOT NULL REFERENCES \(TableHistory)(id) ON DELETE CASCADE, " + + "date REAL NOT NULL, " + // Microseconds since epoch. + "type INTEGER NOT NULL, " + + "is_local TINYINT NOT NULL, " + // Some visits are local. Some are remote ('mirrored'). This boolean flag is the split. + "UNIQUE (siteID, date, type) " + + ") " + + let indexShouldUpload: String + if self.supportsPartialIndices { + // There's no point tracking rows that are not flagged for upload. + indexShouldUpload = + "CREATE INDEX IF NOT EXISTS \(IndexHistoryShouldUpload) " + + "ON \(TableHistory) (should_upload) WHERE should_upload = 1" + } else { + indexShouldUpload = + "CREATE INDEX IF NOT EXISTS \(IndexHistoryShouldUpload) " + + "ON \(TableHistory) (should_upload)" + } + + let indexSiteIDDate = + "CREATE INDEX IF NOT EXISTS \(IndexVisitsSiteIDIsLocalDate) " + + "ON \(TableVisits) (siteID, is_local, date)" + + let faviconSites = + "CREATE TABLE IF NOT EXISTS \(TableFaviconSites) (" + + "id INTEGER PRIMARY KEY AUTOINCREMENT, " + + "siteID INTEGER NOT NULL REFERENCES \(TableHistory)(id) ON DELETE CASCADE, " + + "faviconID INTEGER NOT NULL REFERENCES \(TableFavicons)(id) ON DELETE CASCADE, " + + "UNIQUE (siteID, faviconID) " + + ") " + + let widestFavicons = + "CREATE VIEW IF NOT EXISTS \(ViewWidestFaviconsForSites) AS " + + "SELECT " + + "\(TableFaviconSites).siteID AS siteID, " + + "\(TableFavicons).id AS iconID, " + + "\(TableFavicons).url AS iconURL, " + + "\(TableFavicons).date AS iconDate, " + + "\(TableFavicons).type AS iconType, " + + "MAX(\(TableFavicons).width) AS iconWidth " + + "FROM \(TableFaviconSites), \(TableFavicons) WHERE " + + "\(TableFaviconSites).faviconID = \(TableFavicons).id " + + "GROUP BY siteID " + + let historyIDsWithIcon = + "CREATE VIEW IF NOT EXISTS \(ViewHistoryIDsWithWidestFavicons) AS " + + "SELECT \(TableHistory).id AS id, " + + "iconID, iconURL, iconDate, iconType, iconWidth " + + "FROM \(TableHistory) " + + "LEFT OUTER JOIN " + + "\(ViewWidestFaviconsForSites) ON history.id = \(ViewWidestFaviconsForSites).siteID " + + let iconForURL = + "CREATE VIEW IF NOT EXISTS \(ViewIconForURL) AS " + + "SELECT history.url AS url, icons.iconID AS iconID FROM " + + "\(TableHistory), \(ViewWidestFaviconsForSites) AS icons WHERE " + + "\(TableHistory).id = icons.siteID " + + // Locally we track faviconID. + // Local changes end up in the mirror, so we track it there too. + // The buffer and the mirror additionally track some server metadata. + let bookmarksLocal = getBookmarksTableCreationStringForTable(TableBookmarksLocal, withAdditionalColumns: self.localColumns + self.iconColumns) + let bookmarksLocalStructure = getBookmarksStructureTableCreationStringForTable(TableBookmarksLocalStructure, referencingMirror: TableBookmarksLocal) + let bookmarksBuffer = getBookmarksTableCreationStringForTable(TableBookmarksBuffer, withAdditionalColumns: self.serverColumns) + let bookmarksBufferStructure = getBookmarksStructureTableCreationStringForTable(TableBookmarksBufferStructure, referencingMirror: TableBookmarksBuffer) + let bookmarksMirror = getBookmarksTableCreationStringForTable(TableBookmarksMirror, withAdditionalColumns: self.serverColumns + self.mirrorColumns + self.iconColumns) + let bookmarksMirrorStructure = getBookmarksStructureTableCreationStringForTable(TableBookmarksMirrorStructure, referencingMirror: TableBookmarksMirror) + + let indexLocalStructureParentIdx = "CREATE INDEX IF NOT EXISTS \(IndexBookmarksLocalStructureParentIdx) " + + "ON \(TableBookmarksLocalStructure) (parent, idx)" + let indexBufferStructureParentIdx = "CREATE INDEX IF NOT EXISTS \(IndexBookmarksBufferStructureParentIdx) " + + "ON \(TableBookmarksBufferStructure) (parent, idx)" + let indexMirrorStructureParentIdx = "CREATE INDEX IF NOT EXISTS \(IndexBookmarksMirrorStructureParentIdx) " + + "ON \(TableBookmarksMirrorStructure) (parent, idx)" + let indexMirrorStructureChild = "CREATE INDEX IF NOT EXISTS \(IndexBookmarksMirrorStructureChild) " + + "ON \(TableBookmarksMirrorStructure) (child)" + + let queries: [String] = [ + // Tables. + self.domainsTableCreate, + history, + favicons, + visits, + bookmarksBuffer, + bookmarksBufferStructure, + bookmarksLocal, + bookmarksLocalStructure, + bookmarksMirror, + bookmarksMirrorStructure, + self.pendingBookmarksDeletions, + faviconSites, + widestFavicons, + historyIDsWithIcon, + iconForURL, + pageMetadataCreate, + pinnedTopSitesTableCreate, + highlightsCreate, + self.remoteDevices, + activityStreamBlocklistCreate, + indexPageMetadataSiteURLCreate, + indexPageMetadataCacheKeyCreate, + self.queueTableCreate, + self.topSitesTableCreate, + syncCommandsTableCreate, + clientsTableCreate, + tabsTableCreate, + + // Indices. + indexBufferStructureParentIdx, + indexLocalStructureParentIdx, + indexMirrorStructureParentIdx, + indexMirrorStructureChild, + indexShouldUpload, + indexSiteIDDate, + + // Views. + self.localBookmarksView, + self.localBookmarksStructureView, + self.bufferBookmarksView, + self.bufferBookmarksWithDeletionsView, + self.bufferBookmarksStructureView, + allBookmarksView, + historyVisitsView, + awesomebarBookmarksView, + awesomebarBookmarksWithIconsView, + ] + + assert(queries.count == AllTablesIndicesAndViews.count, "Did you forget to add your table, index, or view to the list?") + + log.debug("Creating \(queries.count) tables, views, and indices.") + + return self.run(db, queries: queries) && + self.prepopulateRootFolders(db) + } + + public func update(_ db: SQLiteDBConnection, from: Int) -> Bool { + let to = self.version + if from == to { + log.debug("Skipping update from \(from) to \(to).") + return true + } + + if from == 0 { + + // If we're upgrading from `0`, it is likely that we have not yet switched + // from tracking the schema version using `tableList` to `PRAGMA user_version`. + // This will write the *previous* version number from `tableList` into + // `PRAGMA user_version` if necessary in addition to upgrading the schema of + // some of the old tables that were previously managed separately. + if self.migrateFromSchemaTableIfNeeded(db) { + let version = db.version + + // If the database is now properly reporting a `user_version`, it may + // still need to be upgraded further to get to the current version of + // the schema. So, let's simply call this `update()` function a second + // time to handle any remaining post-v31 migrations. + if version > 0 { + return self.update(db, from: version) + } + } + + // Otherwise, this is likely an upgrade from before Bug 1160399, so + // let's drop and re-create. + log.debug("Updating schema \(self.name) from zero. Assuming drop and recreate.") + return drop(db) && create(db) + } + + if from > to { + // This is likely an upgrade from before Bug 1160399. + log.debug("Downgrading browser tables. Assuming drop and recreate.") + return drop(db) && create(db) + } + + log.debug("Updating schema \(self.name) from \(from) to \(to).") + + if from < 4 && to >= 4 { + return drop(db) && create(db) + } + + if from < 5 && to >= 5 { + if !self.run(db, sql: self.queueTableCreate) { + return false + } + } + + if from < 6 && to >= 6 { + if !self.run(db, queries: [ + "DROP INDEX IF EXISTS \(IndexVisitsSiteIDDate)", + "CREATE INDEX IF NOT EXISTS \(IndexVisitsSiteIDIsLocalDate) ON \(TableVisits) (siteID, is_local, date)", + self.domainsTableCreate, + "ALTER TABLE \(TableHistory) ADD COLUMN domain_id INTEGER REFERENCES \(TableDomains)(id) ON DELETE CASCADE", + ]) { + return false + } + + let urls = db.executeQuery("SELECT DISTINCT url FROM \(TableHistory) WHERE url IS NOT NULL", + factory: { $0["url"] as! String }) + if !fillDomainNamesFromCursor(urls, db: db) { + return false + } + } + + if from < 8 && to == 8 { + // Nothing to do: we're just shifting the favicon table to be owned by this class. + return true + } + + if from < 9 && to >= 9 { + if !self.run(db, sql: getBookmarksTableCreationStringForTable(TableBookmarksMirror)) { + return false + } + } + + if from < 10 && to >= 10 { + if !self.run(db, sql: getBookmarksStructureTableCreationStringForTable(TableBookmarksMirrorStructure, referencingMirror: TableBookmarksMirror)) { + return false + } + + let indexStructureParentIdx = "CREATE INDEX IF NOT EXISTS \(IndexBookmarksMirrorStructureParentIdx) " + + "ON \(TableBookmarksMirrorStructure) (parent, idx)" + if !self.run(db, sql: indexStructureParentIdx) { + return false + } + } + + if from < 11 && to >= 11 { + if !self.run(db, sql: self.topSitesTableCreate) { + return false + } + } + + if from < 12 && to >= 12 { + let bookmarksLocal = getBookmarksTableCreationStringForTable(TableBookmarksLocal, withAdditionalColumns: self.localColumns + self.iconColumns) + let bookmarksLocalStructure = getBookmarksStructureTableCreationStringForTable(TableBookmarksLocalStructure, referencingMirror: TableBookmarksLocal) + let bookmarksMirror = getBookmarksTableCreationStringForTable(TableBookmarksMirror, withAdditionalColumns: self.serverColumns + self.mirrorColumns + self.iconColumns) + let bookmarksMirrorStructure = getBookmarksStructureTableCreationStringForTable(TableBookmarksMirrorStructure, referencingMirror: TableBookmarksMirror) + + let indexLocalStructureParentIdx = "CREATE INDEX IF NOT EXISTS \(IndexBookmarksLocalStructureParentIdx) " + + "ON \(TableBookmarksLocalStructure) (parent, idx)" + let indexBufferStructureParentIdx = "CREATE INDEX IF NOT EXISTS \(IndexBookmarksBufferStructureParentIdx) " + + "ON \(TableBookmarksBufferStructure) (parent, idx)" + let indexMirrorStructureParentIdx = "CREATE INDEX IF NOT EXISTS \(IndexBookmarksMirrorStructureParentIdx) " + + "ON \(TableBookmarksMirrorStructure) (parent, idx)" + + let prep = [ + // Drop indices. + "DROP INDEX IF EXISTS idx_bookmarksMirrorStructure_parent_idx", + + // Rename the old mirror tables to buffer. + // The v11 one is the same shape as the current buffer table. + "ALTER TABLE \(TableBookmarksMirror) RENAME TO \(TableBookmarksBuffer)", + "ALTER TABLE \(TableBookmarksMirrorStructure) RENAME TO \(TableBookmarksBufferStructure)", + + // Create the new mirror and local tables. + bookmarksLocal, + bookmarksMirror, + bookmarksLocalStructure, + bookmarksMirrorStructure, + ] + + // Only migrate bookmarks. The only folders are our roots, and we'll create those later. + // There should be nothing else in the table, and no structure. + // Our old bookmarks table didn't have creation date, so we use the current timestamp. + let modified = Date.now() + let status = SyncStatus.new.rawValue + + // We don't specify a title, expecting it to be generated on the fly, because we're smarter than Android. + // We also don't migrate the 'id' column; we'll generate new ones that won't conflict with our roots. + let migrateArgs: Args = [BookmarkRoots.MobileFolderGUID] + let migrateLocal = + "INSERT INTO \(TableBookmarksLocal) " + + "(guid, type, bmkUri, title, faviconID, local_modified, sync_status, parentid, parentName) " + + "SELECT guid, type, url AS bmkUri, title, faviconID, " + + "\(modified) AS local_modified, \(status) AS sync_status, ?, '' " + + "FROM \(_TableBookmarks) WHERE type IS \(BookmarkNodeType.bookmark.rawValue)" + + // Create structure for our migrated bookmarks. + // In order to get contiguous positions (idx), we first insert everything we just migrated under + // Mobile Bookmarks into a temporary table, then use rowid as our idx. + + let temporaryTable = + "CREATE TEMPORARY TABLE children AS " + + "SELECT guid FROM \(_TableBookmarks) WHERE " + + "type IS \(BookmarkNodeType.bookmark.rawValue) ORDER BY id ASC" + + let createStructure = + "INSERT INTO \(TableBookmarksLocalStructure) (parent, child, idx) " + + "SELECT ? AS parent, guid AS child, (rowid - 1) AS idx FROM children" + + let migrate: [(String, Args?)] = [ + (migrateLocal, migrateArgs), + (temporaryTable, nil), + (createStructure, migrateArgs), + + // Drop the temporary table. + ("DROP TABLE children", nil), + + // Drop the old bookmarks table. + ("DROP TABLE \(_TableBookmarks)", nil), + + // Create indices for each structure table. + (indexBufferStructureParentIdx, nil), + (indexLocalStructureParentIdx, nil), + (indexMirrorStructureParentIdx, nil), + ] + + if !self.run(db, queries: prep) || + !self.prepopulateRootFolders(db) || + !self.run(db, queries: migrate) { + return false + } + // TODO: trigger a sync? + } + + // Add views for the overlays. + if from < 14 && to >= 14 { + let indexMirrorStructureChild = + "CREATE INDEX IF NOT EXISTS \(IndexBookmarksMirrorStructureChild) " + + "ON \(TableBookmarksMirrorStructure) (child)" + if !self.run(db, queries: [ + self.bufferBookmarksStructureView, + self.localBookmarksStructureView, + indexMirrorStructureChild]) { + return false + } + } + + if from == 14 && to >= 15 { + // We screwed up some of the views. Recreate them. + if !self.run(db, queries: [ + "DROP VIEW IF EXISTS \(ViewBookmarksBufferStructureOnMirror)", + "DROP VIEW IF EXISTS \(ViewBookmarksLocalStructureOnMirror)", + self.bufferBookmarksStructureView, + self.localBookmarksStructureView]) { + return false + } + } + + if from < 16 && to >= 16 { + if !self.run(db, queries: [ + oldAllBookmarksView, // Replaced in v30. The new one is not compatible here. + historyVisitsView, + awesomebarBookmarksView, // … but this depends on ViewAllBookmarks. + awesomebarBookmarksWithIconsView]) { + return false + } + } + + // That view is re-created later + // if from < 17 && to >= 17 { + // if !self.run(db, queries: [ + // // Adds the local_modified, server_modified times to the local bookmarks view + // "DROP VIEW IF EXISTS \(ViewBookmarksLocalOnMirror)", + // self.localBookmarksView]) { + // return false + // } + // } + + if from < 18 && to >= 18 { + if !self.run(db, queries: [ + // Adds the Activity Stream blocklist table + activityStreamBlocklistCreate]) { + return false + } + } + + if from < 19 && to >= 19 { + if !self.run(db, queries: [ + // Adds tables/indicies for metadata content + pageMetadataCreate, + indexPageMetadataCacheKeyCreate]) { + return false + } + } + + // That view is re-created later + // if from < 20 && to >= 20 { + // if !self.run(db, queries: [ + // "DROP VIEW IF EXISTS \(ViewBookmarksBufferOnMirror)", + // self.bufferBookmarksView]) { + // return false + // } + // } + + if from < 21 && to >= 21 { + if !self.run(db, queries: [ + "DROP VIEW IF EXISTS \(ViewHistoryVisits)", + self.historyVisitsView, + indexPageMetadataSiteURLCreate]) { + return false + } + } + + // Someone upgrading from v21 will get these tables anyway. + // So, there's no need to create them only to be dropped and + // re-created at v27 anyway. + // if from < 22 && to >= 22 { + // if !self.run(db, queries: [ + // "DROP TABLE IF EXISTS \(TablePageMetadata)", + // pageMetadataCreate, + // indexPageMetadataCacheKeyCreate, + // indexPageMetadataSiteURLCreate]) { + // return false + // } + // } + // + // if from < 23 && to >= 23 { + // if !self.run(db, queries: [ + // highlightsCreate]) { + // return false + // } + // } + // + // if from < 24 && to >= 24 { + // if !self.run(db, queries: [ + // // We can safely drop the highlights cache table since it gets cleared on every invalidate anyways. + // "DROP TABLE IF EXISTS \(TableHighlights)", + // highlightsCreate + // ]) { + // return false + // } + // } + + // Someone upgrading from v21 will get this table anyway. + // So, there's no need to create it only to be dropped and + // re-created at v26 anyway. + // if from < 25 && to >= 25 { + // if !self.run(db, queries: [ + // pinnedTopSitesTableCreate + // ]) { + // return false + // } + // } + + if from < 26 && to >= 26 { + if !self.run(db, queries: [ + // The old pin table was never released so we can safely drop + "DROP TABLE IF EXISTS \(TablePinnedTopSites)", + pinnedTopSitesTableCreate + ]) { + return false + } + } + + if from < 27 && to >= 27 { + if !self.run(db, queries: [ + "DROP TABLE IF EXISTS \(TablePageMetadata)", + "DROP TABLE IF EXISTS \(TableHighlights)", + pageMetadataCreate, + indexPageMetadataCacheKeyCreate, + indexPageMetadataSiteURLCreate, + highlightsCreate + ]) { + return false + } + } + + if from < 28 && to >= 28 { + if !self.run(db, queries: [ + self.pendingBookmarksDeletions + ]) { + return false + } + } + + if from < 29 && to >= 29 { + if !self.run(db, queries: [ + self.remoteDevices + ]) { + return false + } + } + + if from < 30 && to >= 30 { + // We changed this view as a follow-up to the above in order to exclude buffer + // deletions from the bookmarked set. + if !self.run(db, queries: [ + "DROP VIEW IF EXISTS \(ViewAllBookmarks)", + allBookmarksView + ]) { + return false + } + } + + // NOTE: These tables should have already existed in prior + // versions, but were managed separately via the SchemaTable. + // Here we create them if they don't already exist to handle + // cases where we are creating a brand new DB. + if from < 31 && to >= 31 { + if !self.run(db, queries: [ + syncCommandsTableCreate, + clientsTableCreate, + tabsTableCreate + ]) { + return false + } + } + + if from < 32 && to >= 32 { + var queries: [String] = [] + // If upgrading from < 12 these tables are created with that column already present. + if from > 12 { + queries.append(contentsOf: [ + "ALTER TABLE \(TableBookmarksLocal) ADD date_added INTEGER", + "ALTER TABLE \(TableBookmarksMirror) ADD date_added INTEGER", + "ALTER TABLE \(TableBookmarksBuffer) ADD date_added INTEGER" + ]) + } + queries.append(contentsOf: [ + "UPDATE \(TableBookmarksLocal) SET date_added = local_modified", + "UPDATE \(TableBookmarksMirror) SET date_added = server_modified" + ]) + if !self.run(db, queries: queries) { + return false + } + } + + if from < 33 && to >= 33 { + if !self.run(db, queries: [ + "DROP VIEW IF EXISTS \(ViewBookmarksBufferOnMirror)", + "DROP VIEW IF EXISTS \(ViewBookmarksBufferWithDeletionsOnMirror)", + "DROP VIEW IF EXISTS \(ViewBookmarksLocalOnMirror)", + self.bufferBookmarksView, + self.bufferBookmarksWithDeletionsView, + self.localBookmarksView + ]) { + return false + } + } + + if from < 34 && to >= 34 { + // Drop over-large items from the database, and truncate + // over-long titles. + // We do this once, and only for local bookmarks: if they + // already escaped, then it's better to let them be. + // Hard-code values here both for simplicity and to make + // migrations predictable. + // We don't need to worry about description: we never wrote it. + if !self.run(db, queries: [ + "DELETE FROM \(TableHistory) WHERE is_deleted = 0 AND length(url) > 65536", + "DELETE FROM \(TablePageMetadata) WHERE length(site_url) > 65536", + "DELETE FROM \(TableBookmarksLocal) WHERE is_deleted = 0 AND length(bmkUri) > 65536", + "UPDATE \(TableBookmarksLocal) SET title = substr(title, 1, 4096)" + + " WHERE is_deleted = 0 AND length(title) > 4096", + ]) { + return false + } + } + + return true + } + + fileprivate func migrateFromSchemaTableIfNeeded(_ db: SQLiteDBConnection) -> Bool { + log.info("Checking if schema table migration is needed.") + + // If `PRAGMA user_version` is v31 or later, we don't need to do anything here. + guard db.version < 31 else { + return true + } + + // Query for the existence of the `tableList` table to determine if we are + // migrating from an older DB version or if this is just a brand new DB. + let sqliteMasterCursor = db.executeQueryUnsafe("SELECT COUNT(*) AS number FROM sqlite_master WHERE type = 'table' AND name = 'tableList'", factory: IntFactory, withArgs: [] as Args) + + let tableListTableExists = sqliteMasterCursor[0] == 1 + sqliteMasterCursor.close() + + // If `tableList` still exists in this DB, then we need to continue to check if + // any table-specific migrations are required before removing it. Otherwise, if + // `tableList` does not exist, it is likely due to this being a brand new DB and + // no additional steps need to be taken at this point. + guard tableListTableExists else { + return true + } + + // If we are unable to migrate the `clients` table from the schema table, we + // have failed and cannot continue. + guard migrateClientsTableFromSchemaTableIfNeeded(db) != .failure else { + return false + } + + // Get the *previous* schema version (prior to v31) specified in `tableList` + // before dropping it. + let previousVersionCursor = db.executeQueryUnsafe("SELECT version FROM tableList WHERE name = 'BROWSER'", factory: IntFactory, withArgs: [] as Args) + + let previousVersion = previousVersionCursor[0] ?? 0 + previousVersionCursor.close() + + // No other intermediate migrations are needed for the remaining tables and + // we have already captured the *previous* schema version specified in + // `tableList`, so we can now safely drop it. + log.info("Schema table migrations complete; Dropping 'tableList' table.") + + let sql = "DROP TABLE IF EXISTS tableList" + do { + try db.executeChange(sql) + } catch let err as NSError { + Sentry.shared.sendWithStacktrace(message: "Error dropping tableList table", tag: SentryTag.browserDB, severity: .error, description: "\(err.localizedDescription)") + return false + } + + // Lastly, write the *previous* schema version (prior to v31) to the database + // using `PRAGMA user_version = ?`. + do { + try db.setVersion(previousVersion) + } catch let err as NSError { + Sentry.shared.sendWithStacktrace(message: "Error setting database version", tag: SentryTag.browserDB, severity: .error, description: "\(err.localizedDescription)") + return false + } + + return true + } + + // Performs the intermediate migrations for the `clients` table that were previously + // being handled by the schema table. This should update older versions of the `clients` + // table prior to v31. If the `clients` table is able to be successfully migrated, this + // will return `.success`. If no migration is required because either the `clients` table + // is already at v31 or it does not exist yet at all, this will return `.skipped`. + // Otherwise, if the `clients` table migration is needed and an error was encountered, we + // return `.failure`. + fileprivate func migrateClientsTableFromSchemaTableIfNeeded(_ db: SQLiteDBConnection) -> SchemaUpgradeResult { + // Query for the existence of the `clients` table to determine if we are + // migrating from an older DB version or if this is just a brand new DB. + let sqliteMasterCursor = db.executeQueryUnsafe("SELECT COUNT(*) AS number FROM sqlite_master WHERE type = 'table' AND name = '\(TableClients)'", factory: IntFactory, withArgs: [] as Args) + + let clientsTableExists = sqliteMasterCursor[0] == 1 + sqliteMasterCursor.close() + + guard clientsTableExists else { + return .skipped + } + + // Check if intermediate migrations are necessary for the 'clients' table. + let previousVersionCursor = db.executeQueryUnsafe("SELECT version FROM tableList WHERE name = '\(TableClients)'", factory: IntFactory, withArgs: [] as Args) + + let previousClientsTableVersion = previousVersionCursor[0] ?? 0 + previousVersionCursor.close() + + guard previousClientsTableVersion > 0 && previousClientsTableVersion <= 3 else { + return .skipped + } + + log.info("Migrating '\(TableClients)' table from version \(previousClientsTableVersion).") + + if previousClientsTableVersion < 2 { + let sql = "ALTER TABLE \(TableClients) ADD COLUMN version TEXT" + do { + try db.executeChange(sql) + } catch let err as NSError { + log.error("Error altering \(TableClients) table: \(err.localizedDescription); SQL was \(sql)") + let extra = ["table": "\(TableClients)", "errorDescription": "\(err.localizedDescription)", "sql": "\(sql)"] + Sentry.shared.sendWithStacktrace(message: "Error altering table", tag: SentryTag.browserDB, severity: .error, extra: extra) + return .failure + } + } + + if previousClientsTableVersion < 3 { + let sql = "ALTER TABLE \(TableClients) ADD COLUMN fxaDeviceId TEXT" + do { + try db.executeChange(sql) + } catch let err as NSError { + log.error("Error altering \(TableClients) table: \(err.localizedDescription); SQL was \(sql)") + let extra = ["table": "\(TableClients)", "errorDescription": "\(err.localizedDescription)", "sql": "\(sql)"] + Sentry.shared.sendWithStacktrace(message: "Error altering table", tag: SentryTag.browserDB, severity: .error, extra: extra) + return .failure + } + } + + return .success + } + + fileprivate func fillDomainNamesFromCursor(_ cursor: Cursor, db: SQLiteDBConnection) -> Bool { + if cursor.count == 0 { + return true + } + + // URL -> hostname, flattened to make args. + var pairs = Args() + pairs.reserveCapacity(cursor.count * 2) + for url in cursor { + if let url = url, let host = url.asURL?.normalizedHost { + pairs.append(url) + pairs.append(host) + } + } + cursor.close() + + let tmpTable = "tmp_hostnames" + let table = "CREATE TEMP TABLE \(tmpTable) (url TEXT NOT NULL UNIQUE, domain TEXT NOT NULL, domain_id INT)" + if !self.run(db, sql: table, args: nil) { + log.error("Can't create temporary table. Unable to migrate domain names. Top Sites is likely to be broken.") + return false + } + + // Now insert these into the temporary table. Chunk by an even number, for obvious reasons. + let chunks = chunk(pairs, by: BrowserDB.MaxVariableNumber - (BrowserDB.MaxVariableNumber % 2)) + for chunk in chunks { + let ins = "INSERT INTO \(tmpTable) (url, domain) VALUES " + + Array(repeating: "(?, ?)", count: chunk.count / 2).joined(separator: ", ") + if !self.run(db, sql: ins, args: Array(chunk)) { + log.error("Couldn't insert domains into temporary table. Aborting migration.") + return false + } + } + + // Now make those into domains. + let domains = "INSERT OR IGNORE INTO \(TableDomains) (domain) SELECT DISTINCT domain FROM \(tmpTable)" + + // … and fill that temporary column. + let domainIDs = "UPDATE \(tmpTable) SET domain_id = (SELECT id FROM \(TableDomains) WHERE \(TableDomains).domain = \(tmpTable).domain)" + + // Update the history table from the temporary table. + let updateHistory = "UPDATE \(TableHistory) SET domain_id = (SELECT domain_id FROM \(tmpTable) WHERE \(tmpTable).url = \(TableHistory).url)" + + // Clean up. + let dropTemp = "DROP TABLE \(tmpTable)" + + // Now run these. + if !self.run(db, queries: [domains, + domainIDs, + updateHistory, + dropTemp]) { + log.error("Unable to migrate domains.") + return false + } + + return true + } + + public func drop(_ db: SQLiteDBConnection) -> Bool { + log.debug("Dropping all browser tables.") + let additional = [ + "DROP TABLE IF EXISTS faviconSites" // We renamed it to match naming convention. + ] + + let views = AllViews.map { "DROP VIEW IF EXISTS \($0)" } + let indices = AllIndices.map { "DROP INDEX IF EXISTS \($0)" } + let tables = AllTables.map { "DROP TABLE IF EXISTS \($0)" } + let queries = Array([views, indices, tables, additional].joined()) + return self.run(db, queries: queries) + } +} diff --git a/mobile/ios/Storage/SQL/LoginsSchema.swift b/mobile/ios/Storage/SQL/LoginsSchema.swift new file mode 100644 index 0000000000..92877cffc8 --- /dev/null +++ b/mobile/ios/Storage/SQL/LoginsSchema.swift @@ -0,0 +1,123 @@ +/* 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 XCGLogger + +let TableLoginsMirror = "loginsM" +let TableLoginsLocal = "loginsL" +let IndexLoginsOverrideHostname = "idx_loginsM_is_overridden_hostname" +let IndexLoginsDeletedHostname = "idx_loginsL_is_deleted_hostname" + +private let AllTables: [String] = [ + TableLoginsMirror, + TableLoginsLocal +] + +private let log = Logger.syncLogger + +open class LoginsSchema: Schema { + static let DefaultVersion = 3 + + public var name: String { return "LOGINS" } + public var version: Int { return LoginsSchema.DefaultVersion } + + public init() {} + + func run(_ db: SQLiteDBConnection, sql: String, args: Args? = nil) -> Bool { + do { + try db.executeChange(sql, withArgs: args) + } catch let err as NSError { + log.error("Error running SQL in LoginsSchema: \(err.localizedDescription)") + log.error("SQL was \(sql)") + return false + } + + return true + } + + // TODO: transaction. + func run(_ db: SQLiteDBConnection, queries: [String]) -> Bool { + for sql in queries { + if !run(db, sql: sql, args: nil) { + return false + } + } + return true + } + + let indexIsOverriddenHostname = + "CREATE INDEX IF NOT EXISTS \(IndexLoginsOverrideHostname) ON \(TableLoginsMirror) (is_overridden, hostname)" + + let indexIsDeletedHostname = + "CREATE INDEX IF NOT EXISTS \(IndexLoginsDeletedHostname) ON \(TableLoginsLocal) (is_deleted, hostname)" + + public func create(_ db: SQLiteDBConnection) -> Bool { + let common = + "id INTEGER PRIMARY KEY AUTOINCREMENT" + + ", hostname TEXT NOT NULL" + + ", httpRealm TEXT" + + ", formSubmitURL TEXT" + + ", usernameField TEXT" + + ", passwordField TEXT" + + ", timesUsed INTEGER NOT NULL DEFAULT 0" + + ", timeCreated INTEGER NOT NULL" + + ", timeLastUsed INTEGER" + + ", timePasswordChanged INTEGER NOT NULL" + + ", username TEXT" + + ", password TEXT NOT NULL" + + let mirror = "CREATE TABLE IF NOT EXISTS \(TableLoginsMirror) (" + + common + + ", guid TEXT NOT NULL UNIQUE" + + ", server_modified INTEGER NOT NULL" + // Integer milliseconds. + ", is_overridden TINYINT NOT NULL DEFAULT 0" + + ")" + + let local = "CREATE TABLE IF NOT EXISTS \(TableLoginsLocal) (" + + common + + ", guid TEXT NOT NULL UNIQUE " + // Typically overlaps one in the mirror unless locally new. + ", local_modified INTEGER" + // Can be null. Client clock. In extremis only. + ", is_deleted TINYINT NOT NULL DEFAULT 0" + // Boolean. Locally deleted. + ", sync_status TINYINT " + // SyncStatus enum. Set when changed or created. + "NOT NULL DEFAULT \(SyncStatus.synced.rawValue)" + + ")" + return self.run(db, queries: [mirror, local, indexIsOverriddenHostname, indexIsDeletedHostname]) + } + + public func update(_ db: SQLiteDBConnection, from: Int) -> Bool { + let to = self.version + if from == to { + log.debug("Skipping update from \(from) to \(to).") + return true + } + + if from == 0 { + // This is likely an upgrade from before Bug 1160399. + log.debug("Updating logins tables from zero. Assuming drop and recreate.") + return drop(db) && create(db) + } + + if from < 3 && to >= 3 { + log.debug("Updating logins tables to include version 3 indices") + return self.run(db, queries: [indexIsOverriddenHostname, indexIsDeletedHostname]) + } + + // TODO: real update! + log.debug("Updating logins table from \(from) to \(to).") + return drop(db) && create(db) + } + + public func drop(_ db: SQLiteDBConnection) -> Bool { + log.debug("Dropping logins table.") + do { + try db.executeChange("DROP TABLE IF EXISTS \(name)") + } catch { + return false + } + + return true + } +} diff --git a/mobile/ios/Storage/SQL/SQLiteBookmarksBase.swift b/mobile/ios/Storage/SQL/SQLiteBookmarksBase.swift new file mode 100644 index 0000000000..8845a6b4c4 --- /dev/null +++ b/mobile/ios/Storage/SQL/SQLiteBookmarksBase.swift @@ -0,0 +1,65 @@ +/* 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 Deferred +import Foundation +import Shared + +private let log = Logger.syncLogger + +class NoSuchSearchKeywordError: MaybeErrorType { + let keyword: String + init(keyword: String) { + self.keyword = keyword + } + var description: String { + return "No such search keyword: \(keyword)." + } +} + +open class SQLiteBookmarks: BookmarksModelFactorySource, KeywordSearchSource { + let db: BrowserDB + let favicons: SQLiteFavicons + + static let defaultFolderTitle: String = NSLocalizedString("Untitled", tableName: "Storage", comment: "The default name for bookmark folders without titles.") + static let defaultItemTitle: String = NSLocalizedString("Untitled", tableName: "Storage", comment: "The default name for bookmark nodes without titles.") + + open lazy var modelFactory: Deferred> = + deferMaybe(SQLiteBookmarksModelFactory(bookmarks: self, direction: .local)) + + public init(db: BrowserDB) { + self.db = db + self.favicons = SQLiteFavicons(db: self.db) + } + + open func isBookmarked(_ url: String, direction: Direction) -> Deferred> { + let sql = "SELECT id FROM " + + "(SELECT id FROM \(direction.valueTable) WHERE " + + " bmkUri = ? AND is_deleted IS NOT 1" + + " UNION ALL " + + " SELECT id FROM \(TableBookmarksMirror) WHERE " + + " bmkUri = ? AND is_deleted IS NOT 1 AND is_overridden IS NOT 1" + + " LIMIT 1)" + let args: Args = [url, url] + + return self.db.queryReturnsResults(sql, args: args) + } + + open func getURLForKeywordSearch(_ keyword: String) -> Deferred> { + let sql = "SELECT bmkUri FROM \(ViewBookmarksBufferOnMirror) WHERE " + + " keyword = ?" + let args: Args = [keyword] + + return self.db.runQuery(sql, args: args, factory: { $0["bmkUri"] as! String }) + >>== { cursor in + if cursor.status == .success { + if let str = cursor[0] { + return deferMaybe(str) + } + } + + return deferMaybe(NoSuchSearchKeywordError(keyword: keyword)) + } + } +} diff --git a/mobile/ios/Storage/SQL/SQLiteBookmarksHelpers.swift b/mobile/ios/Storage/SQL/SQLiteBookmarksHelpers.swift new file mode 100644 index 0000000000..c64c9ada9c --- /dev/null +++ b/mobile/ios/Storage/SQL/SQLiteBookmarksHelpers.swift @@ -0,0 +1,75 @@ +/* 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 + +public func titleForSpecialGUID(_ guid: GUID) -> String? { + switch guid { + case BookmarkRoots.RootGUID: + return "" + case BookmarkRoots.MobileFolderGUID: + return BookmarksFolderTitleMobile + case BookmarkRoots.ToolbarFolderGUID: + return BookmarksFolderTitleToolbar + case BookmarkRoots.MenuFolderGUID: + return BookmarksFolderTitleMenu + case BookmarkRoots.UnfiledFolderGUID: + return BookmarksFolderTitleUnsorted + default: + return nil + } +} + +class BookmarkURLTooLargeError: MaybeErrorType { + init() { + } + var description: String { + return "URL too long to bookmark." + } +} + +extension String { + public func truncateToUTF8ByteCount(_ keep: Int) -> String { + let byteCount = self.lengthOfBytes(using: String.Encoding.utf8) + if byteCount <= keep { + return self + } + let toDrop = keep - byteCount + + // If we drop this many characters from the string, we will drop at least this many bytes. + // That's aggressive, but that's OK for our purposes. + guard let endpoint = self.index(self.endIndex, offsetBy: toDrop, limitedBy: self.startIndex) else { + return "" + } + return self.substring(to: endpoint) + } +} + +extension SQLiteBookmarks: ShareToDestination { + public func addToMobileBookmarks(_ url: URL, title: String, favicon: Favicon?) -> Success { + if url.absoluteString.lengthOfBytes(using: String.Encoding.utf8) > AppConstants.DB_URL_LENGTH_MAX { + return deferMaybe(BookmarkURLTooLargeError()) + } + + let title = title.truncateToUTF8ByteCount(AppConstants.DB_TITLE_LENGTH_MAX) + + return isBookmarked(String(describing: url), direction: Direction.local) + >>== { yes in + guard !yes else { return succeed() } + return self.insertBookmark(url, title: title, favicon: favicon, + intoFolder: BookmarkRoots.MobileFolderGUID, + withTitle: BookmarksFolderTitleMobile) + } + } + + public func shareItem(_ item: ShareItem) -> Success { + // We parse here in anticipation of getting real URLs at some point. + if let url = item.url.asURL { + let title = item.title ?? url.absoluteString + return self.addToMobileBookmarks(url, title: title, favicon: item.favicon) + } + return succeed() + } +} diff --git a/mobile/ios/Storage/SQL/SQLiteBookmarksModel.swift b/mobile/ios/Storage/SQL/SQLiteBookmarksModel.swift new file mode 100644 index 0000000000..56576fe1a8 --- /dev/null +++ b/mobile/ios/Storage/SQL/SQLiteBookmarksModel.swift @@ -0,0 +1,948 @@ +/* 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 Deferred +import Foundation +import Shared + +private let log = Logger.syncLogger +private let desktopBookmarksLabel: String = NSLocalizedString("Desktop Bookmarks", tableName: "BookmarkPanel", comment: "The folder name for the virtual folder that contains all desktop bookmarks.") + +public enum Direction { + case buffer + case local + + var structureTable: String { + switch self { + case .local: + return TableBookmarksLocalStructure + case .buffer: + return TableBookmarksBufferStructure + } + } + + var valueTable: String { + switch self { + case .local: + return TableBookmarksLocal + case .buffer: + return TableBookmarksBuffer + } + } + + var valueView: String { + switch self { + case .local: + return ViewBookmarksLocalOnMirror + case .buffer: + return ViewBookmarksBufferWithDeletionsOnMirror + } + } + + var structureView: String { + switch self { + case .local: + return ViewBookmarksLocalStructureOnMirror + case .buffer: + return ViewBookmarksBufferStructureOnMirror + } + } +} + +public protocol KeywordSearchSource { + func getURLForKeywordSearch(_ keyword: String) -> Deferred> +} + +open class SQLiteBookmarksModelFactory: BookmarksModelFactory { + fileprivate let bookmarks: SQLiteBookmarks + fileprivate let direction: Direction + + public init(bookmarks: SQLiteBookmarks, direction: Direction) { + self.bookmarks = bookmarks + self.direction = direction + } + + public func factoryForIndex(_ index: Int, inFolder folder: BookmarkFolder) -> BookmarksModelFactory { + return self + } + + fileprivate func withDifferentDirection(_ direction: Direction) -> SQLiteBookmarksModelFactory { + if self.direction == direction { + return self + } + return SQLiteBookmarksModelFactory(bookmarks: self.bookmarks, direction: direction) + } + + fileprivate func getChildrenWithParent(_ parentGUID: GUID, excludingGUIDs: [GUID]?=nil, includeIcon: Bool) -> Deferred>> { + return self.bookmarks.getChildrenWithParent(parentGUID, direction: self.direction, excludingGUIDs: excludingGUIDs, includeIcon: includeIcon) + } + + fileprivate func getRootChildren() -> Deferred>> { + return self.getChildrenWithParent(BookmarkRoots.RootGUID, excludingGUIDs: [BookmarkRoots.RootGUID], includeIcon: true) + } + + fileprivate func getChildren(_ guid: String) -> Deferred>> { + return self.getChildrenWithParent(guid, includeIcon: true) + } + + func folderForGUID(_ guid: GUID, title: String) -> Deferred> { + return self.getChildren(guid) + >>== { cursor in + + if cursor.status == .failure { + return deferMaybe(DatabaseError(description: "Couldn't get children: \(cursor.statusMessage).")) + } + + return deferMaybe(SQLiteBookmarkFolder(guid: guid, title: title, children: cursor)) + } + } + + fileprivate func modelWithRoot(_ root: BookmarkFolder) -> Deferred> { + return deferMaybe(BookmarksModel(modelFactory: self, root: root)) + } + + open func modelForFolder(_ guid: String, title: String) -> Deferred> { + if guid == BookmarkRoots.MobileFolderGUID { + return self.modelForRoot() + } + + if guid == BookmarkRoots.FakeDesktopFolderGUID { + return self.modelForDesktopBookmarks() + } + + let outputTitle = titleForSpecialGUID(guid) ?? title + return self.folderForGUID(guid, title: outputTitle) + >>== self.modelWithRoot + } + + open func modelForFolder(_ folder: BookmarkFolder) -> Deferred> { + return self.modelForFolder(folder.guid, title: folder.title) + } + + open func modelForFolder(_ guid: String) -> Deferred> { + return self.modelForFolder(guid, title: "") + } + + open func modelForRoot() -> Deferred> { + log.debug("Getting model for root.") + let getFolder = self.folderForGUID(BookmarkRoots.MobileFolderGUID, title: BookmarksFolderTitleMobile) + if self.direction == .buffer { + return getFolder >>== self.modelWithRoot + } + + // Return a virtual model containing "Desktop bookmarks" prepended to the local mobile bookmarks. + return getFolder >>== { folder in + self.extendWithDesktopBookmarksFolder(folder, factory: self) + } + } + + open var nullModel: BookmarksModel { + let children = Cursor(status: .failure, msg: "Null model") + let folder = SQLiteBookmarkFolder(guid: "Null", title: "Null", children: children) + return BookmarksModel(modelFactory: self, root: folder) + } + + open func isBookmarked(_ url: String) -> Deferred> { + return self.bookmarks.isBookmarked(url, direction: self.direction) + } + + open func removeByURL(_ url: String) -> Success { + if self.direction == Direction.buffer { + return deferMaybe(DatabaseError(description: "Refusing to remove URL from buffer in model.")) + } + + // Find all of the records for the provided URL. Don't bother with + // any that are already deleted! + return self.bookmarks.nonDeletedGUIDsForURL(url) + >>== self.bookmarks.removeGUIDs + } + + open func removeByGUID(_ guid: GUID) -> Success { + if self.direction == Direction.buffer { + return deferMaybe(DatabaseError(description: "Refusing to remove GUID from buffer in model.")) + } + + log.debug("removeByGUID: \(guid)") + return self.bookmarks.removeGUIDs([guid]) + } + + func hasDesktopBookmarks() -> Deferred> { + // This is very lazy, but it has the nice property of keeping Desktop Bookmarks visible + // for a while after you mark the last desktop child as deleted. + let parents: Args = [ + // Local. + BookmarkRoots.MenuFolderGUID, + BookmarkRoots.ToolbarFolderGUID, + BookmarkRoots.UnfiledFolderGUID, + + // Mirror. + BookmarkRoots.MenuFolderGUID, + BookmarkRoots.ToolbarFolderGUID, + BookmarkRoots.UnfiledFolderGUID, + ] + + let sql = + "SELECT 1 FROM \(self.direction.structureTable) WHERE parent IN (?, ?, ?)" + + " UNION ALL " + + "SELECT 1 FROM \(TableBookmarksMirrorStructure) WHERE parent IN (?, ?, ?)" + + " LIMIT 1" + + return self.bookmarks.db.queryReturnsResults(sql, args: parents) + } + + func getDesktopRoots() -> Deferred>> { + if self.direction == .buffer { + // DesktopRoots excludes the Mobile folder, local and non-local mobile are aggregated + return self.bookmarks.getRecordsWithGUIDs(BookmarkRoots.DesktopRoots, direction: self.direction, includeIcon: false) + } + + // We deliberately exclude the mobile folder, because we're inverting the containment + // relationship here. + let exclude = [BookmarkRoots.MobileFolderGUID, BookmarkRoots.RootGUID] + return self.getChildrenWithParent(BookmarkRoots.RootGUID, excludingGUIDs: exclude, includeIcon: false) + } + + /** + * Prepend the provided mobile bookmarks folder with a single folder. + * The prepended folder is "Desktop Bookmarks". It contains mirrored folders. + */ + open func extendWithDesktopBookmarksFolder(_ mobile: BookmarkFolder, factory: BookmarksModelFactory) -> Deferred> { + + func onlyMobile() -> Deferred> { + // No desktop bookmarks. + log.debug("No desktop bookmarks. Only showing mobile.") + return deferMaybe(BookmarksModel(modelFactory: factory, root: mobile)) + } + + return self.hasDesktopBookmarks() >>== { yes in + if !yes { + return onlyMobile() + } + + return self.getDesktopRoots() >>== { cursor in + if cursor.count == 0 { + // This shouldn't occur. + return onlyMobile() + } + + let desktop = self.folderForDesktopBookmarksCursor(cursor) + let prepended = PrependedBookmarkFolder(main: mobile, prepend: desktop) + return deferMaybe(BookmarksModel(modelFactory: factory, root: prepended)) + } + } + } + + fileprivate func modelForDesktopBookmarks() -> Deferred> { + return self.getDesktopRoots() >>== { cursor in + let desktop = self.folderForDesktopBookmarksCursor(cursor) + return deferMaybe(BookmarksModel(modelFactory: self, root: desktop)) + } + } + + fileprivate func folderForDesktopBookmarksCursor(_ cursor: Cursor) -> SQLiteBookmarkFolder { + return SQLiteBookmarkFolder(guid: BookmarkRoots.FakeDesktopFolderGUID, title: desktopBookmarksLabel, children: cursor) + } +} + +class EditableBufferBookmarksSQLiteBookmarksModelFactory: SQLiteBookmarksModelFactory { + override func getChildrenWithParent(_ parentGUID: GUID, excludingGUIDs: [GUID]?, includeIcon: Bool) -> Deferred>> { + if parentGUID == BookmarkRoots.MobileFolderGUID { + return self.bookmarks.getChildrenWithParent(parentGUID, direction: self.direction, excludingGUIDs: excludingGUIDs, includeIcon: includeIcon, factory: BookmarkFactory.editableItemsFactory) + } + return super.getChildrenWithParent(parentGUID, excludingGUIDs: excludingGUIDs, includeIcon: includeIcon) + } + + override func removeByGUID(_ guid: GUID) -> Success { + log.debug("Removing \(guid) from buffer.") + return self.bookmarks.markBufferBookmarkAsDeleted(guid) + } +} + +private func isEditableExpression(_ direction: Direction) -> String { + if direction == .buffer { + return "0" + } + + return "SELECT exists( " + + " SELECT exists(SELECT 1 FROM \(TableBookmarksBuffer)) AS hasBuffer, exists(SELECT 1 FROM \(TableBookmarksMirror)) AS hasMirror " + + " WHERE hasBuffer IS 0 OR hasMirror IS 0" + + ")" +} + +extension SQLiteBookmarks { + + fileprivate func getRecordsWithGUIDs(_ guids: [GUID], direction: Direction, includeIcon: Bool) -> Deferred>> { + + let args: Args = guids + let varlist = BrowserDB.varlist(args.count) + let values = + "SELECT -1 AS id, guid, type, date_added, is_deleted, parentid, parentName, feedUri, pos, title, bmkUri, siteUri, folderName, faviconID, (\(isEditableExpression(direction))) AS isEditable " + + "FROM \(direction.valueView) WHERE guid IN \(varlist) AND NOT is_deleted" + + let withIcon = [ + "SELECT bookmarks.id AS id, bookmarks.guid AS guid, bookmarks.type AS type,", + " bookmarks.date_added AS date_added,", + " bookmarks.is_deleted AS is_deleted,", + " bookmarks.parentid AS parentid, bookmarks.parentName AS parentName,", + " bookmarks.feedUri AS feedUri, bookmarks.pos AS pos, title AS title,", + " bookmarks.bmkUri AS bmkUri, bookmarks.siteUri AS siteUri,", + " bookmarks.folderName AS folderName,", + " bookmarks.isEditable AS isEditable,", + " favicons.url AS iconURL, favicons.date AS iconDate, favicons.type AS iconType", + "FROM (", values, ") AS bookmarks", + "LEFT OUTER JOIN favicons ON bookmarks.faviconID = favicons.id", + "ORDER BY title ASC", + ].joined(separator: " ") + + let sql = (includeIcon ? withIcon : values) + " ORDER BY title ASC" + return self.db.runQuery(sql, args: args, factory: BookmarkFactory.factory) + } + + /** + * Return the children of the provided parent. + * Rows are ordered by positional index. + * This method is aware of is_overridden and deletion, using local override structure by preference. + * Note that a folder can be empty locally; we thus use the flag rather than looking at the structure itself. + */ + func getChildrenWithParent(_ parentGUID: GUID, direction: Direction, excludingGUIDs: [GUID]?=nil, includeIcon: Bool, factory: @escaping (SDRow) -> BookmarkNode = BookmarkFactory.factory) -> Deferred>> { + + precondition((excludingGUIDs ?? []).count < 100, "Sanity bound for the number of GUIDs we can exclude.") + + let valueView = direction.valueView + let structureView = direction.structureView + + let structure = + "SELECT parent, child AS guid, idx FROM \(structureView) " + + "WHERE parent = ?" + + let values = + "SELECT -1 AS id, guid, type, date_added, is_deleted, parentid, parentName, feedUri, pos, title, bmkUri, siteUri, folderName, faviconID, (\(isEditableExpression(direction))) AS isEditable " + + "FROM \(valueView)" + + // We exclude queries and dynamic containers, because we can't + // usefully display them. + let typeQuery = BookmarkNodeType.query.rawValue + let typeDynamic = BookmarkNodeType.dynamicContainer.rawValue + let typeFilter = " vals.type NOT IN (\(typeQuery), \(typeDynamic))" + + let args: Args + let exclusion: String + if let excludingGUIDs = excludingGUIDs { + args = ([parentGUID] + excludingGUIDs).map { $0 } + exclusion = "\(typeFilter) AND vals.guid NOT IN " + BrowserDB.varlist(excludingGUIDs.count) + } else { + args = [parentGUID] + exclusion = typeFilter + } + + let fleshed = + "SELECT vals.id AS id, vals.guid AS guid, vals.type AS type, vals.date_added AS date_added, vals.is_deleted AS is_deleted, " + + " vals.parentid AS parentid, vals.parentName AS parentName, vals.feedUri AS feedUri, " + + " vals.siteUri AS siteUri," + + " vals.pos AS pos, vals.title AS title, vals.bmkUri AS bmkUri, vals.folderName AS folderName, " + + " vals.faviconID AS faviconID, " + + " vals.isEditable AS isEditable, " + + " structure.idx AS idx, " + + " structure.parent AS _parent " + + "FROM (\(structure)) AS structure JOIN (\(values)) AS vals " + + "ON vals.guid = structure.guid " + + "WHERE " + exclusion + + let withIcon = + "SELECT bookmarks.id AS id, bookmarks.guid AS guid, bookmarks.type AS type, " + + " bookmarks.date_added AS date_added, " + + " bookmarks.is_deleted AS is_deleted, " + + " bookmarks.parentid AS parentid, bookmarks.parentName AS parentName, " + + " bookmarks.feedUri AS feedUri, bookmarks.siteUri AS siteUri, " + + " bookmarks.pos AS pos, title AS title, " + + " bookmarks.bmkUri AS bmkUri, bookmarks.folderName AS folderName, " + + " bookmarks.idx AS idx, bookmarks._parent AS _parent, " + + " bookmarks.isEditable AS isEditable, " + + " favicons.url AS iconURL, favicons.date AS iconDate, favicons.type AS iconType " + + "FROM (\(fleshed)) AS bookmarks " + + "LEFT OUTER JOIN favicons ON bookmarks.faviconID = favicons.id" + + let sql = (includeIcon ? withIcon : fleshed) + " ORDER BY idx ASC" + return self.db.runQuery(sql, args: args, factory: factory) + } + + // This is only used from tests. + func clearBookmarks() -> Success { + log.warning("CALLING clearBookmarks -- this should only be used from tests.") + return self.db.run([ + ("DELETE FROM \(TableBookmarksLocal) WHERE parentid IS NOT ?", [BookmarkRoots.RootGUID]), + self.favicons.getCleanupFaviconsQuery() + ]) + } + + public func removeGUIDs(_ guids: [GUID]) -> Success { + log.debug("removeGUIDs: \(guids)") + + // Override any parents that aren't already overridden. We're about to remove some + // of their children. + return self.overrideParentsOfGUIDs(guids) + + // Find, recursively, any children of the provided GUIDs. This will only be the case + // if you specify folders. This is a special case because we're removing *all* + // children, so we don't need to do the reindexing dance. + >>> { self.deleteChildrenOfGUIDs(guids) } + + // Override any records that aren't already overridden. + >>> { self.overrideGUIDs(guids) } + + // Then delete the already-overridden records. We do this one at a time in order + // to get indices correct in edge cases. (We do bulk-delete their children + // one layer at a time, at least.) + >>> { walk(guids, f: self.removeLocalByGUID) } + } + + fileprivate func nonDeletedGUIDsForURL(_ url: String) -> Deferred> { + let sql = "SELECT DISTINCT guid FROM \(ViewBookmarksLocalOnMirror) WHERE bmkUri = ? AND is_deleted = 0" + let args: Args = [url] + + return self.db.runQuery(sql, args: args, factory: { $0[0] as! GUID }) >>== { guids in + return deferMaybe(guids.asArray()) + } + } + + fileprivate func overrideParentsOfGUIDs(_ guids: [GUID]) -> Success { + log.debug("Overriding parents of \(guids).") + + // TODO: Yes, this can be done in one go. + let getParentsSQL = + "SELECT DISTINCT parent FROM \(ViewBookmarksLocalStructureOnMirror) " + + "WHERE child IN \(BrowserDB.varlist(guids.count)) AND is_overridden = 0" + let getParentsArgs: Args = guids + + return self.db.runQuery(getParentsSQL, args: getParentsArgs, factory: { $0[0] as! GUID }) + >>== { parentsCursor in + let parents = parentsCursor.asArray() + log.debug("Overriding parents: \(parents).") + let (sql, args) = self.getSQLToOverrideFolders(parents, atModifiedTime: Date.now()) + return self.db.run(sql.map { ($0, args) }) + } + } + + fileprivate func overrideGUIDs(_ guids: [GUID]) -> Success { + log.debug("Overriding GUIDs: \(guids).") + let (sql, args) = self.getSQLToOverrideNonFolders(guids, atModifiedTime: Date.now()) + return self.db.run(sql.map { ($0, args) }) + } + + // Recursive. + fileprivate func deleteChildrenOfGUIDs(_ guids: [GUID]) -> Success { + if guids.isEmpty { + return succeed() + } + + precondition(BookmarkRoots.All.intersection(guids).isEmpty, "You can't even touch the roots for removal.") + + log.debug("Deleting children of \(guids).") + + let topArgs: Args = guids + let topVarlist = BrowserDB.varlist(topArgs.count) + let query = + "SELECT child FROM \(ViewBookmarksLocalStructureOnMirror) " + + "WHERE parent IN \(topVarlist)" + + // We're deleting whole folders, so we don't need to worry about indices. + return self.db.runQuery(query, args: topArgs, factory: { $0[0] as! GUID }) + >>== { children in + let childGUIDs = children.asArray() + log.debug("… children of \(guids) are \(childGUIDs).") + + if childGUIDs.isEmpty { + log.debug("No children; nothing more to do.") + return succeed() + } + + let childArgs: Args = childGUIDs + let childVarlist = BrowserDB.varlist(childArgs.count) + + // Mirror the children if they're not already. + // We use the non-folder version of this query because we're recursively + // destroying structure right after this, so there's no point cloning the + // mirror structure. + // Then delete the children's children, so we don't leave orphans. This is + // recursive, so by the time this succeeds we know that all of these records + // have no remaining children. + let (overrideSQL, overrideArgs) = self.getSQLToOverrideNonFolders(childGUIDs, atModifiedTime: Date.now()) + + return self.deleteChildrenOfGUIDs(childGUIDs) + >>> { self.db.run(overrideSQL.map { ($0, overrideArgs) }) } + >>> { + // Delete the children themselves. + + // Remove each child from structure. We use the top list to save effort. + let deleteStructure = + "DELETE FROM \(TableBookmarksLocalStructure) WHERE parent IN \(topVarlist)" + + // If a bookmark is New, delete it outright. + let deleteNew = + "DELETE FROM \(TableBookmarksLocal) WHERE guid IN \(childVarlist) AND sync_status = \(SyncStatus.new.rawValue)" + + // If a bookmark is Changed, mark it as deleted and bump its modified time. + let markChanged = self.getMarkDeletedSQLWithWhereFragment("guid IN \(childVarlist)") + + return self.db.run([ + (deleteStructure, topArgs), + (deleteNew, childArgs), + (markChanged, childArgs), + ]) + } + } + } + + fileprivate func getMarkDeletedSQLWithWhereFragment(_ whereFragment: String) -> String { + let sql = + "UPDATE \(TableBookmarksLocal) SET" + + " date_added = NULL" + + ", is_deleted = 1" + + ", local_modified = \(Date.now())" + + ", bmkUri = NULL" + + ", feedUri = NULL" + + ", siteUri = NULL" + + ", pos = NULL" + + ", title = NULL" + + ", tags = NULL" + + ", keyword = NULL" + + ", description = NULL" + + ", parentid = NULL" + + ", parentName = NULL" + + ", folderName = NULL" + + ", queryId = NULL" + + " WHERE \(whereFragment) AND sync_status = \(SyncStatus.changed.rawValue)" + + return sql + } + /** + * This depends on the record's parent already being overridden if necessary. + */ + fileprivate func removeLocalByGUID(_ guid: GUID) -> Success { + let args: Args = [guid] + + // Find the index we're currently occupying. + let previousIndexSubquery = "SELECT idx FROM \(TableBookmarksLocalStructure) WHERE child = ?" + + // Fix up the indices of subsequent siblings. + let updateIndices = + "UPDATE \(TableBookmarksLocalStructure) SET idx = (idx - 1) WHERE idx > (\(previousIndexSubquery))" + + // If the bookmark is New, delete it outright. + let deleteNew = + "DELETE FROM \(TableBookmarksLocal) WHERE guid = ? AND sync_status = \(SyncStatus.new.rawValue)" + + // If the bookmark is Changed, mark it as deleted and bump its modified time. + let markChanged = self.getMarkDeletedSQLWithWhereFragment("guid = ?") + + // Its parent must be either New or Changed, so we don't need to re-mirror it. + // TODO: bump the parent's modified time, because the child list changed? + + // Now delete from structure. + let deleteStructure = + "DELETE FROM \(TableBookmarksLocalStructure) WHERE child = ?" + + return self.db.run([ + (updateIndices, args), + (deleteNew, args), + (markChanged, args), + (deleteStructure, args), + ]) + } + + fileprivate func markBufferBookmarkAsDeleted(_ guid: GUID) -> Success { + let insertInPendingDeletions = + "INSERT OR IGNORE INTO \(TablePendingBookmarksDeletions) " + + "(id) " + + "VALUES (?)" + let args: Args = [guid] + return self.db.run(insertInPendingDeletions, withArgs: args) + } +} + +class SQLiteBookmarkFolder: BookmarkFolder { + fileprivate let cursor: Cursor + override var count: Int { + return cursor.count + } + + override subscript(index: Int) -> BookmarkNode { + let bookmark = cursor[index] + return bookmark! as BookmarkNode + } + + init(guid: String, title: String, children: Cursor) { + self.cursor = children + super.init(guid: guid, title: title) + } + + override func removeItemWithGUID(_ guid: GUID) -> BookmarkFolder? { + let without = cursor.asArray().filter { $0.guid != guid } + return MemoryBookmarkFolder(guid: self.guid, title: self.title, children: without) + } +} + +class BookmarkFactory { + fileprivate class func addIcon(_ bookmark: BookmarkNode, row: SDRow) { + // TODO: share this logic with SQLiteHistory. + if let faviconURL = row["iconURL"] as? String, + let date = row["iconDate"] as? Double, + let faviconType = row["iconType"] as? Int, + let type = IconType(rawValue: faviconType) { + bookmark.favicon = Favicon(url: faviconURL, + date: Date(timeIntervalSince1970: date), + type: type) + } + } + + fileprivate class func livemarkFactory(_ row: SDRow) -> BookmarkItem { + let id = row["id"] as? Int + let guid = row["guid"] as! String + let url = row["siteUri"] as! String + let title = row["title"] as? String ?? "Livemark" // TODO + let isEditable = row.getBoolean("isEditable") // Defaults to false. + let bookmark = BookmarkItem(guid: guid, title: title, url: url, isEditable: isEditable) + bookmark.id = id + BookmarkFactory.addIcon(bookmark, row: row) + return bookmark + } + + // We ignore queries altogether inside the model factory. + fileprivate class func queryFactory(_ row: SDRow) -> BookmarkItem { + log.warning("Creating a BookmarkItem from a query. This is almost certainly unexpected.") + let id = row["id"] as? Int + let guid = row["guid"] as! String + let title = row["title"] as? String ?? SQLiteBookmarks.defaultItemTitle + let isEditable = row.getBoolean("isEditable") // Defaults to false. + let bookmark = BookmarkItem(guid: guid, title: title, url: "about:blank", isEditable: isEditable) + bookmark.id = id + BookmarkFactory.addIcon(bookmark, row: row) + return bookmark + } + + fileprivate class func separatorFactory(_ row: SDRow) -> BookmarkSeparator { + let id = row["id"] as? Int + let guid = row["guid"] as! String + let separator = BookmarkSeparator(guid: guid) + separator.id = id + return separator + } + + fileprivate class func itemRowFactory(_ row: SDRow, forceEditable: Bool = false) -> BookmarkItem { + let id = row["id"] as? Int + let guid = row["guid"] as! String + let url = row["bmkUri"] as! String + let title = row["title"] as? String ?? url + let isEditable = forceEditable || row.getBoolean("isEditable") // Defaults to false. + let bookmark = BookmarkItem(guid: guid, title: title, url: url, isEditable: isEditable) + bookmark.id = id + BookmarkFactory.addIcon(bookmark, row: row) + return bookmark + } + + fileprivate class func itemFactory(_ row: SDRow) -> BookmarkItem { + return BookmarkFactory.itemRowFactory(row, forceEditable: false) + } + + fileprivate class func folderFactory(_ row: SDRow) -> BookmarkFolder { + let id = row["id"] as? Int + let guid = row["guid"] as! String + let isEditable = row.getBoolean("isEditable") // Defaults to false. + let title = titleForSpecialGUID(guid) ?? + row["title"] as? String ?? + SQLiteBookmarks.defaultFolderTitle + + let folder = BookmarkFolder(guid: guid, title: title, isEditable: isEditable) + folder.id = id + BookmarkFactory.addIcon(folder, row: row) + return folder + } + + class func factory(_ row: SDRow) -> BookmarkNode { + return BookmarkFactory.rowFactory(row, forceEditable: false) + } + + class func editableItemsFactory(_ row: SDRow) -> BookmarkNode { + return BookmarkFactory.rowFactory(row, forceEditable: true) + } + + class func rowFactory(_ row: SDRow, forceEditable: Bool = false) -> BookmarkNode { + if let typeCode = row["type"] as? Int, let type = BookmarkNodeType(rawValue: typeCode) { + switch type { + case .bookmark: + return itemRowFactory(row, forceEditable: forceEditable) + case .dynamicContainer: + // This should never be hit: we exclude dynamic containers from our models. + fallthrough + case .folder: + return folderFactory(row) + case .separator: + return separatorFactory(row) + case .livemark: + return livemarkFactory(row) + case .query: + // This should never be hit: we exclude queries from our models. + return queryFactory(row) + } + } + assert(false, "Invalid bookmark data.") + return itemFactory(row) // This will fail, but it keeps the compiler happy. + } + + // N.B., doesn't include children! + class func mirrorItemFactory(_ row: SDRow) -> BookmarkMirrorItem { + // TODO + // let id = row["id"] as! Int + + let guid = row["guid"] as! GUID + let typeCode = row["type"] as! Int + let is_deleted = row.getBoolean("is_deleted") + let parentid = row["parentid"] as? GUID + let parentName = row["parentName"] as? String + let feedUri = row["feedUri"] as? String + let siteUri = row["siteUri"] as? String + let pos = row["pos"] as? Int + let title = row["title"] as? String + let description = row["description"] as? String + let bmkUri = row["bmkUri"] as? String + let tags = row["tags"] as? String + let keyword = row["keyword"] as? String + let folderName = row["folderName"] as? String + let queryId = row["queryId"] as? String + let date_added = row.getTimestamp("date_added") + + // Local and mirror only. + let faviconID = row["faviconID"] as? Int + + // Local only. + let local_modified = row.getTimestamp("local_modified") + + // Mirror and buffer. + let server_modified = row.getTimestamp("server_modified") + let hasDupe = row.getBoolean("hasDupe") + + // Mirror only. TODO + //let is_overridden = row.getBoolean("is_overridden") + + // Use the struct initializer directly. Yes, this doesn't validate as strongly as + // using the static constructors, but it'll be as valid as the contents of the DB. + let type = BookmarkNodeType(rawValue: typeCode)! + + // This one might really be missing (it's local-only), so do this the hard way. + let syncStatus: SyncStatus? + if let s = row["sync_status"] as? Int { + syncStatus = SyncStatus(rawValue: s) + } else { + syncStatus = nil + } + let item = BookmarkMirrorItem(guid: guid, type: type, dateAdded: date_added, serverModified: server_modified ?? 0, + isDeleted: is_deleted, hasDupe: hasDupe, parentID: parentid, parentName: parentName, + feedURI: feedUri, siteURI: siteUri, + pos: pos, + title: title, description: description, + bookmarkURI: bmkUri, tags: tags, keyword: keyword, + folderName: folderName, queryID: queryId, + children: nil, + faviconID: faviconID, localModified: local_modified, + syncStatus: syncStatus) + return item + } +} + +extension SQLiteBookmarks: SearchableBookmarks { + public func bookmarksByURL(_ url: URL) -> Deferred>> { + let inner = + "SELECT id, type, date_added, guid, bmkUri, title, faviconID FROM \(TableBookmarksLocal) " + + "WHERE " + + "type = \(BookmarkNodeType.bookmark.rawValue) AND is_deleted IS NOT 1 AND bmkUri = ? " + + "UNION ALL " + + "SELECT id, type, date_added, guid, bmkUri, title, faviconID FROM \(TableBookmarksMirror) " + + "WHERE " + + "type = \(BookmarkNodeType.bookmark.rawValue) AND is_overridden IS NOT 1 AND is_deleted IS NOT 1 AND bmkUri = ? " + + let sql = + "SELECT bookmarks.id AS id, bookmarks.type AS type, bookmarks.date_added AS date_added, guid, bookmarks.bmkUri AS bmkUri, title, " + + "favicons.url AS iconURL, favicons.date AS iconDate, favicons.type AS iconType " + + "FROM (\(inner)) AS bookmarks " + + "LEFT OUTER JOIN favicons ON bookmarks.faviconID = favicons.id" + + let u = url.absoluteString + let args: Args = [u, u] + return db.runQuery(sql, args: args, factory: BookmarkFactory.itemFactory) + } +} + +extension SQLiteBookmarks { + // We're in sync (even partially) if the mirror is non-empty. + // We can show fallback desktop bookmarks if the mirror is empty and the buffer contains + // children of the roots. + func hasOnlyUnmergedRemoteBookmarks() -> Deferred> { + let parents: Args = [ + BookmarkRoots.MenuFolderGUID, + BookmarkRoots.ToolbarFolderGUID, + BookmarkRoots.UnfiledFolderGUID, + BookmarkRoots.MobileFolderGUID, + ] + let sql = [ + "SELECT", + "not exists(SELECT 1 FROM \(TableBookmarksMirror))", + "AND", + "exists(SELECT 1 FROM \(TableBookmarksBufferStructure) WHERE parent IN (?, ?, ?, ?))", + ].joined(separator: " ") + return self.db.runQuery(sql, args: parents, factory: { $0[0] as! Int == 1 }) + >>== { row in + guard row.status == .success, + let result = row[0] else { + // if the query did not succeed, we should return false so that we can use local bookmarks + return deferMaybe(false) + } + return deferMaybe(result) + } + } +} + +// It's a factory where the root contains Desktop Bookmarks from the buffer, and +// mobile bookmarks from local. +open class UnsyncedBookmarksFallbackModelFactory: BookmarksModelFactory { + let localFactory: SQLiteBookmarksModelFactory + let bufferFactory: SQLiteBookmarksModelFactory + + init(bookmarks: SQLiteBookmarks) { + // This relies on SQLiteBookmarks being the storage for both directions. + self.localFactory = SQLiteBookmarksModelFactory(bookmarks: bookmarks, direction: .local) + if AppConstants.MOZ_SIMPLE_BOOKMARKS_SYNCING { + self.bufferFactory = EditableBufferBookmarksSQLiteBookmarksModelFactory(bookmarks: bookmarks, direction: .buffer) + } else { + self.bufferFactory = SQLiteBookmarksModelFactory(bookmarks: bookmarks, direction: .buffer) + } + } + + // This is a special-case class, so here's the special-case behavior to + // know how to handle a folder that contains items drawn from different + // parts of the database. We look for the special kinds of folders we + // nest at the top level, and then we pick a folder to match. + public func factoryForIndex(_ index: Int, inFolder folder: BookmarkFolder) -> BookmarksModelFactory { + let concatenated: ConcatenatedBookmarkFolder + let i: Int + + // We have either just remote and local mobile bookmarks, or we have Desktop Bookmarks + // followed by remote and local mobile bookmarks. Handle either. + if let prepended = folder as? PrependedBookmarkFolder { + if index == 0 { + return self + } + + guard let c = prepended.main as? ConcatenatedBookmarkFolder else { + return self + } + i = index - 1 // Drop the prepend. + concatenated = c + } else { + guard let c = folder as? ConcatenatedBookmarkFolder else { + return self + } + i = index + concatenated = c + } + + if i < concatenated.pivot { + return self.bufferFactory // This comes first in our concatenation. + } + return self.localFactory + } + + open func modelForFolder(_ folder: BookmarkFolder) -> Deferred> { + return self.modelForFolder(folder.guid, title: folder.title) + } + + open func modelForFolder(_ guid: GUID) -> Deferred> { + return self.modelForFolder(guid, title: "") + } + + open func modelForFolder(_ guid: GUID, title: String) -> Deferred> { + if guid == BookmarkRoots.MobileFolderGUID { + return self.modelForRoot() + } + + if guid == BookmarkRoots.FakeDesktopFolderGUID { + return self.bufferFactory.modelForFolder(guid, title: title) + } + + return self.localFactory.modelForFolder(guid, title: title) + } + + open func modelForRoot() -> Deferred> { + log.debug("Getting model for fallback root.") + // Return a virtual model containing "Desktop bookmarks" prepended to the local mobile bookmarks. + return self.localFactory.folderForGUID(BookmarkRoots.MobileFolderGUID, title: BookmarksFolderTitleMobile) + >>== { + localMobileFolder in + + self.bufferFactory.folderForGUID(BookmarkRoots.MobileFolderGUID, title: BookmarksFolderTitleMobile) >>== { + bufferMobileFolder in + + let bufferAndLocalMobile = ConcatenatedBookmarkFolder(main: bufferMobileFolder, append: localMobileFolder) + return self.bufferFactory.hasDesktopBookmarks() >>== { yes in + guard yes else { + return deferMaybe(BookmarksModel(modelFactory: self, root: bufferAndLocalMobile)) + } + return self.bufferFactory.getDesktopRoots() >>== { cursor in + let desktop = self.bufferFactory.folderForDesktopBookmarksCursor(cursor) + let withDesktopPrepended = PrependedBookmarkFolder(main: bufferAndLocalMobile, prepend: desktop) + return deferMaybe(BookmarksModel(modelFactory: self, root: withDesktopPrepended)) + } + } + } + } + } + + // Whenever async construction is necessary, we fall into a pattern of needing + // a placeholder that behaves correctly for the period between kickoff and set. + open var nullModel: BookmarksModel { + let children = Cursor(status: .failure, msg: "Null model") + let folder = SQLiteBookmarkFolder(guid: "Null", title: "Null", children: children) + return BookmarksModel(modelFactory: self, root: folder) + } + + open func isBookmarked(_ url: String) -> Deferred> { + // We don't include buffer items in this check, because we can't un-star them! + return self.localFactory.isBookmarked(url) + } + + open func removeByGUID(_ guid: GUID) -> Success { + return self.localFactory.removeByGUID(guid) + } + + open func removeByURL(_ url: String) -> Success { + return self.localFactory.removeByURL(url) + } +} + +open class MergedSQLiteBookmarks: BookmarksModelFactorySource, KeywordSearchSource { + let local: SQLiteBookmarks + let buffer: SQLiteBookmarkBufferStorage + + // Figuring out our factory can require hitting the DB, so this is async. + // Note that we check *every time* -- we don't want to get stuck in a dead + // end when you might sync soon. + open var modelFactory: Deferred> { + return self.local.hasOnlyUnmergedRemoteBookmarks() >>== { yes in + if yes { + log.debug("Only unmerged remote bookmarks; using fallback factory.") + return deferMaybe(UnsyncedBookmarksFallbackModelFactory(bookmarks: self.local)) + } + log.debug("Using local+mirror bookmark factory.") + return self.local.modelFactory + } + } + + public init(db: BrowserDB) { + self.local = SQLiteBookmarks(db: db) + self.buffer = SQLiteBookmarkBufferStorage(db: db) + } + + open func getURLForKeywordSearch(_ keyword: String) -> Deferred> { + return self.local.getURLForKeywordSearch(keyword) + } +} diff --git a/mobile/ios/Storage/SQL/SQLiteBookmarksResetting.swift b/mobile/ios/Storage/SQL/SQLiteBookmarksResetting.swift new file mode 100644 index 0000000000..1347f6c81a --- /dev/null +++ b/mobile/ios/Storage/SQL/SQLiteBookmarksResetting.swift @@ -0,0 +1,146 @@ +/* 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 XCGLogger + +private let log = Logger.syncLogger + +extension MergedSQLiteBookmarks: AccountRemovalDelegate { + public func onRemovedAccount() -> Success { + return self.local.onRemovedAccount() >>> self.buffer.onRemovedAccount + } +} + +extension MergedSQLiteBookmarks: ResettableSyncStorage { + public func resetClient() -> Success { + return self.local.resetClient() >>> self.buffer.resetClient + } +} + +extension SQLiteBookmarkBufferStorage: AccountRemovalDelegate { + public func onRemovedAccount() -> Success { + return self.resetClient() + } +} + +extension SQLiteBookmarkBufferStorage: ResettableSyncStorage { + /** + * Our buffer is simply a copy of server contents. That means we should + * be very willing to drop it and re-populate it from the server whenever we might + * be out of sync. See Bug 1212431 Comment 2. + */ + public func resetClient() -> Success { + return self.wipeBookmarks() + } + + public func wipeBookmarks() -> Success { + return self.db.run([ + "DELETE FROM \(TableBookmarksBufferStructure)", + "DELETE FROM \(TableBookmarksBuffer)", + ]) + } +} + +extension SQLiteBookmarks { + /** + * If a synced record is deleted locally, but hasn't been synced to the server, + * then `preserveDeletions=true` will result in that deletion being kept. + * + * During a reset, we'll redownload all server records. If we don't keep the + * local deletion, then when we re-process the (non-deleted) server counterpart + * to the now-missing local record, it'll be reinserted: the user's deletion will + * be undone. + * + * Right now we don't preserve deletions when removing the Firefox Account, but + * we could do so if we were willing to trade local database space to handle this + * possible situation. + */ + fileprivate func collapseMirrorIntoLocalPreservingDeletions(_ preserveDeletions: Bool) -> Success { + // As implemented, this won't work correctly without ON DELETE CASCADE. + assert(SwiftData.EnableForeignKeys) + + // 1. Wait until we commit to complain about constraint violations. + let deferForeignKeys = + "PRAGMA defer_foreign_keys = ON" + + // 2. Drop anything from local that's deleted. We don't need to track the + // deletion now. Optional: keep them around if they're non-uploaded changes. + let removeLocalDeletions = + "DELETE FROM \(TableBookmarksLocal) WHERE is_deleted IS 1 " + + (preserveDeletions ? "AND sync_status IS NOT \(SyncStatus.changed.rawValue)" : "") + + // 3. Mark everything in local as New. + let markLocalAsNew = + "UPDATE \(TableBookmarksLocal) SET sync_status = \(SyncStatus.new.rawValue)" + + // 4. Insert into local anything not overridden left in mirror. + // Note that we use the server modified time as our substitute local modified time. + // This will provide an ounce of conflict avoidance if the user re-links the same + // account at a later date. + let copyMirrorContents = + "INSERT OR IGNORE INTO \(TableBookmarksLocal) " + + "(sync_status, local_modified, " + + " guid, date_added, type, bmkUri, title, parentid, parentName, feedUri, siteUri, pos," + + " description, tags, keyword, folderName, queryId, faviconID) " + + "SELECT " + + "\(SyncStatus.new.rawValue) AS sync_status, " + + "server_modified AS local_modified, " + + "guid, date_added, type, bmkUri, title, parentid, parentName, " + + "feedUri, siteUri, pos, description, tags, keyword, folderName, queryId, faviconID " + + "FROM \(TableBookmarksMirror) WHERE is_overridden IS 0" + + // 5.(pre) I have a database right in front of me that violates an assumption: a full + // bookmarksMirrorStructure and an empty bookmarksMirror. Clean up, just in case. + let removeOverriddenStructure = + "DELETE FROM \(TableBookmarksMirrorStructure) WHERE parent IN (SELECT guid FROM \(TableBookmarksMirror) WHERE is_overridden IS 1)" + + // 5. Insert into localStructure anything left in mirrorStructure. + // This won't copy the structure of any folders that were already overridden -- + // we already deleted those, and the deletions cascaded. + let copyMirrorStructure = + "INSERT INTO \(TableBookmarksLocalStructure) SELECT * FROM \(TableBookmarksMirrorStructure)" + + // 6. Blank the mirror. + let removeMirrorStructure = + "DELETE FROM \(TableBookmarksMirrorStructure)" + + let removeMirrorContents = + "DELETE FROM \(TableBookmarksMirror)" + + return db.run([ + deferForeignKeys, + removeLocalDeletions, + markLocalAsNew, + copyMirrorContents, + removeOverriddenStructure, + copyMirrorStructure, + removeMirrorStructure, + removeMirrorContents, + ]) + } +} +extension SQLiteBookmarks: AccountRemovalDelegate { + public func onRemovedAccount() -> Success { + return self.collapseMirrorIntoLocalPreservingDeletions(false) + } +} + +extension SQLiteBookmarks: ResettableSyncStorage { + public func resetClient() -> Success { + // Flip flags to prompt a re-sync. + // + // We copy the mirror to local, preserving local changes, apart from + // deletions of records that were never synced. + // + // Records that match the server record that we'll redownload will be + // marked as Synced and won't be reuploaded. + // + // Records that are present locally but aren't on the server will be + // uploaded. + // + return self.collapseMirrorIntoLocalPreservingDeletions(true) + } +} diff --git a/mobile/ios/Storage/SQL/SQLiteBookmarksSyncing.swift b/mobile/ios/Storage/SQL/SQLiteBookmarksSyncing.swift new file mode 100644 index 0000000000..f7ecee0605 --- /dev/null +++ b/mobile/ios/Storage/SQL/SQLiteBookmarksSyncing.swift @@ -0,0 +1,1216 @@ +/* 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 Deferred +import Foundation +import Shared +import XCGLogger + +private let log = Logger.syncLogger + +extension SQLiteBookmarks: LocalItemSource { + public func getLocalItemWithGUID(_ guid: GUID) -> Deferred> { + return self.db.getMirrorItemFromTable(TableBookmarksLocal, guid: guid) + } + + public func getLocalItemsWithGUIDs(_ guids: T) -> Deferred> where T.Iterator.Element == GUID { + return self.db.getMirrorItemsFromTable(TableBookmarksLocal, guids: guids) + } + + public func prefetchLocalItemsWithGUIDs(_ guids: T) -> Success where T.Iterator.Element == GUID { + log.debug("Not implemented for SQLiteBookmarks.") + return succeed() + } +} + +extension SQLiteBookmarks: MirrorItemSource { + public func getMirrorItemWithGUID(_ guid: GUID) -> Deferred> { + return self.db.getMirrorItemFromTable(TableBookmarksMirror, guid: guid) + } + + public func getMirrorItemsWithGUIDs(_ guids: T) -> Deferred> where T.Iterator.Element == GUID { + return self.db.getMirrorItemsFromTable(TableBookmarksMirror, guids: guids) + } + + public func prefetchMirrorItemsWithGUIDs(_ guids: T) -> Success where T.Iterator.Element == GUID { + log.debug("Not implemented for SQLiteBookmarks.") + return succeed() + } +} + +extension SQLiteBookmarks { + func getSQLToOverrideFolder(_ folder: GUID, atModifiedTime modified: Timestamp) -> (sql: [String], args: Args) { + return self.getSQLToOverrideFolders([folder], atModifiedTime: modified) + } + + func getSQLToOverrideFolders(_ folders: [GUID], atModifiedTime modified: Timestamp) -> (sql: [String], args: Args) { + if folders.isEmpty { + return (sql: [], args: []) + } + + let vars = BrowserDB.varlist(folders.count) + let args: Args = folders + + // Copy it to the local table. + // Most of these will be NULL, because we're only dealing with folders, + // and typically only the Mobile Bookmarks root. + let overrideSQL = "INSERT OR IGNORE INTO \(TableBookmarksLocal) " + + "(guid, type, date_added, bmkUri, title, parentid, parentName, feedUri, siteUri, pos," + + " description, tags, keyword, folderName, queryId, is_deleted, " + + " local_modified, sync_status, faviconID) " + + "SELECT guid, type, date_added, bmkUri, title, parentid, parentName, " + + "feedUri, siteUri, pos, description, tags, keyword, folderName, queryId, " + + "is_deleted, " + + "\(modified) AS local_modified, \(SyncStatus.changed.rawValue) AS sync_status, faviconID " + + "FROM \(TableBookmarksMirror) WHERE guid IN \(vars)" + + // Copy its mirror structure. + let dropSQL = "DELETE FROM \(TableBookmarksLocalStructure) WHERE parent IN \(vars)" + let copySQL = "INSERT INTO \(TableBookmarksLocalStructure) " + + "SELECT * FROM \(TableBookmarksMirrorStructure) WHERE parent IN \(vars)" + + // Mark as overridden. + let markSQL = "UPDATE \(TableBookmarksMirror) SET is_overridden = 1 WHERE guid IN \(vars)" + return (sql: [overrideSQL, dropSQL, copySQL, markSQL], args: args) + } + + func getSQLToOverrideNonFolders(_ records: [GUID], atModifiedTime modified: Timestamp) -> (sql: [String], args: Args) { + log.info("Getting SQL to override \(records).") + if records.isEmpty { + return (sql: [], args: []) + } + + let vars = BrowserDB.varlist(records.count) + let args: Args = records.map { $0 } + + // Copy any that aren't overridden to the local table. + let overrideSQL = + "INSERT OR IGNORE INTO \(TableBookmarksLocal) " + + "(guid, type, date_added, bmkUri, title, parentid, parentName, feedUri, siteUri, pos," + + " description, tags, keyword, folderName, queryId, is_deleted, " + + " local_modified, sync_status, faviconID) " + + "SELECT guid, type, date_added, bmkUri, title, parentid, parentName, " + + "feedUri, siteUri, pos, description, tags, keyword, folderName, queryId, " + + "is_deleted, " + + "\(modified) AS local_modified, \(SyncStatus.changed.rawValue) AS sync_status, faviconID " + + "FROM \(TableBookmarksMirror) WHERE guid IN \(vars) AND is_overridden = 0" + + // Mark as overridden. + let markSQL = "UPDATE \(TableBookmarksMirror) SET is_overridden = 1 WHERE guid IN \(vars)" + return (sql: [overrideSQL, markSQL], args: args) + } + + /** + * Insert a bookmark into the specified folder. + * If the folder doesn't exist, or is deleted, insertion will fail. + * + * Preconditions: + * * `deferred` has not been filled. + * * this function is called inside a transaction that hasn't been finished. + * + * Postconditions: + * * `deferred` has been filled with success or failure. + * * the transaction will include any status/overlay changes necessary to save the bookmark. + * * the return value determines whether the transaction should be committed, and + * matches the success-ness of the Deferred. + * + * Sorry about the long line. If we break it, the indenting below gets crazy. + */ + fileprivate func insertBookmarkInTransaction(url: URL, title: String, favicon: Favicon?, intoFolder parent: GUID, withTitle parentTitle: String, conn: SQLiteDBConnection) throws { + + log.debug("Inserting bookmark in transaction on thread \(Thread.current)") + + // Keep going if this does not throw. + func change(_ sql: String, args: Args?, desc: String) throws { + try conn.executeChange(sql, withArgs: args) + } + + let urlString = url.absoluteString + let newGUID = Bytes.generateGUID() + let now = Date.now() + let parentArgs: Args = [parent] + + //// Insert the new bookmark and icon without touching structure. + var args: Args = [ + newGUID, + BookmarkNodeType.bookmark.rawValue, + now, + urlString, + title, + parent, + parentTitle, + Date.nowNumber(), + SyncStatus.new.rawValue, + ] + + let faviconID: Int? + + // Insert the favicon. + if let icon = favicon { + faviconID = self.favicons.insertOrUpdateFaviconInTransaction(icon, conn: conn) + } else { + faviconID = nil + } + + log.debug("Inserting bookmark with GUID \(newGUID) and specified icon \(faviconID ??? "nil").") + + // If the caller didn't provide an icon (and they usually don't!), + // do a reverse lookup in history. We use a view to make this simple. + let iconValue: String + if let faviconID = faviconID { + iconValue = "?" + args.append(faviconID ) + } else { + iconValue = "(SELECT iconID FROM \(ViewIconForURL) WHERE url = ?)" + args.append(urlString ) + } + + let insertSQL = "INSERT INTO \(TableBookmarksLocal) " + + "(guid, type, date_added, bmkUri, title, parentid, parentName, local_modified, sync_status, faviconID) " + + "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, \(iconValue))" + try change(insertSQL, args: args, desc: "Error inserting \(newGUID).") + + func bumpParentStatus(_ status: Int) throws { + let bumpSQL = "UPDATE \(TableBookmarksLocal) SET sync_status = \(status), local_modified = \(now) WHERE guid = ?" + try change(bumpSQL, args: parentArgs, desc: "Error bumping \(parent)'s modified time.") + } + + func overrideParentMirror() throws { + // We do this slightly tortured work so that we can reuse these queries + // in a different context. + let (sql, args) = getSQLToOverrideFolder(parent, atModifiedTime: now) + var generator = sql.makeIterator() + while let query = generator.next() { + try change(query, args: args, desc: "Error running overriding query.") + } + } + + //// Make sure our parent is overridden and appropriately bumped. + // See discussion here: + // Note that this isn't as obvious as you might think. We must: + let localStatusFactory: (SDRow) -> (Int, Bool) = { row in + let status = row["sync_status"] as! Int + let deleted = (row["is_deleted"] as! Int) != 0 + return (status, deleted) + } + + let overriddenFactory: (SDRow) -> Bool = { row in + row.getBoolean("is_overridden") + } + + // TODO: these can be merged into a single query. + let mirrorStatusSQL = "SELECT is_overridden FROM \(TableBookmarksMirror) WHERE guid = ?" + let localStatusSQL = "SELECT sync_status, is_deleted FROM \(TableBookmarksLocal) WHERE guid = ?" + let mirrorStatus = conn.executeQuery(mirrorStatusSQL, factory: overriddenFactory, withArgs: parentArgs)[0] + let localStatus = conn.executeQuery(localStatusSQL, factory: localStatusFactory, withArgs: parentArgs)[0] + + let parentExistsInMirror = mirrorStatus != nil + let parentExistsLocally = localStatus != nil + + // * Figure out if we were already overridden. We only want to re-clone + // if we weren't. + if !parentExistsLocally { + if !parentExistsInMirror { + throw DatabaseError(description: "Folder \(parent) doesn't exist in either mirror or local.") + } + // * Mark the parent folder as overridden if necessary. + // Overriding the parent involves copying the parent's structure, so that + // we can amend it, but also the parent's row itself so that we know it's + // changed. + try overrideParentMirror() + } else { + let (status, deleted) = localStatus! + if deleted { + throw DatabaseError(description: "Local folder \(parent) is deleted.") + } + + // * Bump the overridden parent's modified time. We already copied its + // structure and values, and if it's in the local table it'll either + // already be New or Changed. + + if let syncStatus = SyncStatus(rawValue: status) { + switch syncStatus { + case .synced: + log.debug("We don't expect folders to ever be marked as Synced.") + try bumpParentStatus(SyncStatus.changed.rawValue) + case .new: + fallthrough + case .changed: + // Leave it marked as new or changed, but bump the timestamp. + try bumpParentStatus(syncStatus.rawValue) + } + } else { + log.warning("Local folder marked with unknown state \(status). This should never occur.") + try bumpParentStatus(SyncStatus.changed.rawValue) + } + } + + /// Add the new bookmark as a child in the modified local structure. + // We always append the new row: after insertion, the new item will have the largest index. + let newIndex = "(SELECT (COALESCE(MAX(idx), -1) + 1) AS newIndex FROM \(TableBookmarksLocalStructure) WHERE parent = ?)" + let structureSQL = "INSERT INTO \(TableBookmarksLocalStructure) (parent, child, idx) " + + "VALUES (?, ?, \(newIndex))" + let structureArgs: Args = [parent, newGUID, parent] + + try change(structureSQL, args: structureArgs, desc: "Error adding new item \(newGUID) to local structure.") + } + + /** + * Assumption: the provided folder GUID exists in either the local table or the mirror table. + */ + func insertBookmark(_ url: URL, title: String, favicon: Favicon?, intoFolder parent: GUID, withTitle parentTitle: String) -> Success { + log.debug("Inserting bookmark task on thread \(Thread.current)") + return db.transaction { conn -> Void in + try self.insertBookmarkInTransaction(url: url, title: title, favicon: favicon, intoFolder: parent, withTitle: parentTitle, conn: conn) + } + } +} + +private extension BookmarkMirrorItem { + // Let's say the buffer structure table looks like this: + // =============================== + // | parent | child | idx | + // ------------------------------- + // | aaaa | bbbb | 5 | + // | aaaa | cccc | 8 | + // | aaaa | dddd | 19 | + // =============================== + // And the self.children array has 5 children (2 new to insert). + // Then this function should be called with offset = 3 and nextIdx = 20 + func getChildrenArgs(offset: Int = 0, nextIdx: Int = 0) -> [Args] { + // Only folders have children, and we manage roots ourselves. + if self.type != .folder || + self.guid == BookmarkRoots.RootGUID { + return [] + } + let parent = self.guid + var idx = nextIdx + return self.children?.suffix(from: offset).map { child in + let ret: Args = [parent, child, idx] + idx += 1 + return ret + } ?? [] + } + + func getUpdateOrInsertArgs() -> Args { + let args: Args = [ + self.type.rawValue , + self.dateAdded, + self.serverModified, + self.isDeleted ? 1 : 0 , + self.hasDupe ? 1 : 0, + self.parentID, + self.parentName ?? "", // Workaround for dirty data before Bug 1318414. + self.feedURI, + self.siteURI, + self.pos, + self.title, + self.description, + self.bookmarkURI, + self.tags, + self.keyword, + self.folderName, + self.queryID, + self.guid, + ] + + return args + } +} + +private func deleteStructureForGUIDs(_ guids: [GUID], fromTable table: String, connection: SQLiteDBConnection, withMaxVars maxVars: Int=BrowserDB.MaxVariableNumber) throws { + log.debug("Deleting \(guids.count) parents from \(table).") + let chunks = chunk(guids, by: maxVars) + for chunk in chunks { + let delStructure = "DELETE FROM \(table) WHERE parent IN \(BrowserDB.varlist(chunk.count))" + + let args: Args = chunk.flatMap { $0 } + try connection.executeChange(delStructure, withArgs: args) + } +} + +private func insertStructureIntoTable(_ table: String, connection: SQLiteDBConnection, children: [Args], maxVars: Int) throws { + if children.isEmpty { + return + } + + // Insert the new structure rows. This uses three vars per row. + let maxRowsPerInsert: Int = maxVars / 3 + let chunks = chunk(children, by: maxRowsPerInsert) + for chunk in chunks { + log.verbose("Inserting \(chunk.count)…") + let childArgs: Args = chunk.flatMap { $0 } // Flatten [[a, b, c], [...]] into [a, b, c, ...]. + let ins = "INSERT INTO \(table) (parent, child, idx) VALUES " + + Array(repeating: "(?, ?, ?)", count: chunk.count).joined(separator: ", ") + log.debug("Inserting \(chunk.count) records (out of \(children.count)).") + try connection.executeChange(ins, withArgs: childArgs) + } +} + +/** + * This stores incoming records in a buffer. + * When appropriate, the buffer is merged with the mirror and local storage + * in the DB. + */ +open class SQLiteBookmarkBufferStorage: BookmarkBufferStorage { + let db: BrowserDB + + public init(db: BrowserDB) { + self.db = db + } + + open func synchronousBufferCount() -> Int? { + return self.db.runQuery("SELECT COUNT(*) FROM \(TableBookmarksBuffer)", args: nil, factory: IntFactory).value.successValue?[0] + } + + public func getUpstreamRecordCount() -> Deferred { + let sql = + "SELECT (SELECT COUNT(*) FROM \(TableBookmarksBuffer)) + " + + "(SELECT COUNT(*) FROM \(TableBookmarksMirror) WHERE is_overridden = 0) " + + "AS c" + + return self.db.runQuery(sql, args: nil, factory: IntFactory).bind { result in + return Deferred(value: result.successValue?[0]!) + } + } + + /** + * Remove child records for any folders that've been deleted or are empty. + */ + fileprivate func deleteChildrenInTransactionWithGUIDs(_ guids: [GUID], connection: SQLiteDBConnection, withMaxVars maxVars: Int=BrowserDB.MaxVariableNumber) throws { + try deleteStructureForGUIDs(guids, fromTable: TableBookmarksBufferStructure, connection: connection, withMaxVars: maxVars) + } + + open func isEmpty() -> Deferred> { + return self.db.queryReturnsNoResults("SELECT 1 FROM \(TableBookmarksBuffer)") + } + + /** + * This is a little gnarly because our DB access layer is rough. + * Within a single transaction, we walk the list of items, attempting to update + * and inserting if the update failed. (TODO: batch the inserts!) + * Once we've added all of the records, we flatten all of their children + * into big arg lists and hard-update the structure table. + */ + open func applyRecords(_ records: [BookmarkMirrorItem]) -> Success { + return self.applyRecords(records, withMaxVars: BrowserDB.MaxVariableNumber) + } + + open func applyRecords(_ records: [BookmarkMirrorItem], withMaxVars maxVars: Int) -> Success { + let guids = records.map { $0.guid } + let deleted = records.filter { $0.isDeleted }.map { $0.guid } + let values = records.map { $0.getUpdateOrInsertArgs() } + let children = records.filter { !$0.isDeleted }.flatMap { $0.getChildrenArgs() } + let folders = records.filter { $0.type == BookmarkNodeType.folder }.map { $0.guid } + + return db.transaction { conn -> Void in + // These have the same values in the same order. + let update = + "UPDATE \(TableBookmarksBuffer) SET " + + "type = ?, date_added = ?, server_modified = ?, is_deleted = ?, " + + "hasDupe = ?, parentid = ?, parentName = ?, " + + "feedUri = ?, siteUri = ?, pos = ?, title = ?, " + + "description = ?, bmkUri = ?, tags = ?, keyword = ?, " + + "folderName = ?, queryId = ? " + + "WHERE guid = ?" + + // We used to use INSERT OR IGNORE here, but it muffles legitimate errors. The only + // real use for that is/was to catch duplicates, but the UPDATE we run first should + // serve that purpose just as well. + let insert = + "INSERT INTO \(TableBookmarksBuffer) " + + "(type, date_added, server_modified, is_deleted, hasDupe, parentid, parentName, " + + "feedUri, siteUri, pos, title, description, bmkUri, tags, keyword, folderName, queryId, guid) " + + "VALUES " + + "(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)" + + for args in values { + try conn.executeChange(update, withArgs: args) + + if conn.numberOfRowsModified > 0 { + continue + } + + try conn.executeChange(insert, withArgs: args) + } + + // Delete existing structure for any folders we've seen. We always trust the folders, + // not the children's parent pointers, so we do this here: we'll insert their current + // children right after, when we process the child structure rows. + // We only drop the child structure for deleted folders, not the record itself. + // Deleted records stay in the buffer table so that we know about the deletion + // when we do a real sync! + + log.debug("\(folders.count) folders and \(deleted.count) deleted maybe-folders to drop from buffer structure table.") + + try self.deleteChildrenInTransactionWithGUIDs(folders + deleted, connection: conn) + + // (Re-)insert children in chunks. + log.debug("Inserting \(children.count) children.") + try insertStructureIntoTable(TableBookmarksBufferStructure, connection: conn, children: children, maxVars: maxVars) + + // Drop pending deletions of items we just received. + // In practice that means we have made the choice that we will always + // discard local deletions if there was a modification or a deletion made remotely. + log.debug("Deleting \(guids.count) pending deletions.") + let chunks = chunk(guids, by: BrowserDB.MaxVariableNumber) + for chunk in chunks { + let delPendingDeletions = "DELETE FROM \(TablePendingBookmarksDeletions) WHERE id IN \(BrowserDB.varlist(chunk.count))" + + let args: Args = chunk.flatMap { $0 } + try conn.executeChange(delPendingDeletions, withArgs: args) + } + } + } + + open func doneApplyingRecordsAfterDownload() -> Success { + self.db.checkpoint() + return succeed() + } +} + +extension SQLiteBookmarkBufferStorage: BufferItemSource { + public func getBufferItemWithGUID(_ guid: GUID) -> Deferred> { + return self.db.getMirrorItemFromTable(TableBookmarksBuffer, guid: guid) + } + + public func getBufferItemsWithGUIDs(_ guids: T) -> Deferred> where T.Iterator.Element == GUID { + return self.db.getMirrorItemsFromTable(TableBookmarksBuffer, guids: guids) + } + + public func getBufferChildrenGUIDsForParent(_ guid: GUID) -> Deferred> { + return self.getChildrenGUIDsOf(guid) + } + + public func prefetchBufferItemsWithGUIDs(_ guids: T) -> Success where T.Iterator.Element == GUID { + log.debug("Not implemented.") + return succeed() + } +} + +extension BrowserDB { + fileprivate func getMirrorItemFromTable(_ table: String, guid: GUID) -> Deferred> { + let args: Args = [guid] + let sql = "SELECT * FROM \(table) WHERE guid = ?" + return self.runQuery(sql, args: args, factory: BookmarkFactory.mirrorItemFactory) + >>== { cursor in + guard let item = cursor[0] else { + return deferMaybe(DatabaseError(description: "Expected to find \(guid) in \(table) but did not.")) + } + return deferMaybe(item) + } + } + + fileprivate func getMirrorItemsFromTable(_ table: String, guids: T) -> Deferred> where T.Iterator.Element == GUID { + var acc: [GUID: BookmarkMirrorItem] = [:] + func accumulate(_ args: Args) -> Success { + let sql = "SELECT * FROM \(table) WHERE guid IN \(BrowserDB.varlist(args.count))" + return self.runQuery(sql, args: args, factory: BookmarkFactory.mirrorItemFactory) + >>== { cursor in + cursor.forEach { row in + guard let row = row else { return } // Oh, Cursor. + acc[row.guid] = row + } + return succeed() + } + } + + let args: Args = guids.map { $0 } + if args.count < BrowserDB.MaxVariableNumber { + return accumulate(args) >>> { deferMaybe(acc) } + } + + let chunks = chunk(args, by: BrowserDB.MaxVariableNumber) + return walk(chunks.lazy.map { Array($0) }, f: accumulate) + >>> { deferMaybe(acc) } + } +} + +extension MergedSQLiteBookmarks: BookmarkBufferStorage { + public func synchronousBufferCount() -> Int? { + return self.buffer.synchronousBufferCount() + } + + public func getUpstreamRecordCount() -> Deferred { + return self.buffer.getUpstreamRecordCount() + } + + public func isEmpty() -> Deferred> { + return self.buffer.isEmpty() + } + + public func applyRecords(_ records: [BookmarkMirrorItem]) -> Success { + return self.buffer.applyRecords(records) + } + + public func doneApplyingRecordsAfterDownload() -> Success { + // It doesn't really matter which one we checkpoint -- they're both backed by the same DB. + return self.buffer.doneApplyingRecordsAfterDownload() + } + + public func validate() -> Success { + return self.buffer.validate() + } + + public func getBufferedDeletions() -> Deferred> { + return self.buffer.getBufferedDeletions() + } + + public func applyBufferCompletionOp(_ op: BufferCompletionOp, itemSources: ItemSources) -> Success { + return self.buffer.applyBufferCompletionOp(op, itemSources: itemSources) + } +} + +extension MergedSQLiteBookmarks: BufferItemSource { + public func getBufferItemWithGUID(_ guid: GUID) -> Deferred> { + return self.buffer.getBufferItemWithGUID(guid) + } + + public func getBufferItemsWithGUIDs(_ guids: T) -> Deferred> where T.Iterator.Element == GUID { + return self.buffer.getBufferItemsWithGUIDs(guids) + } + + public func getBufferChildrenGUIDsForParent(_ guid: GUID) -> Deferred> { + return self.buffer.getChildrenGUIDsOf(guid) + } + + public func prefetchBufferItemsWithGUIDs(_ guids: T) -> Success where T.Iterator.Element == GUID { + return self.buffer.prefetchBufferItemsWithGUIDs(guids) + } +} + +extension MergedSQLiteBookmarks: MirrorItemSource { + public func getMirrorItemWithGUID(_ guid: GUID) -> Deferred> { + return self.local.getMirrorItemWithGUID(guid) + } + + public func getMirrorItemsWithGUIDs(_ guids: T) -> Deferred> where T.Iterator.Element == GUID { + return self.local.getMirrorItemsWithGUIDs(guids) + } + + public func prefetchMirrorItemsWithGUIDs(_ guids: T) -> Success where T.Iterator.Element == GUID { + return self.local.prefetchMirrorItemsWithGUIDs(guids) + } +} + +extension MergedSQLiteBookmarks: LocalItemSource { + public func getLocalItemWithGUID(_ guid: GUID) -> Deferred> { + return self.local.getLocalItemWithGUID(guid) + } + + public func getLocalItemsWithGUIDs(_ guids: T) -> Deferred> where T.Iterator.Element == GUID { + return self.local.getLocalItemsWithGUIDs(guids) + } + + public func prefetchLocalItemsWithGUIDs(_ guids: T) -> Success where T.Iterator.Element == GUID { + return self.local.prefetchLocalItemsWithGUIDs(guids) + } +} + +extension MergedSQLiteBookmarks: ShareToDestination { + public func shareItem(_ item: ShareItem) -> Success { + return self.local.shareItem(item) + } +} + +// Not actually implementing SyncableBookmarks, just a utility for MergedSQLiteBookmarks to do so. +extension SQLiteBookmarks { + public func isUnchanged() -> Deferred> { + return self.db.queryReturnsNoResults("SELECT 1 FROM \(TableBookmarksLocal)") + } + + // Retrieve all the local bookmarks that are not present remotely in order to avoid merge logic later. + public func getLocalBookmarksModifications(limit: Int) -> Deferred> { + let deletionsQuery = + "SELECT id FROM \(TablePendingBookmarksDeletions) " + + "LIMIT ?" + let deletionsArgs: Args = [limit] + + return db.runQuery(deletionsQuery, args: deletionsArgs, factory: StringFactory) >>== { + let deletedGUIDs = $0.asArray() + let newLimit = limit - deletedGUIDs.count + + let additionsQuery = + "SELECT * FROM \(TableBookmarksLocal) AS bookmarks " + + "WHERE type = \(BookmarkNodeType.bookmark.rawValue) AND sync_status = \(SyncStatus.new.rawValue) AND parentID = ? " + + "AND NOT EXISTS (SELECT 1 FROM \(TableBookmarksBuffer) buf WHERE buf.guid = bookmarks.guid) " + + "LIMIT ?" + let additionsArgs: Args = [BookmarkRoots.MobileFolderGUID, newLimit] + + return self.db.runQuery(additionsQuery, args: additionsArgs, factory: BookmarkFactory.mirrorItemFactory) >>== { + return deferMaybe((deletedGUIDs, $0.asArray())) + } + } + } + + public func getLocalDeletions() -> Deferred> { + let sql = + "SELECT guid, local_modified FROM \(TableBookmarksLocal) " + + "WHERE is_deleted = 1" + + return self.db.runQuery(sql, args: nil, factory: { ($0["guid"] as! GUID, $0.getTimestamp("local_modified")!) }) + >>== { deferMaybe($0.asArray()) } + } +} + +extension MergedSQLiteBookmarks: SyncableBookmarks { + public func isUnchanged() -> Deferred> { + return self.local.isUnchanged() + } + + public func getLocalBookmarksModifications(limit: Int) -> Deferred> { + return self.local.getLocalBookmarksModifications(limit: limit) + } + + public func getLocalDeletions() -> Deferred> { + return self.local.getLocalDeletions() + } + + public func treeForMirror() -> Deferred> { + return self.local.treeForMirror() + } + + public func treesForEdges() -> Deferred> { + return self.local.treeForLocal() >>== { local in + return self.local.treeForBuffer() >>== { buffer in + return deferMaybe((local: local, buffer: buffer)) + } + } + } +} + +// MARK: - Validation of buffer contents. + +// Note that these queries tend to not have exceptions for deletions. +// That's because a record can't be deleted in isolation -- if it's +// deleted its parent should be changed, too -- and so our views will +// correctly reflect that. We'll have updated rows in the structure table, +// and updated values -- and thus a complete override -- for the parent and +// the deleted child. +private let allBufferStructuresReferToRecords = [ +"SELECT s.child AS pointee, s.parent AS pointer FROM", +ViewBookmarksBufferStructureOnMirror, +"s LEFT JOIN", +ViewBookmarksBufferOnMirror, +"b ON b.guid = s.child WHERE b.guid IS NULL", +].joined(separator: " ") + +private let allNonDeletedBufferRecordsAreInStructure = [ +"SELECT b.guid AS missing, b.parentid AS parent FROM", +ViewBookmarksBufferOnMirror, "b LEFT JOIN", +ViewBookmarksBufferStructureOnMirror, +"s ON b.guid = s.child WHERE s.child IS NULL AND", +"b.is_deleted IS 0 AND b.parentid IS NOT '\(BookmarkRoots.RootGUID)'", +].joined(separator: " ") + +private let allRecordsAreChildrenOnce = [ +"SELECT s.child FROM", +ViewBookmarksBufferStructureOnMirror, +"s INNER JOIN (", +"SELECT child, COUNT(*) AS dupes FROM", ViewBookmarksBufferStructureOnMirror, +"GROUP BY child HAVING dupes > 1", +") i ON s.child = i.child", +].joined(separator: " ") + +private let bufferParentidMatchesStructure = [ +"SELECT b.guid, b.parentid, s.parent, s.child, s.idx FROM", +TableBookmarksBuffer, "b JOIN", TableBookmarksBufferStructure, +"s ON b.guid = s.child WHERE b.is_deleted IS 0 AND b.parentid IS NOT s.parent", +].joined(separator: " ") + +public enum BufferInconsistency { + case missingValues + case missingStructure + case overlappingStructure + case parentIDDisagreement + + public var query: String { + switch self { + case .missingValues: + return allBufferStructuresReferToRecords + case .missingStructure: + return allNonDeletedBufferRecordsAreInStructure + case .overlappingStructure: + return allRecordsAreChildrenOnce + case .parentIDDisagreement: + return bufferParentidMatchesStructure + } + } + + public var trackingEvent: String { + switch self { + case .missingValues: + return "missingvalues" + case .missingStructure: + return "missingstructure" + case .overlappingStructure: + return "overlappingstructure" + case .parentIDDisagreement: + return "parentiddisagreement" + } + } + + public var description: String { + switch self { + case .missingValues: + return "Not all buffer structures refer to records." + case .missingStructure: + return "Not all buffer records are in structure." + case .overlappingStructure: + return "Some buffer structures refer to the same records." + case .parentIDDisagreement: + return "Some buffer record parent IDs don't match structure." + } + } + + var idsFactory: (SDRow) -> [String] { + switch self { + case .missingValues: + return self.getConcernedIDs(colNames: ["pointee", "pointer"]) + case .missingStructure: + return self.getConcernedIDs(colNames: ["missing", "parent"]) + case .overlappingStructure: + return self.getConcernedIDs(colNames: ["child"]) + case .parentIDDisagreement: + return self.getConcernedIDs(colNames: ["guid", "parentid", "parent", "child"]) + } + } + + private func getConcernedIDs(colNames: [String]) -> ((SDRow) -> [String]) { + return { (row: SDRow) in + colNames.flatMap({ row[$0] as? String}) + } + } + + public static let all: [BufferInconsistency] = [.missingValues, .missingStructure, .overlappingStructure, .parentIDDisagreement] +} + +public struct BufferInvalidError: MaybeErrorType { + public let description = "Bookmarks buffer contains invalid data" + public let inconsistencies: [BufferInconsistency: [GUID]] + public let validationDuration: Int64 + + public init(inconsistencies: [BufferInconsistency: [GUID]], validationDuration: Int64) { + self.inconsistencies = inconsistencies + self.validationDuration = validationDuration + } +} + +extension SQLiteBookmarkBufferStorage { + public func validate() -> Success { + func idsFor(inconsistency inc: BufferInconsistency) -> () -> Deferred> { + return { + self.db.runQuery(inc.query, args: nil, factory: inc.idsFactory) + >>== { deferMaybe((type: inc, ids: $0.asArray().reduce([], +))) } + } + } + + let start = Date.now() + let ops = BufferInconsistency.all.map { idsFor(inconsistency: $0) } + return accumulate(ops) >>== { results in + var inconsistencies = [BufferInconsistency: [GUID]]() + results.forEach { type, ids in + guard !ids.isEmpty else { return } + inconsistencies[type] = ids + } + + return inconsistencies.isEmpty ? succeed() : + deferMaybe(BufferInvalidError(inconsistencies: inconsistencies, validationDuration: Int64(Date.now() - start))) + } + } + + public func getBufferedDeletions() -> Deferred> { + let sql = + "SELECT guid, server_modified FROM \(TableBookmarksBuffer) " + + "WHERE is_deleted = 1" + + return self.db.runQuery(sql, args: nil, factory: { ($0["guid"] as! GUID, $0.getTimestamp("server_modified")!) }) + >>== { deferMaybe($0.asArray()) } + } +} + +extension SQLiteBookmarks { + fileprivate func structureQueryForTable(_ table: String, structure: String) -> String { + // We use a subquery so we get back rows for overridden folders, even when their + // children aren't in the shadowing table. + let sql = + "SELECT s.parent AS parent, s.child AS child, COALESCE(m.type, -1) AS type " + + "FROM \(structure) s LEFT JOIN \(table) m ON s.child = m.guid AND m.is_deleted IS NOT 1 " + + "ORDER BY s.parent, s.idx ASC" + return sql + } + + fileprivate func remainderQueryForTable(_ table: String, structure: String) -> String { + // This gives us value rows that aren't children of a folder. + // You might notice that these complementary LEFT JOINs are how you + // express a FULL OUTER JOIN in sqlite. + // We exclude folders here because if they have children, they'll appear + // in the structure query, and if they don't, they'll appear in the bottom + // half of this query. + let sql = + "SELECT m.guid AS guid, m.type AS type " + + "FROM \(table) m LEFT JOIN \(structure) s ON s.child = m.guid " + + "WHERE m.is_deleted IS NOT 1 AND m.type IS NOT \(BookmarkNodeType.folder.rawValue) AND s.child IS NULL " + + + "UNION ALL " + + + // This gives us folders with no children. + "SELECT m.guid AS guid, m.type AS type " + + "FROM \(table) m LEFT JOIN \(structure) s ON s.parent = m.guid " + + "WHERE m.is_deleted IS NOT 1 AND m.type IS \(BookmarkNodeType.folder.rawValue) AND s.parent IS NULL " + + return sql + } + + fileprivate func statusQueryForTable(_ table: String) -> String { + return "SELECT guid, is_deleted FROM \(table)" + } + + fileprivate func treeForTable(_ table: String, structure: String, alwaysIncludeRoots includeRoots: Bool) -> Deferred> { + // The structure query doesn't give us non-structural rows -- that is, if you + // make a value-only change to a record, and it's not otherwise mentioned by + // way of being a child of a structurally modified folder, it won't appear here at all. + // It also doesn't give us empty folders, because they have no children. + // We run a separate query to get those. + let structureSQL = self.structureQueryForTable(table, structure: structure) + let remainderSQL = self.remainderQueryForTable(table, structure: structure) + let statusSQL = self.statusQueryForTable(table) + + func structureFactory(_ row: SDRow) -> StructureRow { + let typeCode = row["type"] as! Int + let type = BookmarkNodeType(rawValue: typeCode) // nil if typeCode is invalid (e.g., -1). + return (parent: row["parent"] as! GUID, child: row["child"] as! GUID, type: type) + } + + func nonStructureFactory(_ row: SDRow) -> BookmarkTreeNode { + let guid = row["guid"] as! GUID + let typeCode = row["type"] as! Int + if let type = BookmarkNodeType(rawValue: typeCode) { + switch type { + case .folder: + return BookmarkTreeNode.folder(guid: guid, children: []) + default: + return BookmarkTreeNode.nonFolder(guid: guid) + } + } else { + return BookmarkTreeNode.unknown(guid: guid) + } + } + + func statusFactory(_ row: SDRow) -> (GUID, Bool) { + return (row["guid"] as! GUID, row.getBoolean("is_deleted")) + } + + return self.db.runQuery(statusSQL, args: nil, factory: statusFactory) + >>== { cursor in + var deleted = Set() + var modified = Set() + cursor.forEach { pair in + let (guid, del) = pair! // Oh, cursor. + if del { + deleted.insert(guid) + } else { + modified.insert(guid) + } + } + + return self.db.runQuery(remainderSQL, args: nil, factory: nonStructureFactory) + >>== { cursor in + let nonFoldersAndEmptyFolders = cursor.asArray() + return self.db.runQuery(structureSQL, args: nil, factory: structureFactory) + >>== { cursor in + let structureRows = cursor.asArray() + let tree = BookmarkTree.mappingsToTreeForStructureRows(structureRows, withNonFoldersAndEmptyFolders: nonFoldersAndEmptyFolders, withDeletedRecords: deleted, modifiedRecords: modified, alwaysIncludeRoots: includeRoots) + return deferMaybe(tree) + } + } + } + } + + public func treeForMirror() -> Deferred> { + return self.treeForTable(TableBookmarksMirror, structure: TableBookmarksMirrorStructure, alwaysIncludeRoots: true) + } + + public func treeForBuffer() -> Deferred> { + return self.treeForTable(TableBookmarksBuffer, structure: TableBookmarksBufferStructure, alwaysIncludeRoots: false) + } + + public func treeForLocal() -> Deferred> { + return self.treeForTable(TableBookmarksLocal, structure: TableBookmarksLocalStructure, alwaysIncludeRoots: false) + } +} + +// MARK: - Applying merge operations. + +public extension SQLiteBookmarkBufferStorage { + public func applyBufferCompletionOp(_ op: BufferCompletionOp, itemSources: ItemSources) -> Success { + log.debug("Marking buffer rows as applied.") + if op.isNoOp { + log.debug("Nothing to do.") + return succeed() + } + + var queries: [(sql: String, args: Args?)] = [] + op.processedBufferChanges.subsetsOfSize(BrowserDB.MaxVariableNumber).forEach { guids in + let varlist = BrowserDB.varlist(guids.count) + let args: Args = guids.map { $0 } + queries.append((sql: "DELETE FROM \(TableBookmarksBufferStructure) WHERE parent IN \(varlist)", args: args)) + queries.append((sql: "DELETE FROM \(TableBookmarksBuffer) WHERE guid IN \(varlist)", args: args)) + } + + return self.db.run(queries) + } + + public func getChildrenGUIDsOf(_ guid: GUID) -> Deferred> { + let sql = "SELECT child FROM \(TableBookmarksBufferStructure) WHERE parent = ? ORDER BY idx ASC" + let args: Args = [guid] + + return self.db.runQuery(sql, args: args, factory: StringFactory) >>== { deferMaybe($0.asArray()) } + } +} + +extension MergedSQLiteBookmarks { + public func applyLocalOverrideCompletionOp(_ op: LocalOverrideCompletionOp, itemSources: ItemSources) -> Success { + log.debug("Applying local op to merged.") + if op.isNoOp { + log.debug("Nothing to do.") + return succeed() + } + + // This is a little tortured because we want it all to happen in a single transaction. + // We walk through the accrued work items, applying them in the right order (e.g., structure + // then value). If at any point we fail, we abort, roll back the transaction by throwing. + return local.db.transaction { conn -> Void in + // So we can trample the DB in any order. + try conn.executeChange("PRAGMA defer_foreign_keys = ON") + + log.debug("Deleting \(op.mirrorItemsToDelete.count) mirror items.") + + try op.mirrorItemsToDelete.withSubsetsOfSize(BrowserDB.MaxVariableNumber) { guids in + let args: Args = guids.map { $0 } + let varlist = BrowserDB.varlist(guids.count) + + let sqlMirrorStructure = "DELETE FROM \(TableBookmarksMirrorStructure) WHERE parent IN \(varlist)" + try conn.executeChange(sqlMirrorStructure, withArgs: args) + + let sqlMirror = "DELETE FROM \(TableBookmarksMirror) WHERE guid IN \(varlist)" + try conn.executeChange(sqlMirror, withArgs: args) + } + + // Copy from other tables for simplicity. + // Do this *before* we throw away local and buffer changes! + // This is one reason why the local override step needs to be processed before the buffer is cleared. + try op.mirrorValuesToCopyFromBuffer.withSubsetsOfSize(BrowserDB.MaxVariableNumber) { guids in + let args: Args = guids.map { $0 } + let varlist = BrowserDB.varlist(guids.count) + let copySQL = [ + "INSERT OR REPLACE INTO \(TableBookmarksMirror)", + "(guid, type, date_added, parentid, parentName, feedUri, siteUri, pos, title, description,", + "bmkUri, tags, keyword, folderName, queryId, server_modified)", + "SELECT guid, type, date_added, parentid, parentName, feedUri, siteUri, pos, title, description,", + "bmkUri, tags, keyword, folderName, queryId, server_modified", + "FROM \(TableBookmarksBuffer)", + "WHERE guid IN", + varlist + ].joined(separator: " ") + + try conn.executeChange(copySQL, withArgs: args) + } + + try op.mirrorValuesToCopyFromLocal.withSubsetsOfSize(BrowserDB.MaxVariableNumber) { guids in + let args: Args = guids.map { $0 } + let varlist = BrowserDB.varlist(guids.count) + let copySQL = [ + "INSERT OR REPLACE INTO \(TableBookmarksMirror)", + "(guid, type, date_added, parentid, parentName, feedUri, siteUri, pos, title, description,", + "bmkUri, tags, keyword, folderName, queryId, faviconID, server_modified)", + "SELECT guid, type, date_added, parentid, parentName, feedUri, siteUri, pos, title, description,", + "bmkUri, tags, keyword, folderName, queryId, faviconID,", + + // This will be fixed up in batches after the initial copy. + "0 AS server_modified", + "FROM \(TableBookmarksLocal) WHERE guid IN", + varlist + ].joined(separator: " ") + + try conn.executeChange(copySQL, withArgs: args) + } + + try op.modifiedTimes.forEach { (time, guids) in + // This will never be too big: we upload in chunks + // smaller than 999! + precondition(guids.count < BrowserDB.MaxVariableNumber) + + log.debug("Swizzling server modified time to \(time) for \(guids.count) GUIDs.") + let args: Args = guids.map { $0 } + let varlist = BrowserDB.varlist(guids.count) + let updateSQL = [ + "UPDATE \(TableBookmarksMirror) SET server_modified = \(time)", + "WHERE guid IN", + varlist, + ].joined(separator: " ") + try conn.executeChange(updateSQL, withArgs: args) + } + + log.debug("Marking \(op.processedLocalChanges.count) local changes as processed.") + try op.processedLocalChanges.withSubsetsOfSize(BrowserDB.MaxVariableNumber) { guids in + let args: Args = guids.map { $0 } + let varlist = BrowserDB.varlist(guids.count) + + let sqlLocalStructure = "DELETE FROM \(TableBookmarksLocalStructure) WHERE parent IN \(varlist)" + try conn.executeChange(sqlLocalStructure, withArgs: args) + + let sqlLocal = "DELETE FROM \(TableBookmarksLocal) WHERE guid IN \(varlist)" + try conn.executeChange(sqlLocal, withArgs: args) + + // If the values change, we'll handle those elsewhere, but at least we need to mark these as non-overridden. + let sqlMirrorOverride = "UPDATE \(TableBookmarksMirror) SET is_overridden = 0 WHERE guid IN \(varlist)" + try conn.executeChange(sqlMirrorOverride, withArgs: args) + } + + if !op.mirrorItemsToUpdate.isEmpty { + let updateSQL = [ + "UPDATE \(TableBookmarksMirror) SET", + "type = ?, date_added = ?, server_modified = ?, is_deleted = ?,", + "hasDupe = ?, parentid = ?, parentName = ?,", + "feedUri = ?, siteUri = ?, pos = ?, title = ?,", + "description = ?, bmkUri = ?, tags = ?, keyword = ?,", + "folderName = ?, queryId = ?, is_overridden = 0", + "WHERE guid = ?", + ].joined(separator: " ") + + try op.mirrorItemsToUpdate.forEach { (_, mirrorItem) in + let args = mirrorItem.getUpdateOrInsertArgs() + try conn.executeChange(updateSQL, withArgs: args) + } + } + + if !op.mirrorItemsToInsert.isEmpty { + let insertSQL = [ + "INSERT OR IGNORE INTO \(TableBookmarksMirror) (", + "type, date_added, server_modified, is_deleted,", + "hasDupe, parentid, parentName,", + "feedUri, siteUri, pos, title,", + "description, bmkUri, tags, keyword,", + "folderName, queryId, guid", + "VALUES", + "(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", + ].joined(separator: " ") + + try op.mirrorItemsToInsert.forEach { (_, mirrorItem) in + let args = mirrorItem.getUpdateOrInsertArgs() + try conn.executeChange(insertSQL, withArgs: args) + } + } + + if !op.mirrorStructures.isEmpty { + let structureRows = + op.mirrorStructures.flatMap { (parent, children) in + return children.enumerated().map { (idx, child) -> Args in + let vals: Args = [parent, child, idx] + return vals + } + } + + let parents = op.mirrorStructures.map { $0.0 } + try deleteStructureForGUIDs(parents, fromTable: TableBookmarksMirrorStructure, connection: conn) + try insertStructureIntoTable(TableBookmarksMirrorStructure, connection: conn, children: structureRows, maxVars: BrowserDB.MaxVariableNumber) + } + } + } + + public func applyBufferUpdatedCompletionOp(_ op: BufferUpdatedCompletionOp) -> Success { + log.debug("Applying buffer updates.") + if op.isNoOp { + log.debug("Nothing to do.") + return succeed() + } + + return local.db.transaction { conn -> Void in + // So we can trample the DB in any order. + try conn.executeChange("PRAGMA defer_foreign_keys = ON") + + try op.deletedValues.withSubsetsOfSize(BrowserDB.MaxVariableNumber) { guids in + let args: Args = guids.map { $0 } + let varlist = BrowserDB.varlist(guids.count) + + // This will leave the idx column sparse for the buffer children. + let sqlBufferStructure = "DELETE FROM \(TableBookmarksBufferStructure) WHERE child IN \(varlist)" + try conn.executeChange(sqlBufferStructure, withArgs: args) + + // This will also delete items from the pending deletions table on cascade. + let sqlBuffer = "DELETE FROM \(TableBookmarksBuffer) WHERE guid IN \(varlist)" + try conn.executeChange(sqlBuffer, withArgs: args) + } + + let insertSQL = [ + "INSERT OR IGNORE INTO \(TableBookmarksBuffer) (", + "type, date_added, server_modified, is_deleted,", + "hasDupe, parentid, parentName,", + "feedUri, siteUri, pos, title,", + "description, bmkUri, tags, keyword,", + "folderName, queryId, guid)", + "VALUES", + "(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", + ].joined(separator: " ") + + let args = op.mobileRoot.getUpdateOrInsertArgs() + try conn.executeChange(insertSQL, withArgs: args) + + // We update server_modified in another operation because the first statement is an INSERT OR IGNORE + // and we can't turn that into a INSERT OR REPLACE because this will cascade delete children + // in the buffer structure table. + let updateMobileRootTimeSQL = [ + "UPDATE \(TableBookmarksBuffer) SET server_modified = \(op.modifiedTime)", + "WHERE guid = ?", + ].joined(separator: " ") + try conn.executeChange(updateMobileRootTimeSQL, withArgs: [op.mobileRoot.guid]) + + try op.bufferValuesToMoveFromLocal.withSubsetsOfSize(BrowserDB.MaxVariableNumber) { guids in + let args: Args = guids.map { $0 } + let varlist = BrowserDB.varlist(guids.count) + + let copySQL = [ + "INSERT OR REPLACE INTO \(TableBookmarksBuffer)", + "(guid, type, date_added, parentid, parentName, feedUri, siteUri, pos, title, description,", + "bmkUri, tags, keyword, folderName, queryId, server_modified)", + "SELECT guid, type, date_added, parentid, parentName, feedUri, siteUri, pos, title, description,", + "bmkUri, tags, keyword, folderName, queryId,", + "\(op.modifiedTime) AS server_modified", + "FROM \(TableBookmarksLocal) WHERE guid IN", + varlist + ].joined(separator: " ") + try conn.executeChange(copySQL, withArgs: args) + } + + try op.bufferValuesToMoveFromLocal.withSubsetsOfSize(BrowserDB.MaxVariableNumber) { guids in + let args: Args = guids.map { $0 } + let varlist = BrowserDB.varlist(guids.count) + + // This will leave the idx column sparse for the local children. + let sqlLocalStructure = "DELETE FROM \(TableBookmarksLocalStructure) WHERE child IN \(varlist)" + try conn.executeChange(sqlLocalStructure, withArgs: args) + + let sqlLocal = "DELETE FROM \(TableBookmarksLocal) WHERE guid IN \(varlist)" + try conn.executeChange(sqlLocal, withArgs: args) + } + + if op.bufferValuesToMoveFromLocal.count > 0 { + guard let allChildren = op.mobileRoot.children else { + let err = DatabaseError(description: "Absent mobileRoot children. Aborting.") + log.error("applyBufferUpdatedCompletionOp(_:) encountered error: \(err.localizedDescription)") + throw err + } + let offset = allChildren.count - op.bufferValuesToMoveFromLocal.count + + let sqlNextIdx = "SELECT (COALESCE(MAX(idx), -1) + 1) AS newIndex FROM \(TableBookmarksBufferStructure) WHERE parent = ?" + let nextIdxArgs: Args = [BookmarkRoots.MobileFolderGUID] + let nextIdx = conn.executeQuery(sqlNextIdx, factory: IntFactory, withArgs: nextIdxArgs).asArray()[0] + + let toInsertArgs = Array(op.mobileRoot.getChildrenArgs(offset: offset, nextIdx: nextIdx)) + try insertStructureIntoTable(TableBookmarksBufferStructure, connection: conn, children: toInsertArgs, maxVars: BrowserDB.MaxVariableNumber) + } + } + } +} diff --git a/mobile/ios/Storage/SQL/SQLiteFavicons.swift b/mobile/ios/Storage/SQL/SQLiteFavicons.swift new file mode 100644 index 0000000000..63f43799d6 --- /dev/null +++ b/mobile/ios/Storage/SQL/SQLiteFavicons.swift @@ -0,0 +1,87 @@ +/* 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 Deferred +import Shared + +open class SQLiteFavicons { + let db: BrowserDB + + required public init(db: BrowserDB) { + self.db = db + } + + public func getFaviconIDQuery(url: String) -> (sql: String, args: Args?) { + var args: Args = [] + args.append(url) + return (sql: "SELECT id FROM \(TableFavicons) WHERE url = ? LIMIT 1", args: args) + } + + public func getInsertFaviconQuery(favicon: Favicon) -> (sql: String, args: Args?) { + var args: Args = [] + args.append(favicon.url) + args.append(favicon.width) + args.append(favicon.height) + args.append(favicon.date) + args.append(favicon.type.rawValue) + return (sql: "INSERT INTO \(TableFavicons) (url, width, height, date, type) VALUES (?,?,?,?,?)", args: args) + } + + public func getUpdateFaviconQuery(favicon: Favicon) -> (sql: String, args: Args?) { + var args = Args() + args.append(favicon.width) + args.append(favicon.height) + args.append(favicon.date) + args.append(favicon.type.rawValue) + args.append(favicon.url) + return (sql: "UPDATE \(TableFavicons) SET width = ?, height = ?, date = ?, type = ? WHERE url = ?", args: args) + } + + public func getCleanupFaviconsQuery() -> (sql: String, args: Args?) { + return (sql: "DELETE FROM \(TableFavicons) " + + "WHERE \(TableFavicons).id NOT IN (" + + "SELECT faviconID FROM \(TableFaviconSites) " + + "UNION ALL " + + "SELECT faviconID FROM \(TableBookmarksLocal) WHERE faviconID IS NOT NULL " + + "UNION ALL " + + "SELECT faviconID FROM \(TableBookmarksMirror) WHERE faviconID IS NOT NULL" + + ")", args: nil) + } + + public func insertOrUpdateFavicon(_ favicon: Favicon) -> Deferred> { + return db.withConnection { conn -> Int in + self.insertOrUpdateFaviconInTransaction(favicon, conn: conn) ?? 0 + } + } + + func insertOrUpdateFaviconInTransaction(_ favicon: Favicon, conn: SQLiteDBConnection) -> Int? { + let query = self.getFaviconIDQuery(url: favicon.url) + let cursor = conn.executeQuery(query.sql, factory: IntFactory, withArgs: query.args) + + if let id = cursor[0] { + let updateQuery = self.getUpdateFaviconQuery(favicon: favicon) + do { + try conn.executeChange(updateQuery.sql, withArgs: updateQuery.args) + } catch { + return nil + } + + return id + } + + let insertQuery = self.getInsertFaviconQuery(favicon: favicon) + do { + try conn.executeChange(insertQuery.sql, withArgs: insertQuery.args) + } catch { + return nil + } + + return conn.lastInsertedRowID + } + + public func cleanupFavicons() -> Success { + return self.db.run([getCleanupFaviconsQuery()]) + } +} diff --git a/mobile/ios/Storage/SQL/SQLiteHistory.swift b/mobile/ios/Storage/SQL/SQLiteHistory.swift new file mode 100644 index 0000000000..6c6579a9c2 --- /dev/null +++ b/mobile/ios/Storage/SQL/SQLiteHistory.swift @@ -0,0 +1,1118 @@ +/* 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 XCGLogger +import Deferred + +private let log = Logger.syncLogger +public let TopSiteCacheSize: Int32 = 16 + +class NoSuchRecordError: MaybeErrorType { + let guid: GUID + init(guid: GUID) { + self.guid = guid + } + var description: String { + return "No such record: \(guid)." + } +} + +private var ignoredSchemes = ["about"] + +public func isIgnoredURL(_ url: URL) -> Bool { + guard let scheme = url.scheme else { return false } + + if let _ = ignoredSchemes.index(of: scheme) { + return true + } + + if url.host == "localhost" { + return true + } + + return false +} + +public func isIgnoredURL(_ url: String) -> Bool { + if let url = URL(string: url) { + return isIgnoredURL(url) + } + + return false +} + +/* +// Here's the Swift equivalent of the below. +func simulatedFrecency(now: MicrosecondTimestamp, then: MicrosecondTimestamp, visitCount: Int) -> Double { + let ageMicroseconds = (now - then) + let ageDays = Double(ageMicroseconds) / 86400000000.0 // In SQL the .0 does the coercion. + let f = 100 * 225 / ((ageSeconds * ageSeconds) + 225) + return Double(visitCount) * max(1.0, f) +} +*/ + +// The constants in these functions were arrived at by utterly unscientific experimentation. + +func getRemoteFrecencySQL() -> String { + let visitCountExpression = "remoteVisitCount" + let now = Date.nowMicroseconds() + let microsecondsPerDay = 86_400_000_000.0 // 1000 * 1000 * 60 * 60 * 24 + let ageDays = "((\(now) - remoteVisitDate) / \(microsecondsPerDay))" + + return "\(visitCountExpression) * max(1, 100 * 110 / (\(ageDays) * \(ageDays) + 110))" +} + +func getLocalFrecencySQL() -> String { + let visitCountExpression = "((2 + localVisitCount) * (2 + localVisitCount))" + let now = Date.nowMicroseconds() + let microsecondsPerDay = 86_400_000_000.0 // 1000 * 1000 * 60 * 60 * 24 + let ageDays = "((\(now) - localVisitDate) / \(microsecondsPerDay))" + + return "\(visitCountExpression) * max(2, 100 * 225 / (\(ageDays) * \(ageDays) + 225))" +} + +fileprivate func computeWordsWithFilter(_ filter: String) -> [String] { + // Split filter on whitespace. + let words = filter.components(separatedBy: .whitespaces) + + // Remove substrings and duplicates. + // TODO: this can probably be improved. + return words.enumerated().filter({ (index: Int, word: String) in + if word.isEmpty { + return false + } + + for i in words.indices where i != index { + if words[i].range(of: word) != nil && (words[i].count != word.count || i < index) { + return false + } + } + + return true + }).map({ $0.1 }) +} + +/** + * Take input like "foo bar" and a template fragment and produce output like + * + * ((x.y LIKE ?) OR (x.z LIKE ?)) AND ((x.y LIKE ?) OR (x.z LIKE ?)) + * + * with args ["foo", "foo", "bar", "bar"]. + */ +internal func computeWhereFragmentWithFilter(_ filter: String, perWordFragment: String, perWordArgs: (String) -> Args) -> (fragment: String, args: Args) { + precondition(!filter.isEmpty) + + let words = computeWordsWithFilter(filter) + return computeWhereFragmentForWords(words, perWordFragment: perWordFragment, perWordArgs: perWordArgs) +} + +internal func computeWhereFragmentForWords(_ words: [String], perWordFragment: String, perWordArgs: (String) -> Args) -> (fragment: String, args: Args) { + assert(!words.isEmpty) + + let fragment = Array(repeating: perWordFragment, count: words.count).joined(separator: " AND ") + let args = words.flatMap(perWordArgs) + return (fragment, args) +} + +extension SDRow { + func getTimestamp(_ column: String) -> Timestamp? { + return (self[column] as? NSNumber)?.uint64Value + } + + func getBoolean(_ column: String) -> Bool { + if let val = self[column] as? Int { + return val != 0 + } + return false + } +} + +/** + * The sqlite-backed implementation of the history protocol. + */ +open class SQLiteHistory { + let db: BrowserDB + let favicons: SQLiteFavicons + let prefs: Prefs + let clearTopSitesQuery: (String, Args?) = ("DELETE FROM \(TableCachedTopSites)", nil) + + required public init(db: BrowserDB, prefs: Prefs) { + self.db = db + self.favicons = SQLiteFavicons(db: self.db) + self.prefs = prefs + } +} + +private let topSitesQuery = "SELECT \(TableCachedTopSites).*, \(TablePageMetadata).provider_name FROM \(TableCachedTopSites) LEFT OUTER JOIN \(TablePageMetadata) ON \(TableCachedTopSites).url = \(TablePageMetadata).site_url ORDER BY frecencies DESC LIMIT (?)" + +/** + * The init for this will perform the heaviest part of the frecency query + * and create a temporary table that can be queried quickly. Currently this accounts for + * >75% of the query time. + * The scope/lifetime of this object is important as the data is 'frozen' until a new instance is created. + */ +fileprivate struct SQLiteFrecentHistory : FrecentHistory { + private let db: BrowserDB + private let prefs: Prefs + + init(db: BrowserDB, prefs: Prefs) { + self.db = db + self.prefs = prefs + + let empty = "DELETE FROM \(TempTableAwesomebarBookmarks)" + let create = + "CREATE TEMP TABLE IF NOT EXISTS \(TempTableAwesomebarBookmarks) (" + + "guid TEXT, url TEXT, title TEXT, description TEXT, " + + "faviconID INT, visitDate DATE)" + let insert = + "INSERT INTO \(TempTableAwesomebarBookmarks) " + + "SELECT b.guid AS guid, b.url AS url, b.title AS title, " + + "b.description AS description, b.faviconID AS faviconID, " + + "h.visitDate AS visitDate " + + "FROM \(ViewAwesomebarBookmarks) b " + + "LEFT JOIN \(ViewHistoryVisits) h ON b.url = h.url" + + let _ = db.transaction { connection in + try connection.executeChange(create) + try connection.executeChange(empty) + try connection.executeChange(insert) + } + } + + func getSites(whereURLContains filter: String?, historyLimit limit: Int, bookmarksLimit: Int) -> Deferred>> { + let factory = SQLiteHistory.basicHistoryColumnFactory + + let params = FrecencyQueryParams.urlCompletion(bookmarksLimit: bookmarksLimit, whereURLContains: filter ?? "", groupClause: "GROUP BY historyID ") + let (query, args) = getFrecencyQuery(historyLimit: limit, params: params) + + return db.runQuery(query, args: args, factory: factory) + } + + func updateTopSitesCacheQuery() -> (String, Args?) { + let limit = Int(prefs.intForKey(PrefsKeys.KeyTopSitesCacheSize) ?? TopSiteCacheSize) + + let (whereData, groupBy) = topSiteClauses() + let params = FrecencyQueryParams.topSites(groupClause: groupBy, whereData: whereData) + let (frecencyQuery, args) = getFrecencyQuery(historyLimit: limit, params: params) + + // We must project, because we get bookmarks in these results. + let insertQuery = [ + "WITH siteFrecency AS (\(frecencyQuery))", + "INSERT INTO \(TableCachedTopSites)", + "SELECT historyID, url, title, guid, domain_id, domain,", + "localVisitDate, remoteVisitDate, localVisitCount, remoteVisitCount,", + "iconID, iconURL, iconDate, iconType, iconWidth, frecencies", + "FROM (SELECT * FROM", + "siteFrecency LEFT JOIN view_history_id_favicon ON", + "siteFrecency.historyID = view_history_id_favicon.id)" + ].joined(separator: " ") + + // @TODO: remove the LEFT JOIN to fill in the icon columns, it is just there because the code has been doing this historically. + // Those favicon data columns are not used, and view_history_id_favicon appears to only contain null data. + + return (insertQuery, args) + } + + private func topSiteClauses() -> (String, String) { + let whereData = "(\(TableDomains).showOnTopSites IS 1) AND (\(TableDomains).domain NOT LIKE 'r.%') AND (\(TableDomains).domain NOT LIKE 'google.%') " + let groupBy = "GROUP BY domain_id " + return (whereData, groupBy) + } + + + enum FrecencyQueryParams { + case urlCompletion(bookmarksLimit: Int, whereURLContains: String, groupClause: String) + case topSites(groupClause: String, whereData: String) + } + + private func getFrecencyQuery(historyLimit: Int, params: FrecencyQueryParams) -> (String, Args?) { + let bookmarksLimit: Int + let groupClause: String + let whereData: String? + let urlFilter: String? + + switch params { + case let .urlCompletion(bmLimit, filter, group): + bookmarksLimit = bmLimit + urlFilter = filter + groupClause = group + whereData = nil + case let .topSites(group, whereArg): + bookmarksLimit = 0 + urlFilter = nil + whereData = whereArg + groupClause = group + } + + let includeBookmarks = bookmarksLimit > 0 + let localFrecencySQL = getLocalFrecencySQL() + let remoteFrecencySQL = getRemoteFrecencySQL() + let sixMonthsInMicroseconds: UInt64 = 15_724_800_000_000 // 182 * 1000 * 1000 * 60 * 60 * 24 + let sixMonthsAgo = Date.nowMicroseconds() - sixMonthsInMicroseconds + + let args: Args + let whereClause: String + let whereFragment = (whereData == nil) ? "" : " AND (\(whereData!))" + + if let urlFilter = urlFilter?.trimmingCharacters(in: .whitespaces), !urlFilter.isEmpty { + let perWordFragment = "((url LIKE ?) OR (title LIKE ?))" + let perWordArgs: (String) -> Args = { ["%\($0)%", "%\($0)%"] } + let (filterFragment, filterArgs) = computeWhereFragmentWithFilter(urlFilter, perWordFragment: perWordFragment, perWordArgs: perWordArgs) + + // No deleted item has a URL, so there is no need to explicitly add that here. + whereClause = "WHERE (\(filterFragment))\(whereFragment)" + + if includeBookmarks { + // We'll need them twice: once to filter history, and once to filter bookmarks. + args = filterArgs + filterArgs + } else { + args = filterArgs + } + } else { + whereClause = " WHERE (\(TableHistory).is_deleted = 0)\(whereFragment)" + args = [] + } + + // Innermost: grab history items and basic visit/domain metadata. + var ungroupedSQL = + "SELECT \(TableHistory).id AS historyID, \(TableHistory).url AS url, \(TableHistory).title AS title, \(TableHistory).guid AS guid, domain_id, domain" + + ", COALESCE(max(case \(TableVisits).is_local when 1 then \(TableVisits).date else 0 end), 0) AS localVisitDate" + + ", COALESCE(max(case \(TableVisits).is_local when 0 then \(TableVisits).date else 0 end), 0) AS remoteVisitDate" + + ", COALESCE(sum(\(TableVisits).is_local), 0) AS localVisitCount" + + ", COALESCE(sum(case \(TableVisits).is_local when 1 then 0 else 1 end), 0) AS remoteVisitCount" + + " FROM \(TableHistory) " + + "INNER JOIN \(TableDomains) ON \(TableDomains).id = \(TableHistory).domain_id " + + "INNER JOIN \(TableVisits) ON \(TableVisits).siteID = \(TableHistory).id " + + if includeBookmarks { + ungroupedSQL.append("LEFT JOIN \(ViewAllBookmarks) on \(ViewAllBookmarks).url = \(TableHistory).url ") + } + ungroupedSQL.append(whereClause.replacingOccurrences(of: "url", with: "\(TableHistory).url").replacingOccurrences(of: "title", with: "\(TableHistory).title")) + if includeBookmarks { + ungroupedSQL.append(" AND \(ViewAllBookmarks).url IS NULL") + } + ungroupedSQL.append(" GROUP BY historyID") + + // Next: limit to only those that have been visited at all within the last six months. + // (Don't do that in the innermost: we want to get the full count, even if some visits are older.) + // Discard all but the 1000 most frecent. + // Compute and return the frecency for all 1000 URLs. + let frecenciedSQL = + "SELECT *, (\(localFrecencySQL) + \(remoteFrecencySQL)) AS frecency" + + " FROM (" + ungroupedSQL + ")" + + " WHERE (" + + "((localVisitCount > 0) OR (remoteVisitCount > 0)) AND " + // Eliminate dead rows from coalescing. + "((localVisitDate > \(sixMonthsAgo)) OR (remoteVisitDate > \(sixMonthsAgo)))" + // Exclude really old items. + ") ORDER BY frecency DESC" + + " LIMIT 1000" // Don't even look at a huge set. This avoids work. + + // Next: merge by domain and select the URL with the max frecency of a domain, ordering by that sum frecency and reducing to a (typically much lower) limit. + // NOTE: When using GROUP BY we need to be explicit about which URL to use when grouping. By using "max(frecency)" the result row + // for that domain will contain the projected URL corresponding to the history item with the max frecency, https://sqlite.org/lang_select.html#resultset + // This is the behavior we want in order to ensure that the most popular URL for a domain is used for the top sites tile. + // TODO: make is_bookmarked here accurate by joining against ViewAllBookmarks. + // TODO: ensure that the same URL doesn't appear twice in the list, either from duplicate + // bookmarks or from being in both bookmarks and history. + let historySQL = [ + "SELECT historyID, url, title, guid, domain_id, domain,", + "max(localVisitDate) AS localVisitDate,", + "max(remoteVisitDate) AS remoteVisitDate,", + "sum(localVisitCount) AS localVisitCount,", + "sum(remoteVisitCount) AS remoteVisitCount,", + "max(frecency) AS maxFrecency,", + "sum(frecency) AS frecencies,", + "0 AS is_bookmarked", + "FROM (", frecenciedSQL, ") ", + groupClause, + "ORDER BY frecencies DESC", + "LIMIT \(historyLimit)", + ].joined(separator: " ") + + let bookmarksSQL = [ + "SELECT NULL AS historyID, url, title, guid, NULL AS domain_id, NULL AS domain,", + "visitDate AS localVisitDate, 0 AS remoteVisitDate, 0 AS localVisitCount,", + "0 AS remoteVisitCount,", + "visitDate AS frecencies,", // Fake this for ordering purposes. + "0 as maxFrecency,", // Need this column for UNION + "1 AS is_bookmarked", + "FROM \(TempTableAwesomebarBookmarks)", + whereClause, // The columns match, so we can reuse this. + "GROUP BY url", + "ORDER BY visitDate DESC LIMIT \(bookmarksLimit)", + ].joined(separator: " ") + + if !includeBookmarks { + return (historySQL, args) + } + + let allSQL = "SELECT * FROM (SELECT * FROM (\(historySQL)) UNION SELECT * FROM (\(bookmarksSQL))) ORDER BY is_bookmarked DESC, frecencies DESC" + return (allSQL, args) + } +} + +extension SQLiteHistory: BrowserHistory { + public func removeSiteFromTopSites(_ site: Site) -> Success { + if let host = (site.url as String).asURL?.normalizedHost { + return self.removeHostFromTopSites(host) + } + return deferMaybe(DatabaseError(description: "Invalid url for site \(site.url)")) + } + + public func removeFromPinnedTopSites(_ site: Site) -> Success { + guard let host = (site.url as String).asURL?.normalizedHost else { + return deferMaybe(DatabaseError(description: "Invalid url for site \(site.url)")) + } + + //do a fuzzy delete so dupes can be removed + let query: (String, Args?) = ("DELETE FROM \(TablePinnedTopSites) where domain = ?", [host]) + return db.run([query]) >>== { + return self.db.run([("UPDATE \(TableDomains) set showOnTopSites = 1 WHERE domain = ?", [host])]) + } + } + + public func getPinnedTopSites() -> Deferred>> { + let sql = "SELECT * from \(TablePinnedTopSites) " + + "LEFT OUTER JOIN view_history_id_favicon on historyID = view_history_id_favicon.id ORDER BY pinDate DESC" + return db.runQuery(sql, args: [], factory: SQLiteHistory.iconHistoryMetadataColumnFactory) + } + + public func addPinnedTopSite(_ site: Site) -> Success { // needs test + let now = Date.now() + guard let guid = site.guid, let host = (site.url as String).asURL?.normalizedHost else { + return deferMaybe(DatabaseError(description: "Invalid site \(site.url)")) + } + + let args: Args = [site.url, now, site.title, site.id, guid, host] + let arglist = BrowserDB.varlist(args.count) + // Prevent the pinned site from being used in topsite calculations + // We dont have to worry about this when removing a pin because the assumption is that a user probably doesnt want it being recommended as a topsite either + return self.removeHostFromTopSites(host) >>== { + return self.db.run([("INSERT OR REPLACE INTO \(TablePinnedTopSites)(url, pinDate, title, historyID, guid, domain) VALUES \(arglist)", args)]) + } + } + + public func removeHostFromTopSites(_ host: String) -> Success { + return db.run([("UPDATE \(TableDomains) set showOnTopSites = 0 WHERE domain = ?", [host])]) + } + + public func removeHistoryForURL(_ url: String) -> Success { + let visitArgs: Args = [url] + let deleteVisits = "DELETE FROM \(TableVisits) WHERE siteID = (SELECT id FROM \(TableHistory) WHERE url = ?)" + + let markArgs: Args = [Date.nowNumber(), url] + let markDeleted = "UPDATE \(TableHistory) SET url = NULL, is_deleted = 1, should_upload = 1, local_modified = ? WHERE url = ?" + //return db.run([(sql: String, args: Args?)]) + let command = [(sql: deleteVisits, args: visitArgs), (sql: markDeleted, args: markArgs), self.favicons.getCleanupFaviconsQuery()] as [(sql: String, args: Args?)] + return db.run(command) + } + + // Note: clearing history isn't really a sane concept in the presence of Sync. + // This method should be split to do something else. + // Bug 1162778. + public func clearHistory() -> Success { + return self.db.run([ + ("DELETE FROM \(TableVisits)", nil), + ("DELETE FROM \(TableHistory)", nil), + ("DELETE FROM \(TableDomains)", nil), + ("DELETE FROM \(TablePageMetadata)", nil), + self.favicons.getCleanupFaviconsQuery() + ]) + // We've probably deleted a lot of stuff. Vacuum now to recover the space. + >>> effect(self.db.vacuum) + } + + func recordVisitedSite(_ site: Site) -> Success { + // Don't store visits to sites with about: protocols + if isIgnoredURL(site.url as String) { + return deferMaybe(IgnoredSiteError()) + } + + return db.withConnection { conn -> Void in + let now = Date.now() + + if self.updateSite(site, atTime: now, withConnection: conn) > 0 { + return + } + + // Insert instead. + if self.insertSite(site, atTime: now, withConnection: conn) > 0 { + return + } + + let err = DatabaseError(description: "Unable to update or insert site; Invalid key returned") + log.error("recordVisitedSite(_:) encountered an error: \(err.localizedDescription)") + throw err + } + } + + func updateSite(_ site: Site, atTime time: Timestamp, withConnection conn: SQLiteDBConnection) -> Int { + // We know we're adding a new visit, so we'll need to upload this record. + // If we ever switch to per-visit change flags, this should turn into a CASE statement like + // CASE WHEN title IS ? THEN max(should_upload, 1) ELSE should_upload END + // so that we don't flag this as changed unless the title changed. + // + // Note that we will never match against a deleted item, because deleted items have no URL, + // so we don't need to unset is_deleted here. + guard let host = (site.url as String).asURL?.normalizedHost else { + return 0 + } + + let update = "UPDATE \(TableHistory) SET title = ?, local_modified = ?, should_upload = 1, domain_id = (SELECT id FROM \(TableDomains) where domain = ?) WHERE url = ?" + let updateArgs: Args? = [site.title, time, host, site.url] + if Logger.logPII { + log.debug("Setting title to \(site.title) for URL \(site.url)") + } + do { + try conn.executeChange(update, withArgs: updateArgs) + return conn.numberOfRowsModified + } catch let error as NSError { + log.warning("Update failed with error: \(error.localizedDescription)") + return 0 + } + } + + fileprivate func insertSite(_ site: Site, atTime time: Timestamp, withConnection conn: SQLiteDBConnection) -> Int { + if let host = (site.url as String).asURL?.normalizedHost { + do { + try conn.executeChange("INSERT OR IGNORE INTO \(TableDomains) (domain) VALUES (?)", withArgs: [host]) + } catch let error as NSError { + log.warning("Domain insertion failed with \(error.localizedDescription)") + return 0 + } + + let insert = "INSERT INTO \(TableHistory) " + + "(guid, url, title, local_modified, is_deleted, should_upload, domain_id) " + + "SELECT ?, ?, ?, ?, 0, 1, id FROM \(TableDomains) WHERE domain = ?" + let insertArgs: Args? = [site.guid ?? Bytes.generateGUID(), site.url, site.title, time, host] + do { + try conn.executeChange(insert, withArgs: insertArgs) + } catch let error as NSError { + log.warning("Site insertion failed with \(error.localizedDescription)") + return 0 + } + + return 1 + } + + if Logger.logPII { + log.warning("Invalid URL \(site.url). Not stored in history.") + } + return 0 + } + + // TODO: thread siteID into this to avoid the need to do the lookup. + func addLocalVisitForExistingSite(_ visit: SiteVisit) -> Success { + return db.withConnection { conn -> Void in + // INSERT OR IGNORE because we *might* have a clock error that causes a timestamp + // collision with an existing visit, and it would really suck to error out for that reason. + let insert = "INSERT OR IGNORE INTO \(TableVisits) (siteID, date, type, is_local) VALUES (" + + "(SELECT id FROM \(TableHistory) WHERE url = ?), ?, ?, 1)" + let realDate = visit.date + let insertArgs: Args? = [visit.site.url, realDate, visit.type.rawValue] + + try conn.executeChange(insert, withArgs: insertArgs) + } + } + + public func addLocalVisit(_ visit: SiteVisit) -> Success { + return recordVisitedSite(visit.site) + >>> { self.addLocalVisitForExistingSite(visit) } + } + + public func getFrecentHistory() -> FrecentHistory { + return SQLiteFrecentHistory(db: db, prefs: prefs) + } + + public func getTopSitesWithLimit(_ limit: Int) -> Deferred>> { + return self.db.runQuery(topSitesQuery, args: [limit], factory: SQLiteHistory.iconHistoryMetadataColumnFactory) + } + + public func setTopSitesNeedsInvalidation() { + prefs.setBool(false, forKey: PrefsKeys.KeyTopSitesCacheIsValid) + } + + public func setTopSitesCacheSize(_ size: Int32) { + let oldValue = prefs.intForKey(PrefsKeys.KeyTopSitesCacheSize) ?? 0 + if oldValue != size { + prefs.setInt(size, forKey: PrefsKeys.KeyTopSitesCacheSize) + setTopSitesNeedsInvalidation() + } + } + + public func refreshTopSitesQuery() -> [(String, Args?)] { + return [clearTopSitesQuery, getFrecentHistory().updateTopSitesCacheQuery()] + } + + public func clearTopSitesCache() -> Success { + return self.db.run([clearTopSitesQuery]) >>> { + self.prefs.removeObjectForKey(PrefsKeys.KeyTopSitesCacheIsValid) + return succeed() + } + } + + public func getSitesByLastVisit(_ limit: Int) -> Deferred>> { + return self.getFilteredSitesByVisitDateWithLimit(limit, whereURLContains: nil, includeIcon: true) + } + + fileprivate func topSiteClauses() -> (String, String) { + let whereData = "(\(TableDomains).showOnTopSites IS 1) AND (\(TableDomains).domain NOT LIKE 'r.%') AND (\(TableDomains).domain NOT LIKE 'google.%') " + let groupBy = "GROUP BY domain_id " + return (whereData, groupBy) + } + + fileprivate func computeWordsWithFilter(_ filter: String) -> [String] { + // Split filter on whitespace. + let words = filter.components(separatedBy: CharacterSet.whitespaces) + + // Remove substrings and duplicates. + // TODO: this can probably be improved. + return words.enumerated().filter({ (index: Int, word: String) in + if word.isEmpty { + return false + } + + for i in words.indices where i != index { + if words[i].range(of: word) != nil && (words[i].characters.count != word.characters.count || i < index) { + return false + } + } + + return true + }).map({ $0.1 }) + } + + /** + * Take input like "foo bar" and a template fragment and produce output like + * + * ((x.y LIKE ?) OR (x.z LIKE ?)) AND ((x.y LIKE ?) OR (x.z LIKE ?)) + * + * with args ["foo", "foo", "bar", "bar"]. + */ + internal func computeWhereFragmentWithFilter(_ filter: String, perWordFragment: String, perWordArgs: (String) -> Args) -> (fragment: String, args: Args) { + precondition(!filter.isEmpty) + + let words = computeWordsWithFilter(filter) + return self.computeWhereFragmentForWords(words, perWordFragment: perWordFragment, perWordArgs: perWordArgs) + } + + internal func computeWhereFragmentForWords(_ words: [String], perWordFragment: String, perWordArgs: (String) -> Args) -> (fragment: String, args: Args) { + assert(!words.isEmpty) + + let fragment = Array(repeating: perWordFragment, count: words.count).joined(separator: " AND ") + let args = words.flatMap(perWordArgs) + return (fragment, args) + } + + fileprivate func getFilteredSitesByVisitDateWithLimit(_ limit: Int, + whereURLContains filter: String? = nil, + includeIcon: Bool = true) -> Deferred>> { + let args: Args? + let whereClause: String + if let filter = filter?.trimmingCharacters(in: CharacterSet.whitespaces), !filter.isEmpty { + let perWordFragment = "((\(TableHistory).url LIKE ?) OR (\(TableHistory).title LIKE ?))" + let perWordArgs: (String) -> Args = { ["%\($0)%", "%\($0)%"] } + let (filterFragment, filterArgs) = computeWhereFragmentWithFilter(filter, perWordFragment: perWordFragment, perWordArgs: perWordArgs) + + // No deleted item has a URL, so there is no need to explicitly add that here. + whereClause = "WHERE (\(filterFragment))" + args = filterArgs + } else { + whereClause = "WHERE (\(TableHistory).is_deleted = 0)" + args = [] + } + + let sql = [ + "SELECT", + "history.id AS historyID, history.url, title, guid, domain_id, domain,", + "COALESCE(MAX(CASE visits.is_local WHEN 1 THEN visits.date ELSE 0 END), 0) AS localVisitDate,", + "COALESCE(MAX(CASE visits.is_local WHEN 0 THEN visits.date ELSE 0 END), 0) AS remoteVisitDate,", + "COALESCE(COUNT(visits.is_local), 0) AS visitCount", + includeIcon ? ", iconID, iconURL, iconDate, iconType, iconWidth" : "", + "FROM", + "history", + "INNER JOIN domains ON domains.id = history.domain_id", + "INNER JOIN visits ON visits.siteID = history.id", + includeIcon ? "LEFT OUTER JOIN view_history_id_favicon ON view_history_id_favicon.id = history.id" : "", + whereClause, + "GROUP BY historyID", + "HAVING COUNT(visits.is_local) > 0", + "ORDER BY MAX(localVisitDate, remoteVisitDate) DESC", + "LIMIT \(limit)", + ].joined(separator: " ") + + let factory = includeIcon ? SQLiteHistory.iconHistoryColumnFactory : SQLiteHistory.basicHistoryColumnFactory + return db.runQuery(sql, args: args, factory: factory) + } +} + +extension SQLiteHistory: Favicons { + // These two getter functions are only exposed for testing purposes (and aren't part of the public interface). + func getFaviconsForURL(_ url: String) -> Deferred>> { + let sql = "SELECT iconID AS id, iconURL AS url, iconDate AS date, iconType AS type, iconWidth AS width FROM " + + "\(ViewWidestFaviconsForSites), \(TableHistory) WHERE " + + "\(TableHistory).id = siteID AND \(TableHistory).url = ?" + let args: Args = [url] + return db.runQuery(sql, args: args, factory: SQLiteHistory.iconColumnFactory) + } + + func getFaviconsForBookmarkedURL(_ url: String) -> Deferred>> { + let sql = + "SELECT " + + " \(TableFavicons).id AS id" + + ", \(TableFavicons).url AS url" + + ", \(TableFavicons).date AS date" + + ", \(TableFavicons).type AS type" + + ", \(TableFavicons).width AS width" + + " FROM \(TableFavicons), \(ViewBookmarksLocalOnMirror) AS bm" + + " WHERE bm.faviconID = \(TableFavicons).id AND bm.bmkUri IS ?" + let args: Args = [url] + return db.runQuery(sql, args: args, factory: SQLiteHistory.iconColumnFactory) + } + + public func getSitesForURLs(_ urls: [String]) -> Deferred>> { + let inExpression = urls.joined(separator: "\",\"") + let sql = "SELECT \(TableHistory).id AS historyID, \(TableHistory).url AS url, title, guid, iconID, iconURL, iconDate, iconType, iconWidth FROM " + + "\(ViewWidestFaviconsForSites), \(TableHistory) WHERE " + + "\(TableHistory).id = siteID AND \(TableHistory).url IN (\"\(inExpression)\")" + let args: Args = [] + return db.runQuery(sql, args: args, factory: SQLiteHistory.iconHistoryColumnFactory) + } + + public func clearAllFavicons() -> Success { + return db.transaction { conn -> Void in + try conn.executeChange("DELETE FROM \(TableFaviconSites)") + try conn.executeChange("DELETE FROM \(TableFavicons)") + } + } + + public func addFavicon(_ icon: Favicon) -> Deferred> { + return self.favicons.insertOrUpdateFavicon(icon) + } + + /** + * This method assumes that the site has already been recorded + * in the history table. + */ + public func addFavicon(_ icon: Favicon, forSite site: Site) -> Deferred> { + if Logger.logPII { + log.verbose("Adding favicon \(icon.url) for site \(site.url).") + } + func doChange(_ query: String, args: Args?) -> Deferred> { + return db.withConnection { conn -> Int in + // Blind! We don't see failure here. + let id = self.favicons.insertOrUpdateFaviconInTransaction(icon, conn: conn) + + // Now set up the mapping. + try conn.executeChange(query, withArgs: args) + + // Try to update the favicon ID column in each bookmarks table. There can be + // multiple bookmarks with a particular URI, and a mirror bookmark can be + // locally changed, so either or both of these statements can update multiple rows. + if let id = id { + icon.id = id + + try? conn.executeChange("UPDATE \(TableBookmarksLocal) SET faviconID = ? WHERE bmkUri = ?", withArgs: [id, site.url]) + try? conn.executeChange("UPDATE \(TableBookmarksMirror) SET faviconID = ? WHERE bmkUri = ?", withArgs: [id, site.url]) + + return id + } + + let err = DatabaseError(description: "Error adding favicon. ID = 0") + log.error("addFavicon(_:, forSite:) encountered an error: \(err.localizedDescription)") + throw err + } + } + + let siteSubselect = "(SELECT id FROM \(TableHistory) WHERE url = ?)" + let iconSubselect = "(SELECT id FROM \(TableFavicons) WHERE url = ?)" + let insertOrIgnore = "INSERT OR IGNORE INTO \(TableFaviconSites)(siteID, faviconID) VALUES " + if let iconID = icon.id { + // Easy! + if let siteID = site.id { + // So easy! + let args: Args? = [siteID, iconID] + return doChange("\(insertOrIgnore) (?, ?)", args: args) + } + + // Nearly easy. + let args: Args? = [site.url, iconID] + return doChange("\(insertOrIgnore) (\(siteSubselect), ?)", args: args) + + } + + // Sigh. + if let siteID = site.id { + let args: Args? = [siteID, icon.url] + return doChange("\(insertOrIgnore) (?, \(iconSubselect))", args: args) + } + + // The worst. + let args: Args? = [site.url, icon.url] + return doChange("\(insertOrIgnore) (\(siteSubselect), \(iconSubselect))", args: args) + } +} + +extension SQLiteHistory: SyncableHistory { + /** + * TODO: + * When we replace an existing row, we want to create a deleted row with the old + * GUID and switch the new one in -- if the old record has escaped to a Sync server, + * we want to delete it so that we don't have two records with the same URL on the server. + * We will know if it's been uploaded because it'll have a server_modified time. + */ + public func ensurePlaceWithURL(_ url: String, hasGUID guid: GUID) -> Success { + let args: Args = [guid, url, guid] + + // The additional IS NOT is to ensure that we don't do a write for no reason. + return db.run("UPDATE \(TableHistory) SET guid = ? WHERE url = ? AND guid IS NOT ?", withArgs: args) + } + + public func deleteByGUID(_ guid: GUID, deletedAt: Timestamp) -> Success { + let args: Args = [guid] + // This relies on ON DELETE CASCADE to remove visits. + return db.run("DELETE FROM \(TableHistory) WHERE guid = ?", withArgs: args) + } + + // Fails on non-existence. + fileprivate func getSiteIDForGUID(_ guid: GUID) -> Deferred> { + let args: Args = [guid] + let query = "SELECT id FROM history WHERE guid = ?" + let factory: (SDRow) -> Int = { return $0["id"] as! Int } + + return db.runQuery(query, args: args, factory: factory) + >>== { cursor in + if cursor.count == 0 { + return deferMaybe(NoSuchRecordError(guid: guid)) + } + return deferMaybe(cursor[0]!) + } + } + + public func storeRemoteVisits(_ visits: [Visit], forGUID guid: GUID) -> Success { + return self.getSiteIDForGUID(guid) + >>== { (siteID: Int) -> Success in + let visitArgs = visits.map { (visit: Visit) -> Args in + let realDate = visit.date + let isLocal = 0 + let args: Args = [siteID, realDate, visit.type.rawValue, isLocal] + return args + } + + // Magic happens here. The INSERT OR IGNORE relies on the multi-column uniqueness + // constraint on `visits`: we allow only one row for (siteID, date, type), so if a + // local visit already exists, this silently keeps it. End result? Any new remote + // visits are added with only one query, keeping any existing rows. + return self.db.bulkInsert(TableVisits, op: .InsertOrIgnore, columns: ["siteID", "date", "type", "is_local"], values: visitArgs) + } + } + + fileprivate struct HistoryMetadata { + let id: Int + let serverModified: Timestamp? + let localModified: Timestamp? + let isDeleted: Bool + let shouldUpload: Bool + let title: String + } + + fileprivate func metadataForGUID(_ guid: GUID) -> Deferred> { + let select = "SELECT id, server_modified, local_modified, is_deleted, should_upload, title FROM \(TableHistory) WHERE guid = ?" + let args: Args = [guid] + let factory = { (row: SDRow) -> HistoryMetadata in + return HistoryMetadata( + id: row["id"] as! Int, + serverModified: row.getTimestamp("server_modified"), + localModified: row.getTimestamp("local_modified"), + isDeleted: row.getBoolean("is_deleted"), + shouldUpload: row.getBoolean("should_upload"), + title: row["title"] as! String + ) + } + return db.runQuery(select, args: args, factory: factory) >>== { cursor in + return deferMaybe(cursor[0]) + } + } + + public func insertOrUpdatePlace(_ place: Place, modified: Timestamp) -> Deferred> { + // One of these things will be true here. + // 0. The item is new. + // (a) We have a local place with the same URL but a different GUID. + // (b) We have never visited this place locally. + // In either case, reconcile and proceed. + // 1. The remote place is not modified when compared to our mirror of it. This + // can occur when we redownload after a partial failure. + // (a) And it's not modified locally, either. Nothing to do. Ideally we + // will short-circuit so we don't need to update visits. (TODO) + // (b) It's modified locally. Don't overwrite anything; let the upload happen. + // 2. The remote place is modified (either title or visits). + // (a) And it's not locally modified. Update the local entry. + // (b) And it's locally modified. Preserve the title of whichever was modified last. + // N.B., this is the only instance where we compare two timestamps to see + // which one wins. + + // We use this throughout. + let serverModified = modified + + // Check to see if our modified time is unchanged, if the record exists locally, etc. + let insertWithMetadata = { (metadata: HistoryMetadata?) -> Deferred> in + if let metadata = metadata { + // The item exists locally (perhaps originally with a different GUID). + if metadata.serverModified == modified { + log.verbose("History item \(place.guid) is unchanged; skipping insert-or-update.") + return deferMaybe(place.guid) + } + + // Otherwise, the server record must have changed since we last saw it. + if metadata.shouldUpload { + // Uh oh, it changed locally. + // This might well just be a visit change, but we can't tell. Usually this conflict is harmless. + log.debug("Warning: history item \(place.guid) changed both locally and remotely. Comparing timestamps from different clocks!") + if let localModified = metadata.localModified, localModified > modified { + log.debug("Local changes overriding remote.") + + // Update server modified time only. (Though it'll be overwritten again after a successful upload.) + let update = "UPDATE \(TableHistory) SET server_modified = ? WHERE id = ?" + let args: Args = [serverModified, metadata.id] + return self.db.run(update, withArgs: args) >>> always(place.guid) + } + + log.verbose("Remote changes overriding local.") + // Fall through. + } + + // The record didn't change locally. Update it. + log.verbose("Updating local history item for guid \(place.guid).") + let update = "UPDATE \(TableHistory) SET title = ?, server_modified = ?, is_deleted = 0 WHERE id = ?" + let args: Args = [place.title, serverModified, metadata.id] + return self.db.run(update, withArgs: args) >>> always(place.guid) + } + + // The record doesn't exist locally. Insert it. + log.verbose("Inserting remote history item for guid \(place.guid).") + if let host = place.url.asURL?.normalizedHost { + if Logger.logPII { + log.debug("Inserting: \(place.url).") + } + + let insertDomain = "INSERT OR IGNORE INTO \(TableDomains) (domain) VALUES (?)" + let insertHistory = "INSERT INTO \(TableHistory) (guid, url, title, server_modified, is_deleted, should_upload, domain_id) " + + "SELECT ?, ?, ?, ?, 0, 0, id FROM \(TableDomains) where domain = ?" + return self.db.run([ + (insertDomain, [host]), + (insertHistory, [place.guid, place.url, place.title, serverModified, host]) + ]) >>> always(place.guid) + } else { + // This is a URL with no domain. Insert it directly. + if Logger.logPII { + log.debug("Inserting: \(place.url) with no domain.") + } + + let insertHistory = "INSERT INTO \(TableHistory) (guid, url, title, server_modified, is_deleted, should_upload, domain_id) " + + "VALUES (?, ?, ?, ?, 0, 0, NULL)" + return self.db.run([ + (insertHistory, [place.guid, place.url, place.title, serverModified]) + ]) >>> always(place.guid) + } + } + + // Make sure that we only need to compare GUIDs by pre-merging on URL. + return self.ensurePlaceWithURL(place.url, hasGUID: place.guid) + >>> { self.metadataForGUID(place.guid) >>== insertWithMetadata } + } + + public func getDeletedHistoryToUpload() -> Deferred> { + // Use the partial index on should_upload to make this nice and quick. + let sql = "SELECT guid FROM \(TableHistory) WHERE \(TableHistory).should_upload = 1 AND \(TableHistory).is_deleted = 1" + let f: (SDRow) -> String = { $0["guid"] as! String } + + return self.db.runQuery(sql, args: nil, factory: f) >>== { deferMaybe($0.asArray()) } + } + + public func getModifiedHistoryToUpload() -> Deferred> { + // What we want to do: find all history items that are flagged for upload, then find a number of recent visits for each item. + // This was originally all in a single SQL query but was seperated into two to save some memory when returning back the cursor. + return getModifiedHistory(limit: 1000) >>== { self.attachVisitsTo(places: $0, visitLimit: 20) } + } + + private func getModifiedHistory(limit: Int) -> Deferred> { + let sql = + "SELECT id, guid, url, title " + + "FROM \(TableHistory) " + + "WHERE should_upload = 1 AND NOT is_deleted = 1 " + + "ORDER BY id " + + "LIMIT ?" + + var places = [Int: Place]() + let placeFactory: (SDRow) -> Void = { row in + let id = row["id"] as! Int + let guid = row["guid"] as! String + let url = row["url"] as! String + let title = row["title"] as! String + places[id] = Place(guid: guid, url: url, title: title) + } + + let args: Args = [limit] + return db.runQuery(sql, args: args, factory: placeFactory) >>> { deferMaybe(places) } + } + + private func attachVisitsTo(places: [Int: Place], visitLimit: Int) -> Deferred> { + // A difficulty here: we don't want to fetch *all* visits, only some number of the most recent. + // (It's not enough to only get new ones, because the server record should contain more.) + // + // That's the greatest-N-per-group problem. We used to do this in SQL, joining + // the visits table to a subselect of visits table, however, ran into OOM issues (Bug 1417034) + // + // Now, we want a more dumb approach with no joins in SQL and doing group-by-site and limit-to-N in swift. + // + // We do this in a single query, rather than the N+1 that desktop takes. + // + // We then need to flatten the cursor. We do that by collecting + // places as a side-effect of the factory, producing visits as a result, and merging in memory. + + // Turn our lazy collection of integers into a comma-seperated string for the IN clause. + let historyIDs = Array(places.keys) + + let sql = + "SELECT siteID, date AS visitDate, type AS visitType " + + "FROM \(TableVisits) " + + "WHERE siteID IN (\(historyIDs.map(String.init).joined(separator: ","))) " + + "ORDER BY siteID DESC, date DESC" + + // We want to get a tuple Visit and Place here. We can either have an explicit tuple factory + // or we use an identity function, and make do without creating extra data structures. + // Since we have a known Out Of Memory issue here, let's avoid extra data structures. + let rowIdentity: (SDRow) -> SDRow = { $0 } + + // We'll need to runQueryUnsafe so we get a LiveSQLiteCursor, i.e. we don't get the cursor + // contents into memory all at once. + return db.runQueryUnsafe(sql, args: nil, factory: rowIdentity) { (cursor: Cursor) -> [Int: [Visit]] in + // Accumulate a mapping of site IDs to list of visits. Each list should be shorter than visitLimit. + // Seed our accumulator with empty lists since we already know which IDs we will be fetching. + var visits = [Int: [Visit]]() + historyIDs.forEach { visits[$0] = [] } + + // We need to iterate through these explicitly, without relying on the + // factory. + for row in cursor.makeIterator() { + guard let row = row, cursor.status == .success else { + throw NSError(domain: "mozilla", code: 0, userInfo: [NSLocalizedDescriptionKey: cursor.statusMessage]) + } + + guard let id = row["siteID"] as? Int, + let existingCount = visits[id]?.count, + existingCount < visitLimit else { + continue + } + + guard let date = row.getTimestamp("visitDate"), + let visitType = row["visitType"] as? Int, + let type = VisitType(rawValue: visitType) else { + continue + } + + let visit = Visit(date: date, type: type) + + // Append the visits in descending date order, so we only get the + // most recent top N. + visits[id]?.append(visit) + } + + return visits + } >>== { visits in + // Join up the places map we received as input with our visits map. + let placesAndVisits: [(Place, [Visit])] = places.flatMap { id, place in + guard let visitsList = visits[id], !visitsList.isEmpty else { + return nil + } + return (place, visitsList) + } + + let recentVisitCount = placesAndVisits.reduce(0) { $0 + $1.1.count } + + log.info("Attaching \(placesAndVisits.count) places to \(recentVisitCount) most recent visits") + return deferMaybe(placesAndVisits) + } + } + + public func markAsDeleted(_ guids: [GUID]) -> Success { + if guids.isEmpty { + return succeed() + } + + log.debug("Wiping \(guids.count) deleted GUIDs.") + return self.db.run(chunk(guids, by: BrowserDB.MaxVariableNumber).flatMap(markAsDeletedStatementForGUIDs)) + } + + fileprivate func markAsDeletedStatementForGUIDs(_ guids: ArraySlice) -> (String, Args?) { + // We deliberately don't limit this to records marked as should_upload, just + // in case a coding error leaves records with is_deleted=1 but not flagged for + // upload -- this will catch those and throw them away. + let inClause = BrowserDB.varlist(guids.count) + let sql = "DELETE FROM \(TableHistory) WHERE is_deleted = 1 AND guid IN \(inClause)" + + let args: Args = guids.map { $0 } + return (sql, args) + } + + public func markAsSynchronized(_ guids: [GUID], modified: Timestamp) -> Deferred> { + if guids.isEmpty { + return deferMaybe(modified) + } + + log.debug("Marking \(guids.count) GUIDs as synchronized. Returning timestamp \(modified).") + return self.db.run(chunk(guids, by: BrowserDB.MaxVariableNumber).flatMap { chunk in + return markAsSynchronizedStatementForGUIDs(chunk, modified: modified) + }) >>> always(modified) + } + + fileprivate func markAsSynchronizedStatementForGUIDs(_ guids: ArraySlice, modified: Timestamp) -> (String, Args?) { + let inClause = BrowserDB.varlist(guids.count) + let sql = + "UPDATE \(TableHistory) SET " + + "should_upload = 0, server_modified = \(modified) " + + "WHERE guid IN \(inClause)" + + let args: Args = guids.map { $0 } + return (sql, args) + } + + public func doneApplyingRecordsAfterDownload() -> Success { + self.db.checkpoint() + return succeed() + } + + public func doneUpdatingMetadataAfterUpload() -> Success { + self.db.checkpoint() + return succeed() + } +} + +extension SQLiteHistory { + // Returns a deferred `true` if there are rows in the DB that have a server_modified time. + // Because we clear this when we reset or remove the account, and never set server_modified + // without syncing, the presence of matching rows directly indicates that a deletion + // would be synced to the server. + public func hasSyncedHistory() -> Deferred> { + return self.db.queryReturnsResults("SELECT 1 FROM \(TableHistory) WHERE server_modified IS NOT NULL LIMIT 1") + } +} + +extension SQLiteHistory: ResettableSyncStorage { + // We don't drop deletions when we reset -- we might need to upload a deleted item + // that never made it to the server. + public func resetClient() -> Success { + let flag = "UPDATE \(TableHistory) SET should_upload = 1, server_modified = NULL" + return self.db.run(flag) + } +} + +extension SQLiteHistory: AccountRemovalDelegate { + public func onRemovedAccount() -> Success { + log.info("Clearing history metadata and deleted items after account removal.") + let discard = "DELETE FROM \(TableHistory) WHERE is_deleted = 1" + return self.db.run(discard) >>> self.resetClient + } +} diff --git a/mobile/ios/Storage/SQL/SQLiteHistoryFactories.swift b/mobile/ios/Storage/SQL/SQLiteHistoryFactories.swift new file mode 100644 index 0000000000..805778fdd5 --- /dev/null +++ b/mobile/ios/Storage/SQL/SQLiteHistoryFactories.swift @@ -0,0 +1,75 @@ +/* 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 + +/* + * Factory methods for converting rows from SQLite into model objects + */ +extension SQLiteHistory { + class func basicHistoryColumnFactory(_ row: SDRow) -> Site { + let id = row["historyID"] as? Int + let url = row["url"] as! String + let title = row["title"] as! String + let guid = row["guid"] as! String + + // Extract a boolean from the row if it's present. + let iB = row["is_bookmarked"] as? Int + let isBookmarked: Bool? = (iB == nil) ? nil : (iB! != 0) + + let site = Site(url: url, title: title, bookmarked: isBookmarked) + site.guid = guid + site.id = id + + // Find the most recent visit, regardless of which column it might be in. + let local = row.getTimestamp("localVisitDate") ?? 0 + let remote = row.getTimestamp("remoteVisitDate") ?? 0 + let either = row.getTimestamp("visitDate") ?? 0 + + let latest = max(local, remote, either) + if latest > 0 { + site.latestVisit = Visit(date: latest, type: VisitType.unknown) + } + + return site + } + + class func iconColumnFactory(_ row: SDRow) -> Favicon? { + if let iconType = row["iconType"] as? Int, + let iconURL = row["iconURL"] as? String, + let iconDate = row["iconDate"] as? Double, + let _ = row["iconID"] as? Int { + let date = Date(timeIntervalSince1970: iconDate) + return Favicon(url: iconURL, date: date, type: IconType(rawValue: iconType)!) + } + return nil + } + + class func pageMetadataColumnFactory(_ row: SDRow) -> PageMetadata? { + guard let siteURL = row["url"] as? String else { + return nil + } + + return PageMetadata(id: row["metadata_id"] as? Int, siteURL: siteURL, mediaURL: row["media_url"] as? String, title: row["metadata_title"] as? String, description: row["description"] as? String, type: row["type"] as? String, providerName: row["provider_name"] as? String, mediaDataURI: nil) + } + + class func iconHistoryColumnFactory(_ row: SDRow) -> Site { + let site = basicHistoryColumnFactory(row) + site.icon = iconColumnFactory(row) + return site + } + + class func iconHistoryMetadataColumnFactory(_ row: SDRow) -> Site { + let site = iconHistoryColumnFactory(row) + site.metadata = pageMetadataColumnFactory(row) + return site + } + + class func basicHistoryMetadataColumnFactory(_ row: SDRow) -> Site { + let site = basicHistoryColumnFactory(row) + site.metadata = pageMetadataColumnFactory(row) + return site + } +} diff --git a/mobile/ios/Storage/SQL/SQLiteHistoryRecommendations.swift b/mobile/ios/Storage/SQL/SQLiteHistoryRecommendations.swift new file mode 100644 index 0000000000..1ecedc34df --- /dev/null +++ b/mobile/ios/Storage/SQL/SQLiteHistoryRecommendations.swift @@ -0,0 +1,179 @@ +/* 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 XCGLogger +import Deferred + +fileprivate let log = Logger.syncLogger + +extension SQLiteHistory: HistoryRecommendations { + // Bookmarks Query + static let removeMultipleDomainsSubquery = + " INNER JOIN (SELECT \(ViewHistoryVisits).domain_id AS domain_id" + + " FROM \(ViewHistoryVisits)" + + " GROUP BY \(ViewHistoryVisits).domain_id) AS domains ON domains.domain_id = \(TableHistory).domain_id" + + static let urisForSimpleSyncedBookmarks = + "SELECT bmkUri FROM \(TableBookmarksBuffer) WHERE server_modified > ? AND is_deleted = 0 " + + "UNION ALL " + + "SELECT bmkUri FROM \(TableBookmarksLocal) WHERE local_modified > ? AND is_deleted = 0" + + static let urisForLocalBookmarks = + "SELECT bmkUri" + + "FROM \(ViewBookmarksLocalOnMirror) " + + "WHERE \(ViewBookmarksLocalOnMirror).server_modified > ? OR " + + "\(ViewBookmarksLocalOnMirror).local_modified > ?" + + static let bookmarkHighlights = + "SELECT historyID, url, siteTitle, guid, is_bookmarked FROM (" + + " SELECT \(TableHistory).id AS historyID, \(TableHistory).url AS url, \(TableHistory).title AS siteTitle, guid, \(TableHistory).domain_id, NULL AS visitDate, 1 AS is_bookmarked" + + " FROM (" + + (AppConstants.MOZ_SIMPLE_BOOKMARKS_SYNCING ? urisForSimpleSyncedBookmarks : urisForLocalBookmarks) + + " )" + + " LEFT JOIN \(TableHistory) ON \(TableHistory).url = bmkUri" + removeMultipleDomainsSubquery + + " WHERE \(TableHistory).title NOT NULL and \(TableHistory).title != '' AND url NOT IN" + + " (SELECT \(TableActivityStreamBlocklist).url FROM \(TableActivityStreamBlocklist))" + + " LIMIT ?" + + ")" + + static let bookmarksQuery = + "SELECT historyID, url, siteTitle AS title, guid, is_bookmarked, iconID, iconURL, iconType, iconDate, iconWidth, \(TablePageMetadata).title AS metadata_title, media_url, type, description, provider_name " + + "FROM (\(bookmarkHighlights) ) " + + "LEFT JOIN \(ViewHistoryIDsWithWidestFavicons) ON \(ViewHistoryIDsWithWidestFavicons).id = historyID " + + "LEFT OUTER JOIN \(TablePageMetadata) ON \(TablePageMetadata).cache_key = url " + + "GROUP BY url" + + // Highlights Query + static let highlightsLimit = 8 + static let blacklistedHosts: Args = [ + "google.com", + "google.ca", + "calendar.google.com", + "mail.google.com", + "mail.yahoo.com", + "search.yahoo.com", + "localhost", + "t.co" + ] + + static let blacklistSubquery = "SELECT \(TableDomains).id FROM \(TableDomains) WHERE \(TableDomains).domain IN " + BrowserDB.varlist(blacklistedHosts.count) + static let removeMultipleDomainsSubqueryFromHighlights = + " INNER JOIN (SELECT \(ViewHistoryVisits).domain_id AS domain_id, MAX(\(ViewHistoryVisits).visitDate) AS visit_date" + + " FROM \(ViewHistoryVisits)" + + " GROUP BY \(ViewHistoryVisits).domain_id) AS domains ON domains.domain_id = \(TableHistory).domain_id AND visitDate = domains.visit_date" + + static let nonRecentHistory = + "SELECT historyID, url, siteTitle, guid, visitCount, visitDate, is_bookmarked, visitCount * icon_url_score * media_url_score AS score FROM (" + + " SELECT \(TableHistory).id as historyID, url, \(TableHistory).title AS siteTitle, guid, visitDate, \(TableHistory).domain_id," + + " (SELECT COUNT(1) FROM \(TableVisits) WHERE s = \(TableVisits).siteID) AS visitCount," + + " (SELECT COUNT(1) FROM \(ViewBookmarksLocalOnMirror) WHERE \(ViewBookmarksLocalOnMirror).bmkUri == url) AS is_bookmarked," + + " CASE WHEN iconURL IS NULL THEN 1 ELSE 2 END AS icon_url_score," + + " CASE WHEN media_url IS NULL THEN 1 ELSE 4 END AS media_url_score" + + " FROM (" + + " SELECT siteID AS s, MAX(date) AS visitDate" + + " FROM \(TableVisits)" + + " WHERE date < ?" + + " GROUP BY siteID" + + " ORDER BY visitDate DESC" + + " )" + + " LEFT JOIN \(TableHistory) ON \(TableHistory).id = s" + + removeMultipleDomainsSubqueryFromHighlights + + " LEFT OUTER JOIN \(ViewHistoryIDsWithWidestFavicons) ON" + + " \(ViewHistoryIDsWithWidestFavicons).id = \(TableHistory).id" + + " LEFT OUTER JOIN \(TablePageMetadata) ON" + + " \(TablePageMetadata).site_url = \(TableHistory).url" + + " WHERE visitCount <= 3 AND \(TableHistory).title NOT NULL AND \(TableHistory).title != '' AND is_bookmarked == 0 AND url NOT IN" + + " (SELECT url FROM \(TableActivityStreamBlocklist))" + + " AND \(TableHistory).domain_id NOT IN (" + + blacklistSubquery + ")" + + ")" + + public func getHighlights() -> Deferred>> { + let highlightsProjection = [ + "historyID", + "\(TableHighlights).cache_key AS cache_key", + "url", + "\(TableHighlights).title AS title", + "guid", + "visitCount", + "visitDate", + "is_bookmarked" + ] + let faviconsProjection = ["iconID", "iconURL", "iconType", "iconDate", "iconWidth"] + let metadataProjections = [ + "\(TablePageMetadata).title AS metadata_title", + "media_url", + "type", + "description", + "provider_name" + ] + + let allProjection = highlightsProjection + faviconsProjection + metadataProjections + + let highlightsHistoryIDs = + "SELECT historyID FROM \(TableHighlights)" + + // Search the history/favicon view with our limited set of highlight IDs + // to avoid doing a full table scan on history + let faviconSearch = + "SELECT * FROM \(ViewHistoryIDsWithWidestFavicons) WHERE id IN (\(highlightsHistoryIDs))" + + let sql = + "SELECT \(allProjection.joined(separator: ",")) " + + "FROM \(TableHighlights) " + + "LEFT JOIN (\(faviconSearch)) AS f1 ON f1.id = historyID " + + "LEFT OUTER JOIN \(TablePageMetadata) ON " + + "\(TablePageMetadata).cache_key = \(TableHighlights).cache_key" + + return self.db.runQuery(sql, args: nil, factory: SQLiteHistory.iconHistoryMetadataColumnFactory) + } + + public func removeHighlightForURL(_ url: String) -> Success { + return self.db.run([("INSERT INTO \(TableActivityStreamBlocklist) (url) VALUES (?)", [url])]) + } + + private func repopulateHighlightsQuery() -> [(String, Args?)] { + let (query, args) = computeHighlightsQuery() + let clearHighlightsQuery = "DELETE FROM \(TableHighlights)" + + let sql = "INSERT INTO \(TableHighlights) " + + "SELECT historyID, url as cache_key, url, title, guid, visitCount, visitDate, is_bookmarked " + + "FROM (\(query))" + return [(clearHighlightsQuery, nil), (sql, args)] + } + + public func repopulate(invalidateTopSites shouldInvalidateTopSites: Bool, invalidateHighlights shouldInvalidateHighlights: Bool) -> Success { + var queries: [(String, Args?)] = [] + if shouldInvalidateTopSites { + queries.append(contentsOf: self.refreshTopSitesQuery()) + } + if shouldInvalidateHighlights { + queries.append(contentsOf: self.repopulateHighlightsQuery()) + } + return self.db.run(queries) + } + + public func getRecentBookmarks(_ limit: Int = 3) -> Deferred>> { + let fiveDaysAgo: UInt64 = Date.now() - (OneDayInMilliseconds * 5) // The data is joined with a millisecond not a microsecond one. (History) + let args = [fiveDaysAgo, fiveDaysAgo, limit] as Args + return self.db.runQuery(SQLiteHistory.bookmarksQuery, args: args, factory: SQLiteHistory.iconHistoryMetadataColumnFactory) + } + + private func computeHighlightsQuery() -> (String, Args) { + let microsecondsPerMinute: UInt64 = 60_000_000 // 1000 * 1000 * 60 + let now = Date.nowMicroseconds() + let thirtyMinutesAgo: UInt64 = now - 30 * microsecondsPerMinute + + let highlightsQuery = + "SELECT historyID, url, siteTitle AS title, guid, visitCount, visitDate, is_bookmarked, score " + + "FROM ( \(SQLiteHistory.nonRecentHistory) ) " + + "GROUP BY url " + + "ORDER BY score DESC " + + "LIMIT \(SQLiteHistory.highlightsLimit)" + let args: Args = [thirtyMinutesAgo] + SQLiteHistory.blacklistedHosts + return (highlightsQuery, args) + } +} diff --git a/mobile/ios/Storage/SQL/SQLiteLogins.swift b/mobile/ios/Storage/SQL/SQLiteLogins.swift new file mode 100644 index 0000000000..9487691929 --- /dev/null +++ b/mobile/ios/Storage/SQL/SQLiteLogins.swift @@ -0,0 +1,894 @@ +/* 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 XCGLogger +import Deferred + +private let log = Logger.syncLogger + +open class SQLiteLogins: BrowserLogins { + + fileprivate let db: BrowserDB + fileprivate static let MainColumns: String = "guid, username, password, hostname, httpRealm, formSubmitURL, usernameField, passwordField" + fileprivate static let MainWithLastUsedColumns: String = MainColumns + ", timeLastUsed, timesUsed" + fileprivate static let LoginColumns: String = MainColumns + ", timeCreated, timeLastUsed, timePasswordChanged, timesUsed" + + public init(db: BrowserDB) { + self.db = db + } + + fileprivate class func populateLogin(_ login: Login, row: SDRow) { + login.formSubmitURL = row["formSubmitURL"] as? String + login.usernameField = row["usernameField"] as? String + login.passwordField = row["passwordField"] as? String + login.guid = row["guid"] as! String + + if let timeCreated = row.getTimestamp("timeCreated"), + let timeLastUsed = row.getTimestamp("timeLastUsed"), + let timePasswordChanged = row.getTimestamp("timePasswordChanged"), + let timesUsed = row["timesUsed"] as? Int { + login.timeCreated = timeCreated + login.timeLastUsed = timeLastUsed + login.timePasswordChanged = timePasswordChanged + login.timesUsed = timesUsed + } + } + + fileprivate class func constructLogin(_ row: SDRow, c: T.Type) -> T { + let credential = URLCredential(user: row["username"] as? String ?? "", + password: row["password"] as! String, + persistence: URLCredential.Persistence.none) + + // There was a bug in previous versions of the app where we saved only the hostname and not the + // scheme and port in the DB. To work with these scheme-less hostnames, we try to extract the scheme and + // hostname by converting to a URL first. If there is no valid hostname or scheme for the URL, + // fallback to returning the raw hostname value from the DB as the host and allow NSURLProtectionSpace + // to use the default (http) scheme. See https://bugzilla.mozilla.org/show_bug.cgi?id=1238103. + + let hostnameString = (row["hostname"] as? String) ?? "" + let hostnameURL = hostnameString.asURL + + let scheme = hostnameURL?.scheme + let port = hostnameURL?.port ?? 0 + + // Check for malformed hostname urls in the DB + let host: String + var malformedHostname = false + if let h = hostnameURL?.host { + host = h + } else { + host = hostnameString + malformedHostname = true + } + + let protectionSpace = URLProtectionSpace(host: host, + port: port, + protocol: scheme, + realm: row["httpRealm"] as? String, + authenticationMethod: nil) + + let login = T(credential: credential, protectionSpace: protectionSpace) + self.populateLogin(login, row: row) + login.hasMalformedHostname = malformedHostname + return login + } + + class func LocalLoginFactory(_ row: SDRow) -> LocalLogin { + let login = self.constructLogin(row, c: LocalLogin.self) + + login.localModified = row.getTimestamp("local_modified") ?? 0 + login.isDeleted = row.getBoolean("is_deleted") + login.syncStatus = SyncStatus(rawValue: row["sync_status"] as! Int)! + + return login + } + + class func MirrorLoginFactory(_ row: SDRow) -> MirrorLogin { + let login = self.constructLogin(row, c: MirrorLogin.self) + + login.serverModified = row.getTimestamp("server_modified")! + login.isOverridden = row.getBoolean("is_overridden") + + return login + } + + fileprivate class func LoginFactory(_ row: SDRow) -> Login { + return self.constructLogin(row, c: Login.self) + } + + fileprivate class func LoginDataFactory(_ row: SDRow) -> LoginData { + return LoginFactory(row) as LoginData + } + + fileprivate class func LoginUsageDataFactory(_ row: SDRow) -> LoginUsageData { + return LoginFactory(row) as LoginUsageData + } + + func notifyLoginDidChange() { + log.debug("Notifying login did change.") + + // For now we don't care about the contents. + // This posts immediately to the shared notification center. + NotificationCenter.default.post(name: NotificationDataLoginDidChange, object: nil) + } + + open func getUsageDataForLoginByGUID(_ guid: GUID) -> Deferred> { + let projection = SQLiteLogins.LoginColumns + let sql = + "SELECT \(projection) FROM " + + "\(TableLoginsLocal) WHERE is_deleted = 0 AND guid = ? " + + "UNION ALL " + + "SELECT \(projection) FROM " + + "\(TableLoginsMirror) WHERE is_overridden = 0 AND guid = ? " + + "LIMIT 1" + + let args: Args = [guid, guid] + return db.runQuery(sql, args: args, factory: SQLiteLogins.LoginUsageDataFactory) + >>== { value in + deferMaybe(value[0]!) + } + } + + open func getLoginDataForGUID(_ guid: GUID) -> Deferred> { + let projection = SQLiteLogins.LoginColumns + let sql = + "SELECT \(projection) FROM " + + "\(TableLoginsLocal) WHERE is_deleted = 0 AND guid = ? " + + "UNION ALL " + + "SELECT \(projection) FROM " + + "\(TableLoginsMirror) WHERE is_overriden IS NOT 1 AND guid = ? " + + "ORDER BY hostname ASC " + + "LIMIT 1" + + let args: Args = [guid, guid] + return db.runQuery(sql, args: args, factory: SQLiteLogins.LoginFactory) + >>== { value in + if let login = value[0] { + return deferMaybe(login) + } else { + return deferMaybe(LoginDataError(description: "Login not found for GUID \(guid)")) + } + } + } + + open func getLoginsForProtectionSpace(_ protectionSpace: URLProtectionSpace) -> Deferred>> { + let projection = SQLiteLogins.MainWithLastUsedColumns + + let sql = + "SELECT \(projection) FROM " + + "\(TableLoginsLocal) WHERE is_deleted = 0 AND hostname IS ? OR hostname IS ?" + + "UNION ALL " + + "SELECT \(projection) FROM " + + "\(TableLoginsMirror) WHERE is_overridden = 0 AND hostname IS ? OR hostname IS ?" + + "ORDER BY timeLastUsed DESC" + + // Since we store hostnames as the full scheme/protocol + host, combine the two to look up in our DB. + // In the case of https://bugzilla.mozilla.org/show_bug.cgi?id=1238103, there may be hostnames without + // a scheme. Check for these as well. + let args: Args = [ + protectionSpace.urlString(), + protectionSpace.host, + protectionSpace.urlString(), + protectionSpace.host, + ] + if Logger.logPII { + log.debug("Looking for login: \(protectionSpace.urlString()) && \(protectionSpace.host)") + } + return db.runQuery(sql, args: args, factory: SQLiteLogins.LoginDataFactory) + } + + // username is really Either; we explicitly match no username. + open func getLoginsForProtectionSpace(_ protectionSpace: URLProtectionSpace, withUsername username: String?) -> Deferred>> { + let projection = SQLiteLogins.MainWithLastUsedColumns + + let args: Args + let usernameMatch: String + if let username = username { + args = [ + protectionSpace.urlString(), username, protectionSpace.host, + protectionSpace.urlString(), username, protectionSpace.host + ] + usernameMatch = "username = ?" + } else { + args = [ + protectionSpace.urlString(), protectionSpace.host, + protectionSpace.urlString(), protectionSpace.host + ] + usernameMatch = "username IS NULL" + } + + if Logger.logPII { + log.debug("Looking for login with username: \(username ?? "nil"), first arg: \(args[0] ?? "nil")") + } + + let sql = + "SELECT \(projection) FROM " + + "\(TableLoginsLocal) WHERE is_deleted = 0 AND hostname IS ? AND \(usernameMatch) OR hostname IS ?" + + "UNION ALL " + + "SELECT \(projection) FROM " + + "\(TableLoginsMirror) WHERE is_overridden = 0 AND hostname IS ? AND \(usernameMatch) OR hostname IS ?" + + "ORDER BY timeLastUsed DESC" + + return db.runQuery(sql, args: args, factory: SQLiteLogins.LoginDataFactory) + } + + open func getAllLogins() -> Deferred>> { + return searchLoginsWithQuery(nil) + } + + open func searchLoginsWithQuery(_ query: String?) -> Deferred>> { + let projection = SQLiteLogins.LoginColumns + var searchClauses = [String]() + var args: Args? = nil + if let query = query, !query.isEmpty { + // Add wildcards to change query to 'contains in' and add them to args. We need 6 args because + // we include the where clause twice: Once for the local table and another for the remote. + args = (0..<6).map { _ in + return "%\(query)%" as String? + } + + searchClauses.append("username LIKE ? ") + searchClauses.append(" password LIKE ? ") + searchClauses.append(" hostname LIKE ?") + } + + let whereSearchClause = searchClauses.count > 0 ? "AND (" + searchClauses.joined(separator: "OR") + ") " : "" + let sql = + "SELECT \(projection) FROM " + + "\(TableLoginsLocal) WHERE is_deleted = 0 " + whereSearchClause + + "UNION ALL " + + "SELECT \(projection) FROM " + + "\(TableLoginsMirror) WHERE is_overridden = 0 " + whereSearchClause + + "ORDER BY hostname ASC" + + return db.runQuery(sql, args: args, factory: SQLiteLogins.LoginFactory) + } + + open func addLogin(_ login: LoginData) -> Success { + if let error = login.isValid.failureValue { + return deferMaybe(error) + } + + let nowMicro = Date.nowMicroseconds() + let nowMilli = nowMicro / 1000 + let dateMicro = nowMicro + let dateMilli = nowMilli + + let args: Args = [ + login.hostname, + login.httpRealm, + login.formSubmitURL, + login.usernameField, + login.passwordField, + login.username, + login.password, + login.guid, + dateMicro, // timeCreated + dateMicro, // timeLastUsed + dateMicro, // timePasswordChanged + dateMilli, // localModified + ] + + let sql = + "INSERT OR IGNORE INTO \(TableLoginsLocal) " + + // Shared fields. + "( hostname" + + ", httpRealm" + + ", formSubmitURL" + + ", usernameField" + + ", passwordField" + + ", timesUsed" + + ", username" + + ", password " + + // Local metadata. + ", guid " + + ", timeCreated" + + ", timeLastUsed" + + ", timePasswordChanged" + + ", local_modified " + + ", is_deleted " + + ", sync_status " + + ") " + + "VALUES (?,?,?,?,?,1,?,?,?,?,?, " + + "?, ?, 0, \(SyncStatus.new.rawValue)" + // Metadata. + ")" + + return db.run(sql, withArgs: args) + >>> effect(self.notifyLoginDidChange) + } + + fileprivate func cloneMirrorToOverlay(whereClause: String?, args: Args?) -> Deferred> { + let shared = "guid, hostname, httpRealm, formSubmitURL, usernameField, passwordField, timeCreated, timeLastUsed, timePasswordChanged, timesUsed, username, password " + let local = ", local_modified, is_deleted, sync_status " + let sql = "INSERT OR IGNORE INTO \(TableLoginsLocal) (\(shared)\(local)) SELECT \(shared), NULL AS local_modified, 0 AS is_deleted, 0 AS sync_status FROM \(TableLoginsMirror) \(whereClause ?? "")" + return self.db.write(sql, withArgs: args) + } + + /** + * Returns success if either a local row already existed, or + * one could be copied from the mirror. + */ + fileprivate func ensureLocalOverlayExistsForGUID(_ guid: GUID) -> Success { + let sql = "SELECT guid FROM \(TableLoginsLocal) WHERE guid = ?" + let args: Args = [guid] + let c = db.runQuery(sql, args: args, factory: { _ in 1 }) + + return c >>== { rows in + if rows.count > 0 { + return succeed() + } + log.debug("No overlay; cloning one for GUID \(guid).") + return self.cloneMirrorToOverlay(guid) + >>== { count in + if count > 0 { + return succeed() + } + log.warning("Failed to create local overlay for GUID \(guid).") + return deferMaybe(NoSuchRecordError(guid: guid)) + } + } + } + + fileprivate func cloneMirrorToOverlay(_ guid: GUID) -> Deferred> { + let whereClause = "WHERE guid = ?" + let args: Args = [guid] + + return self.cloneMirrorToOverlay(whereClause: whereClause, args: args) + } + + fileprivate func markMirrorAsOverridden(_ guid: GUID) -> Success { + let args: Args = [guid] + let sql = + "UPDATE \(TableLoginsMirror) SET " + + "is_overridden = 1 " + + "WHERE guid = ?" + + return self.db.run(sql, withArgs: args) + } + + /** + * Replace the local DB row with the provided GUID. + * If no local overlay exists, one is first created. + * + * If `significant` is `true`, the `sync_status` of the row is bumped to at least `Changed`. + * If it's already `New`, it remains marked as `New`. + * + * This flag allows callers to make minor changes (such as incrementing a usage count) + * without triggering an upload or a conflict. + */ + open func updateLoginByGUID(_ guid: GUID, new: LoginData, significant: Bool) -> Success { + if let error = new.isValid.failureValue { + return deferMaybe(error) + } + + // Right now this method is only ever called if the password changes at + // point of use, so we always set `timePasswordChanged` and `timeLastUsed`. + // We can (but don't) also assume that `significant` will always be `true`, + // at least for the time being. + let nowMicro = Date.nowMicroseconds() + let nowMilli = nowMicro / 1000 + let dateMicro = nowMicro + let dateMilli = nowMilli + + let args: Args = [ + dateMilli, // local_modified + dateMicro, // timeLastUsed + dateMicro, // timePasswordChanged + new.httpRealm, + new.formSubmitURL, + new.usernameField, + new.passwordField, + new.password, + new.hostname, + new.username, + guid, + ] + + let update = + "UPDATE \(TableLoginsLocal) SET " + + " local_modified = ?, timeLastUsed = ?, timePasswordChanged = ?" + + ", httpRealm = ?, formSubmitURL = ?, usernameField = ?" + + ", passwordField = ?, timesUsed = timesUsed + 1" + + ", password = ?, hostname = ?, username = ?" + + + // We keep rows marked as New in preference to marking them as changed. This allows us to + // delete them immediately if they don't reach the server. + (significant ? ", sync_status = max(sync_status, 1) " : "") + + " WHERE guid = ?" + + return self.ensureLocalOverlayExistsForGUID(guid) + >>> { self.markMirrorAsOverridden(guid) } + >>> { self.db.run(update, withArgs: args) } + >>> effect(self.notifyLoginDidChange) + } + + open func addUseOfLoginByGUID(_ guid: GUID) -> Success { + let sql = + "UPDATE \(TableLoginsLocal) SET " + + "timesUsed = timesUsed + 1, timeLastUsed = ?, local_modified = ? " + + "WHERE guid = ? AND is_deleted = 0" + + // For now, mere use is not enough to flip sync_status to Changed. + + let nowMicro = Date.nowMicroseconds() + let nowMilli = nowMicro / 1000 + let args: Args = [nowMicro, nowMilli, guid] + + return self.ensureLocalOverlayExistsForGUID(guid) + >>> { self.markMirrorAsOverridden(guid) } + >>> { self.db.run(sql, withArgs: args) } + } + + open func removeLoginByGUID(_ guid: GUID) -> Success { + return removeLoginsWithGUIDs([guid]) + } + + fileprivate func getDeletionStatementsForGUIDs(_ guids: ArraySlice, nowMillis: Timestamp) -> [(sql: String, args: Args?)] { + let inClause = BrowserDB.varlist(guids.count) + + // Immediately delete anything that's marked as new -- i.e., it's never reached + // the server. + let delete = + "DELETE FROM \(TableLoginsLocal) WHERE guid IN \(inClause) AND sync_status = \(SyncStatus.new.rawValue)" + + // Otherwise, mark it as changed. + let update = + "UPDATE \(TableLoginsLocal) SET " + + " local_modified = \(nowMillis)" + + ", sync_status = \(SyncStatus.changed.rawValue)" + + ", is_deleted = 1" + + ", password = ''" + + ", hostname = ''" + + ", username = ''" + + " WHERE guid IN \(inClause)" + + let markMirrorAsOverridden = + "UPDATE \(TableLoginsMirror) SET " + + "is_overridden = 1 " + + "WHERE guid IN \(inClause)" + + let insert = + "INSERT OR IGNORE INTO \(TableLoginsLocal) " + + "(guid, local_modified, is_deleted, sync_status, hostname, timeCreated, timePasswordChanged, password, username) " + + "SELECT guid, \(nowMillis), 1, \(SyncStatus.changed.rawValue), '', timeCreated, \(nowMillis)000, '', '' FROM \(TableLoginsMirror) WHERE guid IN \(inClause)" + + let args: Args = guids.map { $0 } + return [ (delete, args), (update, args), (markMirrorAsOverridden, args), (insert, args)] + } + + open func removeLoginsWithGUIDs(_ guids: [GUID]) -> Success { + let timestamp = Date.now() + return db.run(chunk(guids, by: BrowserDB.MaxVariableNumber).flatMap { + self.getDeletionStatementsForGUIDs($0, nowMillis: timestamp) + }) >>> effect(self.notifyLoginDidChange) + } + + open func removeAll() -> Success { + // Immediately delete anything that's marked as new -- i.e., it's never reached + // the server. If Sync isn't set up, this will be everything. + let delete = + "DELETE FROM \(TableLoginsLocal) WHERE sync_status = \(SyncStatus.new.rawValue)" + + let nowMillis = Date.now() + + // Mark anything we haven't already deleted. + let update = + "UPDATE \(TableLoginsLocal) SET local_modified = \(nowMillis), sync_status = \(SyncStatus.changed.rawValue), is_deleted = 1, password = '', hostname = '', username = '' WHERE is_deleted = 0" + + // Copy all the remaining rows from our mirror, marking them as locally deleted. The + // OR IGNORE will cause conflicts due to non-unique guids to be dropped, preserving + // anything we already deleted. + let insert = + "INSERT OR IGNORE INTO \(TableLoginsLocal) (guid, local_modified, is_deleted, sync_status, hostname, timeCreated, timePasswordChanged, password, username) " + + "SELECT guid, \(nowMillis), 1, \(SyncStatus.changed.rawValue), '', timeCreated, \(nowMillis)000, '', '' FROM \(TableLoginsMirror)" + + // After that, we mark all of the mirror rows as overridden. + return self.db.run(delete) + >>> { self.db.run(update) } + >>> { self.db.run("UPDATE \(TableLoginsMirror) SET is_overridden = 1") } + >>> { self.db.run(insert) } + >>> effect(self.notifyLoginDidChange) + } +} + +// When a server change is detected (e.g., syncID changes), we should consider shifting the contents +// of the mirror into the local overlay, allowing a content-based reconciliation to occur on the next +// full sync. Or we could flag the mirror as to-clear, download the server records and un-clear, and +// resolve the remainder on completion. This assumes that a fresh start will typically end up with +// the exact same records, so we might as well keep the shared parents around and double-check. +extension SQLiteLogins: SyncableLogins { + /** + * Delete the login with the provided GUID. Succeeds if the GUID is unknown. + */ + public func deleteByGUID(_ guid: GUID, deletedAt: Timestamp) -> Success { + // Simply ignore the possibility of a conflicting local change for now. + let local = "DELETE FROM \(TableLoginsLocal) WHERE guid = ?" + let remote = "DELETE FROM \(TableLoginsMirror) WHERE guid = ?" + let args: Args = [guid] + + return self.db.run(local, withArgs: args) >>> { self.db.run(remote, withArgs: args) } + } + + func getExistingMirrorRecordByGUID(_ guid: GUID) -> Deferred> { + let sql = "SELECT * FROM \(TableLoginsMirror) WHERE guid = ? LIMIT 1" + let args: Args = [guid] + return self.db.runQuery(sql, args: args, factory: SQLiteLogins.MirrorLoginFactory) >>== { deferMaybe($0[0]) } + } + + func getExistingLocalRecordByGUID(_ guid: GUID) -> Deferred> { + let sql = "SELECT * FROM \(TableLoginsLocal) WHERE guid = ? LIMIT 1" + let args: Args = [guid] + return self.db.runQuery(sql, args: args, factory: SQLiteLogins.LocalLoginFactory) >>== { deferMaybe($0[0]) } + } + + fileprivate func storeReconciledLogin(_ login: Login) -> Success { + let dateMilli = Date.now() + + let args: Args = [ + dateMilli, // local_modified + login.httpRealm, + login.formSubmitURL, + login.usernameField, + login.passwordField, + login.timeLastUsed, + login.timePasswordChanged, + login.timesUsed, + login.password, + login.hostname, + login.username, + login.guid, + ] + + let update = + "UPDATE \(TableLoginsLocal) SET " + + " local_modified = ?" + + ", httpRealm = ?, formSubmitURL = ?, usernameField = ?" + + ", passwordField = ?, timeLastUsed = ?, timePasswordChanged = ?, timesUsed = ?" + + ", password = ?" + + ", hostname = ?, username = ?" + + ", sync_status = \(SyncStatus.changed.rawValue) " + + " WHERE guid = ?" + + return self.db.run(update, withArgs: args) + } + + public func applyChangedLogin(_ upstream: ServerLogin) -> Success { + // Our login storage tracks the shared parent from the last sync (the "mirror"). + // This allows us to conclusively determine what changed in the case of conflict. + // + // Our first step is to determine whether the record is changed or new: i.e., whether + // or not it's present in the mirror. + // + // TODO: these steps can be done in a single query. Make it work, make it right, make it fast. + // TODO: if there's no mirror record, all incoming records can be applied in one go; the only + // reason we need to fetch first is to establish the shared parent. That would be nice. + let guid = upstream.guid + return self.getExistingMirrorRecordByGUID(guid) >>== { mirror in + return self.getExistingLocalRecordByGUID(guid) >>== { local in + return self.applyChangedLogin(upstream, local: local, mirror: mirror) + } + } + } + + fileprivate func applyChangedLogin(_ upstream: ServerLogin, local: LocalLogin?, mirror: MirrorLogin?) -> Success { + // Once we have the server record, the mirror record (if any), and the local overlay (if any), + // we can always know which state a record is in. + + // If it's present in the mirror, then we can proceed directly to handling the change; + // we assume that once a record makes it into the mirror, that the local record association + // has already taken place, and we're tracking local changes correctly. + if let mirror = mirror { + log.debug("Mirror record found for changed record \(mirror.guid).") + if let local = local { + log.debug("Changed local overlay found for \(local.guid). Resolving conflict with 3WM.") + // * Changed remotely and locally (conflict). Resolve the conflict using a three-way merge: the + // local mirror is the shared parent of both the local overlay and the new remote record. + // Apply results as in the co-creation case. + return self.resolveConflictBetween(local: local, upstream: upstream, shared: mirror) + } + + log.debug("No local overlay found. Updating mirror to upstream.") + // * Changed remotely but not locally. Apply the remote changes to the mirror. + // There is no local overlay to discard or resolve against. + return self.updateMirrorToLogin(upstream, fromPrevious: mirror) + } + + // * New both locally and remotely with no shared parent (cocreation). + // Or we matched the GUID, and we're assuming we just forgot the mirror. + // + // Merge and apply the results remotely, writing the result into the mirror and discarding the overlay + // if the upload succeeded. (Doing it in this order allows us to safely replay on failure.) + // + // If the local and remote record are the same, this is trivial. + // At this point we also switch our local GUID to match the remote. + if let local = local { + // We might have randomly computed the same GUID on two devices connected + // to the same Sync account. + // With our 9-byte GUIDs, the chance of that happening is very small, so we + // assume that this device has previously connected to this account, and we + // go right ahead with a merge. + log.debug("Local record with GUID \(local.guid) but no mirror. This is unusual; assuming disconnect-reconnect scenario. Smushing.") + return self.resolveConflictWithoutParentBetween(local: local, upstream: upstream) + } + + // If it's not present, we must first check whether we have a local record that's substantially + // the same -- the co-creation or re-sync case. + // + // In this case, we apply the server record to the mirror, change the local record's GUID, + // and proceed to reconcile the change on a content basis. + return self.findLocalRecordByContent(upstream) >>== { local in + if let local = local { + log.debug("Local record \(local.guid) content-matches new remote record \(upstream.guid). Smushing.") + return self.resolveConflictWithoutParentBetween(local: local, upstream: upstream) + } + + // * New upstream only; no local overlay, content-based merge, + // or shared parent in the mirror. Insert it in the mirror. + log.debug("Never seen remote record \(upstream.guid). Mirroring.") + return self.insertNewMirror(upstream) + } + } + + // N.B., the final guid is sometimes a WHERE and sometimes inserted. + fileprivate func mirrorArgs(_ login: ServerLogin) -> Args { + let args: Args = [ + login.serverModified, + login.httpRealm, + login.formSubmitURL, + login.usernameField, + login.passwordField, + login.timesUsed, + login.timeLastUsed, + login.timePasswordChanged, + login.timeCreated, + login.password, + login.hostname, + login.username, + login.guid, + ] + return args + } + + /** + * Called when we have a changed upstream record and no local changes. + * There's no need to flip the is_overridden flag. + */ + fileprivate func updateMirrorToLogin(_ login: ServerLogin, fromPrevious previous: Login) -> Success { + let args = self.mirrorArgs(login) + let sql = + "UPDATE \(TableLoginsMirror) SET " + + " server_modified = ?" + + ", httpRealm = ?, formSubmitURL = ?, usernameField = ?" + + ", passwordField = ?" + + + // These we need to coalesce, because we might be supplying zeroes if the remote has + // been overwritten by an older client. In this case, preserve the old value in the + // mirror. + ", timesUsed = coalesce(nullif(?, 0), timesUsed)" + + ", timeLastUsed = coalesce(nullif(?, 0), timeLastUsed)" + + ", timePasswordChanged = coalesce(nullif(?, 0), timePasswordChanged)" + + ", timeCreated = coalesce(nullif(?, 0), timeCreated)" + + ", password = ?, hostname = ?, username = ?" + + " WHERE guid = ?" + + return self.db.run(sql, withArgs: args) + } + + /** + * Called when we have a completely new record. Naturally the new record + * is marked as non-overridden. + */ + fileprivate func insertNewMirror(_ login: ServerLogin, isOverridden: Int = 0) -> Success { + let args = self.mirrorArgs(login) + let sql = + "INSERT OR IGNORE INTO \(TableLoginsMirror) (" + + " is_overridden, server_modified" + + ", httpRealm, formSubmitURL, usernameField" + + ", passwordField, timesUsed, timeLastUsed, timePasswordChanged, timeCreated" + + ", password, hostname, username, guid" + + ") VALUES (\(isOverridden), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)" + + return self.db.run(sql, withArgs: args) + } + + /** + * We assume a local record matches if it has the same username (password can differ), + * hostname, httpRealm. We also check that the formSubmitURLs are either blank or have the + * same host and port. + * + * This is roughly the same as desktop's .matches(): + * + */ + fileprivate func findLocalRecordByContent(_ login: Login) -> Deferred> { + let primary = + "SELECT * FROM \(TableLoginsLocal) WHERE " + + "hostname IS ? AND httpRealm IS ? AND username IS ?" + + var args: Args = [login.hostname, login.httpRealm, login.username] + let sql: String + + if login.formSubmitURL == nil { + sql = primary + " AND formSubmitURL IS NULL" + } else if login.formSubmitURL!.isEmpty { + sql = primary + } else { + if let hostPort = login.formSubmitURL?.asURL?.hostPort { + // Substring check will suffice for now. TODO: proper host/port check after fetching the cursor. + sql = primary + " AND (formSubmitURL = '' OR (instr(formSubmitURL, ?) > 0))" + args.append(hostPort) + } else { + log.warning("Incoming formSubmitURL is non-empty but is not a valid URL with a host. Not matching local.") + return deferMaybe(nil) + } + } + + return self.db.runQuery(sql, args: args, factory: SQLiteLogins.LocalLoginFactory) + >>== { cursor in + switch cursor.count { + case 0: + return deferMaybe(nil) + case 1: + // Great! + return deferMaybe(cursor[0]) + default: + // TODO: join against the mirror table to exclude local logins that + // already match a server record. + // Right now just take the first. + log.warning("Got \(cursor.count) local logins with matching details! This is most unexpected.") + return deferMaybe(cursor[0]) + } + } + } + + fileprivate func resolveConflictBetween(local: LocalLogin, upstream: ServerLogin, shared: Login) -> Success { + // Attempt to compute two delta sets by comparing each new record to the shared record. + // Then we can merge the two delta sets -- either perfectly or by picking a winner in the case + // of a true conflict -- and produce a resultant record. + + let localDeltas = (local.localModified, local.deltas(from: shared)) + let upstreamDeltas = (upstream.serverModified, upstream.deltas(from: shared)) + + let mergedDeltas = Login.mergeDeltas(a: localDeltas, b: upstreamDeltas) + + // Not all Sync clients handle the optional timestamp fields introduced in Bug 555755. + // We might get a server record with no timestamps, and it will differ from the original + // mirror! + // We solve that by refusing to generate deltas that discard information. We'll preserve + // the local values -- either from the local record or from the last shared parent that + // still included them -- and propagate them back to the server. + // It's OK for us to reconcile and reupload; it causes extra work for every client, but + // should not cause looping. + let resultant = shared.applyDeltas(mergedDeltas) + + // We can immediately write the downloaded upstream record -- the old one -- to + // the mirror store. + // We then apply this record to the local store, and mark it as needing upload. + // When the reconciled record is uploaded, it'll be flushed into the mirror + // with the correct modified time. + return self.updateMirrorToLogin(upstream, fromPrevious: shared) + >>> { self.storeReconciledLogin(resultant) } + } + + fileprivate func resolveConflictWithoutParentBetween(local: LocalLogin, upstream: ServerLogin) -> Success { + // Do the best we can. Either the local wins and will be + // uploaded, or the remote wins and we delete our overlay. + if local.timePasswordChanged > upstream.timePasswordChanged { + log.debug("Conflicting records with no shared parent. Using newer local record.") + return self.insertNewMirror(upstream, isOverridden: 1) + } + + log.debug("Conflicting records with no shared parent. Using newer remote record.") + let args: Args = [local.guid] + return self.insertNewMirror(upstream, isOverridden: 0) + >>> { self.db.run("DELETE FROM \(TableLoginsLocal) WHERE guid = ?", withArgs: args) } + } + + public func getModifiedLoginsToUpload() -> Deferred> { + let sql = + "SELECT * FROM \(TableLoginsLocal) " + + "WHERE sync_status IS NOT \(SyncStatus.synced.rawValue) AND is_deleted = 0" + + // Swift 2.0: use Cursor.asArray directly. + return self.db.runQuery(sql, args: nil, factory: SQLiteLogins.LoginFactory) + >>== { deferMaybe($0.asArray()) } + } + + public func getDeletedLoginsToUpload() -> Deferred> { + // There are no logins that are marked as deleted that were not originally synced -- + // others are deleted immediately. + let sql = + "SELECT guid FROM \(TableLoginsLocal) " + + "WHERE is_deleted = 1" + + // Swift 2.0: use Cursor.asArray directly. + return self.db.runQuery(sql, args: nil, factory: { return $0["guid"] as! GUID }) + >>== { deferMaybe($0.asArray()) } + } + + /** + * Chains through the provided timestamp. + */ + public func markAsSynchronized(_ guids: T, modified: Timestamp) -> Deferred> where T.Iterator.Element == GUID { + // Update the mirror from the local record that we just uploaded. + // sqlite doesn't support UPDATE FROM, so instead of running 10 subqueries * n GUIDs, + // we issue a single DELETE and a single INSERT on the mirror, then throw away the + // local overlay that we just uploaded with another DELETE. + log.debug("Marking \(guids.count) GUIDs as synchronized.") + + let queries: [(String, Args?)] = chunkCollection(guids, by: BrowserDB.MaxVariableNumber) { guids in + let args: Args = guids.map { $0 } + let inClause = BrowserDB.varlist(args.count) + + let delMirror = "DELETE FROM \(TableLoginsMirror) WHERE guid IN \(inClause)" + + let insMirror = + "INSERT OR IGNORE INTO \(TableLoginsMirror) (" + + " is_overridden, server_modified" + + ", httpRealm, formSubmitURL, usernameField" + + ", passwordField, timesUsed, timeLastUsed, timePasswordChanged, timeCreated" + + ", password, hostname, username, guid" + + ") SELECT 0, \(modified)" + + ", httpRealm, formSubmitURL, usernameField" + + ", passwordField, timesUsed, timeLastUsed, timePasswordChanged, timeCreated" + + ", password, hostname, username, guid " + + "FROM \(TableLoginsLocal) " + + "WHERE guid IN \(inClause)" + + let delLocal = "DELETE FROM \(TableLoginsLocal) WHERE guid IN \(inClause)" + + return [(delMirror, args), + (insMirror, args), + (delLocal, args)] + } + + return self.db.run(queries) + >>> always(modified) + } + + public func markAsDeleted(_ guids: T) -> Success where T.Iterator.Element == GUID { + log.debug("Marking \(guids.count) GUIDs as deleted.") + + let queries: [(String, Args?)] = chunkCollection(guids, by: BrowserDB.MaxVariableNumber) { guids in + let args: Args = guids.map { $0 } + let inClause = BrowserDB.varlist(args.count) + return [("DELETE FROM \(TableLoginsMirror) WHERE guid IN \(inClause)", args), + ("DELETE FROM \(TableLoginsLocal) WHERE guid IN \(inClause)", args)] + } + + return self.db.run(queries) + } + + public func hasSyncedLogins() -> Deferred> { + let checkLoginsMirror = "SELECT 1 FROM \(TableLoginsMirror)" + let checkLoginsLocal = "SELECT 1 FROM \(TableLoginsLocal) WHERE sync_status IS NOT \(SyncStatus.new.rawValue)" + + let sql = "\(checkLoginsMirror) UNION ALL \(checkLoginsLocal)" + return self.db.queryReturnsResults(sql) + } +} + +extension SQLiteLogins: ResettableSyncStorage { + /** + * Clean up any metadata. + * TODO: is this safe for a regular reset? It forces a content-based merge. + */ + public func resetClient() -> Success { + // Clone all the mirrors so we don't lose data. + return self.cloneMirrorToOverlay(whereClause: nil, args: nil) + + // Drop all of the mirror data. + >>> { self.db.run("DELETE FROM \(TableLoginsMirror)") } + + // Mark all of the local data as new. + >>> { self.db.run("UPDATE \(TableLoginsLocal) SET sync_status = \(SyncStatus.new.rawValue)") } + } +} + +extension SQLiteLogins: AccountRemovalDelegate { + public func onRemovedAccount() -> Success { + return self.resetClient() + } +} diff --git a/mobile/ios/Storage/SQL/SQLiteMetadata.swift b/mobile/ios/Storage/SQL/SQLiteMetadata.swift new file mode 100644 index 0000000000..a1cc54f7f5 --- /dev/null +++ b/mobile/ios/Storage/SQL/SQLiteMetadata.swift @@ -0,0 +1,58 @@ +/* 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 Deferred +import Shared + +/// The sqlite-backed implementation of the metadata protocol containing images and content for pages. +open class SQLiteMetadata { + let db: BrowserDB + + required public init(db: BrowserDB) { + self.db = db + } +} + +extension SQLiteMetadata: Metadata { + // A cache key is a conveninent, readable identifier for a site in the metadata database which helps + // with deduping entries for the same page. + typealias CacheKey = String + + /// Persists the given PageMetadata object to browser.db in the page_metadata table. + /// + /// - parameter metadata: Metadata object + /// - parameter pageURL: URL of page metadata was fetched from + /// - parameter expireAt: Expiration/TTL interval for when this metadata should expire at. + /// + /// - returns: Deferred on success + public func storeMetadata(_ metadata: PageMetadata, forPageURL pageURL: URL, + expireAt: UInt64) -> Success { + guard let cacheKey = pageURL.displayURL?.absoluteString else { + return succeed() + } + + // Replace any matching cache_key entries if they exist + let selectUniqueCacheKey = "COALESCE((SELECT cache_key FROM \(TablePageMetadata) WHERE cache_key = ?), ?)" + let args: Args = [cacheKey, cacheKey, metadata.siteURL, metadata.mediaURL, metadata.title, + metadata.type, metadata.description, metadata.providerName, + expireAt] + + let insert = + "INSERT OR REPLACE INTO \(TablePageMetadata)" + + "(cache_key, site_url, media_url, title, type, description, provider_name, expired_at) " + + "VALUES ( \(selectUniqueCacheKey), ?, ?, ?, ?, ?, ?, ?)" + + return self.db.run(insert, withArgs: args) + } + + /// Purges any metadata items living in page_metadata that are expired. + /// + /// - returns: Deferred on success + public func deleteExpiredMetadata() -> Success { + let sql = "DELETE FROM page_metadata WHERE expired_at <= (CAST(strftime('%s', 'now') AS LONG)*1000)" + return self.db.run(sql) + } + +} diff --git a/mobile/ios/Storage/SQL/SQLiteQueue.swift b/mobile/ios/Storage/SQL/SQLiteQueue.swift new file mode 100644 index 0000000000..645658d882 --- /dev/null +++ b/mobile/ios/Storage/SQL/SQLiteQueue.swift @@ -0,0 +1,35 @@ +/* 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 XCGLogger +import Deferred + +private let log = Logger.syncLogger + +open class SQLiteQueue: TabQueue { + let db: BrowserDB + + public init(db: BrowserDB) { + self.db = db + } + + open func addToQueue(_ tab: ShareItem) -> Success { + let args: Args = [tab.url, tab.title] + return db.run("INSERT OR IGNORE INTO \(TableQueuedTabs) (url, title) VALUES (?, ?)", withArgs: args) + } + + fileprivate func factory(_ row: SDRow) -> ShareItem { + return ShareItem(url: row["url"] as! String, title: row["title"] as? String, favicon: nil) + } + + open func getQueuedTabs() -> Deferred>> { + return db.runQuery("SELECT url, title FROM \(TableQueuedTabs)", args: nil, factory: self.factory) + } + + open func clearQueuedTabs() -> Success { + return db.run("DELETE FROM \(TableQueuedTabs)") + } +} diff --git a/mobile/ios/Storage/SQL/SQLiteRemoteClientsAndTabs.swift b/mobile/ios/Storage/SQL/SQLiteRemoteClientsAndTabs.swift new file mode 100644 index 0000000000..5b16e473fa --- /dev/null +++ b/mobile/ios/Storage/SQL/SQLiteRemoteClientsAndTabs.swift @@ -0,0 +1,382 @@ +/* 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 XCGLogger +import Deferred + +private let log = Logger.syncLogger + +open class SQLiteRemoteClientsAndTabs: RemoteClientsAndTabs { + let db: BrowserDB + + public init(db: BrowserDB) { + self.db = db + } + + class func remoteClientFactory(_ row: SDRow) -> RemoteClient { + let guid = row["guid"] as? String + let name = row["name"] as! String + let mod = (row["modified"] as! NSNumber).uint64Value + let type = row["type"] as? String + let form = row["formfactor"] as? String + let os = row["os"] as? String + let version = row["version"] as? String + let fxaDeviceId = row["fxaDeviceId"] as? String + return RemoteClient(guid: guid, name: name, modified: mod, type: type, formfactor: form, os: os, version: version, fxaDeviceId: fxaDeviceId) + } + + class func remoteTabFactory(_ row: SDRow) -> RemoteTab { + let clientGUID = row["client_guid"] as? String + let url = URL(string: row["url"] as! String)! // TODO: find a way to make this less dangerous. + let title = row["title"] as! String + let history = SQLiteRemoteClientsAndTabs.convertStringToHistory(row["history"] as? String) + let lastUsed = row.getTimestamp("last_used")! + return RemoteTab(clientGUID: clientGUID, URL: url, title: title, history: history, lastUsed: lastUsed, icon: nil) + } + + class func convertStringToHistory(_ history: String?) -> [URL] { + guard let data = history?.data(using: String.Encoding.utf8), + let decoded = try? JSONSerialization.jsonObject(with: data, options: [JSONSerialization.ReadingOptions.allowFragments]), + let urlStrings = decoded as? [String] else { + return [] + } + return optFilter(urlStrings.flatMap { URL(string: $0) }) + } + + class func convertHistoryToString(_ history: [URL]) -> String? { + let historyAsStrings = optFilter(history.map { $0.absoluteString }) + + guard let data = try? JSONSerialization.data(withJSONObject: historyAsStrings, options: []) else { + return nil + } + return String(data: data, encoding: String.Encoding(rawValue: String.Encoding.utf8.rawValue)) + } + + open func wipeClients() -> Success { + return db.run("DELETE FROM \(TableClients)") + } + + open func wipeRemoteTabs() -> Success { + return db.run("DELETE FROM \(TableTabs) WHERE client_guid IS NOT NULL") + } + + open func wipeTabs() -> Success { + return db.run("DELETE FROM \(TableTabs)") + } + + open func insertOrUpdateTabs(_ tabs: [RemoteTab]) -> Deferred> { + return self.insertOrUpdateTabsForClientGUID(nil, tabs: tabs) + } + + open func insertOrUpdateTabsForClientGUID(_ clientGUID: String?, tabs: [RemoteTab]) -> Deferred> { + let deleteQuery = "DELETE FROM \(TableTabs) WHERE client_guid IS ?" + let deleteArgs: Args = [clientGUID] + + return db.transaction { connection -> Int in + // Delete any existing tabs. + try connection.executeChange(deleteQuery, withArgs: deleteArgs) + + // Insert replacement tabs. + var inserted = 0 + for tab in tabs { + let args: Args = [ + tab.clientGUID, + tab.URL.absoluteString, + tab.title, + SQLiteRemoteClientsAndTabs.convertHistoryToString(tab.history), + NSNumber(value: tab.lastUsed) + ] + + let lastInsertedRowID = connection.lastInsertedRowID + + // We trust that each tab's clientGUID matches the supplied client! + // Really tabs shouldn't have a GUID at all. Future cleanup! + try connection.executeChange("INSERT INTO \(TableTabs) (client_guid, url, title, history, last_used) VALUES (?, ?, ?, ?, ?)", withArgs: args) + + if connection.lastInsertedRowID == lastInsertedRowID { + log.debug("Unable to INSERT RemoteTab!") + } else { + inserted += 1 + } + } + + return inserted + } + } + + open func insertOrUpdateClients(_ clients: [RemoteClient]) -> Deferred> { + // TODO: insert multiple clients in a single query. + // ORM systems are foolish. + return db.transaction { connection -> Int in + var succeeded = 0 + + // Update or insert client records. + for client in clients { + let args: Args = [ + client.name, + NSNumber(value: client.modified), + client.type, + client.formfactor, + client.os, + client.version, + client.fxaDeviceId, + client.guid + ] + + try connection.executeChange("UPDATE \(TableClients) SET name = ?, modified = ?, type = ?, formfactor = ?, os = ?, version = ?, fxaDeviceId = ? WHERE guid = ?", withArgs: args) + + if connection.numberOfRowsModified == 0 { + let args: Args = [ + client.guid, + client.name, + NSNumber(value: client.modified), + client.type, + client.formfactor, + client.os, + client.version, + client.fxaDeviceId + ] + + let lastInsertedRowID = connection.lastInsertedRowID + + try connection.executeChange("INSERT INTO \(TableClients) (guid, name, modified, type, formfactor, os, version, fxaDeviceId) VALUES (?, ?, ?, ?, ?, ?, ?, ?)", withArgs: args) + + if connection.lastInsertedRowID == lastInsertedRowID { + log.debug("INSERT did not change last inserted row ID.") + } + } + + succeeded += 1 + } + + return succeeded + } + } + + open func insertOrUpdateClient(_ client: RemoteClient) -> Deferred> { + return insertOrUpdateClients([client]) + } + + open func deleteClient(guid: GUID) -> Success { + let deleteTabsQuery = "DELETE FROM \(TableTabs) WHERE client_guid = ?" + let deleteClientQuery = "DELETE FROM \(TableClients) WHERE guid = ?" + let deleteArgs: Args = [guid] + + return db.transaction { connection -> Void in + try connection.executeChange(deleteClientQuery, withArgs: deleteArgs) + try connection.executeChange(deleteTabsQuery, withArgs: deleteArgs) + } + } + + open func getClient(guid: GUID) -> Deferred> { + let factory = SQLiteRemoteClientsAndTabs.remoteClientFactory + return self.db.runQuery("SELECT * FROM \(TableClients) WHERE guid = ?", args: [guid], factory: factory) >>== { deferMaybe($0[0]) } + } + + open func getClient(fxaDeviceId: String) -> Deferred> { + let factory = SQLiteRemoteClientsAndTabs.remoteClientFactory + return self.db.runQuery("SELECT * FROM \(TableClients) WHERE fxaDeviceId = ?", args: [fxaDeviceId], factory: factory) >>== { deferMaybe($0[0]) } + } + + open func getClientWithId(_ clientID: GUID) -> Deferred> { + return self.getClient(guid: clientID) + } + + open func getClients() -> Deferred> { + return db.withConnection { connection -> [RemoteClient] in + let cursor = connection.executeQuery("SELECT * FROM \(TableClients) WHERE EXISTS (SELECT 1 FROM \(TableRemoteDevices) rd WHERE rd.guid = fxaDeviceId) ORDER BY modified DESC", factory: SQLiteRemoteClientsAndTabs.remoteClientFactory) + defer { + cursor.close() + } + + return cursor.asArray() + } + } + + open func getClientGUIDs() -> Deferred>> { + let c = db.runQuery("SELECT guid FROM \(TableClients) WHERE guid IS NOT NULL", args: nil, factory: { $0["guid"] as! String }) + return c >>== { cursor in + let guids = Set(cursor.asArray()) + return deferMaybe(guids) + } + } + + open func getTabsForClientWithGUID(_ guid: GUID?) -> Deferred> { + let tabsSQL: String + let clientArgs: Args? + if let _ = guid { + tabsSQL = "SELECT * FROM \(TableTabs) WHERE client_guid = ?" + clientArgs = [guid] + } else { + tabsSQL = "SELECT * FROM \(TableTabs) WHERE client_guid IS NULL" + clientArgs = nil + } + + log.debug("Looking for tabs for client with guid: \(guid ?? "nil")") + return db.runQuery(tabsSQL, args: clientArgs, factory: SQLiteRemoteClientsAndTabs.remoteTabFactory) >>== { + let tabs = $0.asArray() + log.debug("Found \(tabs.count) tabs for client with guid: \(guid ?? "nil")") + return deferMaybe(tabs) + } + } + + open func getClientsAndTabs() -> Deferred> { + return db.withConnection { conn -> ([RemoteClient], [RemoteTab]) in + let clientsCursor = conn.executeQuery("SELECT * FROM \(TableClients) WHERE EXISTS (SELECT 1 FROM \(TableRemoteDevices) rd WHERE rd.guid = fxaDeviceId) ORDER BY modified DESC", factory: SQLiteRemoteClientsAndTabs.remoteClientFactory) + let tabsCursor = conn.executeQuery("SELECT * FROM \(TableTabs) WHERE client_guid IS NOT NULL ORDER BY client_guid DESC, last_used DESC", factory: SQLiteRemoteClientsAndTabs.remoteTabFactory) + + defer { + clientsCursor.close() + tabsCursor.close() + } + + return (clientsCursor.asArray(), tabsCursor.asArray()) + } >>== { clients, tabs in + var acc = [String: [RemoteTab]]() + for tab in tabs { + if let guid = tab.clientGUID { + if acc[guid] == nil { + acc[guid] = [tab] + } else { + acc[guid]!.append(tab) + } + } else { + log.error("RemoteTab (\(tab)) has a nil clientGUID") + } + } + + // Most recent first. + let fillTabs: (RemoteClient) -> ClientAndTabs = { client in + var tabs: [RemoteTab]? = nil + if let guid: String = client.guid { + tabs = acc[guid] + } + return ClientAndTabs(client: client, tabs: tabs ?? []) + } + + return deferMaybe(clients.map(fillTabs)) + } + } + + open func deleteCommands() -> Success { + return db.run("DELETE FROM \(TableSyncCommands)") + } + + open func deleteCommands(_ clientGUID: GUID) -> Success { + return db.run("DELETE FROM \(TableSyncCommands) WHERE client_guid = ?", withArgs: [clientGUID] as Args) + } + + open func insertCommand(_ command: SyncCommand, forClients clients: [RemoteClient]) -> Deferred> { + return insertCommands([command], forClients: clients) + } + + open func insertCommands(_ commands: [SyncCommand], forClients clients: [RemoteClient]) -> Deferred> { + return db.transaction { connection -> Int in + var numberOfInserts = 0 + + // Update or insert client records. + for command in commands { + for client in clients { + do { + if let commandID = try self.insert(connection, sql: "INSERT INTO \(TableSyncCommands) (client_guid, value) VALUES (?, ?)", args: [client.guid, command.value] as Args) { + log.verbose("Inserted command: \(commandID)") + numberOfInserts += 1 + } else { + log.warning("Command not inserted, but no error!") + } + } catch let err as NSError { + log.error("insertCommands(_:, forClients:) failed: \(err.localizedDescription) (numberOfInserts: \(numberOfInserts)") + throw err + } + } + } + + return numberOfInserts + } + } + + open func getCommands() -> Deferred> { + return db.withConnection { connection -> [GUID: [SyncCommand]] in + let cursor = connection.executeQuery("SELECT * FROM \(TableSyncCommands)", factory: { row -> SyncCommand in + SyncCommand( + id: row["command_id"] as? Int, + value: row["value"] as! String, + clientGUID: row["client_guid"] as? GUID) + }) + defer { + cursor.close() + } + + return self.clientsFromCommands(cursor.asArray()) + } + } + + func clientsFromCommands(_ commands: [SyncCommand]) -> [GUID: [SyncCommand]] { + var syncCommands = [GUID: [SyncCommand]]() + for command in commands { + var cmds: [SyncCommand] = syncCommands[command.clientGUID!] ?? [SyncCommand]() + cmds.append(command) + syncCommands[command.clientGUID!] = cmds + } + return syncCommands + } + + func insert(_ db: SQLiteDBConnection, sql: String, args: Args?) throws -> Int? { + let lastID = db.lastInsertedRowID + try db.executeChange(sql, withArgs: args) + + let id = db.lastInsertedRowID + if id == lastID { + log.debug("INSERT did not change last inserted row ID.") + return nil + } + + return id + } +} + +extension SQLiteRemoteClientsAndTabs: RemoteDevices { + open func replaceRemoteDevices(_ remoteDevices: [RemoteDevice]) -> Success { + // Drop corrupted records and our own record too. + let remoteDevices = remoteDevices.filter { $0.id != nil && $0.type != nil && !$0.isCurrentDevice } + + return db.transaction { conn -> Void in + try conn.executeChange("DELETE FROM \(TableRemoteDevices)") + + let now = Date.now() + + for device in remoteDevices { + let sql = + "INSERT INTO \(TableRemoteDevices) (guid, name, type, is_current_device, date_created, date_modified, last_access_time) " + + "VALUES (?, ?, ?, ?, ?, ?, ?)" + let args: Args = [device.id, device.name, device.type, device.isCurrentDevice, now, now, device.lastAccessTime] + try conn.executeChange(sql, withArgs: args) + } + } + } +} + +extension SQLiteRemoteClientsAndTabs: ResettableSyncStorage { + public func resetClient() -> Success { + // For this engine, resetting is equivalent to wiping. + return self.clear() + } + + public func clear() -> Success { + return db.transaction { conn -> Void in + try conn.executeChange("DELETE FROM \(TableTabs) WHERE client_guid IS NOT NULL") + try conn.executeChange("DELETE FROM \(TableClients)") + } + } +} + +extension SQLiteRemoteClientsAndTabs: AccountRemovalDelegate { + public func onRemovedAccount() -> Success { + log.info("Clearing clients and tabs after account removal.") + // TODO: Bug 1168690 - delete our client and tabs records from the server. + return self.resetClient() + } +} diff --git a/mobile/ios/Storage/SQL/Schema.swift b/mobile/ios/Storage/SQL/Schema.swift new file mode 100644 index 0000000000..0c22d65460 --- /dev/null +++ b/mobile/ios/Storage/SQL/Schema.swift @@ -0,0 +1,23 @@ +/* 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 + +/** + * Something that knows how to construct a database. + */ +public protocol Schema { + var name: String { get } + var version: Int { get } + + func create(_ db: SQLiteDBConnection) -> Bool + func update(_ db: SQLiteDBConnection, from: Int) -> Bool + func drop(_ db: SQLiteDBConnection) -> Bool +} + +enum SchemaUpgradeResult { + case success + case failure + case skipped +} diff --git a/mobile/ios/Storage/Sharing.swift b/mobile/ios/Storage/Sharing.swift new file mode 100644 index 0000000000..51fa13a1c8 --- /dev/null +++ b/mobile/ios/Storage/Sharing.swift @@ -0,0 +1,30 @@ +/* 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 UIKit +import Shared + +// A small structure to encapsulate all the possible data that we can get +// from an application sharing a web page or a URL. +public struct ShareItem { + public let url: String + public let title: String? + public let favicon: Favicon? + + public init(url: String, title: String?, favicon: Favicon?) { + self.url = url + self.title = title + self.favicon = favicon + } + + // We only support sharing HTTP and HTTPS URLs, as well as data URIs. + public var isShareable: Bool { + return URL(string: url)?.isWebPage() ?? false + } +} + +public protocol ShareToDestination { + func shareItem(_ item: ShareItem) -> Success +} diff --git a/mobile/ios/Storage/Site.swift b/mobile/ios/Storage/Site.swift new file mode 100644 index 0000000000..0da738f264 --- /dev/null +++ b/mobile/ios/Storage/Site.swift @@ -0,0 +1,95 @@ +/* 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 UIKit +import Shared + +public protocol Identifiable: Equatable { + var id: Int? { get set } +} + +public func ==(lhs: T, rhs: T) -> Bool where T: Identifiable { + return lhs.id == rhs.id +} + +public enum IconType: Int { + public func isPreferredTo (_ other: IconType) -> Bool { + return rank > other.rank + } + + fileprivate var rank: Int { + switch self { + case .appleIconPrecomposed: + return 5 + case .appleIcon: + return 4 + case .icon: + return 3 + case .local: + return 2 + case .guess: + return 1 + case .noneFound: + return 0 + } + } + + case icon = 0 + case appleIcon = 1 + case appleIconPrecomposed = 2 + case guess = 3 + case local = 4 + case noneFound = 5 +} + +open class Favicon: Identifiable { + open var id: Int? + + open let url: String + open let date: Date + open var width: Int? + open var height: Int? + open let type: IconType + + public init(url: String, date: Date = Date(), type: IconType) { + self.url = url + self.date = date + self.type = type + } +} + +// TODO: Site shouldn't have all of these optional decorators. Include those in the +// cursor results, perhaps as a tuple. +open class Site: Identifiable { + open var id: Int? + var guid: String? + + open var tileURL: URL { + return URL(string: url)?.domainURL ?? URL(string: "about:blank")! + } + + open let url: String + open let title: String + open var metadata: PageMetadata? + // Sites may have multiple favicons. We'll return the largest. + open var icon: Favicon? + open var latestVisit: Visit? + open fileprivate(set) var bookmarked: Bool? + + public convenience init(url: String, title: String) { + self.init(url: url, title: title, bookmarked: false, guid: nil) + } + + public init(url: String, title: String, bookmarked: Bool?, guid: String? = nil) { + self.url = url + self.title = title + self.bookmarked = bookmarked + self.guid = guid + } + + open func setBookmarked(_ bookmarked: Bool) { + self.bookmarked = bookmarked + } + +} diff --git a/mobile/ios/Storage/Storage-Bridging-Header.h b/mobile/ios/Storage/Storage-Bridging-Header.h new file mode 100644 index 0000000000..2be82409d9 --- /dev/null +++ b/mobile/ios/Storage/Storage-Bridging-Header.h @@ -0,0 +1,10 @@ +#ifndef Client_Storage_Bridging_Header_h +#define Client_Storage_Bridging_Header_h + +#define SQLITE_HAS_CODEC 1 + +#import "Shared-Bridging-Header.h" +#import +#import "ThirdParty/sqlcipher/sqlite3.h" + +#endif diff --git a/mobile/ios/Storage/Storage.xcodeproj/project.pbxproj b/mobile/ios/Storage/Storage.xcodeproj/project.pbxproj new file mode 100644 index 0000000000..640f468ea7 --- /dev/null +++ b/mobile/ios/Storage/Storage.xcodeproj/project.pbxproj @@ -0,0 +1,727 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + 0B2770451A89276900CE7692 /* BookmarksSqlite.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B2770441A89276900CE7692 /* BookmarksSqlite.swift */; }; + 0B2770471A893C2C00CE7692 /* TestBookmarks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B2770461A893C2C00CE7692 /* TestBookmarks.swift */; }; + 0B2770481A893C4700CE7692 /* BookmarksSqlite.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B2770441A89276900CE7692 /* BookmarksSqlite.swift */; }; + 0B2770491A893C9500CE7692 /* Bookmarks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0BA497801A7B094B004C8E17 /* Bookmarks.swift */; }; + 0B2770DD1A8AD83200CE7692 /* Bytes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B2770DC1A8AD83200CE7692 /* Bytes.swift */; }; + 0B2770E91A8AD85000CE7692 /* Bytes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B2770DC1A8AD83200CE7692 /* Bytes.swift */; }; + 0B30602F1A80338F0085B8BC /* Passwords.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B30602E1A80338F0085B8BC /* Passwords.swift */; }; + 0B3060311A8151B70085B8BC /* SQLitePasswords.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B3060301A8151B70085B8BC /* SQLitePasswords.swift */; }; + 0BA4977E1A7B0941004C8E17 /* SQLiteHistory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0BA4977A1A7B0941004C8E17 /* SQLiteHistory.swift */; }; + 0BA497821A7B094B004C8E17 /* Bookmarks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0BA497801A7B094B004C8E17 /* Bookmarks.swift */; }; + 0BA497831A7B094B004C8E17 /* Visit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0BA497811A7B094B004C8E17 /* Visit.swift */; }; + 0BA4978C1A7B0E44004C8E17 /* Cursor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 282DA4C01A699EF200A406E2 /* Cursor.swift */; }; + 0BA4978D1A7B0E55004C8E17 /* FileAccessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 282DA4D01A69A14500A406E2 /* FileAccessor.swift */; }; + 0BAE69561A7E1FD100B3609D /* SchemaTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0BAE69551A7E1FD100B3609D /* SchemaTable.swift */; }; + 0BAE69571A7E209200B3609D /* SchemaTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0BAE69551A7E1FD100B3609D /* SchemaTable.swift */; }; + 0BAE69591A7EEF8E00B3609D /* TestTableTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0BAE69581A7EEF8E00B3609D /* TestTableTable.swift */; }; + 0BCFBBAF1A77650E0087E26D /* GenericTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A59B3F1AAD619686DB9450C /* GenericTable.swift */; }; + 0BCFBBB41A7769A30087E26D /* JoinedHistoryVisitsTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A59BCB8D38CE6E6F5F5F2DE /* JoinedHistoryVisitsTable.swift */; }; + 0BCFBBB51A7769B70087E26D /* VisitsTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A59B22E909B6CAC95DBE879 /* VisitsTable.swift */; }; + 0BCFBBB81A776A200087E26D /* MockFiles.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0BCFBBB71A776A200087E26D /* MockFiles.swift */; }; + 0BF42D3E1A7C148F00889E28 /* Favicons.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0BF42D3D1A7C148F00889E28 /* Favicons.swift */; }; + 0BF42D401A7C18CD00889E28 /* SQLFavicons.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0BF42D3F1A7C18CD00889E28 /* SQLFavicons.swift */; }; + 0BF42D421A7C31E300889E28 /* JoinedFaviconsHistoryTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0BF42D411A7C31E300889E28 /* JoinedFaviconsHistoryTable.swift */; }; + 0BF42D441A7C31EE00889E28 /* FaviconsTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0BF42D431A7C31EE00889E28 /* FaviconsTable.swift */; }; + 0BF42D461A7C52C800889E28 /* TestFaviconsTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0BF42D451A7C52C800889E28 /* TestFaviconsTable.swift */; }; + 0BF42D471A7C558000889E28 /* Visit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0BA497811A7B094B004C8E17 /* Visit.swift */; }; + 0BF42D481A7C55D200889E28 /* Site.swift in Sources */ = {isa = PBXBuildFile; fileRef = 282DA4D61A69A28000A406E2 /* Site.swift */; }; + 0BF42D4A1A7C55F000889E28 /* FaviconsTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0BF42D431A7C31EE00889E28 /* FaviconsTable.swift */; }; + 0BF42D4B1A7C55F300889E28 /* JoinedFaviconsHistoryTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0BF42D411A7C31E300889E28 /* JoinedFaviconsHistoryTable.swift */; }; + 0BF42D4D1A7CC4ED00889E28 /* TestJoinedFaviconTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0BF42D4C1A7CC4ED00889E28 /* TestJoinedFaviconTable.swift */; }; + 282DA49B1A699E0300A406E2 /* Storage.h in Headers */ = {isa = PBXBuildFile; fileRef = 282DA49A1A699E0300A406E2 /* Storage.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 282DA4A11A699E0300A406E2 /* Storage.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 282DA4951A699E0300A406E2 /* Storage.framework */; }; + 282DA4A81A699E0300A406E2 /* StorageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 282DA4A71A699E0300A406E2 /* StorageTests.swift */; }; + 282DA4C21A699EF200A406E2 /* Cursor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 282DA4C01A699EF200A406E2 /* Cursor.swift */; }; + 282DA4C51A699F8A00A406E2 /* SwiftData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 282DA4C41A699F8A00A406E2 /* SwiftData.swift */; }; + 282DA4C61A699F8A00A406E2 /* SwiftData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 282DA4C41A699F8A00A406E2 /* SwiftData.swift */; }; + 282DA4D21A69A14500A406E2 /* FileAccessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 282DA4D01A69A14500A406E2 /* FileAccessor.swift */; }; + 282DA4D41A69A24300A406E2 /* History.swift in Sources */ = {isa = PBXBuildFile; fileRef = 282DA4D31A69A24300A406E2 /* History.swift */; }; + 282DA4D71A69A28000A406E2 /* Site.swift in Sources */ = {isa = PBXBuildFile; fileRef = 282DA4D61A69A28000A406E2 /* Site.swift */; }; + 282DA5211A69BCB700A406E2 /* module.modulemap in Sources */ = {isa = PBXBuildFile; fileRef = 282DA51F1A69BCB700A406E2 /* module.modulemap */; }; + 282DA5241A69BCBB00A406E2 /* libsqlite3.0.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = 282DA5231A69BCBB00A406E2 /* libsqlite3.0.dylib */; }; + 4A59B21913DCFA81C7CD9C91 /* GenericTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A59B3F1AAD619686DB9450C /* GenericTable.swift */; }; + 4A59B334A243792894D6E614 /* TestHistoryTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A59B6E9AB0A08F28227C050 /* TestHistoryTable.swift */; }; + 4A59BA1E93D5603E198EDCB9 /* HistoryTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A59BB07A2812FE4B516FD30 /* HistoryTable.swift */; }; + 4A59BAEB154EA6E1B621E067 /* BrowserDB.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A59BF61F0D956D2A1BDAE29 /* BrowserDB.swift */; }; + 4A59BB4F36A20F30229034C6 /* VisitsTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A59B22E909B6CAC95DBE879 /* VisitsTable.swift */; }; + 4A59BC6033BD93D1DC36E18F /* TestJoinedHistoryVisits.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A59B33FF96A39A59CBBBBD7 /* TestJoinedHistoryVisits.swift */; }; + 4A59BD24F6210866F59EF0F6 /* JoinedHistoryVisitsTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A59BCB8D38CE6E6F5F5F2DE /* JoinedHistoryVisitsTable.swift */; }; + 4A59BF0BBF50FC218A6D1143 /* BrowserDB.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A59BF61F0D956D2A1BDAE29 /* BrowserDB.swift */; }; + 4A59BF2F408CDEF8D50AF9D3 /* TestVisitsTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A59BE48DBF43989C51D3F98 /* TestVisitsTable.swift */; }; + 4A59BFF7C37812C7955B9E79 /* HistoryTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A59BB07A2812FE4B516FD30 /* HistoryTable.swift */; }; + E47726181A8A794B00FC058B /* ReadingList.swift in Sources */ = {isa = PBXBuildFile; fileRef = E47726171A8A794B00FC058B /* ReadingList.swift */; }; + E47726191A8A794B00FC058B /* ReadingList.swift in Sources */ = {isa = PBXBuildFile; fileRef = E47726171A8A794B00FC058B /* ReadingList.swift */; }; + E477261B1A8A7DE500FC058B /* SQLiteReadingList.swift in Sources */ = {isa = PBXBuildFile; fileRef = E477261A1A8A7DE500FC058B /* SQLiteReadingList.swift */; }; + E477261C1A8A7DE500FC058B /* SQLiteReadingList.swift in Sources */ = {isa = PBXBuildFile; fileRef = E477261A1A8A7DE500FC058B /* SQLiteReadingList.swift */; }; + E477261E1A8A7EDA00FC058B /* ReadingListTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = E477261D1A8A7EDA00FC058B /* ReadingListTable.swift */; }; + E477261F1A8A7EDA00FC058B /* ReadingListTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = E477261D1A8A7EDA00FC058B /* ReadingListTable.swift */; }; + E47726211A8A87F400FC058B /* TestReadingListTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = E47726201A8A87F400FC058B /* TestReadingListTable.swift */; }; + E4F43AC21A8CF9A200ACAC05 /* TestSQLiteReadingList.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4F43AC11A8CF9A200ACAC05 /* TestSQLiteReadingList.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 282DA4A21A699E0300A406E2 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 282DA48C1A699E0300A406E2 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 282DA4941A699E0300A406E2; + remoteInfo = Storage; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXFileReference section */ + 0B2770441A89276900CE7692 /* BookmarksSqlite.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BookmarksSqlite.swift; sourceTree = ""; }; + 0B2770461A893C2C00CE7692 /* TestBookmarks.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestBookmarks.swift; sourceTree = ""; }; + 0B2770DC1A8AD83200CE7692 /* Bytes.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Bytes.swift; path = ../../Sync/Bytes.swift; sourceTree = ""; }; + 0B30602E1A80338F0085B8BC /* Passwords.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Passwords.swift; sourceTree = ""; }; + 0B3060301A8151B70085B8BC /* SQLitePasswords.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SQLitePasswords.swift; sourceTree = ""; }; + 0BA4977A1A7B0941004C8E17 /* SQLiteHistory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = SQLiteHistory.swift; path = SQL/SQLiteHistory.swift; sourceTree = ""; }; + 0BA497801A7B094B004C8E17 /* Bookmarks.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Bookmarks.swift; sourceTree = ""; }; + 0BA497811A7B094B004C8E17 /* Visit.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Visit.swift; sourceTree = ""; }; + 0BAE69551A7E1FD100B3609D /* SchemaTable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SchemaTable.swift; sourceTree = ""; }; + 0BAE69581A7EEF8E00B3609D /* TestTableTable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestTableTable.swift; sourceTree = ""; }; + 0BCFBBB71A776A200087E26D /* MockFiles.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockFiles.swift; sourceTree = ""; }; + 0BF42D3D1A7C148F00889E28 /* Favicons.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Favicons.swift; sourceTree = ""; }; + 0BF42D3F1A7C18CD00889E28 /* SQLFavicons.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SQLFavicons.swift; sourceTree = ""; }; + 0BF42D411A7C31E300889E28 /* JoinedFaviconsHistoryTable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JoinedFaviconsHistoryTable.swift; sourceTree = ""; }; + 0BF42D431A7C31EE00889E28 /* FaviconsTable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FaviconsTable.swift; sourceTree = ""; }; + 0BF42D451A7C52C800889E28 /* TestFaviconsTable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestFaviconsTable.swift; sourceTree = ""; }; + 0BF42D4C1A7CC4ED00889E28 /* TestJoinedFaviconTable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestJoinedFaviconTable.swift; sourceTree = ""; }; + 282DA4951A699E0300A406E2 /* Storage.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Storage.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 282DA4991A699E0300A406E2 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 282DA49A1A699E0300A406E2 /* Storage.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Storage.h; sourceTree = ""; }; + 282DA4A01A699E0300A406E2 /* StorageTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = StorageTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 282DA4A61A699E0300A406E2 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 282DA4A71A699E0300A406E2 /* StorageTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StorageTests.swift; sourceTree = ""; }; + 282DA4C01A699EF200A406E2 /* Cursor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Cursor.swift; sourceTree = ""; }; + 282DA4C41A699F8A00A406E2 /* SwiftData.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = SwiftData.swift; path = ThirdParty/SwiftData.swift; sourceTree = ""; }; + 282DA4D01A69A14500A406E2 /* FileAccessor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FileAccessor.swift; sourceTree = ""; }; + 282DA4D31A69A24300A406E2 /* History.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = History.swift; sourceTree = ""; }; + 282DA4D61A69A28000A406E2 /* Site.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Site.swift; sourceTree = ""; }; + 282DA51F1A69BCB700A406E2 /* module.modulemap */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = "sourcecode.module-map"; path = module.modulemap; sourceTree = ""; }; + 282DA5231A69BCBB00A406E2 /* libsqlite3.0.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = libsqlite3.0.dylib; path = /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS8.1.sdk/usr/lib/libsqlite3.0.dylib; sourceTree = ""; }; + 4A59B22E909B6CAC95DBE879 /* VisitsTable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = VisitsTable.swift; path = SQL/VisitsTable.swift; sourceTree = ""; }; + 4A59B33FF96A39A59CBBBBD7 /* TestJoinedHistoryVisits.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestJoinedHistoryVisits.swift; sourceTree = ""; }; + 4A59B3F1AAD619686DB9450C /* GenericTable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = GenericTable.swift; path = SQL/GenericTable.swift; sourceTree = ""; }; + 4A59B6E9AB0A08F28227C050 /* TestHistoryTable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestHistoryTable.swift; sourceTree = ""; }; + 4A59BB07A2812FE4B516FD30 /* HistoryTable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = HistoryTable.swift; path = SQL/HistoryTable.swift; sourceTree = ""; }; + 4A59BCB8D38CE6E6F5F5F2DE /* JoinedHistoryVisitsTable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = JoinedHistoryVisitsTable.swift; path = SQL/JoinedHistoryVisitsTable.swift; sourceTree = ""; }; + 4A59BE48DBF43989C51D3F98 /* TestVisitsTable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestVisitsTable.swift; sourceTree = ""; }; + 4A59BF61F0D956D2A1BDAE29 /* BrowserDB.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = BrowserDB.swift; path = SQL/BrowserDB.swift; sourceTree = ""; }; + E47726171A8A794B00FC058B /* ReadingList.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReadingList.swift; sourceTree = ""; }; + E477261A1A8A7DE500FC058B /* SQLiteReadingList.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = SQLiteReadingList.swift; path = SQL/SQLiteReadingList.swift; sourceTree = ""; }; + E477261D1A8A7EDA00FC058B /* ReadingListTable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReadingListTable.swift; sourceTree = ""; }; + E47726201A8A87F400FC058B /* TestReadingListTable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestReadingListTable.swift; sourceTree = ""; }; + E4F43AC11A8CF9A200ACAC05 /* TestSQLiteReadingList.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestSQLiteReadingList.swift; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 282DA4911A699E0300A406E2 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 282DA5241A69BCBB00A406E2 /* libsqlite3.0.dylib in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 282DA49D1A699E0300A406E2 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 282DA4A11A699E0300A406E2 /* Storage.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 282DA48B1A699E0300A406E2 = { + isa = PBXGroup; + children = ( + 282DA5231A69BCBB00A406E2 /* libsqlite3.0.dylib */, + 282DA5201A69BCB700A406E2 /* modules */, + 282DA4971A699E0300A406E2 /* Storage */, + 282DA4C31A699F5600A406E2 /* Third-Party Source */, + 282DA4A41A699E0300A406E2 /* StorageTests */, + 282DA4961A699E0300A406E2 /* Products */, + ); + sourceTree = ""; + }; + 282DA4961A699E0300A406E2 /* Products */ = { + isa = PBXGroup; + children = ( + 282DA4951A699E0300A406E2 /* Storage.framework */, + 282DA4A01A699E0300A406E2 /* StorageTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 282DA4971A699E0300A406E2 /* Storage */ = { + isa = PBXGroup; + children = ( + 0B2770DC1A8AD83200CE7692 /* Bytes.swift */, + 0BA497801A7B094B004C8E17 /* Bookmarks.swift */, + E47726171A8A794B00FC058B /* ReadingList.swift */, + 0BA497811A7B094B004C8E17 /* Visit.swift */, + 282DA4D91A69A2C400A406E2 /* SQL */, + 282DA4D01A69A14500A406E2 /* FileAccessor.swift */, + 282DA4D61A69A28000A406E2 /* Site.swift */, + 282DA4D31A69A24300A406E2 /* History.swift */, + 282DA4C01A699EF200A406E2 /* Cursor.swift */, + 282DA49A1A699E0300A406E2 /* Storage.h */, + 282DA4981A699E0300A406E2 /* Supporting Files */, + 0BF42D3D1A7C148F00889E28 /* Favicons.swift */, + 0B30602E1A80338F0085B8BC /* Passwords.swift */, + ); + path = Storage; + sourceTree = ""; + }; + 282DA4981A699E0300A406E2 /* Supporting Files */ = { + isa = PBXGroup; + children = ( + 282DA4991A699E0300A406E2 /* Info.plist */, + ); + name = "Supporting Files"; + sourceTree = ""; + }; + 282DA4A41A699E0300A406E2 /* StorageTests */ = { + isa = PBXGroup; + children = ( + 282DA4A71A699E0300A406E2 /* StorageTests.swift */, + 282DA4A51A699E0300A406E2 /* Supporting Files */, + 4A59B6E9AB0A08F28227C050 /* TestHistoryTable.swift */, + 0BF42D451A7C52C800889E28 /* TestFaviconsTable.swift */, + 4A59B33FF96A39A59CBBBBD7 /* TestJoinedHistoryVisits.swift */, + 0BF42D4C1A7CC4ED00889E28 /* TestJoinedFaviconTable.swift */, + 4A59BE48DBF43989C51D3F98 /* TestVisitsTable.swift */, + 0BCFBBB71A776A200087E26D /* MockFiles.swift */, + 0BAE69581A7EEF8E00B3609D /* TestTableTable.swift */, + 0B2770461A893C2C00CE7692 /* TestBookmarks.swift */, + E47726201A8A87F400FC058B /* TestReadingListTable.swift */, + E4F43AC11A8CF9A200ACAC05 /* TestSQLiteReadingList.swift */, + ); + path = StorageTests; + sourceTree = ""; + }; + 282DA4A51A699E0300A406E2 /* Supporting Files */ = { + isa = PBXGroup; + children = ( + 282DA4A61A699E0300A406E2 /* Info.plist */, + ); + name = "Supporting Files"; + sourceTree = ""; + }; + 282DA4C31A699F5600A406E2 /* Third-Party Source */ = { + isa = PBXGroup; + children = ( + 282DA4C41A699F8A00A406E2 /* SwiftData.swift */, + ); + name = "Third-Party Source"; + sourceTree = ""; + }; + 282DA4D91A69A2C400A406E2 /* SQL */ = { + isa = PBXGroup; + children = ( + 0B2770441A89276900CE7692 /* BookmarksSqlite.swift */, + 4A59BF61F0D956D2A1BDAE29 /* BrowserDB.swift */, + 0BF42D431A7C31EE00889E28 /* FaviconsTable.swift */, + 4A59B3F1AAD619686DB9450C /* GenericTable.swift */, + 4A59BB07A2812FE4B516FD30 /* HistoryTable.swift */, + 0BF42D411A7C31E300889E28 /* JoinedFaviconsHistoryTable.swift */, + 4A59BCB8D38CE6E6F5F5F2DE /* JoinedHistoryVisitsTable.swift */, + E477261D1A8A7EDA00FC058B /* ReadingListTable.swift */, + 0BAE69551A7E1FD100B3609D /* SchemaTable.swift */, + 0BF42D3F1A7C18CD00889E28 /* SQLFavicons.swift */, + 0BA4977A1A7B0941004C8E17 /* SQLiteHistory.swift */, + 0B3060301A8151B70085B8BC /* SQLitePasswords.swift */, + E477261A1A8A7DE500FC058B /* SQLiteReadingList.swift */, + 4A59B22E909B6CAC95DBE879 /* VisitsTable.swift */, + ); + name = SQL; + sourceTree = ""; + }; + 282DA5201A69BCB700A406E2 /* modules */ = { + isa = PBXGroup; + children = ( + 282DA51F1A69BCB700A406E2 /* module.modulemap */, + ); + path = modules; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXHeadersBuildPhase section */ + 282DA4921A699E0300A406E2 /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + 282DA49B1A699E0300A406E2 /* Storage.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXHeadersBuildPhase section */ + +/* Begin PBXNativeTarget section */ + 282DA4941A699E0300A406E2 /* Storage */ = { + isa = PBXNativeTarget; + buildConfigurationList = 282DA4AB1A699E0300A406E2 /* Build configuration list for PBXNativeTarget "Storage" */; + buildPhases = ( + 282DA4901A699E0300A406E2 /* Sources */, + 282DA4911A699E0300A406E2 /* Frameworks */, + 282DA4921A699E0300A406E2 /* Headers */, + 282DA4931A699E0300A406E2 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Storage; + productName = Storage; + productReference = 282DA4951A699E0300A406E2 /* Storage.framework */; + productType = "com.apple.product-type.framework"; + }; + 282DA49F1A699E0300A406E2 /* StorageTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 282DA4AE1A699E0300A406E2 /* Build configuration list for PBXNativeTarget "StorageTests" */; + buildPhases = ( + 282DA49C1A699E0300A406E2 /* Sources */, + 282DA49D1A699E0300A406E2 /* Frameworks */, + 282DA49E1A699E0300A406E2 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 282DA4A31A699E0300A406E2 /* PBXTargetDependency */, + ); + name = StorageTests; + productName = StorageTests; + productReference = 282DA4A01A699E0300A406E2 /* StorageTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 282DA48C1A699E0300A406E2 /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 0610; + ORGANIZATIONNAME = Mozilla; + TargetAttributes = { + 282DA4941A699E0300A406E2 = { + CreatedOnToolsVersion = 6.1.1; + }; + 282DA49F1A699E0300A406E2 = { + CreatedOnToolsVersion = 6.1.1; + }; + }; + }; + buildConfigurationList = 282DA48F1A699E0300A406E2 /* Build configuration list for PBXProject "Storage" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + en, + ); + mainGroup = 282DA48B1A699E0300A406E2; + productRefGroup = 282DA4961A699E0300A406E2 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 282DA4941A699E0300A406E2 /* Storage */, + 282DA49F1A699E0300A406E2 /* StorageTests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 282DA4931A699E0300A406E2 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 282DA49E1A699E0300A406E2 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 282DA4901A699E0300A406E2 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 0B30602F1A80338F0085B8BC /* Passwords.swift in Sources */, + 0BA497821A7B094B004C8E17 /* Bookmarks.swift in Sources */, + 282DA4C51A699F8A00A406E2 /* SwiftData.swift in Sources */, + E47726181A8A794B00FC058B /* ReadingList.swift in Sources */, + E477261E1A8A7EDA00FC058B /* ReadingListTable.swift in Sources */, + 0BA4977E1A7B0941004C8E17 /* SQLiteHistory.swift in Sources */, + 282DA5211A69BCB700A406E2 /* module.modulemap in Sources */, + 0BA4978C1A7B0E44004C8E17 /* Cursor.swift in Sources */, + 0BF42D401A7C18CD00889E28 /* SQLFavicons.swift in Sources */, + 0B2770451A89276900CE7692 /* BookmarksSqlite.swift in Sources */, + 282DA4D71A69A28000A406E2 /* Site.swift in Sources */, + 0B2770DD1A8AD83200CE7692 /* Bytes.swift in Sources */, + 282DA4D41A69A24300A406E2 /* History.swift in Sources */, + 0BA497831A7B094B004C8E17 /* Visit.swift in Sources */, + 0BAE69561A7E1FD100B3609D /* SchemaTable.swift in Sources */, + 4A59BB4F36A20F30229034C6 /* VisitsTable.swift in Sources */, + 0BF42D421A7C31E300889E28 /* JoinedFaviconsHistoryTable.swift in Sources */, + 0BF42D441A7C31EE00889E28 /* FaviconsTable.swift in Sources */, + E477261B1A8A7DE500FC058B /* SQLiteReadingList.swift in Sources */, + 4A59B21913DCFA81C7CD9C91 /* GenericTable.swift in Sources */, + 0BF42D3E1A7C148F00889E28 /* Favicons.swift in Sources */, + 0BA4978D1A7B0E55004C8E17 /* FileAccessor.swift in Sources */, + 4A59BD24F6210866F59EF0F6 /* JoinedHistoryVisitsTable.swift in Sources */, + 0B3060311A8151B70085B8BC /* SQLitePasswords.swift in Sources */, + 4A59BFF7C37812C7955B9E79 /* HistoryTable.swift in Sources */, + 4A59BAEB154EA6E1B621E067 /* BrowserDB.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 282DA49C1A699E0300A406E2 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 282DA4D21A69A14500A406E2 /* FileAccessor.swift in Sources */, + 0BCFBBB81A776A200087E26D /* MockFiles.swift in Sources */, + 0BF42D4B1A7C55F300889E28 /* JoinedFaviconsHistoryTable.swift in Sources */, + E4F43AC21A8CF9A200ACAC05 /* TestSQLiteReadingList.swift in Sources */, + 0BCFBBB51A7769B70087E26D /* VisitsTable.swift in Sources */, + 282DA4A81A699E0300A406E2 /* StorageTests.swift in Sources */, + 0BCFBBB41A7769A30087E26D /* JoinedHistoryVisitsTable.swift in Sources */, + 0BF42D461A7C52C800889E28 /* TestFaviconsTable.swift in Sources */, + 0BCFBBAF1A77650E0087E26D /* GenericTable.swift in Sources */, + E477261C1A8A7DE500FC058B /* SQLiteReadingList.swift in Sources */, + 282DA4C61A699F8A00A406E2 /* SwiftData.swift in Sources */, + 0B2770481A893C4700CE7692 /* BookmarksSqlite.swift in Sources */, + 0B2770471A893C2C00CE7692 /* TestBookmarks.swift in Sources */, + 0BF42D471A7C558000889E28 /* Visit.swift in Sources */, + E47726191A8A794B00FC058B /* ReadingList.swift in Sources */, + E47726211A8A87F400FC058B /* TestReadingListTable.swift in Sources */, + 282DA4C21A699EF200A406E2 /* Cursor.swift in Sources */, + 0B2770491A893C9500CE7692 /* Bookmarks.swift in Sources */, + 0BF42D481A7C55D200889E28 /* Site.swift in Sources */, + 0B2770E91A8AD85000CE7692 /* Bytes.swift in Sources */, + 4A59BA1E93D5603E198EDCB9 /* HistoryTable.swift in Sources */, + 0BAE69571A7E209200B3609D /* SchemaTable.swift in Sources */, + 4A59BF0BBF50FC218A6D1143 /* BrowserDB.swift in Sources */, + E477261F1A8A7EDA00FC058B /* ReadingListTable.swift in Sources */, + 4A59B334A243792894D6E614 /* TestHistoryTable.swift in Sources */, + 4A59BC6033BD93D1DC36E18F /* TestJoinedHistoryVisits.swift in Sources */, + 0BF42D4D1A7CC4ED00889E28 /* TestJoinedFaviconTable.swift in Sources */, + 0BF42D4A1A7C55F000889E28 /* FaviconsTable.swift in Sources */, + 4A59BF2F408CDEF8D50AF9D3 /* TestVisitsTable.swift in Sources */, + 0BAE69591A7EEF8E00B3609D /* TestTableTable.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 282DA4A31A699E0300A406E2 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 282DA4941A699E0300A406E2 /* Storage */; + targetProxy = 282DA4A21A699E0300A406E2 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin XCBuildConfiguration section */ + 282DA4A91A699E0300A406E2 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_SYMBOLS_PRIVATE_EXTERN = NO; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 8.1; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_INCLUDE_PATHS = "$(PROJECT_DIR)/modules"; + SWIFT_OBJC_BRIDGING_HEADER = ""; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + TARGETED_DEVICE_FAMILY = "1,2"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Debug; + }; + 282DA4AA1A699E0300A406E2 /* FennecAurora */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_IDENTITY = "iPhone Distribution"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; + CONFIGURATION_BUILD_DIR = "$(BUILD_DIR)/Release$(EFFECTIVE_PLATFORM_NAME)"; + COPY_PHASE_STRIP = YES; + CURRENT_PROJECT_VERSION = 1; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 8.1; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SWIFT_INCLUDE_PATHS = "$(PROJECT_DIR)/modules"; + SWIFT_OBJC_BRIDGING_HEADER = ""; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = FennecAurora; + }; + 282DA4AC1A699E0300A406E2 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + APPLICATION_EXTENSION_API_ONLY = YES; + CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES = YES; + CODE_SIGN_IDENTITY = "iPhone Developer"; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "/Users/rnewman/moz/git/firefox-ios/SQLite/build/Debug-iphoneos", + "$(USER_LIBRARY_DIR)/Developer/Xcode/DerivedData/Client-fhgufntxvuxtwfbmkckrmnagytst/Build/Products/Debug-iphoneos", + ); + INFOPLIST_FILE = Storage/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SWIFT_INCLUDE_PATHS = "$(PROJECT_DIR)/modules"; + }; + name = Debug; + }; + 282DA4AD1A699E0300A406E2 /* FennecAurora */ = { + isa = XCBuildConfiguration; + buildSettings = { + APPLICATION_EXTENSION_API_ONLY = YES; + CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES = YES; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "/Users/rnewman/moz/git/firefox-ios/SQLite/build/Debug-iphoneos", + "$(USER_LIBRARY_DIR)/Developer/Xcode/DerivedData/Client-fhgufntxvuxtwfbmkckrmnagytst/Build/Products/Debug-iphoneos", + ); + INFOPLIST_FILE = Storage/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SWIFT_INCLUDE_PATHS = "$(PROJECT_DIR)/modules"; + }; + name = FennecAurora; + }; + 282DA4AF1A699E0300A406E2 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + FRAMEWORK_SEARCH_PATHS = ( + "$(SDKROOT)/Developer/Library/Frameworks", + "$(inherited)", + ); + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + INFOPLIST_FILE = StorageTests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Debug; + }; + 282DA4B01A699E0300A406E2 /* FennecAurora */ = { + isa = XCBuildConfiguration; + buildSettings = { + FRAMEWORK_SEARCH_PATHS = ( + "$(SDKROOT)/Developer/Library/Frameworks", + "$(inherited)", + ); + INFOPLIST_FILE = StorageTests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = FennecAurora; + }; + E4D438F51A8D3197003FCF55 /* FennecNightly */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + CONFIGURATION_BUILD_DIR = "$(BUILD_DIR)/Release$(EFFECTIVE_PLATFORM_NAME)"; + COPY_PHASE_STRIP = YES; + CURRENT_PROJECT_VERSION = 1; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 8.1; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SWIFT_INCLUDE_PATHS = "$(PROJECT_DIR)/modules"; + SWIFT_OBJC_BRIDGING_HEADER = ""; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = FennecNightly; + }; + E4D438F61A8D3197003FCF55 /* FennecNightly */ = { + isa = XCBuildConfiguration; + buildSettings = { + APPLICATION_EXTENSION_API_ONLY = YES; + CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES = YES; + CODE_SIGN_IDENTITY = "iPhone Developer"; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "/Users/rnewman/moz/git/firefox-ios/SQLite/build/Debug-iphoneos", + "$(USER_LIBRARY_DIR)/Developer/Xcode/DerivedData/Client-fhgufntxvuxtwfbmkckrmnagytst/Build/Products/Debug-iphoneos", + ); + INFOPLIST_FILE = Storage/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SWIFT_INCLUDE_PATHS = "$(PROJECT_DIR)/modules"; + }; + name = FennecNightly; + }; + E4D438F71A8D3197003FCF55 /* FennecNightly */ = { + isa = XCBuildConfiguration; + buildSettings = { + FRAMEWORK_SEARCH_PATHS = ( + "$(SDKROOT)/Developer/Library/Frameworks", + "$(inherited)", + ); + INFOPLIST_FILE = StorageTests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = FennecNightly; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 282DA48F1A699E0300A406E2 /* Build configuration list for PBXProject "Storage" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 282DA4A91A699E0300A406E2 /* Debug */, + 282DA4AA1A699E0300A406E2 /* FennecAurora */, + E4D438F51A8D3197003FCF55 /* FennecNightly */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = FennecAurora; + }; + 282DA4AB1A699E0300A406E2 /* Build configuration list for PBXNativeTarget "Storage" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 282DA4AC1A699E0300A406E2 /* Debug */, + 282DA4AD1A699E0300A406E2 /* FennecAurora */, + E4D438F61A8D3197003FCF55 /* FennecNightly */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = FennecAurora; + }; + 282DA4AE1A699E0300A406E2 /* Build configuration list for PBXNativeTarget "StorageTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 282DA4AF1A699E0300A406E2 /* Debug */, + 282DA4B01A699E0300A406E2 /* FennecAurora */, + E4D438F71A8D3197003FCF55 /* FennecNightly */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = FennecAurora; + }; +/* End XCConfigurationList section */ + }; + rootObject = 282DA48C1A699E0300A406E2 /* Project object */; +} diff --git a/mobile/ios/Storage/SuggestedSites.swift b/mobile/ios/Storage/SuggestedSites.swift new file mode 100644 index 0000000000..b4a6f937e6 --- /dev/null +++ b/mobile/ios/Storage/SuggestedSites.swift @@ -0,0 +1,46 @@ +/* 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 UIKit +import Shared + +open class SuggestedSite: Site { + override open var tileURL: URL { + return URL(string: url as String) ?? URL(string: "about:blank")! + } + + let trackingId: Int + init(data: SuggestedSiteData) { + self.trackingId = data.trackingId + super.init(url: data.url, title: data.title, bookmarked: nil) + self.guid = "default" + data.title // A guid is required in the case the site might become a pinned site + } +} + +public let SuggestedSites: SuggestedSitesCursor = SuggestedSitesCursor() + +open class SuggestedSitesCursor: ArrayCursor { + fileprivate init() { + let locale = Locale.current + let sites = DefaultSuggestedSites.sites[locale.identifier] ?? + DefaultSuggestedSites.sites["default"]! as Array + let tiles = sites.map({ data -> SuggestedSite in + var site = data + if let domainMap = DefaultSuggestedSites.urlMap[data.url], let localizedURL = domainMap[locale.identifier] { + site.url = localizedURL + } + return SuggestedSite(data: site) + }) + super.init(data: tiles, status: .success, statusMessage: "Loaded") + } +} + +public struct SuggestedSiteData { + var url: String + var bgColor: String + var imageUrl: String + var faviconUrl: String + var trackingId: Int + var title: String +} diff --git a/mobile/ios/Storage/SyncQueue.swift b/mobile/ios/Storage/SyncQueue.swift new file mode 100644 index 0000000000..643432ecce --- /dev/null +++ b/mobile/ios/Storage/SyncQueue.swift @@ -0,0 +1,65 @@ +/* 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 Shared +import Deferred +import SwiftyJSON + +public struct SyncCommand: Equatable { + public let value: String + public var commandID: Int? + public var clientGUID: GUID? + + let version: String? + + public init(value: String) { + self.value = value + self.version = nil + self.commandID = nil + self.clientGUID = nil + } + + public init(id: Int, value: String) { + self.value = value + self.version = nil + self.commandID = id + self.clientGUID = nil + } + + public init(id: Int?, value: String, clientGUID: GUID?) { + self.value = value + self.version = nil + self.clientGUID = clientGUID + self.commandID = id + } + + /** + * Sent displayURI commands include the sender client GUID. + */ + public static func displayURIFromShareItem(_ shareItem: ShareItem, asClient sender: GUID) -> SyncCommand { + let jsonObj: [String: Any] = [ + "command": "displayURI", + "args": [shareItem.url, sender, shareItem.title ?? ""] + ] + return SyncCommand(value: JSON(object: jsonObj).stringValue()!) + } + + public func withClientGUID(_ clientGUID: String?) -> SyncCommand { + return SyncCommand(id: self.commandID, value: self.value, clientGUID: clientGUID) + } +} + +public func ==(lhs: SyncCommand, rhs: SyncCommand) -> Bool { + return lhs.value == rhs.value +} + +public protocol SyncCommands { + func deleteCommands() -> Success + func deleteCommands(_ clientGUID: GUID) -> Success + + func getCommands() -> Deferred> + + func insertCommand(_ command: SyncCommand, forClients clients: [RemoteClient]) -> Deferred> + func insertCommands(_ commands: [SyncCommand], forClients clients: [RemoteClient]) -> Deferred> +} diff --git a/mobile/ios/Storage/Syncable.swift b/mobile/ios/Storage/Syncable.swift new file mode 100644 index 0000000000..8c806f9fb6 --- /dev/null +++ b/mobile/ios/Storage/Syncable.swift @@ -0,0 +1,24 @@ +/* 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 Shared + +/** + * This exists to allow resets after meta/global or crypto/key changes. + * + * 'Reset' in this case means that timestamps and progress tracking are + * discarded: this storage is reconfigured such that all data will be + * reuploaded, and all data will be re-merged as necessary. + * + * This protocol is primarily consumed by `ResettableSynchronizer`, and + * is invoked when a significant server change is observed — changed keys, + * changed engine elections or syncIDs, or a node reassignment. + */ +public protocol ResettableSyncStorage { + func resetClient() -> Success +} + +public protocol AccountRemovalDelegate { + func onRemovedAccount() -> Success +} diff --git a/mobile/ios/Storage/ThirdParty/SwiftData.swift b/mobile/ios/Storage/ThirdParty/SwiftData.swift new file mode 100644 index 0000000000..ade5651fd7 --- /dev/null +++ b/mobile/ios/Storage/ThirdParty/SwiftData.swift @@ -0,0 +1,1488 @@ +/* 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/. */ + +/* + * This is a heavily modified version of SwiftData.swift by Ryan Fowler + * This has been enhanced to support custom files, correct binding, versioning, + * and a streaming results via Cursors. The API has also been changed to use NSError, Cursors, and + * to force callers to request a connection before executing commands. Database creation helpers, savepoint + * helpers, image support, and other features have been removed. + */ + +// SwiftData.swift +// +// Copyright (c) 2014 Ryan Fowler +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation +import UIKit +import Deferred +import Shared +import XCGLogger + +private let DatabaseBusyTimeout: Int32 = 3 * 1000 +private let log = Logger.syncLogger + +public class DBOperationCancelled : MaybeErrorType { + public var description: String { + return "Database operation cancelled" + } +} + +class DeferredDBOperation: Deferred, Cancellable { + fileprivate var dispatchWorkItem: DispatchWorkItem? + private var _running = false + + func cancel() { + objc_sync_enter(self) + defer { objc_sync_exit(self) } + dispatchWorkItem?.cancel() + } + + var cancelled: Bool { + objc_sync_enter(self) + defer { objc_sync_exit(self) } + return dispatchWorkItem?.isCancelled ?? false + } + + var running: Bool { + get { + objc_sync_enter(self) + defer { objc_sync_exit(self) } + return _running + } + set { + objc_sync_enter(self) + defer { objc_sync_exit(self) } + _running = newValue + } + } +} + +enum SQLiteDBConnectionCreatedResult { + case success + case failure + case needsRecovery +} + +// SQLite standard error codes when the DB file is locked, busy or the disk is +// full. These error codes indicate that any issues with writing to the database +// are temporary and we should not wipe out and re-create the database file when +// we encounter them. +enum SQLiteDBRecoverableError: Int { + case Busy = 5 + case Locked = 6 + case ReadOnly = 8 + case IOErr = 10 + case Full = 13 +} + +/** + * Handle to a SQLite database. + * Each instance holds a single connection that is shared across all queries. + */ +open class SwiftData { + let filename: String + let schema: Schema + let files: FileAccessor + + static var EnableWAL = true + static var EnableForeignKeys = true + + /// Used to keep track of the corrupted databases we've logged. + static var corruptionLogsWritten = Set() + + /// Used for testing. + static var ReuseConnections = true + + /// For thread-safe access to the shared connection. + fileprivate let sharedConnectionQueue: DispatchQueue + + /// Shared connection to this database. + fileprivate var sharedConnection: ConcreteSQLiteDBConnection? + fileprivate var key: String? + fileprivate var prevKey: String? + + /// A simple state flag to track whether we should accept new connection requests. + /// If a connection request is made while the database is closed, a + /// FailedSQLiteDBConnection will be returned. + fileprivate(set) var closed = false + + init(filename: String, key: String? = nil, prevKey: String? = nil, schema: Schema, files: FileAccessor) { + self.filename = filename + self.key = key + self.prevKey = prevKey + self.schema = schema + self.files = files + + self.sharedConnectionQueue = DispatchQueue(label: "SwiftData queue: \(filename)", attributes: []) + + // Ensure that multi-thread mode is enabled by default. + // See https://www.sqlite.org/threadsafe.html + assert(sqlite3_threadsafe() == 2) + } + + /** + * The real meat of all the execute methods. This is used internally to open and + * close a database connection and run a block of code inside it. + */ + func withConnection(_ flags: SwiftData.Flags, synchronous: Bool = false, _ callback: @escaping (_ connection: SQLiteDBConnection) throws -> T) -> Deferred> { + let deferred = DeferredDBOperation>() + + let queue = self.sharedConnectionQueue + + func doWork() { + if deferred.cancelled { + deferred.fill(Maybe(failure: DBOperationCancelled())) + return + } + + deferred.running = true + defer { + deferred.running = false + } + + if !self.closed && self.sharedConnection == nil { + self.sharedConnection = ConcreteSQLiteDBConnection(filename: self.filename, flags: SwiftData.Flags.readWriteCreate.toSQL(), key: self.key, prevKey: self.prevKey, schema: self.schema, files: self.files) + } + + guard let connection = SwiftData.ReuseConnections ? self.sharedConnection : + ConcreteSQLiteDBConnection(filename: self.filename, flags: flags.toSQL(), key: self.key, prevKey: self.prevKey, schema: self.schema, files: self.files) else { + do { + _ = try callback(FailedSQLiteDBConnection()) + + deferred.fill(Maybe(failure: NSError(domain: "mozilla", code: 0, userInfo: [NSLocalizedDescriptionKey: "Could not create a connection"]))) + } catch let err as NSError { + deferred.fill(Maybe(failure: DatabaseError(err: err))) + } + return + } + + do { + let result = try callback(connection) + deferred.fill(Maybe(success: result)) + } catch let err as NSError { + deferred.fill(Maybe(failure: DatabaseError(err: err))) + } + } + + let work = DispatchWorkItem { doWork() } + deferred.dispatchWorkItem = work + + if synchronous { + queue.sync(execute: work) + } else { + queue.async(execute: work) + } + + return deferred + } + + /** + * Helper for opening a connection, starting a transaction, and then running a block of code inside it. + * The code block can return true if the transaction should be committed. False if we should roll back. + */ + func transaction(synchronous: Bool = false, _ transactionClosure: @escaping (_ connection: SQLiteDBConnection) throws -> T) -> Deferred> { + return withConnection(SwiftData.Flags.readWriteCreate, synchronous: synchronous) { connection in + try connection.transaction(transactionClosure) + } + } + + /// Don't use this unless you know what you're doing. The deinitializer should be used to achieve refcounting semantics. + /// The shutdown is *sync*, meaning the queue will complete the current db operations before closing. + /// If an operation is queued with an open connection, it will execute before this runs. + func forceClose() { + sharedConnectionQueue.sync { + self.closed = true + self.sharedConnection = nil + } + } + + /// Reopens a database that had previously been force-closed. + /// Does nothing if this database is already open. + func reopenIfClosed() { + sharedConnectionQueue.sync { + self.closed = false + } + } + + public func cancel() { + if let db = sharedConnection?.sqliteDB, !closed { + sqlite3_interrupt(db) + } + } + + public func suspendQueue() { + sharedConnectionQueue.suspend() + } + + public func resumeQueue() { + sharedConnectionQueue.resume() + } + + public enum Flags { + case readOnly + case readWrite + case readWriteCreate + + fileprivate func toSQL() -> Int32 { + switch self { + case .readOnly: + return SQLITE_OPEN_READONLY + case .readWrite: + return SQLITE_OPEN_READWRITE + case .readWriteCreate: + return SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE + } + } + } +} + +/** + * Wrapper class for a SQLite statement. + * This class helps manage the statement lifecycle. By holding a reference to the SQL connection, we ensure + * the connection is never deinitialized while the statement is active. This class is responsible for + * finalizing the SQL statement once it goes out of scope. + */ +private class SQLiteDBStatement { + var pointer: OpaquePointer? + fileprivate let connection: ConcreteSQLiteDBConnection + + init(connection: ConcreteSQLiteDBConnection, query: String, args: [Any?]?) throws { + self.connection = connection + + let status = sqlite3_prepare_v2(connection.sqliteDB, query, -1, &pointer, nil) + if status != SQLITE_OK { + throw connection.createErr("During: SQL Prepare \(query)", status: Int(status)) + } + + if let args = args, + let bindError = bind(args) { + throw bindError + } + } + + /// Binds arguments to the statement. + fileprivate func bind(_ objects: [Any?]) -> NSError? { + let count = Int(sqlite3_bind_parameter_count(pointer)) + if count < objects.count { + return connection.createErr("During: Bind", status: 202) + } + if count > objects.count { + return connection.createErr("During: Bind", status: 201) + } + + for (index, obj) in objects.enumerated() { + var status: Int32 = SQLITE_OK + + // Doubles also pass obj as Int, so order is important here. + if obj is Double { + status = sqlite3_bind_double(pointer, Int32(index+1), obj as! Double) + } else if obj is Int { + status = sqlite3_bind_int(pointer, Int32(index+1), Int32(obj as! Int)) + } else if obj is Bool { + status = sqlite3_bind_int(pointer, Int32(index+1), (obj as! Bool) ? 1 : 0) + } else if obj is String { + typealias CFunction = @convention(c) (UnsafeMutableRawPointer?) -> Void + let transient = unsafeBitCast(-1, to: CFunction.self) + status = sqlite3_bind_text(pointer, Int32(index+1), (obj as! String).cString(using: String.Encoding.utf8)!, -1, transient) + } else if obj is Data { + status = sqlite3_bind_blob(pointer, Int32(index+1), ((obj as! Data) as NSData).bytes, -1, nil) + } else if obj is Date { + let timestamp = (obj as! Date).timeIntervalSince1970 + status = sqlite3_bind_double(pointer, Int32(index+1), timestamp) + } else if obj is UInt64 { + status = sqlite3_bind_double(pointer, Int32(index+1), Double(obj as! UInt64)) + } else if obj == nil { + status = sqlite3_bind_null(pointer, Int32(index+1)) + } + + if status != SQLITE_OK { + return connection.createErr("During: Bind", status: Int(status)) + } + } + + return nil + } + + func close() { + if nil != self.pointer { + sqlite3_finalize(self.pointer) + self.pointer = nil + } + } + + deinit { + if nil != self.pointer { + sqlite3_finalize(self.pointer) + } + } +} + +public protocol SQLiteDBConnection { + var lastInsertedRowID: Int { get } + var numberOfRowsModified: Int { get } + var version: Int { get } + + func executeChange(_ sqlStr: String) throws -> Void + func executeChange(_ sqlStr: String, withArgs args: Args?) throws -> Void + + func executeQuery(_ sqlStr: String, factory: @escaping ((SDRow) -> T)) -> Cursor + func executeQuery(_ sqlStr: String, factory: @escaping ((SDRow) -> T), withArgs args: Args?) -> Cursor + func executeQueryUnsafe(_ sqlStr: String, factory: @escaping ((SDRow) -> T), withArgs args: Args?) -> Cursor + + func transaction(_ transactionClosure: @escaping (_ connection: SQLiteDBConnection) throws -> T) throws -> T + + func interrupt() + func checkpoint() + func checkpoint(_ mode: Int32) + func vacuum() throws -> Void + func setVersion(_ version: Int) throws -> Void +} + +// Represents a failure to open. +class FailedSQLiteDBConnection: SQLiteDBConnection { + var lastInsertedRowID: Int = 0 + var numberOfRowsModified: Int = 0 + var version: Int = 0 + + fileprivate func fail(_ str: String) -> NSError { + return NSError(domain: "mozilla", code: 0, userInfo: [NSLocalizedDescriptionKey: str]) + } + + func executeChange(_ sqlStr: String, withArgs args: Args?) throws -> Void { + throw fail("Non-open connection; can't execute change.") + } + func executeChange(_ sqlStr: String) throws -> Void { + throw fail("Non-open connection; can't execute change.") + } + + func executeQuery(_ sqlStr: String, factory: @escaping ((SDRow) -> T)) -> Cursor { + return Cursor(err: fail("Non-open connection; can't execute query.")) + } + func executeQuery(_ sqlStr: String, factory: @escaping ((SDRow) -> T), withArgs args: Args?) -> Cursor { + return Cursor(err: fail("Non-open connection; can't execute query.")) + } + func executeQueryUnsafe(_ sqlStr: String, factory: @escaping ((SDRow) -> T), withArgs args: Args?) -> Cursor { + return Cursor(err: fail("Non-open connection; can't execute query.")) + } + + func transaction(_ transactionClosure: @escaping (_ connection: SQLiteDBConnection) throws -> T) throws -> T { + throw fail("Non-open connection; can't start transaction.") + } + + func interrupt() {} + func checkpoint() {} + func checkpoint(_ mode: Int32) {} + func vacuum() throws -> Void { + throw fail("Non-open connection; can't vacuum.") + } + func setVersion(_ version: Int) throws -> Void { + throw fail("Non-open connection; can't set user_version.") + } +} + +open class ConcreteSQLiteDBConnection: SQLiteDBConnection { + open var lastInsertedRowID: Int { + return Int(sqlite3_last_insert_rowid(sqliteDB)) + } + + open var numberOfRowsModified: Int { + return Int(sqlite3_changes(sqliteDB)) + } + + open var version: Int { + return pragma("user_version", factory: IntFactory) ?? 0 + } + + fileprivate var sqliteDB: OpaquePointer? + fileprivate let filename: String + fileprivate let schema: Schema + fileprivate let files: FileAccessor + + fileprivate let debug_enabled = false + + init?(filename: String, flags: Int32, key: String? = nil, prevKey: String? = nil, schema: Schema, files: FileAccessor) { + log.debug("Opening connection to \(filename).") + + self.filename = filename + self.schema = schema + self.files = files + + func doOpen() -> Bool { + if let failure = openWithFlags(flags) { + log.warning("Opening connection to \(filename) failed: \(failure).") + return false + } + + if key == nil && prevKey == nil { + do { + try self.prepareCleartext() + } catch { + return false + } + } else { + do { + try self.prepareEncrypted(flags, key: key, prevKey: prevKey) + } catch { + return false + } + } + + return true + } + + // If we cannot even open the database file, return `nil` to force SwiftData + // into using a `FailedSQLiteDBConnection` so we can retry opening again later. + if !doOpen() { + let extra = ["filename" : filename] + Sentry.shared.sendWithStacktrace(message: "Cannot open a database connection.", tag: SentryTag.swiftData, severity: .error, extra: extra) + return nil + } + + // Now that we've successfully opened a connection to the database file, call + // `prepareSchema()`. If it succeeds, our work here is done. If it returns + // `.failure`, this means there was a temporary error preventing us from initing + // the schema (e.g. SQLITE_BUSY, SQLITE_LOCK, SQLITE_FULL); so we return `nil` to + // force SwiftData into using a `FailedSQLiteDBConnection` to retry preparing the + // schema again later. However, if it returns `.needsRecovery`, this means there + // was a permanent error preparing the schema and we need to move the current + // database file to a backup location and start over with a brand new one. + switch self.prepareSchema() { + case .success: + log.debug("Database successfully created or updated.") + case .failure: + Sentry.shared.sendWithStacktrace(message: "Failed to create or update the database schema.", tag: SentryTag.swiftData, severity: .error) + return nil + case .needsRecovery: + Sentry.shared.sendWithStacktrace(message: "Database schema cannot be created or updated due to an unrecoverable error.", tag: SentryTag.swiftData, severity: .error) + + // We need to close this new connection before we can move the database file to + // its backup location. If we cannot even close the connection, something has + // gone really wrong. In that case, bail out and return `nil` to force SwiftData + // into using a `FailedSQLiteDBConnection` so we can retry again later. + if let error = self.closeCustomConnection(immediately: true) { + Sentry.shared.sendWithStacktrace(message: "Cannot close the database connection to begin recovery.", tag: SentryTag.swiftData, severity: .error, description: error.localizedDescription) + return nil + } + + // Move the current database file to its backup location. + self.moveDatabaseFileToBackupLocation() + + // If we cannot open the *new* database file (which shouldn't happen), return + // `nil` to force SwiftData into using a `FailedSQLiteDBConnection` so we can + // retry opening again later. + if !doOpen() { + log.error("Cannot re-open a database connection to the new database file to begin recovery.") + Sentry.shared.sendWithStacktrace(message: "Cannot re-open a database connection to the new database file to begin recovery.", tag: SentryTag.swiftData, severity: .error) + return nil + } + + // Notify the world that we re-created the database schema. This allows us to + // reset Sync and start over in the case of corruption. + defer { + let baseFilename = URL(fileURLWithPath: self.filename).lastPathComponent + NotificationCenter.default.post(name: NotificationDatabaseWasRecreated, object: baseFilename) + } + + // Now that we've got a brand new database file, let's call `prepareSchema()` on + // it to re-create the schema. Again, if this fails (it shouldn't), return `nil` + // to force SwiftData into using a `FailedSQLiteDBConnection` so we can retry + // again later. + if self.prepareSchema() != .success { + log.error("Cannot re-create the schema in the new database file to complete recovery.") + Sentry.shared.sendWithStacktrace(message: "Cannot re-create the schema in the new database file to complete recovery.", tag: SentryTag.swiftData, severity: .error) + return nil + } + } + } + + deinit { + log.debug("deinit: closing connection on thread \(Thread.current).") + self.closeCustomConnection() + } + + fileprivate func setKey(_ key: String?) -> NSError? { + sqlite3_key(sqliteDB, key ?? "", Int32((key ?? "").characters.count)) + let cursor = executeQuery("SELECT count(*) FROM sqlite_master;", factory: IntFactory, withArgs: nil as Args?) + if cursor.status != .success { + return NSError(domain: "mozilla", code: 0, userInfo: [NSLocalizedDescriptionKey: "Invalid key"]) + } + return nil + } + + fileprivate func reKey(_ oldKey: String?, newKey: String?) -> NSError? { + sqlite3_key(sqliteDB, oldKey ?? "", Int32((oldKey ?? "").characters.count)) + sqlite3_rekey(sqliteDB, newKey ?? "", Int32((newKey ?? "").characters.count)) + // Check that the new key actually works + sqlite3_key(sqliteDB, newKey ?? "", Int32((newKey ?? "").characters.count)) + let cursor = executeQuery("SELECT count(*) FROM sqlite_master;", factory: IntFactory, withArgs: nil as Args?) + if cursor.status != .success { + return NSError(domain: "mozilla", code: 0, userInfo: [NSLocalizedDescriptionKey: "Rekey failed"]) + } + + return nil + } + + public func setVersion(_ version: Int) throws -> Void { + try executeChange("PRAGMA user_version = \(version)") + } + + public func interrupt() { + log.debug("Interrupt") + sqlite3_interrupt(sqliteDB) + } + + fileprivate func pragma(_ pragma: String, expected: T?, factory: @escaping (SDRow) -> T, message: String) throws { + let cursorResult = self.pragma(pragma, factory: factory) + if cursorResult != expected { + log.error("\(message): \(cursorResult.debugDescription), \(expected.debugDescription)") + throw NSError(domain: "mozilla", code: 0, userInfo: [NSLocalizedDescriptionKey: "PRAGMA didn't return expected output: \(message)."]) + } + } + + fileprivate func pragma(_ pragma: String, factory: @escaping (SDRow) -> T) -> T? { + let cursor = executeQueryUnsafe("PRAGMA \(pragma)", factory: factory, withArgs: [] as Args) + defer { cursor.close() } + return cursor[0] + } + + fileprivate func prepareShared() { + if SwiftData.EnableForeignKeys { + let _ = pragma("foreign_keys=ON", factory: IntFactory) + } + + // Retry queries before returning locked errors. + sqlite3_busy_timeout(self.sqliteDB, DatabaseBusyTimeout) + } + + fileprivate func prepareEncrypted(_ flags: Int32, key: String?, prevKey: String? = nil) throws { + // Setting the key needs to be the first thing done with the database. + if let _ = setKey(key) { + if let err = closeCustomConnection(immediately: true) { + log.error("Couldn't close connection: \(err). Failing to open.") + throw err + } + if let err = openWithFlags(flags) { + Sentry.shared.sendWithStacktrace(message: "Error opening database with flags.", tag: SentryTag.swiftData, severity: .error, description: "\(err.code), \(err)") + throw err + } + if let err = reKey(prevKey, newKey: key) { + // Note: Don't log the error here as it may contain sensitive data. + Sentry.shared.sendWithStacktrace(message: "Unable to encrypt database.", tag: SentryTag.swiftData, severity: .error) + throw err + } + } + + if SwiftData.EnableWAL { + log.info("Enabling WAL mode.") + try pragma("journal_mode=WAL", expected: "wal", + factory: StringFactory, message: "WAL journal mode set") + } + + self.prepareShared() + } + + fileprivate func prepareCleartext() throws { + // If we just created the DB -- i.e., no tables have been created yet -- then + // we can set the page size right now and save a vacuum. + // + // For where these values come from, see Bug 1213623. + // + // Note that sqlcipher uses cipher_page_size instead, but we don't set that + // because it needs to be set from day one. + + let desiredPageSize = 32 * 1024 + let _ = pragma("page_size=\(desiredPageSize)", factory: IntFactory) + + let currentPageSize = pragma("page_size", factory: IntFactory) + + // This has to be done without WAL, so we always hop into rollback/delete journal mode. + if currentPageSize != desiredPageSize { + try pragma("journal_mode=DELETE", expected: "delete", + factory: StringFactory, message: "delete journal mode set") + + try pragma("page_size=\(desiredPageSize)", expected: nil, + factory: IntFactory, message: "Page size set") + + log.info("Vacuuming to alter database page size from \(currentPageSize ?? 0) to \(desiredPageSize).") + + do { + try vacuum() + log.debug("Vacuuming succeeded.") + } catch let err as NSError { + log.error("Vacuuming failed: \(err.localizedDescription).") + } + } + + if SwiftData.EnableWAL { + log.info("Enabling WAL mode.") + + let desiredPagesPerJournal = 16 + let desiredCheckpointSize = desiredPagesPerJournal * desiredPageSize + let desiredJournalSizeLimit = 3 * desiredCheckpointSize + + /* + * With whole-module-optimization enabled in Xcode 7.2 and 7.2.1, the + * compiler seems to eagerly discard these queries if they're simply + * inlined, causing a crash in `pragma`. + * + * Hackily hold on to them. + */ + let journalModeQuery = "journal_mode=WAL" + let autoCheckpointQuery = "wal_autocheckpoint=\(desiredPagesPerJournal)" + let journalSizeQuery = "journal_size_limit=\(desiredJournalSizeLimit)" + + try withExtendedLifetime(journalModeQuery, { + try pragma(journalModeQuery, expected: "wal", + factory: StringFactory, message: "WAL journal mode set") + }) + try withExtendedLifetime(autoCheckpointQuery, { + try pragma(autoCheckpointQuery, expected: desiredPagesPerJournal, + factory: IntFactory, message: "WAL autocheckpoint set") + }) + try withExtendedLifetime(journalSizeQuery, { + try pragma(journalSizeQuery, expected: desiredJournalSizeLimit, + factory: IntFactory, message: "WAL journal size limit set") + }) + } + + self.prepareShared() + } + + // Creates the database schema in a new database. + fileprivate func createSchema() -> Bool { + log.debug("Trying to create schema \(self.schema.name) at version \(self.schema.version)") + if !schema.create(self) { + // If schema couldn't be created, we'll bail without setting the `PRAGMA user_version`. + log.debug("Creation failed.") + return false + } + + do { + try setVersion(schema.version) + } catch let error as NSError { + log.error("Unable to set the schema version; \(error.localizedDescription)") + } + + return true + } + + // Updates the database schema in an existing database. + fileprivate func updateSchema() -> Bool { + log.debug("Trying to update schema \(self.schema.name) from version \(self.version) to \(self.schema.version)") + if !schema.update(self, from: self.version) { + // If schema couldn't be updated, we'll bail without setting the `PRAGMA user_version`. + log.debug("Updating failed.") + return false + } + + do { + try setVersion(schema.version) + } catch let error as NSError { + log.error("Unable to set the schema version; \(error.localizedDescription)") + } + + return true + } + + // Drops the database schema from an existing database. + fileprivate func dropSchema() -> Bool { + log.debug("Trying to drop schema \(self.schema.name)") + if !self.schema.drop(self) { + // If schema couldn't be dropped, we'll bail without setting the `PRAGMA user_version`. + log.debug("Dropping failed.") + return false + } + + do { + try setVersion(0) + } catch let error as NSError { + log.error("Unable to reset the schema version; \(error.localizedDescription)") + } + + return true + } + + // Checks if the database schema needs created or updated and acts accordingly. + // Calls to this function will be serialized to prevent race conditions when + // creating or updating the schema. + fileprivate func prepareSchema() -> SQLiteDBConnectionCreatedResult { + Sentry.shared.addAttributes(["dbSchema.\(schema.name).version": schema.version]) + + // Get the current schema version for the database. + let currentVersion = self.version + + // If the current schema version for the database matches the specified + // `Schema` version, no further action is necessary and we can bail out. + // NOTE: This assumes that we always use *ONE* `Schema` per database file + // since SQLite can only track a single value in `PRAGMA user_version`. + if currentVersion == schema.version { + log.debug("Schema \(self.schema.name) already exists at version \(self.schema.version). Skipping additional schema preparation.") + return .success + } + + // Set an attribute for Sentry to include with any future error/crash + // logs to indicate what schema version we're coming from and going to. + Sentry.shared.addAttributes(["dbUpgrade.\(self.schema.name).from": currentVersion, "dbUpgrade.\(self.schema.name).to": self.schema.version]) + + // This should not ever happen since the schema version should always be + // increasing whenever a structural change is made in an app update. + guard currentVersion <= schema.version else { + let errorString = "\(self.schema.name) cannot be downgraded from version \(currentVersion) to \(self.schema.version)." + Sentry.shared.sendWithStacktrace(message: "Schema cannot be downgraded.", tag: SentryTag.swiftData, severity: .error, description: errorString) + return .failure + } + + log.debug("Schema \(self.schema.name) needs created or updated from version \(currentVersion) to \(self.schema.version).") + + var success = true + + do { + success = try transaction { connection -> Bool in + log.debug("Create or update \(self.schema.name) version \(self.schema.version) on \(Thread.current.description).") + + // If `PRAGMA user_version` is zero, check if we can safely create the + // database schema from scratch. + if connection.version == 0 { + // Query for the existence of the `tableList` table to determine if we are + // migrating from an older DB version. + let sqliteMasterCursor = connection.executeQueryUnsafe("SELECT COUNT(*) AS number FROM sqlite_master WHERE type = 'table' AND name = 'tableList'", factory: IntFactory, withArgs: [] as Args) + + let tableListTableExists = sqliteMasterCursor[0] == 1 + sqliteMasterCursor.close() + + // If the `tableList` table doesn't exist, we can simply invoke + // `createSchema()` to create a brand new DB from scratch. + if !tableListTableExists { + log.debug("Schema \(self.schema.name) doesn't exist. Creating.") + success = self.createSchema() + return success + } + } + + Sentry.shared.send(message: "Attempting to update schema", tag: SentryTag.swiftData, severity: .info, description: "\(currentVersion) to \(self.schema.version).") + + // If we can't create a brand new schema from scratch, we must + // call `updateSchema()` to go through the update process. + if self.updateSchema() { + log.debug("Updated schema \(self.schema.name).") + success = true + return success + } + + // If we failed to update the schema, we'll drop everything from the DB + // and create everything again from scratch. Assuming our schema upgrade + // code is correct, this *shouldn't* happen. If it does, log it to Sentry. + Sentry.shared.sendWithStacktrace(message: "Update failed for schema. Dropping and re-creating.", tag: SentryTag.swiftData, severity: .error, description: "\(self.schema.name) from version \(currentVersion) to \(self.schema.version)") + + // If we can't even drop the schema here, something has gone really wrong, so + // return `false` which should force us into recovery. + if !self.dropSchema() { + Sentry.shared.sendWithStacktrace(message: "Unable to drop schema.", tag: SentryTag.swiftData, severity: .error, description: "\(self.schema.name) from version \(currentVersion).") + success = false + return success + } + + // Try to re-create the schema. If this fails, we are out of options and we'll + // return `false` which should force us into recovery. + success = self.createSchema() + return success + } + } catch let error as NSError { + // If we got an error trying to get a transaction, then we either bail out early and return + // `.failure` if we think we can retry later or return `.needsRecovery` if the error is not + // recoverable. + Sentry.shared.sendWithStacktrace(message: "Unable to get a transaction", tag: SentryTag.swiftData, severity: .error, description: "\(error.localizedDescription)") + + // Check if the error we got is recoverable (e.g. SQLITE_BUSY, SQLITE_LOCK, SQLITE_FULL). + // If so, just return `.failure` so we can retry preparing the schema again later. + if let _ = SQLiteDBRecoverableError(rawValue: error.code) { + return .failure + } + + // Otherwise, this is a non-recoverable error and we return `.needsRecovery` so the database + // file can be backed up and a new one will be created. + return .needsRecovery + } + + // If any of our operations inside the transaction failed, this also means we need to go through + // the recovery process to re-create the database from scratch. + if !success { + return .needsRecovery + } + + // No error means we're all good! \o/ + return .success + } + + fileprivate func moveDatabaseFileToBackupLocation() { + let baseFilename = URL(fileURLWithPath: filename).lastPathComponent + + // Attempt to make a backup as long as the database file still exists. + if files.exists(baseFilename) { + Sentry.shared.sendWithStacktrace(message: "Couldn't create or update schema. Attempted to move db to another location.", tag: SentryTag.swiftData, severity: .warning, description: "Attempting to move '\(baseFilename)' for schema '\(self.schema.name)'") + + // Note that a backup file might already exist! We append a counter to avoid this. + var bakCounter = 0 + var bak: String + repeat { + bakCounter += 1 + bak = "\(baseFilename).bak.\(bakCounter)" + } while files.exists(bak) + + do { + try files.move(baseFilename, toRelativePath: bak) + + let shm = baseFilename + "-shm" + let wal = baseFilename + "-wal" + log.debug("Moving \(shm) and \(wal)…") + if files.exists(shm) { + log.debug("\(shm) exists.") + try files.move(shm, toRelativePath: bak + "-shm") + } + if files.exists(wal) { + log.debug("\(wal) exists.") + try files.move(wal, toRelativePath: bak + "-wal") + } + + log.debug("Finished moving database \(baseFilename) successfully.") + } catch let error as NSError { + Sentry.shared.sendWithStacktrace(message: "Unable to move db to another location", tag: SentryTag.swiftData, severity: .error, description: "DB file '\(baseFilename)'. \(error.localizedDescription)") + } + } else { + // No backup was attempted since the database file did not exist. + Sentry.shared.sendWithStacktrace(message: "The database file has been deleted while previously in use.", tag: SentryTag.swiftData, description: "DB file '\(baseFilename)'") + } + } + + public func checkpoint() { + self.checkpoint(SQLITE_CHECKPOINT_FULL) + } + + /** + * Blindly attempts a WAL checkpoint on all attached databases. + */ + public func checkpoint(_ mode: Int32) { + guard sqliteDB != nil else { + log.warning("Trying to checkpoint a nil DB!") + return + } + + log.debug("Running WAL checkpoint on \(self.filename) on thread \(Thread.current).") + sqlite3_wal_checkpoint_v2(sqliteDB, nil, mode, nil, nil) + log.debug("WAL checkpoint done on \(self.filename).") + } + + public func vacuum() throws -> Void { + try executeChange("VACUUM") + } + + /// Creates an error from a sqlite status. Will print to the console if debug_enabled is set. + /// Do not call this unless you're going to return this error. + fileprivate func createErr(_ description: String, status: Int) -> NSError { + var msg = SDError.errorMessageFromCode(status) + + if debug_enabled { + log.debug("SwiftData Error -> \(description)") + log.debug(" -> Code: \(status) - \(msg)") + } + + if let errMsg = String(validatingUTF8: sqlite3_errmsg(sqliteDB)) { + msg += " " + errMsg + if debug_enabled { + log.debug(" -> Details: \(errMsg)") + } + } + + return NSError(domain: "org.mozilla", code: status, userInfo: [NSLocalizedDescriptionKey: msg]) + } + + /// Open the connection. This is called when the db is created. You should not call it yourself. + fileprivate func openWithFlags(_ flags: Int32) -> NSError? { + let status = sqlite3_open_v2(filename.cString(using: String.Encoding.utf8)!, &sqliteDB, flags, nil) + if status != SQLITE_OK { + return createErr("During: Opening Database with Flags", status: Int(status)) + } + return nil + } + + /// Closes a connection. This is called via deinit. Do not call this yourself. + @discardableResult fileprivate func closeCustomConnection(immediately: Bool = false) -> NSError? { + log.debug("Closing custom connection for \(self.filename) on \(Thread.current).") + // TODO: add a lock here? + let db = self.sqliteDB + self.sqliteDB = nil + + // Don't bother trying to call sqlite3_close multiple times. + guard db != nil else { + log.warning("Connection was nil.") + return nil + } + + var status = sqlite3_close(db) + + if status != SQLITE_OK { + Sentry.shared.sendWithStacktrace(message: "Got error status while attempting to close.", tag: SentryTag.swiftData, severity: .error, description: "SQLite status: \(status)") + + if immediately { + return createErr("During: closing database with flags", status: Int(status)) + } + + // Note that if we use sqlite3_close_v2, this will still return SQLITE_OK even if + // there are outstanding prepared statements + status = sqlite3_close_v2(db) + + if status != SQLITE_OK { + + // Based on the above comment regarding sqlite3_close_v2, this shouldn't happen. + Sentry.shared.sendWithStacktrace(message: "Got error status while attempting to close_v2.", tag: SentryTag.swiftData, severity: .error, description: "SQLite status: \(status)") + return createErr("During: closing database with flags", status: Int(status)) + } + } + + log.debug("Closed \(self.filename).") + return nil + } + + open func executeChange(_ sqlStr: String) throws -> Void { + try executeChange(sqlStr, withArgs: nil) + } + + /// Executes a change on the database. + open func executeChange(_ sqlStr: String, withArgs args: Args?) throws -> Void { + var error: NSError? + let statement: SQLiteDBStatement? + do { + statement = try SQLiteDBStatement(connection: self, query: sqlStr, args: args) + } catch let error1 as NSError { + error = error1 + statement = nil + } + + // Close, not reset -- this isn't going to be reused. + defer { statement?.close() } + + if let error = error { + // Special case: Write additional info to the database log in the case of a database corruption. + if error.code == Int(SQLITE_CORRUPT) { + writeCorruptionInfoForDBNamed(filename, toLogger: Logger.corruptLogger) + Sentry.shared.sendWithStacktrace(message: "SQLITE_CORRUPT", tag: SentryTag.swiftData, severity: .error, description: "DB file '\(filename)'. \(error.localizedDescription)") + } + + let message = "Error code: \(error.code), \(error) for SQL \(String(sqlStr.characters.prefix(500)))." + Sentry.shared.sendWithStacktrace(message: "SQL error", tag: SentryTag.swiftData, severity: .error, description: message) + + throw error + } + + let status = sqlite3_step(statement!.pointer) + + if status != SQLITE_DONE && status != SQLITE_OK { + throw createErr("During: SQL Step \(sqlStr)", status: Int(status)) + } + } + + public func executeQuery(_ sqlStr: String, factory: @escaping ((SDRow) -> T)) -> Cursor { + return self.executeQuery(sqlStr, factory: factory, withArgs: nil) + } + + /// Queries the database. + /// Returns a cursor pre-filled with the complete result set. + public func executeQuery(_ sqlStr: String, factory: @escaping ((SDRow) -> T), withArgs args: Args?) -> Cursor { + var error: NSError? + let statement: SQLiteDBStatement? + do { + statement = try SQLiteDBStatement(connection: self, query: sqlStr, args: args) + } catch let error1 as NSError { + error = error1 + statement = nil + } + + // Close, not reset -- this isn't going to be reused, and the FilledSQLiteCursor + // consumes everything. + defer { statement?.close() } + + if let error = error { + // Special case: Write additional info to the database log in the case of a database corruption. + if error.code == Int(SQLITE_CORRUPT) { + writeCorruptionInfoForDBNamed(filename, toLogger: Logger.corruptLogger) + Sentry.shared.sendWithStacktrace(message: "SQLITE_CORRUPT", tag: SentryTag.swiftData, severity: .error, description: "DB file '\(filename)'. \(error.localizedDescription)") + } + Sentry.shared.sendWithStacktrace(message: "SQL error", tag: SentryTag.swiftData, severity: .error, description: "Error code: \(error.code), \(error) for SQL \(String(sqlStr.characters.prefix(500))).") + + return Cursor(err: error) + } + + return FilledSQLiteCursor(statement: statement!, factory: factory) + } + + func writeCorruptionInfoForDBNamed(_ dbFilename: String, toLogger logger: XCGLogger) { + DispatchQueue.global(qos: DispatchQoS.default.qosClass).sync { + guard !SwiftData.corruptionLogsWritten.contains(dbFilename) else { return } + + logger.error("Corrupt DB detected! DB filename: \(dbFilename)") + + let dbFileSize = ("file://\(dbFilename)".asURL)?.allocatedFileSize() ?? 0 + logger.error("DB file size: \(dbFileSize) bytes") + + logger.error("Integrity check:") + + let args: [Any?]? = nil + let messages = self.executeQueryUnsafe("PRAGMA integrity_check", factory: StringFactory, withArgs: args) + defer { messages.close() } + + if messages.status == CursorStatus.success { + for message in messages { + logger.error(message) + } + logger.error("----") + } else { + logger.error("Couldn't run integrity check: \(messages.statusMessage).") + } + + // Write call stack. + logger.error("Call stack: ") + for message in Thread.callStackSymbols { + logger.error(" >> \(message)") + } + logger.error("----") + + // Write open file handles. + let openDescriptors = FSUtils.openFileDescriptors() + logger.error("Open file descriptors: ") + for (k, v) in openDescriptors { + logger.error(" \(k): \(v)") + } + logger.error("----") + + SwiftData.corruptionLogsWritten.insert(dbFilename) + } + } + + /** + * Queries the database. + * Returns a live cursor that holds the query statement and database connection. + * Instances of this class *must not* leak outside of the connection queue! + */ + public func executeQueryUnsafe(_ sqlStr: String, factory: @escaping ((SDRow) -> T), withArgs args: Args?) -> Cursor { + var error: NSError? + let statement: SQLiteDBStatement? + do { + statement = try SQLiteDBStatement(connection: self, query: sqlStr, args: args) + } catch let error1 as NSError { + error = error1 + statement = nil + } + if let error = error { + return Cursor(err: error) + } + + return LiveSQLiteCursor(statement: statement!, factory: factory) + } + + public func transaction(_ transactionClosure: @escaping (_ connection: SQLiteDBConnection) throws -> T) throws -> T { + do { + try executeChange("BEGIN EXCLUSIVE") + } catch let err as NSError { + Sentry.shared.sendWithStacktrace(message: "BEGIN EXCLUSIVE failed.", tag: SentryTag.swiftData, severity: .error, description: "\(err.code), \(err)") + throw err + } + + var result: T + + do { + result = try transactionClosure(self) + } catch let err as NSError { + log.error("Op in transaction threw an error. Rolling back.") + Sentry.shared.sendWithStacktrace(message: "Op in transaction threw an error. Rolling back.", tag: SentryTag.swiftData, severity: .error) + + do { + try executeChange("ROLLBACK") + } catch let err as NSError { + Sentry.shared.sendWithStacktrace(message: "ROLLBACK after errored op in transaction failed", tag: SentryTag.swiftData, severity: .error, description: "\(err.code), \(err)") + throw err + } + + throw err + } + + log.verbose("Op in transaction succeeded. Committing.") + + do { + try executeChange("COMMIT") + } catch let err as NSError { + Sentry.shared.sendWithStacktrace(message: "COMMIT failed. Rolling back.", tag: SentryTag.swiftData, severity: .error, description: "\(err.code), \(err)") + + do { + try executeChange("ROLLBACK") + } catch let err as NSError { + Sentry.shared.sendWithStacktrace(message: "ROLLBACK after failed COMMIT failed.", tag: SentryTag.swiftData, severity: .error, description: "\(err.code), \(err)") + throw err + } + + throw err + } + + return result + } +} + +/// Helper for queries that return a single integer result. +func IntFactory(_ row: SDRow) -> Int { + return row[0] as! Int +} + +/// Helper for queries that return a single String result. +func StringFactory(_ row: SDRow) -> String { + return row[0] as! String +} + +/// Wrapper around a statement for getting data from a row. This provides accessors for subscript indexing +/// and a generator for iterating over columns. +open class SDRow: Sequence { + // The sqlite statement this row came from. + fileprivate let statement: SQLiteDBStatement + + // The columns of this database. The indices of these are assumed to match the indices + // of the statement. + fileprivate let columnNames: [String] + + fileprivate init(statement: SQLiteDBStatement, columns: [String]) { + self.statement = statement + self.columnNames = columns + } + + // Return the value at this index in the row + fileprivate func getValue(_ index: Int) -> Any? { + let i = Int32(index) + + let type = sqlite3_column_type(statement.pointer, i) + var ret: Any? = nil + + switch type { + case SQLITE_NULL: + return nil + case SQLITE_INTEGER: + //Everyone expects this to be an Int. On Ints larger than 2^31 this will lose information. + ret = Int(truncatingBitPattern: sqlite3_column_int64(statement.pointer, i)) + case SQLITE_TEXT: + if let text = sqlite3_column_text(statement.pointer, i) { + return String(cString: text) + } + case SQLITE_BLOB: + if let blob = sqlite3_column_blob(statement.pointer, i) { + let size = sqlite3_column_bytes(statement.pointer, i) + ret = Data(bytes: blob, count: Int(size)) + } + case SQLITE_FLOAT: + ret = Double(sqlite3_column_double(statement.pointer, i)) + default: + log.warning("SwiftData Warning -> Column: \(index) is of an unrecognized type, returning nil") + } + + return ret + } + + // Accessor getting column 'key' in the row + subscript(key: Int) -> Any? { + return getValue(key) + } + + // Accessor getting a named column in the row. This (currently) depends on + // the columns array passed into this Row to find the correct index. + subscript(key: String) -> Any? { + get { + if let index = columnNames.index(of: key) { + return getValue(index) + } + return nil + } + } + + // Allow iterating through the row. + public func makeIterator() -> AnyIterator { + let nextIndex = 0 + return AnyIterator() { + if nextIndex < self.columnNames.count { + return self.getValue(nextIndex) + } + return nil + } + } +} + +/// Helper for pretty printing SQL (and other custom) error codes. +private struct SDError { + fileprivate static func errorMessageFromCode(_ errorCode: Int) -> String { + switch errorCode { + case -1: + return "No error" + // SQLite error codes and descriptions as per: http://www.sqlite.org/c3ref/c_abort.html + case 0: + return "Successful result" + case 1: + return "SQL error or missing database" + case 2: + return "Internal logic error in SQLite" + case 3: + return "Access permission denied" + case 4: + return "Callback routine requested an abort" + case 5: + return "The database file is busy" + case 6: + return "A table in the database is locked" + case 7: + return "A malloc() failed" + case 8: + return "Attempt to write a readonly database" + case 9: + return "Operation terminated by sqlite3_interrupt()" + case 10: + return "Some kind of disk I/O error occurred" + case 11: + return "The database disk image is malformed" + case 12: + return "Unknown opcode in sqlite3_file_control()" + case 13: + return "Insertion failed because database is full" + case 14: + return "Unable to open the database file" + case 15: + return "Database lock protocol error" + case 16: + return "Database is empty" + case 17: + return "The database schema changed" + case 18: + return "String or BLOB exceeds size limit" + case 19: + return "Abort due to constraint violation" + case 20: + return "Data type mismatch" + case 21: + return "Library used incorrectly" + case 22: + return "Uses OS features not supported on host" + case 23: + return "Authorization denied" + case 24: + return "Auxiliary database format error" + case 25: + return "2nd parameter to sqlite3_bind out of range" + case 26: + return "File opened that is not a database file" + case 27: + return "Notifications from sqlite3_log()" + case 28: + return "Warnings from sqlite3_log()" + case 100: + return "sqlite3_step() has another row ready" + case 101: + return "sqlite3_step() has finished executing" + + // Custom SwiftData errors + // Binding errors + case 201: + return "Not enough objects to bind provided" + case 202: + return "Too many objects to bind provided" + + // Custom connection errors + case 301: + return "A custom connection is already open" + case 302: + return "Cannot open a custom connection inside a transaction" + case 303: + return "Cannot open a custom connection inside a savepoint" + case 304: + return "A custom connection is not currently open" + case 305: + return "Cannot close a custom connection inside a transaction" + case 306: + return "Cannot close a custom connection inside a savepoint" + + // Index and table errors + case 401: + return "At least one column name must be provided" + case 402: + return "Error extracting index names from sqlite_master" + case 403: + return "Error extracting table names from sqlite_master" + + // Transaction and savepoint errors + case 501: + return "Cannot begin a transaction within a savepoint" + case 502: + return "Cannot begin a transaction within another transaction" + + // Unknown error + default: + return "Unknown error" + } + } +} + +/// Provides access to the result set returned by a database query. +/// The entire result set is cached, so this does not retain a reference +/// to the statement or the database connection. +private class FilledSQLiteCursor: ArrayCursor { + fileprivate init(statement: SQLiteDBStatement, factory: (SDRow) -> T) { + let (data, status, statusMessage) = FilledSQLiteCursor.getValues(statement, factory: factory) + super.init(data: data, status: status, statusMessage: statusMessage) + } + + /// Return an array with the set of results and release the statement. + fileprivate class func getValues(_ statement: SQLiteDBStatement, factory: (SDRow) -> T) -> ([T], CursorStatus, String) { + var rows = [T]() + var status = CursorStatus.success + var statusMessage = "Success" + var count = 0 + + var columns = [String]() + let columnCount = sqlite3_column_count(statement.pointer) + for i in 0..: Cursor { + fileprivate var statement: SQLiteDBStatement! + + // Function for generating objects of type T from a row. + fileprivate let factory: (SDRow) -> T + + // Status of the previous fetch request. + fileprivate var sqlStatus: Int32 = 0 + + // Number of rows in the database + // XXX - When Cursor becomes an interface, this should be a normal property, but right now + // we can't override the Cursor getter for count with a stored property. + fileprivate var _count: Int = 0 + override var count: Int { + get { + if status != .success { + return 0 + } + return _count + } + } + + fileprivate var position: Int = -1 { + didSet { + // If we're already there, shortcut out. + if oldValue == position { + return + } + + var stepStart = oldValue + + // If we're currently somewhere in the list after this position + // we'll have to jump back to the start. + if position < oldValue { + sqlite3_reset(self.statement.pointer) + stepStart = -1 + } + + // Now step up through the list to the requested position + for _ in stepStart.. T) { + self.factory = factory + self.statement = statement + + // The only way I know to get a count. Walk through the entire statement to see how many rows there are. + var count = 0 + self.sqlStatus = sqlite3_step(statement.pointer) + while self.sqlStatus != SQLITE_DONE { + count += 1 + self.sqlStatus = sqlite3_step(statement.pointer) + } + + sqlite3_reset(statement.pointer) + self._count = count + + super.init(status: .success, msg: "success") + } + + // Helper for finding all the column names in this statement. + fileprivate lazy var columns: [String] = { + // This untangles all of the columns and values for this row when its created + let columnCount = sqlite3_column_count(self.statement.pointer) + var columns = [String]() + for i: Int32 in 0 ..< columnCount { + let columnName = String(cString: sqlite3_column_name(self.statement.pointer, i)) + columns.append(columnName) + } + return columns + }() + + override subscript(index: Int) -> T? { + get { + if status != .success { + return nil + } + + self.position = index + if self.sqlStatus != SQLITE_ROW { + return nil + } + + let row = SDRow(statement: statement, columns: self.columns) + return self.factory(row) + } + } + + override func close() { + statement = nil + super.close() + } +} diff --git a/mobile/ios/Storage/Visit.swift b/mobile/ios/Storage/Visit.swift new file mode 100644 index 0000000000..7eb31357e1 --- /dev/null +++ b/mobile/ios/Storage/Visit.swift @@ -0,0 +1,121 @@ +/* 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 SwiftyJSON + +// These are taken from the Places docs +// http://mxr.mozilla.org/mozilla-central/source/toolkit/components/places/nsINavHistoryService.idl#1187 +@objc public enum VisitType: Int { + case unknown = 0 + + /** + * This transition type means the user followed a link and got a new toplevel + * window. + */ + case link = 1 + + /** + * This transition type means that the user typed the page's URL in the + * URL bar or selected it from URL bar autocomplete results, clicked on + * it from a history query (from the History sidebar, History menu, + * or history query in the personal toolbar or Places organizer). + */ + case typed = 2 + + case bookmark = 3 + case embed = 4 + case permanentRedirect = 5 + case temporaryRedirect = 6 + case download = 7 + case framedLink = 8 +} + +// WKWebView has these: +/* +WKNavigationTypeLinkActivated, +WKNavigationTypeFormSubmitted, +WKNavigationTypeBackForward, +WKNavigationTypeReload, +WKNavigationTypeFormResubmitted, +WKNavigationTypeOther = -1, +*/ + +/** + * SiteVisit is a sop to the existing API, which expects to be able to go + * backwards from a visit to a site, and preserve the ID of the database row. + * Visit is the model of what lives on the wire: just a date and a type. + * Ultimately we'll end up with something similar to ClientAndTabs: the tabs + * don't need to know about the client, and visits don't need to know about + * the site, because they're bound together. + * + * (Furthermore, we probably shouldn't ever need something like SiteVisit + * to reach the UI: we care about "last visited", "visit count", or just + * "places ordered by frecency" — we don't care about lists of visits.) + */ + +open class Visit: Hashable { + open let date: MicrosecondTimestamp + open let type: VisitType + + open var hashValue: Int { + return date.hashValue ^ type.hashValue + } + + public init(date: MicrosecondTimestamp, type: VisitType = .unknown) { + self.date = date + self.type = type + } + + open class func fromJSON(_ json: [String: Any]) -> Visit? { + if let type = json["type"] as? Int, + let typeEnum = VisitType(rawValue: type), + let date = json["date"] as? Int64, date >= 0 { + return Visit(date: MicrosecondTimestamp(date), type: typeEnum) + } + return nil + } + + open func toJSON() -> [String: Any] { + let d = NSNumber(value: self.date) + let o: [String: Any] = ["type": self.type.rawValue, "date": d] + return o + } +} + +public func ==(lhs: Visit, rhs: Visit) -> Bool { + return lhs.date == rhs.date && + lhs.type == rhs.type +} + +open class SiteVisit: Visit { + var id: Int? + open let site: Site + + open override var hashValue: Int { + return date.hashValue ^ type.hashValue ^ (id?.hashValue ?? 0) ^ (site.id ?? 0) + } + + public init(site: Site, date: MicrosecondTimestamp, type: VisitType = .unknown) { + self.site = site + super.init(date: date, type: type) + } +} + +public func ==(lhs: SiteVisit, rhs: SiteVisit) -> Bool { + if let lhsID = lhs.id, let rhsID = rhs.id { + if lhsID != rhsID { + return false + } + } else { + if lhs.id != nil || rhs.id != nil { + return false + } + } + + // TODO: compare Site. + return lhs.date == rhs.date && + lhs.type == rhs.type +} diff --git a/mobile/ios/StoragePerfTests/Info.plist b/mobile/ios/StoragePerfTests/Info.plist new file mode 100644 index 0000000000..6366f45b20 --- /dev/null +++ b/mobile/ios/StoragePerfTests/Info.plist @@ -0,0 +1,22 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + BNDL + CFBundleShortVersionString + 10.6 + CFBundleVersion + 1 + + diff --git a/mobile/ios/StoragePerfTests/StoragePerfTests.swift b/mobile/ios/StoragePerfTests/StoragePerfTests.swift new file mode 100644 index 0000000000..121611a61c --- /dev/null +++ b/mobile/ios/StoragePerfTests/StoragePerfTests.swift @@ -0,0 +1,107 @@ +/* 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 +@testable import Storage +import Deferred +import XCTest + +class MockFiles: FileAccessor { + init() { + let docPath = NSSearchPathForDirectoriesInDomains(FileManager.SearchPathDirectory.documentDirectory, FileManager.SearchPathDomainMask.userDomainMask, true)[0] + super.init(rootPath: (docPath as NSString).appendingPathComponent("testing")) + } +} + +// Start everything three months ago. +let threeMonthsInMillis: UInt64 = 3 * 30 * 24 * 60 * 60 * 1000 +let threeMonthsInMicros: UInt64 = UInt64(threeMonthsInMillis) * UInt64(1000) + +let baseInstantInMillis = Date.now() - threeMonthsInMillis +let baseInstantInMicros = Date.nowMicroseconds() - threeMonthsInMicros + +func advanceMicrosecondTimestamp(_ timestamp: MicrosecondTimestamp, by: Int) -> MicrosecondTimestamp { + return timestamp + UInt64(by) +} + +extension Site { + func asPlace() -> Place { + return Place(guid: self.guid!, url: self.url, title: self.title) + } +} + +class TestSQLiteHistoryFrecencyPerf: XCTestCase { + func testFrecencyPerf() { + let files = MockFiles() + let db = BrowserDB(filename: "browser.db", schema: BrowserSchema(), files: files) + let prefs = MockProfilePrefs() + let history = SQLiteHistory(db: db, prefs: prefs) + + let count = 500 + + history.clearHistory().succeeded() + populateHistoryForFrecencyCalculations(history, siteCount: count) + + self.measureMetrics([XCTPerformanceMetric_WallClockTime], automaticallyStartMeasuring: true) { + for _ in 0...5 { + history.getFrecentHistory().getSites(whereURLContains: nil, historyLimit: 10, bookmarksLimit: 0).succeeded() + } + self.stopMeasuring() + } + } +} + +class TestSQLiteHistoryTopSitesCachePref: XCTestCase { + func testCachePerf() { + let files = MockFiles() + let db = BrowserDB(filename: "browser.db", schema: BrowserSchema(), files: files) + let prefs = MockProfilePrefs() + let history = SQLiteHistory(db: db, prefs: prefs) + + let count = 500 + + history.clearHistory().succeeded() + populateHistoryForFrecencyCalculations(history, siteCount: count) + + history.setTopSitesNeedsInvalidation() + self.measureMetrics([XCTPerformanceMetric_WallClockTime], automaticallyStartMeasuring: true) { + history.repopulate(invalidateTopSites: true, invalidateHighlights: true).succeeded() + self.stopMeasuring() + } + } +} + +// MARK - Private Test Helper Methods + +private enum VisitOrigin { + case local + case remote +} + +private func populateHistoryForFrecencyCalculations(_ history: SQLiteHistory, siteCount count: Int) { + for i in 0...count { + let site = Site(url: "http://s\(i)ite\(i)/foo", title: "A \(i)") + site.guid = "abc\(i)def" + + let baseMillis: UInt64 = baseInstantInMillis - 20000 + history.insertOrUpdatePlace(site.asPlace(), modified: baseMillis).succeeded() + + for j in 0...20 { + let visitTime = advanceMicrosecondTimestamp(baseInstantInMicros, by: (1000000 * i) + (1000 * j)) + addVisitForSite(site, intoHistory: history, from: .local, atTime: visitTime) + addVisitForSite(site, intoHistory: history, from: .remote, atTime: visitTime) + } + } +} + +private func addVisitForSite(_ site: Site, intoHistory history: SQLiteHistory, from: VisitOrigin, atTime: MicrosecondTimestamp) { + let visit = SiteVisit(site: site, date: atTime, type: VisitType.link) + switch from { + case .local: + history.addLocalVisit(visit).succeeded() + case .remote: + history.storeRemoteVisits([visit], forGUID: site.guid!).succeeded() + } +} diff --git a/mobile/ios/StorageTests/CertTests.swift b/mobile/ios/StorageTests/CertTests.swift new file mode 100644 index 0000000000..5ed815e3f3 --- /dev/null +++ b/mobile/ios/StorageTests/CertTests.swift @@ -0,0 +1,46 @@ +/* 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 XCTest +import Storage + +class CertTests: XCTestCase { + func testCertStore() { + let certStore = CertStore() + let origin1 = "www.mozilla.org:80" + let origin2 = "people.mozilla.org:80" + let cert1 = getCertificate("testcert1") + let cert2 = getCertificate("testcert2") + + // Check that contains return false for certs not in store. + XCTAssertFalse(certStore.containsCertificate(cert1, forOrigin: origin1)) + + // Add a certificate. + certStore.addCertificate(cert1, forOrigin: origin1) + + // Check that the cert is in the store. + XCTAssert(certStore.containsCertificate(cert1, forOrigin: origin1)) + + // Check that the cert is unique to the origin. + XCTAssertFalse(certStore.containsCertificate(cert1, forOrigin: origin2)) + XCTAssertFalse(certStore.containsCertificate(cert2, forOrigin: origin1)) + + // Add a different certificate for the same origin. + certStore.addCertificate(cert2, forOrigin: origin1) + + // Check that adding a cert for an existing origin doesn't do a replace. + XCTAssert(certStore.containsCertificate(cert1, forOrigin: origin1)) + XCTAssert(certStore.containsCertificate(cert2, forOrigin: origin1)) + + // Check that adding an existing cert has no effect. + certStore.addCertificate(cert1, forOrigin: origin1) + XCTAssert(certStore.containsCertificate(cert1, forOrigin: origin1)) + } + + fileprivate func getCertificate(_ file: String) -> SecCertificate { + let path = Bundle(for: type(of: self)).path(forResource: file, ofType: "pem") + let data = try? Data(contentsOf: URL(fileURLWithPath: path!)) + return SecCertificateCreateWithData(nil, data! as CFData)! + } +} diff --git a/mobile/ios/StorageTests/DiskImageStoreTests.swift b/mobile/ios/StorageTests/DiskImageStoreTests.swift new file mode 100644 index 0000000000..9a88163866 --- /dev/null +++ b/mobile/ios/StorageTests/DiskImageStoreTests.swift @@ -0,0 +1,76 @@ +/* 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 +@testable import Storage +import UIKit + +import XCTest + +class DiskImageStoreTests: XCTestCase { + var files: FileAccessor! + var store: DiskImageStore! + + override func setUp() { + files = MockFiles() + store = DiskImageStore(files: files, namespace: "DiskImageStoreTests", quality: 1) + + _ = store.clearExcluding(Set()).value + } + + func testStore() { + var success = false + + // Avoid image comparison and use size of the image for equality + let redImage = makeImageWithColor(UIColor.red, size: CGSize(width: 100, height: 100)) + let blueImage = makeImageWithColor(UIColor.blue, size: CGSize(width: 17, height: 17)) + + [(key: "blue", image: blueImage), (key: "red", image: redImage)].forEach() { (key, image) in + XCTAssertNil(getImage(key), "\(key) key is nil") + success = putImage(key, image: image) + XCTAssert(success, "\(key) image added to store") + XCTAssertEqual(getImage(key)!.size.width, image.size.width, "Images are equal") + + success = putImage(key, image: image) + XCTAssertFalse(success, "\(key) image not added again") + } + + _ = store.clearExcluding(Set(["red"])).value + XCTAssertNotNil(getImage("red"), "Red image still exists") + XCTAssertNil(getImage("blue"), "Blue image cleared") + } + + private func makeImageWithColor(_ color: UIColor, size: CGSize) -> UIImage { + let rect = CGRect(x: 0, y: 0, width: size.width, height: size.height) + UIGraphicsBeginImageContextWithOptions(size, false, 1.0) + color.setFill() + UIRectFill(rect) + let image = UIGraphicsGetImageFromCurrentImageContext()! + UIGraphicsEndImageContext() + return image + } + + private func getImage(_ key: String) -> UIImage? { + let expectation = self.expectation(description: "Get succeeded") + var image: UIImage? + store.get(key).upon { + image = $0.successValue + expectation.fulfill() + } + waitForExpectations(timeout: 10, handler: nil) + return image + } + + private func putImage(_ key: String, image: UIImage) -> Bool { + let expectation = self.expectation(description: "Put succeeded") + var success = false + store.put(key, image: image).upon { + success = $0.isSuccess + expectation.fulfill() + } + waitForExpectations(timeout: 10, handler: nil) + return success + } +} diff --git a/mobile/ios/StorageTests/Info.plist b/mobile/ios/StorageTests/Info.plist new file mode 100644 index 0000000000..9411e4a06e --- /dev/null +++ b/mobile/ios/StorageTests/Info.plist @@ -0,0 +1,24 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + BNDL + CFBundleShortVersionString + 10.6 + CFBundleSignature + ???? + CFBundleVersion + 1 + + diff --git a/mobile/ios/StorageTests/MockFiles.swift b/mobile/ios/StorageTests/MockFiles.swift new file mode 100644 index 0000000000..9298d62b6c --- /dev/null +++ b/mobile/ios/StorageTests/MockFiles.swift @@ -0,0 +1,23 @@ +/* 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 +@testable import Storage + +import XCTest + +class MockFiles: FileAccessor { + init() { + let docPath = NSSearchPathForDirectoriesInDomains(FileManager.SearchPathDirectory.documentDirectory, FileManager.SearchPathDomainMask.userDomainMask, true)[0] + super.init(rootPath: (docPath as NSString).appendingPathComponent("testing")) + } +} + +class SupportingFiles: FileAccessor { + init() { + let path = Bundle.main.bundlePath + "/PlugIns/StorageTests.xctest/" + NSLog("Supporting files: \(path)") + super.init(rootPath: path) + } +} diff --git a/mobile/ios/StorageTests/StorageTestUtils.swift b/mobile/ios/StorageTests/StorageTestUtils.swift new file mode 100644 index 0000000000..b5b90bb7fd --- /dev/null +++ b/mobile/ios/StorageTests/StorageTestUtils.swift @@ -0,0 +1,140 @@ +/* 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/. */ + +// Note that this file is imported into SyncTests, too. + +import Deferred +import Foundation +import Shared +@testable import Storage +import XCTest + +extension BrowserDB { + func assertQueryReturns(_ query: String, int: Int) { + XCTAssertEqual(int, self.runQuery(query, args: nil, factory: IntFactory).value.successValue![0]) + } +} + +extension BrowserDB { + func moveLocalToMirrorForTesting() { + // This is a risky process -- it's not the same logic that the real synchronizer uses + // (because I haven't written it yet), so it might end up lying. We do what we can. + let valueSQL = [ + "INSERT OR IGNORE INTO \(TableBookmarksMirror)", + "(guid, type, date_added, bmkUri, title, parentid, parentName, feedUri, siteUri, pos,", + " description, tags, keyword, folderName, queryId,", + " is_overridden, server_modified, faviconID)", + "SELECT guid, type, date_added, bmkUri, title, parentid, parentName,", + "feedUri, siteUri, pos, description, tags, keyword, folderName, queryId,", + "0 AS is_overridden, \(Date.now()) AS server_modified, faviconID", + "FROM \(TableBookmarksLocal)", + ].joined(separator: " ") + + // Copy its mirror structure. + let structureSQL = "INSERT INTO \(TableBookmarksMirrorStructure) SELECT * FROM \(TableBookmarksLocalStructure)" + + // Throw away the old. + let deleteLocalStructureSQL = "DELETE FROM \(TableBookmarksLocalStructure)" + let deleteLocalSQL = "DELETE FROM \(TableBookmarksLocal)" + + self.run([ + valueSQL, + structureSQL, + deleteLocalStructureSQL, + deleteLocalSQL, + ]).succeeded() + } + + func moveBufferToMirrorForTesting() { + let valueSQL = [ + "INSERT OR IGNORE INTO \(TableBookmarksMirror)", + "(guid, type, date_added, bmkUri, title, parentid, parentName, feedUri, siteUri, pos,", + "description, tags, keyword, folderName, queryId, server_modified)", + "SELECT", + "guid, type, date_added, bmkUri, title, parentid, parentName, feedUri, siteUri, pos,", + "description, tags, keyword, folderName, queryId, server_modified", + "FROM \(TableBookmarksBuffer)", + ].joined(separator: " ") + + let structureSQL = "INSERT INTO \(TableBookmarksMirrorStructure) SELECT * FROM \(TableBookmarksBufferStructure)" + let deleteBufferStructureSQL = "DELETE FROM \(TableBookmarksBufferStructure)" + let deleteBufferSQL = "DELETE FROM \(TableBookmarksBuffer)" + + self.run([ + valueSQL, + structureSQL, + deleteBufferStructureSQL, + deleteBufferSQL, + ]).succeeded() + } +} + +extension BrowserDB { + func getGUIDs(_ sql: String) -> [GUID] { + func guidFactory(_ row: SDRow) -> GUID { + return row[0] as! GUID + } + + guard let cursor = self.runQuery(sql, args: nil, factory: guidFactory).value.successValue else { + XCTFail("Unable to get cursor.") + return [] + } + return cursor.asArray() + } + + func getPositionsForChildrenOfParent(_ parent: GUID, fromTable table: String) -> [GUID: Int] { + let args: Args = [parent] + let factory: (SDRow) -> (GUID, Int) = { + return ($0["child"] as! GUID, $0["idx"] as! Int) + } + let cursor = self.runQuery("SELECT child, idx FROM \(table) WHERE parent = ?", args: args, factory: factory).value.successValue! + return cursor.reduce([:], { (dict, pair) in + var dict = dict + if let (k, v) = pair { + dict[k] = v + } + return dict + }) + } + + func isLocallyDeleted(_ guid: GUID) -> Bool? { + let args: Args = [guid] + let cursor = self.runQuery("SELECT is_deleted FROM \(TableBookmarksLocal) WHERE guid = ?", args: args, factory: { $0.getBoolean("is_deleted") }).value.successValue! + return cursor[0] + } + + func isOverridden(_ guid: GUID) -> Bool? { + let args: Args = [guid] + let cursor = self.runQuery("SELECT is_overridden FROM \(TableBookmarksMirror) WHERE guid = ?", args: args, factory: { $0.getBoolean("is_overridden") }).value.successValue! + return cursor[0] + } + + func getSyncStatusForGUID(_ guid: GUID) -> SyncStatus? { + let args: Args = [guid] + let cursor = self.runQuery("SELECT sync_status FROM \(TableBookmarksLocal) WHERE guid = ?", args: args, factory: { $0[0] as! Int }).value.successValue! + if let raw = cursor[0] { + return SyncStatus(rawValue: raw) + } + return nil + } + + func getRecordByURL(_ url: String, fromTable table: String) -> BookmarkMirrorItem { + let args: Args = [url] + return self.runQuery("SELECT * FROM \(table) WHERE bmkUri = ?", args: args, factory: BookmarkFactory.mirrorItemFactory).value.successValue![0]! + } + + func getRecordByGUID(_ guid: GUID, fromTable table: String) -> BookmarkMirrorItem { + let args: Args = [guid] + return self.runQuery("SELECT * FROM \(table) WHERE guid = ?", args: args, factory: BookmarkFactory.mirrorItemFactory).value.successValue![0]! + } + + func getChildrenOfFolder(_ folder: GUID) -> [GUID] { + let args: Args = [folder] + let sql = + "SELECT child FROM \(ViewBookmarksLocalStructureOnMirror) " + + "WHERE parent = ? " + + "ORDER BY idx ASC" + return self.runQuery(sql, args: args, factory: { $0[0] as! GUID }).value.successValue!.asArray() + } +} diff --git a/mobile/ios/StorageTests/SyncCommandsTests.swift b/mobile/ios/StorageTests/SyncCommandsTests.swift new file mode 100644 index 0000000000..6bd918ba49 --- /dev/null +++ b/mobile/ios/StorageTests/SyncCommandsTests.swift @@ -0,0 +1,237 @@ +/* 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 +@testable import Storage + +import XCTest +import SwiftyJSON + +func byValue(_ a: SyncCommand, b: SyncCommand) -> Bool { + return a.value < b.value +} + +func byClient(_ a: RemoteClient, b: RemoteClient) -> Bool { + return a.guid! < b.guid! +} + +class SyncCommandsTests: XCTestCase { + + var clients: [RemoteClient] = [RemoteClient]() + var clientsAndTabs: SQLiteRemoteClientsAndTabs! + + var shareItems = [ShareItem]() + + var multipleCommands: [ShareItem] = [ShareItem]() + var wipeCommand: SyncCommand! + var db: BrowserDB! + + override func setUp() { + let files = MockFiles() + do { + try files.remove("browser.db") + } catch _ { + } + db = BrowserDB(filename: "browser.db", schema: BrowserSchema(), files: files) + + // create clients + let now = Date.now() + let client1GUID = Bytes.generateGUID() + let client2GUID = Bytes.generateGUID() + let client3GUID = Bytes.generateGUID() + + self.clients.append(RemoteClient(guid: client1GUID, name: "Test client 1", modified: (now - OneMinuteInMilliseconds), type: "mobile", formfactor: "largetablet", os: "iOS", version: "55.0.1", fxaDeviceId: nil)) + self.clients.append(RemoteClient(guid: client2GUID, name: "Test client 2", modified: (now - OneHourInMilliseconds), type: "desktop", formfactor: "laptop", os: "Darwin", version: "55.0.1", fxaDeviceId: nil)) + self.clients.append(RemoteClient(guid: client3GUID, name: "Test local client", modified: (now - OneMinuteInMilliseconds), type: "mobile", formfactor: "largetablet", os: "iOS", version: "55.0.1", fxaDeviceId: nil)) + clientsAndTabs = SQLiteRemoteClientsAndTabs(db: db) + clientsAndTabs.insertOrUpdateClients(clients).succeeded() + + shareItems.append(ShareItem(url: "http://mozilla.com", title: "Mozilla", favicon: nil)) + shareItems.append(ShareItem(url: "http://slashdot.org", title: "Slashdot", favicon: nil)) + shareItems.append(ShareItem(url: "http://news.bbc.co.uk", title: "BBC News", favicon: nil)) + shareItems.append(ShareItem(url: "http://news.bbc.co.uk", title: nil, favicon: nil)) + + wipeCommand = SyncCommand(value: "{'command':'wipeAll', 'args':[]}") + } + + override func tearDown() { + clientsAndTabs.deleteCommands().succeeded() + clientsAndTabs.clear().succeeded() + } + + func testCreateSyncCommandFromShareItem() { + let shareItem = shareItems[0] + let syncCommand = SyncCommand.displayURIFromShareItem(shareItem, asClient: "abcdefghijkl") + XCTAssertNil(syncCommand.commandID) + XCTAssertNotNil(syncCommand.value) + let jsonObj: [String: Any] = [ + "command": "displayURI", + "args": [shareItem.url, "abcdefghijkl", shareItem.title ?? ""] + ] + XCTAssertEqual(JSON(object: jsonObj).stringValue(), syncCommand.value) + } + + func testInsertWithNoURLOrTitle() { + // Test insert command to table for + let e = self.expectation(description: "Insert.") + clientsAndTabs.insertCommand(self.wipeCommand, forClients: clients).upon { + XCTAssertTrue($0.isSuccess) + XCTAssertEqual(3, $0.successValue!) + + let commandCursorDeferred = self.db.withConnection { connection -> Cursor in + let select = "SELECT COUNT(*) FROM \(TableSyncCommands)" + return connection.executeQuery(select, factory: IntFactory, withArgs: nil) + } + + let commandCursor = commandCursorDeferred.value.successValue! + + XCTAssertNotNil(commandCursor[0]) + XCTAssertEqual(3, commandCursor[0]!) + e.fulfill() + } + self.waitForExpectations(timeout: 5, handler: nil) + } + + func testInsertWithURLOnly() { + let shareItem = shareItems[3] + let syncCommand = SyncCommand.displayURIFromShareItem(shareItem, asClient: "abcdefghijkl") + + let e = self.expectation(description: "Insert.") + clientsAndTabs.insertCommand(syncCommand, forClients: clients).upon { + XCTAssertTrue($0.isSuccess) + XCTAssertEqual(3, $0.successValue!) + + let commandCursorDeferred = self.db.withConnection { connection -> Cursor in + let select = "SELECT COUNT(*) FROM \(TableSyncCommands)" + return connection.executeQuery(select, factory: IntFactory, withArgs: nil) + } + + let commandCursor = commandCursorDeferred.value.successValue! + + XCTAssertNotNil(commandCursor[0]) + XCTAssertEqual(3, commandCursor[0]!) + e.fulfill() + } + self.waitForExpectations(timeout: 5, handler: nil) + } + + func testInsertWithMultipleCommands() { + let e = self.expectation(description: "Insert.") + let syncCommands = shareItems.map { item in + return SyncCommand.displayURIFromShareItem(item, asClient: "abcdefghijkl") + } + clientsAndTabs.insertCommands(syncCommands, forClients: clients).upon { + XCTAssertTrue($0.isSuccess) + XCTAssertEqual(12, $0.successValue!) + + let commandCursorDeferred = self.db.withConnection { connection -> Cursor in + let select = "SELECT COUNT(*) FROM \(TableSyncCommands)" + return connection.executeQuery(select, factory: IntFactory, withArgs: nil) + } + + let commandCursor = commandCursorDeferred.value.successValue! + + XCTAssertNotNil(commandCursor[0]) + XCTAssertEqual(12, commandCursor[0]!) + e.fulfill() + } + self.waitForExpectations(timeout: 5, handler: nil) + } + + func testGetForAllClients() { + let syncCommands = shareItems.map { item in + return SyncCommand.displayURIFromShareItem(item, asClient: "abcdefghijkl") + }.sorted(by: byValue) + clientsAndTabs.insertCommands(syncCommands, forClients: clients).succeeded() + + let b = self.expectation(description: "Get for invalid client.") + clientsAndTabs.getCommands().upon({ result in + XCTAssertTrue(result.isSuccess) + if let clientCommands = result.successValue { + XCTAssertEqual(clientCommands.count, self.clients.count) + for client in clientCommands.keys { + XCTAssertEqual(syncCommands, clientCommands[client]!.sorted(by: byValue)) + } + } else { + XCTFail("Expected no commands!") + } + b.fulfill() + }) + self.waitForExpectations(timeout: 5, handler: nil) + } + + func testDeleteForValidClient() { + let syncCommands = shareItems.map { item in + return SyncCommand.displayURIFromShareItem(item, asClient: "abcdefghijkl") + }.sorted(by: byValue) + + var client = self.clients[0] + let a = self.expectation(description: "delete for client.") + let b = self.expectation(description: "Get for deleted client.") + let c = self.expectation(description: "Get for not deleted client.") + clientsAndTabs.insertCommands(syncCommands, forClients: clients).upon { + XCTAssertTrue($0.isSuccess) + XCTAssertEqual(12, $0.successValue!) + + let result = self.clientsAndTabs.deleteCommands(client.guid!).value + XCTAssertTrue(result.isSuccess) + a.fulfill() + + let commandCursorDeferred = self.db.withConnection { connection -> Cursor in + let select = "SELECT COUNT(*) FROM \(TableSyncCommands) WHERE client_guid = '\(client.guid!)'" + return connection.executeQuery(select, factory: IntFactory, withArgs: nil) + } + + let commandCursor = commandCursorDeferred.value.successValue! + + XCTAssertNotNil(commandCursor[0]) + XCTAssertEqual(0, commandCursor[0]!) + b.fulfill() + + client = self.clients[1] + let commandCursor2Deferred = self.db.withConnection { connection -> Cursor in + let select = "SELECT COUNT(*) FROM \(TableSyncCommands) WHERE client_guid = '\(client.guid!)'" + return connection.executeQuery(select, factory: IntFactory, withArgs: nil) + } + + let commandCursor2 = commandCursor2Deferred.value.successValue! + + XCTAssertNotNil(commandCursor2[0]) + XCTAssertEqual(4, commandCursor2[0]!) + c.fulfill() + } + + self.waitForExpectations(timeout: 5, handler: nil) + } + + func testDeleteForAllClients() { + let syncCommands = shareItems.map { item in + return SyncCommand.displayURIFromShareItem(item, asClient: "abcdefghijkl") + } + + let a = self.expectation(description: "Wipe for all clients.") + let b = self.expectation(description: "Get for clients.") + clientsAndTabs.insertCommands(syncCommands, forClients: clients).upon { + XCTAssertTrue($0.isSuccess) + XCTAssertEqual(12, $0.successValue!) + + let result = self.clientsAndTabs.deleteCommands().value + XCTAssertTrue(result.isSuccess) + a.fulfill() + + self.clientsAndTabs.getCommands().upon({ result in + XCTAssertTrue(result.isSuccess) + if let clientCommands = result.successValue { + XCTAssertEqual(0, clientCommands.count) + } else { + XCTFail("Expected no commands!") + } + b.fulfill() + }) + } + + self.waitForExpectations(timeout: 5, handler: nil) + } +} diff --git a/mobile/ios/StorageTests/SyncTelemetryTests.swift b/mobile/ios/StorageTests/SyncTelemetryTests.swift new file mode 100644 index 0000000000..a853330020 --- /dev/null +++ b/mobile/ios/StorageTests/SyncTelemetryTests.swift @@ -0,0 +1,118 @@ +/* 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 SwiftyJSON +@testable import Sync + +import XCTest + +fileprivate class MockFailure: MaybeErrorType { + let record: Record + + var description: String { + return "Failed to store or upload record: \(record)" + } + + init(record: Record) { + self.record = record + } +} + +class SyncTelemetryTests: XCTestCase { +} + +// MARK: IndependentRecordSynchronizer +extension SyncTelemetryTests { + private func getMockedIndependentRecordSynchronizer() -> IndependentRecordSynchronizer { + let prefs = MockProfilePrefs() + let scratchpad = Scratchpad(b: KeyBundle.random(), persistingTo: prefs) + let delegate = MockSyncDelegate() + + return IndependentRecordSynchronizer(scratchpad: scratchpad, delegate: delegate, basePrefs: prefs, why: .scheduled, collection: "mockHistory") + } + + func testApplyIncomingRecordsReportsDownloadStats() { + let synchronizer = getMockedIndependentRecordSynchronizer() + synchronizer.statsSession.start() + + // Fake remote records for incoming changes + let payloadA = CleartextPayloadJSON("{\"id\":\"A\",\"title\": \"A\"}") + let A = Record(id: "A", payload: payloadA) + + let payloadB = CleartextPayloadJSON("{\"id\":\"B\",\"title\": \"B\"}") + let B = Record(id: "B", payload: payloadB) + + let remoteRecords = [A, B] + + let _ = synchronizer.applyIncomingRecords(remoteRecords) { record in + return record.id == "B" ? deferMaybe(MockFailure(record: record)) : succeed() + }.value + + let session = synchronizer.statsSession.end() + + let downloadStats = session.downloadStats + XCTAssertEqual(downloadStats.applied, 2) + XCTAssertEqual(downloadStats.succeeded, 1) + XCTAssertEqual(downloadStats.failed, 1) + } + + func testApplyIncomingRecordsToStorageReportsDownloadStats() { + let synchronizer = getMockedIndependentRecordSynchronizer() + synchronizer.statsSession.start() + + // Fake remote records for incoming changes + let payloadA = CleartextPayloadJSON("{\"id\":\"A\",\"title\": \"A\"}") + let A = Record(id: "A", payload: payloadA) + + let payloadB = CleartextPayloadJSON("{\"id\":\"B\",\"title\": \"B\"}") + let B = Record(id: "B", payload: payloadB) + + let records = [A, B] + + let _ = synchronizer.applyIncomingToStorage(records, fetched: Date.now()) { record in + return record.id == "B" ? deferMaybe(MockFailure(record: record)) : succeed() + }.value + + let session = synchronizer.statsSession.end() + + let downloadStats = session.downloadStats + XCTAssertEqual(downloadStats.applied, 2) + XCTAssertEqual(downloadStats.succeeded, 1) + XCTAssertEqual(downloadStats.failed, 1) + } + + func testUploadRecordsReportsUploadStats() { + let synchronizer = getMockedIndependentRecordSynchronizer() + synchronizer.statsSession.start() + + let now = Date.now() + + // Fake local records for outgoing changes + let payloadC = CleartextPayloadJSON("{\"id\":\"C\",\"title\": \"C\"}") + let C = Record(id: "C", payload: payloadC) + + let payloadD = CleartextPayloadJSON("{\"id\":\"D\", \"title\": \"D\"}") + let D = Record(id: "D", payload: payloadD) + let records = [C, D] + + // Mock out a response for the uploader + let uploader: BatchUploadFunction = { _, _, _ in + let result = POSTResult(success: [C.id], failed: [D.id: "Invalid GUID"]) + let response = StorageResponse(value: result, metadata: ResponseMetadata(status: 200, headers: [:])) + return deferMaybe(response) + } + + let miniConfig = InfoConfiguration(maxRequestBytes: 1_048_576, maxPostRecords: 2, maxPostBytes: 1_048_576, maxTotalRecords: 10, maxTotalBytes: 104_857_600) + let collectionClient = MockSyncCollectionClient(uploader: uploader, infoConfig: miniConfig, collection: "mockdata", encrypter: getEncrypter()) + let _ = synchronizer.uploadRecords(records, lastTimestamp: now, storageClient: collectionClient) { _, _ in + return deferMaybe(now) + }.value + + let uploadStats = synchronizer.statsSession.uploadStats + XCTAssertEqual(uploadStats.sent, 1) + XCTAssertEqual(uploadStats.sentFailed, 1) + } +} diff --git a/mobile/ios/StorageTests/TestBrowserDB.swift b/mobile/ios/StorageTests/TestBrowserDB.swift new file mode 100644 index 0000000000..3363c703fa --- /dev/null +++ b/mobile/ios/StorageTests/TestBrowserDB.swift @@ -0,0 +1,124 @@ +/* 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 +@testable import Storage +import XCGLogger + +import XCTest + +private let log = XCGLogger.default + +class TestBrowserDB: XCTestCase { + let files = MockFiles() + + fileprivate func rm(_ path: String) { + do { + try files.remove(path) + } catch { + } + } + + override func setUp() { + super.setUp() + rm("foo.db") + rm("foo.db-shm") + rm("foo.db-wal") + rm("foo.db.bak.1") + rm("foo.db.bak.1-shm") + rm("foo.db.bak.1-wal") + } + + class MockFailingSchema: Schema { + var name: String { return "FAILURE" } + var version: Int { return BrowserSchema.DefaultVersion + 1 } + func drop(_ db: SQLiteDBConnection) -> Bool { + return true + } + func create(_ db: SQLiteDBConnection) -> Bool { + return false + } + func update(_ db: SQLiteDBConnection, from: Int) -> Bool { + return false + } + } + + fileprivate class MockListener { + var notification: Notification? + @objc + func onDatabaseWasRecreated(_ notification: Notification) { + self.notification = notification + } + } + + func testUpgradeV33toV34RemovesLongURLs() { + let db = BrowserDB(filename: "v33.db", schema: BrowserSchema(), files: SupportingFiles()) + let results = db.runQuery("SELECT bmkUri, title FROM bookmarksLocal WHERE type = 1", args: nil, factory: { row in + (row[0] as! String, row[1] as! String) + }).value.successValue! + + // The bookmark with the long URL has been deleted. + XCTAssertTrue(results.count == 1) + + let remaining = results[0]! + + // This one's title has been truncated to 4096 chars. + XCTAssertEqual(remaining.1.characters.count, 4096) + XCTAssertEqual(remaining.1.utf8.count, 4096) + XCTAssertTrue(remaining.1.hasPrefix("abcdefghijkl")) + XCTAssertEqual(remaining.0, "http://example.com/short") + } + + func testMovesDB() { + var db = BrowserDB(filename: "foo.db", schema: BrowserSchema(), files: self.files) + db.run("CREATE TABLE foo (bar TEXT)").succeeded() // Just so we have writes in the WAL. + + XCTAssertTrue(files.exists("foo.db")) + XCTAssertTrue(files.exists("foo.db-shm")) + XCTAssertTrue(files.exists("foo.db-wal")) + + // Grab a pointer to the -shm so we can compare later. + let shmAAttributes = try! files.attributesForFileAt(relativePath: "foo.db-shm") + let creationA = shmAAttributes[FileAttributeKey.creationDate] as! Date + let inodeA = (shmAAttributes[FileAttributeKey.systemFileNumber] as! NSNumber).uintValue + + XCTAssertFalse(files.exists("foo.db.bak.1")) + XCTAssertFalse(files.exists("foo.db.bak.1-shm")) + XCTAssertFalse(files.exists("foo.db.bak.1-wal")) + + let center = NotificationCenter.default + let listener = MockListener() + center.addObserver(listener, selector: #selector(MockListener.onDatabaseWasRecreated(_:)), name: NotificationDatabaseWasRecreated, object: nil) + defer { center.removeObserver(listener) } + + // It'll still fail, but it moved our old DB. + // Our current observation is that closing the DB deletes the .shm file and also + // checkpoints the WAL. + db.forceClose() + + db = BrowserDB(filename: "foo.db", schema: MockFailingSchema(), files: self.files) + db.run("CREATE TABLE foo (bar TEXT)").failed() // This won't actually write since we'll get a failed connection + db = BrowserDB(filename: "foo.db", schema: BrowserSchema(), files: self.files) + db.run("CREATE TABLE foo (bar TEXT)").succeeded() // Just so we have writes in the WAL. + + XCTAssertTrue(files.exists("foo.db")) + XCTAssertTrue(files.exists("foo.db-shm")) + XCTAssertTrue(files.exists("foo.db-wal")) + + // But now it's been reopened, it's not the same -shm! + let shmBAttributes = try! files.attributesForFileAt(relativePath: "foo.db-shm") + let creationB = shmBAttributes[FileAttributeKey.creationDate] as! Date + let inodeB = (shmBAttributes[FileAttributeKey.systemFileNumber] as! NSNumber).uintValue + XCTAssertTrue(creationA.compare(creationB) != ComparisonResult.orderedDescending) + XCTAssertNotEqual(inodeA, inodeB) + + XCTAssertTrue(files.exists("foo.db.bak.1")) + XCTAssertFalse(files.exists("foo.db.bak.1-shm")) + XCTAssertFalse(files.exists("foo.db.bak.1-wal")) + + // The right notification was issued. + XCTAssertEqual("foo.db", (listener.notification?.object as? String)) + } +} diff --git a/mobile/ios/StorageTests/TestLogins.swift b/mobile/ios/StorageTests/TestLogins.swift new file mode 100644 index 0000000000..8068c1dfed --- /dev/null +++ b/mobile/ios/StorageTests/TestLogins.swift @@ -0,0 +1,741 @@ +/* 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 +@testable import Storage +import XCGLogger + +import XCTest + +private let log = XCGLogger.default + +class TestSQLiteLogins: XCTestCase { + var db: BrowserDB! + var logins: SQLiteLogins! + + let formSubmitURL = "http://submit.me" + let login = Login.createWithHostname("hostname1", username: "username1", password: "password1", formSubmitURL: "http://submit.me") + + override func setUp() { + super.setUp() + + let files = MockFiles() + self.db = BrowserDB(filename: "testsqlitelogins.db", schema: LoginsSchema(), files: files) + self.logins = SQLiteLogins(db: self.db) + + let expectation = self.expectation(description: "Remove all logins.") + self.removeAllLogins().upon({ res in expectation.fulfill() }) + waitForExpectations(timeout: 10.0, handler: nil) + } + + func testAddLogin() { + log.debug("Created \(self.login)") + let expectation = self.expectation(description: "Add login") + + addLogin(login) + >>> getLoginsFor(login.protectionSpace, expected: [login]) + >>> done(expectation) + + waitForExpectations(timeout: 10.0, handler: nil) + } + + func testGetOrder() { + let expectation = self.expectation(description: "Add login") + + // Different GUID. + let login2 = Login.createWithHostname("hostname1", username: "username2", password: "password2") + login2.formSubmitURL = "http://submit.me" + + addLogin(login) >>> { self.addLogin(login2) } >>> + getLoginsFor(login.protectionSpace, expected: [login2, login]) >>> + done(expectation) + + waitForExpectations(timeout: 10.0, handler: nil) + } + + func testRemoveLogin() { + let expectation = self.expectation(description: "Remove login") + + addLogin(login) + >>> { self.removeLogin(self.login) } + >>> getLoginsFor(login.protectionSpace, expected: []) + >>> done(expectation) + + waitForExpectations(timeout: 10.0, handler: nil) + } + + func testRemoveLogins() { + let loginA = Login.createWithHostname("alphabet.com", username: "username1", password: "password1", formSubmitURL: formSubmitURL) + let loginB = Login.createWithHostname("alpha.com", username: "username2", password: "password2", formSubmitURL: formSubmitURL) + let loginC = Login.createWithHostname("berry.com", username: "username3", password: "password3", formSubmitURL: formSubmitURL) + let loginD = Login.createWithHostname("candle.com", username: "username4", password: "password4", formSubmitURL: formSubmitURL) + + func addLogins() -> Success { + addLogin(loginA).succeeded() + addLogin(loginB).succeeded() + addLogin(loginC).succeeded() + addLogin(loginD).succeeded() + return succeed() + } + + addLogins().succeeded() + let guids = [loginA.guid, loginB.guid] + logins.removeLoginsWithGUIDs(guids).succeeded() + let result = logins.getAllLogins().value.successValue! + XCTAssertEqual(result.count, 2) + } + + func testRemoveManyLogins() { + log.debug("Remove a large number of logins at once") + var guids: [GUID] = [] + for i in 0..<2000 { + let login = Login.createWithHostname("mozilla.org", username: "Fire", password: "fox", formSubmitURL: formSubmitURL) + if i <= 1000 { + guids += [login.guid] + } + addLogin(login).succeeded() + } + logins.removeLoginsWithGUIDs(guids).succeeded() + let result = logins.getAllLogins().value.successValue! + XCTAssertEqual(result.count, 999) + } + + func testUpdateLogin() { + let expectation = self.expectation(description: "Update login") + let updated = Login.createWithHostname("hostname1", username: "username1", password: "password3", formSubmitURL: formSubmitURL) + updated.guid = self.login.guid + + addLogin(login) >>> { self.updateLogin(updated) } >>> + getLoginsFor(login.protectionSpace, expected: [updated]) >>> + done(expectation) + + waitForExpectations(timeout: 10.0, handler: nil) + } + + func testAddInvalidLogin() { + let emptyPasswordLogin = Login.createWithHostname("hostname1", username: "username1", password: "", formSubmitURL: formSubmitURL) + var result = logins.addLogin(emptyPasswordLogin).value + XCTAssertNil(result.successValue) + XCTAssertNotNil(result.failureValue) + XCTAssertEqual(result.failureValue?.description, "Can't add a login with an empty password.") + + let emptyHostnameLogin = Login.createWithHostname("", username: "username1", password: "password", formSubmitURL: formSubmitURL) + result = logins.addLogin(emptyHostnameLogin).value + XCTAssertNil(result.successValue) + XCTAssertNotNil(result.failureValue) + XCTAssertEqual(result.failureValue?.description, "Can't add a login with an empty hostname.") + + let credential = URLCredential(user: "username", password: "password", persistence: .forSession) + let protectionSpace = URLProtectionSpace(host: "https://website.com", port: 443, protocol: "https", realm: "Basic Auth", authenticationMethod: "Basic Auth") + let bothFormSubmitURLAndRealm = Login.createWithCredential(credential, protectionSpace: protectionSpace) + bothFormSubmitURLAndRealm.formSubmitURL = "http://submit.me" + result = logins.addLogin(bothFormSubmitURLAndRealm).value + XCTAssertNil(result.successValue) + XCTAssertNotNil(result.failureValue) + XCTAssertEqual(result.failureValue?.description, "Can't add a login with both a httpRealm and formSubmitURL.") + + let noFormSubmitURLOrRealm = Login.createWithHostname("host", username: "username1", password: "password", formSubmitURL: nil) + result = logins.addLogin(noFormSubmitURLOrRealm).value + XCTAssertNil(result.successValue) + XCTAssertNotNil(result.failureValue) + XCTAssertEqual(result.failureValue?.description, "Can't add a login without a httpRealm or formSubmitURL.") + } + + func testUpdateInvalidLogin() { + let updated = Login.createWithHostname("hostname1", username: "username1", password: "", formSubmitURL: formSubmitURL) + updated.guid = self.login.guid + + addLogin(login).succeeded() + var result = logins.updateLoginByGUID(login.guid, new: updated, significant: true).value + XCTAssertNil(result.successValue) + XCTAssertNotNil(result.failureValue) + XCTAssertEqual(result.failureValue?.description, "Can't add a login with an empty password.") + + let emptyHostnameLogin = Login.createWithHostname("", username: "username1", password: "", formSubmitURL: formSubmitURL) + emptyHostnameLogin.guid = self.login.guid + result = logins.updateLoginByGUID(login.guid, new: emptyHostnameLogin, significant: true).value + XCTAssertNil(result.successValue) + XCTAssertNotNil(result.failureValue) + XCTAssertEqual(result.failureValue?.description, "Can't add a login with an empty hostname.") + + let credential = URLCredential(user: "username", password: "password", persistence: .forSession) + let protectionSpace = URLProtectionSpace(host: "https://website.com", port: 443, protocol: "https", realm: "Basic Auth", authenticationMethod: "Basic Auth") + let bothFormSubmitURLAndRealm = Login.createWithCredential(credential, protectionSpace: protectionSpace) + bothFormSubmitURLAndRealm.formSubmitURL = "http://submit.me" + bothFormSubmitURLAndRealm.guid = self.login.guid + result = logins.updateLoginByGUID(login.guid, new: bothFormSubmitURLAndRealm, significant: true).value + XCTAssertNil(result.successValue) + XCTAssertNotNil(result.failureValue) + XCTAssertEqual(result.failureValue?.description, "Can't add a login with both a httpRealm and formSubmitURL.") + + let noFormSubmitURLOrRealm = Login.createWithHostname("host", username: "username1", password: "password", formSubmitURL: nil) + noFormSubmitURLOrRealm.guid = self.login.guid + result = logins.updateLoginByGUID(login.guid, new: noFormSubmitURLOrRealm, significant: true).value + XCTAssertNil(result.successValue) + XCTAssertNotNil(result.failureValue) + XCTAssertEqual(result.failureValue?.description, "Can't add a login without a httpRealm or formSubmitURL.") + } + + func testSearchLogins() { + let loginA = Login.createWithHostname("alphabet.com", username: "username1", password: "password1", formSubmitURL: formSubmitURL) + let loginB = Login.createWithHostname("alpha.com", username: "username2", password: "password2", formSubmitURL: formSubmitURL) + let loginC = Login.createWithHostname("berry.com", username: "username3", password: "password3", formSubmitURL: formSubmitURL) + let loginD = Login.createWithHostname("candle.com", username: "username4", password: "password4", formSubmitURL: formSubmitURL) + + func addLogins() -> Success { + addLogin(loginA).succeeded() + addLogin(loginB).succeeded() + addLogin(loginC).succeeded() + addLogin(loginD).succeeded() + return succeed() + } + + func checkAllLogins() -> Success { + return logins.getAllLogins() >>== { results in + XCTAssertEqual(results.count, 4) + return succeed() + } + } + + func checkSearchHostnames() -> Success { + return logins.searchLoginsWithQuery("pha") >>== { results in + XCTAssertEqual(results.count, 2) + XCTAssertEqual(results[0]!.hostname, "http://alpha.com") + XCTAssertEqual(results[1]!.hostname, "http://alphabet.com") + return succeed() + } + } + + func checkSearchUsernames() -> Success { + return logins.searchLoginsWithQuery("username") >>== { results in + XCTAssertEqual(results.count, 4) + XCTAssertEqual(results[0]!.username, "username2") + XCTAssertEqual(results[1]!.username, "username1") + XCTAssertEqual(results[2]!.username, "username3") + XCTAssertEqual(results[3]!.username, "username4") + return succeed() + } + } + + func checkSearchPasswords() -> Success { + return logins.searchLoginsWithQuery("pass") >>== { results in + XCTAssertEqual(results.count, 4) + XCTAssertEqual(results[0]!.password, "password2") + XCTAssertEqual(results[1]!.password, "password1") + XCTAssertEqual(results[2]!.password, "password3") + XCTAssertEqual(results[3]!.password, "password4") + return succeed() + } + } + + XCTAssertTrue(addLogins().value.isSuccess) + + XCTAssertTrue(checkAllLogins().value.isSuccess) + XCTAssertTrue(checkSearchHostnames().value.isSuccess) + XCTAssertTrue(checkSearchUsernames().value.isSuccess) + XCTAssertTrue(checkSearchPasswords().value.isSuccess) + + XCTAssertTrue(removeAllLogins().value.isSuccess) + } + + /* + func testAddUseOfLogin() { + let expectation = self.self.expectation(description: "Add visit") + + if var usageData = login as? LoginUsageData { + usageData.timeCreated = Date.nowMicroseconds() + } + + addLogin(login) >>> + addUseDelayed(login, time: 1) >>> + getLoginDetailsFor(login, expected: login as! LoginUsageData) >>> + done(login.protectionSpace, expectation: expectation) + + waitForExpectations(timeout: 10.0, handler: nil) + } + */ + + func done(_ expectation: XCTestExpectation) -> () -> Success { + return { + self.removeAllLogins() + >>> self.getLoginsFor(self.login.protectionSpace, expected: []) + >>> { + expectation.fulfill() + return succeed() + } + } + } + + // Note: These functions are all curried so that we pass arguments, but still chain them below + func addLogin(_ login: LoginData) -> Success { + log.debug("Add \(login)") + return logins.addLogin(login) + } + + func updateLogin(_ login: LoginData) -> Success { + log.debug("Update \(login)") + return logins.updateLoginByGUID(login.guid, new: login, significant: true) + } + + func addUseDelayed(_ login: Login, time: UInt32) -> Success { + sleep(time) + login.timeLastUsed = Date.nowMicroseconds() + let res = logins.addUseOfLoginByGUID(login.guid) + sleep(time) + return res + } + + func getLoginsFor(_ protectionSpace: URLProtectionSpace, expected: [LoginData]) -> (() -> Success) { + return { + log.debug("Get logins for \(protectionSpace)") + return self.logins.getLoginsForProtectionSpace(protectionSpace) >>== { results in + XCTAssertEqual(expected.count, results.count) + for (index, login) in expected.enumerated() { + XCTAssertEqual(results[index]!.username!, login.username!) + XCTAssertEqual(results[index]!.hostname, login.hostname) + XCTAssertEqual(results[index]!.password, login.password) + } + return succeed() + } + } + } + + /* + func getLoginDetailsFor(login: LoginData, expected: LoginUsageData) -> (() -> Success) { + return { + log.debug("Get details for \(login)") + let deferred = self.logins.getUsageDataForLogin(login) + log.debug("Final result \(deferred)") + return deferred >>== { l in + log.debug("Got cursor") + XCTAssertLessThan(expected.timePasswordChanged - l.timePasswordChanged, 10) + XCTAssertLessThan(expected.timeLastUsed - l.timeLastUsed, 10) + XCTAssertLessThan(expected.timeCreated - l.timeCreated, 10) + return succeed() + } + } + } + */ + + func removeLogin(_ login: LoginData) -> Success { + log.debug("Remove \(login)") + return logins.removeLoginByGUID(login.guid) + } + + func removeAllLogins() -> Success { + log.debug("Remove All") + // Because we don't want to just mark them as deleted. + return self.db.run("DELETE FROM \(TableLoginsMirror)") >>> { self.db.run("DELETE FROM \(TableLoginsLocal)") } + } +} + +class TestSQLiteLoginsPerf: XCTestCase { + var db: BrowserDB! + var logins: SQLiteLogins! + + override func setUp() { + super.setUp() + let files = MockFiles() + self.db = BrowserDB(filename: "testsqlitelogins.db", schema: LoginsSchema(), files: files) + self.logins = SQLiteLogins(db: self.db) + } + + func testLoginsSearchMatchOnePerf() { + populateTestLogins() + + // Measure time to find one entry amongst the 1000 of them + self.measureMetrics([XCTPerformanceMetric_WallClockTime], automaticallyStartMeasuring: true) { + for _ in 0...5 { + self.logins.searchLoginsWithQuery("username500").succeeded() + } + self.stopMeasuring() + } + + XCTAssertTrue(removeAllLogins().value.isSuccess) + } + + func testLoginsSearchMatchAllPerf() { + populateTestLogins() + + // Measure time to find all matching results + self.measureMetrics([XCTPerformanceMetric_WallClockTime], automaticallyStartMeasuring: true) { + for _ in 0...5 { + self.logins.searchLoginsWithQuery("username").succeeded() + } + self.stopMeasuring() + } + + XCTAssertTrue(removeAllLogins().value.isSuccess) + } + + func testLoginsGetAllPerf() { + populateTestLogins() + + // Measure time to find all matching results + self.measureMetrics([XCTPerformanceMetric_WallClockTime], automaticallyStartMeasuring: true) { + for _ in 0...5 { + self.logins.getAllLogins().succeeded() + } + self.stopMeasuring() + } + + XCTAssertTrue(removeAllLogins().value.isSuccess) + } + + func populateTestLogins() { + for i in 0..<1000 { + let login = Login.createWithHostname("website\(i).com", username: "username\(i)", password: "password\(i)", formSubmitURL: "test") + addLogin(login).succeeded() + } + } + + func addLogin(_ login: LoginData) -> Success { + return logins.addLogin(login) + } + + func removeAllLogins() -> Success { + log.debug("Remove All") + // Because we don't want to just mark them as deleted. + return self.db.run("DELETE FROM \(TableLoginsMirror)") >>> { self.db.run("DELETE FROM \(TableLoginsLocal)") } + } +} + +class TestSyncableLogins: XCTestCase { + var db: BrowserDB! + var logins: SQLiteLogins! + + override func setUp() { + super.setUp() + + let files = MockFiles() + self.db = BrowserDB(filename: "testsyncablelogins.db", schema: LoginsSchema(), files: files) + self.logins = SQLiteLogins(db: self.db) + + let expectation = self.expectation(description: "Remove all logins.") + self.removeAllLogins().upon({ res in expectation.fulfill() }) + waitForExpectations(timeout: 10.0, handler: nil) + } + + func removeAllLogins() -> Success { + log.debug("Remove All") + // Because we don't want to just mark them as deleted. + return self.db.run("DELETE FROM \(TableLoginsMirror)") >>> { self.db.run("DELETE FROM \(TableLoginsLocal)") } + } + + func testDiffers() { + let guid = "abcdabcdabcd" + let host = "http://example.com" + let user = "username" + let loginA1 = Login(guid: guid, hostname: host, username: user, password: "password1") + loginA1.formSubmitURL = "\(host)/form1/" + loginA1.usernameField = "afield" + + let loginA2 = Login(guid: guid, hostname: host, username: user, password: "password1") + loginA2.formSubmitURL = "\(host)/form1/" + loginA2.usernameField = "somefield" + + let loginB = Login(guid: guid, hostname: host, username: user, password: "password2") + loginB.formSubmitURL = "\(host)/form1/" + + let loginC = Login(guid: guid, hostname: host, username: user, password: "password") + loginC.formSubmitURL = "\(host)/form2/" + + XCTAssert(loginA1.isSignificantlyDifferentFrom(loginB)) + XCTAssert(loginA1.isSignificantlyDifferentFrom(loginC)) + XCTAssert(loginA2.isSignificantlyDifferentFrom(loginB)) + XCTAssert(loginA2.isSignificantlyDifferentFrom(loginC)) + XCTAssert(!loginA1.isSignificantlyDifferentFrom(loginA2)) + } + + func testLocalNewStaysNewAndIsRemoved() { + let guidA = "abcdabcdabcd" + let loginA1 = Login(guid: guidA, hostname: "http://example.com", username: "username", password: "password") + loginA1.formSubmitURL = "http://example.com/form/" + loginA1.timesUsed = 1 + XCTAssertTrue((self.logins as BrowserLogins).addLogin(loginA1).value.isSuccess) + + let local1 = self.logins.getExistingLocalRecordByGUID(guidA).value.successValue! + XCTAssertNotNil(local1) + XCTAssertEqual(local1!.guid, guidA) + XCTAssertEqual(local1!.syncStatus, SyncStatus.new) + XCTAssertEqual(local1!.timesUsed, 1) + + XCTAssertTrue(self.logins.addUseOfLoginByGUID(guidA).value.isSuccess) + + // It's still new. + let local2 = self.logins.getExistingLocalRecordByGUID(guidA).value.successValue! + XCTAssertNotNil(local2) + XCTAssertEqual(local2!.guid, guidA) + XCTAssertEqual(local2!.syncStatus, SyncStatus.new) + XCTAssertEqual(local2!.timesUsed, 2) + + // It's removed immediately, because it was never synced. + XCTAssertTrue((self.logins as BrowserLogins).removeLoginByGUID(guidA).value.isSuccess) + XCTAssertNil(self.logins.getExistingLocalRecordByGUID(guidA).value.successValue!) + } + + func testApplyLogin() { + let guidA = "abcdabcdabcd" + let loginA1 = ServerLogin(guid: guidA, hostname: "http://example.com", username: "username", password: "password", modified: 1234) + loginA1.formSubmitURL = "http://example.com/form/" + loginA1.timesUsed = 3 + + XCTAssertTrue(self.logins.applyChangedLogin(loginA1).value.isSuccess) + + let local = self.logins.getExistingLocalRecordByGUID(guidA).value.successValue! + let mirror = self.logins.getExistingMirrorRecordByGUID(guidA).value.successValue! + + XCTAssertTrue(nil == local) + XCTAssertTrue(nil != mirror) + + XCTAssertEqual(mirror!.guid, guidA) + XCTAssertFalse(mirror!.isOverridden) + XCTAssertEqual(mirror!.serverModified, Timestamp(1234), "Timestamp matches.") + XCTAssertEqual(mirror!.timesUsed, 3) + XCTAssertTrue(nil == mirror!.httpRealm) + XCTAssertTrue(nil == mirror!.passwordField) + XCTAssertTrue(nil == mirror!.usernameField) + XCTAssertEqual(mirror!.formSubmitURL!, "http://example.com/form/") + XCTAssertEqual(mirror!.hostname, "http://example.com") + XCTAssertEqual(mirror!.username!, "username") + XCTAssertEqual(mirror!.password, "password") + + // Change it. + let loginA2 = ServerLogin(guid: guidA, hostname: "http://example.com", username: "username", password: "newpassword", modified: 2234) + loginA2.formSubmitURL = "http://example.com/form/" + loginA2.timesUsed = 4 + + XCTAssertTrue(self.logins.applyChangedLogin(loginA2).value.isSuccess) + let changed = self.logins.getExistingMirrorRecordByGUID(guidA).value.successValue! + + XCTAssertTrue(nil != changed) + XCTAssertFalse(changed!.isOverridden) + XCTAssertEqual(changed!.serverModified, Timestamp(2234), "Timestamp is new.") + XCTAssertEqual(changed!.username!, "username") + XCTAssertEqual(changed!.password, "newpassword") + XCTAssertEqual(changed!.timesUsed, 4) + + // Change it locally. + let preUse = Date.now() + XCTAssertTrue(self.logins.addUseOfLoginByGUID(guidA).value.isSuccess) + + let localUsed = self.logins.getExistingLocalRecordByGUID(guidA).value.successValue! + let mirrorUsed = self.logins.getExistingMirrorRecordByGUID(guidA).value.successValue! + + XCTAssertNotNil(localUsed) + XCTAssertNotNil(mirrorUsed) + + XCTAssertEqual(mirrorUsed!.guid, guidA) + XCTAssertEqual(localUsed!.guid, guidA) + XCTAssertEqual(mirrorUsed!.password, "newpassword") + XCTAssertEqual(localUsed!.password, "newpassword") + + XCTAssertTrue(mirrorUsed!.isOverridden) // It's now overridden. + XCTAssertEqual(mirrorUsed!.serverModified, Timestamp(2234), "Timestamp is new.") + + XCTAssertTrue(localUsed!.localModified >= preUse) // Local record is modified. + XCTAssertEqual(localUsed!.syncStatus, SyncStatus.synced) // Uses aren't enough to warrant upload. + + // Uses are local until reconciled. + XCTAssertEqual(localUsed!.timesUsed, 5) + XCTAssertEqual(mirrorUsed!.timesUsed, 4) + + // Change the password and form URL locally. + let newLocalPassword = Login(guid: guidA, hostname: "http://example.com", username: "username", password: "yupyup") + newLocalPassword.formSubmitURL = "http://example.com/form2/" + + let preUpdate = Date.now() + + // Updates always bump our usages, too. + XCTAssertTrue(self.logins.updateLoginByGUID(guidA, new: newLocalPassword, significant: true).value.isSuccess) + + let localAltered = self.logins.getExistingLocalRecordByGUID(guidA).value.successValue! + let mirrorAltered = self.logins.getExistingMirrorRecordByGUID(guidA).value.successValue! + + XCTAssertFalse(mirrorAltered!.isSignificantlyDifferentFrom(mirrorUsed!)) // The mirror is unchanged. + XCTAssertFalse(mirrorAltered!.isSignificantlyDifferentFrom(localUsed!)) + XCTAssertTrue(mirrorAltered!.isOverridden) // It's still overridden. + + XCTAssertTrue(localAltered!.isSignificantlyDifferentFrom(localUsed!)) + XCTAssertEqual(localAltered!.password, "yupyup") + XCTAssertEqual(localAltered!.formSubmitURL!, "http://example.com/form2/") + XCTAssertTrue(localAltered!.localModified >= preUpdate) + XCTAssertEqual(localAltered!.syncStatus, SyncStatus.changed) // Changes are enough to warrant upload. + XCTAssertEqual(localAltered!.timesUsed, 6) + XCTAssertEqual(mirrorAltered!.timesUsed, 4) + } + + func testDeltas() { + // Shared. + let guidA = "abcdabcdabcd" + let loginA1 = ServerLogin(guid: guidA, hostname: "http://example.com", username: "username", password: "password", modified: 1234) + loginA1.timeCreated = 1200 + loginA1.timeLastUsed = 1234 + loginA1.timePasswordChanged = 1200 + loginA1.formSubmitURL = "http://example.com/form/" + loginA1.timesUsed = 3 + + let a1a1 = loginA1.deltas(from: loginA1) + XCTAssertEqual(0, a1a1.nonCommutative.count) + XCTAssertEqual(0, a1a1.nonConflicting.count) + XCTAssertEqual(0, a1a1.commutative.count) + + let loginA2 = ServerLogin(guid: guidA, hostname: "http://example.com", username: "username", password: "password", modified: 1235) + loginA2.timeCreated = 1200 + loginA2.timeLastUsed = 1235 + loginA2.timePasswordChanged = 1200 + loginA2.timesUsed = 4 + + let a1a2 = loginA2.deltas(from: loginA1) + + XCTAssertEqual(2, a1a2.nonCommutative.count) + XCTAssertEqual(0, a1a2.nonConflicting.count) + XCTAssertEqual(1, a1a2.commutative.count) + + switch a1a2.commutative[0] { + case let .timesUsed(increment): + XCTAssertEqual(increment, 1) + break + } + switch a1a2.nonCommutative[0] { + case let .formSubmitURL(to): + XCTAssertNil(to) + break + default: + XCTFail("Unexpected non-commutative login field.") + } + switch a1a2.nonCommutative[1] { + case let .timeLastUsed(to): + XCTAssertEqual(to, 1235) + break + default: + XCTFail("Unexpected non-commutative login field.") + } + + let loginA3 = ServerLogin(guid: guidA, hostname: "http://example.com", username: "username", password: "something else", modified: 1280) + loginA3.timeCreated = 1200 + loginA3.timeLastUsed = 1250 + loginA3.timePasswordChanged = 1250 + loginA3.formSubmitURL = "http://example.com/form/" + loginA3.timesUsed = 5 + + let a1a3 = loginA3.deltas(from: loginA1) + + XCTAssertEqual(3, a1a3.nonCommutative.count) + XCTAssertEqual(0, a1a3.nonConflicting.count) + XCTAssertEqual(1, a1a3.commutative.count) + + switch a1a3.commutative[0] { + case let .timesUsed(increment): + XCTAssertEqual(increment, 2) + break + } + + switch a1a3.nonCommutative[0] { + case let .password(to): + XCTAssertEqual("something else", to) + break + default: + XCTFail("Unexpected non-commutative login field.") + } + switch a1a3.nonCommutative[1] { + case let .timeLastUsed(to): + XCTAssertEqual(to, 1250) + break + default: + XCTFail("Unexpected non-commutative login field.") + } + switch a1a3.nonCommutative[2] { + case let .timePasswordChanged(to): + XCTAssertEqual(to, 1250) + break + default: + XCTFail("Unexpected non-commutative login field.") + } + + // Now apply the deltas to the original record and check that they match! + XCTAssertFalse(loginA1.applyDeltas(a1a2).isSignificantlyDifferentFrom(loginA2)) + XCTAssertFalse(loginA1.applyDeltas(a1a3).isSignificantlyDifferentFrom(loginA3)) + + let merged = Login.mergeDeltas(a: (loginA2.serverModified, a1a2), b: (loginA3.serverModified, a1a3)) + let mCCount = merged.commutative.count + let a2CCount = a1a2.commutative.count + let a3CCount = a1a3.commutative.count + XCTAssertEqual(mCCount, a2CCount + a3CCount) + + let mNCount = merged.nonCommutative.count + let a2NCount = a1a2.nonCommutative.count + let a3NCount = a1a3.nonCommutative.count + XCTAssertLessThanOrEqual(mNCount, a2NCount + a3NCount) + XCTAssertGreaterThanOrEqual(mNCount, max(a2NCount, a3NCount)) + + let mFCount = merged.nonConflicting.count + let a2FCount = a1a2.nonConflicting.count + let a3FCount = a1a3.nonConflicting.count + XCTAssertLessThanOrEqual(mFCount, a2FCount + a3FCount) + XCTAssertGreaterThanOrEqual(mFCount, max(a2FCount, a3FCount)) + + switch merged.commutative[0] { + case let .timesUsed(increment): + XCTAssertEqual(1, increment) + } + switch merged.commutative[1] { + case let .timesUsed(increment): + XCTAssertEqual(2, increment) + } + + switch merged.nonCommutative[0] { + case let .password(to): + XCTAssertEqual("something else", to) + break + default: + XCTFail("Unexpected non-commutative login field.") + } + switch merged.nonCommutative[1] { + case let .formSubmitURL(to): + XCTAssertNil(to) + break + default: + XCTFail("Unexpected non-commutative login field.") + } + switch merged.nonCommutative[2] { + case let .timeLastUsed(to): + XCTAssertEqual(to, 1250) + break + default: + XCTFail("Unexpected non-commutative login field.") + } + switch merged.nonCommutative[3] { + case let .timePasswordChanged(to): + XCTAssertEqual(to, 1250) + break + default: + XCTFail("Unexpected non-commutative login field.") + } + + // Applying the merged deltas gives us the expected login. + let expected = Login(guid: guidA, hostname: "http://example.com", username: "username", password: "something else") + expected.timeCreated = 1200 + expected.timeLastUsed = 1250 + expected.timePasswordChanged = 1250 + expected.formSubmitURL = nil + expected.timesUsed = 6 + + let applied = loginA1.applyDeltas(merged) + XCTAssertFalse(applied.isSignificantlyDifferentFrom(expected)) + XCTAssertFalse(expected.isSignificantlyDifferentFrom(applied)) + } + + func testLoginsIsSynced() { + let loginA = Login.createWithHostname("alphabet.com", username: "username1", password: "password1") + let serverLoginA = ServerLogin(guid: loginA.guid, hostname: "alpha.com", username: "username1", password: "password1", modified: Date.now()) + + XCTAssertFalse(logins.hasSyncedLogins().value.successValue ?? true) + let _ = logins.addLogin(loginA).value + XCTAssertFalse(logins.hasSyncedLogins().value.successValue ?? false) + let _ = logins.applyChangedLogin(serverLoginA).value + XCTAssertTrue(logins.hasSyncedLogins().value.successValue ?? false) + } +} diff --git a/mobile/ios/StorageTests/TestSQLiteBookmarks.swift b/mobile/ios/StorageTests/TestSQLiteBookmarks.swift new file mode 100644 index 0000000000..89e7834b26 --- /dev/null +++ b/mobile/ios/StorageTests/TestSQLiteBookmarks.swift @@ -0,0 +1,1150 @@ +/* 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 +@testable import Storage + +import XCTest + +private func getBrowserDB(_ filename: String, files: FileAccessor) -> BrowserDB? { + return BrowserDB(filename: filename, schema: BrowserSchema(), files: files) +} + +extension SQLiteBookmarks { + var testFactory: SQLiteBookmarksModelFactory { + return SQLiteBookmarksModelFactory(bookmarks: self, direction: .local) + } +} + +// MARK: - Tests. + +class TestTruncation: XCTestCase { + func testTruncate() { + let a = "🤠" + let b = "abcdefghi" + let c = "" + XCTAssertEqual(a.truncateToUTF8ByteCount(1), "") + XCTAssertEqual(a.truncateToUTF8ByteCount(4), a) + XCTAssertEqual(a.truncateToUTF8ByteCount(16), a) + + XCTAssertEqual(b.truncateToUTF8ByteCount(16), b) + XCTAssertEqual(b.truncateToUTF8ByteCount(5), "abcde") + XCTAssertEqual(b.truncateToUTF8ByteCount(0), "") + XCTAssertEqual(c.truncateToUTF8ByteCount(1), c) + XCTAssertEqual(c.truncateToUTF8ByteCount(4), c) + } +} + +class BrowserDBV15: Schema { + var name: String = "BROWSER" + var version: Int = 15 + + func getHistoryTableCreationString() -> String { + return "CREATE TABLE IF NOT EXISTS history (" + + "id INTEGER PRIMARY KEY AUTOINCREMENT, " + + "guid TEXT NOT NULL UNIQUE, " + // Not null, but the value might be replaced by the server's. + "url TEXT UNIQUE, " + // May only be null for deleted records. + "title TEXT NOT NULL, " + + "server_modified INTEGER, " + // Can be null. Integer milliseconds. + "local_modified INTEGER, " + // Can be null. Client clock. In extremis only. + "is_deleted TINYINT NOT NULL, " + // Boolean. Locally deleted. + "should_upload TINYINT NOT NULL, " + // Boolean. Set when changed or visits added. + "domain_id INTEGER REFERENCES \(TableDomains)(id) ON DELETE CASCADE, " + + "CONSTRAINT urlOrDeleted CHECK (url IS NOT NULL OR is_deleted = 1)" + + ")" + } + + func getDomainsTableCreationString() -> String { + return "CREATE TABLE IF NOT EXISTS \(TableDomains) (" + + "id INTEGER PRIMARY KEY AUTOINCREMENT, " + + "domain TEXT NOT NULL UNIQUE, " + + "showOnTopSites TINYINT NOT NULL DEFAULT 1" + + ")" + } + + func getQueueTableCreationString() -> String { + return "CREATE TABLE IF NOT EXISTS \(TableQueuedTabs) (" + + "url TEXT NOT NULL UNIQUE, " + + "title TEXT" + + ") " + } + + func getVisitsTableCreationString() -> String { + return "CREATE TABLE IF NOT EXISTS \(TableVisits) (" + + "id INTEGER PRIMARY KEY AUTOINCREMENT, " + + "siteID INTEGER NOT NULL REFERENCES history(id) ON DELETE CASCADE, " + + "date REAL NOT NULL, " + // Microseconds since epoch. + "type INTEGER NOT NULL, " + + "is_local TINYINT NOT NULL, " + // Some visits are local. Some are remote ('mirrored'). This boolean flag is the split. + "UNIQUE (siteID, date, type) " + + ") " + } + + func getFaviconsTableCreationString() -> String { + return "CREATE TABLE IF NOT EXISTS favicons (" + + "id INTEGER PRIMARY KEY AUTOINCREMENT, " + + "url TEXT NOT NULL UNIQUE, " + + "width INTEGER, " + + "height INTEGER, " + + "type INTEGER NOT NULL, " + + "date REAL NOT NULL" + + ") " + } + + func getBookmarksTableCreationStringForTable(_ table: String, withAdditionalColumns: String="") -> String { + // The stupid absence of naming conventions here is thanks to pre-Sync Weave. Sorry. + // For now we have the simplest possible schema: everything in one. + let sql = + "CREATE TABLE IF NOT EXISTS \(table) " + + + // Shared fields. + "( id INTEGER PRIMARY KEY AUTOINCREMENT" + + ", guid TEXT NOT NULL UNIQUE" + + ", type TINYINT NOT NULL" + // Type enum. + + // Record/envelope metadata that'll allow us to do merges. + ", is_deleted TINYINT NOT NULL DEFAULT 0" + // Boolean + + ", parentid TEXT" + // GUID + ", parentName TEXT" + + + // Type-specific fields. These should be NOT NULL in many cases, but we're going + // for a sparse schema, so this'll do for now. Enforce these in the application code. + ", feedUri TEXT, siteUri TEXT" + // LIVEMARKS + ", pos INT" + // SEPARATORS + ", title TEXT, description TEXT" + // FOLDERS, BOOKMARKS, QUERIES + ", bmkUri TEXT, tags TEXT, keyword TEXT" + // BOOKMARKS, QUERIES + ", folderName TEXT, queryId TEXT" + // QUERIES + withAdditionalColumns + + ", CONSTRAINT parentidOrDeleted CHECK (parentid IS NOT NULL OR is_deleted = 1)" + + ", CONSTRAINT parentNameOrDeleted CHECK (parentName IS NOT NULL OR is_deleted = 1)" + + ")" + + return sql + } + + func getBookmarksStructureTableCreationStringForTable(_ table: String, referencingMirror mirror: String) -> String { + let sql = + "CREATE TABLE IF NOT EXISTS \(table) " + + "( parent TEXT NOT NULL REFERENCES \(mirror)(guid) ON DELETE CASCADE" + + ", child TEXT NOT NULL" + // Should be the GUID of a child. + ", idx INTEGER NOT NULL" + // Should advance from 0. + ")" + + return sql + } + + let iconColumns = ", faviconID INTEGER REFERENCES \(TableFavicons)(id) ON DELETE SET NULL" + let mirrorColumns = ", is_overridden TINYINT NOT NULL DEFAULT 0" + + let serverColumns = ", server_modified INTEGER NOT NULL" + // Milliseconds. + ", hasDupe TINYINT NOT NULL DEFAULT 0" // Boolean, 0 (false) if deleted. + + let localColumns = ", local_modified INTEGER" + // Can be null. Client clock. In extremis only. + ", sync_status TINYINT NOT NULL" // SyncStatus enum. Set when changed or created. + + func create(_ db: SQLiteDBConnection) -> Bool { + let bookmarksLocal = self.getBookmarksTableCreationStringForTable(TableBookmarksLocal, withAdditionalColumns: self.localColumns + self.iconColumns) + let bookmarksLocalStructure = self.getBookmarksStructureTableCreationStringForTable(TableBookmarksLocalStructure, referencingMirror: TableBookmarksLocal) + let bookmarksBuffer = getBookmarksTableCreationStringForTable(TableBookmarksBuffer, withAdditionalColumns: self.serverColumns) + let bookmarksBufferStructure = self.getBookmarksStructureTableCreationStringForTable(TableBookmarksBufferStructure, referencingMirror: TableBookmarksBuffer) + let bookmarksMirror = getBookmarksTableCreationStringForTable(TableBookmarksMirror, withAdditionalColumns: self.serverColumns + self.mirrorColumns + self.iconColumns) + let bookmarksMirrorStructure = self.getBookmarksStructureTableCreationStringForTable(TableBookmarksMirrorStructure, referencingMirror: TableBookmarksMirror) + + let stmts = [ + bookmarksLocal, + bookmarksLocalStructure, + bookmarksBuffer, + bookmarksBufferStructure, + bookmarksMirror, + bookmarksMirrorStructure, + self.getHistoryTableCreationString(), + self.getDomainsTableCreationString(), + self.getQueueTableCreationString(), + self.getVisitsTableCreationString(), + self.getFaviconsTableCreationString() + ] + do { + for sql in stmts { + try db.executeChange(sql, withArgs: nil) + } + } catch _ as NSError { + return false + } + return true + } + + func update(_ db: SQLiteDBConnection, from: Int) -> Bool { + // Do nothing + return false + } + + func drop(_ db: SQLiteDBConnection) -> Bool { + // Do nothing + return false + } + + +} + +class TestSQLiteBookmarks: XCTestCase { + let files = MockFiles() + + fileprivate func remove(_ path: String) { + do { + try self.files.remove(path) + } catch {} + } + + override func tearDown() { + self.remove("TSQLBtestBookmarks.db") + self.remove("TSQLBtestBufferStorage.db") + self.remove("TSQLBtestLocalAndMirror.db") + self.remove("TSQLBtestRecursiveAndURLDelete.db") + self.remove("TSQLBtestUnrooted.db") + self.remove("TSQLBtestTreeBuilding.db") + self.remove("TSQLBtestLocalBookmarksModifications.db") + self.remove("TSQLBtestApplyBufferUpdatedCompletionOp.db") + self.remove("TSQLBtestApplyRecordsPendingDeletions.db") + self.remove("TSQLBtestDBUpgradeBeforeDateAdded.db") + super.tearDown() + } + + func testDateAddedMigration() { + let schema = BrowserDBV15() + let destination = BrowserSchema() + var db = BrowserDB(filename: "TSQLBtestDBUpgradeBeforeDateAdded.db", schema: schema, files: files) + XCTAssertTrue(db.withConnection({ connection -> Int in + connection.version + }).value.successValue == schema.version, "Creating BrowserSchema beforeDateAdded") + db.forceClose() + + db = BrowserDB(filename: "TSQLBtestDBUpgradeBeforeDateAdded.db", schema: destination, files: files) + XCTAssertTrue(db.withConnection({ connection -> Int in + connection.version + }).value.successValue == destination.version, "Upgrading BrowserSchema to the latest version") + db.forceClose() + } + + func testBookmarks() { + guard let db = getBrowserDB("TSQLBtestBookmarks.db", files: self.files) else { + XCTFail("Unable to create browser DB.") + return + } + let bookmarks = SQLiteBookmarks(db: db) + let factory = bookmarks.testFactory + + let url = "http://url1/" + let u = url.asURL! + + bookmarks.addToMobileBookmarks(u, title: "Title", favicon: nil).succeeded() + let model = factory.modelForFolder(BookmarkRoots.MobileFolderGUID).value.successValue + XCTAssertEqual((model?.current[0] as? BookmarkItem)?.url, url) + XCTAssertTrue(factory.isBookmarked(url).value.successValue ?? false) + factory.removeByURL("").succeeded() + + // Grab that GUID and move it into desktop bookmarks. + let guid = (model?.current[0] as! BookmarkItem).guid + + // Desktop bookmarks. + XCTAssertFalse(factory.hasDesktopBookmarks().value.successValue ?? true) + let toolbar = BookmarkRoots.ToolbarFolderGUID + XCTAssertTrue(bookmarks.db.run([ + "UPDATE \(TableBookmarksLocal) SET parentid = '\(toolbar)' WHERE guid = '\(guid)'", + "UPDATE \(TableBookmarksLocalStructure) SET parent = '\(toolbar)' WHERE child = '\(guid)'", + ]).value.isSuccess) + XCTAssertTrue(factory.hasDesktopBookmarks().value.successValue ?? true) + } + + func testGetLocalBookmarksModifications() { + guard let db = getBrowserDB("TSQLBtestLocalBookmarksModifications.db", files: self.files) else { + XCTFail("Unable to create browser DB.") + return + } + let bookmarks = SQLiteBookmarks(db: db) + + let localQuery = + "INSERT INTO \(TableBookmarksLocal) (guid, type, bmkUri, title, parentid, parentName, sync_status) " + + "VALUES " + + "(?, \(BookmarkNodeType.folder.rawValue), NULL, ?, ?, '', 2), " + + "(?, \(BookmarkNodeType.bookmark.rawValue), ?, ?, ?, '', 2), " + + "(?, \(BookmarkNodeType.bookmark.rawValue), ?, ?, ?, '', 2), " + + "(?, \(BookmarkNodeType.bookmark.rawValue), ?, ?, ?, '', 2), " + + "(?, \(BookmarkNodeType.bookmark.rawValue), ?, ?, ?, '', 0), " + + "(?, \(BookmarkNodeType.bookmark.rawValue), ?, ?, ?, '', 2), " + + "(?, \(BookmarkNodeType.bookmark.rawValue), ?, ?, ?, '', 2), " + + "(?, \(BookmarkNodeType.bookmark.rawValue), ?, ?, ?, '', 2) " + + let localArgs: Args = [ + "folder123", "123 (nok)", BookmarkRoots.MobileFolderGUID, + "bookmark123", "http://example.org/1", "Bookmark in folder 123 (nok)", "folder123", + "bookmark_other_folder", "http://example.org/2", "Bookmark in another folder (nok)", BookmarkRoots.ToolbarFolderGUID, + "bookmark_good_nonsynced", "http://example.org/3", "Bookmark 1 (ok)", BookmarkRoots.MobileFolderGUID, + "bookmark_good_synced", "http://example.org/4", "Bookmark 2 (nok)", BookmarkRoots.MobileFolderGUID, + "bookmark_duplicate_in_buffer", "http://example.org/5", "Bookmark in buffer(nok)", BookmarkRoots.MobileFolderGUID, + "bookmark_good_additional", "http://example.org/6", "Bookmark additional 1", BookmarkRoots.MobileFolderGUID, + "bookmark_good_additional_over_limit", "http://example.org/7", "Bookmark additional 2", BookmarkRoots.MobileFolderGUID, + ] + + let structureQuery = + "INSERT INTO \(TableBookmarksLocalStructure) (parent, child, idx) VALUES " + + "(?, ?, ?), " + + "(?, ?, ?), " + + "(?, ?, ?), " + + "(?, ?, ?), " + + "(?, ?, ?), " + + "(?, ?, ?), " + + "(?, ?, ?), " + + "(?, ?, ?) " + + let structureArgs: Args = [ + BookmarkRoots.MobileFolderGUID, "folder123", 0, + "folder123", "bookmark123", 0, + BookmarkRoots.ToolbarFolderGUID, "bookmark_other_folder", 0, + BookmarkRoots.MobileFolderGUID, "bookmark_good_nonsynced", 1, + BookmarkRoots.MobileFolderGUID, "bookmark_good_synced", 2, + BookmarkRoots.MobileFolderGUID, "bookmark_duplicate_in_buffer", 3, + BookmarkRoots.MobileFolderGUID, "bookmark_good_additional", 4, + BookmarkRoots.MobileFolderGUID, "bookmark_good_additional_over_limit", 5, + ] + + let bufferQuery = + "INSERT INTO \(TableBookmarksBuffer) (guid, type, bmkUri, title, parentid, parentName, server_modified) VALUES " + + "(?, \(BookmarkNodeType.bookmark.rawValue), ?, ?, ?, '', ?), " + + "(?, \(BookmarkNodeType.bookmark.rawValue), ?, ?, ?, '', ?) " + + let bufferArgs: Args = [ + "bookmark_duplicate_in_buffer", "http://example.org/5", "Bookmark in buffer(nok)", BookmarkRoots.MobileFolderGUID, Date.now(), + "bookmark_to_delete", "http://example.org/delme", "Bookmark in buffer to delete(ok)", BookmarkRoots.MobileFolderGUID, 88888 + ] + + let bufferStructureQuery = + "INSERT INTO \(TableBookmarksBufferStructure) (parent, child, idx) VALUES " + + "(?, ?, ?), " + + "(?, ?, ?)" + + let bufferStructureArgs: Args = [ + "bookmark_duplicate_in_buffer", "bookmark_duplicate_in_buffer", 0, // It's its own parent because we are lazy. + "bookmark_to_delete", "bookmark_to_delete", 0 + ] + + let pendingDeletionsQuery = + "INSERT INTO \(TablePendingBookmarksDeletions) (id) VALUES " + + "(?)" + + let pendingDeletionsArgs: Args = [ + "bookmark_to_delete" + ] + + db.run([ + (sql: localQuery, args: localArgs), + (sql: structureQuery, args: structureArgs), + (sql: bufferQuery, args: bufferArgs), + (sql: bufferStructureQuery, args: bufferStructureArgs), + (sql: pendingDeletionsQuery, args: pendingDeletionsArgs), + ]).succeeded() + + var modifications = bookmarks.getLocalBookmarksModifications(limit: 3).value.successValue! + XCTAssertEqual(modifications.additions.count, 2) + XCTAssertEqual(modifications.additions.map { $0.guid }, ["bookmark_good_nonsynced", "bookmark_good_additional"]) + XCTAssertEqual(modifications.deletions.count, 1) + XCTAssertEqual(modifications.deletions, ["bookmark_to_delete"]) + + // Deletions are prioritized. + modifications = bookmarks.getLocalBookmarksModifications(limit: 1).value.successValue! + XCTAssertEqual(modifications.additions.count, 0) + XCTAssertEqual(modifications.deletions.count, 1) + XCTAssertEqual(modifications.deletions, ["bookmark_to_delete"]) + } + + func testApplyRecordsRemovesPendingDeletions() { + guard let db = getBrowserDB("TSQLBtestApplyRecordsPendingDeletions.db", files: self.files) else { + XCTFail("Unable to create browser DB.") + return + } + let bookmarks = MergedSQLiteBookmarks(db: db) + + let bufferQuery = + "INSERT INTO \(TableBookmarksBuffer) (guid, type, bmkUri, title, parentid, parentName, server_modified) VALUES " + + "(?, \(BookmarkNodeType.bookmark.rawValue), ?, ?, ?, '', ?), " + + "(?, \(BookmarkNodeType.bookmark.rawValue), ?, ?, ?, '', ?) " + + let bufferArgs: Args = [ + "bkm1", "http://example.org/1", "Bookmark 1", BookmarkRoots.MobileFolderGUID, Date.now(), + "bkm2", "http://example.org/2", "Bookmark 2", BookmarkRoots.MobileFolderGUID, Date.now() + ] + + let bufferStructureQuery = + "INSERT INTO \(TableBookmarksBufferStructure) (parent, child, idx) VALUES " + + "(?, ?, ?), " + + "(?, ?, ?)" + + let bufferStructureArgs: Args = [ + "bkm1", "bkm1", 0, // It's its own parent because we are lazy. + "bkm2", "bkm2", 0 + ] + + let pendingDeletionsQuery = + "INSERT INTO \(TablePendingBookmarksDeletions) (id) VALUES " + + "(?)" + + let pendingDeletionsArgs: Args = [ + "bkm2" + ] + + db.run([ + (sql: bufferQuery, args: bufferArgs), + (sql: bufferStructureQuery, args: bufferStructureArgs), + (sql: pendingDeletionsQuery, args: pendingDeletionsArgs), + ]).succeeded() + + let modified: [BookmarkMirrorItem] = [BookmarkMirrorItem.bookmark("bkm2", dateAdded: Date.now(), modified: Date.now(), hasDupe: false, parentID: "bkm2", parentName: nil, title: "BKM 2", description: nil, URI: "https://test.com", tags: "", keyword: nil)] + bookmarks.applyRecords(modified).succeeded() + + XCTAssertTrue(db.queryReturnsNoResults("SELECT * FROM \(TablePendingBookmarksDeletions)").value.successValue!) + } + + func testApplyBufferUpdatedCompletionOp() { + guard let db = getBrowserDB("TSQLBtestApplyBufferUpdatedCompletionOp.db", files: self.files) else { + XCTFail("Unable to create browser DB.") + return + } + let bookmarks = SQLiteBookmarks(db: db) + bookmarks.addToMobileBookmarks("http://example.org/1".asURL!, title: "Bookmark 1", favicon: nil).succeeded() + bookmarks.addToMobileBookmarks("http://example.org/2".asURL!, title: "Bookmark 2", favicon: nil).succeeded() + bookmarks.addToMobileBookmarks("http://example.org/3".asURL!, title: "Bookmark 3", favicon: nil).succeeded() + bookmarks.addToMobileBookmarks("http://example.org/4".asURL!, title: "Bookmark 4", favicon: nil).succeeded() + var localTree = bookmarks.treeForLocal().value.successValue! + var mobileFolderNode = localTree.find(BookmarkRoots.MobileFolderGUID)! + let localChildrenGUIDs = mobileFolderNode.children!.map { $0.recordGUID } + XCTAssertEqual(localChildrenGUIDs.count, 4) + let childrenGUIDsUploaded = localChildrenGUIDs.dropLast(1) + let childrenGUIDsFailed = localChildrenGUIDs.dropFirst(3) + + let bufferQuery = + "INSERT INTO \(TableBookmarksBuffer) (guid, type, bmkUri, title, parentid, parentName, server_modified) VALUES " + + "(?, \(BookmarkNodeType.folder.rawValue), NULL, ?, ?, '', ?), " + + "(?, \(BookmarkNodeType.folder.rawValue), NULL, ?, ?, '', ?), " + + "(?, \(BookmarkNodeType.bookmark.rawValue), ?, ?, ?, '', ?), " + + "(?, \(BookmarkNodeType.bookmark.rawValue), ?, ?, ?, '', ?), " + + "(?, \(BookmarkNodeType.bookmark.rawValue), ?, ?, ?, '', ?) " + + let bkmBufModified = Date.now() + let bufferArgs: Args = [ + BookmarkRoots.RootGUID, "", BookmarkRoots.RootGUID, 123, + BookmarkRoots.MobileFolderGUID, "Mobile Bookmarks", BookmarkRoots.RootGUID, 456, + "bkmbuf", "http://example.org/5", "Bookmark 5", BookmarkRoots.MobileFolderGUID, bkmBufModified, + "bkmtodelete", "http://example.org/to_delete", "Bookmark to delete", BookmarkRoots.MobileFolderGUID, 88888, + "bkmtodelete_uploadfail", "http://example.org/to_delete_updfail", "Bookmark to delete but upload failed", BookmarkRoots.MobileFolderGUID, 88888 + ] + + let bufferStructureQuery = + "INSERT INTO \(TableBookmarksBufferStructure) (parent, child, idx) VALUES " + + "(?, ?, ?), " + + "(?, ?, ?), " + + "(?, ?, ?), " + + "(?, ?, ?), " + + "(?, ?, ?)" + + let bufferStructureArgs: Args = [ + BookmarkRoots.RootGUID, BookmarkRoots.RootGUID, 0, + BookmarkRoots.RootGUID, BookmarkRoots.MobileFolderGUID, 0, + BookmarkRoots.MobileFolderGUID, "bkmbuf", 0, + BookmarkRoots.MobileFolderGUID, "bkmtodelete", 1, + BookmarkRoots.MobileFolderGUID, "bkmtodelete_uploadfail", 2, + ] + + let pendingDeletionsQuery = + "INSERT INTO \(TablePendingBookmarksDeletions) (id) VALUES " + + "(?), " + + "(?)" + + let pendingDeletionsArgs: Args = [ + "bkmtodelete", "bkmtodelete_uploadfail" + ] + + db.run([ + (sql: bufferQuery, args: bufferArgs), + (sql: bufferStructureQuery, args: bufferStructureArgs), + (sql: pendingDeletionsQuery, args: pendingDeletionsArgs), + ]).succeeded() + + let mobileRoot = BookmarkMirrorItem.folder(BookmarkRoots.MobileFolderGUID, dateAdded: Date.now(), modified: Date.now(), hasDupe: false, parentID: BookmarkRoots.MobileFolderGUID, + parentName: nil, title: "Mobile Bookmarks", description: nil, children: ["bkmbuf"] + childrenGUIDsUploaded) + let op = BufferUpdatedCompletionOp(bufferValuesToMoveFromLocal: Set(childrenGUIDsUploaded), deletedValues: Set(["bkmtodelete"]), mobileRoot: mobileRoot, modifiedTime: 123456) + + let mergedBookmarks = MergedSQLiteBookmarks(db: db) + mergedBookmarks.applyBufferUpdatedCompletionOp(op).succeeded() + + let rootFolder = mergedBookmarks.getBufferItemWithGUID(BookmarkRoots.RootGUID).value.successValue! + XCTAssertEqual(rootFolder.serverModified, 123) + let mobileFolder = mergedBookmarks.getBufferItemWithGUID(BookmarkRoots.MobileFolderGUID).value.successValue! + XCTAssertEqual(mobileFolder.serverModified, 123456) + let childrenGUIDs = mergedBookmarks.getBufferChildrenGUIDsForParent(BookmarkRoots.MobileFolderGUID).value.successValue! + XCTAssertEqual(childrenGUIDs, ["bkmbuf", "bkmtodelete_uploadfail"] + childrenGUIDsUploaded) + let children = mergedBookmarks.getBufferItemsWithGUIDs(["bkmbuf"] + childrenGUIDsUploaded).value.successValue! + for item in children.values { + XCTAssertEqual(item.serverModified, item.guid == "bkmbuf" ? bkmBufModified : 123456) + } + localTree = bookmarks.treeForLocal().value.successValue! + mobileFolderNode = localTree.find(BookmarkRoots.MobileFolderGUID)! + XCTAssertEqual(mobileFolderNode.children!.map { $0.recordGUID }, Array(childrenGUIDsFailed)) + } + + fileprivate func createStockMirrorTree(_ db: BrowserDB) { + // Set up a mirror tree. + let mirrorQuery = + "INSERT INTO \(TableBookmarksMirror) (guid, type, bmkUri, title, parentid, parentName, description, tags, keyword, is_overridden, server_modified, pos) " + + "VALUES " + + "(?, \(BookmarkNodeType.folder.rawValue), NULL, ?, ?, '', '', '', '', 0, \(Date.now()), NULL), " + + "(?, \(BookmarkNodeType.folder.rawValue), NULL, ?, ?, '', '', '', '', 0, \(Date.now()), NULL), " + + "(?, \(BookmarkNodeType.folder.rawValue), NULL, ?, ?, '', '', '', '', 0, \(Date.now()), NULL), " + + + "(?, \(BookmarkNodeType.separator.rawValue), NULL, NULL, ?, '', '', '', '', 0, \(Date.now()), 0), " + + + "(?, \(BookmarkNodeType.bookmark.rawValue), ?, ?, ?, '', '', '', '', 0, \(Date.now()), NULL), " + + "(?, \(BookmarkNodeType.bookmark.rawValue), ?, ?, ?, '', '', '', '', 0, \(Date.now()), NULL), " + + "(?, \(BookmarkNodeType.bookmark.rawValue), ?, ?, ?, '', '', '', '', 0, \(Date.now()), NULL), " + + "(?, \(BookmarkNodeType.bookmark.rawValue), ?, ?, ?, '', '', '', '', 0, \(Date.now()), NULL), " + + "(?, \(BookmarkNodeType.bookmark.rawValue), ?, ?, ?, '', '', '', '', 0, \(Date.now()), NULL) " + + let mirrorArgs: Args = [ + "folderAAAAAA", "AAA", BookmarkRoots.ToolbarFolderGUID, + "folderBBBBBB", "BBB", BookmarkRoots.MenuFolderGUID, + "folderCCCCCC", "CCC", "folderBBBBBB", + + "separator101", "folderAAAAAA", + + "bookmark1001", "http://example.org/1", "Bookmark 1", "folderAAAAAA", + "bookmark1002", "http://example.org/1", "Bookmark 1 Again", "folderAAAAAA", + "bookmark2001", "http://example.org/2", "Bookmark 2", "folderAAAAAA", + "bookmark2002", "http://example.org/2", "Bookmark 2 Again", "folderCCCCCC", + "bookmark3001", "http://example.org/3", "Bookmark 3", "folderBBBBBB", + ] + + let structureQuery = + "INSERT INTO \(TableBookmarksMirrorStructure) (parent, child, idx) VALUES " + + "(?, ?, ?), " + + "(?, ?, ?), " + + "(?, ?, ?), " + + "(?, ?, ?), " + + "(?, ?, ?), " + + "(?, ?, ?), " + + "(?, ?, ?), " + + "(?, ?, ?), " + + "(?, ?, ?) " + + let structureArgs: Args = [ + BookmarkRoots.ToolbarFolderGUID, "folderAAAAAA", 0, + BookmarkRoots.MenuFolderGUID, "folderBBBBBB", 0, + "folderAAAAAA", "bookmark1001", 0, + "folderAAAAAA", "separator101", 1, + "folderAAAAAA", "bookmark1002", 2, + "folderAAAAAA", "bookmark2001", 3, + "folderBBBBBB", "bookmark3001", 0, + "folderBBBBBB", "folderCCCCCC", 1, + "folderCCCCCC", "bookmark2002", 0, + ] + + db.moveLocalToMirrorForTesting() // So we have the roots. + db.run([ + (sql: mirrorQuery, args: mirrorArgs), + (sql: structureQuery, args: structureArgs), + ]).succeeded() + } + + fileprivate func isUnknown(_ folder: BookmarkTreeNode, withGUID: GUID) { + switch folder { + case .unknown(let guid): + XCTAssertEqual(withGUID, guid) + default: + XCTFail("Not an unknown with GUID \(withGUID).") + } + } + + fileprivate func isNonFolder(_ folder: BookmarkTreeNode, withGUID: GUID) { + switch folder { + case .nonFolder(let guid): + XCTAssertEqual(withGUID, guid) + default: + XCTFail("Not a non-folder with GUID \(withGUID).") + } + } + + fileprivate func isFolder(_ folder: BookmarkTreeNode, withGUID: GUID) { + switch folder { + case .folder(let record): + XCTAssertEqual(withGUID, record.guid) + default: + XCTFail("Not a folder with GUID \(withGUID).") + } + } + + fileprivate func areFolders(_ folders: [BookmarkTreeNode], withGUIDs: [GUID]) { + folders.zip(withGUIDs).forEach { (node, guid) in + self.isFolder(node, withGUID: guid) + } + } + + fileprivate func assertTreeIsEmpty(_ treeMaybe: Maybe) { + guard let tree = treeMaybe.successValue else { + XCTFail("Couldn't get tree!") + return + } + XCTAssertTrue(tree.orphans.isEmpty) + XCTAssertTrue(tree.deleted.isEmpty) + XCTAssertTrue(tree.isEmpty) + } + + fileprivate func assertTreeContainsOnlyRoots(_ treeMaybe: Maybe) { + guard let tree = treeMaybe.successValue else { + XCTFail("Couldn't get tree!") + return + } + + XCTAssertTrue(tree.orphans.isEmpty) + XCTAssertTrue(tree.deleted.isEmpty) + XCTAssertFalse(tree.isEmpty) + XCTAssertEqual(1, tree.subtrees.count) + if case let .folder(guid, children) = tree.subtrees[0] { + XCTAssertEqual(guid, "root________") + XCTAssertEqual(4, children.count) + children.forEach { child in + guard case let .folder(_, lower) = child, lower.isEmpty else { + XCTFail("Child \(child) wasn't empty!") + return + } + } + } else { + XCTFail("Tree didn't contain root.") + } + } + + func testUnrootedBufferRowsDontAppearInTrees() { + guard let db = getBrowserDB("TSQLBtestUnrooted.db", files: self.files) else { + XCTFail("Unable to create browser DB.") + return + } + + let bookmarks = SQLiteBookmarks(db: db) + self.assertTreeContainsOnlyRoots(bookmarks.treeForMirror().value) + self.assertTreeIsEmpty(bookmarks.treeForBuffer().value) + self.assertTreeContainsOnlyRoots(bookmarks.treeForLocal().value) + + let args: Args = [ + "unrooted0001", BookmarkNodeType.bookmark.rawValue, 0, "somefolder01", "Some Folder", "I have no folder", "http://example.org/", + "rooted000002", BookmarkNodeType.bookmark.rawValue, 0, "somefolder02", "Some Other Folder", "I have a folder", "http://example.org/", + "somefolder02", BookmarkNodeType.folder.rawValue, 0, BookmarkRoots.MobileFolderGUID, "Mobile Bookmarks", "Some Other Folder", + ] + let now = Date.now() + let bufferSQL = + "INSERT INTO \(TableBookmarksBuffer) (server_modified, guid, type, date_added, is_deleted, parentid, parentName, title, bmkUri) VALUES " + + "(\(now), ?, ?, \(now), ?, ?, ?, ?, ?), " + + "(\(now), ?, ?, \(now), ?, ?, ?, ?, ?), " + + "(\(now), ?, ?, \(now), ?, ?, ?, ?, NULL)" + + let bufferStructureSQL = "INSERT INTO \(TableBookmarksBufferStructure) (parent, child, idx) VALUES ('somefolder02', 'rooted000002', 0)" + db.run(bufferSQL, withArgs: args).succeeded() + db.run(bufferStructureSQL).succeeded() + + let tree = bookmarks.treeForBuffer().value.successValue! + XCTAssertFalse(tree.orphans.contains("somefolder02")) // Folders are never orphans; they appear in subtrees instead. + XCTAssertFalse(tree.orphans.contains("rooted000002")) // This tree contains its parent, so it's not an orphan. + XCTAssertTrue(tree.orphans.contains("unrooted0001")) + XCTAssertEqual(Set(tree.subtrees.map { $0.recordGUID }), Set(["somefolder02"])) + } + + func testTreeBuilding() { + guard let db = getBrowserDB("TSQLBtestTreeBuilding.db", files: self.files) else { + XCTFail("Unable to create browser DB.") + return + } + + let bookmarks = SQLiteBookmarks(db: db) + self.assertTreeContainsOnlyRoots(bookmarks.treeForMirror().value) + self.assertTreeIsEmpty(bookmarks.treeForBuffer().value) + self.assertTreeContainsOnlyRoots(bookmarks.treeForLocal().value) + + self.createStockMirrorTree(db) + self.assertTreeIsEmpty(bookmarks.treeForBuffer().value) + + // Local was emptied when we moved the roots to the mirror. + self.assertTreeIsEmpty(bookmarks.treeForLocal().value) + + guard let tree = bookmarks.treeForMirror().value.successValue else { + XCTFail("Couldn't get tree!") + return + } + + // Mirror is no longer empty. + XCTAssertFalse(tree.isEmpty) + + // There's one root. + XCTAssertEqual(1, tree.subtrees.count) + if case let .folder(guid, children) = tree.subtrees[0] { + XCTAssertEqual("root________", guid) + XCTAssertEqual(4, children.count) + self.areFolders(children, withGUIDs: BookmarkRoots.RootChildren) + } else { + XCTFail("Root should be a folder.") + } + + // Every GUID is in the tree's nodes. + ["folderAAAAAA", + "folderBBBBBB", + "folderCCCCCC"].forEach { + isFolder(tree.lookup[$0]!, withGUID: $0) + } + + ["separator101", + "bookmark1001", + "bookmark1002", + "bookmark2001", + "bookmark2002", + "bookmark3001"].forEach { + isNonFolder(tree.lookup[$0]!, withGUID: $0) + } + + let expectedCount = + BookmarkRoots.RootChildren.count + 1 + // The roots. + 6 + // Non-folders. + 3 // Folders. + + XCTAssertEqual(expectedCount, tree.lookup.count) + + // There are no orphans and no deletions. + XCTAssertTrue(tree.orphans.isEmpty) + XCTAssertTrue(tree.deleted.isEmpty) + + // root________ + // menu________ + // folderBBBBBB + // bookmark3001 + if case let .folder(guidR, rootChildren) = tree.subtrees[0] { + XCTAssertEqual(guidR, "root________") + if case let .folder(guidM, menuChildren) = rootChildren[0] { + XCTAssertEqual(guidM, "menu________") + if case let .folder(guidB, bbbChildren) = menuChildren[0] { + XCTAssertEqual(guidB, "folderBBBBBB") + // BBB contains bookmark3001. + if case let .nonFolder(guidBM) = bbbChildren[0] { + XCTAssertEqual(guidBM, "bookmark3001") + } else { + XCTFail("First child of BBB should be bookmark3001.") + } + + // BBB contains folderCCCCCC. + if case let .folder(guidBF, _) = bbbChildren[1] { + XCTAssertEqual(guidBF, "folderCCCCCC") + } else { + XCTFail("Second child of BBB should be folderCCCCCC.") + } + } else { + XCTFail("First child of menu should be BBB.") + } + } else { + XCTFail("First child of root should be menu________") + } + } else { + XCTFail("First root should be root________") + } + + // Add a bookmark. It'll override the folder. + bookmarks.insertBookmark("https://foo.com/".asURL!, title: "Foo", favicon: nil, intoFolder: "folderBBBBBB", withTitle: "BBB").succeeded() + let newlyInserted = db.getRecordByURL("https://foo.com/", fromTable: TableBookmarksLocal).guid + + guard let local = bookmarks.treeForLocal().value.successValue else { + XCTFail("Couldn't get local tree!") + return + } + + XCTAssertFalse(local.isEmpty) + XCTAssertEqual(4, local.lookup.count) // Folder, new bookmark, original two children. + XCTAssertEqual(1, local.subtrees.count) + if case let .folder(guid, children) = local.subtrees[0] { + XCTAssertEqual("folderBBBBBB", guid) + + // We have shadows of the original two children. + XCTAssertEqual(3, children.count) + self.isUnknown(children[0], withGUID: "bookmark3001") + self.isUnknown(children[1], withGUID: "folderCCCCCC") + self.isNonFolder(children[2], withGUID: newlyInserted) + } else { + XCTFail("Root should be folderBBBBBB.") + } + + // Insert partial data into the buffer. + // We insert: + let bufferArgs: Args = [ + // * A folder whose parent isn't present in the structure. + "ihavenoparent", BookmarkNodeType.folder.rawValue, 0, "myparentnoexist", "No Exist", "No Parent", + // * A folder with no children. + "ihavenochildren", BookmarkNodeType.folder.rawValue, 0, "ihavenoparent", "No Parent", "No Children", + // * A folder that meets both criteria. + "xhavenoparent", BookmarkNodeType.folder.rawValue, 0, "myparentnoexist", "No Exist", "No Parent And No Children", + // * A changed bookmark with no parent. + "changedbookmark", BookmarkNodeType.bookmark.rawValue, 0, "folderCCCCCC", "CCC", "I changed", "http://change.org/", + // * A deleted record. + "iwasdeleted", BookmarkNodeType.bookmark.rawValue, + ] + + let now = Date.now() + let bufferSQL = "INSERT INTO \(TableBookmarksBuffer) (server_modified, guid, type, date_added, is_deleted, parentid, parentName, title, bmkUri) VALUES " + + "(\(now), ?, ?, \(now), ?, ?, ?, ?, NULL), " + + "(\(now), ?, ?, \(now), ?, ?, ?, ?, NULL), " + + "(\(now), ?, ?, \(now), ?, ?, ?, ?, NULL), " + + "(\(now), ?, ?, \(now), ?, ?, ?, ?, ?), " + + "(\(now), ?, ?, \(now), 1, NULL, NULL, NULL, NULL) " + + let bufferStructureSQL = "INSERT INTO \(TableBookmarksBufferStructure) (parent, child, idx) VALUES (?, ?, ?)" + let bufferStructureArgs: Args = ["ihavenoparent", "ihavenochildren", 0] + db.run([(bufferSQL, bufferArgs), (bufferStructureSQL, bufferStructureArgs)]).succeeded() + + // Now build the tree. + guard let partialBuffer = bookmarks.treeForBuffer().value.successValue else { + XCTFail("Couldn't get buffer tree!") + return + } + + XCTAssertEqual(partialBuffer.deleted, Set(["iwasdeleted"])) + XCTAssertEqual(partialBuffer.orphans, Set(["changedbookmark"])) + XCTAssertEqual(partialBuffer.subtreeGUIDs, Set(["ihavenoparent", "xhavenoparent"])) + if case let .folder(_, children) = partialBuffer.lookup["ihavenochildren"]! { + XCTAssertTrue(children.isEmpty) + } else { + XCTFail("Couldn't look up childless folder.") + } + } + + func testRecursiveAndURLDelete() { + guard let db = getBrowserDB("TSQLBtestRecursiveAndURLDelete.db", files: self.files) else { + XCTFail("Unable to create browser DB.") + return + } + + let bookmarks = SQLiteBookmarks(db: db) + self.createStockMirrorTree(db) + + let menuOverridden = BookmarkRoots.MenuFolderGUID + XCTAssertFalse(db.isOverridden(menuOverridden) ?? true) + + func getMenuChildren() -> [GUID] { + return db.getChildrenOfFolder(BookmarkRoots.MenuFolderGUID) + } + + XCTAssertEqual(["folderBBBBBB"], getMenuChildren()) + + // Locally add an item to the menu. This'll override the menu folder. + bookmarks.insertBookmark(URL(string: "http://example.com/2")!, title: "Bookmark 2 added locally", favicon: nil, intoFolder: BookmarkRoots.MenuFolderGUID, withTitle: "Bookmarks Menu").succeeded() + + XCTAssertTrue(db.isOverridden(BookmarkRoots.MenuFolderGUID) ?? false) + + let menuChildrenBeforeRecursiveDelete = getMenuChildren() + XCTAssertEqual(2, menuChildrenBeforeRecursiveDelete.count) + XCTAssertEqual("folderBBBBBB", menuChildrenBeforeRecursiveDelete[0]) + + let locallyAddedBookmark2 = menuChildrenBeforeRecursiveDelete[1] + + // It's local, so it's not overridden. + // N.B., these tests all use XCTAssertEqual instead of XCTAssert{True,False} because + // the former plays well with optionals. + XCTAssertNil(db.isOverridden(locallyAddedBookmark2)) + XCTAssertEqual(false, db.isLocallyDeleted(locallyAddedBookmark2)) + + // Now let's delete folder B and check that things that weren't deleted now are. + XCTAssertEqual(false, db.isOverridden("folderBBBBBB")) + XCTAssertNil(db.isLocallyDeleted("folderBBBBBB")) + XCTAssertEqual(false, db.isOverridden("folderCCCCCC")) + XCTAssertNil(db.isLocallyDeleted("folderCCCCCC")) + XCTAssertEqual(false, db.isOverridden("bookmark2002")) + XCTAssertNil(db.isLocallyDeleted("bookmark2002")) + XCTAssertEqual(false, db.isOverridden("bookmark3001")) + XCTAssertNil(db.isLocallyDeleted("bookmark3001")) + + bookmarks.testFactory.removeByGUID("folderBBBBBB").succeeded() + + XCTAssertEqual(true, db.isOverridden("folderBBBBBB")) + XCTAssertEqual(true, db.isLocallyDeleted("folderBBBBBB")) + XCTAssertEqual(true, db.isOverridden("folderCCCCCC")) + XCTAssertEqual(true, db.isLocallyDeleted("folderCCCCCC")) + XCTAssertEqual(true, db.isOverridden("bookmark2002")) + XCTAssertEqual(true, db.isLocallyDeleted("bookmark2002")) + XCTAssertEqual(true, db.isOverridden("bookmark3001")) + XCTAssertEqual(true, db.isLocallyDeleted("bookmark3001")) + + // Still there. + XCTAssertNil(db.isOverridden(locallyAddedBookmark2)) + XCTAssertFalse(db.isLocallyDeleted(locallyAddedBookmark2) ?? true) + + let menuChildrenAfterRecursiveDelete = getMenuChildren() + XCTAssertEqual(1, menuChildrenAfterRecursiveDelete.count) + XCTAssertEqual(locallyAddedBookmark2, menuChildrenAfterRecursiveDelete[0]) + + // Now let's delete by URL. + XCTAssertEqual(false, db.isOverridden("bookmark1001")) + XCTAssertNil(db.isLocallyDeleted("bookmark1001")) + XCTAssertEqual(false, db.isOverridden("bookmark1002")) + XCTAssertNil(db.isLocallyDeleted("bookmark1002")) + + bookmarks.testFactory.removeByURL("http://example.org/1").succeeded() + + // To conclude, check the entire hierarchy. + // Menu: overridden, only the locally-added bookmark 2. + // B, 3001, C, 2002: locally deleted. + // A: overridden, children [separator101, 2001]. + // No bookmarks with URL /1. + XCTAssertEqual(true, db.isOverridden(BookmarkRoots.MenuFolderGUID)) + XCTAssertEqual(true, db.isOverridden("folderBBBBBB")) + XCTAssertEqual(true, db.isLocallyDeleted("folderBBBBBB")) + XCTAssertEqual(true, db.isOverridden("folderCCCCCC")) + XCTAssertEqual(true, db.isLocallyDeleted("folderCCCCCC")) + XCTAssertEqual(true, db.isOverridden("bookmark2002")) + XCTAssertEqual(true, db.isLocallyDeleted("bookmark2002")) + XCTAssertEqual(true, db.isOverridden("bookmark3001")) + XCTAssertEqual(true, db.isLocallyDeleted("bookmark3001")) + XCTAssertEqual(true, db.isOverridden("bookmark1001")) + XCTAssertEqual(true, db.isLocallyDeleted("bookmark1001")) + XCTAssertEqual(true, db.isOverridden("bookmark1002")) + XCTAssertEqual(true, db.isLocallyDeleted("bookmark1002")) + + let menuChildrenAfterURLDelete = getMenuChildren() + XCTAssertEqual(1, menuChildrenAfterURLDelete.count) + XCTAssertEqual(locallyAddedBookmark2, menuChildrenAfterURLDelete[0]) + + XCTAssertEqual(["separator101", "bookmark2001"], db.getChildrenOfFolder("folderAAAAAA")) + } + + func testLocalAndMirror() { + guard let db = getBrowserDB("TSQLBtestLocalAndMirror.db", files: self.files) else { + XCTFail("Unable to create browser DB.") + return + } + + // Preconditions. + let rootGUIDs = [ + BookmarkRoots.RootGUID, + BookmarkRoots.MobileFolderGUID, + BookmarkRoots.MenuFolderGUID, + BookmarkRoots.ToolbarFolderGUID, + BookmarkRoots.UnfiledFolderGUID, + ] + + let positioned = [ + BookmarkRoots.MenuFolderGUID, + BookmarkRoots.ToolbarFolderGUID, + BookmarkRoots.UnfiledFolderGUID, + BookmarkRoots.MobileFolderGUID, + ] + + XCTAssertEqual(rootGUIDs, db.getGUIDs("SELECT guid FROM \(TableBookmarksLocal) ORDER BY id")) + XCTAssertEqual(positioned, db.getGUIDs("SELECT child FROM \(TableBookmarksLocalStructure) ORDER BY idx")) + XCTAssertEqual([], db.getGUIDs("SELECT guid FROM \(TableBookmarksMirror)")) + XCTAssertEqual([], db.getGUIDs("SELECT child FROM \(TableBookmarksMirrorStructure)")) + + // Add a local bookmark. + let bookmarks = SQLiteBookmarks(db: db) + bookmarks.insertBookmark("http://example.org/".asURL!, title: "Example", favicon: nil, intoFolder: BookmarkRoots.MobileFolderGUID, withTitle: "The Mobile").succeeded() + + let rowA = db.getRecordByURL("http://example.org/", fromTable: TableBookmarksLocal) + XCTAssertEqual(rowA.bookmarkURI, "http://example.org/") + XCTAssertEqual(rowA.title, "Example") + XCTAssertEqual(rowA.parentName, "The Mobile") + XCTAssertEqual(rootGUIDs + [rowA.guid], db.getGUIDs("SELECT guid FROM \(TableBookmarksLocal) ORDER BY id")) + XCTAssertEqual([rowA.guid], db.getGUIDs("SELECT child FROM \(TableBookmarksLocalStructure) WHERE parent = '\(BookmarkRoots.MobileFolderGUID)' ORDER BY idx")) + XCTAssertEqual(SyncStatus.new, db.getSyncStatusForGUID(rowA.guid)) + + // Add another. Order should be maintained. + bookmarks.insertBookmark("https://reddit.com/".asURL!, title: "Reddit", favicon: nil, intoFolder: BookmarkRoots.MobileFolderGUID, withTitle: "Mobile").succeeded() + + let rowB = db.getRecordByURL("https://reddit.com/", fromTable: TableBookmarksLocal) + XCTAssertEqual(rowB.bookmarkURI, "https://reddit.com/") + XCTAssertEqual(rowB.title, "Reddit") + XCTAssertEqual(rootGUIDs + [rowA.guid, rowB.guid], db.getGUIDs("SELECT guid FROM \(TableBookmarksLocal) ORDER BY id")) + XCTAssertEqual([rowA.guid, rowB.guid], db.getGUIDs("SELECT child FROM \(TableBookmarksLocalStructure) WHERE parent = '\(BookmarkRoots.MobileFolderGUID)' ORDER BY idx")) + XCTAssertEqual(SyncStatus.new, db.getSyncStatusForGUID(rowA.guid)) + XCTAssertEqual(SyncStatus.new, db.getSyncStatusForGUID(rowB.guid)) + + // The indices should be 0, 1. + let positions = db.getPositionsForChildrenOfParent(BookmarkRoots.MobileFolderGUID, fromTable: TableBookmarksLocalStructure) + XCTAssertEqual(positions.count, 2) + XCTAssertEqual(positions[rowA.guid], 0) + XCTAssertEqual(positions[rowB.guid], 1) + + // Delete the first. sync_status was New, so the row was immediately deleted. + bookmarks.testFactory.removeByURL("http://example.org/").succeeded() + XCTAssertEqual(rootGUIDs + [rowB.guid], db.getGUIDs("SELECT guid FROM \(TableBookmarksLocal) ORDER BY id")) + XCTAssertEqual([rowB.guid], db.getGUIDs("SELECT child FROM \(TableBookmarksLocalStructure) WHERE parent = '\(BookmarkRoots.MobileFolderGUID)' ORDER BY idx")) + let positionsAfterDelete = db.getPositionsForChildrenOfParent(BookmarkRoots.MobileFolderGUID, fromTable: TableBookmarksLocalStructure) + XCTAssertEqual(positionsAfterDelete.count, 1) + XCTAssertEqual(positionsAfterDelete[rowB.guid], 0) + + // Manually shuffle all of these into the mirror, as if we were fully synchronized. + db.moveLocalToMirrorForTesting() + XCTAssertEqual([], db.getGUIDs("SELECT guid FROM \(TableBookmarksLocal) ORDER BY id")) + XCTAssertEqual([], db.getGUIDs("SELECT child FROM \(TableBookmarksLocalStructure)")) + XCTAssertEqual(rootGUIDs + [rowB.guid], db.getGUIDs("SELECT guid FROM \(TableBookmarksMirror) ORDER BY id")) + XCTAssertEqual([rowB.guid], db.getGUIDs("SELECT child FROM \(TableBookmarksMirrorStructure) WHERE parent = '\(BookmarkRoots.MobileFolderGUID)' ORDER BY idx")) + let mirrorPositions = db.getPositionsForChildrenOfParent(BookmarkRoots.MobileFolderGUID, fromTable: TableBookmarksMirrorStructure) + XCTAssertEqual(mirrorPositions.count, 1) + XCTAssertEqual(mirrorPositions[rowB.guid], 0) + + // Now insert a new mobile bookmark. + bookmarks.insertBookmark("https://letsencrypt.org/".asURL!, title: "Let's Encrypt", favicon: nil, intoFolder: BookmarkRoots.MobileFolderGUID, withTitle: "Mobile").succeeded() + + // The mobile bookmarks folder is overridden. + XCTAssertEqual(true, db.isOverridden(BookmarkRoots.MobileFolderGUID)) + + // Our previous inserted bookmark is not. + XCTAssertEqual(false, db.isOverridden(rowB.guid)) + + let rowC = db.getRecordByURL("https://letsencrypt.org/", fromTable: TableBookmarksLocal) + + // We have the old structure in the mirror. + XCTAssertEqual(rootGUIDs + [rowB.guid], db.getGUIDs("SELECT guid FROM \(TableBookmarksMirror) ORDER BY id")) + XCTAssertEqual([rowB.guid], db.getGUIDs("SELECT child FROM \(TableBookmarksMirrorStructure) WHERE parent = '\(BookmarkRoots.MobileFolderGUID)' ORDER BY idx")) + + // We have the new structure in the local table. + XCTAssertEqual(Set([BookmarkRoots.MobileFolderGUID, rowC.guid]), Set(db.getGUIDs("SELECT guid FROM \(TableBookmarksLocal) ORDER BY id"))) + XCTAssertEqual([rowB.guid, rowC.guid], db.getGUIDs("SELECT child FROM \(TableBookmarksLocalStructure) WHERE parent = '\(BookmarkRoots.MobileFolderGUID)' ORDER BY idx")) + + // Parent is changed. The new record is New. The unmodified and deleted records aren't present. + XCTAssertNil(db.getSyncStatusForGUID(rowA.guid)) + XCTAssertNil(db.getSyncStatusForGUID(rowB.guid)) + XCTAssertEqual(SyncStatus.new, db.getSyncStatusForGUID(rowC.guid)) + XCTAssertEqual(SyncStatus.changed, db.getSyncStatusForGUID(BookmarkRoots.MobileFolderGUID)) + + // If we delete the old record, we mark it as changed, and it's no longer in the structure. + bookmarks.testFactory.removeByGUID(rowB.guid).succeeded() + XCTAssertEqual(SyncStatus.changed, db.getSyncStatusForGUID(rowB.guid)) + XCTAssertEqual([rowC.guid], db.getGUIDs("SELECT child FROM \(TableBookmarksLocalStructure) WHERE parent = '\(BookmarkRoots.MobileFolderGUID)' ORDER BY idx")) + + // Add a duplicate to test multi-deletion (unstar). + bookmarks.insertBookmark("https://letsencrypt.org/".asURL!, title: "Let's Encrypt", favicon: nil, intoFolder: BookmarkRoots.MobileFolderGUID, withTitle: "Mobile").succeeded() + let guidD = db.getGUIDs("SELECT child FROM \(TableBookmarksLocalStructure) WHERE parent = '\(BookmarkRoots.MobileFolderGUID)' ORDER BY idx").last! + XCTAssertNotEqual(rowC.guid, guidD) + XCTAssertEqual(SyncStatus.new, db.getSyncStatusForGUID(guidD)) + XCTAssertEqual(SyncStatus.changed, db.getSyncStatusForGUID(BookmarkRoots.MobileFolderGUID)) + + // Delete by URL. + // If we delete the new records, they just go away -- there's no server version to delete. + bookmarks.testFactory.removeByURL(rowC.bookmarkURI!).succeeded() + XCTAssertNil(db.getSyncStatusForGUID(rowC.guid)) + XCTAssertNil(db.getSyncStatusForGUID(guidD)) + XCTAssertEqual([], db.getGUIDs("SELECT child FROM \(TableBookmarksLocalStructure) WHERE parent = '\(BookmarkRoots.MobileFolderGUID)' ORDER BY idx")) + + // The mirror structure is unchanged after all this. + XCTAssertEqual(rootGUIDs + [rowB.guid], db.getGUIDs("SELECT guid FROM \(TableBookmarksMirror) ORDER BY id")) + XCTAssertEqual([rowB.guid], db.getGUIDs("SELECT child FROM \(TableBookmarksMirrorStructure) WHERE parent = '\(BookmarkRoots.MobileFolderGUID)' ORDER BY idx")) + } + + /* + // This is dead test code after we eliminated the merged view. + // Expect this to be ported to reflect post-sync state. + func testBookmarkStructure() { + guard let db = getBrowserDB("TSQLBtestBufferStorage.db", files: self.files) else { + XCTFail("Unable to create browser DB.") + return + } + let bookmarks = MergedSQLiteBookmarks(db: db) + + guard let model = bookmarks.modelForRoot().value.successValue else { + XCTFail("Unable to get root.") + return + } + let root = model.current + + // The root isn't useful. The merged bookmark implementation doesn't use it. + XCTAssertEqual(root.guid, BookmarkRoots.RootGUID) + XCTAssertEqual(0, root.count) + + // Specifically fetching the fake desktop folder works, though. + guard let desktopModel = bookmarks.modelForFolder(BookmarkRoots.FakeDesktopFolderGUID).value.successValue else { + XCTFail("Unable to get desktop bookmarks.") + return + } + + // It contains the toolbar. + let desktopFolder = desktopModel.current + XCTAssertEqual(desktopFolder.guid, BookmarkRoots.FakeDesktopFolderGUID) + XCTAssertEqual(1, desktopFolder.count) + + guard let toolbarModel = desktopModel.selectFolder(BookmarkRoots.ToolbarFolderGUID).value.successValue else { + XCTFail("Unable to get toolbar.") + return + } + + // The toolbar is the child, and it has the two bookmarks as entries. + let toolbarFolder = toolbarModel.current + XCTAssertEqual(toolbarFolder.guid, BookmarkRoots.ToolbarFolderGUID) + XCTAssertEqual(2, toolbarFolder.count) + + guard let first = toolbarModel.current[0] else { + XCTFail("Expected to get AAA.") + return + } + guard let second = toolbarModel.current[1] else { + XCTFail("Expected to get BBB.") + return + } + XCTAssertEqual(first.guid, "aaaaaaaaaaaa") + XCTAssertEqual(first.title, "AAA") + XCTAssertEqual((first as? BookmarkItem)?.url, "http://getfirefox.com") + XCTAssertEqual(second.guid, "bbbbbbbbbbbb") + XCTAssertEqual(second.title, "BBB") + XCTAssertEqual((second as? BookmarkItem)?.url, "http://getfirefox.com") + + let del: [BookmarkMirrorItem] = [BookmarkMirrorItem.deleted(BookmarkNodeType.Bookmark, guid: "aaaaaaaaaaaa", modified: Date.now())] + bookmarks.applyRecords(del, withMaxVars: 1) + + guard let newToolbar = bookmarks.modelForFolder(BookmarkRoots.ToolbarFolderGUID).value.successValue else { + XCTFail("Unable to get toolbar.") + return + } + XCTAssertEqual(newToolbar.current.count, 1) + XCTAssertEqual(newToolbar.current[0]?.guid, "bbbbbbbbbbbb") + } + */ + + func testBufferStorage() { + guard let db = getBrowserDB("TSQLBtestBufferStorage.db", files: self.files) else { + XCTFail("Unable to create browser DB.") + return + } + let bookmarks = SQLiteBookmarkBufferStorage(db: db) + + let record1 = BookmarkMirrorItem.bookmark("aaaaaaaaaaaa", dateAdded: Date.now(), modified: Date.now(), hasDupe: false, parentID: BookmarkRoots.ToolbarFolderGUID, parentName: "Bookmarks Toolbar", title: "AAA", description: "AAA desc", URI: "http://getfirefox.com", tags: "[]", keyword: nil) + let record2 = BookmarkMirrorItem.bookmark("bbbbbbbbbbbb", dateAdded: Date.now(), modified: Date.now() + 10, hasDupe: false, parentID: BookmarkRoots.ToolbarFolderGUID, parentName: "Bookmarks Toolbar", title: "BBB", description: "BBB desc", URI: "http://getfirefox.com", tags: "[]", keyword: nil) + let toolbar = BookmarkMirrorItem.folder("toolbar", dateAdded: Date.now(), modified: Date.now(), hasDupe: false, parentID: "places", parentName: "", title: "Bookmarks Toolbar", description: "Add bookmarks to this folder to see them displayed on the Bookmarks Toolbar", children: ["aaaaaaaaaaaa", "bbbbbbbbbbbb"]) + let recordsA: [BookmarkMirrorItem] = [record1, toolbar, record2] + bookmarks.applyRecords(recordsA, withMaxVars: 3).succeeded() + + // Insert mobile bookmarks as produced by Firefox 40-something on desktop. + // swiftlint:disable line_length + + let children = ["M87np9Vfh_2s", "-JxRyqNte-ue", "6lIQzUtbjE8O", "eOg3jPSslzXl", "1WJIi9EjQErp", "z5uRo45Rvfbd", "EK3lcNd0sUFN", "gFD3GTljgu12", "eRZGsbN1ew9-", "widfEdgGn9de", "l7eTOR4Uf6xq", "vPbxG-gpN4Rb", "4dwJ8CototFe", "zK-kw9Ii6ScW", "eDmDU-gtEFW6", "lKjqWQaL_syt", "ETVDvWgGT31Q", "3Z_bMIHPSZQ8", "Fqu4_bJOk7fT", "Uo_5K1QrA67j", "gDTXNg4m1AJZ", "zpds8P-9xews", "87zjNtVGPtEp", "ZJru8Sn3qhW7", "txVnzBBBOgLP", "JTnRqFaj_oNa", "soaMlfmM4kjR", "g8AcVBjo6IRf", "uPUDaiG4q637", "rfq2bUud_w4d", "XBGxsiuUG2UD", "-VQRnJlyAvMs", "6wu7TScKdTU7", "ZeFji2hLVpLj", "HpCn_TVizMWX", "IPR5HZwRdlwi", "00JFOGuWnhWB", "P1jb3qKt32Vg", "D6MQJ43V1Ir5", "qWSoXFteRfsq", "o2avfYqEdomL", "xRS0U0YnjK9G", "VgOgzE_xfP4w", "SwP3rMJGvoO3", "Hf2jEgI_-PWa", "AyhmBi7Cv598", "-PaMuzTJXxVk", "JMhYrg8SlY5K", "SQeySEjzyplL", "GTAwd2UkEQEe", "x3RsZj5Ilebr", "sRZWZqPi74FP", "amHR50TpygA6", "XSk782ceVNN6", "ipiMyYQzeypI", "ph2k3Nqfhau4", "m5JKC3hAEQ0H", "yTVerkmQbNxk", "7taA6FbbbUbH", "PZvpbSRuJLPs", "C8atoa25U94F", "KOfNJk_ISLc6", "Bt74lBG9tJq6", "BuHoY2rUhuKA", "XTmoWKnwfIPl", "ZATwa3oTD1m0", "e8TczN5It6Am", "6kCUYs8hQtKg", "jDD8s5aiKoex", "QmpmcrYwLU29", "nCRcekynuJ08", "resttaI4J9tu", "EKSX3HV55VU3", "2-yCz0EIsVls", "sSeeGw3VbBY-", "qfpCrU34w9y0", "RKDgzPWecD6m", "5SgXEKu_dICW", "R143WAeB5E5r", "8Ns4-NiKG62r", "4AHuZDvop5XX", "YCP1OsO1goFF", "CYYaU1mQ_N6t", "UGkzEOMK8cuU", "1RzZOarkzQBa", "qSW2Z3cZSI9c", "ooPlKEAfQsnn", "jIUScoKLiXQt", "bjNTKugzRRL1", "hR24ZVnHUZcs", "3j2IDAZgUyYi", "xnWcy-sQDJRu", "UCcgJqGk3bTV", "WSSRWeptH9tq", "4ugv47OGD2E2", "XboCZgUx-x3x", "HrmWqiqsuLrm", "OjdxvRJ3Jb6j"] + // swiftlint:enable line_length + + let mA = BookmarkMirrorItem.bookmark("jIUScoKLiXQt", dateAdded: Date.now(), modified: Date.now(), hasDupe: false, parentID: "mobile", parentName: "mobile", title: "Join the Engineering Leisure Class — Medium", description: nil, URI: "https://medium.com/@chrisloer/join-the-engineering-leisure-class-b3083c09a78e", tags: "[]", keyword: nil) + + let mB = BookmarkMirrorItem.folder("UjAHxFOGEqU8", dateAdded: Date.now(), modified: Date.now(), hasDupe: false, parentID: "places", parentName: "", title: "mobile", description: nil, children: children) + bookmarks.applyRecords([mA, mB]).succeeded() + + func childCount(_ parent: GUID) -> Int? { + let sql = "SELECT COUNT(*) AS childCount FROM \(TableBookmarksBufferStructure) WHERE parent = ?" + let args: Args = [parent] + return db.runQuery(sql, args: args, factory: { $0["childCount"] as! Int }).value.successValue?[0] + } + + // We have children. + XCTAssertEqual(children.count, childCount("UjAHxFOGEqU8")) + + // Insert an empty mobile bookmarks folder, so we can verify that the structure table is wiped. + let mBEmpty = BookmarkMirrorItem.folder("UjAHxFOGEqU8", dateAdded: Date.now(), modified: Date.now() + 1, hasDupe: false, parentID: "places", parentName: "", title: "mobile", description: nil, children: []) + bookmarks.applyRecords([mBEmpty]).succeeded() + + // We no longer have children. + XCTAssertEqual(0, childCount("UjAHxFOGEqU8")) + } +} diff --git a/mobile/ios/StorageTests/TestSQLiteHistory.swift b/mobile/ios/StorageTests/TestSQLiteHistory.swift new file mode 100644 index 0000000000..2ea7dbc9ac --- /dev/null +++ b/mobile/ios/StorageTests/TestSQLiteHistory.swift @@ -0,0 +1,1625 @@ +/* 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 +@testable import Storage +import Deferred + +import XCTest + +let threeMonthsInMillis: UInt64 = 3 * 30 * 24 * 60 * 60 * 1000 +let threeMonthsInMicros: UInt64 = UInt64(threeMonthsInMillis) * UInt64(1000) + +// Start everything three months ago. +let baseInstantInMillis = Date.now() - threeMonthsInMillis +let baseInstantInMicros = Date.nowMicroseconds() - threeMonthsInMicros + +func advanceTimestamp(_ timestamp: Timestamp, by: Int) -> Timestamp { + return timestamp + UInt64(by) +} + +func advanceMicrosecondTimestamp(_ timestamp: MicrosecondTimestamp, by: Int) -> MicrosecondTimestamp { + return timestamp + UInt64(by) +} + +extension Site { + func asPlace() -> Place { + return Place(guid: self.guid!, url: self.url, title: self.title) + } +} + +class BaseHistoricalBrowserSchema: Schema { + var name: String { return "BROWSER" } + var version: Int { return -1 } + + func update(_ db: SQLiteDBConnection, from: Int) -> Bool { + fatalError("Should never be called.") + } + + func create(_ db: SQLiteDBConnection) -> Bool { + return false + } + + func drop(_ db: SQLiteDBConnection) -> Bool { + return false + } + + var supportsPartialIndices: Bool { + let v = sqlite3_libversion_number() + return v >= 3008000 // 3.8.0. + } + + let oldFaviconsSQL = + "CREATE TABLE IF NOT EXISTS favicons (" + + "id INTEGER PRIMARY KEY AUTOINCREMENT, " + + "url TEXT NOT NULL UNIQUE, " + + "width INTEGER, " + + "height INTEGER, " + + "type INTEGER NOT NULL, " + + "date REAL NOT NULL" + + ") " + + func run(_ db: SQLiteDBConnection, sql: String?, args: Args? = nil) -> Bool { + if let sql = sql { + do { + try db.executeChange(sql, withArgs: args) + } catch { + return false + } + } + + return true + } + + func run(_ db: SQLiteDBConnection, queries: [String?]) -> Bool { + for sql in queries { + if let sql = sql { + if !run(db, sql: sql) { + return false + } + } + } + return true + } + + func run(_ db: SQLiteDBConnection, queries: [String]) -> Bool { + for sql in queries { + if !run(db, sql: sql) { + return false + } + } + return true + } +} + +// Versions of BrowserSchema that we care about: +// v6, prior to 001c73ea1903c238be1340950770879b40c41732, July 2015. +// This is when we first started caring about database versions. +// +// v7, 81e22fa6f7446e27526a5a9e8f4623df159936c3. History tiles. +// +// v8, 02c08ddc6d805d853bbe053884725dc971ef37d7. Favicons. +// +// v10, 4428c7d181ff4779ab1efb39e857e41bdbf4de67. Mirroring. We skipped v9. +// +// These tests snapshot the table creation code at each of these points. + +class BrowserSchemaV6: BaseHistoricalBrowserSchema { + override var version: Int { return 6 } + + func prepopulateRootFolders(_ db: SQLiteDBConnection) -> Bool { + let type = BookmarkNodeType.folder.rawValue + let root = BookmarkRoots.RootID + + let titleMobile = NSLocalizedString("Mobile Bookmarks", tableName: "Storage", comment: "The title of the folder that contains mobile bookmarks. This should match bookmarks.folder.mobile.label on Android.") + let titleMenu = NSLocalizedString("Bookmarks Menu", tableName: "Storage", comment: "The name of the folder that contains desktop bookmarks in the menu. This should match bookmarks.folder.menu.label on Android.") + let titleToolbar = NSLocalizedString("Bookmarks Toolbar", tableName: "Storage", comment: "The name of the folder that contains desktop bookmarks in the toolbar. This should match bookmarks.folder.toolbar.label on Android.") + let titleUnsorted = NSLocalizedString("Unsorted Bookmarks", tableName: "Storage", comment: "The name of the folder that contains unsorted desktop bookmarks. This should match bookmarks.folder.unfiled.label on Android.") + + let args: Args = [ + root, BookmarkRoots.RootGUID, type, "Root", root, + BookmarkRoots.MobileID, BookmarkRoots.MobileFolderGUID, type, titleMobile, root, + BookmarkRoots.MenuID, BookmarkRoots.MenuFolderGUID, type, titleMenu, root, + BookmarkRoots.ToolbarID, BookmarkRoots.ToolbarFolderGUID, type, titleToolbar, root, + BookmarkRoots.UnfiledID, BookmarkRoots.UnfiledFolderGUID, type, titleUnsorted, root, + ] + + let sql = + "INSERT INTO bookmarks (id, guid, type, url, title, parent) VALUES " + + "(?, ?, ?, NULL, ?, ?), " + // Root + "(?, ?, ?, NULL, ?, ?), " + // Mobile + "(?, ?, ?, NULL, ?, ?), " + // Menu + "(?, ?, ?, NULL, ?, ?), " + // Toolbar + "(?, ?, ?, NULL, ?, ?) " // Unsorted + + return self.run(db, sql: sql, args: args) + } + + func CreateHistoryTable() -> String { + return "CREATE TABLE IF NOT EXISTS \(TableHistory) (" + + "id INTEGER PRIMARY KEY AUTOINCREMENT, " + + "guid TEXT NOT NULL UNIQUE, " + // Not null, but the value might be replaced by the server's. + "url TEXT UNIQUE, " + // May only be null for deleted records. + "title TEXT NOT NULL, " + + "server_modified INTEGER, " + // Can be null. Integer milliseconds. + "local_modified INTEGER, " + // Can be null. Client clock. In extremis only. + "is_deleted TINYINT NOT NULL, " + // Boolean. Locally deleted. + "should_upload TINYINT NOT NULL, " + // Boolean. Set when changed or visits added. + "domain_id INTEGER REFERENCES \(TableDomains)(id) ON DELETE CASCADE, " + + "CONSTRAINT urlOrDeleted CHECK (url IS NOT NULL OR is_deleted = 1)" + + ")" + } + + func CreateDomainsTable() -> String { + return "CREATE TABLE IF NOT EXISTS \(TableDomains) (" + + "id INTEGER PRIMARY KEY AUTOINCREMENT, " + + "domain TEXT NOT NULL UNIQUE, " + + "showOnTopSites TINYINT NOT NULL DEFAULT 1" + + ")" + } + + func CreateQueueTable() -> String { + return "CREATE TABLE IF NOT EXISTS \(TableQueuedTabs) (" + + "url TEXT NOT NULL UNIQUE, " + + "title TEXT" + + ") " + } + + override func create(_ db: SQLiteDBConnection) -> Bool { + let visits = + "CREATE TABLE IF NOT EXISTS \(TableVisits) (" + + "id INTEGER PRIMARY KEY AUTOINCREMENT, " + + "siteID INTEGER NOT NULL REFERENCES \(TableHistory)(id) ON DELETE CASCADE, " + + "date REAL NOT NULL, " + // Microseconds since epoch. + "type INTEGER NOT NULL, " + + "is_local TINYINT NOT NULL, " + // Some visits are local. Some are remote ('mirrored'). This boolean flag is the split. + "UNIQUE (siteID, date, type) " + + ") " + + let indexShouldUpload: String + if self.supportsPartialIndices { + // There's no point tracking rows that are not flagged for upload. + indexShouldUpload = + "CREATE INDEX IF NOT EXISTS \(IndexHistoryShouldUpload) " + + "ON \(TableHistory) (should_upload) WHERE should_upload = 1" + } else { + indexShouldUpload = + "CREATE INDEX IF NOT EXISTS \(IndexHistoryShouldUpload) " + + "ON \(TableHistory) (should_upload)" + } + + let indexSiteIDDate = + "CREATE INDEX IF NOT EXISTS \(IndexVisitsSiteIDIsLocalDate) " + + "ON \(TableVisits) (siteID, is_local, date)" + + let faviconSites = + "CREATE TABLE IF NOT EXISTS \(TableFaviconSites) (" + + "id INTEGER PRIMARY KEY AUTOINCREMENT, " + + "siteID INTEGER NOT NULL REFERENCES \(TableHistory)(id) ON DELETE CASCADE, " + + "faviconID INTEGER NOT NULL REFERENCES \(TableFavicons)(id) ON DELETE CASCADE, " + + "UNIQUE (siteID, faviconID) " + + ") " + + let widestFavicons = + "CREATE VIEW IF NOT EXISTS \(ViewWidestFaviconsForSites) AS " + + "SELECT " + + "\(TableFaviconSites).siteID AS siteID, " + + "\(TableFavicons).id AS iconID, " + + "\(TableFavicons).url AS iconURL, " + + "\(TableFavicons).date AS iconDate, " + + "\(TableFavicons).type AS iconType, " + + "MAX(\(TableFavicons).width) AS iconWidth " + + "FROM \(TableFaviconSites), \(TableFavicons) WHERE " + + "\(TableFaviconSites).faviconID = \(TableFavicons).id " + + "GROUP BY siteID " + + let historyIDsWithIcon = + "CREATE VIEW IF NOT EXISTS \(ViewHistoryIDsWithWidestFavicons) AS " + + "SELECT \(TableHistory).id AS id, " + + "iconID, iconURL, iconDate, iconType, iconWidth " + + "FROM \(TableHistory) " + + "LEFT OUTER JOIN " + + "\(ViewWidestFaviconsForSites) ON history.id = \(ViewWidestFaviconsForSites).siteID " + + let iconForURL = + "CREATE VIEW IF NOT EXISTS \(ViewIconForURL) AS " + + "SELECT history.url AS url, icons.iconID AS iconID FROM " + + "\(TableHistory), \(ViewWidestFaviconsForSites) AS icons WHERE " + + "\(TableHistory).id = icons.siteID " + + let bookmarks = + "CREATE TABLE IF NOT EXISTS bookmarks (" + + "id INTEGER PRIMARY KEY AUTOINCREMENT, " + + "guid TEXT NOT NULL UNIQUE, " + + "type TINYINT NOT NULL, " + + "url TEXT, " + + "parent INTEGER REFERENCES bookmarks(id) NOT NULL, " + + "faviconID INTEGER REFERENCES favicons(id) ON DELETE SET NULL, " + + "title TEXT" + + ") " + + let queries = [ + // This used to be done by FaviconsTable. + self.oldFaviconsSQL, + CreateDomainsTable(), + CreateHistoryTable(), + visits, bookmarks, faviconSites, + indexShouldUpload, indexSiteIDDate, + widestFavicons, historyIDsWithIcon, iconForURL, + CreateQueueTable(), + ] + + return self.run(db, queries: queries) && + self.prepopulateRootFolders(db) + } +} + +class BrowserSchemaV7: BaseHistoricalBrowserSchema { + override var version: Int { return 7 } + + func prepopulateRootFolders(_ db: SQLiteDBConnection) -> Bool { + let type = BookmarkNodeType.folder.rawValue + let root = BookmarkRoots.RootID + + let titleMobile = NSLocalizedString("Mobile Bookmarks", tableName: "Storage", comment: "The title of the folder that contains mobile bookmarks. This should match bookmarks.folder.mobile.label on Android.") + let titleMenu = NSLocalizedString("Bookmarks Menu", tableName: "Storage", comment: "The name of the folder that contains desktop bookmarks in the menu. This should match bookmarks.folder.menu.label on Android.") + let titleToolbar = NSLocalizedString("Bookmarks Toolbar", tableName: "Storage", comment: "The name of the folder that contains desktop bookmarks in the toolbar. This should match bookmarks.folder.toolbar.label on Android.") + let titleUnsorted = NSLocalizedString("Unsorted Bookmarks", tableName: "Storage", comment: "The name of the folder that contains unsorted desktop bookmarks. This should match bookmarks.folder.unfiled.label on Android.") + + let args: Args = [ + root, BookmarkRoots.RootGUID, type, "Root", root, + BookmarkRoots.MobileID, BookmarkRoots.MobileFolderGUID, type, titleMobile, root, + BookmarkRoots.MenuID, BookmarkRoots.MenuFolderGUID, type, titleMenu, root, + BookmarkRoots.ToolbarID, BookmarkRoots.ToolbarFolderGUID, type, titleToolbar, root, + BookmarkRoots.UnfiledID, BookmarkRoots.UnfiledFolderGUID, type, titleUnsorted, root, + ] + + let sql = + "INSERT INTO bookmarks (id, guid, type, url, title, parent) VALUES " + + "(?, ?, ?, NULL, ?, ?), " + // Root + "(?, ?, ?, NULL, ?, ?), " + // Mobile + "(?, ?, ?, NULL, ?, ?), " + // Menu + "(?, ?, ?, NULL, ?, ?), " + // Toolbar + "(?, ?, ?, NULL, ?, ?) " // Unsorted + + return self.run(db, sql: sql, args: args) + } + + func getHistoryTableCreationString() -> String { + return "CREATE TABLE IF NOT EXISTS history (" + + "id INTEGER PRIMARY KEY AUTOINCREMENT, " + + "guid TEXT NOT NULL UNIQUE, " + // Not null, but the value might be replaced by the server's. + "url TEXT UNIQUE, " + // May only be null for deleted records. + "title TEXT NOT NULL, " + + "server_modified INTEGER, " + // Can be null. Integer milliseconds. + "local_modified INTEGER, " + // Can be null. Client clock. In extremis only. + "is_deleted TINYINT NOT NULL, " + // Boolean. Locally deleted. + "should_upload TINYINT NOT NULL, " + // Boolean. Set when changed or visits added. + "domain_id INTEGER REFERENCES \(TableDomains)(id) ON DELETE CASCADE, " + + "CONSTRAINT urlOrDeleted CHECK (url IS NOT NULL OR is_deleted = 1)" + + ")" + } + + func getDomainsTableCreationString() -> String { + return "CREATE TABLE IF NOT EXISTS \(TableDomains) (" + + "id INTEGER PRIMARY KEY AUTOINCREMENT, " + + "domain TEXT NOT NULL UNIQUE, " + + "showOnTopSites TINYINT NOT NULL DEFAULT 1" + + ")" + } + + func getQueueTableCreationString() -> String { + return "CREATE TABLE IF NOT EXISTS \(TableQueuedTabs) (" + + "url TEXT NOT NULL UNIQUE, " + + "title TEXT" + + ") " + } + + override func create(_ db: SQLiteDBConnection) -> Bool { + // Right now we don't need to track per-visit deletions: Sync can't + // represent them! See Bug 1157553 Comment 6. + // We flip the should_upload flag on the history item when we add a visit. + // If we ever want to support logic like not bothering to sync if we added + // and then rapidly removed a visit, then we need an 'is_new' flag on each visit. + let visits = + "CREATE TABLE IF NOT EXISTS \(TableVisits) (" + + "id INTEGER PRIMARY KEY AUTOINCREMENT, " + + "siteID INTEGER NOT NULL REFERENCES history(id) ON DELETE CASCADE, " + + "date REAL NOT NULL, " + // Microseconds since epoch. + "type INTEGER NOT NULL, " + + "is_local TINYINT NOT NULL, " + // Some visits are local. Some are remote ('mirrored'). This boolean flag is the split. + "UNIQUE (siteID, date, type) " + + ") " + + let indexShouldUpload: String + if self.supportsPartialIndices { + // There's no point tracking rows that are not flagged for upload. + indexShouldUpload = + "CREATE INDEX IF NOT EXISTS \(IndexHistoryShouldUpload) " + + "ON history (should_upload) WHERE should_upload = 1" + } else { + indexShouldUpload = + "CREATE INDEX IF NOT EXISTS \(IndexHistoryShouldUpload) " + + "ON history (should_upload)" + } + + let indexSiteIDDate = + "CREATE INDEX IF NOT EXISTS \(IndexVisitsSiteIDIsLocalDate) " + + "ON \(TableVisits) (siteID, is_local, date)" + + let faviconSites = + "CREATE TABLE IF NOT EXISTS \(TableFaviconSites) (" + + "id INTEGER PRIMARY KEY AUTOINCREMENT, " + + "siteID INTEGER NOT NULL REFERENCES history(id) ON DELETE CASCADE, " + + "faviconID INTEGER NOT NULL REFERENCES favicons(id) ON DELETE CASCADE, " + + "UNIQUE (siteID, faviconID) " + + ") " + + let widestFavicons = + "CREATE VIEW IF NOT EXISTS \(ViewWidestFaviconsForSites) AS " + + "SELECT " + + "\(TableFaviconSites).siteID AS siteID, " + + "favicons.id AS iconID, " + + "favicons.url AS iconURL, " + + "favicons.date AS iconDate, " + + "favicons.type AS iconType, " + + "MAX(favicons.width) AS iconWidth " + + "FROM \(TableFaviconSites), favicons WHERE " + + "\(TableFaviconSites).faviconID = favicons.id " + + "GROUP BY siteID " + + let historyIDsWithIcon = + "CREATE VIEW IF NOT EXISTS \(ViewHistoryIDsWithWidestFavicons) AS " + + "SELECT history.id AS id, " + + "iconID, iconURL, iconDate, iconType, iconWidth " + + "FROM history " + + "LEFT OUTER JOIN " + + "\(ViewWidestFaviconsForSites) ON history.id = \(ViewWidestFaviconsForSites).siteID " + + let iconForURL = + "CREATE VIEW IF NOT EXISTS \(ViewIconForURL) AS " + + "SELECT history.url AS url, icons.iconID AS iconID FROM " + + "\(TableHistory), \(ViewWidestFaviconsForSites) AS icons WHERE " + + "\(TableHistory).id = icons.siteID " + + let bookmarks = + "CREATE TABLE IF NOT EXISTS bookmarks (" + + "id INTEGER PRIMARY KEY AUTOINCREMENT, " + + "guid TEXT NOT NULL UNIQUE, " + + "type TINYINT NOT NULL, " + + "url TEXT, " + + "parent INTEGER REFERENCES bookmarks(id) NOT NULL, " + + "faviconID INTEGER REFERENCES favicons(id) ON DELETE SET NULL, " + + "title TEXT" + + ") " + + let queries = [ + // This used to be done by FaviconsTable. + self.oldFaviconsSQL, + getDomainsTableCreationString(), + getHistoryTableCreationString(), + visits, bookmarks, faviconSites, + indexShouldUpload, indexSiteIDDate, + widestFavicons, historyIDsWithIcon, iconForURL, + getQueueTableCreationString(), + ] + + return self.run(db, queries: queries) && + self.prepopulateRootFolders(db) + } +} + +class BrowserSchemaV8: BaseHistoricalBrowserSchema { + override var version: Int { return 8 } + + func prepopulateRootFolders(_ db: SQLiteDBConnection) -> Bool { + let type = BookmarkNodeType.folder.rawValue + let root = BookmarkRoots.RootID + + let titleMobile = NSLocalizedString("Mobile Bookmarks", tableName: "Storage", comment: "The title of the folder that contains mobile bookmarks. This should match bookmarks.folder.mobile.label on Android.") + let titleMenu = NSLocalizedString("Bookmarks Menu", tableName: "Storage", comment: "The name of the folder that contains desktop bookmarks in the menu. This should match bookmarks.folder.menu.label on Android.") + let titleToolbar = NSLocalizedString("Bookmarks Toolbar", tableName: "Storage", comment: "The name of the folder that contains desktop bookmarks in the toolbar. This should match bookmarks.folder.toolbar.label on Android.") + let titleUnsorted = NSLocalizedString("Unsorted Bookmarks", tableName: "Storage", comment: "The name of the folder that contains unsorted desktop bookmarks. This should match bookmarks.folder.unfiled.label on Android.") + + let args: Args = [ + root, BookmarkRoots.RootGUID, type, "Root", root, + BookmarkRoots.MobileID, BookmarkRoots.MobileFolderGUID, type, titleMobile, root, + BookmarkRoots.MenuID, BookmarkRoots.MenuFolderGUID, type, titleMenu, root, + BookmarkRoots.ToolbarID, BookmarkRoots.ToolbarFolderGUID, type, titleToolbar, root, + BookmarkRoots.UnfiledID, BookmarkRoots.UnfiledFolderGUID, type, titleUnsorted, root, + ] + + let sql = + "INSERT INTO bookmarks (id, guid, type, url, title, parent) VALUES " + + "(?, ?, ?, NULL, ?, ?), " + // Root + "(?, ?, ?, NULL, ?, ?), " + // Mobile + "(?, ?, ?, NULL, ?, ?), " + // Menu + "(?, ?, ?, NULL, ?, ?), " + // Toolbar + "(?, ?, ?, NULL, ?, ?) " // Unsorted + + return self.run(db, sql: sql, args: args) + } + + func getHistoryTableCreationString() -> String { + return "CREATE TABLE IF NOT EXISTS \(TableHistory) (" + + "id INTEGER PRIMARY KEY AUTOINCREMENT, " + + "guid TEXT NOT NULL UNIQUE, " + // Not null, but the value might be replaced by the server's. + "url TEXT UNIQUE, " + // May only be null for deleted records. + "title TEXT NOT NULL, " + + "server_modified INTEGER, " + // Can be null. Integer milliseconds. + "local_modified INTEGER, " + // Can be null. Client clock. In extremis only. + "is_deleted TINYINT NOT NULL, " + // Boolean. Locally deleted. + "should_upload TINYINT NOT NULL, " + // Boolean. Set when changed or visits added. + "domain_id INTEGER REFERENCES \(TableDomains)(id) ON DELETE CASCADE, " + + "CONSTRAINT urlOrDeleted CHECK (url IS NOT NULL OR is_deleted = 1)" + + ")" + } + + func getDomainsTableCreationString() -> String { + return "CREATE TABLE IF NOT EXISTS \(TableDomains) (" + + "id INTEGER PRIMARY KEY AUTOINCREMENT, " + + "domain TEXT NOT NULL UNIQUE, " + + "showOnTopSites TINYINT NOT NULL DEFAULT 1" + + ")" + } + + func getQueueTableCreationString() -> String { + return "CREATE TABLE IF NOT EXISTS \(TableQueuedTabs) (" + + "url TEXT NOT NULL UNIQUE, " + + "title TEXT" + + ") " + } + + override func create(_ db: SQLiteDBConnection) -> Bool { + let favicons = + "CREATE TABLE IF NOT EXISTS favicons (" + + "id INTEGER PRIMARY KEY AUTOINCREMENT, " + + "url TEXT NOT NULL UNIQUE, " + + "width INTEGER, " + + "height INTEGER, " + + "type INTEGER NOT NULL, " + + "date REAL NOT NULL" + + ") " + + // Right now we don't need to track per-visit deletions: Sync can't + // represent them! See Bug 1157553 Comment 6. + // We flip the should_upload flag on the history item when we add a visit. + // If we ever want to support logic like not bothering to sync if we added + // and then rapidly removed a visit, then we need an 'is_new' flag on each visit. + let visits = + "CREATE TABLE IF NOT EXISTS visits (" + + "id INTEGER PRIMARY KEY AUTOINCREMENT, " + + "siteID INTEGER NOT NULL REFERENCES history(id) ON DELETE CASCADE, " + + "date REAL NOT NULL, " + // Microseconds since epoch. + "type INTEGER NOT NULL, " + + "is_local TINYINT NOT NULL, " + // Some visits are local. Some are remote ('mirrored'). This boolean flag is the split. + "UNIQUE (siteID, date, type) " + + ") " + + let indexShouldUpload: String + if self.supportsPartialIndices { + // There's no point tracking rows that are not flagged for upload. + indexShouldUpload = + "CREATE INDEX IF NOT EXISTS \(IndexHistoryShouldUpload) " + + "ON \(TableHistory) (should_upload) WHERE should_upload = 1" + } else { + indexShouldUpload = + "CREATE INDEX IF NOT EXISTS \(IndexHistoryShouldUpload) " + + "ON \(TableHistory) (should_upload)" + } + + let indexSiteIDDate = + "CREATE INDEX IF NOT EXISTS \(IndexVisitsSiteIDIsLocalDate) " + + "ON \(TableVisits) (siteID, is_local, date)" + + let faviconSites = + "CREATE TABLE IF NOT EXISTS \(TableFaviconSites) (" + + "id INTEGER PRIMARY KEY AUTOINCREMENT, " + + "siteID INTEGER NOT NULL REFERENCES history(id) ON DELETE CASCADE, " + + "faviconID INTEGER NOT NULL REFERENCES favicons(id) ON DELETE CASCADE, " + + "UNIQUE (siteID, faviconID) " + + ") " + + let widestFavicons = + "CREATE VIEW IF NOT EXISTS \(ViewWidestFaviconsForSites) AS " + + "SELECT " + + "\(TableFaviconSites).siteID AS siteID, " + + "favicons.id AS iconID, " + + "favicons.url AS iconURL, " + + "favicons.date AS iconDate, " + + "favicons.type AS iconType, " + + "MAX(favicons.width) AS iconWidth " + + "FROM \(TableFaviconSites), favicons WHERE " + + "\(TableFaviconSites).faviconID = favicons.id " + + "GROUP BY siteID " + + let historyIDsWithIcon = + "CREATE VIEW IF NOT EXISTS \(ViewHistoryIDsWithWidestFavicons) AS " + + "SELECT history.id AS id, " + + "iconID, iconURL, iconDate, iconType, iconWidth " + + "FROM history " + + "LEFT OUTER JOIN " + + "\(ViewWidestFaviconsForSites) ON history.id = \(ViewWidestFaviconsForSites).siteID " + + let iconForURL = + "CREATE VIEW IF NOT EXISTS \(ViewIconForURL) AS " + + "SELECT history.url AS url, icons.iconID AS iconID FROM " + + "history, \(ViewWidestFaviconsForSites) AS icons WHERE " + + "history.id = icons.siteID " + + let bookmarks = + "CREATE TABLE IF NOT EXISTS bookmarks (" + + "id INTEGER PRIMARY KEY AUTOINCREMENT, " + + "guid TEXT NOT NULL UNIQUE, " + + "type TINYINT NOT NULL, " + + "url TEXT, " + + "parent INTEGER REFERENCES bookmarks(id) NOT NULL, " + + "faviconID INTEGER REFERENCES favicons(id) ON DELETE SET NULL, " + + "title TEXT" + + ") " + + let queries: [String] = [ + getDomainsTableCreationString(), + getHistoryTableCreationString(), + favicons, + visits, + bookmarks, + faviconSites, + indexShouldUpload, + indexSiteIDDate, + widestFavicons, + historyIDsWithIcon, + iconForURL, + getQueueTableCreationString(), + ] + + return self.run(db, queries: queries) && + self.prepopulateRootFolders(db) + } +} + +class BrowserSchemaV10: BaseHistoricalBrowserSchema { + override var version: Int { return 10 } + + func prepopulateRootFolders(_ db: SQLiteDBConnection) -> Bool { + let type = BookmarkNodeType.folder.rawValue + let root = BookmarkRoots.RootID + + let titleMobile = NSLocalizedString("Mobile Bookmarks", tableName: "Storage", comment: "The title of the folder that contains mobile bookmarks. This should match bookmarks.folder.mobile.label on Android.") + let titleMenu = NSLocalizedString("Bookmarks Menu", tableName: "Storage", comment: "The name of the folder that contains desktop bookmarks in the menu. This should match bookmarks.folder.menu.label on Android.") + let titleToolbar = NSLocalizedString("Bookmarks Toolbar", tableName: "Storage", comment: "The name of the folder that contains desktop bookmarks in the toolbar. This should match bookmarks.folder.toolbar.label on Android.") + let titleUnsorted = NSLocalizedString("Unsorted Bookmarks", tableName: "Storage", comment: "The name of the folder that contains unsorted desktop bookmarks. This should match bookmarks.folder.unfiled.label on Android.") + + let args: Args = [ + root, BookmarkRoots.RootGUID, type, "Root", root, + BookmarkRoots.MobileID, BookmarkRoots.MobileFolderGUID, type, titleMobile, root, + BookmarkRoots.MenuID, BookmarkRoots.MenuFolderGUID, type, titleMenu, root, + BookmarkRoots.ToolbarID, BookmarkRoots.ToolbarFolderGUID, type, titleToolbar, root, + BookmarkRoots.UnfiledID, BookmarkRoots.UnfiledFolderGUID, type, titleUnsorted, root, + ] + + let sql = + "INSERT INTO bookmarks (id, guid, type, url, title, parent) VALUES " + + "(?, ?, ?, NULL, ?, ?), " + // Root + "(?, ?, ?, NULL, ?, ?), " + // Mobile + "(?, ?, ?, NULL, ?, ?), " + // Menu + "(?, ?, ?, NULL, ?, ?), " + // Toolbar + "(?, ?, ?, NULL, ?, ?) " // Unsorted + + return self.run(db, sql: sql, args: args) + } + + func getHistoryTableCreationString(forVersion version: Int = BrowserSchema.DefaultVersion) -> String { + return "CREATE TABLE IF NOT EXISTS history (" + + "id INTEGER PRIMARY KEY AUTOINCREMENT, " + + "guid TEXT NOT NULL UNIQUE, " + // Not null, but the value might be replaced by the server's. + "url TEXT UNIQUE, " + // May only be null for deleted records. + "title TEXT NOT NULL, " + + "server_modified INTEGER, " + // Can be null. Integer milliseconds. + "local_modified INTEGER, " + // Can be null. Client clock. In extremis only. + "is_deleted TINYINT NOT NULL, " + // Boolean. Locally deleted. + "should_upload TINYINT NOT NULL, " + // Boolean. Set when changed or visits added. + "domain_id INTEGER REFERENCES \(TableDomains)(id) ON DELETE CASCADE, " + + "CONSTRAINT urlOrDeleted CHECK (url IS NOT NULL OR is_deleted = 1)" + + ")" + } + + func getDomainsTableCreationString() -> String { + return "CREATE TABLE IF NOT EXISTS \(TableDomains) (" + + "id INTEGER PRIMARY KEY AUTOINCREMENT, " + + "domain TEXT NOT NULL UNIQUE, " + + "showOnTopSites TINYINT NOT NULL DEFAULT 1" + + ")" + } + + func getQueueTableCreationString() -> String { + return "CREATE TABLE IF NOT EXISTS \(TableQueuedTabs) (" + + "url TEXT NOT NULL UNIQUE, " + + "title TEXT" + + ") " + } + + func getBookmarksMirrorTableCreationString() -> String { + // The stupid absence of naming conventions here is thanks to pre-Sync Weave. Sorry. + // For now we have the simplest possible schema: everything in one. + let sql = + "CREATE TABLE IF NOT EXISTS \(TableBookmarksMirror) " + + + // Shared fields. + "( id INTEGER PRIMARY KEY AUTOINCREMENT" + + ", guid TEXT NOT NULL UNIQUE" + + ", type TINYINT NOT NULL" + // Type enum. TODO: BookmarkNodeType needs to be extended. + + // Record/envelope metadata that'll allow us to do merges. + ", server_modified INTEGER NOT NULL" + // Milliseconds. + ", is_deleted TINYINT NOT NULL DEFAULT 0" + // Boolean + + ", hasDupe TINYINT NOT NULL DEFAULT 0" + // Boolean, 0 (false) if deleted. + ", parentid TEXT" + // GUID + ", parentName TEXT" + + + // Type-specific fields. These should be NOT NULL in many cases, but we're going + // for a sparse schema, so this'll do for now. Enforce these in the application code. + ", feedUri TEXT, siteUri TEXT" + // LIVEMARKS + ", pos INT" + // SEPARATORS + ", title TEXT, description TEXT" + // FOLDERS, BOOKMARKS, QUERIES + ", bmkUri TEXT, tags TEXT, keyword TEXT" + // BOOKMARKS, QUERIES + ", folderName TEXT, queryId TEXT" + // QUERIES + ", CONSTRAINT parentidOrDeleted CHECK (parentid IS NOT NULL OR is_deleted = 1)" + + ", CONSTRAINT parentNameOrDeleted CHECK (parentName IS NOT NULL OR is_deleted = 1)" + + ")" + + return sql + } + + /** + * We need to explicitly store what's provided by the server, because we can't rely on + * referenced child nodes to exist yet! + */ + func getBookmarksMirrorStructureTableCreationString() -> String { + return "CREATE TABLE IF NOT EXISTS \(TableBookmarksMirrorStructure) " + + "( parent TEXT NOT NULL REFERENCES \(TableBookmarksMirror)(guid) ON DELETE CASCADE" + + ", child TEXT NOT NULL" + // Should be the GUID of a child. + ", idx INTEGER NOT NULL" + // Should advance from 0. + ")" + } + + override func create(_ db: SQLiteDBConnection) -> Bool { + let favicons = + "CREATE TABLE IF NOT EXISTS favicons (" + + "id INTEGER PRIMARY KEY AUTOINCREMENT, " + + "url TEXT NOT NULL UNIQUE, " + + "width INTEGER, " + + "height INTEGER, " + + "type INTEGER NOT NULL, " + + "date REAL NOT NULL" + + ") " + + // Right now we don't need to track per-visit deletions: Sync can't + // represent them! See Bug 1157553 Comment 6. + // We flip the should_upload flag on the history item when we add a visit. + // If we ever want to support logic like not bothering to sync if we added + // and then rapidly removed a visit, then we need an 'is_new' flag on each visit. + let visits = + "CREATE TABLE IF NOT EXISTS visits (" + + "id INTEGER PRIMARY KEY AUTOINCREMENT, " + + "siteID INTEGER NOT NULL REFERENCES history(id) ON DELETE CASCADE, " + + "date REAL NOT NULL, " + // Microseconds since epoch. + "type INTEGER NOT NULL, " + + "is_local TINYINT NOT NULL, " + // Some visits are local. Some are remote ('mirrored'). This boolean flag is the split. + "UNIQUE (siteID, date, type) " + + ") " + + let indexShouldUpload: String + if self.supportsPartialIndices { + // There's no point tracking rows that are not flagged for upload. + indexShouldUpload = + "CREATE INDEX IF NOT EXISTS \(IndexHistoryShouldUpload) " + + "ON history (should_upload) WHERE should_upload = 1" + } else { + indexShouldUpload = + "CREATE INDEX IF NOT EXISTS \(IndexHistoryShouldUpload) " + + "ON history (should_upload)" + } + + let indexSiteIDDate = + "CREATE INDEX IF NOT EXISTS \(IndexVisitsSiteIDIsLocalDate) " + + "ON visits (siteID, is_local, date)" + + let faviconSites = + "CREATE TABLE IF NOT EXISTS \(TableFaviconSites) (" + + "id INTEGER PRIMARY KEY AUTOINCREMENT, " + + "siteID INTEGER NOT NULL REFERENCES history(id) ON DELETE CASCADE, " + + "faviconID INTEGER NOT NULL REFERENCES favicons(id) ON DELETE CASCADE, " + + "UNIQUE (siteID, faviconID) " + + ") " + + let widestFavicons = + "CREATE VIEW IF NOT EXISTS \(ViewWidestFaviconsForSites) AS " + + "SELECT " + + "\(TableFaviconSites).siteID AS siteID, " + + "favicons.id AS iconID, " + + "favicons.url AS iconURL, " + + "favicons.date AS iconDate, " + + "favicons.type AS iconType, " + + "MAX(favicons.width) AS iconWidth " + + "FROM \(TableFaviconSites), favicons WHERE " + + "\(TableFaviconSites).faviconID = favicons.id " + + "GROUP BY siteID " + + let historyIDsWithIcon = + "CREATE VIEW IF NOT EXISTS \(ViewHistoryIDsWithWidestFavicons) AS " + + "SELECT history.id AS id, " + + "iconID, iconURL, iconDate, iconType, iconWidth " + + "FROM history " + + "LEFT OUTER JOIN " + + "\(ViewWidestFaviconsForSites) ON history.id = \(ViewWidestFaviconsForSites).siteID " + + let iconForURL = + "CREATE VIEW IF NOT EXISTS \(ViewIconForURL) AS " + + "SELECT history.url AS url, icons.iconID AS iconID FROM " + + "history, \(ViewWidestFaviconsForSites) AS icons WHERE " + + "history.id = icons.siteID " + + let bookmarks = + "CREATE TABLE IF NOT EXISTS bookmarks (" + + "id INTEGER PRIMARY KEY AUTOINCREMENT, " + + "guid TEXT NOT NULL UNIQUE, " + + "type TINYINT NOT NULL, " + + "url TEXT, " + + "parent INTEGER REFERENCES bookmarks(id) NOT NULL, " + + "faviconID INTEGER REFERENCES favicons(id) ON DELETE SET NULL, " + + "title TEXT" + + ") " + + let bookmarksMirror = getBookmarksMirrorTableCreationString() + let bookmarksMirrorStructure = getBookmarksMirrorStructureTableCreationString() + + let indexStructureParentIdx = "CREATE INDEX IF NOT EXISTS \(IndexBookmarksMirrorStructureParentIdx) " + + "ON \(TableBookmarksMirrorStructure) (parent, idx)" + + let queries: [String] = [ + getDomainsTableCreationString(), + getHistoryTableCreationString(), + favicons, + visits, + bookmarks, + bookmarksMirror, + bookmarksMirrorStructure, + indexStructureParentIdx, + faviconSites, + indexShouldUpload, + indexSiteIDDate, + widestFavicons, + historyIDsWithIcon, + iconForURL, + getQueueTableCreationString(), + ] + + return self.run(db, queries: queries) && + self.prepopulateRootFolders(db) + } +} + +class TestSQLiteHistory: XCTestCase { + let files = MockFiles() + + fileprivate func deleteDatabases() { + for v in ["6", "7", "8", "10", "6-data"] { + do { + try files.remove("browser-v\(v).db") + } catch {} + } + do { + try files.remove("browser.db") + try files.remove("historysynced.db") + } catch {} + } + + override func tearDown() { + super.tearDown() + self.deleteDatabases() + } + + override func setUp() { + super.setUp() + + // Just in case tearDown didn't run or succeed last time! + self.deleteDatabases() + } + + // Test that our visit partitioning for frecency is correct. + func testHistoryLocalAndRemoteVisits() { + let db = BrowserDB(filename: "browser.db", schema: BrowserSchema(), files: files) + let prefs = MockProfilePrefs() + let history = SQLiteHistory(db: db, prefs: prefs) + + let siteL = Site(url: "http://url1/", title: "title local only") + let siteR = Site(url: "http://url2/", title: "title remote only") + let siteB = Site(url: "http://url3/", title: "title local and remote") + + siteL.guid = "locallocal12" + siteR.guid = "remoteremote" + siteB.guid = "bothbothboth" + + let siteVisitL1 = SiteVisit(site: siteL, date: baseInstantInMicros + 1000, type: VisitType.link) + let siteVisitL2 = SiteVisit(site: siteL, date: baseInstantInMicros + 2000, type: VisitType.link) + + let siteVisitR1 = SiteVisit(site: siteR, date: baseInstantInMicros + 1000, type: VisitType.link) + let siteVisitR2 = SiteVisit(site: siteR, date: baseInstantInMicros + 2000, type: VisitType.link) + let siteVisitR3 = SiteVisit(site: siteR, date: baseInstantInMicros + 3000, type: VisitType.link) + + let siteVisitBL1 = SiteVisit(site: siteB, date: baseInstantInMicros + 4000, type: VisitType.link) + let siteVisitBR1 = SiteVisit(site: siteB, date: baseInstantInMicros + 5000, type: VisitType.link) + + let deferred = + history.clearHistory() + >>> { history.addLocalVisit(siteVisitL1) } + >>> { history.addLocalVisit(siteVisitL2) } + >>> { history.addLocalVisit(siteVisitBL1) } + >>> { history.insertOrUpdatePlace(siteL.asPlace(), modified: baseInstantInMillis + 2) } + >>> { history.insertOrUpdatePlace(siteR.asPlace(), modified: baseInstantInMillis + 3) } + >>> { history.insertOrUpdatePlace(siteB.asPlace(), modified: baseInstantInMillis + 5) } + + // Do this step twice, so we exercise the dupe-visit handling. + >>> { history.storeRemoteVisits([siteVisitR1, siteVisitR2, siteVisitR3], forGUID: siteR.guid!) } + >>> { history.storeRemoteVisits([siteVisitR1, siteVisitR2, siteVisitR3], forGUID: siteR.guid!) } + + >>> { history.storeRemoteVisits([siteVisitBR1], forGUID: siteB.guid!) } + + >>> { + history.getFrecentHistory().getSites(whereURLContains: nil, historyLimit: 3, bookmarksLimit: 0) + >>== { (sites: Cursor) -> Success in + XCTAssertEqual(3, sites.count) + + // Two local visits beat a single later remote visit and one later local visit. + // Two local visits beat three remote visits. + XCTAssertEqual(siteL.guid!, sites[0]!.guid!) + XCTAssertEqual(siteB.guid!, sites[1]!.guid!) + XCTAssertEqual(siteR.guid!, sites[2]!.guid!) + return succeed() + } + + // This marks everything as modified so we can fetch it. + >>> history.onRemovedAccount + + // Now check that we have no duplicate visits. + >>> { history.getModifiedHistoryToUpload() + >>== { (places) -> Success in + if let (_, visits) = places.find({$0.0.guid == siteR.guid!}) { + XCTAssertEqual(3, visits.count) + } else { + XCTFail("Couldn't find site R.") + } + return succeed() + } + } + } + + XCTAssertTrue(deferred.value.isSuccess) + } + + func testUpgrades() { + let sources: [(Int, Schema)] = [ + (6, BrowserSchemaV6()), + (7, BrowserSchemaV7()), + (8, BrowserSchemaV8()), + (10, BrowserSchemaV10()), + ] + + let destination = BrowserSchema() + + for (version, schema) in sources { + var db = BrowserDB(filename: "browser-v\(version).db", schema: schema, files: files) + XCTAssertTrue(db.withConnection({ connection -> Int in + connection.version + }).value.successValue == schema.version, "Creating BrowserSchema at version \(version)") + db.forceClose() + + db = BrowserDB(filename: "browser-v\(version).db", schema: destination, files: files) + XCTAssertTrue(db.withConnection({ connection -> Int in + connection.version + }).value.successValue == destination.version, "Upgrading BrowserSchema from version \(version) to version \(schema.version)") + db.forceClose() + } + } + + func testUpgradesWithData() { + var db = BrowserDB(filename: "browser-v6-data.db", schema: BrowserSchemaV6(), files: files) + + // Insert some data. + let queries = [ + "INSERT INTO domains (id, domain) VALUES (1, 'example.com')", + "INSERT INTO history (id, guid, url, title, server_modified, local_modified, is_deleted, should_upload, domain_id) VALUES (5, 'guid', 'http://www.example.com', 'title', 5, 10, 0, 1, 1)", + "INSERT INTO visits (siteID, date, type, is_local) VALUES (5, 15, 1, 1)", + "INSERT INTO favicons (url, width, height, type, date) VALUES ('http://www.example.com/favicon.ico', 10, 10, 1, 20)", + "INSERT INTO favicon_sites (siteID, faviconID) VALUES (5, 1)", + "INSERT INTO bookmarks (guid, type, url, parent, faviconID, title) VALUES ('guid', 1, 'http://www.example.com', 0, 1, 'title')" + ] + + XCTAssertTrue(db.run(queries).value.isSuccess) + + // And we can upgrade to the current version. + db = BrowserDB(filename: "browser-v6-data.db", schema: BrowserSchema(), files: files) + + let prefs = MockProfilePrefs() + let history = SQLiteHistory(db: db, prefs: prefs) + let results = history.getSitesByLastVisit(10).value.successValue + XCTAssertNotNil(results) + XCTAssertEqual(results![0]?.url, "http://www.example.com") + + db.forceClose() + } + + func testDomainUpgrade() { + let db = BrowserDB(filename: "browser.db", schema: BrowserSchema(), files: files) + let prefs = MockProfilePrefs() + let history = SQLiteHistory(db: db, prefs: prefs) + + let site = Site(url: "http://www.example.com/test1.1", title: "title one") + + // Insert something with an invalid domain ID. We have to manually do this since domains are usually hidden. + let insertDeferred = db.withConnection { connection -> Void in + try connection.executeChange("PRAGMA foreign_keys = OFF") + let insert = "INSERT INTO \(TableHistory) (guid, url, title, local_modified, is_deleted, should_upload, domain_id) VALUES (?, ?, ?, ?, ?, ?, ?)" + let args: Args = [Bytes.generateGUID(), site.url, site.title, Date.now(), 0, 0, -1] + try connection.executeChange(insert, withArgs: args) + } + + XCTAssertTrue(insertDeferred.value.isSuccess) + + // Now insert it again. This should update the domain. + history.addLocalVisit(SiteVisit(site: site, date: Date.nowMicroseconds(), type: VisitType.link)).succeeded() + + // domain_id isn't normally exposed, so we manually query to get it. + let resultsDeferred = db.withConnection { connection -> Cursor in + let sql = "SELECT domain_id FROM \(TableHistory) WHERE url = ?" + let args: Args = [site.url] + return connection.executeQuery(sql, factory: { $0[0] as? Int }, withArgs: args) + } + + let results = resultsDeferred.value.successValue! + let domain = results[0]! // Unwrap to get the first item from the cursor. + XCTAssertNil(domain) + } + + func testDomains() { + let db = BrowserDB(filename: "browser.db", schema: BrowserSchema(), files: files) + let prefs = MockProfilePrefs() + let history = SQLiteHistory(db: db, prefs: prefs) + + let initialGuid = Bytes.generateGUID() + let site11 = Site(url: "http://www.example.com/test1.1", title: "title one") + let site12 = Site(url: "http://www.example.com/test1.2", title: "title two") + let site13 = Place(guid: initialGuid, url: "http://www.example.com/test1.3", title: "title three") + let site3 = Site(url: "http://www.example2.com/test1", title: "title three") + let expectation = self.expectation(description: "First.") + + let clearTopSites = "DELETE FROM \(TableCachedTopSites)" + let updateTopSites: [(String, Args?)] = [(clearTopSites, nil), (history.getFrecentHistory().updateTopSitesCacheQuery())] + + func countTopSites() -> Deferred>> { + return db.runQuery("SELECT COUNT(*) from \(TableCachedTopSites)", args: nil, factory: { sdrow -> Int in + return sdrow[0] as? Int ?? 0 + }) + } + + history.clearHistory().bind({ success in + return all([history.addLocalVisit(SiteVisit(site: site11, date: Date.nowMicroseconds(), type: VisitType.link)), + history.addLocalVisit(SiteVisit(site: site12, date: Date.nowMicroseconds(), type: VisitType.link)), + history.addLocalVisit(SiteVisit(site: site3, date: Date.nowMicroseconds(), type: VisitType.link))]) + }).bind({ (results: [Maybe<()>]) in + return history.insertOrUpdatePlace(site13, modified: Date.nowMicroseconds()) + }).bind({ guid -> Success in + XCTAssertEqual(guid.successValue!, initialGuid, "Guid is correct") + return db.run(updateTopSites) + }).bind({ success in + XCTAssertTrue(success.isSuccess, "update was successful") + return countTopSites() + }).bind({ (count: Maybe>) -> Success in + XCTAssert(count.successValue![0] == 2, "2 sites returned") + return history.removeSiteFromTopSites(site11) + }).bind({ success -> Success in + XCTAssertTrue(success.isSuccess, "Remove was successful") + return db.run(updateTopSites) + }).bind({ success -> Deferred>> in + XCTAssertTrue(success.isSuccess, "update was successful") + return countTopSites() + }) + .upon({ (count: Maybe>) in + XCTAssert(count.successValue![0] == 1, "1 site returned") + expectation.fulfill() + }) + + waitForExpectations(timeout: 10.0) { error in + return + } + } + + func testHistoryIsSynced() { + let db = BrowserDB(filename: "historysynced.db", schema: BrowserSchema(), files: files) + let prefs = MockProfilePrefs() + let history = SQLiteHistory(db: db, prefs: prefs) + + let initialGUID = Bytes.generateGUID() + let site = Place(guid: initialGUID, url: "http://www.example.com/test1.3", title: "title") + + XCTAssertFalse(history.hasSyncedHistory().value.successValue ?? true) + + XCTAssertTrue(history.insertOrUpdatePlace(site, modified: Date.now()).value.isSuccess) + + XCTAssertTrue(history.hasSyncedHistory().value.successValue ?? false) + } + + // This is a very basic test. Adds an entry, retrieves it, updates it, + // and then clears the database. + func testHistoryTable() { + let db = BrowserDB(filename: "browser.db", schema: BrowserSchema(), files: files) + let prefs = MockProfilePrefs() + let history = SQLiteHistory(db: db, prefs: prefs) + let bookmarks = SQLiteBookmarks(db: db) + + let site1 = Site(url: "http://url1/", title: "title one") + let site1Changed = Site(url: "http://url1/", title: "title one alt") + + let siteVisit1 = SiteVisit(site: site1, date: Date.nowMicroseconds(), type: VisitType.link) + let siteVisit2 = SiteVisit(site: site1Changed, date: Date.nowMicroseconds() + 1000, type: VisitType.bookmark) + + let site2 = Site(url: "http://url2/", title: "title two") + let siteVisit3 = SiteVisit(site: site2, date: Date.nowMicroseconds() + 2000, type: VisitType.link) + + let expectation = self.expectation(description: "First.") + func done() -> Success { + expectation.fulfill() + return succeed() + } + + func checkSitesByFrecency(_ f: @escaping (Cursor) -> Success) -> () -> Success { + return { + history.getFrecentHistory().getSites(whereURLContains: nil, historyLimit: 10, bookmarksLimit: 0) + >>== f + } + } + + func checkSitesByDate(_ f: @escaping (Cursor) -> Success) -> () -> Success { + return { + history.getSitesByLastVisit(10) + >>== f + } + } + + func checkSitesWithFilter(_ filter: String, f: @escaping (Cursor) -> Success) -> () -> Success { + return { + history.getFrecentHistory().getSites(whereURLContains: filter, historyLimit: 10, bookmarksLimit: 0) + >>== f + } + } + + func checkDeletedCount(_ expected: Int) -> () -> Success { + return { + history.getDeletedHistoryToUpload() + >>== { guids in + XCTAssertEqual(expected, guids.count) + return succeed() + } + } + } + + history.clearHistory() + >>> { history.addLocalVisit(siteVisit1) } + >>> checkSitesByFrecency { (sites: Cursor) -> Success in + XCTAssertEqual(1, sites.count) + XCTAssertEqual(site1.title, sites[0]!.title) + XCTAssertEqual(site1.url, sites[0]!.url) + sites.close() + return succeed() + } + >>> { history.addLocalVisit(siteVisit2) } + >>> checkSitesByFrecency { (sites: Cursor) -> Success in + XCTAssertEqual(1, sites.count) + XCTAssertEqual(site1Changed.title, sites[0]!.title) + XCTAssertEqual(site1Changed.url, sites[0]!.url) + sites.close() + return succeed() + } + >>> { history.addLocalVisit(siteVisit3) } + >>> checkSitesByFrecency { (sites: Cursor) -> Success in + XCTAssertEqual(2, sites.count) + // They're in order of frecency. + XCTAssertEqual(site1Changed.title, sites[0]!.title) + XCTAssertEqual(site2.title, sites[1]!.title) + return succeed() + } + >>> checkSitesByDate { (sites: Cursor) -> Success in + XCTAssertEqual(2, sites.count) + // They're in order of date last visited. + let first = sites[0]! + let second = sites[1]! + XCTAssertEqual(site2.title, first.title) + XCTAssertEqual(site1Changed.title, second.title) + XCTAssertTrue(siteVisit3.date == first.latestVisit!.date) + return succeed() + } + >>> checkSitesWithFilter("two") { (sites: Cursor) -> Success in + XCTAssertEqual(1, sites.count) + let first = sites[0]! + XCTAssertEqual(site2.title, first.title) + return succeed() + } + >>> + checkDeletedCount(0) + >>> { history.removeHistoryForURL("http://url2/") } + >>> + checkDeletedCount(1) + >>> checkSitesByFrecency { (sites: Cursor) -> Success in + XCTAssertEqual(1, sites.count) + // They're in order of frecency. + XCTAssertEqual(site1Changed.title, sites[0]!.title) + return succeed() + } + >>> { history.clearHistory() } + >>> + checkDeletedCount(0) + >>> checkSitesByDate { (sites: Cursor) -> Success in + XCTAssertEqual(0, sites.count) + return succeed() + } + >>> checkSitesByFrecency { (sites: Cursor) -> Success in + XCTAssertEqual(0, sites.count) + return succeed() + } + >>> done + + waitForExpectations(timeout: 10.0) { error in + return + } + } + + func testFaviconTable() { + let db = BrowserDB(filename: "browser.db", schema: BrowserSchema(), files: files) + let prefs = MockProfilePrefs() + let history = SQLiteHistory(db: db, prefs: prefs) + let bookmarks = SQLiteBookmarks(db: db) + + let expectation = self.expectation(description: "First.") + func done() -> Success { + expectation.fulfill() + return succeed() + } + + func updateFavicon() -> Success { + let fav = Favicon(url: "http://url2/", date: Date(), type: .icon) + fav.id = 1 + let site = Site(url: "http://bookmarkedurl/", title: "My Bookmark") + return history.addFavicon(fav, forSite: site) >>> succeed + } + + func checkFaviconForBookmarkIsNil() -> Success { + return bookmarks.bookmarksByURL("http://bookmarkedurl/".asURL!) >>== { results in + XCTAssertEqual(1, results.count) + XCTAssertNil(results[0]?.favicon) + return succeed() + } + } + + func checkFaviconWasSetForBookmark() -> Success { + return history.getFaviconsForBookmarkedURL("http://bookmarkedurl/") >>== { results in + XCTAssertEqual(1, results.count) + if let actualFaviconURL = results[0]??.url { + XCTAssertEqual("http://url2/", actualFaviconURL) + } + return succeed() + } + } + + func removeBookmark() -> Success { + return bookmarks.testFactory.removeByURL("http://bookmarkedurl/") + } + + func checkFaviconWasRemovedForBookmark() -> Success { + return history.getFaviconsForBookmarkedURL("http://bookmarkedurl/") >>== { results in + XCTAssertEqual(0, results.count) + return succeed() + } + } + + history.clearAllFavicons() + >>> bookmarks.clearBookmarks + >>> { bookmarks.addToMobileBookmarks("http://bookmarkedurl/".asURL!, title: "Title", favicon: nil) } + >>> checkFaviconForBookmarkIsNil + >>> updateFavicon + >>> checkFaviconWasSetForBookmark + >>> removeBookmark + >>> checkFaviconWasRemovedForBookmark + >>> done + + waitForExpectations(timeout: 10.0) { error in + return + } + } + + func testTopSitesFrecencyOrder() { + let db = BrowserDB(filename: "browser.db", schema: BrowserSchema(), files: files) + let prefs = MockProfilePrefs() + let history = SQLiteHistory(db: db, prefs: prefs) + + history.setTopSitesCacheSize(20) + history.clearTopSitesCache().succeeded() + history.clearHistory().succeeded() + + // Lets create some history. This will create 100 sites that will have 21 local and 21 remote visits + populateHistoryForFrecencyCalculations(history, siteCount: 100) + + // Create a new site thats for an existing domain but a different URL. + let site = Site(url: "http://s\(5)ite\(5).com/foo-different-url", title: "A \(5) different url") + site.guid = "abc\(5)defhi" + history.insertOrUpdatePlace(site.asPlace(), modified: baseInstantInMillis - 20000).succeeded() + // Don't give it any remote visits. But give it 100 local visits. This should be the new Topsite! + for i in 0...100 { + addVisitForSite(site, intoHistory: history, from: .local, atTime: advanceTimestamp(baseInstantInMicros, by: 1000000 * i)) + } + + let expectation = self.expectation(description: "First.") + func done() -> Success { + expectation.fulfill() + return succeed() + } + + func loadCache() -> Success { + return history.repopulate(invalidateTopSites: true, invalidateHighlights: true) >>> succeed + } + + func checkTopSitesReturnsResults() -> Success { + return history.getTopSitesWithLimit(20) >>== { topSites in + XCTAssertEqual(topSites.count, 20) + XCTAssertEqual(topSites[0]!.guid, "abc\(5)defhi") + return succeed() + } + } + + loadCache() + >>> checkTopSitesReturnsResults + >>> done + + waitForExpectations(timeout: 10.0) { error in + return + } + } + + func testTopSitesFiltersGoogle() { + let db = BrowserDB(filename: "browser.db", schema: BrowserSchema(), files: files) + let prefs = MockProfilePrefs() + let history = SQLiteHistory(db: db, prefs: prefs) + + history.setTopSitesCacheSize(20) + history.clearTopSitesCache().succeeded() + history.clearHistory().succeeded() + // Lets create some history. This will create 100 sites that will have 21 local and 21 remote visits + populateHistoryForFrecencyCalculations(history, siteCount: 100) + + func createTopSite(url: String, guid: String) { + let site = Site(url: url, title: "Hi") + site.guid = guid + history.insertOrUpdatePlace(site.asPlace(), modified: baseInstantInMillis - 20000).succeeded() + // Don't give it any remote visits. But give it 100 local visits. This should be the new Topsite! + for i in 0...100 { + addVisitForSite(site, intoHistory: history, from: .local, atTime: advanceTimestamp(baseInstantInMicros, by: 1000000 * i)) + } + } + + createTopSite(url: "http://google.com", guid: "abcgoogle") // should not be a topsite + createTopSite(url: "http://www.google.com", guid: "abcgoogle1") // should not be a topsite + createTopSite(url: "http://google.co.za", guid: "abcgoogleza") // should not be a topsite + createTopSite(url: "http://docs.google.com", guid: "docsgoogle") // should be a topsite + + let expectation = self.expectation(description: "First.") + func done() -> Success { + expectation.fulfill() + return succeed() + } + + func loadCache() -> Success { + return history.repopulate(invalidateTopSites: true, invalidateHighlights: true) >>> succeed + } + + func checkTopSitesReturnsResults() -> Success { + return history.getTopSitesWithLimit(20) >>== { topSites in + XCTAssertEqual(topSites[0]?.guid, "docsgoogle") // google docs should be the first topsite + // make sure all other google guids are not in the topsites array + topSites.forEach { + let guid: String = $0!.guid! // type checking is hard + XCTAssertNil(["abcgoogle", "abcgoogle1", "abcgoogleza"].index(of: guid)) + } + XCTAssertEqual(topSites.count, 20) + return succeed() + } + } + + loadCache() + >>> checkTopSitesReturnsResults + >>> done + + waitForExpectations(timeout: 10.0) { error in + return + } + } + + func testTopSitesCache() { + let db = BrowserDB(filename: "browser.db", schema: BrowserSchema(), files: files) + let prefs = MockProfilePrefs() + let history = SQLiteHistory(db: db, prefs: prefs) + + history.setTopSitesCacheSize(20) + history.clearTopSitesCache().succeeded() + history.clearHistory().succeeded() + + // Make sure that we get back the top sites + populateHistoryForFrecencyCalculations(history, siteCount: 100) + + // Add extra visits to the 5th site to bubble it to the top of the top sites cache + let site = Site(url: "http://s\(5)ite\(5).com/foo", title: "A \(5)") + site.guid = "abc\(5)def" + for i in 0...20 { + addVisitForSite(site, intoHistory: history, from: .local, atTime: advanceTimestamp(baseInstantInMicros, by: 1000000 * i)) + } + + let expectation = self.expectation(description: "First.") + func done() -> Success { + expectation.fulfill() + return succeed() + } + + func loadCache() -> Success { + return history.repopulate(invalidateTopSites: true, invalidateHighlights: true) >>> succeed + } + + func checkTopSitesReturnsResults() -> Success { + return history.getTopSitesWithLimit(20) >>== { topSites in + XCTAssertEqual(topSites.count, 20) + XCTAssertEqual(topSites[0]!.guid, "abc\(5)def") + return succeed() + } + } + + func invalidateIfNeededDoesntChangeResults() -> Success { + return history.repopulate(invalidateTopSites: true, invalidateHighlights: true) >>> { + return history.getTopSitesWithLimit(20) >>== { topSites in + XCTAssertEqual(topSites.count, 20) + XCTAssertEqual(topSites[0]!.guid, "abc\(5)def") + return succeed() + } + } + } + + func addVisitsToZerothSite() -> Success { + let site = Site(url: "http://s\(0)ite\(0).com/foo", title: "A \(0)") + site.guid = "abc\(0)def" + for i in 0...20 { + addVisitForSite(site, intoHistory: history, from: .local, atTime: advanceTimestamp(baseInstantInMicros, by: 1000000 * i)) + } + return succeed() + } + + func markInvalidation() -> Success { + history.setTopSitesNeedsInvalidation() + return succeed() + } + + func checkSitesInvalidate() -> Success { + history.repopulate(invalidateTopSites: true, invalidateHighlights: true).succeeded() + + return history.getTopSitesWithLimit(20) >>== { topSites in + XCTAssertEqual(topSites.count, 20) + XCTAssertEqual(topSites[0]!.guid, "abc\(5)def") + XCTAssertEqual(topSites[1]!.guid, "abc\(0)def") + return succeed() + } + } + + loadCache() + >>> checkTopSitesReturnsResults + >>> invalidateIfNeededDoesntChangeResults + >>> markInvalidation + >>> addVisitsToZerothSite + >>> checkSitesInvalidate + >>> done + + waitForExpectations(timeout: 10.0) { error in + return + } + } + + func testPinnedTopSites() { + let db = BrowserDB(filename: "browser.db", schema: BrowserSchema(), files: files) + let prefs = MockProfilePrefs() + let history = SQLiteHistory(db: db, prefs: prefs) + + history.setTopSitesCacheSize(20) + history.clearTopSitesCache().succeeded() + history.clearHistory().succeeded() + + // add 2 sites to pinned topsite + // get pinned site and make sure it exists in the right order + // remove pinned sites + // make sure pinned sites dont exist + + // create pinned sites. + let site1 = Site(url: "http://s\(1)ite\(1).com/foo", title: "A \(1)") + site1.id = 1 + site1.guid = "abc\(1)def" + addVisitForSite(site1, intoHistory: history, from: .local, atTime: Date.now()) + let site2 = Site(url: "http://s\(2)ite\(2).com/foo", title: "A \(2)") + site2.id = 2 + site2.guid = "abc\(2)def" + addVisitForSite(site2, intoHistory: history, from: .local, atTime: Date.now()) + + + let expectation = self.expectation(description: "First.") + func done() -> Success { + expectation.fulfill() + return succeed() + } + + func addPinnedSites() -> Success { + return history.addPinnedTopSite(site1) >>== { + sleep(1) // Sleep to prevent intermittent issue with sorting on the timestamp + return history.addPinnedTopSite(site2) + } + } + + func checkPinnedSites() -> Success { + return history.getPinnedTopSites() >>== { pinnedSites in + XCTAssertEqual(pinnedSites.count, 2) + XCTAssertEqual(pinnedSites[0]!.url, site2.url) + XCTAssertEqual(pinnedSites[1]!.url, site1.url, "The older pinned site should be last") + return succeed() + } + } + + func removePinnedSites() -> Success { + return history.removeFromPinnedTopSites(site2) >>== { + return history.getPinnedTopSites() >>== { pinnedSites in + XCTAssertEqual(pinnedSites.count, 1, "There should only be one pinned site") + XCTAssertEqual(pinnedSites[0]!.url, site1.url, "Site2 should be the only pin left") + return succeed() + } + } + } + + func dupePinnedSite() -> Success { + return history.addPinnedTopSite(site1) >>== { + return history.getPinnedTopSites() >>== { pinnedSites in + XCTAssertEqual(pinnedSites.count, 1, "There should not be a dupe") + XCTAssertEqual(pinnedSites[0]!.url, site1.url, "Site2 should be the only pin left") + return succeed() + } + } + } + + func removeHistory() -> Success { + return history.clearHistory() >>== { + return history.getPinnedTopSites() >>== { pinnedSites in + XCTAssertEqual(pinnedSites.count, 1, "Pinned sites should exist after a history clear") + return succeed() + } + } + } + + addPinnedSites() + >>> checkPinnedSites + >>> removePinnedSites + >>> dupePinnedSite + >>> removeHistory + >>> done + + waitForExpectations(timeout: 10.0) { error in + return + } + + } +} + +class TestSQLiteHistoryTransactionUpdate: XCTestCase { + func testUpdateInTransaction() { + let files = MockFiles() + let db = BrowserDB(filename: "browser.db", schema: BrowserSchema(), files: files) + let prefs = MockProfilePrefs() + let history = SQLiteHistory(db: db, prefs: prefs) + + history.clearHistory().succeeded() + let site = Site(url: "http://site/foo", title: "AA") + site.guid = "abcdefghiabc" + + history.insertOrUpdatePlace(site.asPlace(), modified: 1234567890).succeeded() + + let ts: MicrosecondTimestamp = baseInstantInMicros + let local = SiteVisit(site: site, date: ts, type: VisitType.link) + XCTAssertTrue(history.addLocalVisit(local).value.isSuccess) + } +} + +class TestSQLiteHistoryFilterSplitting: XCTestCase { + let history: SQLiteHistory = { + let files = MockFiles() + let db = BrowserDB(filename: "browser.db", schema: BrowserSchema(), files: files) + let prefs = MockProfilePrefs() + return SQLiteHistory(db: db, prefs: prefs) + }() + + func testWithSingleWord() { + let (fragment, args) = computeWhereFragmentWithFilter("foo", perWordFragment: "?", perWordArgs: { [$0] }) + XCTAssertEqual(fragment, "?") + XCTAssert(stringArgsEqual(args, ["foo"])) + } + + func testWithIdenticalWords() { + let (fragment, args) = computeWhereFragmentWithFilter("foo fo foo", perWordFragment: "?", perWordArgs: { [$0] }) + XCTAssertEqual(fragment, "?") + XCTAssert(stringArgsEqual(args, ["foo"])) + } + + func testWithDistinctWords() { + let (fragment, args) = computeWhereFragmentWithFilter("foo bar", perWordFragment: "?", perWordArgs: { [$0] }) + XCTAssertEqual(fragment, "? AND ?") + XCTAssert(stringArgsEqual(args, ["foo", "bar"])) + } + + func testWithDistinctWordsAndWhitespace() { + let (fragment, args) = computeWhereFragmentWithFilter(" foo bar ", perWordFragment: "?", perWordArgs: { [$0] }) + XCTAssertEqual(fragment, "? AND ?") + XCTAssert(stringArgsEqual(args, ["foo", "bar"])) + } + + func testWithSubstrings() { + let (fragment, args) = computeWhereFragmentWithFilter("foo bar foobar", perWordFragment: "?", perWordArgs: { [$0] }) + XCTAssertEqual(fragment, "?") + XCTAssert(stringArgsEqual(args, ["foobar"])) + } + + func testWithSubstringsAndIdenticalWords() { + let (fragment, args) = computeWhereFragmentWithFilter("foo bar foobar foobar", perWordFragment: "?", perWordArgs: { [$0] }) + XCTAssertEqual(fragment, "?") + XCTAssert(stringArgsEqual(args, ["foobar"])) + } + + fileprivate func stringArgsEqual(_ one: Args, _ other: Args) -> Bool { + return one.elementsEqual(other, by: { (oneElement: Any?, otherElement: Any?) -> Bool in + return (oneElement as! String) == (otherElement as! String) + }) + } +} + +// MARK - Private Test Helper Methods + +enum VisitOrigin { + case local + case remote +} + +private func populateHistoryForFrecencyCalculations(_ history: SQLiteHistory, siteCount count: Int) { + for i in 0...count { + let site = Site(url: "http://s\(i)ite\(i).com/foo", title: "A \(i)") + site.guid = "abc\(i)def" + + let baseMillis: UInt64 = baseInstantInMillis - 20000 + history.insertOrUpdatePlace(site.asPlace(), modified: baseMillis).succeeded() + + for j in 0...20 { + let visitTime = advanceMicrosecondTimestamp(baseInstantInMicros, by: (1000000 * i) + (1000 * j)) + addVisitForSite(site, intoHistory: history, from: .local, atTime: visitTime) + addVisitForSite(site, intoHistory: history, from: .remote, atTime: visitTime - 100) + } + } +} + +func addVisitForSite(_ site: Site, intoHistory history: SQLiteHistory, from: VisitOrigin, atTime: MicrosecondTimestamp) { + let visit = SiteVisit(site: site, date: atTime, type: VisitType.link) + switch from { + case .local: + history.addLocalVisit(visit).succeeded() + case .remote: + history.storeRemoteVisits([visit], forGUID: site.guid!).succeeded() + } +} diff --git a/mobile/ios/StorageTests/TestSQLiteHistoryRecommendations.swift b/mobile/ios/StorageTests/TestSQLiteHistoryRecommendations.swift new file mode 100644 index 0000000000..706228f151 --- /dev/null +++ b/mobile/ios/StorageTests/TestSQLiteHistoryRecommendations.swift @@ -0,0 +1,333 @@ +/* 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 +@testable import Storage +import Deferred +import SDWebImage + +import XCTest + +private let microsecondsPerMinute: UInt64 = 60_000_000 // 1000 * 1000 * 60 +private let oneHourInMicroseconds: UInt64 = 60 * microsecondsPerMinute +private let oneDayInMicroseconds: UInt64 = 24 * oneHourInMicroseconds + +class TestSQLiteHistoryRecommendations: XCTestCase { + let files = MockFiles() + + var db: BrowserDB! + var prefs: MockProfilePrefs! + var history: SQLiteHistory! + var bookmarks: MergedSQLiteBookmarks! + var metadata: SQLiteMetadata! + + override func setUp() { + super.setUp() + + db = BrowserDB(filename: "browser.db", schema: BrowserSchema(), files: files) + metadata = SQLiteMetadata(db: db) + prefs = MockProfilePrefs() + history = SQLiteHistory(db: db, prefs: prefs) + bookmarks = MergedSQLiteBookmarks(db: db) + } + + override func tearDown() { + // Clear out anything we might have changed on disk + history.clearHistory().succeeded() + db.run("DELETE FROM \(TablePageMetadata)").succeeded() + db.run("DELETE FROM \(TableHighlights)").succeeded() + db.run("DELETE FROM \(TableActivityStreamBlocklist)").succeeded() + + SDWebImageManager.shared().imageCache?.clearDisk() + SDWebImageManager.shared().imageCache?.clearMemory() + + super.tearDown() + } + + /* + * Verify that we return a non-recent history highlight if: + * + * 1. We haven't visited the site in the last 30 minutes + * 2. We've only visited the site less than or equal to 3 times + * 3. The site we visited has a non-empty title + * + */ + func testHistoryHighlights() { + let startTime = Date.nowMicroseconds() + let oneHourAgo = startTime - oneHourInMicroseconds + let fifteenMinutesAgo = startTime - 15 * microsecondsPerMinute + + /* + * Site A: 1 visit, 1 hour ago = highlight + * Site B: 1 visits, 15 minutes ago = non-highlight + * Site C: 3 visits, 1 hour ago = highlight + * Site D: 4 visits, 1 hour ago = non-highlight + */ + let siteA = Site(url: "http://siteA/", title: "A") + let siteB = Site(url: "http://siteB/", title: "B") + let siteC = Site(url: "http://siteC/", title: "C") + let siteD = Site(url: "http://siteD/", title: "D") + + let siteVisitA1 = SiteVisit(site: siteA, date: oneHourAgo, type: .link) + let siteVisitB1 = SiteVisit(site: siteB, date: fifteenMinutesAgo, type: .link) + + let siteVisitC1 = SiteVisit(site: siteC, date: oneHourAgo + 1, type: .link) + let siteVisitC2 = SiteVisit(site: siteC, date: oneHourAgo + 1000, type: .link) + let siteVisitC3 = SiteVisit(site: siteC, date: oneHourAgo + 2000, type: .link) + + let siteVisitD1 = SiteVisit(site: siteD, date: oneHourAgo, type: .link) + let siteVisitD2 = SiteVisit(site: siteD, date: oneHourAgo + 1000, type: .link) + let siteVisitD3 = SiteVisit(site: siteD, date: oneHourAgo + 2000, type: .link) + let siteVisitD4 = SiteVisit(site: siteD, date: oneHourAgo + 3000, type: .link) + + history.clearHistory().succeeded() + history.addLocalVisit(siteVisitA1).succeeded() + + history.addLocalVisit(siteVisitB1).succeeded() + + history.addLocalVisit(siteVisitC1).succeeded() + history.addLocalVisit(siteVisitC2).succeeded() + history.addLocalVisit(siteVisitC3).succeeded() + + history.addLocalVisit(siteVisitD1).succeeded() + history.addLocalVisit(siteVisitD2).succeeded() + history.addLocalVisit(siteVisitD3).succeeded() + history.addLocalVisit(siteVisitD4).succeeded() + history.repopulate(invalidateTopSites: true, invalidateHighlights: true).succeeded() + let highlights = history.getHighlights().value.successValue! + XCTAssertEqual(highlights.count, 2) + XCTAssertEqual(highlights[0]!.title, "A") + XCTAssertEqual(highlights[1]!.title, "C") + } + + /* + * Verify that we do not return a highlight if + * its domain is in the blacklist + * + */ + func testBlacklistHighlights() { + let startTime = Date.nowMicroseconds() + let oneHourAgo = startTime - oneHourInMicroseconds + let fifteenMinutesAgo = startTime - 15 * microsecondsPerMinute + + /* + * Site A: 1 visit, 1 hour ago = highlight that is on the blacklist + * Site B: 1 visits, 15 minutes ago = non-highlight + * Site C: 3 visits, 1 hour ago = highlight that is on the blacklist + * Site D: 4 visits, 1 hour ago = non-highlight + */ + let siteA = Site(url: "http://www.google.com", title: "A") + let siteB = Site(url: "http://siteB/", title: "B") + let siteC = Site(url: "http://www.search.yahoo.com/", title: "C") + let siteD = Site(url: "http://siteD/", title: "D") + + let siteVisitA1 = SiteVisit(site: siteA, date: oneHourAgo, type: .link) + let siteVisitB1 = SiteVisit(site: siteB, date: fifteenMinutesAgo, type: .link) + + let siteVisitC1 = SiteVisit(site: siteC, date: oneHourAgo, type: .link) + let siteVisitC2 = SiteVisit(site: siteC, date: oneHourAgo + 1000, type: .link) + let siteVisitC3 = SiteVisit(site: siteC, date: oneHourAgo + 2000, type: .link) + + let siteVisitD1 = SiteVisit(site: siteD, date: oneHourAgo, type: .link) + let siteVisitD2 = SiteVisit(site: siteD, date: oneHourAgo + 1000, type: .link) + let siteVisitD3 = SiteVisit(site: siteD, date: oneHourAgo + 2000, type: .link) + let siteVisitD4 = SiteVisit(site: siteD, date: oneHourAgo + 3000, type: .link) + + history.clearHistory().succeeded() + history.addLocalVisit(siteVisitA1).succeeded() + + history.addLocalVisit(siteVisitB1).succeeded() + + history.addLocalVisit(siteVisitC1).succeeded() + history.addLocalVisit(siteVisitC2).succeeded() + history.addLocalVisit(siteVisitC3).succeeded() + + history.addLocalVisit(siteVisitD1).succeeded() + history.addLocalVisit(siteVisitD2).succeeded() + history.addLocalVisit(siteVisitD3).succeeded() + history.addLocalVisit(siteVisitD4).succeeded() + + history.repopulate(invalidateTopSites: true, invalidateHighlights: true).succeeded() + let highlights = history.getHighlights().value.successValue! + XCTAssertEqual(highlights.count, 0) + } + + /* + * Verify that we return the most recent highlight per domain + */ + func testMostRecentUniqueDomainReturnedInHighlights() { + let startTime = Date.nowMicroseconds() + let oneHourAgo = startTime - oneHourInMicroseconds + let twoHoursAgo = startTime - 2 * oneHourInMicroseconds + + /* + * Site A: 1 visit, 1 hour ago = highlight + * Site C: 2 visits, 2 hours ago = highlight with the same domain + */ + let siteA = Site(url: "http://www.foo.com/", title: "A") + let siteC = Site(url: "http://m.foo.com/", title: "C") + + let siteVisitA1 = SiteVisit(site: siteA, date: oneHourAgo, type: .link) + + let siteVisitC1 = SiteVisit(site: siteC, date: twoHoursAgo, type: .link) + let siteVisitC2 = SiteVisit(site: siteC, date: twoHoursAgo + 1000, type: .link) + + history.clearHistory().succeeded() + history.addLocalVisit(siteVisitA1).succeeded() + + history.addLocalVisit(siteVisitC1).succeeded() + history.addLocalVisit(siteVisitC2).succeeded() + + history.repopulate(invalidateTopSites: true, invalidateHighlights: true).succeeded() + let highlights = history.getHighlights().value.successValue! + XCTAssertEqual(highlights.count, 1) + XCTAssertEqual(highlights[0]!.title, "A") + } + + func testBookmarkHighlights() { + history.clearHistory().succeeded() + populateForRecommendationCalculations(history, bookmarks: bookmarks, metadata: metadata, historyCount: 10, bookmarkCount: 10) + + let sites = history.getRecentBookmarks(5).value.successValue?.asArray() + XCTAssertEqual(sites!.count, 5, "5 bookmarks should have been fetched") + sites!.forEach { XCTAssertEqual($0.guid, "bookmark-\(sites!.index(of: $0)!)"); XCTAssertEqual($0.metadata?.description, "Test Description") } + } + + func testMetadataReturnedInHighlights() { + let startTime = Date.nowMicroseconds() + let oneHourAgo = startTime - oneHourInMicroseconds + + let siteA = Site(url: "http://siteA.com", title: "Site A") + let siteB = Site(url: "http://siteB.com/", title: "Site B") + let siteC = Site(url: "http://siteC.com/", title: "Site C") + + let siteVisitA1 = SiteVisit(site: siteA, date: oneHourAgo, type: .link) + let siteVisitB1 = SiteVisit(site: siteB, date: oneHourAgo + 1000, type: .link) + + let siteVisitC1 = SiteVisit(site: siteC, date: oneHourAgo, type: .link) + let siteVisitC2 = SiteVisit(site: siteC, date: oneHourAgo + 1000, type: .link) + let siteVisitC3 = SiteVisit(site: siteC, date: oneHourAgo + 2000, type: .link) + + history.clearHistory().succeeded() + history.addLocalVisit(siteVisitA1).succeeded() + + history.addLocalVisit(siteVisitB1).succeeded() + + history.addLocalVisit(siteVisitC1).succeeded() + history.addLocalVisit(siteVisitC2).succeeded() + history.addLocalVisit(siteVisitC3).succeeded() + + // add metadata for 2 of the sites + let metadata = SQLiteMetadata(db: db) + let pageA = PageMetadata(id: nil, siteURL: siteA.url, mediaURL: "http://image.com", + title: siteA.title, description: "Test Description", type: nil, providerName: nil, mediaDataURI: nil, cacheImages: false) + metadata.storeMetadata(pageA, forPageURL: siteA.url.asURL!, expireAt: Date.now() + 3000).succeeded() + let pageB = PageMetadata(id: nil, siteURL: siteB.url, mediaURL: "http://image.com", + title: siteB.title, description: "Test Description", type: nil, providerName: nil, mediaDataURI: nil, cacheImages: false) + metadata.storeMetadata(pageB, forPageURL: siteB.url.asURL!, expireAt: Date.now() + 3000).succeeded() + let pageC = PageMetadata(id: nil, siteURL: siteC.url, mediaURL: "http://image.com", + title: siteC.title, description: "Test Description", type: nil, providerName: nil, mediaDataURI: nil, cacheImages: false) + metadata.storeMetadata(pageC, forPageURL: siteC.url.asURL!, expireAt: Date.now() + 3000).succeeded() + + history.repopulate(invalidateTopSites: true, invalidateHighlights: true).succeeded() + let highlights = history.getHighlights().value.successValue! + XCTAssertEqual(highlights.count, 3) + + for highlight in highlights { + XCTAssertNotNil(highlight?.metadata) + XCTAssertNotNil(highlight?.metadata?.mediaURL) + } + } + + func testRemoveHighlightForURL() { + let startTime = Date.nowMicroseconds() + let oneHourAgo = startTime - oneHourInMicroseconds + + let siteA = Site(url: "http://siteA/", title: "A") + let siteVisitA1 = SiteVisit(site: siteA, date: oneHourAgo, type: .link) + + history.clearHistory().succeeded() + history.addLocalVisit(siteVisitA1).succeeded() + + history.repopulate(invalidateTopSites: true, invalidateHighlights: true).succeeded() + var highlights = history.getHighlights().value.successValue! + XCTAssertEqual(highlights.count, 1) + XCTAssertEqual(highlights[0]!.title, "A") + + history.removeHighlightForURL(siteA.url).succeeded() + history.repopulate(invalidateTopSites: true, invalidateHighlights: true).succeeded() + highlights = history.getHighlights().value.successValue! + XCTAssertEqual(highlights.count, 0) + } + + func testClearHighlightsCache() { + let startTime = Date.nowMicroseconds() + let oneHourAgo = startTime - oneHourInMicroseconds + + let siteA = Site(url: "http://siteA/", title: "A") + let siteVisitA1 = SiteVisit(site: siteA, date: oneHourAgo, type: .link) + + history.clearHistory().succeeded() + history.addLocalVisit(siteVisitA1).succeeded() + + history.repopulate(invalidateTopSites: true, invalidateHighlights: true).succeeded() + let highlights = history.getHighlights().value.successValue! + XCTAssertEqual(highlights.count, 1) + XCTAssertEqual(highlights[0]!.title, "A") + } +} + +class TestSQLiteHistoryRecommendationsPerf: XCTestCase { + func testRecommendationPref() { + let files = MockFiles() + let db = BrowserDB(filename: "browser.db", schema: BrowserSchema(), files: files) + let metadata = SQLiteMetadata(db: db) + let prefs = MockProfilePrefs() + let history = SQLiteHistory(db: db, prefs: prefs) + let bookmarks = MergedSQLiteBookmarks(db: db) + + let count = 500 + + history.clearHistory().succeeded() + populateForRecommendationCalculations(history, bookmarks: bookmarks, metadata: metadata, historyCount: count, bookmarkCount: count) + self.measureMetrics([XCTPerformanceMetric_WallClockTime], automaticallyStartMeasuring: true) { + for _ in 0...5 { + history.repopulate(invalidateTopSites: true, invalidateHighlights: true).succeeded() + } + self.stopMeasuring() + } + } +} + +private func populateForRecommendationCalculations(_ history: SQLiteHistory, bookmarks: MergedSQLiteBookmarks, metadata: SQLiteMetadata, historyCount: Int, bookmarkCount: Int) { + let baseMillis: UInt64 = baseInstantInMillis - 20000 + + for i in 0.. Deferred>> { + let sql = "SELECT * FROM \(TablePageMetadata)" + return db.runQuery(sql, args: nil, factory: pageMetadataFactory) +} + +private func removeAllMetadata(_ db: BrowserDB) -> Success { + return db.run("DELETE FROM \(TablePageMetadata)") +} + +private func pageMetadataFactory(_ row: SDRow) -> PageMetadata { + let id = row["id"] as! Int + let siteURL = row["site_url"] as! String + let mediaURL = row["media_url"] as? String + let title = row["title"] as? String + let description = row["description"] as? String + let type = row["type"] as? String + let providerName = row["provider_name"] as? String + return PageMetadata(id: id, siteURL: siteURL, mediaURL: mediaURL, title: title, + description: description, type: type, providerName: providerName, mediaDataURI: nil, cacheImages: false) +} diff --git a/mobile/ios/StorageTests/TestSQLiteRemoteClientsAndTabs.swift b/mobile/ios/StorageTests/TestSQLiteRemoteClientsAndTabs.swift new file mode 100644 index 0000000000..09f2d106f9 --- /dev/null +++ b/mobile/ios/StorageTests/TestSQLiteRemoteClientsAndTabs.swift @@ -0,0 +1,303 @@ +/* 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 +@testable import Storage +import Deferred + +import XCTest + +open class MockRemoteClientsAndTabs: RemoteClientsAndTabs { + open let clientsAndTabs: [ClientAndTabs] + + public init() { + let now = Date.now() + let client1GUID = Bytes.generateGUID() + let client2GUID = Bytes.generateGUID() + let u11 = URL(string: "http://test.com/test1")! + let tab11 = RemoteTab(clientGUID: client1GUID, URL: u11, title: "Test 1", history: [ ], lastUsed: (now - OneMinuteInMilliseconds), icon: nil) + + let u12 = URL(string: "http://test.com/test2")! + let tab12 = RemoteTab(clientGUID: client1GUID, URL: u12, title: "Test 2", history: [], lastUsed: (now - OneHourInMilliseconds), icon: nil) + + let tab21 = RemoteTab(clientGUID: client2GUID, URL: u11, title: "Test 1", history: [], lastUsed: (now - OneDayInMilliseconds), icon: nil) + + let u22 = URL(string: "http://different.com/test2")! + let tab22 = RemoteTab(clientGUID: client2GUID, URL: u22, title: "Different Test 2", history: [], lastUsed: now + OneHourInMilliseconds, icon: nil) + + let client1 = RemoteClient(guid: client1GUID, name: "Test client 1", modified: (now - OneMinuteInMilliseconds), type: "mobile", formfactor: "largetablet", os: "iOS", version: "55.0.1", fxaDeviceId: "fxa1") + let client2 = RemoteClient(guid: client2GUID, name: "Test client 2", modified: (now - OneHourInMilliseconds), type: "desktop", formfactor: "laptop", os: "Darwin", version: "55.0.1", fxaDeviceId: "fxa2") + + let localClient = RemoteClient(guid: nil, name: "Test local client", modified: (now - OneMinuteInMilliseconds), type: "mobile", formfactor: "largetablet", os: "iOS", version: "55.0.1", fxaDeviceId: "fxa3") + let localUrl1 = URL(string: "http://test.com/testlocal1")! + let localTab1 = RemoteTab(clientGUID: nil, URL: localUrl1, title: "Local test 1", history: [], lastUsed: (now - OneMinuteInMilliseconds), icon: nil) + let localUrl2 = URL(string: "http://test.com/testlocal2")! + let localTab2 = RemoteTab(clientGUID: nil, URL: localUrl2, title: "Local test 2", history: [], lastUsed: (now - OneMinuteInMilliseconds), icon: nil) + + // Tabs are ordered most-recent-first. + self.clientsAndTabs = [ClientAndTabs(client: client1, tabs: [tab11, tab12]), + ClientAndTabs(client: client2, tabs: [tab22, tab21]), + ClientAndTabs(client: localClient, tabs: [localTab1, localTab2])] + } + + open func onRemovedAccount() -> Success { + return succeed() + } + + open func wipeClients() -> Success { + return succeed() + } + + open func wipeRemoteTabs() -> Deferred> { + return succeed() + } + + open func wipeTabs() -> Success { + return succeed() + } + + open func insertOrUpdateClients(_ clients: [RemoteClient]) -> Deferred> { + return deferMaybe(0) + } + + open func insertOrUpdateClient(_ client: RemoteClient) -> Deferred> { + return deferMaybe(0) + } + + open func insertOrUpdateTabs(_ tabs: [RemoteTab]) -> Deferred> { + return insertOrUpdateTabsForClientGUID(nil, tabs: [RemoteTab]()) + } + + open func insertOrUpdateTabsForClientGUID(_ clientGUID: String?, tabs: [RemoteTab]) -> Deferred> { + return deferMaybe(-1) + } + + open func getClientsAndTabs() -> Deferred> { + return deferMaybe(self.clientsAndTabs) + } + + open func getClients() -> Deferred> { + return deferMaybe(self.clientsAndTabs.map { $0.client }) + } + + public func getClient(guid: GUID) -> Deferred> { + return deferMaybe(self.clientsAndTabs.find { clientAndTabs in + return clientAndTabs.client.guid == guid + }?.client) + } + + public func getClient(fxaDeviceId: GUID) -> Deferred> { + return deferMaybe(self.clientsAndTabs.find { clientAndTabs in + return clientAndTabs.client.fxaDeviceId == fxaDeviceId + }?.client) + } + + open func getClientWithId(_ clientID: GUID) -> Deferred> { + return getClient(guid: clientID) + } + + open func getClientGUIDs() -> Deferred>> { + return deferMaybe(Set(optFilter(self.clientsAndTabs.map { $0.client.guid }))) + } + + open func getTabsForClientWithGUID(_ guid: GUID?) -> Deferred> { + return deferMaybe(optFilter(self.clientsAndTabs.map { $0.client.guid == guid ? $0.tabs : nil })[0]) + } + + open func deleteClient(guid: GUID) -> Success { return succeed() } + + open func deleteCommands() -> Success { return succeed() } + open func deleteCommands(_ clientGUID: GUID) -> Success { return succeed() } + + open func getCommands() -> Deferred> { return deferMaybe([GUID: [SyncCommand]]()) } + + open func insertCommand(_ command: SyncCommand, forClients clients: [RemoteClient]) -> Deferred> { return deferMaybe(0) } + open func insertCommands(_ commands: [SyncCommand], forClients clients: [RemoteClient]) -> Deferred> { return deferMaybe(0) } +} + +func removeLocalClient(_ a: ClientAndTabs) -> Bool { + return a.client.guid != nil +} + +func byGUID(_ a: ClientAndTabs, b: ClientAndTabs) -> Bool { + guard let aGUID = a.client.guid, let bGUID = b.client.guid else { + return false + } + return aGUID < bGUID +} + +func byURL(_ a: RemoteTab, b: RemoteTab) -> Bool { + return a.URL.absoluteString < b.URL.absoluteString +} + +class SQLRemoteClientsAndTabsTests: XCTestCase { + var clientsAndTabs: SQLiteRemoteClientsAndTabs! + + lazy var clients: [ClientAndTabs] = MockRemoteClientsAndTabs().clientsAndTabs + + override func setUp() { + let files = MockFiles() + do { + try files.remove("browser.db") + } catch _ { + } + clientsAndTabs = SQLiteRemoteClientsAndTabs(db: BrowserDB(filename: "browser.db", schema: BrowserSchema(), files: files)) + } + + func testInsertGetClear() { + // Insert some test data. + var remoteDevicesToInsert: [RemoteDevice] = [] + // Filter the local client from mock test data. + let remoteClients = clients.filter(removeLocalClient) + for c in remoteClients { + let e = self.expectation(description: "Insert.") + clientsAndTabs.insertOrUpdateClient(c.client).upon { + XCTAssertTrue($0.isSuccess) + e.fulfill() + } + let remoteDevice = RemoteDevice(id: c.client.fxaDeviceId!, name: "FxA Device", type: "desktop", isCurrentDevice: false, lastAccessTime: 12345678) + remoteDevicesToInsert.append(remoteDevice) + clientsAndTabs.insertOrUpdateTabsForClientGUID(c.client.guid, tabs: c.tabs).succeeded() + } + _ = clientsAndTabs.replaceRemoteDevices(remoteDevicesToInsert).succeeded() + + let f = self.expectation(description: "Get after insert.") + clientsAndTabs.getClientsAndTabs().upon { + if let got = $0.successValue { + let expected = remoteClients.sorted(by: byGUID) + let actual = got.sorted(by: byGUID) + + // This comparison will fail if the order of the tabs changes. We sort the result + // as part of the DB query, so it's not actively sorted in Swift. + XCTAssertEqual(expected, actual) + } else { + XCTFail("Expected clients!") + } + f.fulfill() + } + + // Update the test data with a client with new tabs, and one with no tabs. + let client0NewTabs = clients[1].tabs.map { $0.withClientGUID(self.clients[0].client.guid) } + let client1NewTabs: [RemoteTab] = [] + let expected = [ + ClientAndTabs(client: clients[0].client, tabs: client0NewTabs), + ClientAndTabs(client: clients[1].client, tabs: client1NewTabs), + ].sorted(by: byGUID) + + func doUpdate(_ guid: String?, tabs: [RemoteTab]) { + let g0 = self.expectation(description: "Update client: \(guid ?? "nil").") + clientsAndTabs.insertOrUpdateTabsForClientGUID(guid, tabs: tabs).upon { + if let rowID = $0.successValue { + XCTAssertTrue(rowID > -1) + } else { + XCTFail("Didn't successfully update.") + } + g0.fulfill() + } + } + + doUpdate(clients[0].client.guid, tabs: client0NewTabs) + doUpdate(clients[1].client.guid, tabs: client1NewTabs) + // Also update the local tabs list. It should still not appear in the expected tabs below. + doUpdate(clients[2].client.guid, tabs: client1NewTabs) + + let h = self.expectation(description: "Get after update.") + clientsAndTabs.getClientsAndTabs().upon { + if let clients = $0.successValue { + XCTAssertEqual(expected, clients.sorted(by: byGUID)) + } else { + XCTFail("Expected clients!") + } + h.fulfill() + } + + // Now clear everything, and verify we have no clients or tabs whatsoever. + let i = self.expectation(description: "Clear.") + clientsAndTabs.clear().upon { + XCTAssertTrue($0.isSuccess) + i.fulfill() + } + + let j = self.expectation(description: "Get after clear.") + clientsAndTabs.getClientsAndTabs().upon { + if let clients = $0.successValue { + XCTAssertEqual(0, clients.count) + } else { + XCTFail("Expected clients!") + } + j.fulfill() + } + + self.waitForExpectations(timeout: 10, handler: nil) + } + + func testGetTabsForClient() { + for c in clients { + let e = self.expectation(description: "Insert.") + clientsAndTabs.insertOrUpdateClient(c.client).upon { + XCTAssertTrue($0.isSuccess) + e.fulfill() + } + clientsAndTabs.insertOrUpdateTabsForClientGUID(c.client.guid, tabs: c.tabs).succeeded() + } + + let e = self.expectation(description: "Get after insert.") + let ct = clients[0] + clientsAndTabs.getTabsForClientWithGUID(ct.client.guid).upon { + if let got = $0.successValue { + // This comparison will fail if the order of the tabs changes. We sort the result + // as part of the DB query, so it's not actively sorted in Swift. + XCTAssertEqual(ct.tabs.count, got.count) + XCTAssertEqual(ct.tabs.sorted(by: byURL), got.sorted(by: byURL)) + } else { + XCTFail("Expected tabs!") + } + e.fulfill() + } + + let f = self.expectation(description: "Get after insert.") + let localClient = clients[0] + clientsAndTabs.getTabsForClientWithGUID(localClient.client.guid).upon { + if let got = $0.successValue { + // This comparison will fail if the order of the tabs changes. We sort the result + // as part of the DB query, so it's not actively sorted in Swift. + XCTAssertEqual(localClient.tabs.count, got.count) + XCTAssertEqual(localClient.tabs.sorted(by: byURL), got.sorted(by: byURL)) + } else { + XCTFail("Expected tabs!") + } + f.fulfill() + } + + self.waitForExpectations(timeout: 10, handler: nil) + } + + func remoteDeviceFactory(_ row: SDRow) -> RemoteDevice { + return RemoteDevice( + id: row["guid"] as? String, + name: row["name"] as! String, + type: row["type"] as? String, + isCurrentDevice: row["is_current_device"] as! Int > 0, + lastAccessTime: row["last_access_time"] as? Timestamp) + } + + func testReplaceRemoteDevices() { + let device1 = RemoteDevice(id: "fx1", name: "Device 1", type: "mobile", isCurrentDevice: false, lastAccessTime: 12345678) + let device2 = RemoteDevice(id: "fx2", name: "Device 2 (local)", type: "desktop", isCurrentDevice: true, lastAccessTime: nil) + let device3 = RemoteDevice(id: nil, name: "Device 3 (fauly)", type: "desktop", isCurrentDevice: false, lastAccessTime: 12345678) + let device4 = RemoteDevice(id: "fx4", name: "Device 4 (fauly)", type: nil, isCurrentDevice: false, lastAccessTime: 12345678) + + _ = clientsAndTabs.replaceRemoteDevices([device1, device2, device3, device4]).succeeded() + + let devices = clientsAndTabs.db.runQuery("SELECT * FROM \(TableRemoteDevices)", args: nil, factory: remoteDeviceFactory).value.successValue!.asArray() + XCTAssertEqual(devices.count, 1) // Fauly devices + local device were not inserted. + + let device5 = RemoteDevice(id: "fx5", name: "Device 5", type: "mobile", isCurrentDevice: false, lastAccessTime: 12345678) + _ = clientsAndTabs.replaceRemoteDevices([device5]).succeeded() + + let newDevices = clientsAndTabs.db.runQuery("SELECT * FROM \(TableRemoteDevices)", args: nil, factory: remoteDeviceFactory).value.successValue!.asArray() + XCTAssertEqual(newDevices.count, 1) // replaceRemoteDevices wipes the whole list before inserting. + } +} diff --git a/mobile/ios/StorageTests/TestSwiftData.swift b/mobile/ios/StorageTests/TestSwiftData.swift new file mode 100644 index 0000000000..8db05db0cf --- /dev/null +++ b/mobile/ios/StorageTests/TestSwiftData.swift @@ -0,0 +1,220 @@ +/* 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 +@testable import Storage + +import XCTest + +// TODO: rewrite this test to not use BrowserSchema. It used to use HistoryTable… +class TestSwiftData: XCTestCase { + var swiftData: SwiftData? + var urlCounter = 1 + var testDB: String! + + override func setUp() { + let files = MockFiles() + do { + try files.remove("testSwiftData.db") + } catch _ { + } + testDB = (try! (files.getAndEnsureDirectory() as NSString)).appendingPathComponent("testSwiftData.db") + swiftData = SwiftData(filename: testDB, schema: BrowserSchema(), files: files) + let table = BrowserSchema() + + // Ensure static flags match expected values. + XCTAssert(SwiftData.ReuseConnections, "Reusing database connections") + XCTAssert(SwiftData.EnableWAL, "WAL enabled") + + XCTAssertNil(addSite(table, url: "http://url0", title: "title0"), "Added url0.") + } + + override func tearDown() { + // Restore static flags to their default values. + SwiftData.ReuseConnections = true + SwiftData.EnableWAL = true + } + + /* + // These two tests broke after pull #427. + func testNoWALOrConnectionReuse() { + SwiftData.EnableWAL = false + SwiftData.ReuseConnections = false + var error = writeDuringRead() + XCTAssertEqual(error!.code, 5, "Got 'database is locked' error") + } + + func testNoConnectionReuse() { + SwiftData.EnableWAL = true + SwiftData.ReuseConnections = false + var error = writeDuringRead() + XCTAssertNotNil(error, "Expected error during write.") + XCTAssertEqual(error?.code ?? 0, 8, "Got 'attempt to write to read-only database' error") + } + */ + + func testNoWAL() { + SwiftData.EnableWAL = false + SwiftData.ReuseConnections = true + let error = writeDuringRead() + XCTAssertNil(error, "Insertion succeeded") + } + + func testDefaultSettings() { + SwiftData.EnableWAL = true + SwiftData.ReuseConnections = true + let error = writeDuringRead() + XCTAssertNil(error, "Insertion succeeded") + } + + func testBusyTimeout() { + SwiftData.EnableWAL = false + SwiftData.ReuseConnections = false + let error = writeDuringRead(closeTimeout: 1) + XCTAssertNil(error, "Insertion succeeded") + } + + func testFilledCursor() { + SwiftData.ReuseConnections = false + SwiftData.EnableWAL = false + XCTAssertNil(writeDuringRead(true), "Insertion succeeded") + } + + fileprivate func writeDuringRead(_ safeQuery: Bool = false, closeTimeout: UInt64? = nil) -> MaybeErrorType? { + + // Query the database and hold the cursor. + var c: Cursor! + let result = swiftData!.withConnection(SwiftData.Flags.readOnly) { db -> Void in + if safeQuery { + c = db.executeQuery("SELECT * FROM history", factory: { $0 }) + } else { + c = db.executeQueryUnsafe("SELECT * FROM history", factory: { $0 }, withArgs: nil) + } + return () + } + + XCTAssertNil(result.value.failureValue, "Queried database") + + // If we have a live cursor, this will step to the first result. + // Stepping through a prepared statement without resetting it will lock the connection. + let _ = c[0] + + // Close the cursor after a delay if there's a close timeout set. + if let closeTimeout = closeTimeout { + let queue = DispatchQueue(label: "cursor timeout queue", attributes: []) + queue.asyncAfter(deadline: DispatchTime.now() + Double(Int64(closeTimeout * NSEC_PER_SEC)) / Double(NSEC_PER_SEC)) { + c.close() + } + } + + defer { urlCounter += 1 } + return addSite(BrowserSchema(), url: "http://url/\(urlCounter)", title: "title\(urlCounter)") + } + + fileprivate func addSite(_ table: BrowserSchema, url: String, title: String) -> MaybeErrorType? { + let result = swiftData!.withConnection(SwiftData.Flags.readWrite) { connection -> Void in + let args: Args = [Bytes.generateGUID(), url, title] + try connection.executeChange("INSERT INTO history (guid, url, title, is_deleted, should_upload) VALUES (?, ?, ?, 0, 0)", withArgs: args) + } + + return result.value.failureValue + } + + func testEncrypt() { + // XXX: Something is holding an open connection to the normal database, making it impossible + // to change its encryption. This kills it so that we can move on. + let files = MockFiles() + do { + try files.remove("testSwiftData.db") + } catch _ { + } + let path = testDB + func verifyData(_ swiftData: SwiftData) -> MaybeErrorType? { + let resultDeferred = swiftData.withConnection(SwiftData.Flags.readOnly) { db -> Void in + return () + } + return resultDeferred.value.failureValue + } + + XCTAssertNotNil(SwiftData(filename: path!, schema: BrowserSchema(), files: files), "Connected to unencrypted database") + + // Encrypt the database. + XCTAssertNil(verifyData(SwiftData(filename: path!, key: "Secret", schema: BrowserSchema(), files: files)), "Encrypted database") + + // Now change the encryption key. + XCTAssertNil(verifyData(SwiftData(filename: path!, key: "Secret2", prevKey: "Secret", schema: BrowserSchema(), files: files)), "Re-encrypted database") + + // Changing the encryption without the prevKey should fail. + XCTAssertNotNil(verifyData(SwiftData(filename: path!, schema: BrowserSchema(), files: files)), "Failed decrypting database") + + // Now remove the encryption key. + XCTAssertNil(verifyData(SwiftData(filename: path!, prevKey: "Secret2", schema: BrowserSchema(), files: files)), "Decrypted database") + } + + func testNulls() { + guard let db = swiftData else { + XCTFail("DB not open") + return + } + db.withConnection(SwiftData.Flags.readWriteCreate) { db in + try! db.executeChange("CREATE TABLE foo ( bar TEXT, baz INTEGER )") + try! db.executeChange("INSERT INTO foo VALUES (NULL, 1), ('here', 2)") + let shouldBeString = db.executeQuery("SELECT bar FROM foo WHERE baz = 2", factory: { (row) in row["bar"] }).asArray()[0] + guard let s = shouldBeString as? String else { + XCTFail("Couldn't cast.") + return + } + XCTAssertEqual(s, "here") + + let shouldBeNull = db.executeQuery("SELECT bar FROM foo WHERE baz = 1", factory: { (row) in row["bar"] }).asArray()[0] + XCTAssertNil(shouldBeNull as? String) + XCTAssertNil(shouldBeNull) + }.succeeded() + } + + func testArrayCursor() { + let data = ["One", "Two", "Three"] + let t = ArrayCursor(data: data) + + // Test subscript access + XCTAssertNil(t[-1], "Subscript -1 returns nil") + XCTAssertEqual(t[0]!, "One", "Subscript zero returns the correct data") + XCTAssertEqual(t[1]!, "Two", "Subscript one returns the correct data") + XCTAssertEqual(t[2]!, "Three", "Subscript two returns the correct data") + XCTAssertNil(t[3], "Subscript three returns nil") + + // Test status data with default initializer + XCTAssertEqual(t.status, CursorStatus.success, "Cursor as correct status") + XCTAssertEqual(t.statusMessage, "Success", "Cursor as correct status message") + XCTAssertEqual(t.count, 3, "Cursor as correct size") + + // Test generator access + var i = 0 + for s in t { + XCTAssertEqual(s!, data[i], "Subscript zero returns the correct data") + i += 1 + } + + // Test creating a failed cursor + let t2 = ArrayCursor(data: data, status: CursorStatus.failure, statusMessage: "Custom status message") + XCTAssertEqual(t2.status, CursorStatus.failure, "Cursor as correct status") + XCTAssertEqual(t2.statusMessage, "Custom status message", "Cursor as correct status message") + XCTAssertEqual(t2.count, 0, "Cursor as correct size") + + // Test subscript access return nil for a failed cursor + XCTAssertNil(t2[0], "Subscript zero returns nil if failure") + XCTAssertNil(t2[1], "Subscript one returns nil if failure") + XCTAssertNil(t2[2], "Subscript two returns nil if failure") + XCTAssertNil(t2[3], "Subscript three returns nil if failure") + + // Test that generator doesn't work with failed cursors + var ran = false + for s in t2 { + print("Got \(s ?? "nil")", terminator: "\n") + ran = true + } + XCTAssertFalse(ran, "for...in didn't run for failed cursor") + } +} diff --git a/mobile/ios/StorageTests/fixtures/v33.db b/mobile/ios/StorageTests/fixtures/v33.db new file mode 100644 index 0000000000..9707dae26e Binary files /dev/null and b/mobile/ios/StorageTests/fixtures/v33.db differ diff --git a/mobile/ios/StorageTests/testcert1.pem b/mobile/ios/StorageTests/testcert1.pem new file mode 100644 index 0000000000..6e47859ee9 Binary files /dev/null and b/mobile/ios/StorageTests/testcert1.pem differ diff --git a/mobile/ios/StorageTests/testcert2.pem b/mobile/ios/StorageTests/testcert2.pem new file mode 100644 index 0000000000..fe6dcf5387 Binary files /dev/null and b/mobile/ios/StorageTests/testcert2.pem differ diff --git a/mobile/ios/Sync/BatchingClient.swift b/mobile/ios/Sync/BatchingClient.swift new file mode 100644 index 0000000000..3901829130 --- /dev/null +++ b/mobile/ios/Sync/BatchingClient.swift @@ -0,0 +1,294 @@ +/* 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 Alamofire +import Shared +import XCGLogger +import Deferred + +open class SerializeRecordFailure: MaybeErrorType, SyncPingFailureFormattable { + open let record: Record + + open var failureReasonName: SyncPingFailureReasonName { + return .otherError + } + + open var description: String { + return "Failed to serialize record: \(record)" + } + + public init(record: Record) { + self.record = record + } +} + +private let log = Logger.syncLogger + +private typealias UploadRecord = (guid: GUID, payload: String, sizeBytes: Int) +public typealias DeferredResponse = Deferred>> + +typealias BatchUploadFunction = (_ lines: [String], _ ifUnmodifiedSince: Timestamp?, _ queryParams: [URLQueryItem]?) -> Deferred>> + +private let commitParam = URLQueryItem(name: "commit", value: "true") + +private enum AccumulateRecordError: MaybeErrorType { + var description: String { + switch self { + case .full: + return "Batch or payload is full." + case .unknown: + return "Unknown errored while trying to accumulate records in batch" + } + } + + case full(uploadOp: DeferredResponse) + case unknown +} + +open class TooManyRecordsError: MaybeErrorType, SyncPingFailureFormattable { + open var description: String { + return "Trying to send too many records in a single batch." + } + open var failureReasonName: SyncPingFailureReasonName { + return .otherError + } +} + +open class RecordsFailedToUpload: MaybeErrorType, SyncPingFailureFormattable { + open var description: String { + return "Some records failed to upload" + } + open var failureReasonName: SyncPingFailureReasonName { + return .otherError + } +} + +open class Sync15BatchClient { + fileprivate(set) var ifUnmodifiedSince: Timestamp? + + fileprivate let config: InfoConfiguration + fileprivate let uploader: BatchUploadFunction + fileprivate let serializeRecord: (Record) -> String? + + fileprivate var batchToken: BatchToken? + + // Keep track of the limits of a single batch + fileprivate var totalBytes: ByteCount = 0 + fileprivate var totalRecords: Int = 0 + + // Keep track of the limits of a single POST + fileprivate var postBytes: ByteCount = 0 + fileprivate var postRecords: Int = 0 + + fileprivate var records = [UploadRecord]() + + fileprivate var onCollectionUploaded: (POSTResult, Timestamp?) -> DeferredTimestamp + + fileprivate func batchQueryParamWithValue(_ value: String) -> URLQueryItem { + return URLQueryItem(name: "batch", value: value) + } + + init(config: InfoConfiguration, ifUnmodifiedSince: Timestamp? = nil, serializeRecord: @escaping (Record) -> String?, + uploader: @escaping BatchUploadFunction, onCollectionUploaded: @escaping (POSTResult, Timestamp?) -> DeferredTimestamp) { + self.config = config + self.ifUnmodifiedSince = ifUnmodifiedSince + self.uploader = uploader + self.serializeRecord = serializeRecord + + self.onCollectionUploaded = onCollectionUploaded + } + + open func endBatch() -> Success { + guard !records.isEmpty else { + return succeed() + } + + if let token = self.batchToken { + return commitBatch(token) >>> succeed + } + + let lines = self.freezePost() + return self.uploader(lines, self.ifUnmodifiedSince, nil) + >>== effect(moveForward) + >>> succeed + } + + // If in batch mode, will discard the batch if any record fails + open func endSingleBatch() -> Deferred> { + return self.start() >>== { response in + let succeeded = response.value.success + guard let token = self.batchToken else { + return deferMaybe((succeeded: succeeded, lastModified: response.metadata.lastModifiedMilliseconds)) + } + guard succeeded.count == self.totalRecords else { + return deferMaybe(RecordsFailedToUpload()) + } + return self.commitBatch(token) >>== { commitResp in + return deferMaybe((succeeded: succeeded, lastModified: commitResp.metadata.lastModifiedMilliseconds)) + } + } + } + + open func addRecords(_ records: [Record], singleBatch: Bool = false) -> Success { + guard !records.isEmpty else { + return succeed() + } + + // Eagerly serializer the record prior to processing them so we can catch any issues + // with record sizes before we start uploading to the server. + let serializeThunks = records.map { record in + return { self.serialize(record) } + } + + return accumulate(serializeThunks) >>== { + let iter = $0.makeIterator() + if singleBatch { + return self.addRecordsInSingleBatch(iter) + } else { + return self.addRecords(iter) + } + } + } + + fileprivate func addRecords(_ generator: IndexingIterator<[UploadRecord]>) -> Success { + var mutGenerator = generator + while let record = mutGenerator.next() { + return accumulateOrUpload(record) >>> { self.addRecords(mutGenerator) } + } + return succeed() + } + + fileprivate func addRecordsInSingleBatch(_ generator: IndexingIterator<[UploadRecord]>) -> Success { + var mutGenerator = generator + while let record = mutGenerator.next() { + guard self.addToPost(record) else { + return deferMaybe(TooManyRecordsError()) + } + } + return succeed() + } + + fileprivate func accumulateOrUpload(_ record: UploadRecord) -> Success { + return accumulateRecord(record).bind { result in + // Try to add the record to our buffer + guard let e = result.failureValue as? AccumulateRecordError else { + return succeed() + } + + switch e { + case .full(let uploadOp): + return uploadOp >>> { self.accumulateOrUpload(record) } + default: + return deferMaybe(e) + } + } + } + + fileprivate func accumulateRecord(_ record: UploadRecord) -> Success { + guard let token = self.batchToken else { + guard addToPost(record) else { + return deferMaybe(AccumulateRecordError.full(uploadOp: self.start())) + } + return succeed() + } + + guard fitsInBatch(record) else { + return deferMaybe(AccumulateRecordError.full(uploadOp: self.commitBatch(token))) + } + + guard addToPost(record) else { + return deferMaybe(AccumulateRecordError.full(uploadOp: self.postInBatch(token))) + } + + addToBatch(record) + return succeed() + } + + fileprivate func serialize(_ record: Record) -> Deferred> { + guard let line = self.serializeRecord(record) else { + return deferMaybe(SerializeRecordFailure(record: record)) + } + + let lineSize = line.utf8.count + guard lineSize < Sync15StorageClient.maxRecordSizeBytes else { + return deferMaybe(RecordTooLargeError(size: lineSize, guid: record.id)) + } + + return deferMaybe((record.id, line, lineSize)) + } + + fileprivate func addToPost(_ record: UploadRecord) -> Bool { + guard postRecords + 1 <= config.maxPostRecords && postBytes + record.sizeBytes <= config.maxPostBytes else { + return false + } + postRecords += 1 + postBytes += record.sizeBytes + records.append(record) + return true + } + + fileprivate func fitsInBatch(_ record: UploadRecord) -> Bool { + return totalRecords + 1 <= config.maxTotalRecords && totalBytes + record.sizeBytes <= config.maxTotalBytes + } + + fileprivate func addToBatch(_ record: UploadRecord) { + totalRecords += 1 + totalBytes += record.sizeBytes + } + + fileprivate func postInBatch(_ token: BatchToken) -> DeferredResponse { + // Push up the current payload to the server and reset + let lines = self.freezePost() + return uploader(lines, self.ifUnmodifiedSince, [batchQueryParamWithValue(token)]) + } + + fileprivate func commitBatch(_ token: BatchToken) -> DeferredResponse { + resetBatch() + let lines = self.freezePost() + let queryParams = [batchQueryParamWithValue(token), commitParam] + return uploader(lines, self.ifUnmodifiedSince, queryParams) + >>== effect(moveForward) + } + + fileprivate func start() -> DeferredResponse { + let postRecordCount = self.postRecords + let postBytesCount = self.postBytes + let lines = freezePost() + return self.uploader(lines, self.ifUnmodifiedSince, [batchQueryParamWithValue("true")]) + >>== effect(moveForward) + >>== { response in + if let token = response.value.batchToken { + self.batchToken = token + + // Now that we've started a batch, make sure to set the counters for the batch to include + // the records we just sent as part of the start call. + self.totalRecords = postRecordCount + self.totalBytes = postBytesCount + } + + return deferMaybe(response) + } + } + + fileprivate func moveForward(_ response: StorageResponse) { + let lastModified = response.metadata.lastModifiedMilliseconds + self.ifUnmodifiedSince = lastModified + _ = self.onCollectionUploaded(response.value, lastModified) + } + + fileprivate func resetBatch() { + totalBytes = 0 + totalRecords = 0 + self.batchToken = nil + } + + fileprivate func freezePost() -> [String] { + let lines = records.map { $0.payload } + self.records = [] + self.postBytes = 0 + self.postRecords = 0 + return lines + } +} diff --git a/mobile/ios/Sync/BookmarkPayload.swift b/mobile/ios/Sync/BookmarkPayload.swift new file mode 100644 index 0000000000..2c4972e023 --- /dev/null +++ b/mobile/ios/Sync/BookmarkPayload.swift @@ -0,0 +1,571 @@ +/* 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 +import SwiftyJSON + +private let log = Logger.syncLogger + +public protocol MirrorItemable { + func toMirrorItem(_ modified: Timestamp) -> BookmarkMirrorItem +} + +extension BookmarkMirrorItem { + func asPayload() -> BookmarkBasePayload { + return BookmarkType.somePayloadFromJSON(self.asJSON()) + } + + func asPayloadWithChildren(_ children: [GUID]?) -> BookmarkBasePayload { + let remappedChildren: [GUID]? + if let children = children { + if BookmarkRoots.RootGUID == self.guid { + // Only the root contains roots, and so only its children + // need to be translated. + remappedChildren = children.map(BookmarkRoots.translateOutgoingRootGUID) + } else { + remappedChildren = children + } + } else { + remappedChildren = nil + } + + let json = self.asJSONWithChildren(remappedChildren) + return BookmarkType.somePayloadFromJSON(json) + } +} + +/** + * Hierarchy: + * - BookmarkBasePayload + * \_ FolderPayload + * \_ LivemarkPayload + * \_ SeparatorPayload + * \_ BookmarkPayload + * \_ BookmarkQueryPayload + */ + +public enum BookmarkType: String { + case livemark + case separator + case folder + case bookmark + case query + case microsummary // Dead: now a bookmark. + + // The result might be invalid, but it won't be nil. + public static func somePayloadFromJSON(_ json: JSON) -> BookmarkBasePayload { + return payloadFromJSON(json) ?? BookmarkBasePayload(json) + } + + public static func payloadFromJSON(_ json: JSON) -> BookmarkBasePayload? { + if json["deleted"].bool ?? false { + // Deleted records won't have a type. + return BookmarkBasePayload(json) + } + + guard let typeString = json["type"].string else { + return nil + } + + guard let type = BookmarkType(rawValue: typeString) else { + return nil + } + + switch type { + case microsummary: + fallthrough + case bookmark: + return BookmarkPayload(json) + case folder: + return FolderPayload(json) + case livemark: + return LivemarkPayload(json) + case separator: + return SeparatorPayload(json) + case query: + return BookmarkQueryPayload(json) + } + } + + public static func isValid(_ type: String?) -> Bool { + guard let type = type else { + return false + } + + return BookmarkType(rawValue: type) != nil + } +} + +open class LivemarkPayload: BookmarkBasePayload { + open var feedURI: String? { + return self["feedUri"].string + } + + open var siteURI: String? { + return self["siteUri"].string + } + + override open func isValid() -> Bool { + if !super.isValid() { + return false + } + return self.hasRequiredStringFields(["feedUri", "siteUri"]) + } + + override open func equalPayloads(_ obj: CleartextPayloadJSON) -> Bool { + guard let p = obj as? LivemarkPayload else { + return false + } + + if !super.equalPayloads(p) { + return false + } + + if self.deleted { + return true + } + + if self.feedURI != p.feedURI { + return false + } + + if self.siteURI != p.siteURI { + return false + } + + return true + } + + override open func toMirrorItem(_ modified: Timestamp) -> BookmarkMirrorItem { + if self.deleted { + return BookmarkMirrorItem.deleted(.livemark, guid: self.id, modified: modified) + } + + return BookmarkMirrorItem.livemark( + self.id, + dateAdded: self["dateAdded"].uInt64, + modified: modified, + hasDupe: self.hasDupe, + // TODO: these might need to be weakened if real-world data is dirty. + parentID: self["parentid"].stringValue, + parentName: self["parentName"].string, + title: self["title"].string, + description: self["description"].string, + feedURI: self.feedURI!, + siteURI: self.siteURI! + ) + } +} + +open class SeparatorPayload: BookmarkBasePayload { + override open func isValid() -> Bool { + if !super.isValid() { + return false + } + if !self["pos"].isInt() { + log.warning("Separator \(self.id) missing pos.") + return false + } + return true + } + + override open func equalPayloads(_ obj: CleartextPayloadJSON) -> Bool { + guard let p = obj as? SeparatorPayload else { + return false + } + + if !super.equalPayloads(p) { + return false + } + + if self.deleted { + return true + } + + if self["pos"].int != p["pos"].int { + return false + } + + return true + } + + override open func toMirrorItem(_ modified: Timestamp) -> BookmarkMirrorItem { + if self.deleted { + return BookmarkMirrorItem.deleted(.separator, guid: self.id, modified: modified) + } + + return BookmarkMirrorItem.separator( + self.id, + dateAdded: self["dateAdded"].uInt64, + modified: modified, + hasDupe: self.hasDupe, + // TODO: these might need to be weakened if real-world data is dirty. + parentID: self["parentid"].string!, + parentName: self["parentName"].string, + pos: self["pos"].int! + ) + } +} + +open class FolderPayload: BookmarkBasePayload { + fileprivate var childrenAreValid: Bool { + return self.hasStringArrayField("children") + } + + override open func isValid() -> Bool { + if !super.isValid() { + return false + } + + if !self.hasRequiredStringFields(["title"]) { + log.warning("Folder \(self.id) missing title.") + return false + } + + if !self.hasOptionalStringFields(["description"]) { + log.warning("Folder \(self.id) missing string description.") + return false + + } + if !self.childrenAreValid { + log.warning("Folder \(self.id) has invalid children.") + return false + } + + return true + } + + open var children: [String] { + return self["children"].arrayValue.map { $0.string! } + } + + override open func equalPayloads(_ obj: CleartextPayloadJSON) -> Bool { + guard let p = obj as? FolderPayload else { + return false + } + + if !super.equalPayloads(p) { + return false + } + + if self.deleted { + return true + } + + if self["title"].string != p["title"].string { + return false + } + + if self["description"].string != p["description"].string { + return false + } + + if self.children != p.children { + return false + } + + return true + } + + override open func toMirrorItem(_ modified: Timestamp) -> BookmarkMirrorItem { + if self.deleted { + return BookmarkMirrorItem.deleted(.folder, guid: self.id, modified: modified) + } + + return BookmarkMirrorItem.folder( + self.id, + dateAdded: self["dateAdded"].uInt64, + modified: modified, + hasDupe: self.hasDupe, + // TODO: these might need to be weakened if real-world data is dirty. + parentID: self["parentid"].string!, + parentName: self["parentName"].string, + title: self["title"].string!, + description: self["description"].string, + children: self.children + ) + } +} + +open class BookmarkPayload: BookmarkBasePayload { + fileprivate static let requiredBookmarkStringFields = ["bmkUri"] + + // Title *should* be required, but can be missing for queries. Great. + fileprivate static let optionalBookmarkStringFields = ["title", "keyword", "description"] + fileprivate static let optionalBookmarkBooleanFields = ["loadInSidebar"] + + override open func isValid() -> Bool { + if !super.isValid() { + return false + } + + if !self.hasRequiredStringFields(BookmarkPayload.requiredBookmarkStringFields) { + log.warning("Bookmark \(self.id) missing required string field.") + return false + } + + if !self.hasStringArrayField("tags") { + log.warning("Bookmark \(self.id) missing tags array. We'll replace with an empty array.") + // Ignore. + } + + if !self.hasOptionalStringFields(BookmarkPayload.optionalBookmarkStringFields) { + return false + } + + return self.hasOptionalBooleanFields(BookmarkPayload.optionalBookmarkBooleanFields) + } + + override open func equalPayloads(_ obj: CleartextPayloadJSON) -> Bool { + guard let p = obj as? BookmarkPayload else { + return false + } + + if !super.equalPayloads(p) { + return false + } + + if self.deleted { + return true + } + + if !BookmarkPayload.requiredBookmarkStringFields.every({ p[$0].string! == self[$0].string! }) { + return false + } + + // TODO: compare optional fields. + + if Set(self.tags) != Set(p.tags) { + return false + } + + if self["loadInSidebar"].bool != p["loadInSidebar"].bool { + return false + } + + return true + } + + lazy var tags: [String] = { + return self["tags"].arrayValue.flatMap { $0.string } + }() + + lazy var tagsString: String = { + if self["tags"].isArray() { + return self["tags"].stringValue() ?? "[]" + } + return "[]" + }() + + override open func toMirrorItem(_ modified: Timestamp) -> BookmarkMirrorItem { + if self.deleted { + return BookmarkMirrorItem.deleted(.bookmark, guid: self.id, modified: modified) + } + + return BookmarkMirrorItem.bookmark( + self.id, + dateAdded: self["dateAdded"].uInt64, + modified: modified, + hasDupe: self.hasDupe, + // TODO: these might need to be weakened if real-world data is dirty. + parentID: self["parentid"].string!, + parentName: self["parentName"].string, + title: self["title"].string ?? "", + description: self["description"].string, + URI: self["bmkUri"].string!, + tags: self.tagsString, // Stringify it so we can put the array in the DB. + keyword: self["keyword"].string + ) + } +} + +open class BookmarkQueryPayload: BookmarkPayload { + override open func isValid() -> Bool { + if !super.isValid() { + return false + } + + if !self.hasOptionalStringFields(["queryId", "folderName"]) { + log.warning("Query \(self.id) missing queryId or folderName.") + return false + } + + return true + } + + override open func equalPayloads(_ obj: CleartextPayloadJSON) -> Bool { + guard let p = obj as? BookmarkQueryPayload else { + return false + } + + if !super.equalPayloads(p) { + return false + } + + if self.deleted { + return true + } + + if self["folderName"].string != p["folderName"].string { + return false + } + + if self["queryId"].string != p["queryId"].string { + return false + } + + return true + } + + override open func toMirrorItem(_ modified: Timestamp) -> BookmarkMirrorItem { + if self.deleted { + return BookmarkMirrorItem.deleted(.query, guid: self.id, modified: modified) + } + + return BookmarkMirrorItem.query( + self.id, + dateAdded: self["dateAdded"].uInt64, + modified: modified, + hasDupe: self.hasDupe, + parentID: self["parentid"].string!, + parentName: self["parentName"].string, + title: self["title"].string ?? "", + description: self["description"].string, + URI: self["bmkUri"].string!, + tags: self.tagsString, // Stringify it so we can put the array in the DB. + keyword: self["keyword"].string, + folderName: self["folderName"].string, + queryID: self["queryID"].string + ) + } +} + +open class BookmarkBasePayload: CleartextPayloadJSON, MirrorItemable { + fileprivate static let requiredStringFields: [String] = ["parentid", "type"] + fileprivate static let optionalBooleanFields: [String] = ["hasDupe"] + + static func deletedPayload(_ guid: GUID) -> BookmarkBasePayload { + let remappedGUID = BookmarkRoots.translateOutgoingRootGUID(guid) + return BookmarkBasePayload(JSON(["id": remappedGUID, "deleted": true])) + } + + func hasStringArrayField(_ name: String) -> Bool { + guard let arr = self[name].array else { + return false + } + return arr.every { $0.isString() } + } + + func hasRequiredStringFields(_ fields: [String]) -> Bool { + return fields.every { self[$0].isString() } + } + + func hasOptionalStringFields(_ fields: [String]) -> Bool { + return fields.every { field in + let val = self[field] + // Yup, 404 is not found, so this means "string or nothing". + let valid = val.isString() || val.isNull() || val.isError() + if !valid { + log.debug("Field \(field) is invalid: \(val).") + } + return valid + } + } + + func hasOptionalBooleanFields(_ fields: [String]) -> Bool { + return fields.every { field in + let val = self[field] + // Yup, 404 is not found, so this means "boolean or nothing". + let valid = val.isBool() || val.isNull() || val.error?.code == 404 + if !valid { + log.debug("Field \(field) is invalid: \(val).") + } + return valid + } + } + + override open func isValid() -> Bool { + if !super.isValid() { + return false + } + + if self["deleted"].bool ?? false { + return true + } + + // If not deleted, we must be a specific, known, type! + if !BookmarkType.isValid(self["type"].string) { + return false + } + + if !(self["parentName"].isString() || self.id == "places") { + if self["parentid"].string! == "places" { + log.debug("Accepting root with missing parent name.") + } else { + // Bug 1318414. + log.warning("Accepting bookmark with missing parent name.") + } + } + + if !self.hasRequiredStringFields(BookmarkBasePayload.requiredStringFields) { + log.warning("Item missing required string field.") + return false + } + + return self.hasOptionalBooleanFields(BookmarkBasePayload.optionalBooleanFields) + } + + open var hasDupe: Bool { + return self["hasDupe"].bool ?? false + } + + /** + * This only makes sense for valid payloads. + */ + override open func equalPayloads(_ obj: CleartextPayloadJSON) -> Bool { + guard let p = obj as? BookmarkBasePayload else { + return false + } + + if !super.equalPayloads(p) { + return false + } + + if self.deleted { + return true + } + + if p.deleted { + return self.deleted == p.deleted + } + + // If either record is deleted, these other fields might be missing. + // But we just checked, so we're good to roll on. + + let same: (String) -> Bool = { field in + let left = self[field].string + let right = p[field].string + return left == right + } + + if !BookmarkBasePayload.requiredStringFields.every(same) { + return false + } + + if p["parentName"].string != self["parentName"].string { + return false + } + + return self.hasDupe == p.hasDupe + } + + // This goes here because extensions cannot override methods yet. + open func toMirrorItem(_ modified: Timestamp) -> BookmarkMirrorItem { + precondition(self.deleted, "Non-deleted items should have a specific type.") + return BookmarkMirrorItem.deleted(.bookmark, guid: self.id, modified: modified) + } +} diff --git a/mobile/ios/Sync/BookmarkTelemetryPing.swift b/mobile/ios/Sync/BookmarkTelemetryPing.swift new file mode 100644 index 0000000000..dc5fcd0105 --- /dev/null +++ b/mobile/ios/Sync/BookmarkTelemetryPing.swift @@ -0,0 +1,53 @@ +/* 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 SwiftyJSON +import Shared + +public func makeAdHocBookmarkMergePing(_ bundle: Bundle, clientID: String, attempt: Int32, bufferRows: Int?, valid: [String: Bool], clientCount: Int) -> JSON { + let anyFailed = valid.reduce(false, { $0 || $1.1 }) + + var out: [String: Any] = [ + "v": 1, + "appV": AppInfo.appVersion, + "build": AppInfo.buildNumber, + "id": clientID, + "attempt": Int(attempt), + "success": !anyFailed, + "date": Date().description, + "clientCount": clientCount, + ] + + if let bufferRows = bufferRows { + out["rows"] = bufferRows + } + + if anyFailed { + valid.forEach { key, value in + out[key] = value + } + } + + return JSON(out) +} + +public func makeAdHocSyncStatusPing(_ bundle: Bundle, clientID: String, statusObject: [String: String]?, engineResults: [String: String]?, resultsFailure: MaybeErrorType?, clientCount: Int) -> JSON { + + let statusObject: Any = statusObject ?? JSON.null + let engineResults: Any = engineResults ?? JSON.null + let resultsFailure: Any = resultsFailure?.description ?? JSON.null + + let out: [String: Any] = [ + "v": 1, + "appV": AppInfo.appVersion, + "build": AppInfo.buildNumber, + "id": clientID, + "date": Date().description, + "clientCount": clientCount, + "statusObject": statusObject, + "engineResults": engineResults, + "resultsFailure": resultsFailure + ] + + return JSON(out) +} diff --git a/mobile/ios/Sync/CleartextPayloadJSON.swift b/mobile/ios/Sync/CleartextPayloadJSON.swift new file mode 100644 index 0000000000..9303acf63d --- /dev/null +++ b/mobile/ios/Sync/CleartextPayloadJSON.swift @@ -0,0 +1,61 @@ +/* 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 SwiftyJSON + +open class BasePayloadJSON { + let json: JSON + required public init(_ jsonString: String) { + self.json = JSON(parseJSON: jsonString) + } + + public init(_ json: JSON) { + self.json = json + } + + // Override me. + fileprivate func isValid() -> Bool { + return self.json.type != .unknown && + self.json.error == nil + } + + subscript(key: String) -> JSON { + get { + return json[key] + } + } +} + +/** + * http://docs.services.mozilla.com/sync/objectformats.html + * "In addition to these custom collection object structures, the + * Encrypted DataObject adds fields like id and deleted." + */ +open class CleartextPayloadJSON: BasePayloadJSON { + // Override me. + override open func isValid() -> Bool { + return super.isValid() && self["id"].isString() + } + + open var id: String { + return self["id"].string! + } + + open var deleted: Bool { + let d = self["deleted"] + if let bool = d.bool { + return bool + } else { + return false + } + } + + // Override me. + // Doesn't check id. Should it? + open func equalPayloads (_ obj: CleartextPayloadJSON) -> Bool { + return self.deleted == obj.deleted + } +} diff --git a/mobile/ios/Sync/ClientPayload.swift b/mobile/ios/Sync/ClientPayload.swift new file mode 100644 index 0000000000..3c60c5ca29 --- /dev/null +++ b/mobile/ios/Sync/ClientPayload.swift @@ -0,0 +1,57 @@ +/* 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 SwiftyJSON + +open class ClientPayload: CleartextPayloadJSON { + override open func isValid() -> Bool { + if !super.isValid() { + return false + } + + if self["deleted"].bool ?? false { + return true + } + + return self["name"].isString() && + self["type"].isString() + } + + var commands: [JSON] { + return self["commands"].array ?? [] // It might not be present at all. + } + + var name: String { + return self["name"].stringValue + } + + var clientType: String { + return self["type"].stringValue + } + + override open func equalPayloads(_ obj: CleartextPayloadJSON) -> Bool { + if !(obj is ClientPayload) { + return false + } + + if !super.equalPayloads(obj) { + return false + } + + let p = obj as! ClientPayload + if p.name != self.name { + return false + } + + if p.clientType != self.clientType { + return false + } + + return true + } + + // TODO: version, protocols. +} diff --git a/mobile/ios/Sync/EncryptedJSON.swift b/mobile/ios/Sync/EncryptedJSON.swift new file mode 100644 index 0000000000..33d0cad00c --- /dev/null +++ b/mobile/ios/Sync/EncryptedJSON.swift @@ -0,0 +1,169 @@ +/* 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 FxA +import Account +import XCGLogger +import SwiftyJSON + +private let log = Logger.syncLogger + +/** + * Turns JSON of the form + * + * { ciphertext: ..., hmac: ..., iv: ...} + * + * into a new JSON object resulting from decrypting and parsing the ciphertext. + */ +open class EncryptedJSON { + var json: JSON + var _cleartext: JSON? // Cache decrypted cleartext. + var _ciphertextBytes: Data? // Cache decoded ciphertext. + var _hmacBytes: Data? // Cache decoded HMAC. + var _ivBytes: Data? // Cache decoded IV. + + var valid: Bool = false + var validated: Bool = false + + let keyBundle: KeyBundle + + public init(json: String, keyBundle: KeyBundle) { + self.keyBundle = keyBundle + self.json = JSON(parseJSON: json) + } + + public init(json: JSON, keyBundle: KeyBundle) { + self.keyBundle = keyBundle + self.json = json + } + + // For validating HMAC: the raw ciphertext as bytes without decoding. + fileprivate var ciphertextB64: Data? { + if let ct = self["ciphertext"].string { + return Bytes.dataFromBase64(ct) + } + return nil + } + + /** + * You probably want to call validate() and then use .ciphertext. + */ + fileprivate var ciphertextBytes: Data? { + return Bytes.decodeBase64(self["ciphertext"].string!) + } + + fileprivate func validate() -> Bool { + if validated { + return valid + } + + defer { validated = true } + + guard self["ciphertext"].isString() && + self["hmac"].isString() && + self["IV"].isString() else { + valid = false + return false + } + + guard let ciphertextForHMAC = self.ciphertextB64 else { + valid = false + return false + } + + guard keyBundle.verify(hmac: self.hmac, ciphertextB64: ciphertextForHMAC) else { + valid = false + return false + } + + // I guess we called validate twice… + if self._ciphertextBytes != nil { + valid = true + return true + } + + // Also verify that the ciphertext is valid base64. Do this by + // retrieving the value in a failable way, leaving the accessors + // to take the dangerous/simple path. + // We can force-unwrap self["ciphertext"] because we already checked + // it when verifying the HMAC above. + guard let data = self.ciphertextBytes else { + log.error("Unable to decode ciphertext base64 in record \(self["id"].string ?? "")") + valid = false + return false + } + + self._ciphertextBytes = data + valid = true + return valid + } + + open func isValid() -> Bool { + return !json.isError() && self.validate() + } + + /** + * Make sure you call isValid first. This API force-unwraps for simplicity. + */ + var ciphertext: Data { + if _ciphertextBytes != nil { + return _ciphertextBytes! + } + + _ciphertextBytes = self.ciphertextBytes + return _ciphertextBytes! + } + + var hmac: Data { + if _hmacBytes != nil { + return _hmacBytes! + } + //NSData(base16EncodedString: self["hmac"].asString!, options: NSDataBase16DecodingOptions.Default) + _hmacBytes = NSData(base16EncodedString: self["hmac"].stringValue, options: []) as Data + return _hmacBytes! + } + + var iv: Data { + if _ivBytes != nil { + return _ivBytes! + } + + _ivBytes = Bytes.decodeBase64(self["IV"].string!) + return _ivBytes! + } + + // Returns nil on error. + open var cleartext: JSON? { + if _cleartext != nil { + return _cleartext + } + + if !isValid() { + log.error("Failed to validate.") + return nil + } + + let decrypted: String? = keyBundle.decrypt(self.ciphertext, iv: self.iv) + if decrypted == nil { + log.error("Failed to decrypt.") + valid = false + return nil + } + + _cleartext = JSON(parseJSON: decrypted!) + return _cleartext! + } + + subscript(key: String) -> JSON { + get { + return json[key] + } + + set { + json[key] = newValue + } + } +} diff --git a/mobile/ios/Sync/EnvelopeJSON.swift b/mobile/ios/Sync/EnvelopeJSON.swift new file mode 100644 index 0000000000..d0525940f4 --- /dev/null +++ b/mobile/ios/Sync/EnvelopeJSON.swift @@ -0,0 +1,73 @@ +/* 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 SwiftyJSON + +open class EnvelopeJSON { + fileprivate let json: JSON + + public init(_ jsonString: String) { + self.json = JSON(parseJSON: jsonString) + } + + public init(_ json: JSON) { + self.json = json + } + + open func isValid() -> Bool { + return !self.json.isError() && + self.json["id"].isString() && + //self["collection"].isString && + self.json["payload"].isString() + } + + open var id: String { + return self.json["id"].string! + } + + open var collection: String { + return self.json["collection"].string ?? "" + } + + open var payload: String { + return self.json["payload"].string! + } + + open var sortindex: Int { + let s = self.json["sortindex"] + return s.int ?? 0 + } + + open var modified: Timestamp { +// if let intValue = self.json["modified"].int64 { +// return Timestamp(intValue) * 1000 +// } + + if let doubleValue = self.json["modified"].double { + return Timestamp(1000 * (doubleValue)) + } + + return 0 + } + + open func toString() -> String { + return self.json.stringValue()! + } + + open func withModified(_ now: Timestamp) -> EnvelopeJSON { + if var d = self.json.dictionary { + d["modified"] = JSON(Double(now) / 1000) + return EnvelopeJSON(JSON(d)) + } + return EnvelopeJSON(JSON(parseJSON: "!")) // Intentionally bad JSON. + } +} + +extension EnvelopeJSON { + func asJSON() -> JSON { + return self.json + } +} diff --git a/mobile/ios/Sync/HistoryPayload.swift b/mobile/ios/Sync/HistoryPayload.swift new file mode 100644 index 0000000000..0f1508037f --- /dev/null +++ b/mobile/ios/Sync/HistoryPayload.swift @@ -0,0 +1,82 @@ +/* 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 SwiftyJSON + +open class HistoryPayload: CleartextPayloadJSON { + open class func fromJSON(_ json: JSON) -> HistoryPayload? { + let p = HistoryPayload(json) + if p.isValid() { + return p + } + return nil + } + + override open func isValid() -> Bool { + if !super.isValid() { + return false + } + + if self["deleted"].bool ?? false { + return true + } + + return self["histUri"].string != nil && // TODO: validate URI. + self["title"].isStringOrNull() && + self["visits"].isArray() + } + + open func asPlace() -> Place { + return Place(guid: self.id, url: self.histURI, title: self.title) + } + + var visits: [Visit] { + let visits = self["visits"].arrayObject as! [[String: Any]] + return optFilter(visits.map(Visit.fromJSON)) + } + + fileprivate var histURI: String { + return self["histUri"].string! + } + + var historyURI: URL { + return self.histURI.asURL! + } + + var title: String { + return self["title"].string ?? "" + } + + override open func equalPayloads(_ obj: CleartextPayloadJSON) -> Bool { + if let p = obj as? HistoryPayload { + if !super.equalPayloads(p) { + return false + } + + if p.deleted { + return self.deleted == p.deleted + } + + // If either record is deleted, these other fields might be missing. + // But we just checked, so we're good to roll on. + + if p.title != self.title { + return false + } + + if p.historyURI != self.historyURI { + return false + } + + // TODO: compare visits. + + return true + } + + return false + } +} diff --git a/mobile/ios/Sync/Info.plist b/mobile/ios/Sync/Info.plist new file mode 100644 index 0000000000..b3205ecaf4 --- /dev/null +++ b/mobile/ios/Sync/Info.plist @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + FMWK + CFBundleShortVersionString + 10.6 + CFBundleSignature + ???? + CFBundleVersion + 1 + NSPrincipalClass + + + diff --git a/mobile/ios/Sync/Info.swift b/mobile/ios/Sync/Info.swift new file mode 100644 index 0000000000..0b41fcb389 --- /dev/null +++ b/mobile/ios/Sync/Info.swift @@ -0,0 +1,75 @@ +/* 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 SwiftyJSON + +open class InfoCollections { + fileprivate let collections: [String: Timestamp] + + init(collections: [String: Timestamp]) { + self.collections = collections + } + + open class func fromJSON(_ json: JSON) -> InfoCollections? { + if let dict = json.dictionary { + var coll = [String: Timestamp]() + for (key, value) in dict { + if let value = value.double { + coll[key] = Timestamp(value * 1000) + } else { + return nil // Invalid, so bail out. + } + } + return InfoCollections(collections: coll) + } + return nil + } + + open func collectionNames() -> [String] { + return Array(self.collections.keys) + } + + open func modified(_ collection: String) -> Timestamp? { + return self.collections[collection] + } + + // Two I/Cs are the same if they have the same modified times for a set of + // collections. If no collections are specified, they're considered the same + // if the other I/C has the same values for this I/C's collections, and + // they have the same collection array. + open func same(_ other: InfoCollections, collections: [String]?) -> Bool { + if let collections = collections { + return collections.every({ self.modified($0) == other.modified($0) }) + } + + // Same collections? + let ours = self.collectionNames() + let theirs = other.collectionNames() + return ours.sameElements(theirs) && same(other, collections: ours) + } +} + +// Response object from https:///info/configuration +public struct InfoConfiguration { + + // Maximum size in bytes of the overall HTTP request body. + public let maxRequestBytes: Int + + // Maximum number of records that can be uploaded to a collection in a single POST request. + public let maxPostRecords: Int + + // Maximum combined size in bytes of the record payloads that can be uploaded to a collection in + // a single POST request. + public let maxPostBytes: Int + + // Maximum total number of records that can be uploaded to a collection as part of a batched upload. + public let maxTotalRecords: Int + + // Maximum total combined size in bytes of the record payloads that can be uploaded to a collection + // as part of a batched upload. + public let maxTotalBytes: Int +} + diff --git a/mobile/ios/Sync/KeyBundle.swift b/mobile/ios/Sync/KeyBundle.swift new file mode 100644 index 0000000000..c9a3c9eb25 --- /dev/null +++ b/mobile/ios/Sync/KeyBundle.swift @@ -0,0 +1,319 @@ +/* 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 FxA +import Account +import SwiftyJSON + +private let KeyLength = 32 + +open class KeyBundle: Hashable { + let encKey: Data + let hmacKey: Data + + open class func fromKB(_ kB: Data) -> KeyBundle { + let salt = Data() + let contextInfo = FxAClient10.KW("oldsync") + let len: UInt = 64 // KeyLength + KeyLength, without type nonsense. + let derived = (kB as NSData).deriveHKDFSHA256Key(withSalt: salt, contextInfo: contextInfo, length: len)! + return KeyBundle(encKey: derived.subdata(in: 0.. KeyBundle { + // Bytes.generateRandomBytes uses SecRandomCopyBytes, which hits /dev/random, which + // on iOS is populated by the OS from kernel-level sources of entropy. + // That should mean that we don't need to seed or initialize anything before calling + // this. That is probably not true on (some versions of) OS X. + return KeyBundle(encKey: Bytes.generateRandomBytes(32), hmacKey: Bytes.generateRandomBytes(32)) + } + + open class var invalid: KeyBundle { + return KeyBundle(encKeyB64: "deadbeef", hmacKeyB64: "deadbeef")! + } + + public init?(encKeyB64: String, hmacKeyB64: String) { + guard let e = Bytes.decodeBase64(encKeyB64), + let h = Bytes.decodeBase64(hmacKeyB64) else { + return nil + } + self.encKey = e + self.hmacKey = h + } + + public init(encKey: Data, hmacKey: Data) { + self.encKey = encKey + self.hmacKey = hmacKey + } + + fileprivate func _hmac(_ ciphertext: Data) -> (data: UnsafeMutablePointer, len: Int) { + let hmacAlgorithm = CCHmacAlgorithm(kCCHmacAlgSHA256) + let digestLen: Int = Int(CC_SHA256_DIGEST_LENGTH) + let result = UnsafeMutablePointer.allocate(capacity: digestLen) + CCHmac(hmacAlgorithm, hmacKey.getBytes(), hmacKey.count, ciphertext.getBytes(), ciphertext.count, result) + return (result, digestLen) + } + + open func hmac(_ ciphertext: Data) -> Data { + let (result, digestLen) = _hmac(ciphertext) + let data = NSMutableData(bytes: result, length: digestLen) + + result.deinitialize() + result.deallocate(capacity: digestLen) + return data as Data + } + + /** + * Returns a hex string for the HMAC. + */ + open func hmacString(_ ciphertext: Data) -> String { + let (result, digestLen) = _hmac(ciphertext) + let hash = NSMutableString() + for i in 0.. (ciphertext: Data, iv: Data)? { + let iv = iv ?? Bytes.generateRandomBytes(16) + + let (success, b, copied) = self.crypt(cleartext, iv: iv, op: CCOperation(kCCEncrypt)) + let byteCount = cleartext.count + kCCBlockSizeAES128 + if success == CCCryptorStatus(kCCSuccess) { + // Hooray! + let d = Data(bytes: b, count: Int(copied)) + b.deallocate(bytes: byteCount, alignedTo: MemoryLayout.size) + return (d, iv) + } + + b.deallocate(bytes: byteCount, alignedTo: MemoryLayout.size) + return nil + } + + // You *must* verify HMAC before calling this. + open func decrypt(_ ciphertext: Data, iv: Data) -> String? { + let (success, b, copied) = self.crypt(ciphertext, iv: iv, op: CCOperation(kCCDecrypt)) + let byteCount = ciphertext.count + kCCBlockSizeAES128 + if success == CCCryptorStatus(kCCSuccess) { + // Hooray! + let d = Data(bytes: b, count: Int(copied)) + let s = NSString(data: d, encoding: String.Encoding.utf8.rawValue) + b.deallocate(bytes: byteCount, alignedTo: MemoryLayout.size) + return s as String? + } + + b.deallocate(bytes: byteCount, alignedTo: MemoryLayout.size) + return nil + } + + fileprivate func crypt(_ input: Data, iv: Data, op: CCOperation) -> (status: CCCryptorStatus, buffer: UnsafeMutableRawPointer, count: Int) { + let resultSize = input.count + kCCBlockSizeAES128 + var copied: Int = 0 + let result = UnsafeMutableRawPointer.allocate(bytes: resultSize, alignedTo: MemoryLayout.size) + + let success: CCCryptorStatus = + CCCrypt(op, + CCHmacAlgorithm(kCCAlgorithmAES128), + CCOptions(kCCOptionPKCS7Padding), + encKey.getBytes(), + kCCKeySizeAES256, + iv.getBytes(), + input.getBytes(), + input.count, + result, + resultSize, + &copied + ) + + return (success, result, copied) + } + + open func verify(hmac: Data, ciphertextB64: Data) -> Bool { + let expectedHMAC = hmac + let computedHMAC = self.hmac(ciphertextB64) + return (expectedHMAC == computedHMAC) + } + + /** + * Swift can't do functional factories. I would like to have one of the following + * approaches be viable: + * + * 1. Derive the constructor from the consumer of the factory. + * 2. Accept a type as input. + * + * Neither of these are viable, so we instead pass an explicit constructor closure. + * + * Most of these approaches produce either odd compiler errors, or -- worse -- + * compile and then yield runtime EXC_BAD_ACCESS (see Radar 20230159). + * + * For this reason, be careful trying to simplify or improve this code. + */ + open func factory(_ f: @escaping (JSON) -> T) -> (String) -> T? { + return { (payload: String) -> T? in + let potential = EncryptedJSON(json: payload, keyBundle: self) + if !potential.isValid() { + return nil + } + + let cleartext = potential.cleartext + if cleartext == nil { + return nil + } + return f(cleartext!) + } + } + + // TODO: how much do we want to move this into EncryptedJSON? + open func serializer(_ f: @escaping (T) -> JSON) -> (Record) -> JSON? { + return { (record: Record) -> JSON? in + let json = f(record.payload) + if json.isNull() { + // This should never happen, but if it does, we don't want to leak this + // record to the server! + return nil + } + + let bytes: Data + do { + // Get the most basic kind of encoding: no pretty printing. + // This can throw; if so, we return nil. + // `rawData` simply calls JSONSerialization.dataWithJSONObject:options:error, which + // guarantees UTF-8 encoded output. + bytes = try json.rawData(options: []) + } catch { + return nil + } + + // Given a valid non-null JSON object, we don't ever expect a round-trip to fail. + assert(!JSON(bytes).isNull()) + + // We pass a null IV, which means "generate me a new one". + // We then include the generated IV in the resulting record. + if let (ciphertext, iv) = self.encrypt(bytes, iv: nil) { + // So we have the encrypted payload. Now let's build the envelope around it. + let ciphertext = ciphertext.base64EncodedString + + // The HMAC is computed over the base64 string. As bytes. Yes, I know. + if let encodedCiphertextBytes = ciphertext.data(using: String.Encoding.ascii, allowLossyConversion: false) { + let hmac = self.hmacString(encodedCiphertextBytes) + let iv = iv.base64EncodedString + + // The payload is stringified JSON. Yes, I know. + let payload: Any = JSON(object: ["ciphertext": ciphertext, "IV": iv, "hmac": hmac]).stringValue()! as Any + let obj = ["id": record.id, + "sortindex": record.sortindex, + // This is how SwiftyJSON wants us to express a null that we want to + // serialize. Yes, this is gross. + "ttl": record.ttl ?? NSNull(), + "payload": payload] + return JSON(object: obj) + } + } + return nil + } + } + + open func asPair() -> [String] { + return [self.encKey.base64EncodedString, self.hmacKey.base64EncodedString] + } + + open var hashValue: Int { + return "\(self.encKey.base64EncodedString) \(self.hmacKey.base64EncodedString)".hashValue + } +} + +public func == (lhs: KeyBundle, rhs: KeyBundle) -> Bool { + return (lhs.encKey == rhs.encKey) && + (lhs.hmacKey == rhs.hmacKey) +} + +open class Keys: Equatable { + let valid: Bool + let defaultBundle: KeyBundle + var collectionKeys: [String: KeyBundle] = [String: KeyBundle]() + + public init(defaultBundle: KeyBundle) { + self.defaultBundle = defaultBundle + self.valid = true + } + + public init(payload: KeysPayload?) { + if let payload = payload, payload.isValid() { + if let keys = payload.defaultKeys { + self.defaultBundle = keys + self.collectionKeys = payload.collectionKeys + self.valid = true + return + } + } + self.defaultBundle = KeyBundle.invalid + self.valid = false + } + + public convenience init(downloaded: EnvelopeJSON, master: KeyBundle) { + let f: (JSON) -> KeysPayload = { KeysPayload($0) } + let keysRecord = Record.fromEnvelope(downloaded, payloadFactory: master.factory(f)) + self.init(payload: keysRecord?.payload) + } + + open class func random() -> Keys { + return Keys(defaultBundle: KeyBundle.random()) + } + + open func forCollection(_ collection: String) -> KeyBundle { + if let bundle = collectionKeys[collection] { + return bundle + } + return defaultBundle + } + + open func encrypter(_ collection: String, encoder: RecordEncoder) -> RecordEncrypter { + return RecordEncrypter(bundle: forCollection(collection), encoder: encoder) + } + + open func asPayload() -> KeysPayload { + let json: JSON = JSON([ + "id": "keys", + "collection": "crypto", + "default": self.defaultBundle.asPair(), + "collections": mapValues(self.collectionKeys, f: { $0.asPair() }) + ]) + return KeysPayload(json) + } +} + +/** + * Yup, these are basically typed tuples. + */ +public struct RecordEncoder { + let decode: (JSON) -> T + let encode: (T) -> JSON +} + +public struct RecordEncrypter { + let serializer: (Record) -> JSON? + let factory: (String) -> T? + + init(bundle: KeyBundle, encoder: RecordEncoder) { + self.serializer = bundle.serializer(encoder.encode) + self.factory = bundle.factory(encoder.decode) + } + + init(serializer: @escaping (Record) -> JSON?, factory: @escaping (String) -> T?) { + self.serializer = serializer + self.factory = factory + } +} + +public func ==(lhs: Keys, rhs: Keys) -> Bool { + return lhs.valid == rhs.valid && + lhs.defaultBundle == rhs.defaultBundle && + lhs.collectionKeys == rhs.collectionKeys +} diff --git a/mobile/ios/Sync/KeysPayload.swift b/mobile/ios/Sync/KeysPayload.swift new file mode 100644 index 0000000000..72570f07b5 --- /dev/null +++ b/mobile/ios/Sync/KeysPayload.swift @@ -0,0 +1,55 @@ +/* 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 SwiftyJSON + +open class KeysPayload: CleartextPayloadJSON { + override open func isValid() -> Bool { + return super.isValid() && + self["default"].isArray() + } + + fileprivate func keyBundleFromPair(_ input: JSON) -> KeyBundle? { + if let pair: [JSON] = input.array { + if let encKey = pair[0].string { + if let hmacKey = pair[1].string { + return KeyBundle(encKeyB64: encKey, hmacKeyB64: hmacKey) + } + } + } + return nil + } + + var defaultKeys: KeyBundle? { + return self.keyBundleFromPair(self["default"]) + } + + var collectionKeys: [String: KeyBundle] { + if let collections: [String: JSON] = self["collections"].dictionary { + return optFilter(mapValues(collections, f: self.keyBundleFromPair)) + } + return [:] + } + + override open func equalPayloads(_ obj: CleartextPayloadJSON) -> Bool { + if !(obj is KeysPayload) { + return false + } + + if !super.equalPayloads(obj) { + return false + } + + let p = obj as! KeysPayload + if p.defaultKeys != self.defaultKeys { + return false + } + + // TODO: check collections. + + return true + } +} diff --git a/mobile/ios/Sync/LoginPayload.swift b/mobile/ios/Sync/LoginPayload.swift new file mode 100644 index 0000000000..b1b321c496 --- /dev/null +++ b/mobile/ios/Sync/LoginPayload.swift @@ -0,0 +1,156 @@ +/* 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 XCGLogger +import SwiftyJSON + +private let log = Logger.syncLogger + +open class LoginPayload: CleartextPayloadJSON { + fileprivate static let OptionalStringFields = [ + "formSubmitURL", + "httpRealm", + ] + + fileprivate static let OptionalNumericFields = [ + "timeLastUsed", + "timeCreated", + "timePasswordChanged", + "timesUsed", + ] + + fileprivate static let RequiredStringFields = [ + "hostname", + "username", + "password", + "usernameField", + "passwordField", + ] + + open class func fromJSON(_ json: JSON) -> LoginPayload? { + let p = LoginPayload(json) + if p.isValid() { + return p + } + return nil + } + + override open func isValid() -> Bool { + if !super.isValid() { + return false + } + + if self["deleted"].bool ?? false { + return true + } + + if !LoginPayload.RequiredStringFields.every({ self[$0].isString() }) { + return false + } + + if !LoginPayload.OptionalStringFields.every({ field in + let val = self[field] + // Yup, 404 is not found, so this means "string or nothing". + let valid = val.isString() || val.isNull() || val.error?.code == 404 + if !valid { + log.debug("Field \(field) is invalid: \(val)") + } + return valid + }) { + return false + } + + if !LoginPayload.OptionalNumericFields.every({ field in + let val = self[field] + // Yup, 404 is not found, so this means "number or nothing". + // We only check for number because we're including timestamps as NSNumbers. + let valid = val.isNumber() || val.isNull() || val.error?.code == 404 + if !valid { + log.debug("Field \(field) is invalid: \(val)") + } + return valid + }) { + return false + } + + return true + } + + open var hostname: String { + return self["hostname"].string! + } + + open var username: String { + return self["username"].string! + } + + open var password: String { + return self["password"].string! + } + + open var usernameField: String { + return self["usernameField"].string! + } + + open var passwordField: String { + return self["passwordField"].string! + } + + open var formSubmitURL: String? { + return self["formSubmitURL"].string + } + + open var httpRealm: String? { + return self["httpRealm"].string + } + + fileprivate func timestamp(_ field: String) -> Timestamp? { + let json = self[field] + if let i = json.int64, i > 0 { + return Timestamp(i) + } + return nil + } + + open var timesUsed: Int? { + return self["timesUsed"].int + } + + open var timeCreated: Timestamp? { + return self.timestamp("timeCreated") + } + + open var timeLastUsed: Timestamp? { + return self.timestamp("timeLastUsed") + } + + open var timePasswordChanged: Timestamp? { + return self.timestamp("timePasswordChanged") + } + + override open func equalPayloads(_ obj: CleartextPayloadJSON) -> Bool { + if let p = obj as? LoginPayload { + if !super.equalPayloads(p) { + return false + } + + if p.deleted || self.deleted { + return self.deleted == p.deleted + } + + // If either record is deleted, these other fields might be missing. + // But we just checked, so we're good to roll on. + + return LoginPayload.RequiredStringFields.every({ field in + p[field].string == self[field].string + }) + + // TODO: optional fields. + } + + return false + } +} diff --git a/mobile/ios/Sync/Record.swift b/mobile/ios/Sync/Record.swift new file mode 100644 index 0000000000..b280128d11 --- /dev/null +++ b/mobile/ios/Sync/Record.swift @@ -0,0 +1,98 @@ +/* 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 XCGLogger + +private let log = Logger.syncLogger + +let ONE_YEAR_IN_SECONDS = 365 * 24 * 60 * 60 + +/** + * Immutable representation for Sync records. + * + * Envelopes consist of: + * Required: "id", "collection", "payload". + * Optional: "modified", "sortindex", "ttl". + * + * Deletedness is a property of the payload. + */ +open class Record { + open let id: String + open let payload: T + + open let modified: Timestamp + open let sortindex: Int + open let ttl: Int? // Seconds. Can be null, which means 'don't expire'. + + // This is a hook for decryption. + // Right now it only parses the string. In subclasses, it'll parse the + // string, decrypt the contents, and return the data as a JSON object. + // From the docs: + // + // payload none string 256k + // A string containing a JSON structure encapsulating the data of the record. + // This structure is defined separately for each WBO type. + // Parts of the structure may be encrypted, in which case the structure + // should also specify a record for decryption. + // + // @seealso EncryptedRecord. + open class func payloadFromPayloadString(_ envelope: EnvelopeJSON, payload: String) -> T? { + return T(payload) + } + + // TODO: consider using error tuples. + open class func fromEnvelope(_ envelope: EnvelopeJSON, payloadFactory: (String) -> T?) -> Record? { + if !(envelope.isValid()) { + log.error("Invalid envelope.") + return nil + } + guard let payload = payloadFactory(envelope.payload) else { + log.error("Unable to parse payload.") + return nil + } + + if !payload.isValid() { + log.error("Invalid payload \(envelope.payload).") + return nil + } + + return Record(envelope: envelope, payload: payload) + } + + /** + * Accepts an envelope and a decrypted payload. + * Inputs are not validated. Use `fromEnvelope` above. + */ + convenience init(envelope: EnvelopeJSON, payload: T) { + // TODO: modified, sortindex, ttl + self.init(id: envelope.id, payload: payload, modified: envelope.modified, sortindex: envelope.sortindex) + } + + init(id: GUID, payload: T, modified: Timestamp = Timestamp(time(nil)), sortindex: Int = 0, ttl: Int? = nil) { + self.id = id + + self.payload = payload + + self.modified = modified + self.sortindex = sortindex + self.ttl = ttl + } + + func equalIdentifiers(_ rec: Record) -> Bool { + return rec.id == self.id + } + + // Override me. + func equalPayloads(_ rec: Record) -> Bool { + return equalIdentifiers(rec) && rec.payload.deleted == self.payload.deleted + } + + func equals(_ rec: Record) -> Bool { + return rec.sortindex == self.sortindex && + rec.modified == self.modified && + equalPayloads(rec) + } +} diff --git a/mobile/ios/Sync/RequestExtensions.swift b/mobile/ios/Sync/RequestExtensions.swift new file mode 100644 index 0000000000..f890f2c588 --- /dev/null +++ b/mobile/ios/Sync/RequestExtensions.swift @@ -0,0 +1,72 @@ +/* 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 Alamofire +import Shared +import SwiftyJSON + +extension DataRequest { + public func responsePartialParsedJSON(_ completionHandler: @escaping (DataResponse) -> Void) -> Self { + return response(responseSerializer: parsedJSONResponseSerializer(), completionHandler: completionHandler) + } + + public func responsePartialParsedJSON(queue: DispatchQueue, completionHandler: @escaping (DataResponse) -> Void) -> Self { + return response(queue: queue, responseSerializer: partialParsedJSONResponseSerializer(), completionHandler: completionHandler) + } + + public func responseParsedJSON(_ partial: Bool, completionHandler: @escaping (DataResponse) -> Void) -> Self { + return response(responseSerializer: parsedJSONResponseSerializer(), completionHandler: completionHandler) + } + + public func responseParsedJSON(queue: DispatchQueue, completionHandler: @escaping (DataResponse) -> Void) -> Self { + return response(queue: queue, responseSerializer: parsedJSONResponseSerializer(), completionHandler: completionHandler) + } +} + +public enum JSONSerializeError: Error { + case noData + case parseError +} + +private func parsedJSONResponseSerializer() -> DataResponseSerializer { + return DataResponseSerializer() { (request, response, data, error) -> Alamofire.Result in + guard let data = data, !data.isEmpty else { + return .failure(JSONSerializeError.noData) + } + + let json = JSON(data: data) + if json.isError() { + return .failure(JSONSerializeError.parseError) + } + + return .success(json) + } +} + +private func partialParsedJSONResponseSerializer() -> DataResponseSerializer { + return DataResponseSerializer() { (request, response, data, error) -> Alamofire.Result in + guard let data = data, !data.isEmpty else { + return .failure(JSONSerializeError.noData) + } + + let o: Any? + do { + try o = JSONSerialization.jsonObject(with: data, options: .allowFragments) + } catch { + return .failure(JSONSerializeError.parseError) + } + + guard let object = o else { + return .failure(JSONSerializeError.noData) + } + + let json = JSON(object) + if json.isError() { + return .failure(JSONSerializeError.parseError) + } + + return .success(json) + } +} diff --git a/mobile/ios/Sync/State.swift b/mobile/ios/Sync/State.swift new file mode 100644 index 0000000000..4373eb55cd --- /dev/null +++ b/mobile/ios/Sync/State.swift @@ -0,0 +1,579 @@ +/* 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 XCGLogger +import SwiftKeychainWrapper +import SwiftyJSON + +private let log = Logger.syncLogger + +/* + * This file includes types that manage intra-sync and inter-sync metadata + * for the use of synchronizers and the state machine. + * + * See docs/sync.md for details on what exactly we need to persist. + */ + +public struct Fetched: Equatable { + let value: T + let timestamp: Timestamp +} + +public func ==(lhs: Fetched, rhs: Fetched) -> Bool { + return lhs.timestamp == rhs.timestamp && + lhs.value == rhs.value +} + +public enum LocalCommand: CustomStringConvertible, Hashable { + // We've seen something (a blank server, a changed global sync ID, a + // crypto/keys with a different meta/global) that requires us to reset all + // local engine timestamps (save the ones listed) and possibly re-upload. + case resetAllEngines(except: Set) + + // We've seen something (a changed engine sync ID, a crypto/keys with a + // different per-engine bulk key) that requires us to reset our local engine + // timestamp and possibly re-upload. + case resetEngine(engine: String) + + // We've seen a change in meta/global: an engine has come or gone. + case enableEngine(engine: String) + case disableEngine(engine: String) + + public func toJSON() -> JSON { + switch self { + case let .resetAllEngines(except): + return JSON(["type": "ResetAllEngines", "except": Array(except).sorted()]) + + case let .resetEngine(engine): + return JSON(["type": "ResetEngine", "engine": engine]) + + case let .enableEngine(engine): + return JSON(["type": "EnableEngine", "engine": engine]) + + case let .disableEngine(engine): + return JSON(["type": "DisableEngine", "engine": engine]) + } + } + + public static func fromJSON(_ json: JSON) -> LocalCommand? { + if json.isError() { + return nil + } + guard let type = json["type"].string else { + return nil + } + switch type { + case "ResetAllEngines": + if let except = json["except"].array, except.every({$0.isString()}) { + return .resetAllEngines(except: Set(except.map({$0.stringValue}))) + } + return nil + case "ResetEngine": + if let engine = json["engine"].string { + return .resetEngine(engine: engine) + } + return nil + case "EnableEngine": + if let engine = json["engine"].string { + return .enableEngine(engine: engine) + } + return nil + case "DisableEngine": + if let engine = json["engine"].string { + return .disableEngine(engine: engine) + } + return nil + default: + return nil + } + } + + public var description: String { + return self.toJSON().description + } + + public var hashValue: Int { + return self.description.hashValue + } +} + +public func ==(lhs: LocalCommand, rhs: LocalCommand) -> Bool { + switch (lhs, rhs) { + case (let .resetAllEngines(exceptL), let .resetAllEngines(exceptR)): + return exceptL == exceptR + + case (let .resetEngine(engineL), let .resetEngine(engineR)): + return engineL == engineR + + case (let .enableEngine(engineL), let .enableEngine(engineR)): + return engineL == engineR + + case (let .disableEngine(engineL), let .disableEngine(engineR)): + return engineL == engineR + + default: + return false + } +} + +/* + * Persistence pref names. + * Note that syncKeyBundle isn't persisted by us. + * + * Note also that fetched keys aren't kept in prefs: we keep the timestamp ("PrefKeysTS"), + * and we keep a 'label'. This label is used to find the real fetched keys in the Keychain. + */ + +private let PrefVersion = "_v" +private let PrefGlobal = "global" +private let PrefGlobalTS = "globalTS" +private let PrefKeyLabel = "keyLabel" +private let PrefKeysTS = "keysTS" +private let PrefLastFetched = "lastFetched" +private let PrefLocalCommands = "localCommands" +private let PrefClientName = "clientName" +private let PrefClientGUID = "clientGUID" +private let PrefHashedUID = "hashedUID" +private let PrefEngineConfiguration = "engineConfiguration" +private let PrefDeviceID = "deviceID" + +class PrefsBackoffStorage: BackoffStorage { + let prefs: Prefs + fileprivate let key = "timestamp" + + init(prefs: Prefs) { + self.prefs = prefs + } + + var serverBackoffUntilLocalTimestamp: Timestamp? { + get { + return self.prefs.unsignedLongForKey(self.key) + } + + set(value) { + if let value = value { + self.prefs.setLong(value, forKey: self.key) + } else { + self.prefs.removeObjectForKey(self.key) + } + } + } + + func clearServerBackoff() { + self.prefs.removeObjectForKey(self.key) + } + + func isInBackoff(_ now: Timestamp) -> Timestamp? { + if let ts = self.serverBackoffUntilLocalTimestamp, now < ts { + return ts + } + return nil + } +} + +/** + * The scratchpad consists of the following: + * + * 1. Cached records. We cache meta/global and crypto/keys until they change. + * 2. Metadata like timestamps, both for cached records and for server fetches. + * 3. User preferences -- engine enablement. + * 4. Client record state. + * 5. Local commands that have yet to be processed. + * + * Note that the scratchpad itself is immutable, but is a class passed by reference. + * Its mutable fields can be mutated, but you can't accidentally e.g., switch out + * meta/global and get confused. + * + * TODO: the Scratchpad needs to be loaded from persistent storage, and written + * back at certain points in the state machine (after a replayable action is taken). + */ +open class Scratchpad { + open class Builder { + var syncKeyBundle: KeyBundle // For the love of god, if you change this, invalidate keys, too! + fileprivate var global: Fetched? + fileprivate var keys: Fetched? + fileprivate var keyLabel: String + var localCommands: Set + var engineConfiguration: EngineConfiguration? + var clientGUID: String + var clientName: String + var fxaDeviceId: String + var hashedUID: String? + var prefs: Prefs + + init(p: Scratchpad) { + self.syncKeyBundle = p.syncKeyBundle + self.prefs = p.prefs + + self.global = p.global + + self.keys = p.keys + self.keyLabel = p.keyLabel + self.localCommands = p.localCommands + self.engineConfiguration = p.engineConfiguration + self.clientGUID = p.clientGUID + self.clientName = p.clientName + self.fxaDeviceId = p.fxaDeviceId + self.hashedUID = p.hashedUID + } + + open func clearLocalCommands() -> Builder { + self.localCommands.removeAll() + return self + } + + open func addLocalCommandsFromKeys(_ keys: Fetched?) -> Builder { + // Getting new keys can force local collection resets. + guard let freshKeys = keys?.value, let staleKeys = self.keys?.value, staleKeys.valid else { + // Removing keys, or new keys and either we didn't have old keys or they weren't valid. Everybody gets a reset! + self.localCommands.insert(LocalCommand.resetAllEngines(except: [])) + return self + } + + // New keys, and we have valid old keys. + if freshKeys.defaultBundle != staleKeys.defaultBundle { + // Default bundle has changed. Reset everything but collections that have unchanged bulk keys. + var except: Set = Set() + // Symmetric difference, like an animal. Swift doesn't allow Hashable tuples; don't fight it. + for (collection, keyBundle) in staleKeys.collectionKeys { + if keyBundle == freshKeys.forCollection(collection) { + except.insert(collection) + } + } + for (collection, keyBundle) in freshKeys.collectionKeys { + if keyBundle == staleKeys.forCollection(collection) { + except.insert(collection) + } + } + self.localCommands.insert(.resetAllEngines(except: except)) + } else { + // Default bundle is the same. Reset collections that have changed bulk keys. + for (collection, keyBundle) in staleKeys.collectionKeys { + if keyBundle != freshKeys.forCollection(collection) { + self.localCommands.insert(.resetEngine(engine: collection)) + } + } + for (collection, keyBundle) in freshKeys.collectionKeys { + if keyBundle != staleKeys.forCollection(collection) { + self.localCommands.insert(.resetEngine(engine: collection)) + } + } + } + return self + } + + open func setKeys(_ keys: Fetched?) -> Builder { + self.keys = keys + return self + } + + open func setGlobal(_ global: Fetched?) -> Builder { + self.global = global + if let global = global { + // We always take the incoming meta/global's engine configuration. + self.engineConfiguration = global.value.engineConfiguration() + } + return self + } + + open func setEngineConfiguration(_ engineConfiguration: EngineConfiguration?) -> Builder { + self.engineConfiguration = engineConfiguration + return self + } + + open func build() -> Scratchpad { + return Scratchpad( + b: self.syncKeyBundle, + m: self.global, + k: self.keys, + keyLabel: self.keyLabel, + localCommands: self.localCommands, + engines: self.engineConfiguration, + clientGUID: self.clientGUID, + clientName: self.clientName, + fxaDeviceId: self.fxaDeviceId, + hashedUID: self.hashedUID, + persistingTo: self.prefs + ) + } + } + + open lazy var backoffStorage: BackoffStorage = { + return PrefsBackoffStorage(prefs: self.prefs.branch("backoff.storage")) + }() + + open func evolve() -> Scratchpad.Builder { + return Scratchpad.Builder(p: self) + } + + // This is never persisted. + let syncKeyBundle: KeyBundle + + // Cached records. + // This cached meta/global is what we use to add or remove enabled engines. See also + // engineConfiguration, below. + // We also use it to detect when meta/global hasn't changed -- compare timestamps. + // + // Note that a Scratchpad held by a Ready state will have the current server meta/global + // here. That means we don't need to track syncIDs separately (which is how desktop and + // Android are implemented). + // If we don't have a meta/global, and thus we don't know syncIDs, it means we haven't + // synced with this server before, and we'll do a fresh sync. + let global: Fetched? + + // We don't store these keys (so-called "collection keys" or "bulk keys") in Prefs. + // Instead, we store a label, which is seeded when you first create a Scratchpad. + // This label is used to retrieve the real keys from your Keychain. + // + // Note that we also don't store the syncKeyBundle here. That's always created from kB, + // provided by the Firefox Account. + // + // Why don't we derive the label from your Sync Key? Firstly, we'd like to be able to + // clean up without having your key. Secondly, we don't want to accidentally load keys + // from the Keychain just because the Sync Key is the same -- e.g., after a node + // reassignment. Randomly generating a label offers all of the benefits with none of the + // problems, with only the cost of persisting that label alongside the rest of the state. + let keys: Fetched? + let keyLabel: String + + // Local commands. + var localCommands: Set + + // Enablement states. + let engineConfiguration: EngineConfiguration? + + // What's our client name? + let clientName: String + let clientGUID: String + let fxaDeviceId: String + let hashedUID: String? + + var hashedDeviceID: String? { + guard let hashedUID = hashedUID else { + return nil + } + return (fxaDeviceId + hashedUID).sha256.hexEncodedString + } + + // Where do we persist when told? + let prefs: Prefs + + init(b: KeyBundle, + m: Fetched?, + k: Fetched?, + keyLabel: String, + localCommands: Set, + engines: EngineConfiguration?, + clientGUID: String, + clientName: String, + fxaDeviceId: String, + hashedUID: String?, + persistingTo prefs: Prefs + ) { + self.syncKeyBundle = b + self.prefs = prefs + + self.keys = k + self.keyLabel = keyLabel + self.global = m + self.engineConfiguration = engines + self.localCommands = localCommands + self.clientGUID = clientGUID + self.clientName = clientName + self.fxaDeviceId = fxaDeviceId + self.hashedUID = hashedUID + } + + // This should never be used in the end; we'll unpickle instead. + // This should be a convenience initializer, but... Swift compiler bug? + init(b: KeyBundle, persistingTo prefs: Prefs) { + self.syncKeyBundle = b + self.prefs = prefs + + self.keys = nil + self.keyLabel = Bytes.generateGUID() + self.global = nil + self.engineConfiguration = nil + self.localCommands = Set() + self.clientGUID = Bytes.generateGUID() + self.clientName = DeviceInfo.defaultClientName() + + self.fxaDeviceId = "unknown_fxaDeviceId" + + self.hashedUID = nil + } + + func freshStartWithGlobal(_ global: Fetched) -> Scratchpad { + // TODO: I *think* a new keyLabel is unnecessary. + return self.evolve() + .setGlobal(global) + .addLocalCommandsFromKeys(nil) + .setKeys(nil) + .build() + } + + fileprivate class func unpickleV1FromPrefs(_ prefs: Prefs, syncKeyBundle: KeyBundle) -> Scratchpad { + let b = Scratchpad(b: syncKeyBundle, persistingTo: prefs).evolve() + + if let mg = prefs.stringForKey(PrefGlobal) { + if let mgTS = prefs.unsignedLongForKey(PrefGlobalTS) { + if let global = MetaGlobal.fromJSON(JSON(parseJSON: mg)) { + _ = b.setGlobal(Fetched(value: global, timestamp: mgTS)) + } else { + log.error("Malformed meta/global in prefs. Ignoring.") + } + } else { + // This should never happen. + log.error("Found global in prefs, but not globalTS!") + } + } + + if let keyLabel = prefs.stringForKey(PrefKeyLabel) { + b.keyLabel = keyLabel + if let ckTS = prefs.unsignedLongForKey(PrefKeysTS) { + let key = "keys." + keyLabel + KeychainWrapper.sharedAppContainerKeychain.ensureStringItemAccessibility(.afterFirstUnlock, forKey: key) + if let keys = KeychainWrapper.sharedAppContainerKeychain.string(forKey: key) { + // We serialize as JSON. + let keys = Keys(payload: KeysPayload(keys)) + if keys.valid { + log.debug("Read keys from Keychain with label \(keyLabel).") + _ = b.setKeys(Fetched(value: keys, timestamp: ckTS)) + } else { + log.error("Invalid keys extracted from Keychain. Discarding.") + } + } else { + log.error("Found keysTS in prefs, but didn't find keys in Keychain!") + } + } + } + + b.clientGUID = prefs.stringForKey(PrefClientGUID) ?? { + log.error("No value found in prefs for client GUID! Generating one.") + return Bytes.generateGUID() + }() + + b.clientName = prefs.stringForKey(PrefClientName) ?? { + log.error("No value found in prefs for client name! Using default.") + return DeviceInfo.defaultClientName() + }() + + b.hashedUID = prefs.stringForKey(PrefHashedUID) + + b.fxaDeviceId = prefs.stringForKey(PrefDeviceID) ?? { + // Migrate from previous way of storing device id. + // This code will only be run once – the id will be stored + // in PrefDeviceID. + let PrefDeviceRegistration = "deviceRegistration" + if let string = prefs.stringForKey(PrefDeviceRegistration) { + let json = JSON(parseJSON: string) + if let id = json["id"].string { + return id + } + prefs.removeObjectForKey(PrefDeviceRegistration) + } + // This is run the first time we sync with a new account. + // It will be replaced by a real fxaDeviceId, from account.deviceRegistration?.id. + log.warning("No value found in prefs for fxaDeviceId! Will overwrite on first sync") + return "unknown_fxaDeviceId" + }() + + if let localCommands: [String] = prefs.stringArrayForKey(PrefLocalCommands) { + b.localCommands = Set(localCommands.flatMap({LocalCommand.fromJSON(JSON(parseJSON: $0))})) + } + + if let engineConfigurationString = prefs.stringForKey(PrefEngineConfiguration) { + if let engineConfiguration = EngineConfiguration.fromJSON(JSON(parseJSON: engineConfigurationString)) { + b.engineConfiguration = engineConfiguration + } else { + log.error("Invalid engineConfiguration found in prefs. Discarding.") + } + } + + return b.build() + } + + /** + * Remove anything that might be left around after prefs is wiped. + */ + open class func clearFromPrefs(_ prefs: Prefs) { + if let keyLabel = prefs.stringForKey(PrefKeyLabel) { + log.debug("Removing saved key from keychain.") + KeychainWrapper.sharedAppContainerKeychain.removeObject(forKey: keyLabel) + } else { + log.debug("No key label; nothing to remove from keychain.") + } + } + + open class func restoreFromPrefs(_ prefs: Prefs, syncKeyBundle: KeyBundle) -> Scratchpad? { + if let ver = prefs.intForKey(PrefVersion) { + switch ver { + case 1: + return unpickleV1FromPrefs(prefs, syncKeyBundle: syncKeyBundle) + default: + return nil + } + } + + log.debug("No scratchpad found in prefs.") + return nil + } + + /** + * Persist our current state to our origin prefs. + * Note that calling this from multiple threads with either mutated or evolved + * scratchpads will cause sadness — individual writes are thread-safe, but the + * overall pseudo-transaction is not atomic. + */ + open func checkpoint() -> Scratchpad { + return pickle(self.prefs) + } + + func pickle(_ prefs: Prefs) -> Scratchpad { + prefs.setInt(1, forKey: PrefVersion) + if let global = global { + prefs.setLong(global.timestamp, forKey: PrefGlobalTS) + prefs.setString(global.value.asPayload().json.stringValue()!, forKey: PrefGlobal) + } else { + prefs.removeObjectForKey(PrefGlobal) + prefs.removeObjectForKey(PrefGlobalTS) + } + + // We store the meat of your keys in the Keychain, using a random identifier that we persist in prefs. + prefs.setString(self.keyLabel, forKey: PrefKeyLabel) + if let keys = self.keys, + let payload = keys.value.asPayload().json.stringValue() { + let label = "keys." + self.keyLabel + log.debug("Storing keys in Keychain with label \(label).") + prefs.setString(self.keyLabel, forKey: PrefKeyLabel) + prefs.setLong(keys.timestamp, forKey: PrefKeysTS) + KeychainWrapper.sharedAppContainerKeychain.set(payload, forKey: label, withAccessibility: .afterFirstUnlock) + } else { + log.debug("Removing keys from Keychain.") + KeychainWrapper.sharedAppContainerKeychain.removeObject(forKey: self.keyLabel) + } + + prefs.setString(clientName, forKey: PrefClientName) + prefs.setString(clientGUID, forKey: PrefClientGUID) + + if let uid = hashedUID { + prefs.setString(uid, forKey: PrefHashedUID) + } + + prefs.setString(fxaDeviceId, forKey: PrefDeviceID) + + let localCommands: [String] = Array(self.localCommands).map({$0.toJSON().stringValue()!}) + prefs.setObject(localCommands, forKey: PrefLocalCommands) + + if let engineConfiguration = self.engineConfiguration { + prefs.setString(engineConfiguration.toJSON().stringValue()!, forKey: PrefEngineConfiguration) + } else { + prefs.removeObjectForKey(PrefEngineConfiguration) + } + + return self + } +} diff --git a/mobile/ios/Sync/StorageClient.swift b/mobile/ios/Sync/StorageClient.swift new file mode 100644 index 0000000000..e8f80d4d4f --- /dev/null +++ b/mobile/ios/Sync/StorageClient.swift @@ -0,0 +1,820 @@ +/* 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 Alamofire +import Shared +import Account +import XCGLogger +import Deferred +import SwiftyJSON + +private let log = Logger.syncLogger + +// Not an error that indicates a server problem, but merely an +// error that encloses a StorageResponse. +open class StorageResponseError: MaybeErrorType, SyncPingFailureFormattable { + open let response: StorageResponse + + open var failureReasonName: SyncPingFailureReasonName { + return .httpError + } + + public init(_ response: StorageResponse) { + self.response = response + } + + open var description: String { + return "Error." + } +} + +open class RequestError: MaybeErrorType, SyncPingFailureFormattable { + open var failureReasonName: SyncPingFailureReasonName { + return .httpError + } + + open var description: String { + return "Request error." + } +} + +open class BadRequestError: StorageResponseError { + open let request: URLRequest? + + public init(request: URLRequest?, response: StorageResponse) { + self.request = request + super.init(response) + } + + override open var description: String { + return "Bad request." + } +} + +open class ServerError: StorageResponseError { + override open var description: String { + return "Server error." + } + + override public init(_ response: StorageResponse) { + super.init(response) + } +} + +open class NotFound: StorageResponseError { + override open var description: String { + return "Not found. (\(T.self))" + } + + override public init(_ response: StorageResponse) { + super.init(response) + } +} + +open class RecordParseError: MaybeErrorType, SyncPingFailureFormattable { + open var description: String { + return "Failed to parse record." + } + + open var failureReasonName: SyncPingFailureReasonName { + return .otherError + } +} + +open class MalformedMetaGlobalError: MaybeErrorType, SyncPingFailureFormattable { + open var description: String { + return "Supplied meta/global for upload did not serialize to valid JSON." + } + + open var failureReasonName: SyncPingFailureReasonName { + return .otherError + } +} + +open class RecordTooLargeError: MaybeErrorType, SyncPingFailureFormattable { + open let guid: GUID + open let size: ByteCount + + open var failureReasonName: SyncPingFailureReasonName { + return .otherError + } + + public init(size: ByteCount, guid: GUID) { + self.size = size + self.guid = guid + } + + open var description: String { + return "Record \(self.guid) too large: \(size) bytes." + } +} + +/** + * Raised when the storage client is refusing to make a request due to a known + * server backoff. + * If you want to bypass this, remove the backoff from the BackoffStorage that + * the storage client is using. + */ +open class ServerInBackoffError: MaybeErrorType, SyncPingFailureFormattable { + fileprivate let until: Timestamp + + open var failureReasonName: SyncPingFailureReasonName { + return .otherError + } + + open var description: String { + let formatter = DateFormatter() + formatter.dateStyle = DateFormatter.Style.short + formatter.timeStyle = DateFormatter.Style.medium + let s = formatter.string(from: Date.fromTimestamp(self.until)) + return "Server in backoff until \(s)." + } + + public init(until: Timestamp) { + self.until = until + } +} + +// Returns milliseconds. Handles decimals. +private func optionalSecondsHeader(_ input: AnyObject?) -> Timestamp? { + if input == nil { + return nil + } + + if let val = input as? String { + if let timestamp = decimalSecondsStringToTimestamp(val) { + return timestamp + } + } + + if let seconds: Double = input as? Double { + // Oh for a BigDecimal library. + return Timestamp(seconds * 1000) + } + + if let seconds: NSNumber = input as? NSNumber { + // Who knows. + return seconds.uint64Value * 1000 + } + + return nil +} + +private func optionalIntegerHeader(_ input: AnyObject?) -> Int64? { + if input == nil { + return nil + } + + if let val = input as? String { + return Scanner(string: val).scanLongLong() + } + + if let val: Double = input as? Double { + // Oh for a BigDecimal library. + return Int64(val) + } + + if let val: NSNumber = input as? NSNumber { + // Who knows. + return val.int64Value + } + + return nil +} + +private func optionalUIntegerHeader(_ input: AnyObject?) -> Timestamp? { + if input == nil { + return nil + } + + if let val = input as? String { + return Scanner(string: val).scanUnsignedLongLong() + } + + if let val: Double = input as? Double { + // Oh for a BigDecimal library. + return Timestamp(val) + } + + if let val: NSNumber = input as? NSNumber { + // Who knows. + return val.uint64Value + } + + return nil +} + +public enum SortOption: String { + case NewestFirst = "newest" + case OldestFirst = "oldest" + case Index = "index" +} + +public struct ResponseMetadata { + public let status: Int + public let alert: String? + public let nextOffset: String? + public let records: UInt64? + public let quotaRemaining: Int64? + public let timestampMilliseconds: Timestamp // Non-optional. Server timestamp when handling request. + public let lastModifiedMilliseconds: Timestamp? // Included for all success responses. Collection or record timestamp. + public let backoffMilliseconds: UInt64? + public let retryAfterMilliseconds: UInt64? + + public init(response: HTTPURLResponse) { + self.init(status: response.statusCode, headers: response.allHeaderFields) + } + + init(status: Int, headers: [AnyHashable: Any]) { + self.status = status + alert = headers["X-Weave-Alert"] as? String + nextOffset = headers["X-Weave-Next-Offset"] as? String + records = optionalUIntegerHeader(headers["X-Weave-Records"] as AnyObject?) + quotaRemaining = optionalIntegerHeader(headers["X-Weave-Quota-Remaining"] as AnyObject?) + timestampMilliseconds = optionalSecondsHeader(headers["X-Weave-Timestamp"] as AnyObject?) ?? 0 + lastModifiedMilliseconds = optionalSecondsHeader(headers["X-Last-Modified"] as AnyObject?) + backoffMilliseconds = optionalSecondsHeader(headers["X-Weave-Backoff"] as AnyObject?) ?? + optionalSecondsHeader(headers["X-Backoff"] as AnyObject?) + retryAfterMilliseconds = optionalSecondsHeader(headers["Retry-After"] as AnyObject?) + } +} + +public struct StorageResponse { + public let value: T + public let metadata: ResponseMetadata + + init(value: T, metadata: ResponseMetadata) { + self.value = value + self.metadata = metadata + } + + init(value: T, response: HTTPURLResponse) { + self.value = value + self.metadata = ResponseMetadata(response: response) + } +} + +public typealias BatchToken = String + +public typealias ByteCount = Int + +public struct POSTResult { + public let success: [GUID] + public let failed: [GUID: String] + public let batchToken: BatchToken? + + public init(success: [GUID], failed: [GUID: String], batchToken: BatchToken? = nil) { + self.success = success + self.failed = failed + self.batchToken = batchToken + } + + public static func fromJSON(_ json: JSON) -> POSTResult? { + if json.isError() { + return nil + } + + let batchToken = json["batch"].string + + if let s = json["success"].array, + let f = json["failed"].dictionary { + var failed = false + let stringOrFail: (JSON) -> String = { $0.string ?? { failed = true; return "" }() } + + // That's the basic structure. Now let's transform the contents. + let successGUIDs = s.map(stringOrFail) + if failed { + return nil + } + let failedGUIDs = mapValues(f, f: stringOrFail) + if failed { + return nil + } + return POSTResult(success: successGUIDs, failed: failedGUIDs, batchToken: batchToken) + } + return nil + } +} + +public typealias Authorizer = (URLRequest) -> URLRequest + +// TODO: don't be so naïve. Use a combination of uptime and wall clock time. +public protocol BackoffStorage { + var serverBackoffUntilLocalTimestamp: Timestamp? { get set } + func clearServerBackoff() + func isInBackoff(_ now: Timestamp) -> Timestamp? // Returns 'until' for convenience. +} + +// Don't forget to batch downloads. +open class Sync15StorageClient { + fileprivate let authorizer: Authorizer + fileprivate let serverURI: URL + + open static let maxRecordSizeBytes: Int = 262_140 // A shade under 256KB. + open static let maxPayloadSizeBytes: Int = 1_000_000 // A shade under 1MB. + open static let maxPayloadItemCount: Int = 100 // Bug 1250747 will raise this. + + var backoff: BackoffStorage + + let workQueue: DispatchQueue + let resultQueue: DispatchQueue + + public init(token: TokenServerToken, workQueue: DispatchQueue, resultQueue: DispatchQueue, backoff: BackoffStorage) { + self.workQueue = workQueue + self.resultQueue = resultQueue + self.backoff = backoff + + // This is a potentially dangerous assumption, but failable initializers up the stack are a giant pain. + // We want the serverURI to *not* have a trailing slash: to efficiently wipe a user's storage, we delete + // the user root (like /1.5/1234567) and not an "empty collection" (like /1.5/1234567/); the storage + // server treats the first like a DROP table and the latter like a DELETE *, and the former is more + // efficient than the latter. + self.serverURI = URL(string: token.api_endpoint.endsWith("/") + ? token.api_endpoint.substring(to: token.api_endpoint.index(before: token.api_endpoint.endIndex)) + : token.api_endpoint)! + self.authorizer = { + (r: URLRequest) -> URLRequest in + var req = r + let helper = HawkHelper(id: token.id, key: token.key.data(using: String.Encoding.utf8, allowLossyConversion: false)!) + req.setValue(helper.getAuthorizationValueFor(r), forHTTPHeaderField: "Authorization") + return req + } + } + + public init(serverURI: URL, authorizer: @escaping Authorizer, workQueue: DispatchQueue, resultQueue: DispatchQueue, backoff: BackoffStorage) { + self.serverURI = serverURI + self.authorizer = authorizer + self.workQueue = workQueue + self.resultQueue = resultQueue + self.backoff = backoff + } + + func updateBackoffFromResponse(_ response: StorageResponse) { + // N.B., we would not have made this request if a backoff were set, so + // we can safely avoid doing the write if there's no backoff in the + // response. + // This logic will have to change if we ever invalidate that assumption. + if let ms = response.metadata.backoffMilliseconds ?? response.metadata.retryAfterMilliseconds { + log.info("Backing off for \(ms)ms.") + self.backoff.serverBackoffUntilLocalTimestamp = ms + Date.now() + } + } + + func errorWrap(_ deferred: Deferred>, handler: @escaping (DataResponse) -> Void) -> (DataResponse) -> Void { + return { response in + log.verbose("Response is \(response.response ??? "nil").") + + /** + * Returns true if handled. + */ + func failFromResponse(_ HTTPResponse: HTTPURLResponse?) -> Bool { + guard let HTTPResponse = HTTPResponse else { + // TODO: better error. + log.error("No response") + let result = Maybe(failure: RecordParseError()) + deferred.fill(result) + return true + } + + log.debug("Status code: \(HTTPResponse.statusCode).") + + let storageResponse = StorageResponse(value: HTTPResponse, metadata: ResponseMetadata(response: HTTPResponse)) + + self.updateBackoffFromResponse(storageResponse) + + if HTTPResponse.statusCode >= 500 { + log.debug("ServerError.") + let result = Maybe(failure: ServerError(storageResponse)) + deferred.fill(result) + return true + } + + if HTTPResponse.statusCode == 404 { + log.debug("NotFound<\(T.self)>.") + let result = Maybe(failure: NotFound(storageResponse)) + deferred.fill(result) + return true + } + + if HTTPResponse.statusCode >= 400 { + log.debug("BadRequestError.") + let result = Maybe(failure: BadRequestError(request: response.request, response: storageResponse)) + deferred.fill(result) + return true + } + + return false + } + + // Check for an error from the request processor. + if response.result.isFailure { + log.error("Response: \(response.response?.statusCode ?? 0). Got error \(response.result.error ??? "nil").") + + // If we got one, we don't want to hit the response nil case above and + // return a RecordParseError, because a RequestError is more fitting. + if let response = response.response { + if failFromResponse(response) { + log.error("This was a failure response. Filled specific error type.") + return + } + } + + log.error("Filling generic RequestError.") + deferred.fill(Maybe(failure: RequestError())) + return + } + + if failFromResponse(response.response) { + return + } + + handler(response) + } + } + + lazy fileprivate var alamofire: SessionManager = { + let ua = UserAgent.syncUserAgent + let configuration = URLSessionConfiguration.ephemeral + var defaultHeaders = SessionManager.default.session.configuration.httpAdditionalHeaders ?? [:] + defaultHeaders["User-Agent"] = ua + configuration.httpAdditionalHeaders = defaultHeaders + return SessionManager(configuration: configuration) + }() + + func requestGET(_ url: URL) -> DataRequest { + var req = URLRequest(url: url as URL) + req.httpMethod = URLRequest.Method.get.rawValue + req.setValue("application/json", forHTTPHeaderField: "Accept") + let authorized: URLRequest = self.authorizer(req) + return alamofire.request(authorized) + .validate(contentType: ["application/json"]) + } + + func requestDELETE(_ url: URL) -> DataRequest { + var req = URLRequest(url: url as URL) + req.httpMethod = URLRequest.Method.delete.rawValue + req.setValue("1", forHTTPHeaderField: "X-Confirm-Delete") + let authorized: URLRequest = self.authorizer(req) + return alamofire.request(authorized) + } + + func requestWrite(_ url: URL, method: String, body: String, contentType: String, ifUnmodifiedSince: Timestamp?) -> Request { + var req = URLRequest(url: url as URL) + req.httpMethod = method + req.setValue(contentType, forHTTPHeaderField: "Content-Type") + + if let ifUnmodifiedSince = ifUnmodifiedSince { + req.setValue(millisecondsToDecimalSeconds(ifUnmodifiedSince), forHTTPHeaderField: "X-If-Unmodified-Since") + } + + req.httpBody = body.data(using: String.Encoding.utf8)! + let authorized: URLRequest = self.authorizer(req) + return alamofire.request(authorized) + } + + func requestPUT(_ url: URL, body: JSON, ifUnmodifiedSince: Timestamp?) -> Request { + return self.requestWrite(url, method: URLRequest.Method.put.rawValue, body: body.stringValue()!, contentType: "application/json;charset=utf-8", ifUnmodifiedSince: ifUnmodifiedSince) + } + + func requestPOST(_ url: URL, body: JSON, ifUnmodifiedSince: Timestamp?) -> Request { + return self.requestWrite(url, method: URLRequest.Method.post.rawValue, body: body.stringValue()!, contentType: "application/json;charset=utf-8", ifUnmodifiedSince: ifUnmodifiedSince) + } + + func requestPOST(_ url: URL, body: [String], ifUnmodifiedSince: Timestamp?) -> Request { + let content = body.joined(separator: "\n") + return self.requestWrite(url, method: URLRequest.Method.post.rawValue, body: content, contentType: "application/newlines", ifUnmodifiedSince: ifUnmodifiedSince) + } + + func requestPOST(_ url: URL, body: [JSON], ifUnmodifiedSince: Timestamp?) -> Request { + return self.requestPOST(url, body: body.map { $0.stringValue()! }, ifUnmodifiedSince: ifUnmodifiedSince) + } + + /** + * Returns true and fills the provided Deferred if our state shows that we're in backoff. + * Returns false otherwise. + */ + fileprivate func checkBackoff(_ deferred: Deferred>) -> Bool { + if let until = self.backoff.isInBackoff(Date.now()) { + deferred.fill(Maybe(failure: ServerInBackoffError(until: until))) + return true + } + return false + } + + fileprivate func doOp(_ op: (URL) -> DataRequest, path: String, f: @escaping (JSON) -> T?) -> Deferred>> { + + let deferred = Deferred>>(defaultQueue: self.resultQueue) + + if self.checkBackoff(deferred) { + return deferred + } + + // Special case "": we want /1.5/1234567 and not /1.5/1234567/. See note about trailing slashes above. + let url: URL + if path == "" { + url = self.serverURI // No trailing slash. + } else { + url = self.serverURI.appendingPathComponent(path) + + } + + let req = op(url) + let handler = self.errorWrap(deferred) { (response: DataResponse) in + if let json: JSON = response.result.value { + if let v = f(json) { + let storageResponse = StorageResponse(value: v, response: response.response!) + deferred.fill(Maybe(success: storageResponse)) + } else { + deferred.fill(Maybe(failure: RecordParseError())) + } + return + } + + deferred.fill(Maybe(failure: RecordParseError())) + } + + _ = req.responseParsedJSON(true, completionHandler: handler) + return deferred + } + + // Sync storage responds with a plain timestamp to a PUT, not with a JSON body. + fileprivate func putResource(_ path: String, body: JSON, ifUnmodifiedSince: Timestamp?, parser: @escaping (String) -> T?) -> Deferred>> { + let url = self.serverURI.appendingPathComponent(path) + return self.putResource(url, body: body, ifUnmodifiedSince: ifUnmodifiedSince, parser: parser) + } + + fileprivate func putResource(_ URL: Foundation.URL, body: JSON, ifUnmodifiedSince: Timestamp?, parser: @escaping (String) -> T?) -> Deferred>> { + + let deferred = Deferred>>(defaultQueue: self.resultQueue) + if self.checkBackoff(deferred) { + return deferred + } + + let req = self.requestPUT(URL, body: body, ifUnmodifiedSince: ifUnmodifiedSince) as! DataRequest + let handler = self.errorWrap(deferred) { (response: DataResponse) in + if let data = response.result.value { + if let v = parser(data) { + let storageResponse = StorageResponse(value: v, response: response.response!) + deferred.fill(Maybe(success: storageResponse)) + } else { + deferred.fill(Maybe(failure: RecordParseError())) + } + return + } + deferred.fill(Maybe(failure: RecordParseError())) + } + req.responseString(completionHandler: handler) + return deferred + } + + fileprivate func getResource(_ path: String, f: @escaping (JSON) -> T?) -> Deferred>> { + return doOp(self.requestGET, path: path, f: f) + } + + fileprivate func deleteResource(_ path: String, f: @escaping (JSON) -> T?) -> Deferred>> { + return doOp(self.requestDELETE, path: path, f: f) + } + + func wipeStorage() -> Deferred>> { + // In Sync 1.5 it's preferred that we delete the root, not /storage. + return deleteResource("", f: { $0 }) + } + + func getInfoCollections() -> Deferred>> { + return getResource("info/collections", f: InfoCollections.fromJSON) + } + + func getMetaGlobal() -> Deferred>> { + return getResource("storage/meta/global") { json in + // We have an envelope. Parse the meta/global record embedded in the 'payload' string. + let envelope = EnvelopeJSON(json) + if envelope.isValid() { + return MetaGlobal.fromJSON(JSON(parseJSON: envelope.payload)) + } + return nil + } + } + + func getCryptoKeys(_ syncKeyBundle: KeyBundle, ifUnmodifiedSince: Timestamp?) -> Deferred>>> { + let syncKey = Keys(defaultBundle: syncKeyBundle) + let encoder = RecordEncoder(decode: { KeysPayload($0) }, encode: { $0.json }) + let encrypter = syncKey.encrypter("keys", encoder: encoder) + let client = self.clientForCollection("crypto", encrypter: encrypter) + return client.get("keys") + } + + func uploadMetaGlobal(_ metaGlobal: MetaGlobal, ifUnmodifiedSince: Timestamp?) -> Deferred>> { + let payload = metaGlobal.asPayload() + if payload.json.isError() { + return Deferred(value: Maybe(failure: MalformedMetaGlobalError())) + } + + let record: JSON = JSON(object: ["payload": payload.json.stringValue() ?? JSON.null as Any, "id": "global"]) + return putResource("storage/meta/global", body: record, ifUnmodifiedSince: ifUnmodifiedSince, parser: decimalSecondsStringToTimestamp) + } + + // The crypto/keys record is a special snowflake: it is encrypted with the Sync key bundle. All other records are + // encrypted with the bulk key bundle (including possibly a per-collection bulk key) stored in crypto/keys. + func uploadCryptoKeys(_ keys: Keys, withSyncKeyBundle syncKeyBundle: KeyBundle, ifUnmodifiedSince: Timestamp?) -> Deferred>> { + let syncKey = Keys(defaultBundle: syncKeyBundle) + let encoder = RecordEncoder(decode: { KeysPayload($0) }, encode: { $0.json }) + let encrypter = syncKey.encrypter("keys", encoder: encoder) + let client = self.clientForCollection("crypto", encrypter: encrypter) + + let record = Record(id: "keys", payload: keys.asPayload()) + return client.put(record, ifUnmodifiedSince: ifUnmodifiedSince) + } + + // It would be convenient to have the storage client manage Keys, but of course we need to use a different set of + // keys to fetch crypto/keys itself. See uploadCryptoKeys. + func clientForCollection(_ collection: String, encrypter: RecordEncrypter) -> Sync15CollectionClient { + let storage = self.serverURI.appendingPathComponent("storage", isDirectory: true) + return Sync15CollectionClient(client: self, serverURI: storage, collection: collection, encrypter: encrypter) + } +} + +private let DefaultInfoConfiguration = InfoConfiguration(maxRequestBytes: 1_048_576, + maxPostRecords: 100, + maxPostBytes: 1_048_576, + maxTotalRecords: 10_000, + maxTotalBytes: 104_857_600) + +/** + * We'd love to nest this in the overall storage client, but Swift + * forbids the nesting of a generic class inside another class. + */ +open class Sync15CollectionClient { + fileprivate let client: Sync15StorageClient + fileprivate let encrypter: RecordEncrypter + fileprivate let collectionURI: URL + fileprivate let collectionQueue = DispatchQueue(label: "com.mozilla.sync.collectionclient", attributes: []) + fileprivate let infoConfig = DefaultInfoConfiguration + + public init(client: Sync15StorageClient, serverURI: URL, collection: String, encrypter: RecordEncrypter) { + self.client = client + self.encrypter = encrypter + self.collectionURI = serverURI.appendingPathComponent(collection, isDirectory: false) + } + + var maxBatchPostRecords: Int { + get { + return infoConfig.maxPostRecords + } + } + + fileprivate func uriForRecord(_ guid: String) -> URL { + return self.collectionURI.appendingPathComponent(guid) + } + + open func newBatch(ifUnmodifiedSince: Timestamp? = nil, onCollectionUploaded: @escaping (POSTResult, Timestamp?) -> DeferredTimestamp) -> Sync15BatchClient { + return Sync15BatchClient(config: infoConfig, + ifUnmodifiedSince: ifUnmodifiedSince, + serializeRecord: self.serializeRecord, + uploader: self.post, + onCollectionUploaded: onCollectionUploaded) + } + + // Exposed so we can batch by size. + open func serializeRecord(_ record: Record) -> String? { + return self.encrypter.serializer(record)?.stringValue() + } + + open func post(_ lines: [String], ifUnmodifiedSince: Timestamp?, queryParams: [URLQueryItem]? = nil) -> Deferred>> { + let deferred = Deferred>>(defaultQueue: client.resultQueue) + + if self.client.checkBackoff(deferred) { + return deferred + } + + let requestURI: URL + if let queryParams = queryParams { + requestURI = self.collectionURI.withQueryParams(queryParams) + } else { + requestURI = self.collectionURI + } + + let req = client.requestPOST(requestURI, body: lines, ifUnmodifiedSince: ifUnmodifiedSince) as! DataRequest + _ = req.responsePartialParsedJSON(queue: collectionQueue, completionHandler: self.client.errorWrap(deferred) { (response: DataResponse) in + if let json: JSON = response.result.value, + let result = POSTResult.fromJSON(json) { + let storageResponse = StorageResponse(value: result, response: response.response!) + deferred.fill(Maybe(success: storageResponse)) + return + } else { + log.warning("Couldn't parse JSON response.") + } + deferred.fill(Maybe(failure: RecordParseError())) + }) + + return deferred + } + + open func post(_ records: [Record], ifUnmodifiedSince: Timestamp?, queryParams: [URLQueryItem]? = nil) -> Deferred>> { + // TODO: charset + // TODO: if any of these fail, we should do _something_. Right now we just ignore them. + let lines = optFilter(records.map(self.serializeRecord)) + return self.post(lines, ifUnmodifiedSince: ifUnmodifiedSince, queryParams: queryParams) + } + + open func put(_ record: Record, ifUnmodifiedSince: Timestamp?) -> Deferred>> { + if let body = self.encrypter.serializer(record) { + return self.client.putResource(uriForRecord(record.id), body: body, ifUnmodifiedSince: ifUnmodifiedSince, parser: decimalSecondsStringToTimestamp) + } + return deferMaybe(RecordParseError()) + } + + open func get(_ guid: String) -> Deferred>>> { + let deferred = Deferred>>>(defaultQueue: client.resultQueue) + + if self.client.checkBackoff(deferred) { + return deferred + } + + let req = client.requestGET(uriForRecord(guid)) + _ = req.responsePartialParsedJSON(queue: collectionQueue, completionHandler: self.client.errorWrap(deferred) { (response: DataResponse) in + + if let json: JSON = response.result.value { + let envelope = EnvelopeJSON(json) + let record = Record.fromEnvelope(envelope, payloadFactory: self.encrypter.factory) + if let record = record { + let storageResponse = StorageResponse(value: record, response: response.response!) + deferred.fill(Maybe(success: storageResponse)) + return + } + } else { + log.warning("Couldn't parse JSON response.") + } + + deferred.fill(Maybe(failure: RecordParseError())) + }) + + return deferred + } + + /** + * Unlike every other Sync client, we use the application/json format for fetching + * multiple requests. The others use application/newlines. We don't want to write + * another Serializer, and we're loading everything into memory anyway. + * + * It is the caller's responsibility to check whether the returned payloads are invalid. + * + * Only non-JSON and malformed envelopes will be dropped. + */ + open func getSince(_ since: Timestamp, sort: SortOption?=nil, limit: Int?=nil, offset: String?=nil) -> Deferred]>>> { + let deferred = Deferred]>>>(defaultQueue: client.resultQueue) + + // Fills the Deferred for us. + if self.client.checkBackoff(deferred) { + return deferred + } + + var params: [URLQueryItem] = [ + URLQueryItem(name: "full", value: "1"), + URLQueryItem(name: "newer", value: millisecondsToDecimalSeconds(since)), + ] + + if let offset = offset { + params.append(URLQueryItem(name: "offset", value: offset)) + } + + if let limit = limit { + params.append(URLQueryItem(name: "limit", value: "\(limit)")) + } + + if let sort = sort { + params.append(URLQueryItem(name: "sort", value: sort.rawValue)) + } + + log.debug("Issuing GET with newer = \(since), offset = \(offset ??? "nil"), sort = \(sort ??? "nil").") + let req = client.requestGET(self.collectionURI.withQueryParams(params)) + + _ = req.responsePartialParsedJSON(queue: collectionQueue, completionHandler: self.client.errorWrap(deferred) { (response: DataResponse) in + + log.verbose("Response is \(response).") + guard let json: JSON = response.result.value else { + log.warning("Non-JSON response.") + deferred.fill(Maybe(failure: RecordParseError())) + return + } + + guard let arr = json.array else { + log.warning("Non-array response.") + deferred.fill(Maybe(failure: RecordParseError())) + return + } + + func recordify(_ json: JSON) -> Record? { + let envelope = EnvelopeJSON(json) + return Record.fromEnvelope(envelope, payloadFactory: self.encrypter.factory) + } + + let records = arr.flatMap(recordify) + let response = StorageResponse(value: records, response: response.response!) + deferred.fill(Maybe(success: response)) + }) + + return deferred + } +} diff --git a/mobile/ios/Sync/Sync-Bridging-Header.h b/mobile/ios/Sync/Sync-Bridging-Header.h new file mode 100644 index 0000000000..d2517c4a1e --- /dev/null +++ b/mobile/ios/Sync/Sync-Bridging-Header.h @@ -0,0 +1,8 @@ +#ifndef Client_Sync_Bridging_Header_h +#define Client_Sync_Bridging_Header_h + +#import +#import "Shared-Bridging-Header.h" +#import "Storage-Bridging-Header.h" + +#endif diff --git a/mobile/ios/Sync/SyncConstants.swift b/mobile/ios/Sync/SyncConstants.swift new file mode 100644 index 0000000000..6c403aaa13 --- /dev/null +++ b/mobile/ios/Sync/SyncConstants.swift @@ -0,0 +1,12 @@ +/* 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 + +public struct SyncConstants { + // Suitable for use in dispatch_time(). + public static let SyncDelayTriggered: Int = 3000 + public static let SyncOnForegroundMinimumDelayMillis: UInt64 = 5 * 60 * 1000 + public static let SyncOnForegroundAfterMillis: Int64 = 5000 +} diff --git a/mobile/ios/Sync/SyncMeta.swift b/mobile/ios/Sync/SyncMeta.swift new file mode 100644 index 0000000000..b00063a77a --- /dev/null +++ b/mobile/ios/Sync/SyncMeta.swift @@ -0,0 +1,149 @@ +/* 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 SwiftyJSON + +// Our engine choices need to persist across server changes. +// Note that EngineConfiguration is not enough to evolve an existing meta/global: +// a meta/global generated from this will have different syncIDs and will +// always use this device's engine versions. +open class EngineConfiguration: Equatable { + let enabled: [String] + let declined: [String] + public init(enabled: [String], declined: [String]) { + self.enabled = enabled + self.declined = declined + } + + open class func fromJSON(_ json: JSON) -> EngineConfiguration? { + if json.isError() { + return nil + } + if let enabled = jsonsToStrings(json["enabled"].array) { + if let declined = jsonsToStrings(json["declined"].array) { + return EngineConfiguration(enabled: enabled, declined: declined) + } + } + return nil + } + + open func toJSON() -> JSON { + let json: [String: AnyObject] = ["enabled": self.enabled as AnyObject, "declined": self.declined as AnyObject] + return JSON(json) + } +} + +public func ==(lhs: EngineConfiguration, rhs: EngineConfiguration) -> Bool { + return Set(lhs.enabled) == Set(rhs.enabled) +} + +extension EngineConfiguration: CustomStringConvertible { + public var description: String { + return "EngineConfiguration(enabled: \(self.enabled.sorted()), declined: \(self.declined.sorted()))" + } +} + +// Equivalent to Android Sync's EngineSettings, but here +// we use them for meta/global itself. +public struct EngineMeta: Equatable { + let version: Int + let syncID: String + + public static func fromJSON(_ json: JSON) -> EngineMeta? { + if let syncID = json["syncID"].string { + if let version = json["version"].int { + return EngineMeta(version: version, syncID: syncID) + } + } + return nil + } + + public static func mapFromJSON(_ map: [String: JSON]?) -> [String: EngineMeta]? { + if let map = map { + return optFilter(mapValues(map, f: EngineMeta.fromJSON)) + } + return nil + } + + public func toJSON() -> JSON { + let json: [String: AnyObject] = ["version": self.version as AnyObject, "syncID": self.syncID as AnyObject] + return JSON(json) + } +} + +public func ==(lhs: EngineMeta, rhs: EngineMeta) -> Bool { + return (lhs.version == rhs.version) && (lhs.syncID == rhs.syncID) +} + +public struct MetaGlobal: Equatable { + let syncID: String + let storageVersion: Int + let engines: [String: EngineMeta] + let declined: [String] + + // TODO: is it more useful to support partial globals? + // TODO: how do we return error states here? + public static func fromJSON(_ json: JSON) -> MetaGlobal? { + if json.isError() { + return nil + } + if let syncID = json["syncID"].string { + if let storageVersion = json["storageVersion"].int { + let engines = EngineMeta.mapFromJSON(json["engines"].dictionary) ?? [:] + let declined = json["declined"].array ?? [] + return MetaGlobal(syncID: syncID, + storageVersion: storageVersion, + engines: engines, + declined: jsonsToStrings(declined) ?? []) + } + } + return nil + } + + public func enginesPayload() -> JSON { + return JSON(mapValues(engines, f: { $0.toJSON() })) + } + + // TODO: make a whole record JSON for this. + public func asPayload() -> CleartextPayloadJSON { + let json: JSON = JSON([ + "syncID": self.syncID, + "storageVersion": self.storageVersion, + "engines": enginesPayload().dictionaryObject as Any, + "declined": self.declined + ]) + return CleartextPayloadJSON(json) + } + + public func withSyncID(_ syncID: String) -> MetaGlobal { + return MetaGlobal(syncID: syncID, storageVersion: self.storageVersion, engines: self.engines, declined: self.declined) + } + + public func engineConfiguration() -> EngineConfiguration { + return EngineConfiguration(enabled: Array(engines.keys), declined: declined) + } +} + +public func ==(lhs: MetaGlobal, rhs: MetaGlobal) -> Bool { + return (lhs.syncID == rhs.syncID) && + (lhs.storageVersion == rhs.storageVersion) && + optArrayEqual(lhs.declined, rhs: rhs.declined) && + optDictionaryEqual(lhs.engines, rhs: rhs.engines) +} + +/** + * Encapsulates a meta/global, identity-derived keys, and keys. + */ +open class SyncMeta { + let syncKey: KeyBundle + + var keys: Keys? + var global: MetaGlobal? + + public init(syncKey: KeyBundle) { + self.syncKey = syncKey + } +} diff --git a/mobile/ios/Sync/SyncStateMachine.swift b/mobile/ios/Sync/SyncStateMachine.swift new file mode 100644 index 0000000000..dc1249dc16 --- /dev/null +++ b/mobile/ios/Sync/SyncStateMachine.swift @@ -0,0 +1,966 @@ +/* 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 Account +import XCGLogger +import Deferred + +private let log = Logger.syncLogger + +private let StorageVersionCurrent = 5 + +// Names of collections for which a synchronizer is implemented locally. +private let LocalEngines: [String] = [ + "bookmarks", + "clients", + "history", + "passwords", + "tabs", +] + +// Names of collections which will appear in a default meta/global produced locally. +// Map collection name to engine version. See http://docs.services.mozilla.com/sync/objectformats.html. +private let DefaultEngines: [String: Int] = [ + "bookmarks": BookmarksStorageVersion, + "clients": ClientsStorageVersion, + "history": HistoryStorageVersion, + "passwords": PasswordsStorageVersion, + "tabs": TabsStorageVersion, + // We opt-in to syncing collections we don't know about, since no client offers to sync non-enabled, + // non-declined engines yet. See Bug 969669. + "forms": 1, + "addons": 1, + "prefs": 2, +] + +// Names of collections which will appear as declined in a default +// meta/global produced locally. +private let DefaultDeclined: [String] = [String]() + +// public for testing. +public func createMetaGlobalWithEngineConfiguration(_ engineConfiguration: EngineConfiguration) -> MetaGlobal { + var engines: [String: EngineMeta] = [:] + for engine in engineConfiguration.enabled { + // We take this device's version, or, if we don't know the correct version, 0. Another client should recognize + // the engine, see an old version, wipe and start again. + // TODO: this client does not yet do this wipe-and-update itself! + let version = DefaultEngines[engine] ?? 0 + engines[engine] = EngineMeta(version: version, syncID: Bytes.generateGUID()) + } + return MetaGlobal(syncID: Bytes.generateGUID(), storageVersion: StorageVersionCurrent, engines: engines, declined: engineConfiguration.declined) +} + +public func createMetaGlobal() -> MetaGlobal { + let engineConfiguration = EngineConfiguration(enabled: Array(DefaultEngines.keys), declined: DefaultDeclined) + return createMetaGlobalWithEngineConfiguration(engineConfiguration) +} + +public typealias TokenSource = () -> Deferred> +public typealias ReadyDeferred = Deferred> + +// See docs in docs/sync.md. + +// You might be wondering why this doesn't have a Sync15StorageClient like FxALoginStateMachine +// does. Well, such a client is pinned to a particular server, and this state machine must +// acknowledge that a Sync client occasionally must migrate between two servers, preserving +// some state from the last. +// The resultant 'Ready' will be able to provide a suitably initialized storage client. +open class SyncStateMachine { + // The keys are used as a set, to prevent cycles in the state machine. + var stateLabelsSeen = [SyncStateLabel: Bool]() + var stateLabelSequence = [SyncStateLabel]() + + let stateLabelsAllowed: Set + + let scratchpadPrefs: Prefs + + /// Use this set of states to constrain the state machine to attempt the barest + /// minimum to get to Ready. This is suitable for extension uses. If it is not possible, + /// then no destructive or expensive actions are taken (e.g. total HTTP requests, + /// duration, records processed, database writes, fsyncs, blanking any local collections) + public static let OptimisticStates = Set(SyncStateLabel.optimisticValues) + + /// The default set of states that the state machine is allowed to use. + public static let AllStates = Set(SyncStateLabel.allValues) + + public init(prefs: Prefs, allowingStates labels: Set = SyncStateMachine.AllStates) { + self.scratchpadPrefs = prefs.branch("scratchpad") + self.stateLabelsAllowed = labels + } + + open class func clearStateFromPrefs(_ prefs: Prefs) { + log.debug("Clearing all Sync prefs.") + Scratchpad.clearFromPrefs(prefs.branch("scratchpad")) // XXX this is convoluted. + prefs.clearAll() + } + + fileprivate func advanceFromState(_ state: SyncState) -> ReadyDeferred { + log.info("advanceFromState: \(state.label)") + + // Record visibility before taking any action. + let labelAlreadySeen = self.stateLabelsSeen.updateValue(true, forKey: state.label) != nil + stateLabelSequence.append(state.label) + + if let ready = state as? Ready { + // Sweet, we made it! + return deferMaybe(ready) + } + + // Cycles are not necessarily a problem, but seeing the same (recoverable) error condition is a problem. + if state is RecoverableSyncState && labelAlreadySeen { + return deferMaybe(StateMachineCycleError()) + } + + guard stateLabelsAllowed.contains(state.label) else { + return deferMaybe(DisallowedStateError(state.label, allowedStates: stateLabelsAllowed)) + } + + return state.advance() >>== self.advanceFromState + } + + open func toReady(_ authState: SyncAuthState) -> ReadyDeferred { + let token = authState.token(Date.now(), canBeExpired: false) + return chainDeferred(token, f: { (token, kB) in + log.debug("Got token from auth state.") + if Logger.logPII { + log.debug("Server is \(token.api_endpoint).") + } + let prior = Scratchpad.restoreFromPrefs(self.scratchpadPrefs, syncKeyBundle: KeyBundle.fromKB(kB)) + if prior == nil { + log.info("No persisted Sync state. Starting over.") + } + var scratchpad = prior ?? Scratchpad(b: KeyBundle.fromKB(kB), persistingTo: self.scratchpadPrefs) + + // Take the scratchpad and add the fxaDeviceId from the state, and hashedUID from the token + let b = Scratchpad.Builder(p: scratchpad) + if let deviceID = authState.deviceID { + b.fxaDeviceId = deviceID + } else { + // Either deviceRegistration hasn't occurred yet (our bug) or + // FxA has given us an UnknownDevice error. + log.warning("Device registration has not taken place before sync.") + } + b.hashedUID = token.hashedFxAUID + + // Detect if we've changed anything in our client record from the last time we synced… + let ourClientUnchanged = (b.fxaDeviceId == scratchpad.fxaDeviceId) + + // …and if so, trigger a reset of clients. + if !ourClientUnchanged { + b.localCommands.insert(LocalCommand.resetEngine(engine: "clients")) + } + + scratchpad = b.build() + + log.info("Advancing to InitialWithLiveToken.") + let state = InitialWithLiveToken(scratchpad: scratchpad, token: token) + + // Start with fresh visibility data. + self.stateLabelsSeen = [:] + self.stateLabelSequence = [] + + return self.advanceFromState(state) + }) + } +} + +public enum SyncStateLabel: String { + case Stub = "STUB" // For 'abstract' base classes. + + case InitialWithExpiredToken = "initialWithExpiredToken" + case InitialWithExpiredTokenAndInfo = "initialWithExpiredTokenAndInfo" + case InitialWithLiveToken = "initialWithLiveToken" + case InitialWithLiveTokenAndInfo = "initialWithLiveTokenAndInfo" + case ResolveMetaGlobalVersion = "resolveMetaGlobalVersion" + case ResolveMetaGlobalContent = "resolveMetaGlobalContent" + case NeedsFreshMetaGlobal = "needsFreshMetaGlobal" + case NewMetaGlobal = "newMetaGlobal" + case HasMetaGlobal = "hasMetaGlobal" + case NeedsFreshCryptoKeys = "needsFreshCryptoKeys" + case HasFreshCryptoKeys = "hasFreshCryptoKeys" + case Ready = "ready" + case FreshStartRequired = "freshStartRequired" // Go around again... once only, perhaps. + case ServerConfigurationRequired = "serverConfigurationRequired" + + case ChangedServer = "changedServer" + case MissingMetaGlobal = "missingMetaGlobal" + case MissingCryptoKeys = "missingCryptoKeys" + case MalformedCryptoKeys = "malformedCryptoKeys" + case SyncIDChanged = "syncIDChanged" + case RemoteUpgradeRequired = "remoteUpgradeRequired" + case ClientUpgradeRequired = "clientUpgradeRequired" + + static let allValues: [SyncStateLabel] = [ + InitialWithExpiredToken, + InitialWithExpiredTokenAndInfo, + InitialWithLiveToken, + InitialWithLiveTokenAndInfo, + NeedsFreshMetaGlobal, + ResolveMetaGlobalVersion, + ResolveMetaGlobalContent, + NewMetaGlobal, + HasMetaGlobal, + NeedsFreshCryptoKeys, + HasFreshCryptoKeys, + Ready, + + FreshStartRequired, + ServerConfigurationRequired, + + ChangedServer, + MissingMetaGlobal, + MissingCryptoKeys, + MalformedCryptoKeys, + SyncIDChanged, + RemoteUpgradeRequired, + ClientUpgradeRequired, + ] + + // This is the list of states needed to get to Ready, or failing. + // This is useful in circumstances where it is important to conserve time and/or battery, and failure + // to timely sync is acceptable. + static let optimisticValues: [SyncStateLabel] = [ + InitialWithLiveToken, + InitialWithLiveTokenAndInfo, + HasMetaGlobal, + HasFreshCryptoKeys, + Ready, + ] +} + +/** + * States in this state machine all implement SyncState. + * + * States are either successful main-flow states, or (recoverable) error states. + * Errors that aren't recoverable are simply errors. + * Main-flow states flow one to one. + * + * (Terminal failure states might be introduced at some point.) + * + * Multiple error states (but typically only one) can arise from each main state transition. + * For example, parsing meta/global can result in a number of different non-routine situations. + * + * For these reasons, and the lack of useful ADTs in Swift, we model the main flow as + * the success branch of a Result, and the recovery flows as a part of the failure branch. + * + * We could just as easily use a ternary Either-style operator, but thanks to Swift's + * optional-cast-let it's no saving to do so. + * + * Because of the lack of type system support, all RecoverableSyncStates must have the same + * signature. That signature implies a possibly multi-state transition; individual states + * will have richer type signatures. + */ +public protocol SyncState { + var label: SyncStateLabel { get } + + func advance() -> Deferred> +} + +/* + * Base classes to avoid repeating initializers all over the place. + */ +open class BaseSyncState: SyncState { + open var label: SyncStateLabel { return SyncStateLabel.Stub } + + open let client: Sync15StorageClient! + let token: TokenServerToken // Maybe expired. + var scratchpad: Scratchpad + + // TODO: 304 for i/c. + open func getInfoCollections() -> Deferred> { + return chain(self.client.getInfoCollections(), f: { + return $0.value + }) + } + + public init(client: Sync15StorageClient, scratchpad: Scratchpad, token: TokenServerToken) { + self.scratchpad = scratchpad + self.token = token + self.client = client + log.info("Inited \(self.label.rawValue)") + } + + open func synchronizer(_ synchronizerClass: T.Type, delegate: SyncDelegate, prefs: Prefs, why: SyncReason) -> T { + return T(scratchpad: self.scratchpad, delegate: delegate, basePrefs: prefs, why: why) + } + + // This isn't a convenience initializer 'cos subclasses can't call convenience initializers. + public init(scratchpad: Scratchpad, token: TokenServerToken) { + let workQueue = DispatchQueue.global() + let resultQueue = DispatchQueue.main + let backoff = scratchpad.backoffStorage + let client = Sync15StorageClient(token: token, workQueue: workQueue, resultQueue: resultQueue, backoff: backoff) + self.scratchpad = scratchpad + self.token = token + self.client = client + log.info("Inited \(self.label.rawValue)") + } + + open func advance() -> Deferred> { + return deferMaybe(StubStateError()) + } +} + +open class BaseSyncStateWithInfo: BaseSyncState { + open let info: InfoCollections + + init(client: Sync15StorageClient, scratchpad: Scratchpad, token: TokenServerToken, info: InfoCollections) { + self.info = info + super.init(client: client, scratchpad: scratchpad, token: token) + } + + init(scratchpad: Scratchpad, token: TokenServerToken, info: InfoCollections) { + self.info = info + super.init(scratchpad: scratchpad, token: token) + } +} + +/* + * Error types. + */ +public protocol SyncError: MaybeErrorType, SyncPingFailureFormattable {} + +extension SyncError { + public var failureReasonName: SyncPingFailureReasonName { + return .unexpectedError + } +} + +open class UnknownError: SyncError { + open var description: String { + return "Unknown error." + } +} + +open class StateMachineCycleError: SyncError { + open var description: String { + return "The Sync state machine encountered a cycle. This is a coding error." + } +} + +open class CouldNotFetchMetaGlobalError: SyncError { + open var description: String { + return "Could not fetch meta/global." + } +} + +open class CouldNotFetchKeysError: SyncError { + open var description: String { + return "Could not fetch crypto/keys." + } +} + +open class StubStateError: SyncError { + open var description: String { + return "Unexpectedly reached a stub state. This is a coding error." + } +} + +open class ClientUpgradeRequiredError: SyncError { + let targetStorageVersion: Int + + public init(target: Int) { + self.targetStorageVersion = target + } + + open var description: String { + return "Client upgrade required to work with storage version \(self.targetStorageVersion)." + } +} + +open class InvalidKeysError: SyncError { + let keys: Keys + + public init(_ keys: Keys) { + self.keys = keys + } + + open var description: String { + return "Downloaded crypto/keys, but couldn't parse them." + } +} + +open class DisallowedStateError: SyncError { + let state: SyncStateLabel + let allowedStates: Set + + public init(_ state: SyncStateLabel, allowedStates: Set) { + self.state = state + self.allowedStates = allowedStates + } + + open var description: String { + return "Sync state machine reached \(String(describing: state)) state, which is disallowed. Legal states are: \(String(describing: allowedStates))" + } +} + +/** + * Error states. These are errors that can be recovered from by taking actions. We use RecoverableSyncState as a + * sentinel: if we see the same recoverable state twice, we bail out and complain that we've seen a cycle. (Seeing + * some states -- principally initial states -- twice is fine.) +*/ + +public protocol RecoverableSyncState: SyncState { +} + +/** + * Recovery: discard our local timestamps and sync states; discard caches. + * Be prepared to handle a conflict between our selected engines and the new + * server's meta/global; if an engine is selected locally but not declined + * remotely, then we'll need to upload a new meta/global and sync that engine. + */ +open class ChangedServerError: RecoverableSyncState { + open var label: SyncStateLabel { return SyncStateLabel.ChangedServer } + + let newToken: TokenServerToken + let newScratchpad: Scratchpad + + public init(scratchpad: Scratchpad, token: TokenServerToken) { + self.newToken = token + self.newScratchpad = Scratchpad(b: scratchpad.syncKeyBundle, persistingTo: scratchpad.prefs) + } + + open func advance() -> Deferred> { + // TODO: mutate local storage to allow for a fresh start. + let state = InitialWithLiveToken(scratchpad: newScratchpad.checkpoint(), token: newToken) + return deferMaybe(state) + } +} + +/** + * Recovery: same as for changed server, but no need to upload a new meta/global. + */ +open class SyncIDChangedError: RecoverableSyncState { + open var label: SyncStateLabel { return SyncStateLabel.SyncIDChanged } + + fileprivate let previousState: BaseSyncStateWithInfo + fileprivate let newMetaGlobal: Fetched + + public init(previousState: BaseSyncStateWithInfo, newMetaGlobal: Fetched) { + self.previousState = previousState + self.newMetaGlobal = newMetaGlobal + } + + open func advance() -> Deferred> { + // TODO: mutate local storage to allow for a fresh start. + let s = self.previousState.scratchpad.evolve().setGlobal(self.newMetaGlobal).setKeys(nil).build().checkpoint() + let state = HasMetaGlobal(client: self.previousState.client, scratchpad: s, token: self.previousState.token, info: self.previousState.info) + return deferMaybe(state) + } +} + +/** + * Recovery: configure the server. + */ +open class ServerConfigurationRequiredError: RecoverableSyncState { + open var label: SyncStateLabel { return SyncStateLabel.ServerConfigurationRequired } + + fileprivate let previousState: BaseSyncStateWithInfo + + public init(previousState: BaseSyncStateWithInfo) { + self.previousState = previousState + } + + open func advance() -> Deferred> { + let client = self.previousState.client! + let s = self.previousState.scratchpad.evolve() + .setGlobal(nil) + .addLocalCommandsFromKeys(nil) + .setKeys(nil) + .build().checkpoint() + // Upload a new meta/global ... + let metaGlobal: MetaGlobal + if let oldEngineConfiguration = s.engineConfiguration { + metaGlobal = createMetaGlobalWithEngineConfiguration(oldEngineConfiguration) + } else { + metaGlobal = createMetaGlobal() + } + return client.uploadMetaGlobal(metaGlobal, ifUnmodifiedSince: nil) + // ... and a new crypto/keys. + >>> { return client.uploadCryptoKeys(Keys.random(), withSyncKeyBundle: s.syncKeyBundle, ifUnmodifiedSince: nil) } + >>> { return deferMaybe(InitialWithLiveToken(client: client, scratchpad: s, token: self.previousState.token)) } + } +} + +/** + * Recovery: wipe the server (perhaps unnecessarily) and proceed to configure the server. + */ +open class FreshStartRequiredError: RecoverableSyncState { + open var label: SyncStateLabel { return SyncStateLabel.FreshStartRequired } + + fileprivate let previousState: BaseSyncStateWithInfo + + public init(previousState: BaseSyncStateWithInfo) { + self.previousState = previousState + } + + open func advance() -> Deferred> { + let client = self.previousState.client! + return client.wipeStorage() + >>> { return deferMaybe(ServerConfigurationRequiredError(previousState: self.previousState)) } + } +} + +open class MissingMetaGlobalError: RecoverableSyncState { + open var label: SyncStateLabel { return SyncStateLabel.MissingMetaGlobal } + + fileprivate let previousState: BaseSyncStateWithInfo + + public init(previousState: BaseSyncStateWithInfo) { + self.previousState = previousState + } + + open func advance() -> Deferred> { + return deferMaybe(FreshStartRequiredError(previousState: self.previousState)) + } +} + +open class MissingCryptoKeysError: RecoverableSyncState { + open var label: SyncStateLabel { return SyncStateLabel.MissingCryptoKeys } + + fileprivate let previousState: BaseSyncStateWithInfo + + public init(previousState: BaseSyncStateWithInfo) { + self.previousState = previousState + } + + open func advance() -> Deferred> { + return deferMaybe(FreshStartRequiredError(previousState: self.previousState)) + } +} + +open class RemoteUpgradeRequired: RecoverableSyncState { + open var label: SyncStateLabel { return SyncStateLabel.RemoteUpgradeRequired } + + fileprivate let previousState: BaseSyncStateWithInfo + + public init(previousState: BaseSyncStateWithInfo) { + self.previousState = previousState + } + + open func advance() -> Deferred> { + return deferMaybe(FreshStartRequiredError(previousState: self.previousState)) + } +} + +open class ClientUpgradeRequired: RecoverableSyncState { + open var label: SyncStateLabel { return SyncStateLabel.ClientUpgradeRequired } + + fileprivate let previousState: BaseSyncStateWithInfo + let targetStorageVersion: Int + + public init(previousState: BaseSyncStateWithInfo, target: Int) { + self.previousState = previousState + self.targetStorageVersion = target + } + + open func advance() -> Deferred> { + return deferMaybe(ClientUpgradeRequiredError(target: self.targetStorageVersion)) + } +} + +/* + * Non-error states. + */ + +open class InitialWithLiveToken: BaseSyncState { + open override var label: SyncStateLabel { return SyncStateLabel.InitialWithLiveToken } + + // This looks totally redundant, but try taking it out, I dare you. + public override init(scratchpad: Scratchpad, token: TokenServerToken) { + super.init(scratchpad: scratchpad, token: token) + } + + // This looks totally redundant, but try taking it out, I dare you. + public override init(client: Sync15StorageClient, scratchpad: Scratchpad, token: TokenServerToken) { + super.init(client: client, scratchpad: scratchpad, token: token) + } + + func advanceWithInfo(_ info: InfoCollections) -> SyncState { + return InitialWithLiveTokenAndInfo(scratchpad: self.scratchpad, token: self.token, info: info) + } + + override open func advance() -> Deferred> { + return chain(getInfoCollections(), f: self.advanceWithInfo) + } +} + +/** + * Each time we fetch a new meta/global, we need to reconcile it with our + * current state. + * + * It might be identical to our current meta/global, in which case we can short-circuit. + * + * We might have no previous meta/global at all, in which case this state + * simply configures local storage to be ready to sync according to the + * supplied meta/global. (Not necessarily datatype elections: those will be per-device.) + * + * Or it might be different. In this case the previous m/g and our local user preferences + * are compared to the new, resulting in some actions and a final state. + * + * This states are similar in purpose to GlobalSession.processMetaGlobal in Android Sync. + */ + +open class ResolveMetaGlobalVersion: BaseSyncStateWithInfo { + let fetched: Fetched + + init(fetched: Fetched, client: Sync15StorageClient, scratchpad: Scratchpad, token: TokenServerToken, info: InfoCollections) { + self.fetched = fetched + super.init(client: client, scratchpad: scratchpad, token: token, info: info) + } + open override var label: SyncStateLabel { return SyncStateLabel.ResolveMetaGlobalVersion } + + class func fromState(_ state: BaseSyncStateWithInfo, fetched: Fetched) -> ResolveMetaGlobalVersion { + return ResolveMetaGlobalVersion(fetched: fetched, client: state.client, scratchpad: state.scratchpad, token: state.token, info: state.info) + } + + override open func advance() -> Deferred> { + // First: check storage version. + let v = fetched.value.storageVersion + if v > StorageVersionCurrent { + // New storage version? Uh-oh. No recovery possible here. + log.info("Client upgrade required for storage version \(v)") + return deferMaybe(ClientUpgradeRequired(previousState: self, target: v)) + } + + if v < StorageVersionCurrent { + // Old storage version? Uh-oh. Wipe and upload both meta/global and crypto/keys. + log.info("Server storage version \(v) is outdated.") + return deferMaybe(RemoteUpgradeRequired(previousState: self)) + } + + return deferMaybe(ResolveMetaGlobalContent.fromState(self, fetched: self.fetched)) + } +} + +open class ResolveMetaGlobalContent: BaseSyncStateWithInfo { + let fetched: Fetched + + init(fetched: Fetched, client: Sync15StorageClient, scratchpad: Scratchpad, token: TokenServerToken, info: InfoCollections) { + self.fetched = fetched + super.init(client: client, scratchpad: scratchpad, token: token, info: info) + } + open override var label: SyncStateLabel { return SyncStateLabel.ResolveMetaGlobalContent } + + class func fromState(_ state: BaseSyncStateWithInfo, fetched: Fetched) -> ResolveMetaGlobalContent { + return ResolveMetaGlobalContent(fetched: fetched, client: state.client, scratchpad: state.scratchpad, token: state.token, info: state.info) + } + + override open func advance() -> Deferred> { + // Check global syncID and contents. + if let previous = self.scratchpad.global?.value { + // Do checks that only apply when we're coming from a previous meta/global. + if previous.syncID != fetched.value.syncID { + log.info("Remote global sync ID has changed. Dropping keys and resetting all local collections.") + let s = self.scratchpad.freshStartWithGlobal(fetched).checkpoint() + return deferMaybe(HasMetaGlobal.fromState(self, scratchpad: s)) + } + + let b = self.scratchpad.evolve() + .setGlobal(fetched) // We always adopt the upstream meta/global record. + + let previousEngines = Set(previous.engines.keys) + let remoteEngines = Set(fetched.value.engines.keys) + + for engine in previousEngines.subtracting(remoteEngines) { + log.info("Remote meta/global disabled previously enabled engine \(engine).") + b.localCommands.insert(.disableEngine(engine: engine)) + } + + for engine in remoteEngines.subtracting(previousEngines) { + log.info("Remote meta/global enabled previously disabled engine \(engine).") + b.localCommands.insert(.enableEngine(engine: engine)) + } + + for engine in remoteEngines.intersection(previousEngines) { + let remoteEngine = fetched.value.engines[engine]! + let previousEngine = previous.engines[engine]! + if previousEngine.syncID != remoteEngine.syncID { + log.info("Remote sync ID for \(engine) has changed. Resetting local.") + b.localCommands.insert(.resetEngine(engine: engine)) + } + } + + let s = b.build().checkpoint() + return deferMaybe(HasMetaGlobal.fromState(self, scratchpad: s)) + } + + // No previous meta/global. Adopt the new meta/global. + let s = self.scratchpad.freshStartWithGlobal(fetched).checkpoint() + return deferMaybe(HasMetaGlobal.fromState(self, scratchpad: s)) + } +} + +private func processFailure(_ failure: MaybeErrorType?) -> MaybeErrorType { + if let failure = failure as? ServerInBackoffError { + log.warning("Server in backoff. Bailing out. \(failure.description)") + return failure + } + + // TODO: backoff etc. for all of these. + if let failure = failure as? ServerError { + // Be passive. + log.error("Server error. Bailing out. \(failure.description)") + return failure + } + + if let failure = failure as? BadRequestError { + // Uh oh. + log.error("Bad request. Bailing out. \(failure.description)") + return failure + } + + log.error("Unexpected failure. \(failure?.description ?? "nil")") + return failure ?? UnknownError() +} + +open class InitialWithLiveTokenAndInfo: BaseSyncStateWithInfo { + open override var label: SyncStateLabel { return SyncStateLabel.InitialWithLiveTokenAndInfo } + + // This method basically hops over HasMetaGlobal, because it's not a state + // that we expect consumers to know about. + override open func advance() -> Deferred> { + // Either m/g and c/k are in our local cache, and they're up-to-date with i/c, + // or we need to fetch them. + // Cached and not changed in i/c? Use that. + // This check would be inaccurate if any other fields were stored in meta/; this + // has been the case in the past, with the Sync 1.1 migration indicator. + if let global = self.scratchpad.global { + if let metaModified = self.info.modified("meta") { + // We check the last time we fetched the record, and that can be + // later than the collection timestamp. All we care about here is if the + // server might have a newer record. + if global.timestamp >= metaModified { + log.debug("Cached meta/global fetched at \(global.timestamp), newer than server modified \(metaModified). Using cached meta/global.") + // Strictly speaking we can avoid fetching if this condition is not true, + // but if meta/ is modified for a different reason -- store timestamps + // for the last collection fetch. This will do for now. + return deferMaybe(HasMetaGlobal.fromState(self)) + } + log.info("Cached meta/global fetched at \(global.timestamp) older than server modified \(metaModified). Fetching fresh meta/global.") + } else { + // No known modified time for meta/. That means the server has no meta/global. + // Drop our cached value and fall through; we'll try to fetch, fail, and + // go through the usual failure flow. + log.warning("Local meta/global fetched at \(global.timestamp) found, but no meta collection on server. Dropping cached meta/global.") + // If we bail because we've been overly optimistic, then we nil out the current (broken) + // meta/global. Next time around, we end up in the "No cached meta/global found" branch. + self.scratchpad = self.scratchpad.evolve().setGlobal(nil).setKeys(nil).build().checkpoint() + } + } else { + log.debug("No cached meta/global found. Fetching fresh meta/global.") + } + + return deferMaybe(NeedsFreshMetaGlobal.fromState(self)) + } +} + +/* + * We've reached NeedsFreshMetaGlobal somehow, but we haven't yet done anything about it + * (e.g. fetch a new one with GET /storage/meta/global ). + * + * If we don't want to hit the network (e.g. from an extension), we should stop if we get to this state. + */ +open class NeedsFreshMetaGlobal: BaseSyncStateWithInfo { + open override var label: SyncStateLabel { return SyncStateLabel.NeedsFreshMetaGlobal } + + class func fromState(_ state: BaseSyncStateWithInfo) -> NeedsFreshMetaGlobal { + return NeedsFreshMetaGlobal(client: state.client, scratchpad: state.scratchpad, token: state.token, info: state.info) + } + + override open func advance() -> Deferred> { + // Fetch. + return self.client.getMetaGlobal().bind { result in + if let resp = result.successValue { + // We use the server's timestamp, rather than the record's modified field. + // Either can be made to work, but the latter has suffered from bugs: see Bug 1210625. + let fetched = Fetched(value: resp.value, timestamp: resp.metadata.timestampMilliseconds) + return deferMaybe(ResolveMetaGlobalVersion.fromState(self, fetched: fetched)) + } + + if let _ = result.failureValue as? NotFound { + // OK, this is easy. + // This state is responsible for creating the new m/g, uploading it, and + // restarting with a clean scratchpad. + return deferMaybe(MissingMetaGlobalError(previousState: self)) + } + + // Otherwise, we have a failure state. Die on the sword! + return deferMaybe(processFailure(result.failureValue)) + } + } +} + +open class HasMetaGlobal: BaseSyncStateWithInfo { + open override var label: SyncStateLabel { return SyncStateLabel.HasMetaGlobal } + + class func fromState(_ state: BaseSyncStateWithInfo) -> HasMetaGlobal { + return HasMetaGlobal(client: state.client, scratchpad: state.scratchpad, token: state.token, info: state.info) + } + + class func fromState(_ state: BaseSyncStateWithInfo, scratchpad: Scratchpad) -> HasMetaGlobal { + return HasMetaGlobal(client: state.client, scratchpad: scratchpad, token: state.token, info: state.info) + } + + override open func advance() -> Deferred> { + // Check if crypto/keys is fresh in the cache already. + if let keys = self.scratchpad.keys, keys.value.valid { + if let cryptoModified = self.info.modified("crypto") { + // Both of these are server timestamps. If the record we stored was fetched after the last time the record was modified, as represented by the "crypto" entry in info/collections, and we're fetching from the + // same server, then the record must be identical, and we can use it directly. If are ever additional records in the crypto collection, this will fetch keys too frequently. In that case, we should use X-I-U-S and expect some 304 responses. + if keys.timestamp >= cryptoModified { + log.debug("Cached keys fetched at \(keys.timestamp), newer than server modified \(cryptoModified). Using cached keys.") + return deferMaybe(HasFreshCryptoKeys.fromState(self, scratchpad: self.scratchpad, collectionKeys: keys.value)) + } + + // The server timestamp is newer, so there might be new keys. + // Re-fetch keys and check to see if the actual contents differ. + // If the keys are the same, we can ignore this change. If they differ, + // we need to re-sync any collection whose keys just changed. + log.info("Cached keys fetched at \(keys.timestamp) older than server modified \(cryptoModified). Fetching fresh keys.") + return deferMaybe(NeedsFreshCryptoKeys.fromState(self, scratchpad: self.scratchpad, staleCollectionKeys: keys.value)) + } else { + // No known modified time for crypto/. That likely means the server has no keys. + // Drop our cached value and fall through; we'll try to fetch, fail, and + // go through the usual failure flow. + log.warning("Local keys fetched at \(keys.timestamp) found, but no crypto collection on server. Dropping cached keys.") + self.scratchpad = self.scratchpad.evolve().setKeys(nil).build().checkpoint() + } + } else { + log.debug("No cached keys found. Fetching fresh keys.") + } + + return deferMaybe(NeedsFreshCryptoKeys.fromState(self, scratchpad: self.scratchpad, staleCollectionKeys: nil)) + } +} + +open class NeedsFreshCryptoKeys: BaseSyncStateWithInfo { + open override var label: SyncStateLabel { return SyncStateLabel.NeedsFreshCryptoKeys } + let staleCollectionKeys: Keys? + + class func fromState(_ state: BaseSyncStateWithInfo, scratchpad: Scratchpad, staleCollectionKeys: Keys?) -> NeedsFreshCryptoKeys { + return NeedsFreshCryptoKeys(client: state.client, scratchpad: scratchpad, token: state.token, info: state.info, keys: staleCollectionKeys) + } + + public init(client: Sync15StorageClient, scratchpad: Scratchpad, token: TokenServerToken, info: InfoCollections, keys: Keys?) { + self.staleCollectionKeys = keys + super.init(client: client, scratchpad: scratchpad, token: token, info: info) + } + + override open func advance() -> Deferred> { + // Fetch crypto/keys. + return self.client.getCryptoKeys(self.scratchpad.syncKeyBundle, ifUnmodifiedSince: nil).bind { result in + if let resp = result.successValue { + let collectionKeys = Keys(payload: resp.value.payload) + if !collectionKeys.valid { + log.error("Unexpectedly invalid crypto/keys during a successful fetch.") + return Deferred(value: Maybe(failure: InvalidKeysError(collectionKeys))) + } + + let fetched = Fetched(value: collectionKeys, timestamp: resp.metadata.timestampMilliseconds) + let s = self.scratchpad.evolve() + .addLocalCommandsFromKeys(fetched) + .setKeys(fetched) + .build().checkpoint() + return deferMaybe(HasFreshCryptoKeys.fromState(self, scratchpad: s, collectionKeys: collectionKeys)) + } + + if let _ = result.failureValue as? NotFound { + // No crypto/keys? We can handle this. Wipe and upload both meta/global and crypto/keys. + return deferMaybe(MissingCryptoKeysError(previousState: self)) + } + + // Otherwise, we have a failure state. + return deferMaybe(processFailure(result.failureValue)) + } + } +} + +open class HasFreshCryptoKeys: BaseSyncStateWithInfo { + open override var label: SyncStateLabel { return SyncStateLabel.HasFreshCryptoKeys } + let collectionKeys: Keys + + class func fromState(_ state: BaseSyncStateWithInfo, scratchpad: Scratchpad, collectionKeys: Keys) -> HasFreshCryptoKeys { + return HasFreshCryptoKeys(client: state.client, scratchpad: scratchpad, token: state.token, info: state.info, keys: collectionKeys) + } + + public init(client: Sync15StorageClient, scratchpad: Scratchpad, token: TokenServerToken, info: InfoCollections, keys: Keys) { + self.collectionKeys = keys + super.init(client: client, scratchpad: scratchpad, token: token, info: info) + } + + override open func advance() -> Deferred> { + return deferMaybe(Ready(client: self.client, scratchpad: self.scratchpad, token: self.token, info: self.info, keys: self.collectionKeys)) + } +} + +public protocol EngineStateChanges { + func collectionsThatNeedLocalReset() -> [String] + func enginesEnabled() -> [String] + func enginesDisabled() -> [String] + func clearLocalCommands() +} + +open class Ready: BaseSyncStateWithInfo { + open override var label: SyncStateLabel { return SyncStateLabel.Ready } + let collectionKeys: Keys + + public var hashedFxADeviceID: String { + return (scratchpad.fxaDeviceId + token.hashedFxAUID).sha256.hexEncodedString + } + + public init(client: Sync15StorageClient, scratchpad: Scratchpad, token: TokenServerToken, info: InfoCollections, keys: Keys) { + self.collectionKeys = keys + super.init(client: client, scratchpad: scratchpad, token: token, info: info) + } +} + +extension Ready: EngineStateChanges { + public func collectionsThatNeedLocalReset() -> [String] { + var needReset: Set = Set() + for command in self.scratchpad.localCommands { + switch command { + case let .resetAllEngines(except: except): + needReset.formUnion(Set(LocalEngines).subtracting(except)) + case let .resetEngine(engine): + needReset.insert(engine) + case .enableEngine, .disableEngine: + break + } + } + return Array(needReset).sorted() + } + + public func enginesEnabled() -> [String] { + var engines: Set = Set() + for command in self.scratchpad.localCommands { + switch command { + case let .enableEngine(engine): + engines.insert(engine) + default: + break + } + } + return Array(engines).sorted() + } + + public func enginesDisabled() -> [String] { + var engines: Set = Set() + for command in self.scratchpad.localCommands { + switch command { + case let .disableEngine(engine): + engines.insert(engine) + default: + break + } + } + return Array(engines).sorted() + } + + public func clearLocalCommands() { + self.scratchpad = self.scratchpad.evolve().clearLocalCommands().build().checkpoint() + } +} diff --git a/mobile/ios/Sync/SyncTelemetryUtils.swift b/mobile/ios/Sync/SyncTelemetryUtils.swift new file mode 100644 index 0000000000..5daa88b761 --- /dev/null +++ b/mobile/ios/Sync/SyncTelemetryUtils.swift @@ -0,0 +1,356 @@ +/* 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 Account +import Storage +import SwiftyJSON +import SyncTelemetry +import Deferred + +fileprivate let log = Logger.syncLogger + +public let PrefKeySyncEvents = "sync.telemetry.events" + +public enum SyncReason: String { + case startup = "startup" + case scheduled = "scheduled" + case backgrounded = "backgrounded" + case user = "user" + case syncNow = "syncNow" + case didLogin = "didLogin" + case push = "push" +} + +public enum SyncPingReason: String { + case shutdown = "shutdown" + case schedule = "schedule" + case idChanged = "idchanged" +} + +public protocol Stats { + func hasData() -> Bool +} + +private protocol DictionaryRepresentable { + func asDictionary() -> [String: Any] +} + +public struct SyncUploadStats: Stats { + var sent: Int = 0 + var sentFailed: Int = 0 + + public func hasData() -> Bool { + return sent > 0 || sentFailed > 0 + } +} + +extension SyncUploadStats: DictionaryRepresentable { + func asDictionary() -> [String: Any] { + return [ + "sent": sent, + "sentFailed": sentFailed + ] + } +} + +public struct SyncDownloadStats: Stats { + var applied: Int = 0 + var succeeded: Int = 0 + var failed: Int = 0 + var newFailed: Int = 0 + var reconciled: Int = 0 + + public func hasData() -> Bool { + return applied > 0 || + succeeded > 0 || + failed > 0 || + newFailed > 0 || + reconciled > 0 + } +} + +extension SyncDownloadStats: DictionaryRepresentable { + func asDictionary() -> [String: Any] { + return [ + "applied": applied, + "succeeded": succeeded, + "failed": failed, + "newFailed": newFailed, + "reconciled": reconciled + ] + } +} + +public struct ValidationStats: Stats, DictionaryRepresentable { + let problems: [ValidationProblem] + let took: Int64 + let checked: Int? + + public func hasData() -> Bool { + return !problems.isEmpty + } + + func asDictionary() -> [String: Any] { + var dict: [String: Any] = [ + "problems": problems.map { $0.asDictionary() }, + "took": took + ] + if let checked = self.checked { + dict["checked"] = checked + } + return dict + } +} + +public struct ValidationProblem: DictionaryRepresentable { + let name: String + let count: Int + + func asDictionary() -> [String: Any] { + return ["name": name, "count": count] + } +} + +public class StatsSession { + var took: Int64 = 0 + var when: Timestamp? + + private var startUptimeNanos: UInt64? + + public func start(when: UInt64 = Date.now()) { + self.when = when + self.startUptimeNanos = DispatchTime.now().uptimeNanoseconds + } + + public func hasStarted() -> Bool { + return startUptimeNanos != nil + } + + public func end() -> Self { + guard let startUptime = startUptimeNanos else { + assertionFailure("SyncOperationStats called end without first calling start!") + return self + } + + // Casting to Int64 should be safe since we're using uptime since boot in both cases. + // Convert to milliseconds as stated in the sync ping format + took = (Int64(DispatchTime.now().uptimeNanoseconds) - Int64(startUptime)) / 1000000 + return self + } +} + +// Stats about a single engine's sync. +public class SyncEngineStatsSession: StatsSession { + public var validationStats: ValidationStats? + + private(set) var uploadStats: SyncUploadStats + private(set) var downloadStats: SyncDownloadStats + + public init(collection: String) { + self.uploadStats = SyncUploadStats() + self.downloadStats = SyncDownloadStats() + } + + public func recordDownload(stats: SyncDownloadStats) { + self.downloadStats.applied += stats.applied + self.downloadStats.succeeded += stats.succeeded + self.downloadStats.failed += stats.failed + self.downloadStats.newFailed += stats.newFailed + self.downloadStats.reconciled += stats.reconciled + } + + public func recordUpload(stats: SyncUploadStats) { + self.uploadStats.sent += stats.sent + self.uploadStats.sentFailed += stats.sentFailed + } +} + +extension SyncEngineStatsSession: DictionaryRepresentable { + func asDictionary() -> [String: Any] { + var dict: [String: Any] = [ + "took": took, + ] + + if downloadStats.hasData() { + dict["incoming"] = downloadStats.asDictionary() + } + + if uploadStats.hasData() { + dict["outgoing"] = uploadStats.asDictionary() + } + + if let validation = self.validationStats, validation.hasData() { + dict["validation"] = validation.asDictionary() + } + + return dict + } +} + +// Stats and metadata for a sync operation. +public class SyncOperationStatsSession: StatsSession { + public let why: SyncReason + public var uid: String? + public var deviceID: String? + + fileprivate let didLogin: Bool + + public init(why: SyncReason, uid: String, deviceID: String?) { + self.why = why + self.uid = uid + self.deviceID = deviceID + self.didLogin = (why == .didLogin) + } +} + +extension SyncOperationStatsSession: DictionaryRepresentable { + func asDictionary() -> [String: Any] { + let whenValue = when ?? 0 + return [ + "when": whenValue, + "took": took, + "didLogin": didLogin, + "why": why.rawValue + ] + } +} + +public enum SyncPingError: MaybeErrorType { + case failedToRestoreScratchpad + + public var description: String { + switch self { + case .failedToRestoreScratchpad: return "Failed to restore Scratchpad from prefs" + } + } +} + +public enum SyncPingFailureReasonName: String { + case httpError = "httperror" + case unexpectedError = "unexpectederror" + case sqlError = "sqlerror" + case otherError = "othererror" +} + +public protocol SyncPingFailureFormattable { + var failureReasonName: SyncPingFailureReasonName { get } +} + +public struct SyncPing: SyncTelemetryPing { + public private(set) var payload: JSON + + public static func from(result: SyncOperationResult, + account: FirefoxAccount, + remoteClientsAndTabs: RemoteClientsAndTabs, + prefs: Prefs, + why: SyncPingReason) -> Deferred> { + // Grab our token so we can use the hashed_fxa_uid and clientGUID from our scratchpad for + // our ping's identifiers + return account.syncAuthState.token(Date.now(), canBeExpired: false) >>== { (token, kB) in + let scratchpadPrefs = prefs.branch("sync.scratchpad") + guard let scratchpad = Scratchpad.restoreFromPrefs(scratchpadPrefs, syncKeyBundle: KeyBundle.fromKB(kB)) else { + return deferMaybe(SyncPingError.failedToRestoreScratchpad) + } + + var ping: [String: Any] = [ + "version": 1, + "why": why.rawValue, + "uid": token.hashedFxAUID, + "deviceID": (scratchpad.clientGUID + token.hashedFxAUID).sha256.hexEncodedString + ] + + // TODO: We don't cache our sync pings so if it fails, it fails. Once we add + // some kind of caching we'll want to make sure we don't dump the events if + // the ping has failed. + let pickledEvents = prefs.arrayForKey(PrefKeySyncEvents) as? [Data] ?? [] + let events = pickledEvents.flatMap(Event.unpickle).map { $0.toArray() } + ping["events"] = events + prefs.setObject(nil, forKey: PrefKeySyncEvents) + + return dictionaryFrom(result: result, storage: remoteClientsAndTabs, token: token) >>== { syncDict in + // TODO: Split the sync ping metadata from storing a single sync. + ping["syncs"] = [syncDict] + return deferMaybe(SyncPing(payload: JSON(ping))) + } + } + } + + // Generates a single sync ping payload that is stored in the 'syncs' list in the sync ping. + private static func dictionaryFrom(result: SyncOperationResult, + storage: RemoteClientsAndTabs, + token: TokenServerToken) -> Deferred> { + return connectedDevices(fromStorage: storage, token: token) >>== { devices in + guard let stats = result.stats else { + return deferMaybe([String: Any]()) + } + + var dict = stats.asDictionary() + if let engineResults = result.engineResults.successValue { + dict["engines"] = SyncPing.enginePingDataFrom(engineResults: engineResults) + } else if let failure = result.engineResults.failureValue { + var errorName: SyncPingFailureReasonName + if let formattableFailure = failure as? SyncPingFailureFormattable { + errorName = formattableFailure.failureReasonName + } else { + errorName = .unexpectedError + } + + dict["failureReason"] = [ + "name": errorName.rawValue, + "error": "\(type(of: failure))", + ] + } + + dict["devices"] = devices + return deferMaybe(dict) + } + } + + // Returns a list of connected devices formatted for use in the 'devices' property in the sync ping. + private static func connectedDevices(fromStorage storage: RemoteClientsAndTabs, + token: TokenServerToken) -> Deferred> { + func dictionaryFrom(client: RemoteClient) -> [String: Any]? { + var device = [String: Any]() + if let os = client.os { + device["os"] = os + } + if let version = client.version { + device["version"] = version + } + if let guid = client.guid { + device["id"] = (guid + token.hashedFxAUID).sha256.hexEncodedString + } + return device + } + + return storage.getClients() >>== { deferMaybe($0.flatMap(dictionaryFrom)) } + } + + private static func enginePingDataFrom(engineResults: EngineResults) -> [[String: Any]] { + return engineResults.map { result in + let (name, status) = result + var engine: [String: Any] = [ + "name": name + ] + + // For complete/partial results, extract out the collect stats + // and add it to engine information. For syncs that were not able to + // start, return why and a reason. + switch status { + case .completed(let stats): + engine.merge(with: stats.asDictionary()) + case .partial(let stats): + engine.merge(with: stats.asDictionary()) + case .notStarted(let reason): + engine.merge(with: [ + "status": reason.telemetryId + ]) + } + + return engine + } + } +} diff --git a/mobile/ios/Sync/Synchronizers/Bookmarks/BookmarksDownloader.swift b/mobile/ios/Sync/Synchronizers/Bookmarks/BookmarksDownloader.swift new file mode 100644 index 0000000000..94b0f6e9ec --- /dev/null +++ b/mobile/ios/Sync/Synchronizers/Bookmarks/BookmarksDownloader.swift @@ -0,0 +1,161 @@ +/* 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 + +private let log = Logger.syncLogger +let BookmarksStorageVersion = 2 + +/** + * This is like a synchronizer, but it downloads records bit by bit, eventually + * notifying that the local storage is up to date with the server contents. + * + * Because batches might be separated over time, it's possible for the server + * state to change between calls. These state changes might include: + * + * 1. New changes arriving. This is fairly routine, but it's worth noting that + * changes might affect existing records that have been batched! + * 2. Wipes. The collection (or the server as a whole) might be deleted. This + * should be accompanied by a change in syncID in meta/global; it's the caller's + * responsibility to detect this. + * 3. A storage format change. This should be unfathomably rare, but if it happens + * we must also be prepared to discard our existing batched data. + * 4. TTL expiry. We need to do better about TTL handling in general, but here + * we might find that a downloaded record is no longer live by the time we + * come to apply it! This doesn't apply to bookmark records, so we will ignore + * it for the moment. + * + * Batch downloading without continuation tokens is achieved as follows: + * + * * A minimum timestamp is established. This starts as zero. + * * A fetch is issued against the server for records changed since that timestamp, + * ordered by modified time ascending, and limited to the batch size. + * * If the batch is complete, we flush it to storage and advance the minimum + * timestamp to just before the newest record in the batch. This ensures that + * a divided set of records with the same modified times will be downloaded + * entirely so long as the set is never larger than the batch size. + * * Iterate until we determine that there are no new records to fetch. + * + * Batch downloading with continuation tokens is much easier: + * + * * A minimum timestamp is established. + * * Make a request with limit=N. + * * Look for an X-Weave-Next-Offset header. Supply that in the next request. + * Also supply X-If-Unmodified-Since to avoid missed modifications. + * + * We do the latter, because we only support Sync 1.5. The use of the offset + * allows us to efficiently process batches, particularly those that contain + * large sets of records with the same timestamp. We still maintain the last + * modified timestamp to allow for resuming a batch in the case of a conflicting + * write, detected via X-I-U-S. + */ + +public class BookmarksMirrorer { + private let downloader: BatchingDownloader + private let storage: BookmarkBufferStorage + private let batchSize: Int + private let statsSession: SyncEngineStatsSession + + public init(storage: BookmarkBufferStorage, client: Sync15CollectionClient, basePrefs: Prefs, collection: String, statsSession: SyncEngineStatsSession, batchSize: Int=100) { + self.storage = storage + self.downloader = BatchingDownloader(collectionClient: client, basePrefs: basePrefs, collection: collection) + self.batchSize = batchSize + self.statsSession = statsSession + } + + var lastModified: Timestamp { + get { + return self.downloader.lastModified + } + } + + // TODO + public func storageFormatDidChange() { + } + + // TODO + public func onWipeWasAppliedToStorage() { + } + + private func applyRecordsFromBatcher() -> Success { + let retrieved = self.downloader.retrieve() + let invalid = retrieved.filter { !$0.payload.isValid() } + + if !invalid.isEmpty { + // There's nothing we can do with invalid input. There's also no point in + // tracking failing GUIDs here yet: if another client reuploads those records + // correctly, we'll encounter them routinely due to a newer timestamp. + // The only improvement we could make is to drop records from the buffer if we + // happen to see a new, invalid one before somehow syncing again, but that's + // unlikely enough that it's not worth doing. + // + // Bug 1258801 tracks recording telemetry for these invalid items, which is + // why we don't simply drop them on the ground at the download stage. + // + // We might also choose to perform certain simple recovery actions here: for example, + // bookmarks with null URIs are clearly invalid, and could be treated as if they + // weren't present on the server, or transparently deleted. + log.warning("Invalid records: \(invalid.map { $0.id }.joined(separator: ", ")).") + } + + let mirrorItems = retrieved.flatMap { record -> BookmarkMirrorItem? in + guard record.payload.isValid() else { + return nil + } + + return (record.payload as MirrorItemable).toMirrorItem(record.modified) + } + + if mirrorItems.isEmpty { + log.debug("Got empty batch.") + return succeed() + } + + log.debug("Applying \(mirrorItems.count) downloaded bookmarks.") + return self.storage.applyRecords(mirrorItems) + } + + public func go(info: InfoCollections, greenLight: @escaping () -> Bool) -> SyncResult { + if !greenLight() { + log.info("Green light turned red. Stopping mirror operation.") + return deferMaybe(SyncStatus.notStarted(.redLight)) + } + + log.debug("Downloading up to \(self.batchSize) records.") + return self.downloader.go(info, limit: self.batchSize) + .bind { result in + guard let end = result.successValue else { + log.warning("Got failure: \(result.failureValue!)") + return deferMaybe(result.failureValue!) + } + switch end { + case .complete: + log.info("Done with batched mirroring.") + return self.applyRecordsFromBatcher() + >>> effect(self.downloader.advance) + >>> self.storage.doneApplyingRecordsAfterDownload + >>> always(SyncStatus.completed(self.statsSession.end())) + case .incomplete: + log.debug("Running another batch.") + // This recursion is fine because Deferred always pushes callbacks onto a queue. + return self.applyRecordsFromBatcher() + >>> effect(self.downloader.advance) + >>> { self.go(info: info, greenLight: greenLight) } + case .interrupted: + log.info("Interrupted. Aborting batching this time.") + return deferMaybe(SyncStatus.partial(self.statsSession)) + case .noNewData: + log.info("No new data. No need to continue batching.") + self.downloader.advance() + return deferMaybe(SyncStatus.completed(self.statsSession.end())) + } + } + } + + func advanceNextDownloadTimestampTo(timestamp: Timestamp) { + self.downloader.advanceTimestampTo(timestamp) + } +} diff --git a/mobile/ios/Sync/Synchronizers/Bookmarks/BookmarksRepairRequestor.swift b/mobile/ios/Sync/Synchronizers/Bookmarks/BookmarksRepairRequestor.swift new file mode 100644 index 0000000000..bbe464b86a --- /dev/null +++ b/mobile/ios/Sync/Synchronizers/Bookmarks/BookmarksRepairRequestor.swift @@ -0,0 +1,571 @@ +/* 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 Deferred +import SwiftyJSON +import SyncTelemetry + +private let log = Logger.syncLogger + +// How long should we wait after sending a repair request before we give up? +private let ResponseIntervalTimeout = OneDayInMilliseconds * 3 + +// The maximum number of IDs we will request to be repaired. Beyond this +// number we assume that trying to repair may do more harm than good and may +// ask another client to wipe the server and reupload everything. Bug 1341972 +// is tracking that work for Desktop. +private let MaxRequestedIDs = 1000 + +// If a repair is in progress, this is the generated GUID for the "flow ID". +private let PrefFlowID = "flowID" +// The IDs we are currently trying to obtain via the repair process. +private let PrefMissingIDs = "ids" +// The ID of the client we're currently trying to get the missing items from. +private let PrefCurrentClient = "currentClient" +// The IDs of the clients we've previously tried to get the missing items +// from. +private let PrefPreviousClients = "previousClients" +// The time, in seconds, when we initiated the most recent client request. +private let PrefLastRepair = "when" +// Our current state. +private let PrefCurrentState = "state" + +private enum RepairState: String { + // We have not started the repair process. + case notRepairing = "" + + // We need to try to find another client to use. + case needNewClient = "repair.need-new-client" + + // We've sent the first request to a client. + case sentRequest = "repair.sent" + + // We've retried a request to a client. + case sentSecondRequest = "repair.sent-again" + + // There were no problems, but we've gone as far as we can. + case finished = "repair.finished" + + // We've found an error that forces us to abort this entire repair cycle. + case aborted = "repair.aborted" +} + +struct RepairResponse { + let collection: String + let request: String + let flowID: String + let clientID: String + let ids: [String] + + static func fromJSON(args: JSON) -> RepairResponse { + return RepairResponse(collection: args["collection"].stringValue, request: args["request"].stringValue, + flowID: args["flowID"].stringValue, clientID: args["clientID"].stringValue, + ids: args["ids"].arrayValue.map { $0.stringValue }) + } +} + +struct RepairRequest { + let collection: String + let request: String + let flowID: String + let requestor: String + let ids: [String] + + func toSyncCommand() -> SyncCommand { + let jsonObj: [String: Any] = [ + "command": "repairRequest", + "args": [ + [ + "collection": collection, + "request": request, + "flowID": flowID, + "requestor": requestor, + "ids": ids + ] + ] + ] + return SyncCommand(value: JSON(object: jsonObj).stringValue()!) + } +} + +private class AbortRepairError: MaybeErrorType { + let description: String + init(_ description: String) { + self.description = description + } +} + +private class UnknownClientError: MaybeErrorType { + let description: String + init(_ description: String) { + self.description = description + } +} + +private class InvalidStateError: MaybeErrorType { + let description: String + init(_ description: String) { + self.description = description + } +} + +class BookmarksRepairRequestor { + let prefs: Prefs + let basePrefs: Prefs + let remoteClients: RemoteClientsAndTabs + let scratchpad: Scratchpad + + init(scratchpad: Scratchpad, basePrefs: Prefs, remoteClients: RemoteClientsAndTabs) { + self.scratchpad = scratchpad + self.basePrefs = basePrefs + self.prefs = basePrefs.branch("repairs.bookmark") + self.remoteClients = remoteClients + } + + /** + * See if the repairer is willing and able to begin a repair process given + * the specified validation information. + * + * - returns: true if a repair was started and false otherwise. + */ + func startRepairs(validationInfo: [BufferInconsistency: [GUID]], flowID: String = Bytes.generateGUID()) -> Deferred> { + guard self.currentState == .notRepairing else { + log.info("Can't start a repair - repair with ID \(self.flowID) is already in progress") + return deferMaybe(false) + } + + let ids = self.getProblemIDs(validationInfo) + + guard ids.count > 0 else { + log.info("Not starting a repair as there are no problems") + return deferMaybe(false) + } + + guard ids.count <= MaxRequestedIDs else { + log.info("Not starting a repair as there are over \(MaxRequestedIDs) problems") + let extra = [ + "flowID": flowID, + "reason": "too many problems: \(ids.count)" + ] + + let event = Event(category: "sync", method: "repair", object: "aborted", extra: extra) + recordTelemetry(event: event) + + return deferMaybe(false) + } + + return self.anyClientsRepairing() >>== { clientsRepairing in + guard !clientsRepairing else { + log.info("Can't start repair, since other clients are already repairing bookmarks") + let extra = [ + "flowID": flowID, + "reason": "other clients repairing" + ] + let event = Event(category: "sync", method: "repair", object: "aborted", extra: extra) + self.recordTelemetry(event: event) + return deferMaybe(false) + } + + log.info("Starting a repair, looking for \(ids.count) missing item(s)") + // Setup our prefs to indicate we are on our way. + self.flowID = flowID + self.currentIDs = ids + self.currentState = .needNewClient + + let extra = ["flowID": flowID, "numIDs": String(ids.count)] + let event = Event(category: "sync", method: "repair", object: "started", extra: extra) + self.recordTelemetry(event: event) + + return self.continueRepairs() + } + } + + /** + * Work out what state our current repair request is in, and whether it can + * proceed to a new state. + * + * - returns: true if we could continue the repair - even if the state didn't + * actually move. Returns false if we aren't actually repairing. + */ + func continueRepairs(response: RepairResponse? = nil) -> Deferred> { + // Note that "aborted" and "finished" should never be current when this + // function returns - this function resets to notRepairing in those cases. + guard self.currentState != .notRepairing else { + return deferMaybe(false) + } + + var abortReason: String? + + func runStateMachine(iteration: Int = 0) -> Deferred> { + let state = self.currentState + log.info("continueRepairs starting with state \(state)") + + return self.advanceRepairState(state: state, response: response).bind { result in + let newState: RepairState + if result.isSuccess { + newState = result.successValue! + log.info("continueRepairs has next state \(newState)") + } else { + let failure = result.failureValue! + if failure is AbortRepairError { + let reason = failure.description + log.info("Repair has been aborted: \(reason)") + newState = .aborted + abortReason = reason + } else { + return deferMaybe(failure) + } + } + let done = deferMaybe((state: state, newState: newState)) + + if newState == .aborted { + return done + } + + self.currentState = newState + if state == newState { + return done + } + + // we loop until the state doesn't change - but enforce a max of 10 times + // to prevent errors causing infinite loops. + return (iteration < 10) ? runStateMachine(iteration: iteration + 1) : done + } + } + + return runStateMachine() >>== { stateMachineResult in + let state = stateMachineResult.state + let newState = stateMachineResult.newState + + if state != newState { + log.error("continueRepairs spun without getting a new state") + } + + if newState == .finished || newState == .aborted { + let method = newState == .finished ? "finished" : "aborted" + var extra = [ + "flowID": self.flowID, + "numIDs": String(self.currentIDs.count), + ] + if abortReason != nil { + extra["reason"] = abortReason + } + + let event = Event(category: "sync", method: "repair", object: method, extra: extra) + self.recordTelemetry(event: event) + + self.prefs.clearAll() + } + return deferMaybe(true) + } + } + + func recordTelemetry(event: Event) { + var events = self.basePrefs.arrayForKey(PrefKeySyncEvents) as? [Data] ?? [] + + if let data = event.pickle(), event.validate() { + events.append(data) + self.basePrefs.setObject(events, forKey: PrefKeySyncEvents) + } else { + log.info("Event not recorded due to validation failure or pickling error!") + } + } + + private func advanceRepairState(state: RepairState, response: RepairResponse?) -> Deferred> { + return self.anyClientsRepairing(flowID: self.flowID) >>== { anyClientsRepairing in + guard !anyClientsRepairing else { + return deferMaybe(AbortRepairError("other clients repairing")) + } + + switch state { + + case .sentRequest, .sentSecondRequest: + let flowID = self.flowID + guard let clientID = self.currentRemoteClient else { + return deferMaybe(InvalidStateError("currentRemoteClient should be defined")) + } + if let response = response { + // We got an explicit response - let's see how we went. + return deferMaybe(self.handleResponse(state: state, response: response)) + } + // So we've sent a request - and don't yet have a response. See if the + // client we sent it to has removed it from its list (ie, whether it + // has synced since we wrote the request.) + return self.remoteClients.getClientWithId(clientID) >>== { client in + guard let client = client else { + // hrmph - the client has disappeared. + log.info("previously requested client \(clientID) has vanished - moving to next step") + let extra = [ + "deviceID": self.scratchpad.hashedDeviceID ?? "unknown_deviceID", + "flowID": flowID + ] + + let event = Event(category: "sync", method: "repair", object: "abandon", value: "missing", extra: extra) + self.recordTelemetry(event: event) + + return deferMaybe(.needNewClient) + } + return self.isCommandPending(clientID: clientID, flowID: flowID) >>== { isCommandPending in + if isCommandPending { + // So the command we previously sent is still queued for the client + // (ie, that client is yet to have synced). Let's see if we should + // give up on that client. + if self.lastRepair + ResponseIntervalTimeout <= Date.now() { + log.info("previous request to client \(clientID) is pending, but has taken too long") + // XXX - should we remove the command? + let extra = [ + "deviceID": self.scratchpad.hashedDeviceID ?? "unknown_deviceID", + "flowID": flowID + ] + + let event = Event(category: "sync", method: "repair", object: "abandon", value: "silent", extra: extra) + self.recordTelemetry(event: event) + + return deferMaybe(.needNewClient) + } + // Let's continue to wait for that client to respond. + // We are now sure that timeLeft > 0, so we can calculate it (Timestamp type is UInt64) + let timeLeft = self.lastRepair + ResponseIntervalTimeout - Date.now() + log.verbose("previous request to client \(clientID) has \(timeLeft) seconds before we give up on it") + return deferMaybe(state) + } + // The command isn't pending - if this was the first request, we give + // it another go (as that client may have cleared the command but is yet + // to complete the sync) + // XXX - note that this is no longer true - the responders don't remove + // their command until they have written a response. This might mean + // we could drop the entire STATE.SENT_SECOND_REQUEST concept??? + if state == .sentRequest { + log.info("previous request to client \(clientID) was removed - trying a second time") + return self.writeRequest(client: client) >>== { success in + return deferMaybe(.sentSecondRequest) + } + } else { + // this was the second time around, so give up on this client + log.info("previous 2 requests to client \(clientID) were removed - need a new client") + return deferMaybe(.needNewClient) + } + } + } + + case .needNewClient: + // We need to find a new client to request. + return self.findNextClient() >>== { client in + guard let nextClient = client else { + return deferMaybe(.finished) + } + if let currentRemoteClient = self.currentRemoteClient { + var previousRemoteClients = self.previousRemoteClients + previousRemoteClients.append(currentRemoteClient) + self.previousRemoteClients = previousRemoteClients + } + self.currentRemoteClient = nextClient.guid! + return self.writeRequest(client: nextClient) >>== { success in + return deferMaybe(.sentRequest) + } + } + + case .aborted: + break // our caller will take the abort action. + + case .finished: + break + + case .notRepairing: + // No repair is in progress. This is a common case, so only log trace. + log.verbose("continue repairs called but no repair in progress.") + break + } + return deferMaybe(state) + } + } + + /** + * Handle being in the SENT_REQUEST or SENT_SECOND_REQUEST state with an + * explicit response. + */ + private func handleResponse(state: RepairState, response: RepairResponse) -> RepairState { + guard let clientID = self.currentRemoteClient else { + log.error("Cannot handle the response of an unknown client") + return state + } + let flowID = self.flowID + guard response.flowID == flowID && response.clientID == clientID && + response.request == "upload" else { + log.info("got a response to a different repair request: \(response)") + // hopefully just a stale request that finally came in (either from + // an entirely different repair flow, or from a client we've since + // given up on.) It doesn't mean we need to abort though... + return state + } + // Pull apart the response and see if it provided everything we asked for. + let remainingIDs = Array(Set(self.currentIDs).subtracting(Set(response.ids))) + log.info("repair response from \(clientID) provided '\(response.ids)', remaining now '\(remainingIDs)'") + self.currentIDs = remainingIDs + let newState: RepairState + if remainingIDs.count > 0 { + // try a new client for the remaining ones. + newState = .needNewClient + } else { + newState = .finished + } + // record telemetry about this + let extra = [ + "deviceID": scratchpad.hashedDeviceID ?? "unknown_deviceID", + "flowID": flowID, + "numIDs": String(response.ids.count) + ] + let event = Event(category: "sync", method: "repair", object: "response", value: "upload", extra: extra) + recordTelemetry(event: event) + return newState + } + + /** + * Issue a repair request to a specific client. + */ + private func writeRequest(client: RemoteClient) -> Success { + log.verbose("writing repair request to client \(client.guid!)") + let ids = self.currentIDs + let flowID = self.flowID + // Post a command to that client. + let request = RepairRequest(collection: "bookmarks", request: "upload", flowID: flowID, requestor: self.scratchpad.clientGUID, ids: ids) + + return self.remoteClients.insertCommand(request.toSyncCommand(), forClients: [client]) >>== { (_: Int) -> Success in + self.lastRepair = Date.now() + // record telemetry about this + let extra = [ + "deviceID": self.scratchpad.hashedDeviceID ?? "unknown_deviceID", + "flowID": flowID, + "numIDs": String(ids.count), + ] + + let event = Event(category: "sync", method: "repair", object: "request", value: "upload", extra: extra) + self.recordTelemetry(event: event) + + return succeed() + } + } + + private func findNextClient() -> Deferred> { + var alreadyDone = self.previousRemoteClients + if let currentRemoteClient = self.currentRemoteClient { + alreadyDone.append(currentRemoteClient) + } + return self.remoteClients.getClients() >>== { remoteClients in + // we want to consider the most-recently synced clients first. + let sortedClients = remoteClients.sorted(by: { (a, b) -> Bool in + return a.modified > b.modified + }) + for client in sortedClients { + log.verbose("findNextClient considering \(client)") + guard let clientID = client.guid else { + continue + } + if !alreadyDone.contains(clientID) && self.isSuitableClient(client) { + return deferMaybe(client) + } + } + log.verbose("findNextClient found no client") + return deferMaybe(nil) + } + } + + private func isSuitableClient(_ client: RemoteClient) -> Bool { + if let type = client.type, + let version = client.version, + let major = Int(version.components(separatedBy: ".")[0]) { + return type == "desktop" && major > 53 + } + return false + } + + private func getProblemIDs(_ validations: [BufferInconsistency: [GUID]]) -> [GUID] { + return validations.reduce([]) { acc, pair in acc + pair.value } + } + + private func anyClientsRepairing(flowID: String? = nil) -> Deferred> { + return self.remoteClients.getCommands() >>== { allCommands in + return deferMaybe(allCommands.contains { (clientID: GUID, commands: [SyncCommand]) in + return commands.contains { (command: SyncCommand) in + let json = JSON(parseJSON: command.value) + guard let cmdName = json["command"].string, + let argsArray = json["args"].array, + let argObj = argsArray[0].dictionary, + let argCol = argObj["collection"]?.string, + let argFlowID = argObj["flowID"]?.string + else { + return false + } + return !((cmdName != "repairResponse" && cmdName != "repairRequest") || + argsArray.count != 1 || + argCol != "bookmarks" || + argFlowID == flowID + ) + } + }) + } + } + + private func isCommandPending(clientID: String, flowID: String) -> Deferred> { + return self.remoteClients.getCommands() >>== { allCommands in + guard let commands = allCommands[clientID] else { + return deferMaybe(false) + } + return deferMaybe(commands.contains { (command: SyncCommand) in + let json = JSON(parseJSON: command.value) + guard let cmdName = json["command"].string, + let argsArray = json["args"].array, + let argObj = argsArray[0].dictionary, + let argCol = argObj["collection"]?.string, + let argFlowID = argObj["flowID"]?.string, + let argRequest = argObj["request"]?.string + else { + return false + } + return cmdName == "repairRequest" && argsArray.count == 1 && + argCol == "bookmarks" && argRequest == "upload" && + argFlowID == flowID + }) + } + } + + private var flowID: String { + get { return self.prefs.stringForKey(PrefFlowID)! } + set { self.prefs.setString(newValue, forKey: PrefFlowID) } + } + + private var currentIDs: [String] { + get { return self.prefs.stringArrayForKey(PrefMissingIDs) ?? [String]() } + set { self.prefs.setObject(newValue, forKey: PrefMissingIDs) } + } + + private var currentRemoteClient: String? { + get { return self.prefs.stringForKey(PrefCurrentClient) } + set { self.prefs.setString(newValue!, forKey: PrefCurrentClient) } + } + + private var previousRemoteClients: [String] { + get { return self.prefs.stringArrayForKey(PrefPreviousClients) ?? [String]() } + set { self.prefs.setObject(newValue, forKey: PrefPreviousClients) } + } + + private var lastRepair: Timestamp { + get { return self.prefs.timestampForKey(PrefLastRepair)! } + set { self.prefs.setTimestamp(newValue, forKey: PrefLastRepair) } + } + + private var currentState: RepairState { + get { + let Default = RepairState.notRepairing + guard let raw = self.prefs.stringForKey(PrefCurrentState) else { + return Default + } + return RepairState(rawValue: raw) ?? Default + } + set { self.prefs.setString(newValue.rawValue, forKey: PrefCurrentState) } + } +} diff --git a/mobile/ios/Sync/Synchronizers/Bookmarks/BookmarksSynchronizer.swift b/mobile/ios/Sync/Synchronizers/Bookmarks/BookmarksSynchronizer.swift new file mode 100644 index 0000000000..cd8aff5346 --- /dev/null +++ b/mobile/ios/Sync/Synchronizers/Bookmarks/BookmarksSynchronizer.swift @@ -0,0 +1,511 @@ +/* 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 Deferred +import Foundation +import Shared +import Storage +import XCGLogger + +private let log = Logger.syncLogger + +typealias UploadFunction = ([Record], _ lastTimestamp: Timestamp?, _ onUpload: @escaping (POSTResult, Timestamp?) -> DeferredTimestamp) -> DeferredTimestamp + +class TrivialBookmarkStorer: BookmarkStorer { + let uploader: UploadFunction + init(uploader: @escaping UploadFunction) { + self.uploader = uploader + } + + func applyUpstreamCompletionOp(_ op: UpstreamCompletionOp, itemSources: ItemSources, trackingTimesInto local: LocalOverrideCompletionOp) -> Deferred> { + log.debug("Uploading \(op.records.count) modified records.") + log.debug("Uploading \(op.amendChildrenFromBuffer.count) amended buffer records.") + log.debug("Uploading \(op.amendChildrenFromMirror.count) amended mirror records.") + log.debug("Uploading \(op.amendChildrenFromLocal.count) amended local records.") + + var records: [Record] = [] + records.reserveCapacity(op.records.count + op.amendChildrenFromBuffer.count + op.amendChildrenFromLocal.count + op.amendChildrenFromMirror.count) + records.append(contentsOf: op.records) + + func accumulateFromAmendMap(_ itemsWithNewChildren: [GUID: [GUID]], fetch: ([GUID: [GUID]]) -> Maybe<[GUID: BookmarkMirrorItem]>) throws /* MaybeErrorType */ { + if itemsWithNewChildren.isEmpty { + return + } + + let fetched = fetch(itemsWithNewChildren) + guard let items = fetched.successValue else { + log.warning("Couldn't fetch items to amend.") + throw fetched.failureValue! + } + + items.forEach { (guid, item) in + let payload = item.asPayloadWithChildren(itemsWithNewChildren[guid]) + let mappedGUID = payload["id"].string ?? guid + let record = Record(id: mappedGUID, payload: payload) + records.append(record) + } + } + + do { + try accumulateFromAmendMap(op.amendChildrenFromBuffer, fetch: { itemSources.buffer.getBufferItemsWithGUIDs($0.keys).value }) + try accumulateFromAmendMap(op.amendChildrenFromMirror, fetch: { itemSources.mirror.getMirrorItemsWithGUIDs($0.keys).value }) + try accumulateFromAmendMap(op.amendChildrenFromLocal, fetch: { itemSources.local.getLocalItemsWithGUIDs($0.keys).value }) + } catch { + return deferMaybe(error as MaybeErrorType) + } + + var success: [GUID] = [] + var failed: [GUID: String] = [:] + + func onUpload(_ result: POSTResult, lastModified: Timestamp?) -> DeferredTimestamp { + success.append(contentsOf: result.success) + result.failed.forEach { guid, message in + failed[guid] = message + } + + log.debug("Uploaded records got timestamp \(lastModified ??? "nil").") + let modified = lastModified ?? 0 + local.setModifiedTime(modified, guids: result.success) + return deferMaybe(modified) + } + + // Chain the last upload timestamp right into our lastFetched timestamp. + // This is what Sync clients tend to do, but we can probably do better. + return uploader(records, op.ifUnmodifiedSince, onUpload) + // As if we uploaded everything in one go. + >>> { deferMaybe(POSTResult(success: success, failed: failed)) } + } +} + +open class MalformedRecordError: MaybeErrorType, SyncPingFailureFormattable { + open var description: String { + return "Malformed record." + } + open var failureReasonName: SyncPingFailureReasonName { + return .otherError + } +} + +// MARK: - External synchronizer interface. + +open class BufferingBookmarksSynchronizer: TimestampedSingleCollectionSynchronizer, Synchronizer { + public required init(scratchpad: Scratchpad, delegate: SyncDelegate, basePrefs: Prefs, why: SyncReason) { + super.init(scratchpad: scratchpad, delegate: delegate, basePrefs: basePrefs, why: why, collection: "bookmarks") + } + + override var storageVersion: Int { + return BookmarksStorageVersion + } + + fileprivate func buildMobileRootRecord(_ local: LocalItemSource, _ buffer: BufferItemSource, additionalChildren: [BookmarkMirrorItem], deletedChildren: [GUID]) -> Deferred>> { + let newBookmarkGUIDs = additionalChildren.map { $0.guid } + return buffer.getBufferItemWithGUID(BookmarkRoots.MobileFolderGUID).bind { maybeMobileRoot in + // Update (or create!) the Mobile Root folder with its new children. + if let mobileRoot = maybeMobileRoot.successValue { + return buffer.getBufferChildrenGUIDsForParent(mobileRoot.guid) + .map { $0.map({ (mobileRoot: mobileRoot, children: $0.filter { !deletedChildren.contains($0) } + newBookmarkGUIDs) }) } + } else { + return local.getLocalItemWithGUID(BookmarkRoots.MobileFolderGUID) + .map { $0.map({ (mobileRoot: $0, children: newBookmarkGUIDs) }) } + } + } >>== { (mobileRoot: BookmarkMirrorItem, children: [GUID]) in + let payload = mobileRoot.asPayloadWithChildren(children) + guard let mappedGUID = payload["id"].string else { + return deferMaybe(MalformedRecordError()) + } + return deferMaybe(Record(id: mappedGUID, payload: payload)) + } + } + + func buildMobileRootAndChildrenRecords(_ local: LocalItemSource, _ buffer: BufferItemSource, additionalChildren: [BookmarkMirrorItem], deletedChildren: [GUID]) -> Deferred, childrenRecords: [Record])>> { + let childrenRecords = + additionalChildren.map { bkm -> Record in + let payload = bkm.asPayload() + let mappedGUID = payload["id"].string ?? bkm.guid + return Record(id: mappedGUID, payload: payload) + } + + deletedChildren.map { guid -> Record in + let payload = BookmarkBasePayload.deletedPayload(guid) + let mappedGUID = payload["id"].string ?? guid + return Record(id: mappedGUID, payload: payload) + } + + return self.buildMobileRootRecord(local, buffer, additionalChildren: additionalChildren, deletedChildren: deletedChildren) >>== { mobileRootRecord in + return deferMaybe((mobileRootRecord: mobileRootRecord, childrenRecords: childrenRecords)) + } + } + + func uploadSomeLocalRecords(_ storage: SyncableBookmarks & LocalItemSource & MirrorItemSource, _ mirrorer: BookmarksMirrorer, _ bookmarksClient: Sync15CollectionClient, mobileRootRecord: Record, childrenRecords: [Record]) -> Success { + var newBookmarkGUIDs: [GUID] = [] + var deletedBookmarksGUIDs: [GUID] = [] + for record in childrenRecords { + // No mutable l-values in Swift :( + if record.payload.deleted { + deletedBookmarksGUIDs.append(record.id) + } else { + newBookmarkGUIDs.append(record.id) + } + } + let records = [mobileRootRecord] + childrenRecords + return self.uploadRecordsSingleBatch(records, lastTimestamp: mirrorer.lastModified, storageClient: bookmarksClient) >>== { (timestamp: Timestamp, succeeded: [GUID]) -> Success in + + let bufferValuesToMoveFromLocal = Set(newBookmarkGUIDs).intersection(Set(succeeded)) + let deletedValues = Set(deletedBookmarksGUIDs).intersection(Set(succeeded)) + let mobileRoot = (mobileRootRecord.payload as MirrorItemable).toMirrorItem(timestamp) + let bufferOP = BufferUpdatedCompletionOp(bufferValuesToMoveFromLocal: bufferValuesToMoveFromLocal, deletedValues: deletedValues, mobileRoot: mobileRoot, modifiedTime: timestamp) + return storage.applyBufferUpdatedCompletionOp(bufferOP) >>> { + mirrorer.advanceNextDownloadTimestampTo(timestamp: timestamp) // We need to advance our batching downloader timestamp to match. See Bug 1253458. + return succeed() + } + } + } + + open func synchronizeBookmarksToStorage(_ storage: SyncableBookmarks & LocalItemSource & MirrorItemSource, usingBuffer buffer: BookmarkBufferStorage & BufferItemSource, withServer storageClient: Sync15StorageClient, info: InfoCollections, greenLight: @escaping () -> Bool, remoteClientsAndTabs: RemoteClientsAndTabs) -> SyncResult { + if self.prefs.boolForKey("dateAddedMigrationDone") != true { + self.lastFetched = 0 + self.prefs.setBool(true, forKey: "dateAddedMigrationDone") + } + + if let reason = self.reasonToNotSync(storageClient) { + return deferMaybe(.notStarted(reason)) + } + + let encoder = RecordEncoder(decode: BookmarkType.somePayloadFromJSON, encode: { $0.json }) + + guard let bookmarksClient = self.collectionClient(encoder, storageClient: storageClient) else { + log.error("Couldn't make bookmarks factory.") + return deferMaybe(FatalError(message: "Couldn't make bookmarks factory.")) + } + + let start = Date.nowMicroseconds() + let mirrorer = BookmarksMirrorer(storage: buffer, client: bookmarksClient, basePrefs: self.prefs, collection: "bookmarks", statsSession: self.statsSession) + let storer = TrivialBookmarkStorer(uploader: { records, lastTimestamp, onUpload in + let timestamp = lastTimestamp ?? self.lastFetched + return self.uploadRecords(records, lastTimestamp: timestamp, storageClient: bookmarksClient, onUpload: onUpload) + >>== effect { timestamp in + // We need to advance our batching downloader timestamp to match. See Bug 1253458. + self.setTimestamp(timestamp) + mirrorer.advanceNextDownloadTimestampTo(timestamp: timestamp) + } + }) + + statsSession.start() + + let doMirror = mirrorer.go(info: info, greenLight: greenLight) + let run: SyncResult + + if !AppConstants.shouldMergeBookmarks { + run = doMirror >>== { result -> SyncResult in + // Validate the buffer to report statistics. + if case .completed = result { + log.debug("Validating completed buffer download.") + return buffer.validate().bind { validationResult in + guard let invalidError = validationResult.failureValue as? BufferInvalidError else { + return deferMaybe(result) + } + return buffer.getUpstreamRecordCount().bind { checked -> Success in + self.statsSession.validationStats = self.validationStatsFrom(error: invalidError, checked: checked) + return self.maybeStartRepairProcedure(greenLight: greenLight, error: invalidError, remoteClientsAndTabs: remoteClientsAndTabs) + } >>> { + return deferMaybe(result) + } + } + } + return deferMaybe(result) + } >>== { result in + guard AppConstants.MOZ_SIMPLE_BOOKMARKS_SYNCING else { + return deferMaybe(result) + } + guard case .completed = result else { + return deferMaybe(result) + } + + // -1 because we also need to upload the mobile root. + return (storage.getLocalBookmarksModifications(limit: bookmarksClient.maxBatchPostRecords - 1) >>== { (deletedGUIDs, newBookmarks) -> Success in + guard newBookmarks.count > 0 || deletedGUIDs.count > 0 else { + return succeed() + } + return self.buildMobileRootAndChildrenRecords(storage, buffer, additionalChildren: newBookmarks, deletedChildren: deletedGUIDs) >>== { (mobileRootRecord, childrenRecords) in + return self.uploadSomeLocalRecords(storage, mirrorer, bookmarksClient, mobileRootRecord: mobileRootRecord, childrenRecords: childrenRecords) + } + }).bind { simpleSyncingResult in + if let failure = simpleSyncingResult.failureValue { + let description = failure is RecordTooLargeError ? "Record too large" : failure.description + Sentry.shared.send(message: "Failed to simple sync bookmarks", tag: SentryTag.bookmarks, severity: .error, description: description) + } + return deferMaybe(result) + } + } + } else { + run = doMirror >>== { result in + // Only bother trying to sync if the mirror operation wasn't interrupted or partial. + if case .completed = result { + return buffer.validate().bind { result in + if let invalidError = result.failureValue as? BufferInvalidError { + return buffer.getUpstreamRecordCount().bind { checked in + self.statsSession.validationStats = self.validationStatsFrom(error: invalidError, checked: checked) + return self.maybeStartRepairProcedure(greenLight: greenLight, error: invalidError, remoteClientsAndTabs: remoteClientsAndTabs) >>> { + return deferMaybe(invalidError) + } + } + } + + let applier = MergeApplier(buffer: buffer, storage: storage, client: storer, statsSession: self.statsSession, greenLight: greenLight) + return applier.go() + } + } + return deferMaybe(result) + } + } + + run.upon { result in + let end = Date.nowMicroseconds() + let duration = end - start + log.info("Bookmark \(AppConstants.shouldMergeBookmarks ? "sync" : "mirroring") took \(duration)µs. Result was \(result.successValue?.description ?? result.failureValue?.description ?? "failure")") + } + + return run + } + + private func validationStatsFrom(error: BufferInvalidError, checked: Int?) -> ValidationStats { + let problems = error.inconsistencies.map { ValidationProblem(name: $0.trackingEvent, count: $1.count) } + return ValidationStats(problems: problems, took: error.validationDuration, checked: checked) + } + + private func maybeStartRepairProcedure(greenLight: () -> Bool, error: BufferInvalidError, remoteClientsAndTabs: RemoteClientsAndTabs) -> Success { + guard AppConstants.MOZ_BOOKMARKS_REPAIR_REQUEST && greenLight() else { + return succeed() + } + log.warning("Buffer inconsistent, starting repair procedure") + let repairer = BookmarksRepairRequestor(scratchpad: self.scratchpad, basePrefs: self.basePrefs, remoteClients: remoteClientsAndTabs) + return repairer.startRepairs(validationInfo: error.inconsistencies).bind { result in + if let repairFailure = result.failureValue { + Sentry.shared.send(message: "Bookmarks repair failure", tag: SentryTag.bookmarks, severity: .error, description: repairFailure.description) + } else { + Sentry.shared.send(message: "Bookmarks repair succeeded", tag: SentryTag.bookmarks, severity: .debug) + } + return succeed() + } + } +} + +class MergeApplier { + let greenLight: () -> Bool + let buffer: BookmarkBufferStorage + let storage: SyncableBookmarks + let client: BookmarkStorer + let merger: BookmarksStorageMerger + let statsSession: SyncEngineStatsSession + + init(buffer: BookmarkBufferStorage & BufferItemSource, storage: SyncableBookmarks & LocalItemSource & MirrorItemSource, client: BookmarkStorer, statsSession: SyncEngineStatsSession, greenLight: @escaping () -> Bool) { + self.greenLight = greenLight + self.buffer = buffer + self.storage = storage + self.merger = ThreeWayBookmarksStorageMerger(buffer: buffer, storage: storage) + self.client = client + self.statsSession = statsSession + } + + // Exposed for use from tests. + func applyResult(_ result: BookmarksMergeResult) -> Success { + return result.applyToClient(self.client, storage: self.storage, buffer: self.buffer) + } + + func go() -> SyncResult { + guard self.greenLight() else { + log.info("Green light turned red; not merging bookmarks.") + return deferMaybe(SyncStatus.completed(statsSession.end())) + } + + return self.merger.merge() + >>== self.applyResult + >>> always(SyncStatus.completed(statsSession.end())) + } +} + +/** + * The merger takes as input an existing storage state (mirror and local override), + * a buffer of new incoming records that relate to the mirror, and performs a three-way + * merge. + * + * The merge itself does not mutate storage. The result of the merge is conceptually a + * tuple: a new mirror state, a set of reconciled + locally changed records to upload, + * and two checklists of buffer and local state to discard. + * + * Typically the merge will be complete, resulting in a new mirror state, records to + * upload, and completely emptied buffer and local. In the case of partial inconsistency + * this will not be the case; incomplete subtrees will remain in the buffer. (We don't + * expect local subtrees to ever be incomplete.) + * + * It is expected that the caller will immediately apply the result in this order: + * + * 1. Upload the remote changes, if any. If this fails we can retry the entire process. + * + * 2(a). Apply the local changes, if any. If this fails we will re-download the records + * we just uploaded, and should reach the same end state. + * This step takes a timestamp key from (1), because pushing a record into the mirror + * requires a server timestamp. + * + * 2(b). Switch to the new mirror state. If this fails, we should find that our reconciled + * server contents apply neatly to our mirror and empty local, and we'll reach the + * same end state. + * + * Mirror state is applied in a sane order to respect relational constraints, even though + * we configure sqlite to defer constraint validation until the transaction is committed. + * That means: + * + * - Add any new records in the value table. + * - Change any existing records in the value table. + * - Update structure. + * - Remove records from the value table. + * + * 3. Apply buffer changes. We only do this after the mirror has advanced; if we fail to + * clean up the buffer, it'll reconcile neatly with the mirror on a subsequent try. + * + * 4. Update bookkeeping timestamps. If this fails we will download uploaded records, + * find they match, and have no repeat merging work to do. + * + * The goal of merging is that the buffer is empty (because we reconciled conflicts and + * updated the server), our local overlay is empty (because we reconciled conflicts and + * applied our changes to the server), and the mirror matches the server. + * + * Note that upstream application is robust: we can use XIUS to ensure that writes don't + * race. Buffer application is similarly robust, because this code owns all writes to the + * buffer. Local and mirror application, however, is not: the user's actions can cause + * changes to write to the database before we're done applying the results of a sync. + * We mitigate this a little by being explicit about the local changes that we're flushing + * (rather than, say, `DELETE FROM local`), but to do better we'd need change detection + * (e.g., an in-memory monotonic counter) or locking to prevent bookmark operations from + * racing. Later! + */ +protocol BookmarksStorageMerger: class { + init(buffer: BookmarkBufferStorage & BufferItemSource, storage: SyncableBookmarks & LocalItemSource & MirrorItemSource) + func merge() -> Deferred> +} + +class NoOpBookmarksMerger: BookmarksStorageMerger { + let buffer: BookmarkBufferStorage & BufferItemSource + let storage: SyncableBookmarks & LocalItemSource & MirrorItemSource + + required init(buffer: BookmarkBufferStorage & BufferItemSource, storage: SyncableBookmarks & LocalItemSource & MirrorItemSource) { + self.buffer = buffer + self.storage = storage + } + + func merge() -> Deferred> { + return deferMaybe(BookmarksMergeResult.NoOp(ItemSources(local: self.storage, mirror: self.storage, buffer: self.buffer))) + } +} + +class ThreeWayBookmarksStorageMerger: BookmarksStorageMerger { + let buffer: BookmarkBufferStorage & BufferItemSource + let storage: SyncableBookmarks & LocalItemSource & MirrorItemSource + + required init(buffer: BookmarkBufferStorage & BufferItemSource, storage: SyncableBookmarks & LocalItemSource & MirrorItemSource) { + self.buffer = buffer + self.storage = storage + } + + // MARK: - BookmarksStorageMerger. + + // Trivial one-way sync. + fileprivate func applyLocalDirectlyToMirror() -> Deferred> { + // Theoretically, we do the following: + // * Construct a virtual bookmark tree overlaying local on the mirror. + // * Walk the tree to produce Sync records. + // * Upload those records. + // * Flatten that tree into the mirror, clearing local. + // + // This is simpler than a full three-way merge: it's tree delta then flatten. + // + // But we are confident that our local changes, when overlaid on the mirror, are + // consistent. So we can take a little bit of a shortcut: process records + // directly, rather than building a tree. + // + // So do the following: + // * Take everything in `local` and turn it into a Sync record. This means pulling + // folder hierarchies out of localStructure, values out of local, and turning + // them into records. Do so in hierarchical order if we can, and set sortindex + // attributes to put folders first. + // * Upload those records in as few batches as possible. Ensure that each batch + // is consistent, if at all possible, though we're hoping for server support for + // atomic writes. + // * Take everything in local that was successfully uploaded and move it into the + // mirror, using the timestamps we tracked from the upload. + // + // Optionally, set 'again' to true in our response, and do this work only for a + // particular subtree (e.g., a single root, or a single branch of changes). This + // allows us to make incremental progress. + + // TODO + log.debug("No special-case local-only merging yet. Falling back to three-way merge.") + return self.threeWayMerge() + } + + fileprivate func applyIncomingDirectlyToMirror() -> Deferred> { + // If the incoming buffer is consistent -- and the result of the mirrorer + // gives us a hint about that! -- then we can move the buffer records into + // the mirror directly. + // + // Note that this is also true for entire subtrees: if none of the children + // of, say, 'menu________' are modified locally, then we can apply it without + // merging. + // + // TODO + log.debug("No special-case remote-only merging yet. Falling back to three-way merge.") + return self.threeWayMerge() + } + + // This is exposed for testing. + func getMerger() -> Deferred> { + return self.storage.treesForEdges() >>== { (local, remote) in + // At this point *might* have two empty trees. This should only be the case if + // there are value-only changes (e.g., a renamed bookmark). + // We don't fail in that case, but we could optimize here. + + // Find the mirror tree so we can compare. + return self.storage.treeForMirror() >>== { mirror in + // At this point we know that there have been changes both locally and remotely. + // (Or, in the general case, changes either locally or remotely.) + + let itemSources = ItemSources(local: CachingLocalItemSource(source: self.storage), mirror: CachingMirrorItemSource(source: self.storage), buffer: CachingBufferItemSource(source: self.buffer)) + return deferMaybe(ThreeWayTreeMerger(local: local, mirror: mirror, remote: remote, itemSources: itemSources)) + } + } + } + + func getMergedTree() -> Deferred> { + return self.getMerger() >>== { $0.produceMergedTree() } + } + + func threeWayMerge() -> Deferred> { + return self.getMerger() >>== { $0.produceMergedTree() >>== $0.produceMergeResultFromMergedTree } + } + + func merge() -> Deferred> { + return self.buffer.isEmpty() >>== { noIncoming in + + // TODO: the presence of empty desktop roots in local storage + // isn't something we really need to worry about. Can we skip it here? + + return self.storage.isUnchanged() >>== { noOutgoing in + switch (noIncoming, noOutgoing) { + case (true, true): + // Nothing to do! + log.debug("No incoming and no outgoing records: no-op.") + return deferMaybe(BookmarksMergeResult.NoOp(ItemSources(local: self.storage, mirror: self.storage, buffer: self.buffer))) + case (true, false): + // No incoming records to apply. Unilaterally apply local changes. + return self.applyLocalDirectlyToMirror() + case (false, true): + // No outgoing changes. Unilaterally apply remote changes if they're consistent. + return self.buffer.validate() >>> self.applyIncomingDirectlyToMirror + default: + // Changes on both sides. Merge. + return self.buffer.validate() >>> self.threeWayMerge + } + } + } + } +} diff --git a/mobile/ios/Sync/Synchronizers/Bookmarks/Merging.swift b/mobile/ios/Sync/Synchronizers/Bookmarks/Merging.swift new file mode 100644 index 0000000000..40f1a23a65 --- /dev/null +++ b/mobile/ios/Sync/Synchronizers/Bookmarks/Merging.swift @@ -0,0 +1,336 @@ +/* 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 Deferred +import Foundation +import Shared +import Storage +import XCGLogger + +private let log = Logger.syncLogger + +// Because generic protocols in Swift are a pain in the ass. +public protocol BookmarkStorer: class { + // TODO: this should probably return a timestamp. + func applyUpstreamCompletionOp(_ op: UpstreamCompletionOp, itemSources: ItemSources, trackingTimesInto local: LocalOverrideCompletionOp) -> Deferred> +} + +open class UpstreamCompletionOp: PerhapsNoOp { + // Upload these records from the buffer, but with these child lists. + open var amendChildrenFromBuffer: [GUID: [GUID]] = [:] + + // Upload these records from the mirror, but with these child lists. + open var amendChildrenFromMirror: [GUID: [GUID]] = [:] + + // Upload these records from local, but with these child lists. + open var amendChildrenFromLocal: [GUID: [GUID]] = [:] + + // Upload these records as-is. + open var records: [Record] = [] + + open let ifUnmodifiedSince: Timestamp? + + open var isNoOp: Bool { + return records.isEmpty + } + + public init(ifUnmodifiedSince: Timestamp?=nil) { + self.ifUnmodifiedSince = ifUnmodifiedSince + } +} + +open class BookmarksMergeResult: PerhapsNoOp { + let uploadCompletion: UpstreamCompletionOp + let overrideCompletion: LocalOverrideCompletionOp + let bufferCompletion: BufferCompletionOp + let itemSources: ItemSources + + open var isNoOp: Bool { + return self.uploadCompletion.isNoOp && + self.overrideCompletion.isNoOp && + self.bufferCompletion.isNoOp + } + + func applyToClient(_ client: BookmarkStorer, storage: SyncableBookmarks, buffer: BookmarkBufferStorage) -> Success { + return client.applyUpstreamCompletionOp(self.uploadCompletion, itemSources: self.itemSources, trackingTimesInto: self.overrideCompletion) + >>> { storage.applyLocalOverrideCompletionOp(self.overrideCompletion, itemSources: self.itemSources) } + >>> { buffer.applyBufferCompletionOp(self.bufferCompletion, itemSources: self.itemSources) } + } + + init(uploadCompletion: UpstreamCompletionOp, overrideCompletion: LocalOverrideCompletionOp, bufferCompletion: BufferCompletionOp, itemSources: ItemSources) { + self.uploadCompletion = uploadCompletion + self.overrideCompletion = overrideCompletion + self.bufferCompletion = bufferCompletion + self.itemSources = itemSources + } + + static func NoOp(_ itemSources: ItemSources) -> BookmarksMergeResult { + return BookmarksMergeResult(uploadCompletion: UpstreamCompletionOp(), overrideCompletion: LocalOverrideCompletionOp(), bufferCompletion: BufferCompletionOp(), itemSources: itemSources) + } +} + +// MARK: - Errors. + +open class BookmarksMergeError: MaybeErrorType, SyncPingFailureFormattable { + fileprivate let error: Error? + + init(error: Error?=nil) { + self.error = error + } + + open var description: String { + return "Merge error: \(self.error ??? "nil")" + } + + open var failureReasonName: SyncPingFailureReasonName { + return .otherError + } +} + +open class BookmarksMergeConsistencyError: BookmarksMergeError { + override open var description: String { + return "Merge consistency error" + } +} + +open class BookmarksMergeErrorTreeIsUnrooted: BookmarksMergeConsistencyError { + open let roots: Set + + public init(roots: Set) { + self.roots = roots + } + + override open var description: String { + return "Tree is unrooted: roots are \(self.roots)" + } +} + +enum MergeState { + case unknown // Default state. + case unchanged // Nothing changed: no work needed. + case remote // Take the associated remote value. + case local // Take the associated local value. + case new(value: T) // Take this synthesized value. + + var isUnchanged: Bool { + if case .unchanged = self { + return true + } + return false + } + + var isUnknown: Bool { + if case .unknown = self { + return true + } + return false + } + + var label: String { + switch self { + case .unknown: + return "Unknown" + case .unchanged: + return "Unchanged" + case .remote: + return "Remote" + case .local: + return "Local" + case .new: + return "New" + } + } +} + +func ==(lhs: MergeState, rhs: MergeState) -> Bool { + switch (lhs, rhs) { + case (.unknown, .unknown): + return true + case (.unchanged, .unchanged): + return true + case (.remote, .remote): + return true + case (.local, .local): + return true + case let (.new(lh), .new(rh)): + return lh == rh + default: + return false + } +} + +/** + * Using this: + * + * You get one for the root. Then you give it children for the roots + * from the mirror. + * + * Then you walk those, populating the remote and local nodes by looking + * at the left/right trees. + * + * By comparing left and right, and doing value-based comparisons if necessary, + * a merge state is decided and assigned for both value and structure. + * + * One then walks both left and right child structures (both to ensure that + * all nodes on both left and right will be visited!) recursively. + */ +class MergedTreeNode { + let guid: GUID + let mirror: BookmarkTreeNode? + var remote: BookmarkTreeNode? + var local: BookmarkTreeNode? + + var hasLocal: Bool { return self.local != nil } + var hasMirror: Bool { return self.mirror != nil } + var hasRemote: Bool { return self.remote != nil } + + var valueState: MergeState = MergeState.unknown + var structureState: MergeState = MergeState.unknown + + var hasDecidedChildren: Bool { + return !self.structureState.isUnknown + } + + var mergedChildren: [MergedTreeNode]? + + // One-sided constructors. + static func forRemote(_ remote: BookmarkTreeNode, mirror: BookmarkTreeNode?=nil) -> MergedTreeNode { + let n = MergedTreeNode(guid: remote.recordGUID, mirror: mirror, structureState: MergeState.remote) + n.remote = remote + n.valueState = MergeState.remote + return n + } + + static func forLocal(_ local: BookmarkTreeNode, mirror: BookmarkTreeNode?=nil) -> MergedTreeNode { + let n = MergedTreeNode(guid: local.recordGUID, mirror: mirror, structureState: MergeState.local) + n.local = local + n.valueState = MergeState.local + return n + } + + static func forUnchanged(_ mirror: BookmarkTreeNode) -> MergedTreeNode { + let n = MergedTreeNode(guid: mirror.recordGUID, mirror: mirror, structureState: MergeState.unchanged) + n.valueState = MergeState.unchanged + return n + } + + init(guid: GUID, mirror: BookmarkTreeNode?, structureState: MergeState) { + self.guid = guid + self.mirror = mirror + self.structureState = structureState + } + + init(guid: GUID, mirror: BookmarkTreeNode?) { + self.guid = guid + self.mirror = mirror + } + + // N.B., you cannot recurse down `decidedStructure`: you'll depart from the + // merged tree. You need to use `mergedChildren` instead. + fileprivate var decidedStructure: BookmarkTreeNode? { + switch self.structureState { + case .unknown: + return nil + case .unchanged: + return self.mirror + case .remote: + return self.remote + case .local: + return self.local + case let .new(node): + return node + } + } + + func asUnmergedTreeNode() -> BookmarkTreeNode { + return self.decidedStructure ?? BookmarkTreeNode.unknown(guid: self.guid) + } + + // Recursive. Starts returning Unknown when nodes haven't been processed. + func asMergedTreeNode() -> BookmarkTreeNode { + guard let decided = self.decidedStructure, + let merged = self.mergedChildren else { + return BookmarkTreeNode.unknown(guid: self.guid) + } + + if case .folder = decided { + let children = merged.map { $0.asMergedTreeNode() } + return BookmarkTreeNode.folder(guid: self.guid, children: children) + } + + return decided + } + + var isFolder: Bool { + return self.mergedChildren != nil + } + + func dump(_ indent: Int) { + precondition(indent < 200) + let r: Character = "R" + let l: Character = "L" + let m: Character = "M" + let ind = indenting(indent) + print(ind, "[V: ", box(self.remote, r), box(self.mirror, m), box(self.local, l), self.guid, self.valueState.label, "]") + guard self.isFolder else { + return + } + + print(ind, "[S: ", self.structureState.label, "]") + if let children = self.mergedChildren { + print(ind, " ..") + for child in children { + child.dump(indent + 2) + } + } + } +} + +private func box(_ x: T?, _ c: Character) -> Character { + if x == nil { + return "□" + } + return c +} + +private func indenting(_ by: Int) -> String { + return String(repeating: " ", count: by) +} + +class MergedTree { + var root: MergedTreeNode + var deleteLocally: Set = Set() + var deleteRemotely: Set = Set() + var deleteFromMirror: Set = Set() + var acceptLocalDeletion: Set = Set() + var acceptRemoteDeletion: Set = Set() + + var allGUIDs: Set { + var out = Set([self.root.guid]) + func acc(_ node: MergedTreeNode) { + guard let children = node.mergedChildren else { + return + } + out.formUnion(Set(children.map { $0.guid })) + children.forEach(acc) + } + acc(self.root) + return out + } + + init(mirrorRoot: BookmarkTreeNode) { + self.root = MergedTreeNode(guid: mirrorRoot.recordGUID, mirror: mirrorRoot, structureState: MergeState.unchanged) + self.root.valueState = MergeState.unchanged + } + + func dump() { + print("Deleted locally: \(self.deleteLocally.joined(separator: ", "))") + print("Deleted remotely: \(self.deleteRemotely.joined(separator: ", "))") + print("Deleted from mirror: \(self.deleteFromMirror.joined(separator: ", "))") + print("Accepted local deletions: \(self.acceptLocalDeletion.joined(separator: ", "))") + print("Accepted remote deletions: \(self.acceptRemoteDeletion.joined(separator: ", "))") + print("Root: ") + self.root.dump(0) + } +} diff --git a/mobile/ios/Sync/Synchronizers/Bookmarks/ThreeWayTreeMerger.swift b/mobile/ios/Sync/Synchronizers/Bookmarks/ThreeWayTreeMerger.swift new file mode 100644 index 0000000000..00aabadf00 --- /dev/null +++ b/mobile/ios/Sync/Synchronizers/Bookmarks/ThreeWayTreeMerger.swift @@ -0,0 +1,1423 @@ +/* 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 Deferred +import Foundation +import Shared +import Storage +import XCGLogger + +private let log = Logger.syncLogger + +private func negate(_ f: @escaping (T) throws -> Bool) -> (T) throws -> Bool { + return { try !f($0) } +} + +extension Collection { + func exclude(_ predicate: @escaping (Self.Iterator.Element) throws -> Bool) throws -> [Self.Iterator.Element] { + return try self.filter(negate(predicate)) + } +} + +/** + * This class takes as input three 'trees'. + * + * The mirror is always complete, never contains deletions, never has + * orphans, and has a single root. + * + * Each of local and remote can contain a number of subtrees (each of which must + * be a folder or root), a number of deleted GUIDs, and a number of orphans (records + * with no known parent). + * + * As output it produces a merged tree. The tree contains the new structure, + * including every record that we're keeping, and also makes note of any deletions. + * + * The merged tree can be walked to yield a set of operations against the original + * three trees. Those operations will make the upstream source and local storage + * match the merge output. + * + * It's very likely that there's almost no overlap between local and remote, and + * thus no real conflicts to resolve -- a three-way merge isn't always a bad thing + * -- but we won't know until we compare records. + * + * + * Even though this is called 'three way merge', it also handles the case + * of a two-way merge (one without a shared parent; for the roots, this will only + * be on a first sync): content-based and structural merging is needed at all + * layers of the hierarchy, so we simply generalize that to also apply to roots. + * + * In a sense, a two-way merge is solved by constructing a shared parent consisting of + * roots, which are implicitly shared. + * + * (Special care must be taken to not deduce that one side has deleted a root, of course, + * as would be the case of a Sync server that doesn't contain + * a Mobile Bookmarks folder -- the set of roots can only grow, not shrink.) + * + * + * To begin with we structurally reconcile. If necessary we will lazily fetch the + * attributes of records in order to do a content-based reconciliation. Once we've + * matched up any records that match (including remapping local GUIDs), we're able to + * process _content_ changes, which is much simpler. + * + * We have to handle an arbitrary combination of the following structural operations: + * + * * Creating a folder. + * Created folders might now hold existing items, new items, or nothing at all. + * * Creating a bookmark. + * It might be in a new folder or an existing folder. + * * Moving one or more leaf records to an existing or new folder. + * * Reordering the children of a folder. + * * Deleting an entire subtree. + * * Deleting an entire subtree apart from some moved nodes. + * * Deleting a leaf node. + * * Transplanting a subtree: moving a folder but not changing its children. + * + * And, of course, the non-structural operations such as renaming or changing URLs. + * + * We ignore all changes to roots themselves; the only acceptable operation on a root + * is to change its children. The Places root is entirely immutable. + * + * Steps: + * * Construct a collection of subtrees for local and buffer, and a complete tree for the mirror. + * The more thorough this step, the more confidence we have in consistency. + * * Fetch all local and remote deletions. These won't be included in structure (for obvious + * reasons); we hold on to them explicitly so we can spot the difference between a move + * and a deletion. + * * Walk each subtree, top-down. At each point if there are two back-pointers to + * the mirror node for a GUID, we have a potential conflict, and we have all three + * parts that we need to resolve it via a content-based or structure-based 3WM. + * + * Observations: + * * If every GUID on each side is present in the mirror, we have no new records. + * * If a non-root GUID is present on both sides but not in the mirror, then either + * we're re-syncing from scratch, or (unlikely) we have a random collision. + * * Otherwise, we have a GUID that we don't recognize. We will structure+content reconcile + * this later -- we first make sure we have handled any tree moves, so that the addition + * of a bookmark to a moved folder on A, and the addition of the same bookmark to the non- + * moved version of the folder, will collide successfully. + * + * When we look at a child list: + * * It's the same. Great! Keep walking down. + * * There are added GUIDs. + * * An added GUID might be a move from elsewhere. Coordinate with the removal step. + * * An added GUID might be a brand new record. If there are local additions too, + * check to see if they value-reconcile, and keep the remote GUID. + * * There are removed GUIDs. + * * A removed GUID might have been deleted. Deletions win. + * * A missing GUID might be a move -- removed from here and added to another folder. + * Process this as a move. + * * The order has changed. + * + * When we get to a subtree that contains no changes, we can never hit conflicts, and + * application becomes easier. + * + * When we run out of subtrees on both sides, we're done. + * + * Note that these trees don't include values. This is because we usually don't need them: + * if there are no conflicts, or no shared parents, we can do everything we need to do + * at this stage simply with structure and GUIDs, and then flush rows straight from the + * buffer into the mirror with a single SQL INSERT. We do need to fetch values later + * in some cases: to amend child lists or otherwise construct outbound records. We do + * need to fetch values immediately in other cases: in order to reconcile conflicts. + * Those should ordinarily be uncommon, and because we know most of the conflicting + * GUIDs up-front, we can prime a cache of records. + */ +class ThreeWayTreeMerger { + let local: BookmarkTree + let mirror: BookmarkTree + let remote: BookmarkTree + var merged: MergedTree + + // Don't merge twice. + var mergeAttempted: Bool = false + + let itemSources: ItemSources + + // Sets computed by looking at the three trees. These are used for diagnostics, + // to simplify operations, to pre-fetch items for value comparison, and for testing. + + let mirrorAllGUIDs: Set // Excluding deletions. + let localAllGUIDs: Set // Excluding deletions. + let remoteAllGUIDs: Set // Excluding deletions. + let localAdditions: Set // New records added locally, not present in the mirror. + let remoteAdditions: Set // New records from the server, not present in the mirror. + let allDeletions: Set // Records deleted locally or remotely. + var allChangedGUIDs: Set // Everything added or changed locally or remotely. + let conflictingGUIDs: Set // Anything added or changed both locally and remotely. + + let nonRemoteKnownGUIDs: Set // Everything existing, added, or deleted locally or in the mirror. + + // For now, track just one list. We might need to split this later. + var done: Set = Set() + + // Local records that we identified as being the same as remote records. + var duped: Set = Set() + + init(local: BookmarkTree, mirror: BookmarkTree, remote: BookmarkTree, itemSources: ItemSources) { + precondition(mirror.root != nil) + assert((mirror.root!.children?.count ?? 0) == BookmarkRoots.RootChildren.count) + precondition(mirror.orphans.isEmpty) + precondition(mirror.deleted.isEmpty) + precondition(mirror.subtrees.count == 1) + + // These are runtime-tested in merge(). assert to make sure that tests + // don't do anything stupid, and we don't slip past those constraints. + //assert(local.isFullyRootedIn(mirror)) + //assert(remote.isFullyRootedIn(mirror)) + + self.local = local + self.mirror = mirror + self.remote = remote + self.itemSources = itemSources + self.merged = MergedTree(mirrorRoot: self.mirror.root!) + + // We won't get here unless every local and remote orphan is correctly rooted + // when overlaid on the mirror, so we don't need to exclude orphans here. + self.mirrorAllGUIDs = self.mirror.modified + self.localAllGUIDs = self.local.modified + self.remoteAllGUIDs = self.remote.modified + + self.localAdditions = localAllGUIDs.subtracting(mirrorAllGUIDs) + self.remoteAdditions = remoteAllGUIDs.subtracting(mirrorAllGUIDs) + self.allDeletions = self.local.deleted.union(self.remote.deleted) + self.allChangedGUIDs = localAllGUIDs.union(self.remoteAllGUIDs) + self.conflictingGUIDs = localAllGUIDs.intersection(remoteAllGUIDs) + self.nonRemoteKnownGUIDs = self.mirrorAllGUIDs.union(self.localAllGUIDs).union(self.local.deleted) + } + + fileprivate func nullOrMatch(_ a: String?, _ b: String?) -> Bool { + guard let a = a, let b = b else { + return true + } + return a == b + } + + /** + * When we have a folder match and new records on each side -- records + * not mentioned in the buffer -- it's possible that the new records + * are the same but have different GUIDs. + * This function will look at value matches to identify a local + * equivalent in this folder, returning nil if none are found. + * + * Note that we don't match records that have already been matched, and + * we don't match any for which a GUID is known in the mirror or remote. + */ + fileprivate func findNewLocalNodeMatchingContentOfRemoteNote(_ remote: BookmarkTreeNode, inFolder parent: GUID, withLocalChildren children: [BookmarkTreeNode], havingSeen seen: Set) -> BookmarkTreeNode? { + // TODO: don't compute this list once per incoming child! Profile me. + let candidates = children.filter { child in + let childGUID = child.recordGUID + return !seen.contains(childGUID) && // Not already used in this folder. + !self.remoteAdditions.contains(childGUID) && // Not locally and remotely added with same GUID. + !self.remote.deleted.contains(childGUID) && // Not remotely deleted. + !self.done.contains(childGUID) // Not already processed elsewhere in the tree. + } + + guard let remoteValue = self.itemSources.buffer.getBufferItemWithGUID(remote.recordGUID).value.successValue else { + log.error("Couldn't find remote value for \(remote.recordGUID).") + return nil + } + + let guids = candidates.map { $0.recordGUID } + guard let items = self.itemSources.local.getLocalItemsWithGUIDs(guids).value.successValue else { + log.error("Couldn't find local values for \(candidates.count) candidates.") + return nil + } + + // Return the first candidate that's a value match. + guard let localItem = (guids.flatMap { items[$0] }.find { $0.sameAs(remoteValue) }) else { + log.debug("Didn't find a local value match for new remote record \(remote.recordGUID).") + return nil + } + + log.debug("Found a local match \(localItem.guid) for new remote record \(remote.recordGUID) in parent \(parent).") + // Find the original contributing child node by GUID. + return children.find { $0.recordGUID == localItem.guid } + } + + fileprivate func takeMirrorChildrenInMergedNode(_ result: MergedTreeNode) throws { + guard let mirrorChildren = result.mirror?.children else { + preconditionFailure("Expected children.") + } + + let out: [MergedTreeNode] = try mirrorChildren.flatMap { child in + // TODO: handle deletions. That might change the below from 'Unchanged' + // to 'New'. + let childGUID = child.recordGUID + if self.done.contains(childGUID) { + log.warning("Not taking mirror child \(childGUID): already done. This is unexpected.") + return nil + } + let localCounterpart = self.local.find(childGUID) + let remoteCounterpart = self.remote.find(childGUID) + return try self.mergeNode(childGUID, localNode: localCounterpart, mirrorNode: child, remoteNode: remoteCounterpart) + } + + result.mergedChildren = out + result.structureState = MergeState.unchanged + } + + fileprivate func oneWayMergeChildListsIntoMergedNode(_ result: MergedTreeNode, fromRemote remote: BookmarkTreeNode) throws { + guard case .folder = remote else { + preconditionFailure("Expected folder from which to merge children.") + } + + result.structureState = MergeState.remote // If the list changes, this will switch to .new. + try self.mergeChildListsIntoMergedNode(result, fromLocal: nil, remote: remote, mirror: self.mirror.find(remote.recordGUID)) + } + + fileprivate func oneWayMergeChildListsIntoMergedNode(_ result: MergedTreeNode, fromLocal local: BookmarkTreeNode) throws { + guard case .folder = local else { + preconditionFailure("Expected folder from which to merge children.") + } + + result.structureState = MergeState.local // If the list changes, this will switch to .new. + try self.mergeChildListsIntoMergedNode(result, fromLocal: local, remote: nil, mirror: self.mirror.find(local.recordGUID)) + } + + fileprivate func mergeChildListsIntoMergedNode(_ result: MergedTreeNode, fromLocal local: BookmarkTreeNode?, remote: BookmarkTreeNode?, mirror: BookmarkTreeNode?) throws { + precondition(local != nil || remote != nil, "Expected either local or remote folder for merge.") + + // The most trivial implementation: take everything in the first list, then append + // everything new in the second list. + // Anything present in both is resolved. + // We can't get away from handling deletions and moves, etc. -- one might have + // created a folder on two devices and moved existing items on one, some of which + // might have been deleted on the other. + // This kind of shit is why bookmark sync is hard. + // See each of the test cases in TestBookmarkTreeMerging, which have helpful diagrams. + var out: [MergedTreeNode] = [] + var seen: Set = Set() + + var changed = false + + func processRemoteOrphansForNode(_ node: BookmarkTreeNode) throws -> [MergedTreeNode]? { + // Now we recursively merge down into our list of orphans. If those contain deleted + // subtrees, excess leaves will be flattened up; we'll get a single list of nodes + // here, and we'll take them as additional children. + + let guid = node.recordGUID + func isLocallyDeleted(_ child: BookmarkTreeNode) throws -> Bool { + return try checkForLocalDeletionOfRemoteNode(child, mirrorNode: self.mirror.find(child.recordGUID)) + } + + guard let orphans = try node.children?.exclude(isLocallyDeleted), !orphans.isEmpty else { + log.debug("No remote orphans from local deletion of \(guid).") + return nil + } + + let mergedOrphans = try orphans.map { (orphan: BookmarkTreeNode) throws -> MergedTreeNode in + let guidO = orphan.recordGUID + let locO = self.local.find(guidO) + let remO = orphan + let mirO = self.mirror.find(guidO) + log.debug("Merging up remote orphan \(guidO).") + return try self.mergeNode(guidO, localNode: locO, mirrorNode: mirO, remoteNode: remO) + } + + log.debug("Collected \(mergedOrphans.count) remote orphans for deleted folder \(guid).") + if mergedOrphans.isEmpty { + return nil + } + changed = true + return mergedOrphans + } + + func processLocalOrphansForNode(_ node: BookmarkTreeNode) throws -> [MergedTreeNode]? { + // Now we recursively merge down into our list of orphans. If those contain deleted + // subtrees, excess leaves will be flattened up; we'll get a single list of nodes + // here, and we'll take them as additional children. + + let guid = node.recordGUID + + if case .folder = node {} else { + log.debug("\(guid) isn't a folder, so it won't have orphans.") + return nil + } + + func isRemotelyDeleted(_ child: BookmarkTreeNode) throws -> Bool { + return try checkForRemoteDeletionOfLocalNode(child, mirrorNode: self.mirror.find(child.recordGUID)) + } + + guard let orphans = try node.children?.exclude(isRemotelyDeleted), !orphans.isEmpty else { + log.debug("No local orphans from remote deletion of folder \(guid).") + return nil + } + + let mergedOrphans = try orphans.map { (orphan: BookmarkTreeNode) throws -> MergedTreeNode in + let guidO = orphan.recordGUID + let locO = orphan + let remO = self.remote.find(guidO) + let mirO = self.mirror.find(guidO) + log.debug("Merging up local orphan \(guidO).") + return try self.mergeNode(guidO, localNode: locO, mirrorNode: mirO, remoteNode: remO) + } + + log.debug("Collected \(mergedOrphans.count) local orphans for deleted folder \(guid).") + if mergedOrphans.isEmpty { + return nil + } + changed = true + return mergedOrphans + } + + func checkForLocalDeletionOfRemoteNode(_ node: BookmarkTreeNode, mirrorNode: BookmarkTreeNode?) throws -> Bool { + let guid = node.recordGUID + + guard self.local.deleted.contains(guid) else { + return false + } + + // It was locally deleted. This would be good enough for us, + // but we need to ensure that any remote children are recursively + // deleted or handled as orphans. + log.warning("Quietly accepting local deletion of record \(guid).") + changed = true + + self.merged.deleteRemotely.insert(guid) + self.merged.acceptLocalDeletion.insert(guid) + if mirrorNode != nil { + self.merged.deleteFromMirror.insert(guid) + } + + if let orphans = try processRemoteOrphansForNode(node) { + out.append(contentsOf: try self.relocateOrphansTo(result, orphans: orphans)) + } + return true + } + + func checkForRemoteDeletionOfLocalNode(_ node: BookmarkTreeNode, mirrorNode: BookmarkTreeNode?) throws -> Bool { + let guid = node.recordGUID + + guard self.remote.deleted.contains(guid) else { + return false + } + + // It was remotely deleted. This would be good enough for us, + // but we need to ensure that any local children are recursively + // deleted or handled as orphans. + log.warning("Quietly accepting remote deletion of record \(guid).") + + self.merged.deleteLocally.insert(guid) + self.merged.acceptRemoteDeletion.insert(guid) + if mirrorNode != nil { + self.merged.deleteFromMirror.insert(guid) + } + + if let orphans = try processLocalOrphansForNode(node) { + out.append(contentsOf: try self.relocateOrphansTo(result, orphans: orphans)) + } + return true + } + + // Do a recursive merge of each child. + if let remote = remote, let children = remote.children { + try children.forEach { rem in + let guid = rem.recordGUID + seen.insert(guid) + + if self.done.contains(guid) { + log.debug("Processing children of \(result.guid). Child \(guid) already seen elsewhere!") + return + } + + if try checkForLocalDeletionOfRemoteNode(rem, mirrorNode: self.mirror.find(guid)) { + log.debug("Child \(guid) is locally deleted.") + return + } + + let mir = self.mirror.find(guid) + if let localByGUID = self.local.find(guid) { + // Let's check the parent of the local match. If it differs, then the matching + // record is elsewhere in the local tree, and we need to decide which place to + // keep it. + // We do so by finding the modification time of the parent on each side, + // unless one of the records is explicitly non-modified. + if let localParentGUID = self.local.parents[guid] { + + // Oh hey look! Ad hoc three-way merge! + let mirrorParentGUID = self.mirror.parents[guid] + + if localParentGUID != result.guid { + log.debug("Local child \(guid) is in folder \(localParentGUID), but remotely is in \(result.guid).") + if mirrorParentGUID != localParentGUID { + log.debug("… and locally it has changed since our last sync, moving from \(mirrorParentGUID ?? "nil") to \(localParentGUID).") + + // Find out which parent is most recent. + if let localRecords = self.itemSources.local.getLocalItemsWithGUIDs([localParentGUID, guid]).value.successValue, + let remoteRecords = self.itemSources.buffer.getBufferItemsWithGUIDs([result.guid, guid]).value.successValue { + + let latestLocalTimestamp = max(localRecords[guid]?.localModified ?? 0, localRecords[localParentGUID]?.localModified ?? 0) + let latestRemoteTimestamp = max(remoteRecords[guid]?.serverModified ?? 0, remoteRecords[result.guid]?.serverModified ?? 0) + log.debug("Latest remote timestamp: \(latestRemoteTimestamp). Latest local timestamp: \(latestLocalTimestamp).") + + if latestLocalTimestamp > latestRemoteTimestamp { + log.debug("Keeping record in its local position. We'll merge these later.") + return + } + + log.debug("Taking remote, because it's later. Merging now.") + } + } else { + log.debug("\(guid) didn't move from \(mirrorParentGUID ?? "nil") since our last sync. Taking remote parent.") + } + } else { + log.debug("\(guid) is locally in \(localParentGUID) and remotely in \(result.guid). Easy.") + } + } + + out.append(try self.mergeNode(guid, localNode: localByGUID, mirrorNode: mir, remoteNode: rem)) + return + } + + // We don't ever have to handle moves in this case: we only search this directory. + let localByContent: BookmarkTreeNode? + if let localChildren = local?.children { + localByContent = self.findNewLocalNodeMatchingContentOfRemoteNote(rem, inFolder: result.guid, withLocalChildren: localChildren, havingSeen: seen) + } else { + localByContent = nil + } + out.append(try self.mergeNode(guid, localNode: localByContent, mirrorNode: mir, remoteNode: rem)) + } + } + + if let local = local, let children = local.children { + try children.forEach { loc in + let guid = loc.recordGUID + if seen.contains(guid) { + log.debug("Already saw local child \(guid).") + return + } + + if self.done.contains(guid) { + log.debug("Already saw local child \(guid) elsewhere.") + return + } + + if try checkForRemoteDeletionOfLocalNode(loc, mirrorNode: self.mirror.find(guid)) { + return + } + + let mir = self.mirror.find(guid) + let rem = self.remote.find(guid) + changed = true + out.append(try self.mergeNode(guid, localNode: loc, mirrorNode: mir, remoteNode: rem)) + } + } + + // Walk the mirror node's children. Any that are deleted on only one side might contribute + // orphans, so descend into those nodes' children on the other side. + if let expectedParent = mirror?.recordGUID, let mirrorChildren = mirror?.children { + try mirrorChildren.forEach { child in + let potentiallyDeleted = child.recordGUID + if seen.contains(potentiallyDeleted) || self.done.contains(potentiallyDeleted) { + return + } + + let locallyDeleted = self.local.deleted.contains(potentiallyDeleted) + let remotelyDeleted = self.remote.deleted.contains(potentiallyDeleted) + if !locallyDeleted && !remotelyDeleted { + log.debug("Mirror child \(potentiallyDeleted) no longer here, but not deleted on either side: must be elsewhere.") + return + } + + if locallyDeleted && remotelyDeleted { + log.debug("Mirror child \(potentiallyDeleted) was deleted both locally and remoted. We cool.") + self.merged.deleteFromMirror.insert(potentiallyDeleted) + self.merged.acceptLocalDeletion.insert(potentiallyDeleted) + self.merged.acceptRemoteDeletion.insert(potentiallyDeleted) + return + } + + if locallyDeleted { + // See if the remote side still thinks this is the parent. + let parent = self.remote.parents[potentiallyDeleted] + if parent == nil || parent == expectedParent { + log.debug("Remote still thinks \(potentiallyDeleted) is here. Processing for orphans.") + if let parentOfOrphans = self.remote.find(potentiallyDeleted), + let orphans = try processRemoteOrphansForNode(parentOfOrphans) { + out.append(contentsOf: try self.relocateOrphansTo(result, orphans: orphans)) + } + } + + // Accept the local deletion, and make a note to apply it elsewhere. + self.merged.deleteFromMirror.insert(potentiallyDeleted) + self.merged.deleteRemotely.insert(potentiallyDeleted) + self.merged.acceptLocalDeletion.insert(potentiallyDeleted) + return + } + + // Remotely deleted. + + let parent = self.local.parents[potentiallyDeleted] + if parent == nil || parent == expectedParent { + log.debug("Local still thinks \(potentiallyDeleted) is here. Processing for orphans.") + if let parentOfOrphans = self.local.find(potentiallyDeleted), + let orphans = try processLocalOrphansForNode(parentOfOrphans) { + out.append(contentsOf: try self.relocateOrphansTo(result, orphans: orphans)) + } + } + + // Accept the remote deletion, and make a note to apply it elsewhere. + self.merged.deleteFromMirror.insert(potentiallyDeleted) + self.merged.deleteLocally.insert(potentiallyDeleted) + self.merged.acceptRemoteDeletion.insert(potentiallyDeleted) + } + } + + log.debug("Setting \(result.guid)'s children to \(out.map { $0.guid }).") + result.mergedChildren = out + + // If the child list didn't change, then we don't need .new. + if changed { + let newStructure = out.map { $0.asMergedTreeNode() } + result.structureState = MergeState.new(value: BookmarkTreeNode.folder(guid: result.guid, children: newStructure)) + return + } + + log.debug("Child list didn't change for \(result.guid). Keeping structure state \(result.structureState).") + } + + fileprivate func resolveThreeWayValueConflict(_ guid: GUID) throws -> MergeState { + // TODO + return try self.resolveTwoWayValueConflict(guid, localGUID: guid) + } + + fileprivate func resolveTwoWayValueConflict(_ guid: GUID, localGUID: GUID) throws -> MergeState { + // We don't check for all roots, because we might have to + // copy them to the mirror or buffer, so we need to pick + // a direction. The Places root is never uploaded. + if BookmarkRoots.RootGUID == guid { + log.debug("Two-way value merge on the root: always unaltered.") + return MergeState.unchanged + } + + let localRecord = self.itemSources.local.getLocalItemWithGUID(localGUID).value.successValue + let remoteRecord = self.itemSources.buffer.getBufferItemWithGUID(guid).value.successValue + + if let local = localRecord { + if let remote = remoteRecord { + // Two-way. + // If they're the same, take the remote record. It saves us having to rewrite + // local values to keep a remote GUID. + if local.sameAs(remote) { + log.debug("Local record \(local.guid) same as remote \(remote.guid). Taking remote.") + return MergeState.remote + } + + log.debug("Comparing local (\(local.localModified ??? "0")) to remote (\(remote.serverModified)) clock for two-way value merge of \(guid).") + if let localModified = local.localModified, localModified > remote.serverModified { + // If we're taking the local record because it was modified, check to see + // whether the remote record has a creation date that we want to keep. + let dateAddedRemote = remote.dateAdded ?? remote.serverModified + if (local.dateAdded ?? UInt64.max) > dateAddedRemote { + return MergeState.new(value: local.copyWithDateAdded(dateAddedRemote)) + } + return MergeState.local + } + return MergeState.remote + } + + // No remote! + log.debug("Expected two-way merge for \(guid), but no remote item found.") + return MergeState.local + } + + if let _ = remoteRecord { + // No local! + log.debug("Expected two-way merge for \(guid), but no local item found.") + return MergeState.remote + } + + // Can't two-way merge with nothing! + log.error("Expected two-way merge for \(guid), but no local or remote item found!") + throw BookmarksMergeError() + } + + // This will never be called with two primary .unknown values. + fileprivate func threeWayMerge(_ guid: GUID, localNode: BookmarkTreeNode, remoteNode: BookmarkTreeNode, mirrorNode: BookmarkTreeNode?) throws -> MergedTreeNode { + if mirrorNode == nil { + log.debug("Two-way merge for \(guid).") + } else { + log.debug("Three-way merge for \(guid).") + } + + if remoteNode.isUnknown { + if localNode.isUnknown { + preconditionFailure("Two unknown nodes!") + } + + log.debug("Taking local node in two/three-way merge: remote bafflingly unchanged.") + // TODO: value-unchanged + return MergedTreeNode.forLocal(localNode) + } + + if localNode.isUnknown { + log.debug("Taking remote node in two/three-way merge: local bafflingly unchanged.") + // TODO: value-unchanged + return MergedTreeNode.forRemote(remoteNode) + } + + let result = MergedTreeNode(guid: guid, mirror: mirrorNode) + result.local = localNode + result.remote = remoteNode + + // Value merge. This applies regardless. + if localNode.isUnknown { + result.valueState = MergeState.remote + } else if remoteNode.isUnknown { + result.valueState = MergeState.local + } else { + if mirrorNode == nil { + result.valueState = try self.resolveTwoWayValueConflict(guid, localGUID: localNode.recordGUID) + } else { + result.valueState = try self.resolveThreeWayValueConflict(guid) + } + } + + switch localNode { + case let .folder(_, localChildren): + if case let .folder(_, remoteChildren) = remoteNode { + // Structural merge. + if localChildren.sameElements(remoteChildren) { + // Great! + log.debug("Local and remote records have same children in two-way merge.") + result.structureState = MergeState.new(value: localNode) // TODO: what if it's the same as the mirror? + try self.mergeChildListsIntoMergedNode(result, fromLocal: localNode, remote: remoteNode, mirror: mirrorNode) + return result + } + + // Merge the two folder lists. + // We know that each side is internally consistent: that is, each + // node in this list is present in the tree once only. But when we + // combine the two lists, we might be inadvertently duplicating a + // record that has already been, or will soon be, found in the other + // tree. We need to be careful to make sure that we don't feature + // a node in the tree more than once. + // Remember to check deletions. + log.debug("Local and remote records have different children. Merging.") + + // Assume it'll be the same as the remote one; mergeChildListsIntoMergedNode + // sets this to New if the structure changes. + result.structureState = MergeState.remote + try self.mergeChildListsIntoMergedNode(result, fromLocal: localNode, remote: remoteNode, mirror: mirrorNode) + return result + } + + case .nonFolder: + if case .nonFolder = remoteNode { + log.debug("Two non-folders with GUID \(guid) collide. Taking remote.") + return result + } + + default: + break + } + + // Otherwise, this must be a GUID collision between different types. + // TODO: Assign a new GUID to the local record + // but do not upload a deletion; these shouldn't merge. + log.debug("Remote and local records with same GUID \(guid) but different types. Consistency error.") + throw BookmarksMergeConsistencyError() + } + + fileprivate func twoWayMerge(_ guid: GUID, localNode: BookmarkTreeNode, remoteNode: BookmarkTreeNode) throws -> MergedTreeNode { + return try self.threeWayMerge(guid, localNode: localNode, remoteNode: remoteNode, mirrorNode: nil) + } + + fileprivate func unchangedIf(_ out: MergedTreeNode, original: BookmarkMirrorItem?, new: BookmarkMirrorItem?) -> MergedTreeNode { + guard let original = original, let new = new else { + return out + } + + if new.sameAs(original) { + out.valueState = MergeState.unchanged + } + return out + } + + fileprivate func takeLocalIfChanged(_ local: BookmarkTreeNode, mirror: BookmarkTreeNode?=nil) -> MergedTreeNode { + let guid = local.recordGUID + let localValues = self.itemSources.local.getLocalItemWithGUID(guid).value.successValue + let mirrorValues = self.itemSources.mirror.getMirrorItemWithGUID(guid).value.successValue + + // We don't expect these to ever fail to exist. + assert(localValues != nil) + + let merged = MergedTreeNode.forLocal(local, mirror: mirror) + return unchangedIf(merged, original: mirrorValues, new: localValues) + } + + fileprivate func takeRemoteIfChanged(_ remote: BookmarkTreeNode, mirror: BookmarkTreeNode?=nil) -> MergedTreeNode { + let guid = remote.recordGUID + let remoteValues = self.itemSources.buffer.getBufferItemWithGUID(guid).value.successValue + let mirrorValues = self.itemSources.mirror.getMirrorItemWithGUID(guid).value.successValue + + assert(remoteValues != nil) + + let merged = MergedTreeNode.forRemote(remote, mirror: mirror) + return unchangedIf(merged, original: mirrorValues, new: remoteValues) + } + + fileprivate var folderNameCache: [GUID: String?] = [:] + func getNameForFolder(_ folder: MergedTreeNode) throws -> String? { + if let name = self.folderNameCache[folder.guid] { + return name + } + let name = try self.fetchNameForFolder(folder) + self.folderNameCache[folder.guid] = name + return name + } + + func fetchNameForFolder(_ folder: MergedTreeNode) throws -> String? { + switch folder.valueState { + case let .new(v): + return v.title + case .unchanged: + if let mirror = folder.mirror?.recordGUID, + let title = self.itemSources.mirror.getMirrorItemWithGUID(mirror).value.successValue?.title { + return title + } + case .remote: + if let remote = folder.remote?.recordGUID, + let title = self.itemSources.buffer.getBufferItemWithGUID(remote).value.successValue?.title { + return title + } + case .local: + if let local = folder.local?.recordGUID, + let title = self.itemSources.local.getLocalItemWithGUID(local).value.successValue?.title { + return title + } + case .unknown: + break + } + + throw BookmarksMergeConsistencyError() + } + + func relocateOrphansTo(_ mergedNode: MergedTreeNode, orphans: [MergedTreeNode]?) throws -> [MergedTreeNode] { + guard let orphans = orphans else { + return [] + } + + let parentName = try self.getNameForFolder(mergedNode) + return try orphans.map { + try self.relocateMergedTreeNode($0, parentID: mergedNode.guid, parentName: parentName) + } + } + + func relocateMergedTreeNode(_ node: MergedTreeNode, parentID: GUID, parentName: String?) throws -> MergedTreeNode { + func copyWithMirrorItem(_ item: BookmarkMirrorItem?) throws -> MergedTreeNode { + guard let item = item else { + throw BookmarksMergeConsistencyError() + } + + if item.parentID == parentID && item.parentName == parentName { + log.debug("Don't need to relocate \(node.guid)'s for value table.") + return node + } + + log.debug("Relocating \(node.guid) to parent \(parentID).") + let n = MergedTreeNode(guid: node.guid, mirror: node.mirror) + n.local = node.local + n.remote = node.remote + n.mergedChildren = node.mergedChildren + n.structureState = node.structureState + n.valueState = .new(value: item.copyWithParentID(parentID, parentName: parentName)) + + return n + } + + switch node.valueState { + case .unknown: + return node + case .unchanged: + return try copyWithMirrorItem(self.itemSources.mirror.getMirrorItemWithGUID(node.guid).value.successValue) + case .local: + return try copyWithMirrorItem(self.itemSources.local.getLocalItemWithGUID(node.guid).value.successValue) + case .remote: + return try copyWithMirrorItem(self.itemSources.buffer.getBufferItemWithGUID(node.guid).value.successValue) + case let .new(value): + return try copyWithMirrorItem(value) + } + } + + // A helper that'll rewrite the resulting node's value to have the right parent. + func mergeNode(_ guid: GUID, intoFolder parentID: GUID, withParentName parentName: String?, localNode: BookmarkTreeNode?, mirrorNode: BookmarkTreeNode?, remoteNode: BookmarkTreeNode?) throws -> MergedTreeNode { + let m = try self.mergeNode(guid, localNode: localNode, mirrorNode: mirrorNode, remoteNode: remoteNode) + + // We could check the parent pointers in the tree, but looking at the values themselves + // will catch any mismatches between the value and structure tables. + return try self.relocateMergedTreeNode(m, parentID: parentID, parentName: parentName) + } + + // We'll end up looking at deletions and such as we go. + // TODO: accumulate deletions into the three buckets as we go. + // + // TODO: if a local or remote node is kept but put in a different folder, we actually + // need to generate a .new node, so we can take the parentid and parentNode that we + // must preserve. + func mergeNode(_ guid: GUID, localNode: BookmarkTreeNode?, mirrorNode: BookmarkTreeNode?, remoteNode: BookmarkTreeNode?) throws -> MergedTreeNode { + if let localGUID = localNode?.recordGUID { + log.verbose("Merging nodes with GUID \(guid). Local match is \(localGUID).") + } else { + log.verbose("Merging nodes with GUID \(guid). No local match.") + } + + // TODO: if the local node has a different GUID, it's because we did a value-based + // merge. Make sure the local row with the differing local GUID doesn't + // stick around. + + // We'll never get here with no nodes at all… right? + precondition((localNode != nil) || (mirrorNode != nil) || (remoteNode != nil)) + + // Note that not all of the input nodes must share a GUID: we might have decided, based on + // value comparison in an earlier recursive call, that a local node will be replaced by a + // remote node, and we'll need to mark the local GUID as a deletion, using its values and + // structure during our reconciling. + // But we will never have the mirror and remote differ. + precondition(nullOrMatch(remoteNode?.recordGUID, mirrorNode?.recordGUID)) + precondition(nullOrMatch(remoteNode?.recordGUID, guid)) + precondition(nullOrMatch(mirrorNode?.recordGUID, guid)) + + // Immediately mark this GUID -- and the local GUID, if it differs -- as done. + // This avoids repeated code in each conditional branch, and avoids the possibility of + // certain moves causing us to hit the same node again. + self.done.insert(guid) + if let otherGUID = localNode?.recordGUID, otherGUID != guid { + log.debug("Marking superseded local record \(otherGUID) as merged.") + self.done.insert(otherGUID) + self.duped.insert(otherGUID) + } + + func takeRemoteAndMergeChildren(_ remote: BookmarkTreeNode, mirror: BookmarkTreeNode?=nil) throws -> MergedTreeNode { + let merged = self.takeRemoteIfChanged(remote, mirror: mirror) + if case .folder = remote { + log.debug("… and it's a folder. Taking remote children.") + try self.oneWayMergeChildListsIntoMergedNode(merged, fromRemote: remote) + } + return merged + } + + func takeLocalAndMergeChildren(_ local: BookmarkTreeNode, mirror: BookmarkTreeNode?=nil) throws -> MergedTreeNode { + let merged = self.takeLocalIfChanged(local, mirror: mirror) + if case .folder = local { + log.debug("… and it's a folder. Taking local children.") + try self.oneWayMergeChildListsIntoMergedNode(merged, fromLocal: local) + } + return merged + } + + func takeMirrorNode(_ mirror: BookmarkTreeNode) throws -> MergedTreeNode { + let merged = MergedTreeNode.forUnchanged(mirror) + if case .folder = mirror { + try self.takeMirrorChildrenInMergedNode(merged) + } + return merged + } + + // If we ended up here with two unknowns, just proceed down the mirror. + // We know we have a mirror, else we'd have a non-unknown edge. + if (localNode?.isUnknown ?? true) && (remoteNode?.isUnknown ?? true) { + precondition(mirrorNode != nil) + log.verbose("Record \(guid) didn't change from mirror.") + + self.done.insert(guid) + return try takeMirrorNode(mirrorNode!) + } + + guard let mirrorNode = mirrorNode else { + // No mirror node: at most a two-way merge. + if let loc = localNode, !loc.isUnknown { + if let rem = remoteNode { + // Two-way merge; probably a disconnect-reconnect scenario. + return try self.twoWayMerge(guid, localNode: loc, remoteNode: rem) + } + + // No remote. Node only exists locally. + // However! The children might be mentioned in the mirror or + // remote tree. + log.verbose("Node \(guid) only exists locally.") + return try takeLocalAndMergeChildren(loc) + } + + // No local. + + guard let rem = remoteNode, !rem.isUnknown else { + // No remote! + // This should not occur: we have preconditions above. + preconditionFailure("Unexpectedly got past our preconditions!") + } + + // Node only exists remotely. Take it. + log.verbose("Node \(guid) only exists remotely.") + return try takeRemoteAndMergeChildren(rem) + } + + // We have a mirror node. + if let loc = localNode, !loc.isUnknown { + if let rem = remoteNode, !rem.isUnknown { + log.debug("Both local and remote changes to mirror item \(guid). Resolving conflict.") + return try self.threeWayMerge(guid, localNode: loc, remoteNode: rem, mirrorNode: mirrorNode) + } + log.verbose("Local-only change to mirror item \(guid).") + return try takeLocalAndMergeChildren(loc, mirror: mirrorNode) + } + + if let rem = remoteNode, !rem.isUnknown { + log.verbose("Remote-only change to mirror item \(guid).") + return try takeRemoteAndMergeChildren(rem, mirror: mirrorNode) + } + + log.verbose("Record \(guid) didn't change from mirror.") + return try takeMirrorNode(mirrorNode) + } + + func merge() -> Deferred> { + return self.produceMergedTree() + >>== self.produceMergeResultFromMergedTree + } + + func produceMergedTree() -> Deferred> { + // Don't ever do this work twice. + if self.mergeAttempted { + return deferMaybe(self.merged) + } + + // Both local and remote should reach a single root when overlayed. If not, it means that + // the tree is inconsistent -- there isn't a full tree present either on the server (or + // local) or in the mirror, or the changes aren't congruent in some way. If we reach this + // state, we cannot proceed. + // + // This is assert()ed in the initializer, too, so that we crash hard and early in developer + // builds and tests. + + if !self.local.isFullyRootedIn(self.mirror) { + log.warning("Local bookmarks not fully rooted when overlayed on mirror. This is most unusual.") + return deferMaybe(BookmarksMergeErrorTreeIsUnrooted(roots: self.local.subtreeGUIDs)) + } + + if !self.remote.isFullyRootedIn(mirror) { + log.warning("Remote bookmarks not fully rooted when overlayed on mirror. Partial read or write in buffer?") + + // This might be a temporary state: another client might not have uploaded all of its + // records yet. Another batch might arrive in a second, or it might arrive a month + // later if the user just closed the lid of their laptop and went on vacation! + // + // This can also be a semi-stable state; e.g., Bug 1235269. It's theoretically + // possible for us to try to recover by requesting reupload from other devices + // by changing syncID, or through some new command. + // + // Regardless, we can't proceed until this situation changes. + return deferMaybe(BookmarksMergeErrorTreeIsUnrooted(roots: self.remote.subtreeGUIDs)) + } + + log.debug("Processing \(self.localAllGUIDs.count) local changes and \(self.remoteAllGUIDs.count) remote changes.") + log.debug("\(self.local.subtrees.count) local subtrees and \(self.remote.subtrees.count) remote subtrees.") + log.debug("Local is adding \(self.localAdditions.count) records, and remote is adding \(self.remoteAdditions.count).") + log.debug("Local and remote have \(self.allDeletions.count) deletions.") + + if !conflictingGUIDs.isEmpty { + log.warning("Expecting conflicts between local and remote: \(self.conflictingGUIDs.joined(separator: ", ")).") + } + + // Pre-fetch items so we don't need to do async work later. + return self.prefetchItems() >>> self.walkProducingMergedTree + } + + fileprivate func prefetchItems() -> Success { + return self.itemSources.prefetchWithGUIDs(self.allChangedGUIDs) + } + + // This should only be called once. + // Callers should ensure validity of inputs. + fileprivate func walkProducingMergedTree() -> Deferred> { + let root = self.merged.root + assert((root.mirror?.children?.count ?? 0) == BookmarkRoots.RootChildren.count) + + // Get to walkin'. + root.structureState = MergeState.unchanged // We never change the root. + root.valueState = MergeState.unchanged + root.local = self.local.find(BookmarkRoots.RootGUID) + + do { + try root.mergedChildren = root.mirror!.children!.map { + let guid = $0.recordGUID + let loc = self.local.find(guid) + let mir = self.mirror.find(guid) + let rem = self.remote.find(guid) + return try self.mergeNode(guid, localNode: loc, mirrorNode: mir, remoteNode: rem) + } + } catch let error as MaybeErrorType { + return deferMaybe(error) + } catch let error { + return deferMaybe(BookmarksMergeError(error: error)) + } + + // If the buffer contains deletions for records that aren't in the mirror or in local, + // then we'll never normally encounter them, and thus we'll never accept the deletion. + // Check for that here. + let additionalRemoteDeletions = self.remote.deleted.subtracting(self.done) + log.debug("Additional remote deletions: \(additionalRemoteDeletions.count).") + self.merged.acceptRemoteDeletion.formUnion(additionalRemoteDeletions) + + self.mergeAttempted = true + + // Validate. Note that we might end up with *more* records than this -- records + // that didn't change naturally aren't present in the change list on either side. + let expected = self.allChangedGUIDs.subtracting(self.allDeletions).subtracting(self.duped) + assert(self.merged.allGUIDs.isSuperset(of: expected)) + assert(self.merged.allGUIDs.intersection(self.allDeletions).isEmpty) + + return deferMaybe(self.merged) + } + + /** + * Input to this function will be a merged tree: a collection of known deletions, + * and a tree of nodes that reflect an action and pointers to the edges and the + * mirror, something like this: + * + * ------------------------------------------------------------------------------- + * Deleted locally: folderBBBBBB + * Deleted remotely: folderDDDDDD + * Deleted from mirror: folderBBBBBB, folderDDDDDD + * Accepted local deletions: folderDDDDDD + * Accepted remote deletions: folderBBBBBB + * Root: + * [V: □ M □ root________ Unchanged ] + * [S: Unchanged ] + * .. + * [V: □ M □ menu________ Unchanged ] + * [S: Unchanged ] + * .. + * [V: □ M L folderCCCCCC Unchanged ] + * [S: New ] + * .. + * [V: R □ □ bookmarkFFFF Remote ] + * [V: □ M □ toolbar_____ Unchanged ] + * [S: Unchanged ] + * .. + * [V: R M □ folderAAAAAA Unchanged ] + * [S: New ] + * .. + * [V: □ □ L r_FpO9_RAXp3 Local ] + * [V: □ M □ unfiled_____ Unchanged ] + * [S: Unchanged ] + * .. + * [V: □ M □ mobile______ Unchanged ] + * [S: Unchanged ] + * .. + * ------------------------------------------------------------------------------- + * + * We walk this tree from the root, breadth-first in order to process folders before + * their children. We look at the valueState and structureState (for folders) to decide + * whether to spit out actions into the completion ops. + * + * Changes recorded in the completion ops are along the lines of "copy the record with + * this GUID from local into the mirror", "upload this record from local", "delete this + * record". Dependencies are encoded by the sequence of completion ops: for example, we + * don't want to drop local changes and update the mirror until the server reflects our + * local state, and we achieve that by only running the local completion op if the + * upload succeeds. + * + * There's a lot of boilerplate in this function. That's partly because the lines of + * switch statements are easier to read through and match up to expected behavior, but + * also because it's simpler than threading all of the edge cases around. + */ + func produceMergeResultFromMergedTree(_ mergedTree: MergedTree) -> Deferred> { + let upstreamOp = UpstreamCompletionOp() + let bufferOp = BufferCompletionOp() + let localOp = LocalOverrideCompletionOp() + + func accumulateNonRootFolder(_ node: MergedTreeNode) { + assert(node.isFolder) + + guard let children = node.mergedChildren else { + preconditionFailure("Shouldn't have a non-Unknown folder without known children.") + } + + let childGUIDs = children.map { $0.guid } + + // Recurse first — because why not? + // We don't expect deep enough bookmark trees that we'll overflow the stack, so + // we don't bother making a queue. + children.forEach(accumulateNode) + + // Verify that computed child lists match the right source node. + switch node.structureState { + case .remote: + assert(node.remote?.children?.map { $0.recordGUID } ?? [] == childGUIDs) + case .local: + assert(node.local?.children?.map { $0.recordGUID } ?? [] == childGUIDs) + case let .new(treeNode): + assert(treeNode.children?.map { $0.recordGUID } ?? [] == childGUIDs) + default: + break + } + + switch node.valueState { + case .unknown: + return // Never occurs: guarded by precondition. + case .unchanged: + // We can't have Unchanged value without a mirror node… + assert(node.hasMirror) + + switch node.structureState { + case .unknown: + return // Never occurs: guarded by precondition. + case .unchanged: + // Nothing changed! + return + case .remote: + // Nothing special to do: no need to amend server. + break + case .local: + upstreamOp.amendChildrenFromMirror[node.guid] = childGUIDs + case .new: + // No change in value, but a new structure. + // Construct a new upstream record from the old mirror value, + // and update the mirror structure. + upstreamOp.amendChildrenFromMirror[node.guid] = childGUIDs + } + + // We always need to do this for Remote, Local, New. + localOp.mirrorStructures[node.guid] = childGUIDs + + case .local: + localOp.mirrorValuesToCopyFromLocal.insert(node.guid) + + // Generate a new upstream record. + upstreamOp.amendChildrenFromLocal[node.guid] = childGUIDs + + // Update the structure in the mirror if necessary. + if case .unchanged = node.structureState { + return + } + localOp.mirrorStructures[node.guid] = childGUIDs + + case .remote: + localOp.mirrorValuesToCopyFromBuffer.insert(node.guid) + + // Update the structure in the mirror if necessary. + switch node.structureState { + case .unchanged: + return + case .remote: + localOp.mirrorStructures[node.guid] = childGUIDs + default: + // We need to upload a new record. + upstreamOp.amendChildrenFromBuffer[node.guid] = childGUIDs + localOp.mirrorStructures[node.guid] = childGUIDs + } + + case let .new(value): + // We can only do this if we stuffed the BookmarkMirrorItem with the right children. + // Verify that with a precondition. + precondition(value.children ?? [] == childGUIDs) + + localOp.mirrorStructures[node.guid] = childGUIDs + if node.hasMirror { + localOp.mirrorItemsToUpdate[node.guid] = value + } else { + localOp.mirrorItemsToInsert[node.guid] = value + } + let record = Record(id: node.guid, payload: value.asPayload()) + upstreamOp.records.append(record) + } + } + + func accumulateRoot(_ node: MergedTreeNode) { + log.debug("Accumulating \(node.guid).") + assert(node.isFolder) + assert(BookmarkRoots.Real.contains(node.guid)) + + guard let children = node.mergedChildren else { + preconditionFailure("Shouldn't have a non-Unknown folder without known children.") + } + + // Recurse first — because why not? + children.forEach(accumulateNode) + + let mirrorVersionIsVirtual = self.mirror.virtual.contains(node.guid) + + // Note that a root can be Unchanged, but be missing from the mirror. That's OK: roots + // don't really have values. Take whichever we find. + if node.mirror == nil || mirrorVersionIsVirtual { + if node.hasLocal { + localOp.mirrorValuesToCopyFromLocal.insert(node.guid) + } else if node.hasRemote { + localOp.mirrorValuesToCopyFromBuffer.insert(node.guid) + } else { + log.warning("No values to copy into mirror for \(node.guid). Need to synthesize root. Empty merge?") + } + } + + log.debug("Need to accumulate a root.") + if case .unchanged = node.structureState { + if !mirrorVersionIsVirtual { + log.debug("Root \(node.guid) is unchanged and already in the mirror.") + return + } + } + + let childGUIDs = children.map { $0.guid } + localOp.mirrorStructures[node.guid] = childGUIDs + + switch node.structureState { + case .remote: + log.debug("Root \(node.guid) taking remote structure.") + return + case .local: + log.debug("Root \(node.guid) taking local structure.") + upstreamOp.amendChildrenFromLocal[node.guid] = childGUIDs + case .new: + log.debug("Root \(node.guid) taking new structure.") + if node.hasMirror && !mirrorVersionIsVirtual { + log.debug("… uploading with mirror value.") + upstreamOp.amendChildrenFromMirror[node.guid] = childGUIDs + } else if node.hasLocal { + log.debug("… uploading with local value.") + upstreamOp.amendChildrenFromLocal[node.guid] = childGUIDs + } else if node.hasRemote { + log.debug("… uploading with remote value.") + upstreamOp.amendChildrenFromBuffer[node.guid] = childGUIDs + } else { + log.warning("No values to copy to remote for \(node.guid). Need to synthesize root. Empty merge?") + } + default: + // Filler. + return + } + } + + func accumulateNode(_ node: MergedTreeNode) { + precondition(!node.valueState.isUnknown) + precondition(!node.isFolder || !node.structureState.isUnknown) + + // These two clauses are common to all: if we walk through a node, + // it means it's been processed, and no longer needs to be kept + // on the edges. + if node.hasLocal { + let localGUID = node.local!.recordGUID + log.debug("Marking \(localGUID) to drop from local.") + localOp.processedLocalChanges.insert(localGUID) + } + + if node.hasRemote { + log.debug("Marking \(node.guid) to drop from buffer.") + bufferOp.processedBufferChanges.insert(node.guid) + } + + if node.isFolder { + if BookmarkRoots.Real.contains(node.guid) { + accumulateRoot(node) + return + } + + // We have to consider structure, and we have to recurse. + accumulateNonRootFolder(node) + return + } + + // Value didn't change, and no structure to handle. Done. + if node.valueState.isUnchanged { + precondition(node.hasMirror, "Can't have an unchanged non-root without there being a mirror record.") + } + + // Not new. Emit copy directives. + + switch node.valueState { + case .remote: + localOp.mirrorValuesToCopyFromBuffer.insert(node.guid) + + case .local: + let localGUID = node.local!.recordGUID + + // If we're taking the local value, we expect to keep the local GUID. + // TODO: this restriction isn't strictly required, but we know that our + // content-based merges will only ever take the remote value. + precondition(localGUID == node.guid, "Can't take local value without keeping local GUID.") + + guard let value = self.itemSources.local.getLocalItemWithGUID(localGUID).value.successValue else { + assertionFailure("Couldn't fetch local value for new item \(localGUID). This should never happen.") + return + } + + let record = Record(id: localGUID, payload: value.asPayload()) + upstreamOp.records.append(record) + + localOp.mirrorValuesToCopyFromLocal.insert(localGUID) + + // New. Emit explicit insertions into all three places, + // and eliminate any existing records for this GUID. + // Note that we don't check structure: this isn't a folder. + case let .new(value): + // + // TODO: ensure that `value` has the right parent GUID!!! + // Reparenting means that the moved node has _new_ values + // pointing to the _new_ parent. + // + // It also must have the correct child list. That isn't a + // problem in this value-only branch. + // + + // Upstream. + let record = Record(id: node.guid, payload: value.asPayload()) + upstreamOp.records.append(record) + + // Mirror. No structure needed. + if node.hasMirror { + localOp.mirrorItemsToUpdate[node.guid] = value + } else { + localOp.mirrorItemsToInsert[node.guid] = value + } + + default: + return // Deliberately incomplete switch. + } + } + + // Upload deleted records for anything we need to delete. + // Each one also ends up being dropped from the buffer. + if !mergedTree.deleteRemotely.isEmpty { + upstreamOp.records.append(contentsOf: mergedTree.deleteRemotely.map { + Record(id: $0, payload: BookmarkBasePayload.deletedPayload($0)) + }) + bufferOp.processedBufferChanges.formUnion(mergedTree.deleteRemotely) + } + + // Drop deleted items from the mirror. + localOp.mirrorItemsToDelete.formUnion(mergedTree.deleteFromMirror) + + // Anything we deleted on either end and accepted, add to the processed lists to be + // automatically dropped. + localOp.processedLocalChanges.formUnion(mergedTree.acceptLocalDeletion) + bufferOp.processedBufferChanges.formUnion(mergedTree.acceptRemoteDeletion) + + // We draw a terminological distinction between accepting a local deletion (which + // drops it from the local table) and deleting an item that's locally modified + // (which drops it from the local table, and perhaps also from the mirror). + // Either way, we put it in the list to drop. + // The former is `localOp.processedLocalChanges`, accumulated as we walk local. + // The latter is `mergedTree.deleteLocally`, accumulated as we process incoming deletions. + localOp.processedLocalChanges.formUnion(mergedTree.deleteLocally) + + // Now walk the final tree to get the substantive changes. + accumulateNode(mergedTree.root) + + // Postconditions. + // None of the work items appear in more than one place. + assert(Set(upstreamOp.amendChildrenFromBuffer.keys).isDisjoint(with: Set(upstreamOp.amendChildrenFromLocal.keys))) + assert(Set(upstreamOp.amendChildrenFromBuffer.keys).isDisjoint(with: Set(upstreamOp.amendChildrenFromMirror.keys))) + assert(Set(upstreamOp.amendChildrenFromLocal.keys).isDisjoint(with: Set(upstreamOp.amendChildrenFromMirror.keys))) + assert(localOp.mirrorItemsToDelete.isDisjoint(with: Set(localOp.mirrorItemsToInsert.keys))) + assert(localOp.mirrorItemsToDelete.isDisjoint(with: Set(localOp.mirrorItemsToUpdate.keys))) + assert(Set(localOp.mirrorItemsToInsert.keys).isDisjoint(with: Set(localOp.mirrorItemsToUpdate.keys))) + assert(localOp.mirrorValuesToCopyFromBuffer.isDisjoint(with: Set(localOp.mirrorValuesToCopyFromLocal))) + + // Pass through the item sources so we're able to apply the parts of the result that + // are in reference to storage. + let result = BookmarksMergeResult(uploadCompletion: upstreamOp, overrideCompletion: localOp, bufferCompletion: bufferOp, itemSources: self.itemSources) + return deferMaybe(result) + } +} diff --git a/mobile/ios/Sync/Synchronizers/ClientsSynchronizer.swift b/mobile/ios/Sync/Synchronizers/ClientsSynchronizer.swift new file mode 100644 index 0000000000..9a3e4d823a --- /dev/null +++ b/mobile/ios/Sync/Synchronizers/ClientsSynchronizer.swift @@ -0,0 +1,432 @@ +/* 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 +import Deferred +import SwiftyJSON + +private let log = Logger.syncLogger +let ClientsStorageVersion = 1 + +// TODO +public protocol Command { + static func fromName(_ command: String, args: [JSON]) -> Command? + func run(_ synchronizer: ClientsSynchronizer) -> Success + static func commandFromSyncCommand(_ syncCommand: SyncCommand) -> Command? +} + +// Shit. +// We need a way to wipe or reset engines. +// We need a way to log out the account. +// So when we sync commands, we're gonna need a delegate of some kind. +open class WipeCommand: Command { + + public init?(command: String, args: [JSON]) { + return nil + } + + open class func fromName(_ command: String, args: [JSON]) -> Command? { + return WipeCommand(command: command, args: args) + } + + open func run(_ synchronizer: ClientsSynchronizer) -> Success { + return succeed() + } + + open static func commandFromSyncCommand(_ syncCommand: SyncCommand) -> Command? { + let json = JSON(parseJSON: syncCommand.value) + if let name = json["command"].string, + let args = json["args"].array { + return WipeCommand.fromName(name, args: args) + } + return nil + } +} + +open class DisplayURICommand: Command { + let uri: URL + let title: String + let sender: String + + public init?(command: String, args: [JSON]) { + if let uri = args[0].string?.asURL, + let sender = args[1].string, + let title = args[2].string { + self.uri = uri + self.sender = sender + self.title = title + } else { + // Oh, Swift. + self.uri = "http://localhost/".asURL! + self.title = "" + return nil + } + } + + open class func fromName(_ command: String, args: [JSON]) -> Command? { + return DisplayURICommand(command: command, args: args) + } + + open func run(_ synchronizer: ClientsSynchronizer) -> Success { + func display(_ deviceName: String? = nil) -> Success { + synchronizer.delegate.displaySentTab(for: uri, title: title, from: deviceName) + return succeed() + } + + guard let getClientWithId = synchronizer.localClients?.getClientWithId(sender) else { + return display() + } + + return getClientWithId >>== { client in + return display(client?.name) + } + } + + open static func commandFromSyncCommand(_ syncCommand: SyncCommand) -> Command? { + let json = JSON(parseJSON: syncCommand.value) + if let name = json["command"].string, + let args = json["args"].array { + return DisplayURICommand.fromName(name, args: args) + } + return nil + } +} + +open class RepairResponseCommand: Command { + let repairResponse: RepairResponse + + public init(command: String, args: [JSON]) { + self.repairResponse = RepairResponse.fromJSON(args: args[0]) + } + + open class func fromName(_ command: String, args: [JSON]) -> Command? { + return RepairResponseCommand(command: command, args: args) + } + + open func run(_ synchronizer: ClientsSynchronizer) -> Success { + let repairer = BookmarksRepairRequestor(scratchpad: synchronizer.scratchpad, basePrefs: synchronizer.basePrefs, remoteClients: synchronizer.localClients!) + return repairer.continueRepairs(response: self.repairResponse) >>> succeed + } + + open static func commandFromSyncCommand(_ syncCommand: SyncCommand) -> Command? { + let json = JSON(parseJSON: syncCommand.value) + if let name = json["command"].string, + let args = json["args"].array { + return RepairResponseCommand.fromName(name, args: args) + } + return nil + } +} + +let Commands: [String: (String, [JSON]) -> Command?] = [ + "wipeAll": WipeCommand.fromName, + "wipeEngine": WipeCommand.fromName, + // resetEngine + // resetAll + // logout + "displayURI": DisplayURICommand.fromName, + "repairResponse": RepairResponseCommand.fromName +] + +open class ClientsSynchronizer: TimestampedSingleCollectionSynchronizer, Synchronizer { + public required init(scratchpad: Scratchpad, delegate: SyncDelegate, basePrefs: Prefs, why: SyncReason) { + super.init(scratchpad: scratchpad, delegate: delegate, basePrefs: basePrefs, why: why, collection: "clients") + } + + var localClients: RemoteClientsAndTabs? + + override var storageVersion: Int { + return ClientsStorageVersion + } + + var clientRecordLastUpload: Timestamp { + set(value) { + self.prefs.setLong(value, forKey: "lastClientUpload") + } + + get { + return self.prefs.unsignedLongForKey("lastClientUpload") ?? 0 + } + } + + // Sync Object Format (Version 1) for Form Factors: http://docs.services.mozilla.com/sync/objectformats.html#id2 + fileprivate enum SyncFormFactorFormat: String { + case phone = "phone" + case tablet = "tablet" + } + + open func getOurClientRecord() -> Record { + let guid = self.scratchpad.clientGUID + let formfactor = formFactorString() + + let json = JSON(object: [ + "id": guid, + "fxaDeviceId": self.scratchpad.fxaDeviceId, + "version": AppInfo.appVersion, + "protocols": ["1.5"], + "name": self.scratchpad.clientName, + "os": "iOS", + "commands": [JSON](), + "type": "mobile", + "appPackage": AppInfo.baseBundleIdentifier, + "application": AppInfo.displayName, + "device": DeviceInfo.deviceModel(), + "formfactor": formfactor]) + + let payload = ClientPayload(json) + return Record(id: guid, payload: payload, ttl: ThreeWeeksInSeconds) + } + + fileprivate func formFactorString() -> String { + let userInterfaceIdiom = UIDevice.current.userInterfaceIdiom + var formfactor: String + + switch userInterfaceIdiom { + case .phone: + formfactor = SyncFormFactorFormat.phone.rawValue + case .pad: + formfactor = SyncFormFactorFormat.tablet.rawValue + default: + formfactor = SyncFormFactorFormat.phone.rawValue + } + + return formfactor + } + + fileprivate func clientRecordToLocalClientEntry(_ record: Record) -> RemoteClient { + let modified = record.modified + let payload = record.payload + return RemoteClient(json: payload.json, modified: modified) + } + + // If this is a fresh start, do a wipe. + // N.B., we don't wipe outgoing commands! (TODO: check this when we implement commands!) + // N.B., but perhaps we should discard outgoing wipe/reset commands! + fileprivate func wipeIfNecessary(_ localClients: RemoteClientsAndTabs) -> Success { + if self.lastFetched == 0 { + return localClients.wipeClients() + } + return succeed() + } + + /** + * Returns whether any commands were found (and thus a replacement record + * needs to be uploaded). Also returns the commands: we run them after we + * upload a replacement record. + */ + fileprivate func processCommandsFromRecord(_ record: Record?, withServer storageClient: Sync15CollectionClient) -> Deferred> { + log.debug("Processing commands from downloaded record.") + + // TODO: short-circuit based on the modified time of the record we uploaded, so we don't need to skip ahead. + if let record = record { + let commands = record.payload.commands + if !commands.isEmpty { + func parse(_ json: JSON) -> Command? { + if let name = json["command"].string, + let args = json["args"].array, + let constructor = Commands[name] { + return constructor(name, args) + } + return nil + } + + // TODO: can we do anything better if a command fails? + return deferMaybe((true, optFilter(commands.map(parse)))) + } + } + + return deferMaybe((false, [])) + } + + fileprivate func uploadClientCommands(toLocalClients localClients: RemoteClientsAndTabs, withServer storageClient: Sync15CollectionClient) -> Success { + return localClients.getCommands() >>== { clientCommands in + return clientCommands.map { (clientGUID, commands) -> Success in + self.syncClientCommands(clientGUID, commands: commands, clientsAndTabs: localClients, withServer: storageClient) + }.allSucceed() + } + } + + fileprivate func syncClientCommands(_ clientGUID: GUID, commands: [SyncCommand], clientsAndTabs: RemoteClientsAndTabs, withServer storageClient: Sync15CollectionClient) -> Success { + + let deleteCommands: () -> Success = { + return clientsAndTabs.deleteCommands(clientGUID).bind({ x in return succeed() }) + } + + log.debug("Fetching current client record for client \(clientGUID).") + let fetch = storageClient.get(clientGUID) + return fetch.bind() { result in + if let response = result.successValue, response.value.payload.isValid() { + let record = response.value + if var clientRecord = record.payload.json.dictionary { + clientRecord["commands"] = JSON(record.payload.commands + commands.map { JSON(parseJSON: $0.value) }) + let uploadRecord = Record(id: clientGUID, payload: ClientPayload(JSON(clientRecord)), ttl: ThreeWeeksInSeconds) + return storageClient.put(uploadRecord, ifUnmodifiedSince: record.modified) + >>== { resp in + log.debug("Client \(clientGUID) commands upload succeeded.") + + // Always succeed, even if we couldn't delete the commands. + return deleteCommands() + } + } + } else { + if let failure = result.failureValue { + log.warning("Failed to fetch record with GUID \(clientGUID).") + if failure is NotFound { + log.debug("Not waiting to see if the client comes back.") + + // TODO: keep these around and retry, expiring after a while. + // For now we just throw them away so we don't fail every time. + return deleteCommands() + } + + if failure is BadRequestError { + log.debug("We made a bad request. Throwing away queued commands.") + return deleteCommands() + } + } + } + + log.error("Client \(clientGUID) commands upload failed: No remote client for GUID") + return deferMaybe(UnknownError()) + } + } + + /** + * Upload our record if either (a) we know we should upload, or (b) + * our own notes tell us we're due to reupload. + */ + fileprivate func maybeUploadOurRecord(_ should: Bool, ifUnmodifiedSince: Timestamp?, toServer storageClient: Sync15CollectionClient) -> Success { + + let lastUpload = self.clientRecordLastUpload + let expired = lastUpload < (Date.now() - (2 * OneDayInMilliseconds)) + log.debug("Should we upload our client record? Caller = \(should), expired = \(expired).") + if !should && !expired { + return succeed() + } + + let iUS: Timestamp? = ifUnmodifiedSince ?? ((lastUpload == 0) ? nil : lastUpload) + + var uploadStats = SyncUploadStats() + return storageClient.put(getOurClientRecord(), ifUnmodifiedSince: iUS) + >>== { resp in + if let ts = resp.metadata.lastModifiedMilliseconds { + // Protocol says this should always be present for success responses. + log.debug("Client record upload succeeded. New timestamp: \(ts).") + self.clientRecordLastUpload = ts + uploadStats.sent += 1 + } else { + uploadStats.sentFailed += 1 + } + self.statsSession.recordUpload(stats: uploadStats) + return succeed() + } + } + + fileprivate func applyStorageResponse(_ response: StorageResponse<[Record]>, toLocalClients localClients: RemoteClientsAndTabs, withServer storageClient: Sync15CollectionClient, notifier: CollectionChangedNotifier?) -> Success { + log.debug("Applying clients response.") + + var downloadStats = SyncDownloadStats() + + let records = response.value + let responseTimestamp = response.metadata.lastModifiedMilliseconds + + log.debug("Got \(records.count) client records.") + + let ourGUID = self.scratchpad.clientGUID + var toInsert = [RemoteClient]() + var ours: Record? = nil + + for (rec) in records { + guard rec.payload.isValid() else { + log.warning("Client record \(rec.id) is invalid. Skipping.") + continue + } + + if rec.id == ourGUID { + if rec.modified == self.clientRecordLastUpload { + log.debug("Skipping our own unmodified record.") + } else { + log.debug("Saw our own record in response.") + ours = rec + } + } else { + toInsert.append(self.clientRecordToLocalClientEntry(rec)) + } + } + + downloadStats.applied += toInsert.count + + // Apply remote changes. + // Collect commands from our own record and reupload if necessary. + // Then run the commands and return. + return localClients.insertOrUpdateClients(toInsert) + >>== { succeeded in + downloadStats.succeeded += succeeded + downloadStats.failed += (toInsert.count - succeeded) + self.statsSession.recordDownload(stats: downloadStats) + return succeed() + } + >>== { self.processCommandsFromRecord(ours, withServer: storageClient) } + >>== { (shouldUpload, commands) in + let isFirstSync = self.lastFetched == 0 + return self.maybeUploadOurRecord(shouldUpload || self.why == .didLogin, ifUnmodifiedSince: ours?.modified, toServer: storageClient) + >>> { self.uploadClientCommands(toLocalClients: localClients, withServer: storageClient) } + >>> { + log.debug("Running \(commands.count) commands.") + for command in commands { + _ = command.run(self) + } + self.lastFetched = responseTimestamp! + if isFirstSync, + let notifier = notifier { + DispatchQueue.global(qos: DispatchQoS.background.qosClass).async { _ = notifier.notifyAll(collectionsChanged: ["clients"], reason: "firstsync") } + } + return succeed() + } + } + } + + open func synchronizeLocalClients(_ localClients: RemoteClientsAndTabs, withServer storageClient: Sync15StorageClient, info: InfoCollections, notifier: CollectionChangedNotifier?) -> SyncResult { + log.debug("Synchronizing clients.") + self.localClients = localClients // Store for later when we process a repairResponse command + + if let reason = self.reasonToNotSync(storageClient) { + switch reason { + case .engineRemotelyNotEnabled: + // This is a hard error for us. + return deferMaybe(FatalError(message: "clients not mentioned in meta/global. Server wiped?")) + default: + return deferMaybe(SyncStatus.notStarted(reason)) + } + } + + let keys = self.scratchpad.keys?.value + let encoder = RecordEncoder(decode: { ClientPayload($0) }, encode: { $0.json }) + let encrypter = keys?.encrypter(self.collection, encoder: encoder) + if encrypter == nil { + log.error("Couldn't make clients encrypter.") + return deferMaybe(FatalError(message: "Couldn't make clients encrypter.")) + } + + let clientsClient = storageClient.clientForCollection(self.collection, encrypter: encrypter!) + + // TODO: some of the commands we process might involve wiping collections or the + // entire profile. We should model this as an explicit status, and return it here + // instead of .completed. + statsSession.start() + // XXX: This is terrible. We always force a re-sync of the clients to work around + // the fact that `fxaDeviceId` may not have been populated if the list of clients + // hadn't changed since before the update to v8.0. To force a re-sync, we get all + // clients since the beginning of time instead of looking at `self.lastFetched`. + return clientsClient.getSince(0) + >>== { response in + return self.wipeIfNecessary(localClients) + >>> { self.applyStorageResponse(response, toLocalClients: localClients, withServer: clientsClient, notifier: notifier) } + } + >>> { deferMaybe(self.completedWithStats) } + } +} diff --git a/mobile/ios/Sync/Synchronizers/Downloader.swift b/mobile/ios/Sync/Synchronizers/Downloader.swift new file mode 100644 index 0000000000..7eff893124 --- /dev/null +++ b/mobile/ios/Sync/Synchronizers/Downloader.swift @@ -0,0 +1,231 @@ +/* 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 +import Deferred + +private let log = Logger.syncLogger + +class BatchingDownloader { + let client: Sync15CollectionClient + let collection: String + let prefs: Prefs + + var batch: [Record] = [] + + func store(_ records: [Record]) { + self.batch += records + } + + func retrieve() -> [Record] { + let ret = self.batch + self.batch = [] + return ret + } + + var _advance: (() -> Void)? + func advance() { + guard let f = self._advance else { + return + } + self._advance = nil + f() + } + + init(collectionClient: Sync15CollectionClient, basePrefs: Prefs, collection: String) { + self.client = collectionClient + self.collection = collection + let branchName = "downloader." + collection + "." + self.prefs = basePrefs.branch(branchName) + + log.info("Downloader configured with prefs '\(self.prefs.getBranchPrefix())'.") + } + + static func resetDownloaderWithPrefs(_ basePrefs: Prefs, collection: String) { + // This leads to stupid paths like 'profile.sync.synchronizer.history..downloader.history..'. + // Sorry, but it's out in the world now... + let branchName = "downloader." + collection + "." + let prefs = basePrefs.branch(branchName) + + let lm = prefs.timestampForKey("lastModified") + let bt = prefs.timestampForKey("baseTimestamp") + log.debug("Resetting downloader prefs \(prefs.getBranchPrefix()). Previous values: \(lm ??? "nil"), \(bt ??? "nil").") + + prefs.removeObjectForKey("nextOffset") + prefs.removeObjectForKey("offsetNewer") + prefs.removeObjectForKey("baseTimestamp") + prefs.removeObjectForKey("lastModified") + } + + /** + * Clients should provide the same set of parameters alongside an `offset` as was + * provided with the initial request. The only thing that varies in our batch fetches + * is `newer`, so we track the original value alongside. + */ + var nextFetchParameters: (String, Timestamp)? { + get { + let o = self.prefs.stringForKey("nextOffset") + let n = self.prefs.timestampForKey("offsetNewer") + guard let offset = o, let newer = n else { + return nil + } + return (offset, newer) + } + set (value) { + if let (offset, newer) = value { + self.prefs.setString(offset, forKey: "nextOffset") + self.prefs.setTimestamp(newer, forKey: "offsetNewer") + } else { + self.prefs.removeObjectForKey("nextOffset") + self.prefs.removeObjectForKey("offsetNewer") + } + } + } + + // Set after each batch, from record timestamps. + var baseTimestamp: Timestamp { + get { + return self.prefs.timestampForKey("baseTimestamp") ?? 0 + } + set (value) { + self.prefs.setTimestamp(value, forKey: "baseTimestamp") + } + } + + // Only set at the end of a batch, from headers. + var lastModified: Timestamp { + get { + return self.prefs.timestampForKey("lastModified") ?? 0 + } + set (value) { + self.prefs.setTimestamp(value, forKey: "lastModified") + } + } + + /** + * Call this when a significant structural server change has been detected. + */ + func reset() -> Success { + self.baseTimestamp = 0 + self.lastModified = 0 + self.nextFetchParameters = nil + self.batch = [] + self._advance = nil + return succeed() + } + + func go(_ info: InfoCollections, limit: Int) -> Deferred> { + guard let modified = info.modified(self.collection) else { + log.debug("No server modified time for collection \(self.collection).") + return deferMaybe(.noNewData) + } + + log.debug("Modified: \(modified); last \(self.lastModified).") + if modified == self.lastModified { + log.debug("No more data to batch-download.") + return deferMaybe(.noNewData) + } + + // If the caller hasn't advanced after the last batch, strange things will happen -- + // potentially looping indefinitely. Warn. + if self._advance != nil && !self.batch.isEmpty { + log.warning("Downloading another batch without having advanced. This might be a bug.") + } + return self.downloadNextBatchWithLimit(limit, infoModified: modified) + } + + func advanceTimestampTo(_ timestamp: Timestamp) { + log.debug("Advancing downloader lastModified from \(self.lastModified) to \(timestamp).") + self.lastModified = timestamp + } + + // We're either fetching from our current base timestamp with no offset, + // or the timestamp we were using when we last saved an offset. + func fetchParameters() -> (String?, Timestamp) { + if let (offset, since) = self.nextFetchParameters { + return (offset, since) + } + return (nil, max(self.lastModified, self.baseTimestamp)) + } + + func downloadNextBatchWithLimit(_ limit: Int, infoModified: Timestamp) -> Deferred> { + let (offset, since) = self.fetchParameters() + log.debug("Fetching newer=\(since), offset=\(offset ?? "nil").") + + let fetch = self.client.getSince(since, sort: SortOption.OldestFirst, limit: limit, offset: offset) + + func handleFailure(_ err: MaybeErrorType) -> Deferred> { + log.debug("Handling failure.") + guard let badRequest = err as? BadRequestError<[Record]>, badRequest.response.metadata.status == 412 else { + // Just pass through the failure. + return deferMaybe(err) + } + + // Conflict. Start again. + log.warning("Server contents changed during offset-based batching. Stepping back.") + self.nextFetchParameters = nil + return deferMaybe(.interrupted) + } + + func handleSuccess(_ response: StorageResponse<[Record]>) -> Deferred> { + log.debug("Handling success.") + let nextOffset = response.metadata.nextOffset + let responseModified = response.value.last?.modified + + // Queue up our metadata advance. We wait until the consumer has fetched + // and processed this batch; they'll call .advance() on success. + self._advance = { + // Shift to the next offset. This might be nil, in which case… fine! + // Note that we preserve the previous 'newer' value from the offset or the original fetch, + // even as we update baseTimestamp. + self.nextFetchParameters = nextOffset == nil ? nil : (nextOffset!, since) + + // If there are records, advance to just before the timestamp of the last. + // If our next fetch with X-Weave-Next-Offset fails, at least we'll start here. + // + // This approach is only valid if we're fetching oldest-first. + if let newBase = responseModified { + log.debug("Advancing baseTimestamp to \(newBase) - 1") + self.baseTimestamp = newBase - 1 + } + + if nextOffset == nil { + // If we can't get a timestamp from the header -- and we should always be able to -- + // we fall back on the collection modified time in i/c, as supplied by the caller. + // In any case where there is no racing writer these two values should be the same. + // If they differ, the header should be later. If it's missing, and we use the i/c + // value, we'll simply redownload some records. + // All bets are off if we hit this case and are filtering somehow… don't do that. + let lm = response.metadata.lastModifiedMilliseconds + log.debug("Advancing lastModified to \(String(describing: lm)) ?? \(infoModified).") + self.lastModified = lm ?? infoModified + } + } + + log.debug("Got success response with \(response.metadata.records ?? 0) records.") + + // Store the incoming records for collection. + self.store(response.value) + + return deferMaybe(nextOffset == nil ? .complete : .incomplete) + } + + return fetch.bind { result in + guard let response = result.successValue else { + return handleFailure(result.failureValue!) + } + return handleSuccess(response) + } + } +} + +public enum DownloadEndState: String { + case complete // We're done. Records are waiting for you. + case incomplete // applyBatch was called, and we think there are more records. + case noNewData // There were no records. + case interrupted // We got a 412 conflict when fetching the next batch. +} diff --git a/mobile/ios/Sync/Synchronizers/HistorySynchronizer.swift b/mobile/ios/Sync/Synchronizers/HistorySynchronizer.swift new file mode 100644 index 0000000000..ac37bd1c5f --- /dev/null +++ b/mobile/ios/Sync/Synchronizers/HistorySynchronizer.swift @@ -0,0 +1,287 @@ +/* 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 +import Deferred +import SwiftyJSON + +private let log = Logger.syncLogger +private let HistoryTTLInSeconds = 5184000 // 60 days. +let HistoryStorageVersion = 1 + +func makeDeletedHistoryRecord(_ guid: GUID) -> Record { + // Local modified time is ignored in upload serialization. + let modified: Timestamp = 0 + + // Sortindex for history is frecency. Make deleted items more frecent than almost + // anything. + let sortindex = 5_000_000 + + let ttl = HistoryTTLInSeconds + + let json: JSON = JSON([ + "id": guid, + "deleted": true, + ]) + let payload = HistoryPayload(json) + return Record(id: guid, payload: payload, modified: modified, sortindex: sortindex, ttl: ttl) +} + +func makeHistoryRecord(_ place: Place, visits: [Visit]) -> Record { + let id = place.guid + let modified: Timestamp = 0 // Ignored in upload serialization. + let sortindex = 1 // TODO: frecency! + let ttl = HistoryTTLInSeconds + let json: JSON = JSON([ + "id": id, + "visits": visits.map { $0.toJSON() }, + "histUri": place.url, + "title": place.title, + ]) + let payload = HistoryPayload(json) + return Record(id: id, payload: payload, modified: modified, sortindex: sortindex, ttl: ttl) +} + +open class HistorySynchronizer: IndependentRecordSynchronizer, Synchronizer { + public required init(scratchpad: Scratchpad, delegate: SyncDelegate, basePrefs: Prefs, why: SyncReason) { + super.init(scratchpad: scratchpad, delegate: delegate, basePrefs: basePrefs, why: why, collection: "history") + } + + override var storageVersion: Int { + return HistoryStorageVersion + } + + fileprivate let batchSize: Int = 1000 // A balance between number of requests and per-request size. + + fileprivate func mask(_ maxFailures: Int) -> (Maybe<()>) -> Success { + var failures = 0 + return { result in + if result.isSuccess { + return Deferred(value: result) + } + + failures += 1 + if failures > maxFailures { + return Deferred(value: result) + } + + log.debug("Masking failure \(failures).") + return succeed() + } + } + + // TODO: this function should establish a transaction at suitable points. + // TODO: a much more efficient way to do this is to: + // 1. Start a transaction. + // 2. Try to update each place. Note failures. + // 3. bulkInsert all failed updates in one go. + // 4. Store all remote visits for all places in one go, constructing a single sequence of visits. + func applyIncomingToStorage(_ storage: SyncableHistory, records: [Record]) -> Success { + // Skip over at most this many failing records before aborting the sync. + let maskSomeFailures = self.mask(3) + + // TODO: it'd be nice to put this in an extension on SyncableHistory. Waiting for Swift 2.0... + func applyRecord(_ rec: Record) -> Success { + let guid = rec.id + let payload = rec.payload + let modified = rec.modified + + // We apply deletions immediately. Yes, this will throw away local visits + // that haven't yet been synced. That's how Sync works, alas. + if payload.deleted { + return storage.deleteByGUID(guid, deletedAt: modified).bind(maskSomeFailures) + } + + // It's safe to apply other remote records, too -- even if we re-download, we know + // from our local cached server timestamp on each record that we've already seen it. + // We have to reconcile on-the-fly: we're about to overwrite the server record, which + // is our shared parent. + let place = rec.payload.asPlace() + + if isIgnoredURL(place.url) { + log.debug("Ignoring incoming record \(guid) because its URL is one we wish to ignore.") + return succeed() + } + + let placeThenVisits = storage.insertOrUpdatePlace(place, modified: modified) + >>> { storage.storeRemoteVisits(payload.visits, forGUID: guid) } + return placeThenVisits.map({ result in + if result.isFailure { + let reason = result.failureValue?.description ?? "unknown reason" + log.error("Record application failed: \(reason)") + } + return result + }).bind(maskSomeFailures) + } + + return self.applyIncomingRecords(records, apply: applyRecord) + } + + fileprivate func uploadModifiedPlaces(_ places: [(Place, [Visit])], lastTimestamp: Timestamp, fromStorage storage: SyncableHistory, withServer storageClient: Sync15CollectionClient) -> DeferredTimestamp { + log.info("Preparing upload…") + + // Build sequences of 1000 history items, sequence by sequence + // These will be uploaded in smaller batches by the upload batcher, but we chunk here + // in order to bound peak memory usage when we call makeHistoryRecord below. + let toUpload = chunk(places, by: 1000) + let perChunk: (ArraySlice<(Place, [Visit])>, Timestamp) -> DeferredTimestamp = { (records, timestamp) in + let recs = records.map(makeHistoryRecord) + log.info("Uploading \(recs.count) history items…") + return self.uploadRecords(recs, lastTimestamp: timestamp, storageClient: storageClient) { result, lastModified in + // We don't do anything with failed. + return storage.markAsSynchronized(result.success, modified: lastModified ?? timestamp) + } + } + + let start = deferMaybe(lastTimestamp) + return walk(toUpload, start: start, f: perChunk) + } + + fileprivate func uploadDeletedPlaces(_ guids: [GUID], lastTimestamp: Timestamp, fromStorage storage: SyncableHistory, withServer storageClient: Sync15CollectionClient) -> DeferredTimestamp { + + let records = guids.map(makeDeletedHistoryRecord) + + // Deletions are smaller, so upload 100 at a time. + return self.uploadRecords(records, lastTimestamp: lastTimestamp, storageClient: storageClient) { result, lastModified in + storage.markAsDeleted(result.success) >>> always(lastModified ?? lastTimestamp) + } + } + + fileprivate func uploadOutgoingFromStorage(_ storage: SyncableHistory, lastTimestamp: Timestamp, withServer storageClient: Sync15CollectionClient) -> Success { + var workWasDone = false + + let uploadDeleted: (Timestamp) -> DeferredTimestamp = { timestamp in + storage.getDeletedHistoryToUpload() + >>== { guids in + if !guids.isEmpty { + workWasDone = true + } + log.info("Uploading \(guids.count) deleted places.") + return self.uploadDeletedPlaces(guids, lastTimestamp: timestamp, fromStorage: storage, withServer: storageClient) + } + } + + let uploadModified: (Timestamp) -> DeferredTimestamp = { timestamp in + storage.getModifiedHistoryToUpload() + >>== { places in + if !places.isEmpty { + workWasDone = true + } + log.info("Uploading \(places.count) modified places.") + return self.uploadModifiedPlaces(places, lastTimestamp: timestamp, fromStorage: storage, withServer: storageClient) + } + } + + // The last clause will checkpoint the DB. But we just checkpointed the DB after downloading records! + // Yes, that's true. Either there will be lots of work to do (e.g., having just marked + // thousands of records as uploaded, or dropping lots of deleted rows), and so it's + // worthwhile… or there won't be much work to do, and the checkpoint will be cheap. + // If we did nothing -- uploaded no deletions, uploaded no modified records -- then we + // don't checkpoint at all. + return deferMaybe(lastTimestamp) + >>== uploadDeleted + >>== uploadModified + >>> effect({ log.debug("Done syncing. Work was done? \(workWasDone)") }) + >>> { workWasDone ? storage.doneUpdatingMetadataAfterUpload() : succeed() } // A closure so we eval workWasDone after it's set! + >>> effect({ log.debug("Done.") }) + } + + /** + * If the green light turns red, we don't want to continue to upload -- doing + * so would cause us to fast-forward our last sync timestamp and skip whatever + * we hadn't yet downloaded. + */ + fileprivate func go(_ info: InfoCollections, greenLight: @escaping () -> Bool, downloader: BatchingDownloader, history: SyncableHistory) -> SyncResult { + + if !greenLight() { + log.info("Green light turned red. Stopping history download.") + return deferMaybe(.partial(self.statsSession)) + } + + func applyBatched() -> Success { + return self.applyIncomingToStorage(history, records: downloader.retrieve()) + >>> effect(downloader.advance) + } + + func onBatchResult(_ result: Maybe) -> SyncResult { + guard let end = result.successValue else { + log.warning("Got failure: \(result.failureValue!)") + return deferMaybe(completedWithStats) + } + + switch end { + case .complete: + log.info("Done with batched mirroring.") + return applyBatched() + >>> history.doneApplyingRecordsAfterDownload + >>> { deferMaybe(self.completedWithStats) } + case .incomplete: + log.debug("Running another batch.") + // This recursion is fine because Deferred always pushes callbacks onto a queue. + return applyBatched() + >>> { self.go(info, greenLight: greenLight, downloader: downloader, history: history) } + case .interrupted: + log.info("Interrupted. Aborting batching this time.") + return deferMaybe(.partial(self.statsSession)) + case .noNewData: + log.info("No new data. No need to continue batching.") + downloader.advance() + return deferMaybe(completedWithStats) + } + } + + return downloader.go(info, limit: self.batchSize) + .bind(onBatchResult) + } + + open func synchronizeLocalHistory(_ history: SyncableHistory, withServer storageClient: Sync15StorageClient, info: InfoCollections, greenLight: @escaping () -> Bool) -> SyncResult { + if let reason = self.reasonToNotSync(storageClient) { + return deferMaybe(.notStarted(reason)) + } + + let encoder = RecordEncoder(decode: { HistoryPayload($0) }, encode: { $0.json }) + + guard let historyClient = self.collectionClient(encoder, storageClient: storageClient) else { + log.error("Couldn't make history factory.") + return deferMaybe(FatalError(message: "Couldn't make history factory.")) + } + + let downloader = BatchingDownloader(collectionClient: historyClient, basePrefs: self.prefs, collection: "history") + + // The original version of the history synchronizer tracked its + // own last fetched time. We need to migrate this into the + // batching downloader. + let since: Timestamp = self.lastFetched + if since > downloader.lastModified { + log.debug("Advancing downloader lastModified to synchronizer lastFetched \(since).") + downloader.lastModified = since + self.lastFetched = 0 + } + + statsSession.start() + return self.go(info, greenLight: greenLight, downloader: downloader, history: history) + >>== { syncResult in + switch syncResult { + case .completed: + // When we're done downloading, we can upload. + return self.uploadOutgoingFromStorage(history, + lastTimestamp: 0, + withServer: historyClient) + >>> { deferMaybe(self.completedWithStats) } + + // If we didn't finish downloading, do nothing further -- just pass + // through the download result. + case .notStarted(_): + return deferMaybe(syncResult) + + case .partial: + log.debug("Didn't finish downloading history; not uploading yet.") + return deferMaybe(syncResult) + } + } + } +} diff --git a/mobile/ios/Sync/Synchronizers/IndependentRecordSynchronizer.swift b/mobile/ios/Sync/Synchronizers/IndependentRecordSynchronizer.swift new file mode 100644 index 0000000000..508acdbac8 --- /dev/null +++ b/mobile/ios/Sync/Synchronizers/IndependentRecordSynchronizer.swift @@ -0,0 +1,147 @@ +/* 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 +import Deferred + +private let log = Logger.syncLogger + +class Uploader { + /** + * Upload just about anything that can be turned into something we can upload. + */ + func sequentialPosts(_ items: [T], by: Int, lastTimestamp: Timestamp, storageOp: @escaping ([T], Timestamp) -> DeferredTimestamp) -> DeferredTimestamp { + + // This needs to be a real Array, not an ArraySlice, + // for the types to line up. + let chunks = chunk(items, by: by).map { Array($0) } + + let start = deferMaybe(lastTimestamp) + + let perChunk: ([T], Timestamp) -> DeferredTimestamp = { (records, timestamp) in + // TODO: detect interruptions -- clients uploading records during our sync -- + // by using ifUnmodifiedSince. We can detect uploaded records since our download + // (chain the download timestamp into this function), and we can detect uploads + // that race with our own (chain download timestamps across 'walk' steps). + // If we do that, we can also advance our last fetch timestamp after each chunk. + log.debug("Uploading \(records.count) records.") + return storageOp(records, timestamp) + } + + return walk(chunks, start: start, f: perChunk) + } +} + +open class IndependentRecordSynchronizer: TimestampedSingleCollectionSynchronizer { + private func reportApplyStatsWrap(apply: @escaping (T) -> Success) -> (T) -> Success { + return { record in + return apply(record).bind({ result in + var stats = SyncDownloadStats() + stats.applied = 1 + if result.isSuccess { + stats.succeeded = 1 + } else { + stats.failed = 1 + } + self.statsSession.recordDownload(stats: stats) + return Deferred(value: result) + }) + } + } + + /** + * Just like the usual applyIncomingToStorage, but doesn't fast-forward the timestamp. + */ + func applyIncomingRecords(_ records: [T], apply: @escaping (T) -> Success) -> Success { + if records.isEmpty { + log.debug("No records; done applying.") + return succeed() + } + + return walk(records, f: reportApplyStatsWrap(apply: apply)) + } + + func applyIncomingToStorage(_ records: [T], fetched: Timestamp, apply: @escaping (T) -> Success) -> Success { + func done() -> Success { + log.debug("Bumping fetch timestamp to \(fetched).") + self.lastFetched = fetched + return succeed() + } + + if records.isEmpty { + log.debug("No records; done applying.") + return done() + } + + return walk(records, f: reportApplyStatsWrap(apply: apply)) >>> done + } +} + +extension TimestampedSingleCollectionSynchronizer { + /** + * On each chunk that we upload, we pass along the server modified timestamp to the next, + * chained through the provided `onUpload` function. + * + * The last chunk passes this modified timestamp out, and we assign it to lastFetched. + * + * The idea of this is twofold: + * + * 1. It does the fast-forwarding that every other Sync client does. + * + * 2. It allows us to (eventually) pass the last collection modified time as If-Unmodified-Since + * on each upload batch, as we do between the download and the upload phase. + * This alone allows us to detect conflicts from racing clients. + * + * In order to implement the latter, we'd need to chain the date from getSince in place of the + * 0 in the call to uploadOutgoingFromStorage in each synchronizer. + */ + func uploadRecords(_ records: [Record], lastTimestamp: Timestamp, storageClient: Sync15CollectionClient, onUpload: @escaping (POSTResult, Timestamp?) -> DeferredTimestamp) -> DeferredTimestamp { + if records.isEmpty { + log.debug("No modified records to upload.") + return deferMaybe(lastTimestamp) + } + + func reportUploadStatsWrap(result: POSTResult, timestamp: Timestamp?) -> DeferredTimestamp { + let stats = SyncUploadStats(sent: result.success.count, sentFailed: result.failed.count) + self.statsSession.recordUpload(stats: stats) + return onUpload(result, timestamp) + } + + let batch = storageClient.newBatch(ifUnmodifiedSince: (lastTimestamp == 0) ? nil : lastTimestamp, onCollectionUploaded: reportUploadStatsWrap) + return batch.addRecords(records) + >>> batch.endBatch + >>> { + let timestamp = batch.ifUnmodifiedSince ?? lastTimestamp + self.setTimestamp(timestamp) + return deferMaybe(timestamp) + } + } + + func uploadRecordsSingleBatch(_ records: [Record], lastTimestamp: Timestamp, storageClient: Sync15CollectionClient) -> Deferred> { + if records.isEmpty { + log.debug("No modified records to upload.") + return deferMaybe((timestamp: lastTimestamp, succeeded: [])) + } + + func reportUploadStatsWrap(result: POSTResult, timestamp: Timestamp?) -> DeferredTimestamp { + let stats = SyncUploadStats(sent: result.success.count, sentFailed: result.failed.count) + self.statsSession.recordUpload(stats: stats) + return deferMaybe(timestamp ?? lastTimestamp) + } + + let batch = storageClient.newBatch(ifUnmodifiedSince: (lastTimestamp == 0) ? nil : lastTimestamp, onCollectionUploaded: reportUploadStatsWrap) + return batch.addRecords(records, singleBatch: true) + >>== batch.endSingleBatch + >>== { (succeeded, lastModified) in + guard let timestamp = lastModified else { + return deferMaybe(FatalError(message: "Could not retrieve lastModified from the server response.")) + } + self.setTimestamp(timestamp) + return deferMaybe((timestamp: timestamp, succeeded: succeeded)) + } + } +} diff --git a/mobile/ios/Sync/Synchronizers/LoginsSynchronizer.swift b/mobile/ios/Sync/Synchronizers/LoginsSynchronizer.swift new file mode 100644 index 0000000000..cfdbba4e47 --- /dev/null +++ b/mobile/ios/Sync/Synchronizers/LoginsSynchronizer.swift @@ -0,0 +1,190 @@ +/* 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 +import Deferred +import SwiftyJSON + +private let log = Logger.syncLogger +let PasswordsStorageVersion = 1 + +private func makeDeletedLoginRecord(_ guid: GUID) -> Record { + // Local modified time is ignored in upload serialization. + let modified: Timestamp = 0 + + // Arbitrary large number: deletions sync down first. + let sortindex = 5_000_000 + + let json: JSON = JSON([ + "id": guid, + "deleted": true, + ]) + let payload = LoginPayload(json) + return Record(id: guid, payload: payload, modified: modified, sortindex: sortindex) +} + +func makeLoginRecord(_ login: Login) -> Record { + let id = login.guid + let modified: Timestamp = 0 // Ignored in upload serialization. + let sortindex = 1 + + let tLU = NSNumber(value: login.timeLastUsed / 1000) + let tPC = NSNumber(value: login.timePasswordChanged / 1000) + let tC = NSNumber(value: login.timeCreated / 1000) + + let dict: [String: Any] = [ + "id": id, + "hostname": login.hostname, + "httpRealm": login.httpRealm as Any, + "formSubmitURL": login.formSubmitURL as Any, + "username": login.username ?? "", + "password": login.password , + "usernameField": login.usernameField ?? "", + "passwordField": login.passwordField ?? "", + "timesUsed": login.timesUsed, + "timeLastUsed": tLU, + "timePasswordChanged": tPC, + "timeCreated": tC, + ] + + let payload = LoginPayload(JSON(dict)) + return Record(id: id, payload: payload, modified: modified, sortindex: sortindex) +} + +/** + * Our current local terminology ("logins") has diverged from the terminology in + * use when Sync was built ("passwords"). I've done my best to draw a reasonable line + * between the server collection/record format/etc. and local stuff: local storage + * works with logins, server records and collection are passwords. + */ +open class LoginsSynchronizer: IndependentRecordSynchronizer, Synchronizer { + public required init(scratchpad: Scratchpad, delegate: SyncDelegate, basePrefs: Prefs, why: SyncReason) { + super.init(scratchpad: scratchpad, delegate: delegate, basePrefs: basePrefs, why: why, collection: "passwords") + } + + override var storageVersion: Int { + return PasswordsStorageVersion + } + + func getLogin(_ record: Record) -> ServerLogin { + let guid = record.id + let payload = record.payload + let modified = record.modified + + let login = ServerLogin(guid: guid, hostname: payload.hostname, username: payload.username, password: payload.password, modified: modified) + login.formSubmitURL = payload.formSubmitURL + login.httpRealm = payload.httpRealm + login.usernameField = payload.usernameField + login.passwordField = payload.passwordField + + // Microseconds locally, milliseconds remotely. We should clean this up. + login.timeCreated = 1000 * (payload.timeCreated ?? 0) + login.timeLastUsed = 1000 * (payload.timeLastUsed ?? 0) + login.timePasswordChanged = 1000 * (payload.timePasswordChanged ?? 0) + login.timesUsed = payload.timesUsed ?? 0 + return login + } + + func applyIncomingToStorage(_ storage: SyncableLogins, records: [Record], fetched: Timestamp) -> Success { + return self.applyIncomingToStorage(records, fetched: fetched) { rec in + let guid = rec.id + let payload = rec.payload + + guard payload.isValid() else { + log.warning("Login record \(guid) is invalid. Skipping.") + return succeed() + } + + // We apply deletions immediately. That might not be exactly what we want -- perhaps you changed + // a password locally after deleting it remotely -- but it's expedient. + if payload.deleted { + return storage.deleteByGUID(guid, deletedAt: rec.modified) + } + + return storage.applyChangedLogin(self.getLogin(rec)) + } + } + + fileprivate func uploadChangedRecords(_ deleted: Set, modified: Set, records: [Record], lastTimestamp: Timestamp, storage: SyncableLogins, withServer storageClient: Sync15CollectionClient) -> Success { + + let onUpload: (POSTResult, Timestamp?) -> DeferredTimestamp = { result, lastModified in + let uploaded = Set(result.success) + return storage.markAsDeleted(uploaded.intersection(deleted)) >>> { storage.markAsSynchronized(uploaded.intersection(modified), modified: lastModified ?? lastTimestamp) } + } + + return uploadRecords(records, + lastTimestamp: lastTimestamp, + storageClient: storageClient, + onUpload: onUpload) >>> succeed + } + + // Find any records for which a local overlay exists. If we want to be really precise, + // we can find the original server modified time for each record and use it as + // If-Unmodified-Since on a PUT, or just use the last fetch timestamp, which should + // be equivalent. + // We will already have reconciled any conflicts on download, so this upload phase should + // be as simple as uploading any changed or deleted items. + fileprivate func uploadOutgoingFromStorage(_ storage: SyncableLogins, lastTimestamp: Timestamp, withServer storageClient: Sync15CollectionClient) -> Success { + let deleted: () -> Deferred, [Record])>> = { + return storage.getDeletedLoginsToUpload() >>== { guids in + let records = guids.map(makeDeletedLoginRecord) + return deferMaybe((Set(guids), records)) + } + } + + let modified: () -> Deferred, [Record])>> = { + return storage.getModifiedLoginsToUpload() >>== { logins in + let guids = Set(logins.map { $0.guid }) + let records = logins.map(makeLoginRecord) + return deferMaybe((guids, records)) + } + } + + return accumulate([deleted, modified]) >>== { result in + let (deletedGUIDs, deletedRecords) = result[0] + let (modifiedGUIDs, modifiedRecords) = result[1] + let allRecords = deletedRecords + modifiedRecords + + return self.uploadChangedRecords(deletedGUIDs, modified: modifiedGUIDs, records: allRecords, + lastTimestamp: lastTimestamp, storage: storage, withServer: storageClient) + } + } + + open func synchronizeLocalLogins(_ logins: SyncableLogins, withServer storageClient: Sync15StorageClient, info: InfoCollections) -> SyncResult { + if let reason = self.reasonToNotSync(storageClient) { + return deferMaybe(.notStarted(reason)) + } + + let encoder = RecordEncoder(decode: { LoginPayload($0) }, encode: { $0.json }) + guard let passwordsClient = self.collectionClient(encoder, storageClient: storageClient) else { + log.error("Couldn't make logins factory.") + return deferMaybe(FatalError(message: "Couldn't make logins factory.")) + } + + let since: Timestamp = self.lastFetched + log.debug("Synchronizing \(self.collection). Last fetched: \(since).") + + let applyIncomingToStorage: (StorageResponse<[Record]>) -> Success = { response in + let ts = response.metadata.timestampMilliseconds + let lm = response.metadata.lastModifiedMilliseconds! + log.debug("Applying incoming password records from response timestamped \(ts), last modified \(lm).") + log.debug("Records header hint: \(response.metadata.records ??? "nil")") + return self.applyIncomingToStorage(logins, records: response.value, fetched: lm) >>> effect { + NotificationCenter.default.post(name: NotificationDataRemoteLoginChangesWereApplied, object: nil) + } + } + + statsSession.start() + return passwordsClient.getSince(since) + >>== applyIncomingToStorage + // TODO: If we fetch sorted by date, we can bump the lastFetched timestamp + // to the last successfully applied record timestamp, no matter where we fail. + // There's no need to do the upload before bumping -- the storage of local changes is stable. + >>> { self.uploadOutgoingFromStorage(logins, lastTimestamp: 0, withServer: passwordsClient) } + >>> { return deferMaybe(self.completedWithStats) } + } +} diff --git a/mobile/ios/Sync/Synchronizers/Synchronizer.swift b/mobile/ios/Sync/Synchronizers/Synchronizer.swift new file mode 100644 index 0000000000..cf7e3a70aa --- /dev/null +++ b/mobile/ios/Sync/Synchronizers/Synchronizer.swift @@ -0,0 +1,293 @@ +/* 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 +import Deferred + +private let log = Logger.syncLogger + +/** + * This exists to pass in external context: e.g., the UIApplication can + * expose notification functionality in this way. + */ +public protocol SyncDelegate { + func displaySentTab(for url: URL, title: String, from deviceName: String?) + // TODO: storage. +} + +/** + * We sometimes want to make a synchronizer start from scratch: to throw away any + * metadata and reset storage to match, allowing us to respond to significant server + * changes. + * + * But instantiating a Synchronizer is a lot of work if we simply want to change some + * persistent state. This protocol describes a static func that fits most synchronizers. + * + * When the returned `Deferred` is filled with a success value, the supplied prefs and + * storage are ready to sync from scratch. + * + * Persisted long-term/local data is kept, and will later be reconciled as appropriate. + */ +public protocol ResettableSynchronizer { + static func resetSynchronizerWithStorage(_ storage: ResettableSyncStorage, basePrefs: Prefs, collection: String) -> Success +} + +/** + * This is a delegate that allows synchronizers to notify other devices in the Sync account + * that a collection changed. + */ +public protocol CollectionChangedNotifier { + func notify(deviceIDs: [GUID], collectionsChanged collections: [String], reason: String) -> Success + func notifyAll(collectionsChanged collections: [String], reason: String) -> Success +} + +// TODO: return values? +/** + * A Synchronizer is (unavoidably) entirely in charge of what it does within a sync. + * For example, it might make incremental progress in building a local cache of remote records, never actually performing an upload or modifying local storage. + * It might only upload data. Etc. + * + * Eventually I envision an intent-like approach, or additional methods, to specify preferences and constraints + * (e.g., "do what you can in a few seconds", or "do a full sync, no matter how long it takes"), but that'll come in time. + * + * A Synchronizer is a two-stage beast. It needs to support synchronization, of course; that + * needs a completely configured client, which can only be obtained from Ready. But it also + * needs to be able to do certain things beforehand: + * + * * Wipe its collections from the server (presumably via a delegate from the state machine). + * * Prepare to sync from scratch ("reset") in response to a changed set of keys, syncID, or node assignment. + * * Wipe local storage ("wipeClient"). + * + * Those imply that some kind of 'Synchronizer' exists throughout the state machine. We *could* + * pickle instructions for eventual delivery next time one is made and synchronized… + */ +public protocol Synchronizer { + init(scratchpad: Scratchpad, delegate: SyncDelegate, basePrefs: Prefs, why: SyncReason) + + /** + * Return a reason if the current state of this synchronizer -- particularly prefs and scratchpad -- + * prevent a routine sync from occurring. + */ + func reasonToNotSync(_: Sync15StorageClient) -> SyncNotStartedReason? +} + +/** + * We sometimes wish to return something more nuanced than simple success or failure. + * For example, refusing to sync because the engine was disabled isn't success (nothing was transferred!) + * but it also isn't an error. + * + * To do this we model real failures -- something went wrong -- as failures in the Result, and + * everything else in this status enum. This will grow as we return more details from a sync to allow + * for batch scheduling, success-case backoff and so on. + */ +public enum SyncStatus { + case completed(SyncEngineStatsSession) + case notStarted(SyncNotStartedReason) + case partial(SyncEngineStatsSession) + + public var description: String { + switch self { + case .completed: + return "Completed" + case let .notStarted(reason): + return "Not started: \(reason.description)" + case .partial: + return "Partial" + } + } +} + +public typealias DeferredTimestamp = Deferred> +public typealias SyncResult = Deferred> +public typealias EngineIdentifier = String +public typealias EngineStatus = (EngineIdentifier, SyncStatus) +public typealias EngineResults = [EngineStatus] +public typealias SyncOperationResult = (engineResults: Maybe, stats: SyncOperationStatsSession?) + +public enum SyncNotStartedReason { + case noAccount + case offline + case backoff(remainingSeconds: Int) + case engineRemotelyNotEnabled(collection: String) + case engineFormatOutdated(needs: Int) + case engineFormatTooNew(expected: Int) // This'll disappear eventually; we'll wipe the server and upload m/g. + case storageFormatOutdated(needs: Int) + case storageFormatTooNew(expected: Int) // This'll disappear eventually; we'll wipe the server and upload m/g. + case stateMachineNotReady // Because we're not done implementing. + case redLight + case unknown // Likely a programming error. + + var telemetryId: String { + switch self { + case .noAccount: + return "sync.not_started.reason.no_account" + case .offline: + return "sync.not_started.reason.offline" + case .backoff(_): + return "sync.not_started.reason.backoff" + case .engineRemotelyNotEnabled(_): + return "sync.not_started.reason.remotely_not_enabled" + case .engineFormatOutdated(_): + return "sync.not_started.reason.format_outdated" + case .engineFormatTooNew(_): // This'll disappear eventually; we'll wipe the server and upload m/g. + return "sync.not_started.reason.format_too_new" + case .storageFormatOutdated(_): + return "sync.not_started.reason.storage_format_outdated" + case .storageFormatTooNew(_): // This'll disappear eventually; we'll wipe the server and upload m/g. + return "sync.not_started.reason.storage_format_too_new" + case .stateMachineNotReady: // Because we're not done implementing. + return "sync.not_started.reason.state_machine_not_ready" + case .redLight: + return "sync.not_started.reason.red_light" + case .unknown: // Likely a programming error + return "sync.not_started.reason.unknown" + } + } + + var description: String { + switch self { + case .noAccount: + return "no account" + case let .backoff(remaining): + return "in backoff: \(remaining) seconds remaining" + default: + return "undescribed reason" + } + } +} + +open class FatalError: SyncError { + let message: String + init(message: String) { + self.message = message + } + + open var description: String { + return self.message + } +} + +public protocol SingleCollectionSynchronizer { + func remoteHasChanges(_ info: InfoCollections) -> Bool +} + +open class BaseCollectionSynchronizer { + let collection: String + + let scratchpad: Scratchpad + let delegate: SyncDelegate + let basePrefs: Prefs + let prefs: Prefs + let why: SyncReason + + var statsSession: SyncEngineStatsSession + + static func prefsForCollection(_ collection: String, withBasePrefs basePrefs: Prefs) -> Prefs { + let branchName = "synchronizer." + collection + "." + return basePrefs.branch(branchName) + } + + init(scratchpad: Scratchpad, delegate: SyncDelegate, basePrefs: Prefs, why: SyncReason, collection: String) { + self.scratchpad = scratchpad + self.delegate = delegate + self.collection = collection + self.basePrefs = basePrefs + self.prefs = BaseCollectionSynchronizer.prefsForCollection(collection, withBasePrefs: basePrefs) + self.statsSession = SyncEngineStatsSession(collection: collection) + self.why = why + + log.info("Synchronizer configured with prefs '\(self.prefs.getBranchPrefix()).'") + } + + var storageVersion: Int { + assert(false, "Override me!") + return 0 + } + + // Short-hand for returning .Complete status + recorded stats + var completedWithStats: SyncStatus { + return .completed(statsSession.end()) + } + + open func reasonToNotSync(_ client: Sync15StorageClient) -> SyncNotStartedReason? { + let now = Date.now() + if let until = client.backoff.isInBackoff(now) { + let remaining = (until - now) / 1000 + return .backoff(remainingSeconds: Int(remaining)) + } + + if let metaGlobal = self.scratchpad.global?.value { + // There's no need to check the global storage format here; the state machine will already have + // done so. + if let engineMeta = metaGlobal.engines[collection] { + if engineMeta.version > self.storageVersion { + return .engineFormatOutdated(needs: engineMeta.version) + } + if engineMeta.version < self.storageVersion { + return .engineFormatTooNew(expected: engineMeta.version) + } + } else { + return .engineRemotelyNotEnabled(collection: self.collection) + } + } else { + // But a missing meta/global is a real problem. + return .stateMachineNotReady + } + + // Success! + return nil + } + + func encrypter(_ encoder: RecordEncoder) -> RecordEncrypter? { + return self.scratchpad.keys?.value.encrypter(self.collection, encoder: encoder) + } + + func collectionClient(_ encoder: RecordEncoder, storageClient: Sync15StorageClient) -> Sync15CollectionClient? { + if let encrypter = self.encrypter(encoder) { + return storageClient.clientForCollection(self.collection, encrypter: encrypter) + } + return nil + } +} + +/** + * Tracks a lastFetched timestamp, uses it to decide if there are any + * remote changes, and exposes a method to fast-forward after upload. + */ +open class TimestampedSingleCollectionSynchronizer: BaseCollectionSynchronizer, SingleCollectionSynchronizer { + + var lastFetched: Timestamp { + set(value) { + self.prefs.setLong(value, forKey: "lastFetched") + } + + get { + return self.prefs.unsignedLongForKey("lastFetched") ?? 0 + } + } + + func setTimestamp(_ timestamp: Timestamp) { + log.debug("Setting post-upload lastFetched to \(timestamp).") + self.lastFetched = timestamp + } + + open func remoteHasChanges(_ info: InfoCollections) -> Bool { + return info.modified(self.collection) ?? 0 > self.lastFetched + } +} + +extension BaseCollectionSynchronizer: ResettableSynchronizer { + public static func resetSynchronizerWithStorage(_ storage: ResettableSyncStorage, basePrefs: Prefs, collection: String) -> Success { + let synchronizerPrefs = BaseCollectionSynchronizer.prefsForCollection(collection, withBasePrefs: basePrefs) + synchronizerPrefs.removeObjectForKey("lastFetched") + + // Not all synchronizers use a batching downloader, but it's + // convenient to just always reset it here. + return storage.resetClient() + >>> effect({ BatchingDownloader.resetDownloaderWithPrefs(synchronizerPrefs, collection: collection) }) + } +} diff --git a/mobile/ios/Sync/Synchronizers/TabsSynchronizer.swift b/mobile/ios/Sync/Synchronizers/TabsSynchronizer.swift new file mode 100644 index 0000000000..d2cd349daf --- /dev/null +++ b/mobile/ios/Sync/Synchronizers/TabsSynchronizer.swift @@ -0,0 +1,206 @@ +/* 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 +import Deferred +import SwiftyJSON + +private let log = Logger.syncLogger +let TabsStorageVersion = 1 + +open class TabsSynchronizer: TimestampedSingleCollectionSynchronizer, Synchronizer { + public required init(scratchpad: Scratchpad, delegate: SyncDelegate, basePrefs: Prefs, why: SyncReason) { + super.init(scratchpad: scratchpad, delegate: delegate, basePrefs: basePrefs, why: why, collection: "tabs") + } + + override var storageVersion: Int { + return TabsStorageVersion + } + + var tabsRecordLastUpload: Timestamp { + set(value) { + self.prefs.setLong(value, forKey: "lastTabsUpload") + } + + get { + return self.prefs.unsignedLongForKey("lastTabsUpload") ?? 0 + } + } + + fileprivate func createOwnTabsRecord(_ tabs: [RemoteTab]) -> Record { + let guid = self.scratchpad.clientGUID + + let tabsJSON = JSON([ + "id": guid, + "clientName": self.scratchpad.clientName, + "tabs": tabs.flatMap { $0.toDictionary() } + ]) + if Logger.logPII { + log.verbose("Sending tabs JSON \(tabsJSON.stringValue() ?? "nil")") + } + let payload = TabsPayload(tabsJSON) + return Record(id: guid, payload: payload, ttl: ThreeWeeksInSeconds) + } + + fileprivate func uploadOurTabs(_ localTabs: RemoteClientsAndTabs, toServer tabsClient: Sync15CollectionClient) -> Success { + // check to see if our tabs have changed or we're in a fresh start + let lastUploadTime: Timestamp? = (self.tabsRecordLastUpload == 0) ? nil : self.tabsRecordLastUpload + if let lastUploadTime = lastUploadTime, + lastUploadTime >= (Date.now() - (OneMinuteInMilliseconds)) { + log.debug("Not uploading tabs: already did so at \(lastUploadTime).") + return succeed() + } + + return localTabs.getTabsForClientWithGUID(nil) >>== { tabs in + if let lastUploadTime = lastUploadTime { + // TODO: track this in memory so we don't have to hit the disk to figure out when our tabs have + // changed and need to be uploaded. + if tabs.every({ $0.lastUsed < lastUploadTime }) { + return succeed() + } + } + + let tabsRecord = self.createOwnTabsRecord(tabs) + log.debug("Uploading our tabs: \(tabs.count).") + + var uploadStats = SyncUploadStats() + uploadStats.sent += 1 + + // We explicitly don't send If-Unmodified-Since, because we always + // want our upload to succeed -- we own the record. + return tabsClient.put(tabsRecord, ifUnmodifiedSince: nil) >>== { resp in + if let ts = resp.metadata.lastModifiedMilliseconds { + // Protocol says this should always be present for success responses. + log.debug("Tabs record upload succeeded. New timestamp: \(ts).") + self.tabsRecordLastUpload = ts + } else { + uploadStats.sentFailed += 1 + } + return succeed() + } >>== effect({ self.statsSession.recordUpload(stats: uploadStats) }) + } + } + + open func synchronizeLocalTabs(_ localTabs: RemoteClientsAndTabs, withServer storageClient: Sync15StorageClient, info: InfoCollections) -> SyncResult { + func onResponseReceived(_ response: StorageResponse<[Record]>) -> Success { + + func afterWipe() -> Success { + var downloadStats = SyncDownloadStats() + + let doInsert: (Record) -> Deferred> = { record in + let remotes = record.payload.isValid() ? record.payload.remoteTabs : [] + let ins = localTabs.insertOrUpdateTabsForClientGUID(record.id, tabs: remotes) + + // Since tabs are all sent within a single record, we don't count number of tabs applied + // but number of records. In this case it's just one. + downloadStats.applied += 1 + ins.upon() { res in + if let inserted = res.successValue { + if inserted != remotes.count { + log.warning("Only inserted \(inserted) tabs, not \(remotes.count). Malformed or missing client?") + } + downloadStats.applied += 1 + } else { + downloadStats.failed += 1 + } + } + return ins + } + + let ourGUID = self.scratchpad.clientGUID + + let records = response.value + let responseTimestamp = response.metadata.lastModifiedMilliseconds + + log.debug("Got \(records.count) tab records.") + + // We can only insert tabs for clients that we know locally, so + // first we fetch the list of IDs and intersect the two. + // TODO: there's a much more efficient way of doing this. + return localTabs.getClientGUIDs() + >>== { clientGUIDs in + let filtered = records.filter({ $0.id != ourGUID && clientGUIDs.contains($0.id) }) + if filtered.count != records.count { + log.debug("Filtered \(records.count) records down to \(filtered.count).") + } + + let allDone = all(filtered.map(doInsert)) + return allDone.bind { (results) -> Success in + if let failure = results.find({ $0.isFailure }) { + return deferMaybe(failure.failureValue!) + } + + self.lastFetched = responseTimestamp! + return succeed() + } + } >>== effect({ self.statsSession.downloadStats }) + } + + // If this is a fresh start, do a wipe. + if self.lastFetched == 0 { + log.info("Last fetch was 0. Wiping tabs.") + return localTabs.wipeRemoteTabs() + >>== afterWipe + } + + return afterWipe() + } + + if let reason = self.reasonToNotSync(storageClient) { + return deferMaybe(SyncStatus.notStarted(reason)) + } + + let keys = self.scratchpad.keys?.value + let encoder = RecordEncoder(decode: { TabsPayload($0) }, encode: { $0.json }) + if let encrypter = keys?.encrypter(self.collection, encoder: encoder) { + let tabsClient = storageClient.clientForCollection(self.collection, encrypter: encrypter) + + statsSession.start() + + if !self.remoteHasChanges(info) { + // upload local tabs if they've changed or we're in a fresh start. + return uploadOurTabs(localTabs, toServer: tabsClient) + >>> { deferMaybe(self.completedWithStats) } + } + + return tabsClient.getSince(self.lastFetched) + >>== onResponseReceived + >>> { self.uploadOurTabs(localTabs, toServer: tabsClient) } + >>> { deferMaybe(self.completedWithStats) } + } + + log.error("Couldn't make tabs factory.") + return deferMaybe(FatalError(message: "Couldn't make tabs factory.")) + } + + /** + * This is a dedicated resetting interface that does both tabs and clients at the + * same time. + */ + open static func resetClientsAndTabsWithStorage(_ storage: ResettableSyncStorage, basePrefs: Prefs) -> Success { + let clientPrefs = BaseCollectionSynchronizer.prefsForCollection("clients", withBasePrefs: basePrefs) + let tabsPrefs = BaseCollectionSynchronizer.prefsForCollection("tabs", withBasePrefs: basePrefs) + clientPrefs.removeObjectForKey("lastFetched") + tabsPrefs.removeObjectForKey("lastFetched") + return storage.resetClient() + } +} + +extension RemoteTab { + public func toDictionary() -> Dictionary? { + let tabHistory = history.flatMap { $0.absoluteString } + if tabHistory.isEmpty { + return nil + } + return [ + "title": title, + "icon": icon?.absoluteString as Any? ?? NSNull(), + "urlHistory": tabHistory, + "lastUsed": millisecondsToDecimalSeconds(lastUsed) + ] + } +} diff --git a/mobile/ios/Sync/TabsPayload.swift b/mobile/ios/Sync/TabsPayload.swift new file mode 100644 index 0000000000..28e32b9602 --- /dev/null +++ b/mobile/ios/Sync/TabsPayload.swift @@ -0,0 +1,155 @@ +/* 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 +import SwiftyJSON + +// Int64.max / 1000. +private let MaxSecondsToConvertInt64: Int64 = 9223372036854775 +private let MaxSecondsToConvertDouble: Double = Double(9223372036854775 as Int64) + +private let log = Logger.browserLogger + +open class TabsPayload: CleartextPayloadJSON { + open class Tab { + let title: String + let urlHistory: [String] + let lastUsed: Timestamp + let icon: String? + + fileprivate init(title: String, urlHistory: [String], lastUsed: Timestamp, icon: String?) { + self.title = title + self.urlHistory = urlHistory + self.lastUsed = lastUsed + self.icon = icon + } + + func toRemoteTabForClient(_ guid: GUID) -> RemoteTab? { + let urls = urlHistory.flatMap({ $0.asURL }) + if urls.isEmpty { + log.debug("Bug 1201875 - Discarding tab as history has no conforming URLs.") + return nil + } + + return RemoteTab(clientGUID: guid, URL: urls[0], title: self.title, history: urls, lastUsed: self.lastUsed, icon: self.icon?.asURL) + } + + class func remoteTabFromJSON(_ json: JSON, clientGUID: GUID) -> RemoteTab? { + return fromJSON(json)?.toRemoteTabForClient(clientGUID) + } + + class func fromJSON(_ json: JSON) -> Tab? { + func getLastUsed(_ json: JSON) -> Timestamp? { + let lastUsed = json["lastUsed"] + if lastUsed.isBool() { + return nil + } + // This might be a string or a number. + if let num = lastUsed.int64 { + if num < 0 { + // Timestamps are unsigned. + return nil + } + if num > MaxSecondsToConvertInt64 { + // This will overflow when multiplied. + return nil + } + return Timestamp(num * 1000) + } + + if let num = lastUsed.double { + if num < 0 { + // Timestamps are unsigned. + return nil + } + if num > MaxSecondsToConvertDouble { + // This will overflow when multiplied. + return nil + } + return Timestamp(num * 1000) + } + + if let num = lastUsed.string { + // Try parsing. + return someKindOfTimestampStringToTimestamp(num) + } + + return nil + } + + if let title = json["title"].string, + let urlHistory = jsonsToStrings(json["urlHistory"].array), + let lastUsed = getLastUsed(json) { + return Tab(title: title, urlHistory: urlHistory, lastUsed: lastUsed, icon: json["icon"].string) + } + + return nil + } + } + + override open func isValid() -> Bool { + if !super.isValid() { + return false + } + + if self["deleted"].bool ?? false { + return true + } + + return self["clientName"].isString() && + self["tabs"].isArray() + } + + // Eventually it'd be nice to unify RemoteTab and Tab. We want to kill the GUID in RemoteTab, + // at which point the only distinction between the two is that RemoteTab is "simple" and + // lives in Storage, and Tab is more closely tied to TabsPayload. + + var remoteTabs: [RemoteTab] { + if let clientGUID = self["id"].string { + let payloadTabs = self["tabs"].arrayValue + let remoteTabs = payloadTabs.flatMap({ Tab.remoteTabFromJSON($0, clientGUID: clientGUID) }) + if payloadTabs.count != remoteTabs.count { + log.debug("Bug 1201875 - Missing remote tabs from sync") + } + return remoteTabs + } + log.debug("no client ID for remote tabs") + return [] + } + + var tabs: [Tab] { + return self["tabs"].arrayValue.flatMap(Tab.fromJSON) + } + + var clientName: String { + return self["clientName"].string! + } + + override open func equalPayloads(_ obj: CleartextPayloadJSON) -> Bool { + if !(obj is TabsPayload) { + return false + } + + if !super.equalPayloads(obj) { + return false + } + + let p = obj as! TabsPayload + if p.clientName != self.clientName { + return false + } + + // TODO: compare tabs. + /* + if p.tabs != self.tabs { + return false; + } + */ + + return true + } +} diff --git a/mobile/ios/SyncTelemetry/Info.plist b/mobile/ios/SyncTelemetry/Info.plist new file mode 100644 index 0000000000..4eb29b3ffd --- /dev/null +++ b/mobile/ios/SyncTelemetry/Info.plist @@ -0,0 +1,24 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + FMWK + CFBundleShortVersionString + 10.6 + CFBundleVersion + $(CURRENT_PROJECT_VERSION) + NSPrincipalClass + + + diff --git a/mobile/ios/SyncTelemetry/SyncPingCentre.swift b/mobile/ios/SyncTelemetry/SyncPingCentre.swift new file mode 100644 index 0000000000..89bc6c317e --- /dev/null +++ b/mobile/ios/SyncTelemetry/SyncPingCentre.swift @@ -0,0 +1,170 @@ +/* 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 Alamofire +import Shared +import JSONSchema +import Deferred + +// MARK: Ping Centre Client +public protocol PingCentreClient { + @discardableResult func sendPing(_ data: [String: Any], validate: Bool) -> Success + @discardableResult func sendBatch(_ data: [[String: Any]], validate: Bool) -> Success +} + +/* + * A Ping Centre Topic has a name and an associated JSON schema describing the ping data. + */ +public struct PingCentreTopic { + public let name: String + public let schema: Schema + public init(name: String, schema: Schema) { + self.name = name + self.schema = schema + } +} + +public struct PingCentre { + public static func clientForTopic(_ topic: PingCentreTopic, clientID: String) -> PingCentreClient { + guard !AppConstants.IsRunningTest else { + return DefaultPingCentreImpl(topic: topic, endpoint: .staging, clientID: clientID) + } + + switch AppConstants.BuildChannel { + case .developer: + return DefaultPingCentreImpl(topic: topic, endpoint: .staging, clientID: clientID) + case .beta: + fallthrough + case .release: + return DefaultPingCentreImpl(topic: topic, endpoint: .production, clientID: clientID) + } + } +} + +public struct PingValidationError: MaybeErrorType { + public let errors: [String] + public var description: String { + return "Ping JSON validation failed with the following errors: \(errors)" + } +} + +public struct PingJSONError: MaybeErrorType { + public let error: Error + public var description: String { + return "Failed to serialize JSON ping into NSData format -- \(error)" + } +} + +enum Endpoint { + case staging + case production + + var url: URL { + switch self { + case .staging: + return URL(string: "https://onyx_tiles.stage.mozaws.net/v3/links/ping-centre")! + case .production: + return URL(string: "https://tiles.services.mozilla.com/v3/links/ping-centre")! + } + } +} + +class DefaultPingCentreImpl: PingCentreClient { + fileprivate let topic: PingCentreTopic + fileprivate let clientID: String + fileprivate let endpoint: Endpoint + fileprivate let manager: SessionManager + + fileprivate let validationQueue: DispatchQueue + fileprivate static let queueLabel = "org.mozilla.pingcentre.validationQueue" + + init(topic: PingCentreTopic, endpoint: Endpoint, clientID: String, + validationQueue: DispatchQueue = DispatchQueue(label: queueLabel), + manager: SessionManager = SessionManager()) { + self.topic = topic + self.clientID = clientID + self.endpoint = endpoint + self.manager = manager + self.validationQueue = validationQueue + } + + public func sendPing(_ data: [String: Any], validate: Bool) -> Success { + return (validate ? validatePayload(data, schema: topic.schema) : succeed()) + >>> { + do { + let request = try self.singleRequestFor(payload: data) + return self.send(request: request) + } catch let e { + return deferMaybe(PingJSONError(error: e)) + } + } + } + + public func sendBatch(_ data: [[String: Any]], validate: Bool) -> Success { + // Ignore call if we don't have anything to send! + guard !data.isEmpty else { + return succeed() + } + + // Walk through all the pings if we need to validate + return (validate ? walk(data) { self.validatePayload($0, schema: self.topic.schema) } : succeed()) + >>> { + do { + let request = try self.batchRequestFor(payloads: data) + return self.send(request: request) + } catch let e { + return deferMaybe(PingJSONError(error: e)) + } + } + } + + fileprivate func singleRequestFor(payload: [String: Any]) throws -> URLRequest { + var request = URLRequest(url: endpoint.url) + request.httpMethod = "POST" + request.setValue("application/json", forHTTPHeaderField: "Content-Type") + var root = payload + root["topic"] = topic.name + root["client_id"] = clientID + request.httpBody = try JSONSerialization.data(withJSONObject: root, options: []) + return request + } + + fileprivate func batchRequestFor(payloads: [[String: Any]]) throws -> URLRequest { + var request = URLRequest(url: endpoint.url) + request.httpMethod = "POST" + request.setValue("application/json", forHTTPHeaderField: "Content-Type") + let root: [String: Any] = [ + "topic": self.topic.name, + "batch-mode": true, + "payloads": payloads + ] + request.httpBody = try JSONSerialization.data(withJSONObject: root, options: []) + return request + } + + fileprivate func send(request: URLRequest) -> Success { + let deferred = Deferred>() + self.manager.request(request as URLRequestConvertible) + .validate(statusCode: 200..<300) + .response(queue: DispatchQueue.global()) { (response) in + if let e = response.error { + NSLog("Failed to send ping to ping centre -- topic: \(self.topic.name), error: \(e)") + deferred.fill(Maybe(failure: e as MaybeErrorType)) + return + } + deferred.fill(Maybe(success: ())) + } + return deferred + } + + fileprivate func validatePayload(_ payload: [String: Any], schema: Schema) -> Success { + return deferDispatchAsync(validationQueue) { + let errors = schema.validate(payload).errors ?? [] + guard errors.isEmpty else { + return deferMaybe(PingValidationError(errors: errors)) + } + return succeed() + } + } +} diff --git a/mobile/ios/SyncTelemetry/SyncTelemetry.h b/mobile/ios/SyncTelemetry/SyncTelemetry.h new file mode 100644 index 0000000000..b63450760a --- /dev/null +++ b/mobile/ios/SyncTelemetry/SyncTelemetry.h @@ -0,0 +1,15 @@ +/* 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 + +//! Project version number for SyncTelemetry. +FOUNDATION_EXPORT double TelemetryVersionNumber; + +//! Project version string for SyncTelemetry. +FOUNDATION_EXPORT const unsigned char TelemetryVersionString[]; + +// In this header, you should import all the public headers of your framework using statements like #import + + diff --git a/mobile/ios/SyncTelemetry/SyncTelemetry.swift b/mobile/ios/SyncTelemetry/SyncTelemetry.swift new file mode 100644 index 0000000000..89c04eceaa --- /dev/null +++ b/mobile/ios/SyncTelemetry/SyncTelemetry.swift @@ -0,0 +1,120 @@ +/* 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 Alamofire +import Foundation +import XCGLogger +import SwiftyJSON +import Shared + +private let log = Logger.browserLogger +private let ServerURL = "https://incoming.telemetry.mozilla.org".asURL! +private let AppName = "Fennec" + +public enum TelemetryDocType: String { + case core = "core" + case sync = "sync" +} + +public protocol SyncTelemetryEvent { + func record(_ prefs: Prefs) +} + +open class SyncTelemetry { + private static var prefs: Prefs? + private static var telemetryVersion: Int = 4 + + open class func initWithPrefs(_ prefs: Prefs) { + assert(self.prefs == nil, "Prefs already initialized") + self.prefs = prefs + } + + open class func recordEvent(_ event: SyncTelemetryEvent) { + guard let prefs = prefs else { + assertionFailure("Prefs not initialized") + return + } + + event.record(prefs) + } + + open class func send(ping: SyncTelemetryPing, docType: TelemetryDocType) { + let docID = UUID().uuidString + let appVersion = Bundle.main.infoDictionary!["CFBundleShortVersionString"] as! String + let buildID = Bundle.main.object(forInfoDictionaryKey: kCFBundleVersionKey as String) as! String + + let channel = AppConstants.BuildChannel.rawValue + let path = "/submit/telemetry/\(docID)/\(docType.rawValue)/\(AppName)/\(appVersion)/\(channel)/\(buildID)" + let url = ServerURL.appendingPathComponent(path) + var request = URLRequest(url: url) + + log.debug("Ping URL: \(url)") + log.debug("Ping payload: \(ping.payload.stringValue() ?? "")") + + // Don't add the common ping format for the mobile core ping. + let pingString: String? + if docType != .core { + var json = JSON(commonPingFormat(forType: docType)) + json["payload"] = ping.payload + pingString = json.stringValue() + } else { + pingString = ping.payload.stringValue() + } + + guard let body = pingString?.data(using: String.Encoding.utf8) else { + log.error("Invalid data!") + assertionFailure() + return + } + + guard channel != "default" else { + log.debug("Non-release build; not sending ping") + return + } + + request.httpMethod = "POST" + request.httpBody = body + request.addValue(Date().toRFC822String(), forHTTPHeaderField: "Date") + request.setValue("application/json", forHTTPHeaderField: "Content-Type") + + SessionManager.default.request(request).response { response in + log.debug("Ping response: \(response.response?.statusCode ?? -1).") + } + } + + private static func commonPingFormat(forType type: TelemetryDocType) -> [String: Any] { + let formatter = DateFormatter() + formatter.dateFormat = "yyyy-MM-dd" + formatter.locale = Locale(identifier: "en_US_POSIX") + formatter.timeZone = TimeZone(secondsFromGMT: 0) + let date = formatter.string(from: NSDate() as Date) + let displayVersion = [ + AppInfo.appVersion, + "b", + AppInfo.buildNumber + ].joined() + let version = ProcessInfo.processInfo.operatingSystemVersion + let osVersion = "\(version.majorVersion).\(version.minorVersion).\(version.patchVersion)" + + return [ + "type": type.rawValue, + "id": UUID().uuidString, + "creationDate": date, + "version": SyncTelemetry.telemetryVersion, + "application": [ + "architecture": "arm", + "buildId": AppInfo.buildNumber, + "name": AppInfo.displayName, + "version": AppInfo.appVersion, + "displayVersion": displayVersion, + "platformVersion": osVersion, + "channel": AppConstants.BuildChannel.rawValue + ] + ] + } +} + +public protocol SyncTelemetryPing { + var payload: JSON { get } +} diff --git a/mobile/ios/SyncTelemetry/SyncTelemetryEvents.swift b/mobile/ios/SyncTelemetry/SyncTelemetryEvents.swift new file mode 100644 index 0000000000..7aac85ce6a --- /dev/null +++ b/mobile/ios/SyncTelemetry/SyncTelemetryEvents.swift @@ -0,0 +1,98 @@ +/* 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 Shared +import SwiftyJSON + +private let log = Logger.browserLogger + +public typealias IdentifierString = String +public extension IdentifierString { + func validate() -> Bool { + // Regex located here: http://gecko.readthedocs.io/en/latest/toolkit/components/telemetry/telemetry/collection/events.html#limits + let regex = try! NSRegularExpression(pattern: "^[a-zA-Z][a-zA-Z0-9_.]*[a-zA-Z0-9]$", options: []) + return regex.matches(in: self, options: [], range: NSRange(location: 0, length: self.characters.count)).count > 0 + } +} + +// Telemetry Events +// Documentation: http://gecko.readthedocs.io/en/latest/toolkit/components/telemetry/telemetry/collection/events.html#events +public struct Event { + let timestamp: Timestamp + let category: IdentifierString + let method: IdentifierString + let object: IdentifierString + let value: String? + let extra: [String: String]? + + public init(category: IdentifierString, + method: IdentifierString, + object: IdentifierString, + value: String? = nil, + extra: [String: String]? = nil) { + + self.init(timestamp: .uptimeInMilliseconds(), + category: category, + method: method, + object: object, + value: value, + extra: extra) + } + + init(timestamp: Timestamp, + category: IdentifierString, + method: IdentifierString, + object: IdentifierString, + value: String? = nil, + extra: [String: String]? = nil) { + self.timestamp = timestamp + self.category = category + self.method = method + self.object = object + self.value = value + self.extra = extra + } + + public func validate() -> Bool { + let results = [category, method, object].map { $0.validate() } + // Fold down the results into false if any of the results is false. + return results.reduce(true) { $0 ? $1 :$0 } + } + + public func pickle() -> Data? { + do { + return try JSONSerialization.data(withJSONObject: toArray(), options: []) + } catch let error { + log.error("Error pickling telemetry event. Error: \(error), Event: \(self)") + return nil + } + } + + public static func unpickle(_ data: Data) -> Event? { + do { + let array = try JSONSerialization.jsonObject(with: data, options: []) as! [Any] + return Event( + timestamp: Timestamp(array[0] as! UInt64), + category: array[1] as! String, + method: array[2] as! String, + object: array[3] as! String, + value: array[4] as? String, + extra: array[5] as? [String: String] + ) + } catch let error { + log.error("Error unpickling telemetry event: \(error)") + return nil + } + } + + public func toArray() -> [Any] { + return [timestamp, category, method, object, value ?? NSNull(), extra ?? NSNull()] + } +} + +extension Event: CustomDebugStringConvertible { + public var debugDescription: String { + return toArray().description + } +} diff --git a/mobile/ios/SyncTelemetryTests/EventTests.swift b/mobile/ios/SyncTelemetryTests/EventTests.swift new file mode 100644 index 0000000000..ff95fec287 --- /dev/null +++ b/mobile/ios/SyncTelemetryTests/EventTests.swift @@ -0,0 +1,102 @@ +/* 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 XCTest +import Shared +@testable import SyncTelemetry + +class EventTests: XCTestCase { + func testPickling() { + let mockTimestamp = Date.now() + XCTAssertEqual(String(data: basicEvent(timestamp: mockTimestamp).pickle()!, encoding: .utf8), + basicEventString(timestamp: mockTimestamp)) + XCTAssertEqual(String(data: basicWithValueEvent(timestamp: mockTimestamp).pickle()!, encoding: .utf8), + basicWithValueString(timestamp: mockTimestamp)) + XCTAssertEqual(String(data: extraEvent(timestamp: mockTimestamp).pickle()!, encoding: .utf8), + extraEventString(timestamp: mockTimestamp)) + } + + func testUnpickling() { + let mockTimestamp = Date.now() + let dataA = basicEventString(timestamp: mockTimestamp).data(using: .utf8)! + XCTAssertEqual(Event.unpickle(dataA), basicEvent(timestamp: mockTimestamp)) + + let dataB = basicWithValueString(timestamp: mockTimestamp).data(using: .utf8)! + XCTAssertEqual(Event.unpickle(dataB), basicWithValueEvent(timestamp: mockTimestamp)) + + let dataC = extraEventString(timestamp: mockTimestamp).data(using: .utf8)! + XCTAssertEqual(Event.unpickle(dataC), extraEvent(timestamp: mockTimestamp)) + } + + func testIdentifierStringValidation() { + let identifierA: IdentifierString = "basic" + XCTAssertTrue(identifierA.validate()) + + let identifierB: IdentifierString = "complex1234_more1234" + XCTAssertTrue(identifierB.validate()) + + let identifierC: IdentifierString = "invalid#!" + XCTAssertFalse(identifierC.validate()) + + let identifierD: IdentifierString = "almost_valid." + XCTAssertFalse(identifierD.validate()) + } + + func testEventValidation() { + let mockTimestamp = Date.now() + let eventA = basicEvent(timestamp: mockTimestamp) + XCTAssertTrue(eventA.validate()) + + let eventB = extraEvent(timestamp: mockTimestamp) + XCTAssertTrue(eventB.validate()) + + let eventC = Event(category: "invalid__", method: "valid", object: "valid") + XCTAssertFalse(eventC.validate()) + + let eventD = Event(category: "valid", method: "invalid__", object: "valid") + XCTAssertFalse(eventD.validate()) + } +} + +// Builder methods for easier mocking of the pickled/unpickled events +extension EventTests { + fileprivate func basicEventString(timestamp: Timestamp) -> String { + return "[\(timestamp),\"test\",\"pickling\",\"this\",null,null]" + } + + fileprivate func basicWithValueString(timestamp: Timestamp) -> String { + return "[\(timestamp),\"test\",\"pickling\",\"this\",\"value\",null]" + } + + fileprivate func extraEventString(timestamp: Timestamp) -> String { + return "[\(timestamp),\"test\",\"pickling\",\"this\",\"value\",{\"flowID\":\"testFlowID\",\"numIDs\":\"12\"}]" + } + + fileprivate func basicEvent(timestamp: Timestamp) -> Event { + return Event(timestamp: timestamp, category: "test", method: "pickling", object: "this") + } + + fileprivate func basicWithValueEvent(timestamp: Timestamp) -> Event { + return Event(timestamp: timestamp, category: "test", method: "pickling", object: "this", value: "value") + } + + fileprivate func extraEvent(timestamp: Timestamp) -> Event { + let extra = ["flowID": "testFlowID", "numIDs": "12"] + return Event(timestamp: timestamp, category: "test", method: "pickling", object: "this", value: "value", extra: extra) + } +} + +// Don't have a need for this in the app but is great for testing! +extension Event: Equatable { + public static func ==(left: Event, right: Event) -> Bool { + let propsAreEqual = (left.category == right.category) && + (left.method == right.method) && + (left.object == right.object) && + (left.value ?? "" == right.value ?? "") && + (left.extra ?? [:] == right.extra ?? [:]) + return propsAreEqual + } +} + diff --git a/mobile/ios/SyncTelemetryTests/Info.plist b/mobile/ios/SyncTelemetryTests/Info.plist new file mode 100644 index 0000000000..6366f45b20 --- /dev/null +++ b/mobile/ios/SyncTelemetryTests/Info.plist @@ -0,0 +1,22 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + BNDL + CFBundleShortVersionString + 10.6 + CFBundleVersion + 1 + + diff --git a/mobile/ios/SyncTests/BatchingClientTests.swift b/mobile/ios/SyncTests/BatchingClientTests.swift new file mode 100644 index 0000000000..d7f1b19733 --- /dev/null +++ b/mobile/ios/SyncTests/BatchingClientTests.swift @@ -0,0 +1,608 @@ +/* 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 Deferred +@testable import Sync + +import XCTest +import SwiftyJSON + +// Always return a gigantic encoded payload. +private func massivify(_ record: Record) -> JSON? { + return JSON([ + "id": record.id, + "foo": String(repeating: "X", count: Sync15StorageClient.maxRecordSizeBytes + 1) + ]) +} + +private func basicSerializer(record: Record) -> String { + return JSON(object: [ + "id": record.id, + "payload": record.payload.json.dictionaryObject as Any + ]).stringValue()! +} + +// Create a basic record with an ID and a title that is the `Site$ID`. +private func createRecordWithID(id: String) -> Record { + let jsonString = "{\"id\":\"\(id)\",\"title\": \"\(id)\"}" + return Record(id: id, + payload: CleartextPayloadJSON(JSON(parseJSON: jsonString)), + modified: 10_000, + sortindex: 123, + ttl: 1_000_000) +} + +private func assertLinesMatchRecords(lines: [String], records: [Record], serializer: (Record) -> String) { + guard lines.count == records.count else { + XCTFail("Number of lines mismatch number of records") + return + } + + lines.enumerated().forEach { index, line in + let record = records[index] + XCTAssertEqual(line, serializer(record)) + } +} + +private func deferEmptyResponse(token batchToken: BatchToken? = nil, lastModified: Timestamp? = nil) -> Deferred>> { + var headers = [String: Any]() + if let lastModified = lastModified { + headers["X-Last-Modified"] = lastModified + } + + return deferMaybe( + StorageResponse(value: POSTResult(success: [], failed: [:], batchToken: batchToken), metadata: ResponseMetadata(status: 200, headers: headers)) + ) +} + +// Small helper operator for comparing query parameters below +private func ==(param1: NSURLQueryItem, param2: NSURLQueryItem) -> Bool { + return param1.name == param2.name && param1.value == param2.value +} + +private let miniConfig = InfoConfiguration(maxRequestBytes: 1_048_576, maxPostRecords: 2, maxPostBytes: 1_048_576, maxTotalRecords: 10, maxTotalBytes: 104_857_600) + +class Sync15BatchClientTests: XCTestCase { + + func testAddLargeRecordFails() { + let uploader: BatchUploadFunction = { _ in deferEmptyResponse(lastModified: 10_000) } + let serializeRecord = { massivify($0)?.stringValue() } + + let batch = Sync15BatchClient(config: miniConfig, + ifUnmodifiedSince: nil, + serializeRecord: serializeRecord, + uploader: uploader, + onCollectionUploaded: { _ in deferMaybe(Date() as! MaybeErrorType)}) + + let record = createRecordWithID(id: "A") + let result = batch.addRecords([record]).value + XCTAssertTrue(result.isFailure) + XCTAssertTrue(result.failureValue! is RecordTooLargeError) + } + + func testFailToSerializeRecord() { + let uploader: BatchUploadFunction = { _ in deferEmptyResponse(lastModified: 10_000) } + let batch = Sync15BatchClient(config: miniConfig, + ifUnmodifiedSince: nil, + serializeRecord: { _ in nil }, + uploader: uploader, + onCollectionUploaded: { _ in deferMaybe(Date.now())}) + + let record = createRecordWithID(id: "A") + let result = batch.addRecords([record]).value + XCTAssertTrue(result.isFailure) + XCTAssertTrue(result.failureValue! is SerializeRecordFailure) + } + + func testBackoffDuringBatchUploading() { + let uploader: BatchUploadFunction = { lines, ius, queryParams in + deferMaybe(ServerInBackoffError(until: 10_000)) + } + + // Setup a configuration so each batch supports two payloads of two records each + let twoRecordBatchesConfig = InfoConfiguration( + maxRequestBytes: 1_048_576, + maxPostRecords: 1, + maxPostBytes: 1_048_576, + maxTotalRecords: 4, + maxTotalBytes: 104_857_600 + ) + + let batch = Sync15BatchClient(config: twoRecordBatchesConfig, + ifUnmodifiedSince: 10_000, + serializeRecord: basicSerializer, + uploader: uploader, + onCollectionUploaded: { _ in deferMaybe(Date.now())}) + + let record = createRecordWithID(id: "A") + batch.addRecords([record]).succeeded() + let result = batch.endBatch().value + + // Verify that when the server goes into backoff, we get those bubbled up through the batching client + XCTAssertTrue(result.isFailure) + XCTAssertTrue(result.failureValue! is ServerInBackoffError) + } + + func testIfUnmodifiedSinceUpdatesForSinglePOSTs() { + var requestCount = 0 + var linesSent = [String]() + var lastIUS: Timestamp? = 0 + + // Since we never return a batch token, the batch client won't batch upload and default to single POSTs + let uploader: BatchUploadFunction = { lines, ius, _ in + linesSent += lines + requestCount += 1 + switch requestCount { + case 1: + XCTAssertEqual(ius, 10_000_000) + return deferEmptyResponse(lastModified: 20_000) + case 2: + XCTAssertEqual(ius, 20_000_000) + return deferEmptyResponse(lastModified: 30_000) + case 3: + XCTAssertEqual(ius, 30_000_000) + lastIUS = ius + return deferEmptyResponse(lastModified: 30_000) + default: + XCTFail() + return deferEmptyResponse(lastModified: 0) + } + } + + let singleRecordConfig = InfoConfiguration( + maxRequestBytes: 1_048_576, + maxPostRecords: 1, + maxPostBytes: 1_048_576, + maxTotalRecords: 10, + maxTotalBytes: 104_857_600 + ) + + let batch = Sync15BatchClient(config: singleRecordConfig, + ifUnmodifiedSince: 10_000_000, + serializeRecord: basicSerializer, + uploader: uploader, + onCollectionUploaded: { _ in deferMaybe(Date.now())}) + + let recordA = createRecordWithID(id: "A") + let recordB = createRecordWithID(id: "B") + let recordC = createRecordWithID(id: "C") + let allRecords = [recordA, recordB, recordC] + + batch.addRecords([recordA, recordB, recordC]).succeeded() + batch.endBatch().succeeded() + + // Validate number of requests sent + XCTAssertEqual(requestCount, 3) + + // Validate contents sent to the server + assertLinesMatchRecords(lines: linesSent, records: allRecords, serializer: basicSerializer) + + // Validate the last IUS we got is the last request + XCTAssertEqual(lastIUS, 30_000_000) + } + + func testUploadBatchUnsupportedBatching() { + var requestCount = 0 + var uploadedCollectionCount = 0 + var linesSent = [String]() + + // Since we never return a batch token, the batch client won't batch upload and default to single POSTs + let uploader: BatchUploadFunction = { lines, ius, _ in + linesSent += lines + requestCount += 1 + return deferEmptyResponse(lastModified: 10_000) + } + + let collectionUploaded: (POSTResult, Timestamp?) -> DeferredTimestamp = { _ in + uploadedCollectionCount += 1 + return deferMaybe(Date.now()) + } + + // Setup a configuration so we each payload would be one record + let twoRecordBatchesConfig = InfoConfiguration( + maxRequestBytes: 1_048_576, + maxPostRecords: 1, + maxPostBytes: 1_048_576, + maxTotalRecords: 10, + maxTotalBytes: 104_857_600 + ) + + let batch = Sync15BatchClient(config: twoRecordBatchesConfig, + ifUnmodifiedSince: 10_000, + serializeRecord: basicSerializer, + uploader: uploader, + onCollectionUploaded: collectionUploaded) + + let recordA = createRecordWithID(id: "A") + let recordB = createRecordWithID(id: "B") + let allRecords = [recordA, recordB] + + batch.addRecords([recordA, recordB]).succeeded() + batch.endBatch().succeeded() + + // Validate number of requests sent. One for the start post, and one for the committing + XCTAssertEqual(requestCount, 2) + + // Validate contents sent to the server + assertLinesMatchRecords(lines: linesSent, records: allRecords, serializer: basicSerializer) + + // Validate we only made 2 calls to collection uploaded since we're doing single POSTs + XCTAssertEqual(uploadedCollectionCount, 2) + } + + /** + Tests sending a batch consisting of 3 full payloads. This batch is regular in the sense that it should + contain a batch=true call, an upload within the batch, and finish with a commit=true upload. + */ + func testUploadRegularSingleBatch() { + var requestCount = 0 + var uploadedCollectionCount = 0 + var linesSent = [String]() + + let allRecords: [Record] = "ABCDEF".characters.reduce([]) { list, char in + return list + [createRecordWithID(id: String(char))] + } + + // Since we never return a batch token, the batch client won't batch upload and default to single POSTs + let uploader: BatchUploadFunction = { lines, ius, queryParams in + linesSent += lines + requestCount += 1 + switch requestCount { + case 1: + let expected = URLQueryItem(name: "batch", value: "true") + XCTAssertEqual(expected, queryParams![0]) + XCTAssertEqual(ius, 10_000) + assertLinesMatchRecords(lines: lines, records: Array(allRecords[0..<2]), serializer: basicSerializer) + return deferEmptyResponse(token: "1", lastModified: 10_000) + case 2: + let expected = URLQueryItem(name: "batch", value: "1") + XCTAssertEqual(expected, queryParams![0]) + XCTAssertEqual(ius, 10_000_000) + assertLinesMatchRecords(lines: lines, records: Array(allRecords[2..<4]), serializer: basicSerializer) + return deferEmptyResponse(token: "1", lastModified: 10_000) + case 3: + let expectedBatch = URLQueryItem(name: "batch", value: "1") + let expectedCommit = URLQueryItem(name: "commit", value: "true") + XCTAssertEqual(expectedBatch, queryParams![0]) + XCTAssertEqual(expectedCommit, queryParams![1]) + XCTAssertEqual(ius, 10_000_000) + assertLinesMatchRecords(lines: lines, records: Array(allRecords[4..<6]), serializer: basicSerializer) + return deferEmptyResponse(lastModified: 20_000) + default: + XCTFail() + return deferEmptyResponse(lastModified: 0) + } + } + + let collectionUploaded: (POSTResult, Timestamp?) -> DeferredTimestamp = { _ in + uploadedCollectionCount += 1 + return deferMaybe(Date.now()) + } + + // Setup a configuration so we send 2 records per each payload + let twoRecordBatchesConfig = InfoConfiguration( + maxRequestBytes: 1_048_576, + maxPostRecords: 2, + maxPostBytes: 1_048_576, + maxTotalRecords: 10, + maxTotalBytes: 104_857_600 + ) + + let batch = Sync15BatchClient(config: twoRecordBatchesConfig, + ifUnmodifiedSince: 10_000, + serializeRecord: basicSerializer, + uploader: uploader, + onCollectionUploaded: collectionUploaded) + + batch.addRecords(allRecords).succeeded() + batch.endBatch().succeeded() + + // Validate number of requests sent. One for the start post, and one for the committing + XCTAssertEqual(requestCount, 3) + + // Validate contents sent to the server + assertLinesMatchRecords(lines: linesSent, records: allRecords, serializer: basicSerializer) + + // Validate we only made one call to the collection upload callback + XCTAssertEqual(uploadedCollectionCount, 2) + XCTAssertEqual(batch.ifUnmodifiedSince!, 20_000_000) + } + + /** + Tests pushing a batch where one of the payloads is not at limit. + */ + func testBatchUploadWithPartialPayload() { + var requestCount = 0 + var uploadedCollectionCount = 0 + var linesSent = [String]() + + let allRecords: [Record] = "ABC".characters.reduce([]) { list, char in + return list + [createRecordWithID(id: String(char))] + } + + // Since we never return a batch token, the batch client won't batch upload and default to single POSTs + let uploader: BatchUploadFunction = { lines, ius, queryParams in + linesSent += lines + requestCount += 1 + switch requestCount { + case 1: + let expected = URLQueryItem(name: "batch", value: "true") + XCTAssertEqual(expected, queryParams![0]) + XCTAssertEqual(ius, 10_000) + assertLinesMatchRecords(lines: lines, records: Array(allRecords[0..<2]), serializer: basicSerializer) + return deferEmptyResponse(token: "1", lastModified: 10_000) + case 2: + let expectedBatch = URLQueryItem(name: "batch", value: "1") + let expectedCommit = URLQueryItem(name: "commit", value: "true") + XCTAssertEqual(expectedBatch, queryParams![0]) + XCTAssertEqual(expectedCommit, queryParams![1]) + XCTAssertEqual(ius, 10_000_000) + assertLinesMatchRecords(lines: lines, records: Array(allRecords[2..<3]), serializer: basicSerializer) + return deferEmptyResponse(lastModified: 20_000) + default: + XCTFail() + return deferEmptyResponse(lastModified: 0) + } + } + + let collectionUploaded: (POSTResult, Timestamp?) -> DeferredTimestamp = { _ in + uploadedCollectionCount += 1 + return deferMaybe(Date.now()) + } + + // Setup a configuration so we send 2 records per each payload + let twoRecordBatchesConfig = InfoConfiguration( + maxRequestBytes: 1_048_576, + maxPostRecords: 2, + maxPostBytes: 1_048_576, + maxTotalRecords: 10, + maxTotalBytes: 104_857_600 + ) + + let batch = Sync15BatchClient(config: twoRecordBatchesConfig, + ifUnmodifiedSince: 10_000, + serializeRecord: basicSerializer, + uploader: uploader, + onCollectionUploaded: collectionUploaded) + + batch.addRecords(allRecords).succeeded() + batch.endBatch().succeeded() + + // Validate number of requests sent. One for the start post, and one for the committing + XCTAssertEqual(requestCount, 2) + + // Validate contents sent to the server + assertLinesMatchRecords(lines: linesSent, records: allRecords, serializer: basicSerializer) + + // Validate we only made one call to the collection upload callback + XCTAssertEqual(uploadedCollectionCount, 2) + XCTAssertEqual(batch.ifUnmodifiedSince!, 20_000_000) + } + + /** + Attempt to send 3 payloads: 2 which are full, 1 that is partial, within a batch that only supports 5 records. + */ + func testBatchUploadWithUnevenPayloadsInBatch() { + var requestCount = 0 + var uploadedCollectionCount = 0 + var linesSent = [String]() + + let allRecords: [Record] = "ABCDE".characters.reduce([]) { list, char in + return list + [createRecordWithID(id: String(char))] + } + + // Since we never return a batch token, the batch client won't batch upload and default to single POSTs + let uploader: BatchUploadFunction = { lines, ius, queryParams in + linesSent += lines + requestCount += 1 + switch requestCount { + case 1: + let expected = URLQueryItem(name: "batch", value: "true") + XCTAssertEqual(expected, queryParams![0]) + XCTAssertEqual(ius, 10_000) + assertLinesMatchRecords(lines: lines, records: Array(allRecords[0..<2]), serializer: basicSerializer) + return deferEmptyResponse(token: "1", lastModified: 10_000) + case 2: + let expectedBatch = URLQueryItem(name: "batch", value: "1") + XCTAssertEqual(expectedBatch, queryParams![0]) + XCTAssertEqual(ius, 10_000_000) + assertLinesMatchRecords(lines: lines, records: Array(allRecords[2..<4]), serializer: basicSerializer) + return deferEmptyResponse(lastModified: 10_000) + case 3: + let expectedBatch = URLQueryItem(name: "batch", value: "1") + let expectedCommit = URLQueryItem(name: "commit", value: "true") + XCTAssertEqual(expectedBatch, queryParams![0]) + XCTAssertEqual(expectedCommit, queryParams![1]) + XCTAssertEqual(ius, 10_000_000) + assertLinesMatchRecords(lines: lines, records: [allRecords[4]], serializer: basicSerializer) + return deferEmptyResponse(lastModified: 20_000) + default: + XCTFail() + return deferEmptyResponse(lastModified: 0) + } + } + + let collectionUploaded: (POSTResult, Timestamp?) -> DeferredTimestamp = { _ in + uploadedCollectionCount += 1 + return deferMaybe(Date.now()) + } + + // Setup a configuration so we send 2 records per each payload + let twoRecordBatchesConfig = InfoConfiguration( + maxRequestBytes: 1_048_576, + maxPostRecords: 2, + maxPostBytes: 1_048_576, + maxTotalRecords: 5, + maxTotalBytes: 104_857_600 + ) + + let batch = Sync15BatchClient(config: twoRecordBatchesConfig, + ifUnmodifiedSince: 10_000, + serializeRecord: basicSerializer, + uploader: uploader, + onCollectionUploaded: collectionUploaded) + + batch.addRecords(allRecords).succeeded() + batch.endBatch().succeeded() + + // Validate number of requests sent. One for the start post, and one for the committing + XCTAssertEqual(requestCount, 3) + + // Validate contents sent to the server + assertLinesMatchRecords(lines: linesSent, records: allRecords, serializer: basicSerializer) + + // Validate we only made one call to the collection upload callback + XCTAssertEqual(uploadedCollectionCount, 2) + XCTAssertEqual(batch.ifUnmodifiedSince!, 20_000_000) + } + + /** + Tests pushing up a single payload as part of a batch. + */ + func testBatchUploadWithSinglePayload() { + var requestCount = 0 + var uploadedCollectionCount = 0 + var linesSent = [String]() + + let recordA = createRecordWithID(id: "A") + + // Since we never return a batch token, the batch client won't batch upload and default to single POSTs + let uploader: BatchUploadFunction = { lines, ius, queryParams in + linesSent += lines + requestCount += 1 + switch requestCount { + case 1: + XCTAssertEqual(ius, 10_000) + assertLinesMatchRecords(lines: lines, records: [recordA], serializer: basicSerializer) + return deferEmptyResponse(lastModified: 20_000) + default: + XCTFail() + return deferEmptyResponse(lastModified: 0) + } + } + + let collectionUploaded: (POSTResult, Timestamp?) -> DeferredTimestamp = { _ in + uploadedCollectionCount += 1 + return deferMaybe(Date.now()) + } + + // Setup a configuration so we send 2 records per each payload + let twoRecordBatchesConfig = InfoConfiguration( + maxRequestBytes: 1_048_576, + maxPostRecords: 2, + maxPostBytes: 1_048_576, + maxTotalRecords: 10, + maxTotalBytes: 104_857_600 + ) + + let batch = Sync15BatchClient(config: twoRecordBatchesConfig, + ifUnmodifiedSince: 10_000, + serializeRecord: basicSerializer, + uploader: uploader, + onCollectionUploaded: collectionUploaded) + + batch.addRecords([recordA]).succeeded() + batch.endBatch().succeeded() + + // Validate number of requests sent. One for the start post, and one for the committing + XCTAssertEqual(requestCount, 1) + + // Validate contents sent to the server + assertLinesMatchRecords(lines: linesSent, records: [recordA], serializer: basicSerializer) + + // Validate we only made one call to the collection upload callback + XCTAssertEqual(uploadedCollectionCount, 1) + XCTAssertEqual(batch.ifUnmodifiedSince!, 20_000_000) + } + + func testMultipleBatchUpload() { + var requestCount = 0 + var uploadedCollectionCount = 0 + var linesSent = [String]() + + let allRecords: [Record] = "ABCDEFGHIJKL".characters.reduce([]) { list, char in + return list + [createRecordWithID(id: String(char))] + } + + // For each upload, verify that we are getting the correct queryParams and records to be sent. + let uploader: BatchUploadFunction = { lines, ius, queryParams in + linesSent += lines + requestCount += 1 + switch requestCount { + case 1: + let expected = URLQueryItem(name: "batch", value: "true") + XCTAssertEqual(expected, queryParams![0]) + assertLinesMatchRecords(lines: lines, records: Array(allRecords[0..<2]), serializer: basicSerializer) + return deferEmptyResponse(token: "1", lastModified: 20_000) + case 2: + let expectedBatch = URLQueryItem(name: "batch", value: "1") + XCTAssertEqual(expectedBatch, queryParams![0]) + assertLinesMatchRecords(lines: lines, records: Array(allRecords[2..<4]), serializer: basicSerializer) + return deferEmptyResponse(lastModified: 20_000) + case 3: + let expectedBatch = URLQueryItem(name: "batch", value: "1") + let expectedCommit = URLQueryItem(name: "commit", value: "true") + XCTAssertEqual(expectedBatch, queryParams![0]) + XCTAssertEqual(expectedCommit, queryParams![1]) + assertLinesMatchRecords(lines: lines, records: Array(allRecords[4..<6]), serializer: basicSerializer) + return deferEmptyResponse(lastModified: 20_000) + case 4: + let expected = URLQueryItem(name: "batch", value: "true") + XCTAssertEqual(expected, queryParams![0]) + assertLinesMatchRecords(lines: lines, records: Array(allRecords[6..<8]), serializer: basicSerializer) + return deferEmptyResponse(token: "2", lastModified: 30_000) + case 5: + let expectedBatch = URLQueryItem(name: "batch", value: "2") + XCTAssertEqual(expectedBatch, queryParams![0]) + assertLinesMatchRecords(lines: lines, records: Array(allRecords[8..<10]), serializer: basicSerializer) + return deferEmptyResponse(lastModified: 30_000) + case 6: + let expectedBatch = URLQueryItem(name: "batch", value: "2") + let expectedCommit = URLQueryItem(name: "commit", value: "true") + XCTAssertEqual(expectedBatch, queryParams![0]) + XCTAssertEqual(expectedCommit, queryParams![1]) + assertLinesMatchRecords(lines: lines, records: Array(allRecords[10..<12]), serializer: basicSerializer) + return deferEmptyResponse(lastModified: 30_000) + default: + XCTFail() + return deferEmptyResponse(lastModified: 0) + } + } + + let collectionUploaded: (POSTResult, Timestamp?) -> DeferredTimestamp = { _ in + uploadedCollectionCount += 1 + return deferMaybe(Date.now()) + } + + // Setup a configuration so each batch supports two payloads of two records each + let twoRecordBatchesConfig = InfoConfiguration( + maxRequestBytes: 1_048_576, + maxPostRecords: 2, + maxPostBytes: 1_048_576, + maxTotalRecords: 6, + maxTotalBytes: 104_857_600 + ) + + let batch = Sync15BatchClient(config: twoRecordBatchesConfig, + ifUnmodifiedSince: 10_000_000, + serializeRecord: basicSerializer, + uploader: uploader, + onCollectionUploaded: collectionUploaded) + + batch.addRecords(allRecords).succeeded() + batch.endBatch().succeeded() + + // Validate number of requests sent. One for the start post, and one for the committing + XCTAssertEqual(requestCount, 6) + + // Validate contents sent to the server + assertLinesMatchRecords(lines: linesSent, records: allRecords, serializer: basicSerializer) + + // Validate we only called collection uploaded when we start and finish a batch. The uploads inside + // a batch should not trigger the callback. + XCTAssertEqual(uploadedCollectionCount, 4) + } +} diff --git a/mobile/ios/SyncTests/CryptoTests.swift b/mobile/ios/SyncTests/CryptoTests.swift new file mode 100644 index 0000000000..a1bd80662d --- /dev/null +++ b/mobile/ios/SyncTests/CryptoTests.swift @@ -0,0 +1,88 @@ +/* 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 UIKit +import Shared +import Storage +@testable import Sync + +import XCTest + +class CryptoTests: XCTestCase { + let hmacB16 = "b1e6c18ac30deb70236bc0d65a46f7a4dce3b8b0e02cf92182b914e3afa5eebc" + let ivB64 = "GX8L37AAb2FZJMzIoXlX8w==" + + let hmacKey = Bytes.decodeBase64("MMntEfutgLTc8FlTLQFms8/xMPmCldqPlq/QQXEjx70=")! + let encKey = Bytes.decodeBase64("9K/wLdXdw+nrTtXo4ZpECyHFNr4d7aYHqeg3KW9+m6Q=")! + + let invalidB64 = "NMsdnRulLwQsVcwxKW9XwaUe7ouJk5~~~~~~~~~~~~~~~" + + let ciphertextB64 = "NMsdnRulLwQsVcwxKW9XwaUe7ouJk5Wn80QhbD80l0HEcZGCynh45qIbeYBik0lgcHbKmlIxTJNwU+OeqipN+/j7MqhjKOGIlvbpiPQQLC6/ffF2vbzL0nzMUuSyvaQzyGGkSYM2xUFt06aNivoQTvU2GgGmUK6MvadoY38hhW2LCMkoZcNfgCqJ26lO1O0sEO6zHsk3IVz6vsKiJ2Hq6VCo7hu123wNegmujHWQSGyf8JeudZjKzfi0OFRRvvm4QAKyBWf0MgrW1F8SFDnVfkq8amCB7NhdwhgLWbN+21NitNwWYknoEWe1m6hmGZDgDT32uxzWxCV8QqqrpH/ZggViEr9uMgoy4lYaWqP7G5WKvvechc62aqnsNEYhH26A5QgzmlNyvB+KPFvPsYzxDnSCjOoRSLx7GG86wT59QZw=" + + let cleartextB64 = "eyJpZCI6IjVxUnNnWFdSSlpYciIsImhpc3RVcmkiOiJmaWxlOi8vL1VzZXJzL2phc29uL0xpYnJhcnkvQXBwbGljYXRpb24lMjBTdXBwb3J0L0ZpcmVmb3gvUHJvZmlsZXMva3NnZDd3cGsuTG9jYWxTeW5jU2VydmVyL3dlYXZlL2xvZ3MvIiwidGl0bGUiOiJJbmRleCBvZiBmaWxlOi8vL1VzZXJzL2phc29uL0xpYnJhcnkvQXBwbGljYXRpb24gU3VwcG9ydC9GaXJlZm94L1Byb2ZpbGVzL2tzZ2Q3d3BrLkxvY2FsU3luY1NlcnZlci93ZWF2ZS9sb2dzLyIsInZpc2l0cyI6W3siZGF0ZSI6MTMxOTE0OTAxMjM3MjQyNSwidHlwZSI6MX1dfQ==" + + override func setUp() { + super.setUp() + // Put setup code here. This method is called before the invocation of each test method in the class. + } + + override func tearDown() { + // Put teardown code here. This method is called after the invocation of each test method in the class. + super.tearDown() + } + + func testHMAC() { + let keyBundle = KeyBundle(encKey: encKey, hmacKey: hmacKey) + // HMAC is computed against the Base64 ciphertext. + let ciphertextRaw: Data = dataFromBase64(b64: ciphertextB64) + XCTAssertNotNil(ciphertextRaw) + XCTAssertEqual(hmacB16, keyBundle.hmacString(ciphertextRaw)) + } + + func dataFromBase64(b64: String) -> Data { + return Bytes.dataFromBase64(b64)! + } + + func testDecrypt() { + let keyBundle = KeyBundle(encKey: encKey, hmacKey: hmacKey) + // Decryption is done against raw bytes. + let ciphertext = Bytes.decodeBase64(ciphertextB64)! + let iv = Bytes.decodeBase64(ivB64)! + let s = keyBundle.decrypt(ciphertext, iv: iv) + let cleartext = NSString(data: Bytes.decodeBase64(cleartextB64)!, + encoding: String.Encoding.utf8.rawValue) + XCTAssertTrue(cleartext!.isEqual(to: s!)) + } + + func testBadBase64() { + XCTAssertNil(Bytes.decodeBase64(invalidB64)) + } + + func testEncrypt() { + let keyBundle = KeyBundle(encKey: encKey, hmacKey: hmacKey) + let cleartext = Bytes.decodeBase64(cleartextB64)! + + // With specified IV. + let iv = Bytes.decodeBase64(ivB64)! + if let (b, ivOut) = keyBundle.encrypt(cleartext, iv: iv) { + // The output IV should be the input. + XCTAssertEqual(ivOut, iv) + XCTAssertEqual(b, Bytes.decodeBase64(ciphertextB64)!) + } else { + XCTFail("Encrypt failed.") + } + + // With a random IV. + if let (b, ivOut) = keyBundle.encrypt(cleartext) { + // The output IV should be different. + // TODO: check that it's not empty! + XCTAssertNotEqual(ivOut, iv) + + // The result will not match the ciphertext for which a different IV was used. + XCTAssertNotEqual(b, Bytes.decodeBase64(ciphertextB64)!) + } else { + XCTFail("Encrypt failed.") + } + } +} diff --git a/mobile/ios/SyncTests/DownloadTests.swift b/mobile/ios/SyncTests/DownloadTests.swift new file mode 100644 index 0000000000..ce63abf116 --- /dev/null +++ b/mobile/ios/SyncTests/DownloadTests.swift @@ -0,0 +1,96 @@ +/* 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 +@testable import Sync + +import XCTest +import SwiftyJSON + +func identity(x: T) -> T { + return x +} + +class DownloadTests: XCTestCase { + func loadEmptyBookmarksIntoServer(server: MockSyncServer) { + server.storeRecords(records: [], inCollection: "bookmarks") + } + + func testBasicDownload() { + let server = getServer(preStart: loadEmptyBookmarksIntoServer) + server.storeRecords(records: [], inCollection: "bookmarks") + + let storageClient = getClient(server: server) + let bookmarksClient = storageClient.clientForCollection("bookmarks", encrypter: getEncrypter()) + + let expectation = self.expectation(description: "Waiting for result.") + let deferred = bookmarksClient.getSince(0) + deferred >>== { response in + XCTAssertEqual(response.metadata.status, 200) + expectation.fulfill() + } + waitForExpectations(timeout: 10, handler: nil) + } + + func testDownloadBatches() { + let guid1: GUID = "abcdefghijkl" + let ts1: Timestamp = 1326254123650 + let rec1 = MockSyncServer.makeValidEnvelope(guid: guid1, modified: ts1) + + let guid2: GUID = "bbcdefghijkl" + let ts2: Timestamp = 1326254125650 + let rec2 = MockSyncServer.makeValidEnvelope(guid: guid2, modified: ts2) + + let server = getServer(preStart: loadEmptyBookmarksIntoServer) + + server.storeRecords(records: [rec1], inCollection: "clients", now: ts1) + + let storageClient = getClient(server: server) + let bookmarksClient = storageClient.clientForCollection("clients", encrypter: getEncrypter()) + let prefs = MockProfilePrefs() + + let batcher = BatchingDownloader(collectionClient: bookmarksClient, basePrefs: prefs, collection: "clients") + + let ic1 = InfoCollections(collections: ["clients": ts1]) + let fetch1 = batcher.go(ic1, limit: 1).value + XCTAssertEqual(fetch1.successValue, DownloadEndState.complete) + XCTAssertEqual(0, batcher.baseTimestamp) // This isn't updated until after success. + let records1 = batcher.retrieve() + XCTAssertEqual(1, records1.count) + XCTAssertEqual(guid1, records1[0].id) + batcher.advance() + XCTAssertNotEqual(0, batcher.baseTimestamp) + + // Fetching again yields nothing, because the collection hasn't + // changed. + XCTAssertEqual(batcher.go(ic1, limit: 1).value.successValue, DownloadEndState.noNewData) + + // More records. Start again. + let _ = batcher.reset().value + + let ic2 = InfoCollections(collections: ["clients": ts2]) + server.storeRecords(records: [rec2], inCollection: "clients", now: ts2) + + let fetch2 = batcher.go(ic2, limit: 1).value + XCTAssertEqual(fetch2.successValue, DownloadEndState.incomplete) + let records2 = batcher.retrieve() + XCTAssertEqual(1, records2.count) + XCTAssertEqual(guid1, records2[0].id) + batcher.advance() + + let fetch3 = batcher.go(ic2, limit: 1).value + XCTAssertEqual(fetch3.successValue, DownloadEndState.complete) + let records3 = batcher.retrieve() + XCTAssertEqual(1, records3.count) + XCTAssertEqual(guid2, records3[0].id) + batcher.advance() + + let fetch4 = batcher.go(ic2, limit: 1).value + XCTAssertEqual(fetch4.successValue, DownloadEndState.noNewData) + let records4 = batcher.retrieve() + XCTAssertEqual(0, records4.count) + batcher.advance() + } +} diff --git a/mobile/ios/SyncTests/HistorySynchronizerTests.swift b/mobile/ios/SyncTests/HistorySynchronizerTests.swift new file mode 100644 index 0000000000..7fe55402e8 --- /dev/null +++ b/mobile/ios/SyncTests/HistorySynchronizerTests.swift @@ -0,0 +1,269 @@ +/* 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 Shared +import Storage +@testable import Sync +import XCGLogger +import Deferred + +import XCTest +import SwiftyJSON + +private let log = Logger.syncLogger + +class MockSyncDelegate: SyncDelegate { + func displaySentTab(for url: URL, title: String, from deviceName: String?) { + } +} + +class DBPlace: Place { + var isDeleted = false + var shouldUpload = false + var serverModified: Timestamp? + var localModified: Timestamp? +} + +class MockSyncableHistory { + var wasReset: Bool = false + var places = [GUID: DBPlace]() + var remoteVisits = [GUID: Set]() + var localVisits = [GUID: Set]() + + init() { + } + + fileprivate func placeForURL(url: String) -> DBPlace? { + return findOneValue(places) { $0.url == url } + } +} + +extension MockSyncableHistory: ResettableSyncStorage { + func resetClient() -> Success { + self.wasReset = true + return succeed() + } +} + +extension MockSyncableHistory: SyncableHistory { + // TODO: consider comparing the timestamp to local visits, perhaps opting to + // not delete the local place (and instead to give it a new GUID) if the visits + // are newer than the deletion. + // Obviously this'll behave badly during reconciling on other devices: + // they might apply our new record first, renaming their local copy of + // the old record with that URL, and thus bring all the old visits back to life. + // Desktop just finds by GUID then deletes by URL. + func deleteByGUID(_ guid: GUID, deletedAt: Timestamp) -> Deferred> { + self.remoteVisits.removeValue(forKey: guid) + self.localVisits.removeValue(forKey: guid) + self.places.removeValue(forKey: guid) + + return succeed() + } + + func hasSyncedHistory() -> Deferred> { + let has = self.places.values.contains(where: { $0.serverModified != nil }) + return deferMaybe(has) + } + + /** + * This assumes that the provided GUID doesn't already map to a different URL! + */ + func ensurePlaceWithURL(_ url: String, hasGUID guid: GUID) -> Success { + // Find by URL. + if let existing = self.placeForURL(url: url) { + let p = DBPlace(guid: guid, url: url, title: existing.title) + p.isDeleted = existing.isDeleted + p.serverModified = existing.serverModified + p.localModified = existing.localModified + self.places.removeValue(forKey: existing.guid) + self.places[guid] = p + } + + return succeed() + } + + func storeRemoteVisits(_ visits: [Visit], forGUID guid: GUID) -> Success { + // Strip out existing local visits. + // We trust that an identical timestamp and type implies an identical visit. + var remote = Set(visits) + if let local = self.localVisits[guid] { + remote.subtract(local) + } + + // Visits are only ever added. + if var r = self.remoteVisits[guid] { + r.formUnion(remote) + } else { + self.remoteVisits[guid] = remote + } + return succeed() + } + + func insertOrUpdatePlace(_ place: Place, modified: Timestamp) -> Deferred> { + // See if we've already applied this one. + if let existingModified = self.places[place.guid]?.serverModified { + if existingModified == modified { + log.debug("Already seen unchanged record \(place.guid).") + return deferMaybe(place.guid) + } + } + + // Make sure that we collide with any matching URLs -- whether locally + // modified or not. Then overwrite the upstream and merge any local changes. + return self.ensurePlaceWithURL(place.url, hasGUID: place.guid) + >>> { + if let existingLocal = self.places[place.guid] { + if existingLocal.shouldUpload { + log.debug("Record \(existingLocal.guid) modified locally and remotely.") + log.debug("Local modified: \(existingLocal.localModified ??? "nil"); remote: \(modified).") + + // Should always be a value if marked as changed. + if let localModified = existingLocal.localModified, localModified > modified { + // Nothing to do: it's marked as changed. + log.debug("Discarding remote non-visit changes!") + self.places[place.guid]?.serverModified = modified + return deferMaybe(place.guid) + } else { + log.debug("Discarding local non-visit changes!") + self.places[place.guid]?.shouldUpload = false + } + } else { + log.debug("Remote record exists, but has no local changes.") + } + } else { + log.debug("Remote record doesn't exist locally.") + } + + // Apply the new remote record. + let p = DBPlace(guid: place.guid, url: place.url, title: place.title) + p.localModified = Date.now() + p.serverModified = modified + p.isDeleted = false + self.places[place.guid] = p + return deferMaybe(place.guid) + } + } + + func getModifiedHistoryToUpload() -> Deferred> { + // TODO. + return deferMaybe([]) + } + + func getDeletedHistoryToUpload() -> Deferred> { + // TODO. + return deferMaybe([]) + } + + func markAsSynchronized(_: [GUID], modified: Timestamp) -> Deferred> { + // TODO + return deferMaybe(0) + } + + func markAsDeleted(_: [GUID]) -> Success { + // TODO + return succeed() + } + + func onRemovedAccount() -> Success { + // TODO + return succeed() + } + + func doneApplyingRecordsAfterDownload() -> Success { + return succeed() + } + + func doneUpdatingMetadataAfterUpload() -> Success { + return succeed() + } +} + +class HistorySynchronizerTests: XCTestCase { + private func applyRecords(records: [Record], toStorage storage: SyncableHistory & ResettableSyncStorage) -> (synchronizer: HistorySynchronizer, prefs: Prefs, scratchpad: Scratchpad) { + let delegate = MockSyncDelegate() + + // We can use these useless values because we're directly injecting decrypted + // payloads; no need for real keys etc. + let prefs = MockProfilePrefs() + let scratchpad = Scratchpad(b: KeyBundle.random(), persistingTo: prefs) + + let synchronizer = HistorySynchronizer(scratchpad: scratchpad, delegate: delegate, basePrefs: prefs, why: .scheduled) + + let expectation = self.expectation(description: "Waiting for application.") + var succeeded = false + synchronizer.applyIncomingToStorage(storage, records: records) + .upon({ result in + succeeded = result.isSuccess + expectation.fulfill() + }) + + waitForExpectations(timeout: 10, handler: nil) + XCTAssertTrue(succeeded, "Application succeeded.") + return (synchronizer, prefs, scratchpad) + } + + func testRecordSerialization() { + let id = "abcdefghi" + let modified: Timestamp = 0 // Ignored in upload serialization. + let sortindex = 1 + let ttl = 12345 + let json: JSON = JSON([ + "id": id, + "visits": [], + "histUri": "http://www.slideshare.net/swadpasc/bpm-edu-netseminarscepwithreactionrulemlprova", + "title": "Semantic Complex Event Processing with \(Character(UnicodeScalar(11)))Reaction RuleML 1.0 and Prova", + ]) + let payload = HistoryPayload(json) + let record = Record(id: id, payload: payload, modified: modified, sortindex: sortindex, ttl: ttl) + let k = KeyBundle.random() + let s = k.serializer({ (x: HistoryPayload) -> JSON in x.json }) + let converter = { (x: JSON) -> HistoryPayload in HistoryPayload(x) } + let f = k.factory(converter) + let serialized = s(record)! + let envelope = EnvelopeJSON(serialized) + + // With a badly serialized payload, we get null JSON! + let p = f(envelope.payload) + XCTAssertFalse(p!.json.isNull()) + + // When we round-trip, the payload should be valid, and we'll get a record here. + let roundtripped = Record.fromEnvelope(envelope, payloadFactory: f) + XCTAssertNotNil(roundtripped) + } + + func testApplyRecords() { + let earliest = Date.now() + + let empty = MockSyncableHistory() + let noRecords = [Record]() + + // Apply no records. + let _ = self.applyRecords(records: noRecords, toStorage: empty) + + // Hey look! Nothing changed. + XCTAssertTrue(empty.places.isEmpty) + XCTAssertTrue(empty.remoteVisits.isEmpty) + XCTAssertTrue(empty.localVisits.isEmpty) + + // Apply one remote record. + let jA = "{\"id\":\"aaaaaa\",\"histUri\":\"http://foo.com/\",\"title\": \"ñ\",\"visits\":[{\"date\":1222222222222222,\"type\":1}]}" + let pA = HistoryPayload.fromJSON(JSON(parseJSON: jA))! + let rA = Record(id: "aaaaaa", payload: pA, modified: earliest + 10000, sortindex: 123, ttl: 1000000) + + let (_, prefs, _) = self.applyRecords(records: [rA], toStorage: empty) + + // The record was stored. This is checking our mock implementation, but real storage should work, too! + + XCTAssertEqual(1, empty.places.count) + XCTAssertEqual(1, empty.remoteVisits.count) + XCTAssertEqual(1, empty.remoteVisits["aaaaaa"]!.count) + XCTAssertTrue(empty.localVisits.isEmpty) + + // Test resetting now that we have a timestamp. + XCTAssertFalse(empty.wasReset) + XCTAssertTrue(HistorySynchronizer.resetSynchronizerWithStorage(empty, basePrefs: prefs, collection: "history").value.isSuccess) + XCTAssertTrue(empty.wasReset) + } +} diff --git a/mobile/ios/SyncTests/Info.plist b/mobile/ios/SyncTests/Info.plist new file mode 100644 index 0000000000..9411e4a06e --- /dev/null +++ b/mobile/ios/SyncTests/Info.plist @@ -0,0 +1,24 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + BNDL + CFBundleShortVersionString + 10.6 + CFBundleSignature + ???? + CFBundleVersion + 1 + + diff --git a/mobile/ios/SyncTests/InfoTests.swift b/mobile/ios/SyncTests/InfoTests.swift new file mode 100644 index 0000000000..a8a390ceac --- /dev/null +++ b/mobile/ios/SyncTests/InfoTests.swift @@ -0,0 +1,56 @@ +/* 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 +@testable import Sync + +import XCTest +import SwiftyJSON + +class InfoTests: XCTestCase { + func testSame() { + let empty = JSON(parseJSON: "{}") + + let oneA = JSON(parseJSON: "{\"foo\": 1234.0, \"bar\": 456.12}") + let oneB = JSON(parseJSON: "{\"bar\": 456.12, \"foo\": 1234.0}") + + let twoA = JSON(parseJSON: "{\"bar\": 456.12}") + let twoB = JSON(parseJSON: "{\"foo\": 1234.0}") + + let iEmpty = InfoCollections.fromJSON(empty)! + let iOneA = InfoCollections.fromJSON(oneA)! + let iOneB = InfoCollections.fromJSON(oneB)! + let iTwoA = InfoCollections.fromJSON(twoA)! + let iTwoB = InfoCollections.fromJSON(twoB)! + + XCTAssertTrue(iEmpty.same(iEmpty, collections: nil)) + XCTAssertTrue(iEmpty.same(iEmpty, collections: [])) + XCTAssertTrue(iEmpty.same(iEmpty, collections: ["anything"])) + + XCTAssertTrue(iEmpty.same(iOneA, collections: [])) + XCTAssertTrue(iEmpty.same(iOneA, collections: ["anything"])) + XCTAssertTrue(iOneA.same(iEmpty, collections: [])) + XCTAssertTrue(iOneA.same(iEmpty, collections: ["anything"])) + + XCTAssertFalse(iEmpty.same(iOneA, collections: ["foo"])) + XCTAssertFalse(iOneA.same(iEmpty, collections: ["foo"])) + XCTAssertFalse(iEmpty.same(iOneA, collections: nil)) + XCTAssertFalse(iOneA.same(iEmpty, collections: nil)) + + XCTAssertTrue(iOneA.same(iOneA, collections: nil)) + XCTAssertTrue(iOneA.same(iOneA, collections: ["foo", "bar", "baz"])) + XCTAssertTrue(iOneA.same(iOneB, collections: ["foo", "bar", "baz"])) + XCTAssertTrue(iOneB.same(iOneA, collections: ["foo", "bar", "baz"])) + + XCTAssertFalse(iTwoA.same(iOneA, collections: nil)) + XCTAssertTrue(iTwoA.same(iOneA, collections: ["bar", "baz"])) + XCTAssertTrue(iOneA.same(iTwoA, collections: ["bar", "baz"])) + XCTAssertTrue(iTwoB.same(iOneA, collections: ["foo", "baz"])) + + XCTAssertFalse(iTwoA.same(iTwoB, collections: nil)) + XCTAssertFalse(iTwoA.same(iTwoB, collections: ["foo"])) + XCTAssertFalse(iTwoA.same(iTwoB, collections: ["bar"])) + } +} diff --git a/mobile/ios/SyncTests/LiveStorageClientTests.swift b/mobile/ios/SyncTests/LiveStorageClientTests.swift new file mode 100644 index 0000000000..c4b16b6701 --- /dev/null +++ b/mobile/ios/SyncTests/LiveStorageClientTests.swift @@ -0,0 +1,131 @@ +/* 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 Account +import Foundation +import FxA +import Shared +import Deferred +@testable import Sync + +import XCTest +import SwiftyJSON + +private class KeyFetchError: MaybeErrorType { + var description: String { + return "key fetch error" + } +} + +class LiveStorageClientTests: LiveAccountTest { + func getKeys(kB: Data, token: TokenServerToken) -> Deferred>> { + let endpoint = token.api_endpoint + XCTAssertTrue(endpoint.range(of: "services.mozilla.com") != nil, "We got a Sync server.") + + let cryptoURI = URL(string: endpoint) + let authorizer: Authorizer = { + (r: URLRequest) -> URLRequest in + var request = r + let helper = HawkHelper(id: token.id, key: token.key.data(using: String.Encoding.utf8, allowLossyConversion: false)!) + request.addValue(helper.getAuthorizationValueFor(r), forHTTPHeaderField: "Authorization") + return request + } + + let keyBundle: KeyBundle = KeyBundle.fromKB(kB as Data) + let encoder = RecordEncoder(decode: { KeysPayload($0) }, encode: { $0.json }) + let encrypter = Keys(defaultBundle: keyBundle).encrypter("crypto", encoder: encoder) + + let workQueue = DispatchQueue.global(qos: DispatchQoS.default.qosClass) + let resultQueue = DispatchQueue.main + let backoff = MockBackoffStorage() + + let storageClient = Sync15StorageClient(serverURI: cryptoURI!, authorizer: authorizer, workQueue: workQueue, resultQueue: resultQueue, backoff: backoff) + let keysFetcher = storageClient.clientForCollection("crypto", encrypter: encrypter) + + return keysFetcher.get("keys").map({ res in + // Unwrap the response. + if let r = res.successValue { + return Maybe(success: r.value) + } + return Maybe(failure: KeyFetchError()) + }) + } + + func getState(user: String, password: String) -> Deferred> { + let err: NSError = NSError(domain: FxAClientErrorDomain, code: 0, userInfo: nil) + return Deferred(value: Maybe(failure: FxAClientError.local(err))) + } + + func getTokenAndDefaultKeys() -> Deferred> { + let authState = self.syncAuthState(Date.now()) + + let keysPayload: Deferred>> = authState.bind { tokenResult in + if let (token, forKey) = tokenResult.successValue { + return self.getKeys(kB: forKey, token: token) + } + XCTAssertEqual(tokenResult.failureValue!.description, "") + return Deferred(value: Maybe(failure: KeyFetchError())) + } + + let result = Deferred>() + keysPayload.upon { res in + if let rec = res.successValue { + XCTAssert(rec.id == "keys", "GUID is correct.") + XCTAssert(rec.modified > 1000, "modified is sane.") + let payload: KeysPayload = rec.payload as KeysPayload + print("Body: \(payload.json.stringValue() ?? "nil")", terminator: "\n") + XCTAssert(rec.id == "keys", "GUID inside is correct.") + if let keys = payload.defaultKeys { + // Extracting the token like this is not great, but... + result.fill(Maybe(success: (authState.value.successValue!.token, keys))) + return + } + } + + result.fill(Maybe(failure: KeyFetchError())) + } + return result + } + + func testLive() { + let expctn = expectation(description: "Waiting on value.") + let deferred = getTokenAndDefaultKeys() + deferred.upon { res in + if let (_, _) = res.successValue { + print("Yay", terminator: "\n") + } else { + XCTAssertEqual(res.failureValue!.description, "") + } + expctn.fulfill() + } + + // client: mgWl22CIzHiE + waitForExpectations(timeout: 20) { (error) in + XCTAssertNil(error, "Error: \(error ??? "nil")") + } + } + + func testStateMachine() { + let expctn = expectation(description: "Waiting on value.") + let authState = self.getAuthState(Date.now()) + + let d = chainDeferred(authState, f: { SyncStateMachine(prefs: MockProfilePrefs()).toReady($0) }) + + d.upon { result in + if let ready = result.successValue { + XCTAssertTrue(ready.collectionKeys.defaultBundle.encKey.count == 32) + XCTAssertTrue(ready.scratchpad.global != nil) + if let clients = ready.scratchpad.global?.value.engines["clients"] { + XCTAssertTrue(clients.syncID.characters.count == 12) + } + } + XCTAssertTrue(result.isSuccess) + expctn.fulfill() + } + + waitForExpectations(timeout: 20) { (error) in + XCTAssertNil(error, "Error: \(error ??? "nil")") + } + } +} diff --git a/mobile/ios/SyncTests/MetaGlobalTests.swift b/mobile/ios/SyncTests/MetaGlobalTests.swift new file mode 100644 index 0000000000..774ff4c34d --- /dev/null +++ b/mobile/ios/SyncTests/MetaGlobalTests.swift @@ -0,0 +1,624 @@ +/* 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/. */ + +@testable import Account +import Foundation +import Shared +import Storage +@testable import Sync +import XCGLogger +import Deferred + +import XCTest +import SwiftyJSON + +private let log = Logger.syncLogger + +class MockSyncAuthState: SyncAuthState { + + let serverRoot: String + let kB: Data + + var deviceID: String? { + return "mock_device_id" + } + + init(serverRoot: String, kB: Data) { + self.serverRoot = serverRoot + self.kB = kB + } + + func invalidate() { + } + + func token(_ now: Timestamp, canBeExpired: Bool) -> Deferred> { + let token = TokenServerToken(id: "id", key: "key", api_endpoint: serverRoot, uid: UInt64(0), hashedFxAUID: "", + durationInSeconds: UInt64(5 * 60), remoteTimestamp: Timestamp(now - 1)) + return deferMaybe((token, self.kB)) + } +} + +class MetaGlobalTests: XCTestCase { + var server: MockSyncServer! + var serverRoot: String! + var kB: Data! + var syncPrefs: Prefs! + var authState: SyncAuthState! + var stateMachine: SyncStateMachine! + + override func setUp() { + kB = Data.randomOfLength(32)! + server = MockSyncServer(username: "1234567") + server.start() + serverRoot = server.baseURL + syncPrefs = MockProfilePrefs() + authState = MockSyncAuthState(serverRoot: serverRoot, kB: kB) + stateMachine = SyncStateMachine(prefs: syncPrefs) + } + + func storeMetaGlobal(metaGlobal: MetaGlobal) { + let envelope = EnvelopeJSON(JSON(object: [ + "id": "global", + "collection": "meta", + "payload": metaGlobal.asPayload().json.stringValue()!, + "modified": Double(Date.now())/1000])) + server.storeRecords(records: [envelope], inCollection: "meta") + } + + func storeCryptoKeys(keys: Keys) { + let keyBundle = KeyBundle.fromKB(kB) + let record = Record(id: "keys", payload: keys.asPayload()) + let envelope = EnvelopeJSON(keyBundle.serializer({ $0.json })(record)!) + server.storeRecords(records: [envelope], inCollection: "crypto") + } + + func assertFreshStart(ready: Ready?, after: Timestamp) { + XCTAssertNotNil(ready) + guard let ready = ready else { + return + } + // We should have wiped. + // We should have uploaded new meta/global and crypto/keys. + XCTAssertGreaterThan(server.collections["meta"]?.records["global"]?.modified ?? 0, after) + XCTAssertGreaterThan(server.collections["meta"]?.modified ?? 0, after) + XCTAssertGreaterThan(server.collections["crypto"]?.records["keys"]?.modified ?? 0, after) + XCTAssertGreaterThan(server.collections["crypto"]?.modified ?? 0, after) + + // And we should have downloaded meta/global and crypto/keys. + XCTAssertNotNil(ready.scratchpad.global) + XCTAssertNotNil(ready.scratchpad.keys) + + // We should have the default engine configuration. + XCTAssertNotNil(ready.scratchpad.engineConfiguration) + guard let engineConfiguration = ready.scratchpad.engineConfiguration else { + return + } + XCTAssertEqual(engineConfiguration.enabled.sorted(), ["addons", "bookmarks", "clients", "forms", "history", "passwords", "prefs", "tabs"]) + XCTAssertEqual(engineConfiguration.declined, []) + + // Basic verifications. + XCTAssertEqual(ready.collectionKeys.defaultBundle.encKey.count, 32) + if let clients = ready.scratchpad.global?.value.engines["clients"] { + XCTAssertTrue(clients.syncID.characters.count == 12) + } + } + + func testMetaGlobalVersionTooNew() { + // There's no recovery from a meta/global version "in the future": just bail out with an UpgradeRequiredError. + storeMetaGlobal(metaGlobal: MetaGlobal(syncID: "id", storageVersion: 6, engines: [String: EngineMeta](), declined: [])) + + let expectation = self.expectation(description: "Waiting on value.") + stateMachine.toReady(authState).upon { result in + XCTAssertEqual(self.stateMachine.stateLabelSequence.map { $0.rawValue }, ["initialWithLiveToken", "initialWithLiveTokenAndInfo", "needsFreshMetaGlobal", "resolveMetaGlobalVersion", "clientUpgradeRequired"]) + XCTAssertNotNil(result.failureValue as? ClientUpgradeRequiredError) + XCTAssertNil(result.successValue) + expectation.fulfill() + } + + waitForExpectations(timeout: 2000) { (error) in + XCTAssertNil(error, "Error: \(error ??? "nil")") + } + } + + func testMetaGlobalVersionTooOld() { + // To recover from a meta/global version "in the past", fresh start. + storeMetaGlobal(metaGlobal: MetaGlobal(syncID: "id", storageVersion: 4, engines: [String: EngineMeta](), declined: [])) + + let afterStores = Date.now() + let expectation = self.expectation(description: "Waiting on value.") + stateMachine.toReady(authState).upon { result in + XCTAssertEqual(self.stateMachine.stateLabelSequence.map { $0.rawValue }, ["initialWithLiveToken", "initialWithLiveTokenAndInfo", "needsFreshMetaGlobal", "resolveMetaGlobalVersion", "remoteUpgradeRequired", + "freshStartRequired", "serverConfigurationRequired", "initialWithLiveToken", "initialWithLiveTokenAndInfo", "needsFreshMetaGlobal", "resolveMetaGlobalVersion", "resolveMetaGlobalContent", "hasMetaGlobal", "needsFreshCryptoKeys", "hasFreshCryptoKeys", "ready"]) + self.assertFreshStart(ready: result.successValue, after: afterStores) + XCTAssertTrue(result.isSuccess) + XCTAssertNil(result.failureValue) + expectation.fulfill() + } + + waitForExpectations(timeout: 2000) { (error) in + XCTAssertNil(error, "Error: \(error ??? "nil")") + } + } + + func testMetaGlobalMissing() { + // To recover from a missing meta/global, fresh start. + let afterStores = Date.now() + let expectation = self.expectation(description: "Waiting on value.") + stateMachine.toReady(authState).upon { result in + XCTAssertEqual(self.stateMachine.stateLabelSequence.map { $0.rawValue }, ["initialWithLiveToken", "initialWithLiveTokenAndInfo", "needsFreshMetaGlobal", "missingMetaGlobal", + "freshStartRequired", "serverConfigurationRequired", "initialWithLiveToken", "initialWithLiveTokenAndInfo", "needsFreshMetaGlobal", "resolveMetaGlobalVersion", "resolveMetaGlobalContent", "hasMetaGlobal", "needsFreshCryptoKeys", "hasFreshCryptoKeys", "ready"]) + self.assertFreshStart(ready: result.successValue, after: afterStores) + XCTAssertTrue(result.isSuccess) + XCTAssertNil(result.failureValue) + expectation.fulfill() + } + + waitForExpectations(timeout: 2000) { (error) in + XCTAssertNil(error, "Error: \(error ??? "nil")") + } + } + + func testCryptoKeysMissing() { + // To recover from a missing crypto/keys, fresh start. + storeMetaGlobal(metaGlobal: createMetaGlobal()) + + let afterStores = Date.now() + let expectation = self.expectation(description: "Waiting on value.") + stateMachine.toReady(authState).upon { result in + XCTAssertEqual(self.stateMachine.stateLabelSequence.map { $0.rawValue }, ["initialWithLiveToken", "initialWithLiveTokenAndInfo", "needsFreshMetaGlobal", "resolveMetaGlobalVersion", "resolveMetaGlobalContent", "hasMetaGlobal", "needsFreshCryptoKeys", "missingCryptoKeys", "freshStartRequired", "serverConfigurationRequired", "initialWithLiveToken", "initialWithLiveTokenAndInfo", "needsFreshMetaGlobal", "resolveMetaGlobalVersion", "resolveMetaGlobalContent", "hasMetaGlobal", "needsFreshCryptoKeys", "hasFreshCryptoKeys", "ready"]) + self.assertFreshStart(ready: result.successValue, after: afterStores) + XCTAssertTrue(result.isSuccess) + XCTAssertNil(result.failureValue) + expectation.fulfill() + } + + waitForExpectations(timeout: 2000) { (error) in + XCTAssertNil(error, "Error: \(error ??? "nil")") + } + } + + func testMetaGlobalAndCryptoKeysFresh() { + // When encountering a valid meta/global and crypto/keys, advance smoothly. + let metaGlobal = MetaGlobal(syncID: "id", storageVersion: 5, engines: [String: EngineMeta](), declined: []) + let cryptoKeys = Keys.random() + storeMetaGlobal(metaGlobal: metaGlobal) + storeCryptoKeys(keys: cryptoKeys) + + let expectation = self.expectation(description: "Waiting on value.") + stateMachine.toReady(authState).upon { result in + XCTAssertEqual(self.stateMachine.stateLabelSequence.map { $0.rawValue }, ["initialWithLiveToken", "initialWithLiveTokenAndInfo", "needsFreshMetaGlobal", "resolveMetaGlobalVersion", "resolveMetaGlobalContent", "hasMetaGlobal", "needsFreshCryptoKeys", "hasFreshCryptoKeys", "ready"]) + XCTAssertNotNil(result.successValue) + guard let ready = result.successValue else { + return + } + + // And we should have downloaded meta/global and crypto/keys. + XCTAssertEqual(ready.scratchpad.global?.value, metaGlobal) + XCTAssertEqual(ready.scratchpad.keys?.value, cryptoKeys) + + // We should have marked all local engines for reset. + XCTAssertEqual(ready.collectionsThatNeedLocalReset(), ["bookmarks", "clients", "history", "passwords", "tabs"]) + ready.clearLocalCommands() + + XCTAssertTrue(result.isSuccess) + XCTAssertNil(result.failureValue) + expectation.fulfill() + } + + waitForExpectations(timeout: 2000) { (error) in + XCTAssertNil(error, "Error: \(error ??? "nil")") + } + + let afterFirstSync = Date.now() + + // Now, run through the state machine again. Nothing's changed remotely, so we should advance quickly. + let secondExpectation = self.expectation(description: "Waiting on value.") + stateMachine.toReady(authState).upon { result in + XCTAssertEqual(self.stateMachine.stateLabelSequence.map { $0.rawValue }, ["initialWithLiveToken", "initialWithLiveTokenAndInfo", "hasMetaGlobal", "hasFreshCryptoKeys", "ready"]) + XCTAssertNotNil(result.successValue) + guard let ready = result.successValue else { + return + } + // And we should have not downloaded a fresh meta/global or crypto/keys. + XCTAssertLessThan(ready.scratchpad.global?.timestamp ?? Timestamp.max, afterFirstSync) + XCTAssertLessThan(ready.scratchpad.keys?.timestamp ?? Timestamp.max, afterFirstSync) + + // We should not have marked any local engines for reset. + XCTAssertEqual(ready.collectionsThatNeedLocalReset(), []) + + XCTAssertTrue(result.isSuccess) + XCTAssertNil(result.failureValue) + secondExpectation.fulfill() + } + + waitForExpectations(timeout: 2000) { (error) in + XCTAssertNil(error, "Error: \(error ??? "nil")") + } + } + + func testFailingOptimisticStateMachine() { + // We test only the optimistic state machine, knowing it will need to go through + // needsFreshMetaGlobal, and fail. + let metaGlobal = MetaGlobal(syncID: "id", storageVersion: 5, engines: [String: EngineMeta](), declined: []) + let cryptoKeys = Keys.random() + storeMetaGlobal(metaGlobal: metaGlobal) + storeCryptoKeys(keys: cryptoKeys) + + stateMachine = SyncStateMachine(prefs: syncPrefs, allowingStates: SyncStateMachine.OptimisticStates) + + let expectation = self.expectation(description: "Waiting on value.") + stateMachine.toReady(authState).upon { result in + XCTAssertEqual(self.stateMachine.stateLabelSequence.map { $0.rawValue }, ["initialWithLiveToken", "initialWithLiveTokenAndInfo", "needsFreshMetaGlobal"]) + XCTAssertNotNil(result.failureValue) + if let failure = result.failureValue as? DisallowedStateError { + XCTAssertEqual(failure.state, SyncStateLabel.NeedsFreshMetaGlobal) + } else { + XCTFail("SyncStatus failed, but with a different error") + } + expectation.fulfill() + } + + waitForExpectations(timeout: 2000) { (error) in + XCTAssertNil(error, "Error: \(error ??? "nil")") + } + } + + func testHappyOptimisticStateMachine() { + // We should be able to quickly progress through a constrained (a.k.a. optimistic) state machine + let metaGlobal = MetaGlobal(syncID: "id", storageVersion: 5, engines: [String: EngineMeta](), declined: []) + let cryptoKeys = Keys.random() + storeMetaGlobal(metaGlobal: metaGlobal) + storeCryptoKeys(keys: cryptoKeys) + + let expectation = self.expectation(description: "Waiting on value.") + stateMachine.toReady(authState).upon { result in + XCTAssertEqual(self.stateMachine.stateLabelSequence.map { $0.rawValue }, ["initialWithLiveToken", "initialWithLiveTokenAndInfo", "needsFreshMetaGlobal", "resolveMetaGlobalVersion", "resolveMetaGlobalContent", "hasMetaGlobal", "needsFreshCryptoKeys", "hasFreshCryptoKeys", "ready"]) + XCTAssertNotNil(result.successValue) + expectation.fulfill() + } + + waitForExpectations(timeout: 2000) { (error) in + XCTAssertNil(error, "Error: \(error ??? "nil")") + } + + // Now, run through the state machine again. Nothing's changed remotely, so we should advance quickly. + // We should be able to use this 'optimistic' path in an extension. + stateMachine = SyncStateMachine(prefs: syncPrefs, allowingStates: SyncStateMachine.OptimisticStates) + + let secondExpectation = self.expectation(description: "Waiting on value.") + + stateMachine.toReady(authState).upon { result in + XCTAssertEqual(self.stateMachine.stateLabelSequence.map { $0.rawValue }, ["initialWithLiveToken", "initialWithLiveTokenAndInfo", "hasMetaGlobal", "hasFreshCryptoKeys", "ready"]) + XCTAssertNotNil(result.successValue) + secondExpectation.fulfill() + } + + waitForExpectations(timeout: 2000) { (error) in + XCTAssertNil(error, "Error: \(error ??? "nil")") + } + } + + func testUpdatedCryptoKeys() { + // When encountering a valid meta/global and crypto/keys, advance smoothly. + let metaGlobal = MetaGlobal(syncID: "id", storageVersion: 5, engines: [String: EngineMeta](), declined: []) + let cryptoKeys = Keys.random() + cryptoKeys.collectionKeys.updateValue(KeyBundle.random(), forKey: "bookmarks") + cryptoKeys.collectionKeys.updateValue(KeyBundle.random(), forKey: "clients") + storeMetaGlobal(metaGlobal: metaGlobal) + storeCryptoKeys(keys: cryptoKeys) + + let expectation = self.expectation(description: "Waiting on value.") + stateMachine.toReady(authState).upon { result in + XCTAssertEqual(self.stateMachine.stateLabelSequence.map { $0.rawValue }, ["initialWithLiveToken", "initialWithLiveTokenAndInfo", "needsFreshMetaGlobal", "resolveMetaGlobalVersion", "resolveMetaGlobalContent", "hasMetaGlobal", "needsFreshCryptoKeys", "hasFreshCryptoKeys", "ready"]) + XCTAssertNotNil(result.successValue) + guard let ready = result.successValue else { + return + } + + // And we should have downloaded meta/global and crypto/keys. + XCTAssertEqual(ready.scratchpad.global?.value, metaGlobal) + XCTAssertEqual(ready.scratchpad.keys?.value, cryptoKeys) + + // We should have marked all local engines for reset. + XCTAssertEqual(ready.collectionsThatNeedLocalReset(), ["bookmarks", "clients", "history", "passwords", "tabs"]) + ready.clearLocalCommands() + + XCTAssertTrue(result.isSuccess) + XCTAssertNil(result.failureValue) + expectation.fulfill() + } + + waitForExpectations(timeout: 2000) { (error) in + XCTAssertNil(error, "Error: \(error ??? "nil")") + } + + let afterFirstSync = Date.now() + + // Store a fresh crypto/keys, with the same default key, one identical collection key, and one changed collection key. + let freshCryptoKeys = Keys(defaultBundle: cryptoKeys.defaultBundle) + freshCryptoKeys.collectionKeys.updateValue(cryptoKeys.forCollection("bookmarks"), forKey: "bookmarks") + freshCryptoKeys.collectionKeys.updateValue(KeyBundle.random(), forKey: "clients") + storeCryptoKeys(keys: freshCryptoKeys) + + // Now, run through the state machine again. + let secondExpectation = self.expectation(description: "Waiting on value.") + stateMachine.toReady(authState).upon { result in + XCTAssertEqual(self.stateMachine.stateLabelSequence.map { $0.rawValue }, ["initialWithLiveToken", "initialWithLiveTokenAndInfo", "hasMetaGlobal", "needsFreshCryptoKeys", "hasFreshCryptoKeys", "ready"]) + XCTAssertNotNil(result.successValue) + guard let ready = result.successValue else { + return + } + // And we should have not downloaded a fresh meta/global ... + XCTAssertLessThan(ready.scratchpad.global?.timestamp ?? Timestamp.max, afterFirstSync) + // ... but we should have downloaded a fresh crypto/keys. + XCTAssertGreaterThanOrEqual(ready.scratchpad.keys?.timestamp ?? Timestamp.min, afterFirstSync) + + // We should have marked only the local engine with a changed key for reset. + XCTAssertEqual(ready.collectionsThatNeedLocalReset(), ["clients"]) + + XCTAssertTrue(result.isSuccess) + XCTAssertNil(result.failureValue) + secondExpectation.fulfill() + } + + waitForExpectations(timeout: 2000) { (error) in + XCTAssertNil(error, "Error: \(error ??? "nil")") + } + + let afterSecondSync = Date.now() + + // Store a fresh crypto/keys, with a changed default key and one identical collection key, and one changed collection key. + let freshCryptoKeys2 = Keys.random() + freshCryptoKeys2.collectionKeys.updateValue(freshCryptoKeys.forCollection("bookmarks"), forKey: "bookmarks") + freshCryptoKeys2.collectionKeys.updateValue(KeyBundle.random(), forKey: "clients") + storeCryptoKeys(keys: freshCryptoKeys2) + + // Now, run through the state machine again. + let thirdExpectation = self.expectation(description: "Waiting on value.") + stateMachine.toReady(authState).upon { result in + XCTAssertEqual(self.stateMachine.stateLabelSequence.map { $0.rawValue }, ["initialWithLiveToken", "initialWithLiveTokenAndInfo", "hasMetaGlobal", "needsFreshCryptoKeys", "hasFreshCryptoKeys", "ready"]) + XCTAssertNotNil(result.successValue) + guard let ready = result.successValue else { + return + } + // And we should have not downloaded a fresh meta/global ... + XCTAssertLessThan(ready.scratchpad.global?.timestamp ?? Timestamp.max, afterSecondSync) + // ... but we should have downloaded a fresh crypto/keys. + XCTAssertGreaterThanOrEqual(ready.scratchpad.keys?.timestamp ?? Timestamp.min, afterSecondSync) + + // We should have marked all local engines as needing reset, except for the engine whose key remained constant. + XCTAssertEqual(ready.collectionsThatNeedLocalReset(), ["clients", "history", "passwords", "tabs"]) + + XCTAssertTrue(result.isSuccess) + XCTAssertNil(result.failureValue) + thirdExpectation.fulfill() + } + + waitForExpectations(timeout: 2000) { (error) in + XCTAssertNil(error, "Error: \(error ??? "nil")") + } + + let afterThirdSync = Date.now() + + // Now store a random crypto/keys, with a different default key (and no bulk keys). + let randomCryptoKeys = Keys.random() + storeCryptoKeys(keys: randomCryptoKeys) + + // Now, run through the state machine again. + let fourthExpectation = self.expectation(description: "Waiting on value.") + stateMachine.toReady(authState).upon { result in + XCTAssertEqual(self.stateMachine.stateLabelSequence.map { $0.rawValue }, ["initialWithLiveToken", "initialWithLiveTokenAndInfo", "hasMetaGlobal", "needsFreshCryptoKeys", "hasFreshCryptoKeys", "ready"]) + XCTAssertNotNil(result.successValue) + guard let ready = result.successValue else { + return + } + // And we should have not downloaded a fresh meta/global ... + XCTAssertLessThan(ready.scratchpad.global?.timestamp ?? Timestamp.max, afterThirdSync) + // ... but we should have downloaded a fresh crypto/keys. + XCTAssertGreaterThanOrEqual(ready.scratchpad.keys?.timestamp ?? Timestamp.min, afterThirdSync) + + // We should have marked all local engines for reset. + XCTAssertEqual(ready.collectionsThatNeedLocalReset(), ["bookmarks", "clients", "history", "passwords", "tabs"]) + + XCTAssertTrue(result.isSuccess) + XCTAssertNil(result.failureValue) + fourthExpectation.fulfill() + } + + waitForExpectations(timeout: 2000) { (error) in + XCTAssertNil(error, "Error: \(error ??? "nil")") + } + } + + private func createUnusualMetaGlobal() -> MetaGlobal { + let metaGlobal = MetaGlobal(syncID: "id", storageVersion: 5, + engines: ["bookmarks": EngineMeta(version: 1, syncID: "bookmarks"), "unknownEngine1": EngineMeta(version: 2, syncID: "engineId1")], + declined: ["clients", "forms", "unknownEngine2"]) + return metaGlobal + } + + func testEngineConfigurations() { + // When encountering a valid meta/global and crypto/keys, advance smoothly. Keep the engine configuration for re-upload. + let metaGlobal = createUnusualMetaGlobal() + let cryptoKeys = Keys.random() + storeMetaGlobal(metaGlobal: metaGlobal) + storeCryptoKeys(keys: cryptoKeys) + + let expectation = self.expectation(description: "Waiting on value.") + stateMachine.toReady(authState).upon { result in + XCTAssertEqual(self.stateMachine.stateLabelSequence.map { $0.rawValue }, ["initialWithLiveToken", "initialWithLiveTokenAndInfo", "needsFreshMetaGlobal", "resolveMetaGlobalVersion", "resolveMetaGlobalContent", "hasMetaGlobal", "needsFreshCryptoKeys", "hasFreshCryptoKeys", "ready"]) + XCTAssertNotNil(result.successValue) + guard let ready = result.successValue else { + return + } + + // We should have saved the engine configuration. + XCTAssertNotNil(ready.scratchpad.engineConfiguration) + guard let engineConfiguration = ready.scratchpad.engineConfiguration else { + return + } + XCTAssertEqual(engineConfiguration, metaGlobal.engineConfiguration()) + + XCTAssertTrue(result.isSuccess) + XCTAssertNil(result.failureValue) + expectation.fulfill() + } + + waitForExpectations(timeout: 2000) { (error) in + XCTAssertNil(error, "Error: \(error ??? "nil")") + } + + // Wipe meta/global. + server.removeAllItemsFromCollection(collection: "meta", atTime: Date.now()) + + // Now, run through the state machine again. We should produce and upload a meta/global reflecting our engine configuration. + let secondExpectation = self.expectation(description: "Waiting on value.") + stateMachine.toReady(authState).upon { result in + XCTAssertEqual(self.stateMachine.stateLabelSequence.map { $0.rawValue }, ["initialWithLiveToken", "initialWithLiveTokenAndInfo", "needsFreshMetaGlobal", "missingMetaGlobal", "freshStartRequired", "serverConfigurationRequired", "initialWithLiveToken", "initialWithLiveTokenAndInfo", "needsFreshMetaGlobal", "resolveMetaGlobalVersion", "resolveMetaGlobalContent", "hasMetaGlobal", "needsFreshCryptoKeys", "hasFreshCryptoKeys", "ready"]) + XCTAssertNotNil(result.successValue) + guard let ready = result.successValue else { + return + } + + // The downloaded meta/global should reflect our local engine configuration. + XCTAssertNotNil(ready.scratchpad.global) + guard let global = ready.scratchpad.global?.value else { + return + } + XCTAssertEqual(global.engineConfiguration(), metaGlobal.engineConfiguration()) + + // We should have the same cached engine configuration. + XCTAssertNotNil(ready.scratchpad.engineConfiguration) + guard let engineConfiguration = ready.scratchpad.engineConfiguration else { + return + } + XCTAssertEqual(engineConfiguration, metaGlobal.engineConfiguration()) + + XCTAssertTrue(result.isSuccess) + XCTAssertNil(result.failureValue) + secondExpectation.fulfill() + } + + waitForExpectations(timeout: 2000) { (error) in + XCTAssertNil(error, "Error: \(error ??? "nil")") + } + } + + func testMetaGlobalModified() { + // When encountering a valid meta/global and crypto/keys, advance smoothly. + let metaGlobal = createUnusualMetaGlobal() + let cryptoKeys = Keys.random() + storeMetaGlobal(metaGlobal: metaGlobal) + storeCryptoKeys(keys: cryptoKeys) + + let expectation = self.expectation(description: "Waiting on value.") + stateMachine.toReady(authState).upon { result in + XCTAssertEqual(self.stateMachine.stateLabelSequence.map { $0.rawValue }, ["initialWithLiveToken", "initialWithLiveTokenAndInfo", "needsFreshMetaGlobal", "resolveMetaGlobalVersion", "resolveMetaGlobalContent", "hasMetaGlobal", "needsFreshCryptoKeys", "hasFreshCryptoKeys", "ready"]) + XCTAssertNotNil(result.successValue) + guard let ready = result.successValue else { + return + } + + // And we should have downloaded meta/global and crypto/keys. + XCTAssertEqual(ready.scratchpad.global?.value, metaGlobal) + XCTAssertEqual(ready.scratchpad.keys?.value, cryptoKeys) + + // We should have marked all local engines for reset. + XCTAssertEqual(ready.collectionsThatNeedLocalReset(), ["bookmarks", "clients", "history", "passwords", "tabs"]) + XCTAssertEqual(ready.enginesEnabled(), []) + XCTAssertEqual(ready.enginesDisabled(), []) + + XCTAssertTrue(result.isSuccess) + XCTAssertNil(result.failureValue) + expectation.fulfill() + } + + waitForExpectations(timeout: 2000) { (error) in + XCTAssertNil(error, "Error: \(error ??? "nil")") + } + + let afterFirstSync = Date.now() + + // Store a meta/global with a new global syncID. + let newMetaGlobal = metaGlobal.withSyncID("newID") + storeMetaGlobal(metaGlobal: newMetaGlobal) + + // Now, run through the state machine again. + let secondExpectation = self.expectation(description: "Waiting on value.") + stateMachine.toReady(authState).upon { result in + XCTAssertEqual(self.stateMachine.stateLabelSequence.map { $0.rawValue }, ["initialWithLiveToken", "initialWithLiveTokenAndInfo", "needsFreshMetaGlobal", "resolveMetaGlobalVersion", "resolveMetaGlobalContent", "hasMetaGlobal", "needsFreshCryptoKeys", "hasFreshCryptoKeys", "ready"]) + XCTAssertNotNil(result.successValue) + guard let ready = result.successValue else { + return + } + // And we should have downloaded a fresh meta/global ... + XCTAssertGreaterThanOrEqual(ready.scratchpad.global?.timestamp ?? Timestamp.min, afterFirstSync) + // ... and we should have downloaded a fresh crypto/keys -- but its timestamp is identical to the old one! + // Therefore, the "needsFreshCryptoKeys" stage above is our test that we re-downloaded crypto/keys. + + // We should have marked all local engines for reset. + XCTAssertEqual(ready.collectionsThatNeedLocalReset(), ["bookmarks", "clients", "history", "passwords", "tabs"]) + + // And our engine configuration should be unchanged. + XCTAssertNotNil(ready.scratchpad.global) + guard let global = ready.scratchpad.global?.value else { + return + } + XCTAssertEqual(global.engineConfiguration(), metaGlobal.engineConfiguration()) + + XCTAssertTrue(result.isSuccess) + XCTAssertNil(result.failureValue) + secondExpectation.fulfill() + } + + waitForExpectations(timeout: 2000) { (error) in + XCTAssertNil(error, "Error: \(error ??? "nil")") + } + + // Now store a meta/global with a changed engine syncID, a new engine, and a new declined entry. + var engines = newMetaGlobal.engines + engines.updateValue(EngineMeta(version: 1, syncID: Bytes.generateGUID()), forKey: "bookmarks") + engines.updateValue(EngineMeta(version: 1, syncID: Bytes.generateGUID()), forKey: "forms") + engines.removeValue(forKey: "unknownEngine1") + var declined = newMetaGlobal.declined.filter({ $0 != "forms" }) + declined.append("unknownEngine1") + let secondMetaGlobal = MetaGlobal(syncID: newMetaGlobal.syncID, storageVersion: 5, engines: engines, declined: declined) + storeMetaGlobal(metaGlobal: secondMetaGlobal) + syncPrefs.removeObjectForKey("scratchpad.localCommands") + + // Now, run through the state machine again. + let thirdExpectation = self.expectation(description: "Waiting on value.") + stateMachine.toReady(authState).upon { result in + XCTAssertEqual(self.stateMachine.stateLabelSequence.map { $0.rawValue }, ["initialWithLiveToken", "initialWithLiveTokenAndInfo", "needsFreshMetaGlobal", "resolveMetaGlobalVersion", "resolveMetaGlobalContent", "hasMetaGlobal", "hasFreshCryptoKeys", "ready"]) + XCTAssertNotNil(result.successValue) + guard let ready = result.successValue else { + return + } + // And we should have downloaded a fresh meta/global ... + XCTAssertGreaterThanOrEqual(ready.scratchpad.global?.timestamp ?? Timestamp.min, afterFirstSync) + // ... and we should have downloaded a fresh crypto/keys -- but its timestamp is identical to the old one! + // Therefore, the "needsFreshCryptoKeys" stage above is our test that we re-downloaded crypto/keys. + + // We should have marked the changed engine for local reset, and identified the enabled and disabled engines. + XCTAssertEqual(ready.collectionsThatNeedLocalReset(), ["bookmarks"]) + XCTAssertEqual(ready.enginesEnabled(), ["forms"]) + XCTAssertEqual(ready.enginesDisabled(), ["unknownEngine1"]) + + // And our engine configuration should reflect the new meta/global on the server. + XCTAssertNotNil(ready.scratchpad.global) + guard let global = ready.scratchpad.global?.value else { + return + } + XCTAssertEqual(global.engineConfiguration(), secondMetaGlobal.engineConfiguration()) + + XCTAssertTrue(result.isSuccess) + XCTAssertNil(result.failureValue) + thirdExpectation.fulfill() + } + + waitForExpectations(timeout: 2000) { (error) in + XCTAssertNil(error, "Error: \(error ??? "nil")") + } + } +} diff --git a/mobile/ios/SyncTests/MockSyncServer.swift b/mobile/ios/SyncTests/MockSyncServer.swift new file mode 100644 index 0000000000..86ceaa29b5 --- /dev/null +++ b/mobile/ios/SyncTests/MockSyncServer.swift @@ -0,0 +1,453 @@ +/* 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 GCDWebServers +import SwiftyJSON +@testable import Sync + +import XCTest + +private let log = Logger.syncLogger + +private func optTimestamp(x: AnyObject?) -> Timestamp? { + guard let str = x as? String else { + return nil + } + return decimalSecondsStringToTimestamp(str) +} + +private func optStringArray(x: AnyObject?) -> [String]? { + guard let str = x as? String else { + return nil + } + return str.components(separatedBy: ",").map { $0.trimmingCharacters(in:NSCharacterSet.whitespacesAndNewlines) } +} + +private struct SyncRequestSpec { + let collection: String + let id: String? + let ids: [String]? + let limit: Int? + let offset: String? + let sort: SortOption? + let newer: Timestamp? + let full: Bool + + static func fromRequest(request: GCDWebServerRequest) -> SyncRequestSpec? { + // Input is "/1.5/user/storage/collection", possibly with "/id" at the end. + // That means we get five or six path components here, the first being empty. + + let parts = request.path!.components(separatedBy: "/").filter { !$0.isEmpty } + let id: String? + let query = request.query as! [String: AnyObject] + let ids = optStringArray(x: query["ids"]) + let newer = optTimestamp(x: query["newer"]) + let full: Bool = query["full"] != nil + + let limit: Int? + if let lim = query["limit"] as? String { + limit = Int(lim) + } else { + limit = nil + } + + let offset = query["offset"] as? String + + let sort: SortOption? + switch query["sort"] as? String ?? "" { + case "oldest": + sort = SortOption.OldestFirst + case "newest": + sort = SortOption.NewestFirst + case "index": + sort = SortOption.Index + default: + sort = nil + } + + if parts.count < 4 { + return nil + } + + if parts[2] != "storage" { + return nil + } + + // Use dropFirst, you say! It's buggy. + switch parts.count { + case 4: + id = nil + case 5: + id = parts[4] + default: + // Uh oh. + return nil + } + return SyncRequestSpec(collection: parts[3], id: id, ids: ids, limit: limit, offset: offset, sort: sort, newer: newer, full: full) + } +} + +struct SyncDeleteRequestSpec { + let collection: String? + let id: GUID? + let ids: [GUID]? + let wholeCollection: Bool + + static func fromRequest(request: GCDWebServerRequest) -> SyncDeleteRequestSpec? { + // Input is "/1.5/user{/storage{/collection{/id}}}". + // That means we get four, five, or six path components here, the first being empty. + return SyncDeleteRequestSpec.fromPath(path: request.path!, withQuery: request.query as! [NSString : AnyObject]) + } + + static func fromPath(path: String, withQuery query: [NSString: AnyObject]) -> SyncDeleteRequestSpec? { + let parts = path.components(separatedBy: "/").filter { !$0.isEmpty } + let queryIDs: [GUID]? = (query["ids"] as? String)?.components(separatedBy: ",") + + guard [2, 4, 5].contains(parts.count) else { + return nil + } + + if parts.count == 2 { + return SyncDeleteRequestSpec(collection: nil, id: nil, ids: queryIDs, wholeCollection: true) + } + + if parts[2] != "storage" { + return nil + } + + if parts.count == 4 { + let hasIDs = queryIDs != nil + return SyncDeleteRequestSpec(collection: parts[3], id: nil, ids: queryIDs, wholeCollection: !hasIDs) + } + + return SyncDeleteRequestSpec(collection: parts[3], id: parts[4], ids: queryIDs, wholeCollection: false) + } +} + +private struct SyncPutRequestSpec { + let collection: String + let id: String + + static func fromRequest(request: GCDWebServerRequest) -> SyncPutRequestSpec? { + // Input is "/1.5/user/storage/collection/id}}}". + // That means we get six path components here, the first being empty. + + let parts = request.path!.components(separatedBy: "/").filter { !$0.isEmpty } + + guard parts.count == 5 else { + return nil + } + + if parts[2] != "storage" { + return nil + } + + return SyncPutRequestSpec(collection: parts[3], id: parts[4]) + } +} + +class MockSyncServer { + let server = GCDWebServer() + let username: String + + var offsets: Int = 0 + var continuations: [String: [EnvelopeJSON]] = [:] + var collections: [String: (modified: Timestamp, records: [String: EnvelopeJSON])] = [:] + var baseURL: String! + + init(username: String) { + self.username = username + } + + class func makeValidEnvelope(guid: GUID, modified: Timestamp) -> EnvelopeJSON { + let clientBody: [String: Any] = [ + "id": guid, + "name": "Foobar", + "commands": [], + "type": "mobile", + ] + let clientBodyString = JSON(object: clientBody).stringValue()! + let clientRecord: [String : Any] = [ + "id": guid, + "collection": "clients", + "payload": clientBodyString, + "modified": Double(modified) / 1000, + ] + return EnvelopeJSON(JSON(object: clientRecord).stringValue()!) + } + + class func withHeaders(response: GCDWebServerResponse, lastModified: Timestamp? = nil, records: Int? = nil, timestamp: Timestamp? = nil) -> GCDWebServerResponse { + let timestamp = timestamp ?? Date.now() + let xWeaveTimestamp = millisecondsToDecimalSeconds(timestamp) + response.setValue("\(xWeaveTimestamp)", forAdditionalHeader: "X-Weave-Timestamp") + + if let lastModified = lastModified { + let xLastModified = millisecondsToDecimalSeconds(lastModified) + response.setValue("\(xLastModified)", forAdditionalHeader: "X-Last-Modified") + } + + if let records = records { + response.setValue("\(records)", forAdditionalHeader: "X-Weave-Records") + } + + return response + } + + func storeRecords(records: [EnvelopeJSON], inCollection collection: String, now: Timestamp? = nil) { + let now = now ?? Date.now() + let coll = self.collections[collection] + var out = coll?.records ?? [:] + records.forEach { + out[$0.id] = $0.withModified(now) + } + let newModified = max(now, coll?.modified ?? 0) + self.collections[collection] = (modified: newModified, records: out) + } + + private func splitArray(items: [T], at: Int) -> ([T], [T]) { + return (Array(items.dropLast(items.count - at)), Array(items.dropFirst(at))) + } + + private func recordsMatchingSpec(spec: SyncRequestSpec) -> (records: [EnvelopeJSON], offsetID: String?)? { + // If we have a provided offset, handle that directly. + if let offset = spec.offset { + log.debug("Got provided offset \(offset).") + guard let remainder = self.continuations[offset] else { + log.error("Unknown offset.") + return nil + } + + // Remove the old one. + self.continuations.removeValue(forKey: offset) + + // Handle the smaller-than-limit or no-provided-limit cases. + guard let limit = spec.limit, limit < remainder.count else { + log.debug("Returning all remaining items.") + return (remainder, nil) + } + + // Record the next continuation and return the first slice of records. + let next = "\(self.offsets)" + self.offsets += 1 + let (returned, remaining) = splitArray(items: remainder, at: limit) + self.continuations[next] = remaining + log.debug("Returning \(limit) items; next continuation is \(next).") + return (returned, next) + } + + guard let records = self.collections[spec.collection]?.records.values else { + // No matching records. + return ([], nil) + } + + var items = Array(records) + log.debug("Got \(items.count) candidate records.") + + if spec.newer ?? 0 > 0 { + items = items.filter { $0.modified > spec.newer! } + } + + if let ids = spec.ids { + let ids = Set(ids) + items = items.filter { ids.contains($0.id) } + } + + if let sort = spec.sort { + switch sort { + case SortOption.NewestFirst: + items = items.sorted { $0.modified > $1.modified } + log.debug("Sorted items newest first: \(items.map { $0.modified })") + case SortOption.OldestFirst: + items = items.sorted { $0.modified < $1.modified } + log.debug("Sorted items oldest first: \(items.map { $0.modified })") + case SortOption.Index: + log.warning("Index sorting not yet supported.") + } + } + + if let limit = spec.limit, items.count > limit { + let next = "\(self.offsets)" + self.offsets += 1 + let (returned, remaining) = splitArray(items: items, at: limit) + self.continuations[next] = remaining + return (returned, next) + } + + return (items, nil) + } + + private func recordResponse(record: EnvelopeJSON) -> GCDWebServerResponse { + let body = record.asJSON().stringValue()! + let bodyData = body.utf8EncodedData + let response = GCDWebServerDataResponse(data: bodyData, contentType: "application/json") + return MockSyncServer.withHeaders(response: response!, lastModified: record.modified) + } + + private func modifiedResponse(timestamp: Timestamp) -> GCDWebServerResponse { + let body = JSON(object: ["modified": timestamp]).stringValue() + let bodyData = body?.utf8EncodedData + let response = GCDWebServerDataResponse(data: bodyData, contentType: "application/json")! + return MockSyncServer.withHeaders(response: response) + } + + func modifiedTimeForCollection(collection: String) -> Timestamp? { + return self.collections[collection]?.modified + } + + func removeAllItemsFromCollection(collection: String, atTime: Timestamp) { + if self.collections[collection] != nil { + self.collections[collection] = (atTime, [:]) + } + } + + func start() { + let basePath = "/1.5/\(self.username)" + let storagePath = "\(basePath)/storage/" + + let infoCollectionsPath = "\(basePath)/info/collections" + server?.addHandler(forMethod: "GET", path: infoCollectionsPath, request: GCDWebServerRequest.self) { (request) -> GCDWebServerResponse! in + var ic = [String: Any]() + var lastModified: Timestamp = 0 + for collection in self.collections.keys { + if let timestamp = self.modifiedTimeForCollection(collection: collection) { + ic[collection] = Double(timestamp) / 1000 + lastModified = max(lastModified, timestamp) + } + + } + let body = JSON(object: ic).stringValue() + let bodyData = body?.utf8EncodedData + + let response = GCDWebServerDataResponse(data: bodyData, contentType: "application/json")! + return MockSyncServer.withHeaders(response: response, lastModified: lastModified, records: ic.count) + } + + let matchPut: GCDWebServerMatchBlock = { method, url, headers, path, query -> GCDWebServerRequest! in + guard method == "PUT", + path?.startsWith(basePath) ?? false else { + return nil + } + return GCDWebServerDataRequest(method: method, url: url, headers: headers, path: path, query: query) + } + + server?.addHandler(match: matchPut) { (request) -> GCDWebServerResponse! in + guard let request = request as? GCDWebServerDataRequest else { + return MockSyncServer.withHeaders(response: GCDWebServerDataResponse(statusCode: 400)) + } + + guard let spec = SyncPutRequestSpec.fromRequest(request: request) else { + return MockSyncServer.withHeaders(response: GCDWebServerDataResponse(statusCode: 400)) + } + var body = JSON(object: request.jsonObject) + body["modified"] = JSON(stringLiteral: millisecondsToDecimalSeconds(Date.now())) + let record = EnvelopeJSON(body) + + self.storeRecords(records: [record], inCollection: spec.collection) + let timestamp = self.modifiedTimeForCollection(collection: spec.collection)! + + let response = GCDWebServerDataResponse(data: millisecondsToDecimalSeconds(timestamp).utf8EncodedData, contentType: "application/json") + return MockSyncServer.withHeaders(response: response!) + } + + let matchDelete: GCDWebServerMatchBlock = { method, url, headers, path, query -> GCDWebServerRequest! in + guard method == "DELETE" && (path?.startsWith(basePath))! else { + return nil + } + return GCDWebServerRequest(method: method, url: url, headers: headers, path: path, query: query) + } + + server?.addHandler(match: matchDelete) { (request) -> GCDWebServerResponse! in + guard let spec = SyncDeleteRequestSpec.fromRequest(request: request!) else { + return GCDWebServerDataResponse(statusCode: 400) + } + + if let collection = spec.collection, let id = spec.id { + guard var items = self.collections[collection]?.records else { + // Unable to find the requested collection. + return MockSyncServer.withHeaders(response: GCDWebServerDataResponse(statusCode: 404)) + } + + guard let item = items[id] else { + // Unable to find the requested id. + return MockSyncServer.withHeaders(response: GCDWebServerDataResponse(statusCode: 404)) + } + items.removeValue(forKey: id) + return self.modifiedResponse(timestamp: item.modified) + } + + if let collection = spec.collection { + if spec.wholeCollection { + self.collections.removeValue(forKey: collection) + } else { + if let ids = spec.ids, + var map = self.collections[collection]?.records { + for id in ids { + map.removeValue(forKey: id) + } + self.collections[collection] = (Date.now(), records: map) + } + } + return self.modifiedResponse(timestamp: Date.now()) + } + + self.collections = [:] + return MockSyncServer.withHeaders(response: GCDWebServerDataResponse(data: "{}".utf8EncodedData, contentType: "application/json")) + } + + let match: GCDWebServerMatchBlock = { method, url, headers, path, query -> GCDWebServerRequest! in + guard method == "GET", path?.startsWith(storagePath) ?? false else { + return nil + } + return GCDWebServerRequest(method: method, url: url, headers: headers, path: path, query: query) + } + + server?.addHandler(match: match) { (request) -> GCDWebServerResponse! in + // 1. Decide what the URL is asking for. It might be a collection fetch or + // an individual record, and it might have query parameters. + + guard let spec = SyncRequestSpec.fromRequest(request: request!) else { + return MockSyncServer.withHeaders(response: GCDWebServerDataResponse(statusCode: 400)) + } + + // 2. Grab the matching set of records. Prune based on TTL, exclude with X-I-U-S, etc. + if let id = spec.id { + guard let collection = self.collections[spec.collection], let record = collection.records[id] else { + // Unable to find the requested collection/id. + return MockSyncServer.withHeaders(response: GCDWebServerDataResponse(statusCode: 404)) + } + + return self.recordResponse(record: record) + } + + guard let (items, offset) = self.recordsMatchingSpec(spec: spec) else { + // Unable to find the provided offset. + return MockSyncServer.withHeaders(response: GCDWebServerDataResponse(statusCode: 400)) + } + + // TODO: TTL + // TODO: X-I-U-S + + let body = JSON(object: items.map { $0.asJSON() }).stringValue() + let bodyData = body?.utf8EncodedData + let response = GCDWebServerDataResponse(data: bodyData, contentType: "application/json") + + // 3. Compute the correct set of headers: timestamps, X-Weave-Records, etc. + if let offset = offset { + response?.setValue(offset, forAdditionalHeader: "X-Weave-Next-Offset") + } + + let timestamp = self.modifiedTimeForCollection(collection: spec.collection)! + log.debug("Returning GET response with X-Last-Modified for \(items.count) records: \(timestamp).") + return MockSyncServer.withHeaders(response: response!, lastModified: timestamp, records: items.count) + } + + if server?.start(withPort: 0, bonjourName: nil) == false { + XCTFail("Can't start the GCDWebServer.") + } + + baseURL = "http://localhost:\(server!.port)\(basePath)" + } +} diff --git a/mobile/ios/SyncTests/MockSyncServerTests.swift b/mobile/ios/SyncTests/MockSyncServerTests.swift new file mode 100644 index 0000000000..83aca94ada --- /dev/null +++ b/mobile/ios/SyncTests/MockSyncServerTests.swift @@ -0,0 +1,194 @@ +/* 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 Shared +import Storage +@testable import Sync +import UIKit + +import XCTest + +class MockSyncServerTests: XCTestCase { + var server: MockSyncServer! + var client: Sync15StorageClient! + + override func setUp() { + server = MockSyncServer(username: "1234567") + server.start() + client = getClient(server: server) + } + + private func getClient(server: MockSyncServer) -> Sync15StorageClient? { + guard let url = server.baseURL.asURL else { + XCTFail("Couldn't get URL.") + return nil + } + + let authorizer: Authorizer = identity + let queue = DispatchQueue.global(qos: DispatchQoS.background.qosClass) + return Sync15StorageClient(serverURI: url, authorizer: authorizer, workQueue: queue, resultQueue: queue, backoff: MockBackoffStorage()) + } + + func testDeleteSpec() { + // Deletion of a collection path itself, versus trailing slash, sets the right flags. + let all = SyncDeleteRequestSpec.fromPath(path: "/1.5/123456/storage/bookmarks", withQuery: [:])! + XCTAssertTrue(all.wholeCollection) + XCTAssertNil(all.ids) + + let some = SyncDeleteRequestSpec.fromPath(path: "/1.5/123456/storage/bookmarks", withQuery: ["ids": "123456,abcdef" as AnyObject])! + XCTAssertFalse(some.wholeCollection) + XCTAssertEqual(["123456", "abcdef"], some.ids!) + + let one = SyncDeleteRequestSpec.fromPath(path: "/1.5/123456/storage/bookmarks/123456", withQuery: [:])! + XCTAssertFalse(one.wholeCollection) + XCTAssertNil(one.ids) + } + + func testInfoCollections() { + server.storeRecords(records: [MockSyncServer.makeValidEnvelope(guid: Bytes.generateGUID(), modified: 0)], inCollection: "bookmarks", now: 1326251111000) + server.storeRecords(records: [], inCollection: "tabs", now: 1326252222500) + server.storeRecords(records: [MockSyncServer.makeValidEnvelope(guid: Bytes.generateGUID(), modified: 0)], inCollection: "bookmarks", now: 1326252222000) + server.storeRecords(records: [MockSyncServer.makeValidEnvelope(guid: Bytes.generateGUID(), modified: 0)], inCollection: "clients", now: 1326253333000) + + let expectation = self.expectation(description: "Waiting for result.") + let before = decimalSecondsStringToTimestamp(millisecondsToDecimalSeconds(Date.now()))! + client.getInfoCollections().upon { result in + XCTAssertNotNil(result.successValue) + guard let response = result.successValue else { + expectation.fulfill() + return + } + let after = decimalSecondsStringToTimestamp(millisecondsToDecimalSeconds(Date.now()))! + + // JSON contents. + XCTAssertEqual(response.value.collectionNames().sorted(), ["bookmarks", "clients", "tabs"]) + XCTAssertEqual(response.value.modified("bookmarks"), 1326252222000) + XCTAssertEqual(response.value.modified("clients"), 1326253333000) + + // X-Weave-Timestamp. + XCTAssertLessThanOrEqual(before, response.metadata.timestampMilliseconds) + XCTAssertLessThanOrEqual(response.metadata.timestampMilliseconds, after) + // X-Weave-Records. + XCTAssertEqual(response.metadata.records, 3) // bookmarks, clients, tabs. + + // X-Last-Modified, max of all collection modified timestamps. + XCTAssertEqual(response.metadata.lastModifiedMilliseconds, 1326253333000) + + expectation.fulfill() + } + waitForExpectations(timeout: 10, handler: nil) + } + + func testGet() { + server.storeRecords(records: [MockSyncServer.makeValidEnvelope(guid: "guid", modified: 0)], inCollection: "bookmarks", now: 1326251111000) + let collectionClient = client.clientForCollection("bookmarks", encrypter: getEncrypter()) + + let expectation = self.expectation(description: "Waiting for result.") + let before = decimalSecondsStringToTimestamp(millisecondsToDecimalSeconds(Date.now()))! + collectionClient.get("guid").upon { result in + XCTAssertNotNil(result.successValue) + guard let response = result.successValue else { + expectation.fulfill() + return + } + let after = decimalSecondsStringToTimestamp(millisecondsToDecimalSeconds(Date.now()))! + + // JSON contents. + XCTAssertEqual(response.value.id, "guid") + XCTAssertEqual(response.value.modified, 1326251111000) + + // X-Weave-Timestamp. + XCTAssertLessThanOrEqual(before, response.metadata.timestampMilliseconds) + XCTAssertLessThanOrEqual(response.metadata.timestampMilliseconds, after) + // X-Weave-Records. + XCTAssertNil(response.metadata.records) + // X-Last-Modified. + XCTAssertEqual(response.metadata.lastModifiedMilliseconds, 1326251111000) + + expectation.fulfill() + } + waitForExpectations(timeout: 10, handler: nil) + + // And now a missing record, which should produce a 404. + collectionClient.get("missing").upon { result in + XCTAssertNotNil(result.failureValue) + guard let response = result.failureValue else { + expectation.fulfill() + return + } + XCTAssertNotNil(response as? NotFound) + } + } + + func testWipeStorage() { + server.storeRecords(records: [MockSyncServer.makeValidEnvelope(guid: "a", modified: 0)], inCollection: "bookmarks", now: 1326251111000) + server.storeRecords(records: [MockSyncServer.makeValidEnvelope(guid: "b", modified: 0)], inCollection: "bookmarks", now: 1326252222000) + server.storeRecords(records: [MockSyncServer.makeValidEnvelope(guid: "c", modified: 0)], inCollection: "clients", now: 1326253333000) + server.storeRecords(records: [], inCollection: "tabs") + + // For now, only testing wiping the storage root, which is the only thing we use in practice. + let expectation = self.expectation(description: "Waiting for result.") + let before = decimalSecondsStringToTimestamp(millisecondsToDecimalSeconds(Date.now()))! + client.wipeStorage().upon { result in + XCTAssertNotNil(result.successValue) + guard let response = result.successValue else { + expectation.fulfill() + return + } + let after = decimalSecondsStringToTimestamp(millisecondsToDecimalSeconds(Date.now()))! + + // JSON contents: should be the empty object. + let jsonData = try! response.value.rawData() + let jsonString = String(data: jsonData, encoding: String.Encoding.utf8)! + XCTAssertEqual(jsonString, "{}") + + // X-Weave-Timestamp. + XCTAssertLessThanOrEqual(before, response.metadata.timestampMilliseconds) + XCTAssertLessThanOrEqual(response.metadata.timestampMilliseconds, after) + // X-Weave-Records. + XCTAssertNil(response.metadata.records) + // X-Last-Modified. + XCTAssertNil(response.metadata.lastModifiedMilliseconds) + + // And we really wiped the data. + XCTAssertTrue(self.server.collections.isEmpty) + + expectation.fulfill() + } + waitForExpectations(timeout: 10, handler: nil) + } + + func testPut() { + // For now, only test uploading crypto/keys. There's nothing special about this PUT, however. + let expectation = self.expectation(description: "Waiting for result.") + let before = decimalSecondsStringToTimestamp(millisecondsToDecimalSeconds(Date.now()))! + client.uploadCryptoKeys(Keys.random(), withSyncKeyBundle: KeyBundle.random(), ifUnmodifiedSince: nil).upon { result in + XCTAssertNotNil(result.successValue) + guard let response = result.successValue else { + expectation.fulfill() + return + } + let after = decimalSecondsStringToTimestamp(millisecondsToDecimalSeconds(Date.now()))! + + // Contents: should be just the record timestamp. + XCTAssertLessThanOrEqual(before, response.value) + XCTAssertLessThanOrEqual(response.value, after) + + // X-Weave-Timestamp. + XCTAssertLessThanOrEqual(before, response.metadata.timestampMilliseconds) + XCTAssertLessThanOrEqual(response.metadata.timestampMilliseconds, after) + // X-Weave-Records. + XCTAssertNil(response.metadata.records) + // X-Last-Modified. + XCTAssertNil(response.metadata.lastModifiedMilliseconds) + + // And we really uploaded the record. + XCTAssertNotNil(self.server.collections["crypto"]) + XCTAssertNotNil(self.server.collections["crypto"]?.records["keys"]) + + expectation.fulfill() + } + waitForExpectations(timeout: 10, handler: nil) + } +} diff --git a/mobile/ios/SyncTests/RecordTests.swift b/mobile/ios/SyncTests/RecordTests.swift new file mode 100644 index 0000000000..edeb6b5c15 --- /dev/null +++ b/mobile/ios/SyncTests/RecordTests.swift @@ -0,0 +1,572 @@ +/* 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 Shared +import Storage +@testable import Sync +import UIKit + +import XCTest +import SwiftyJSON + +class RecordTests: XCTestCase { + func testGUIDs() { + let s = Bytes.generateGUID() + print("Got GUID: \(s)", terminator: "\n") + XCTAssertEqual(12, s.lengthOfBytes(using: String.Encoding.utf8)) + } + + func testSwiftyJSONSerializingControlChars() { + let input = "{\"foo\":\"help \\u000b this\"}" + let json = JSON(parseJSON: input) + XCTAssertNil(json.error) + XCTAssertNil(json.null) + XCTAssertEqual(input, json.stringValue()) + + let pairs: [String: Any] = ["foo": "help \(Character(UnicodeScalar(11))) this"] + let built = JSON(object: pairs) + XCTAssertEqual(input, built.stringValue()) + } + + func testEnvelopeNullTTL() { + let p = CleartextPayloadJSON(JSON(object: ["id": "guid"])) + let r = Record(id: "guid", payload: p, modified: Date.now(), sortindex: 15, ttl: nil) + let k = KeyBundle.random() + let s = k.serializer({ $0.json }) + let json = s(r)! + XCTAssertEqual(json["id"].stringValue, "guid") + XCTAssertTrue(json["ttl"].isNull()) + } + + func testParsedNulls() { + // Make this a thorough test: use a real-ish blob of JSON. + // Look to see whether fields with explicit null values match isNull(). + let fullRecord = "{\"id\":\"global\"," + + "\"payload\":" + + "\"{\\\"syncID\\\":\\\"zPSQTm7WBVWB\\\"," + + "\\\"declined\\\":[\\\"bookmarks\\\"]," + + "\\\"storageVersion\\\":5," + + "\\\"engines\\\":{" + + "\\\"clients\\\":{\\\"version\\\":1,\\\"syncID\\\": null}," + + "\\\"tabs\\\":null}}\"," + + "\"username\":\"5817483\"," + + "\"modified\":1.32046073744E9}" + + let record = EnvelopeJSON(fullRecord) + let bodyJSON = JSON(parseJSON: record.payload) + + XCTAssertTrue(bodyJSON["engines"]["tabs"].isNull()) + + let clients = bodyJSON["engines"]["clients"] + + // Make sure we're really getting a value out. + XCTAssertEqual(clients["version"].int, 1) + + // An explicit null in the input has .type == null, so our .isNull works. + XCTAssertTrue(clients["syncID"].isNull()) + + // Oh, and it's a valid meta/global. + let global = MetaGlobal.fromJSON(bodyJSON) + XCTAssertTrue(global != nil) + } + + func testEnvelopeJSON() { + let e = EnvelopeJSON(JSON(parseJSON: "{}")) + XCTAssertFalse(e.isValid()) + + let ee = EnvelopeJSON("{\"id\": \"foo\"}") + XCTAssertFalse(ee.isValid()) + XCTAssertEqual(ee.id, "foo") + + let eee = EnvelopeJSON(JSON(parseJSON: "{\"id\": \"foo\", \"collection\": \"bar\", \"payload\": \"baz\"}")) + XCTAssertTrue(eee.isValid()) + XCTAssertEqual(eee.id, "foo") + XCTAssertEqual(eee.collection, "bar") + XCTAssertEqual(eee.payload, "baz") + } + + func testRecord() { + // This is malformed JSON (no closing brace). + let malformedPayload = "{\"id\": \"abcdefghijkl\", \"collection\": \"clients\", \"payload\": \"in" + + // Invalid: the payload isn't stringified JSON. + let invalidPayload = "{\"id\": \"abcdefghijkl\", \"collection\": \"clients\", \"payload\": \"invalid\"}" + + // Invalid: the payload is missing a GUID. + let emptyPayload = "{\"id\": \"abcdefghijkl\", \"collection\": \"clients\", \"payload\": \"{}\"}" + + // This one is invalid because the payload "id" isn't a string. + // (It'll also fail implicitly because the guid doesn't match the envelope.) + let badPayloadGUIDPayload: [String: Any] = ["id": 0] + let badPayloadGUIDPayloadString = JSON(object: badPayloadGUIDPayload).stringValue()! + let badPayloadGUIDRecord: [String: Any] = ["id": "abcdefghijkl", + "collection": "clients", + "payload": badPayloadGUIDPayloadString] + let badPayloadGUIDRecordString = JSON(object: badPayloadGUIDRecord).stringValue()! + + // This one is invalid because the payload doesn't contain an "id" at all, but it's non-empty. + // See also `emptyPayload` above. + // (It'll also fail implicitly because the guid doesn't match the envelope.) + let noPayloadGUIDPayload: [String: Any] = ["some": "thing"] + let noPayloadGUIDPayloadString = JSON(object: noPayloadGUIDPayload).stringValue()! + let noPayloadGUIDRecord: [String: Any] = ["id": "abcdefghijkl", + "collection": "clients", + "payload": noPayloadGUIDPayloadString] + let noPayloadGUIDRecordString = JSON(object: noPayloadGUIDRecord).stringValue()! + + // And this is a valid record. + let clientBody: [String: Any] = ["id": "abcdefghijkl", "name": "Foobar", "commands": [], "type": "mobile"] + let clientBodyString = JSON(object: clientBody).stringValue()! + let clientRecord: [String: Any] = ["id": "abcdefghijkl", "collection": "clients", "payload": clientBodyString] + let clientPayload = JSON(object: clientRecord).stringValue()! + + let cleartextClientsFactory: (String) -> ClientPayload? = { + (s: String) -> ClientPayload? in + return ClientPayload(s) + } + + let clearFactory: (String) -> CleartextPayloadJSON? = { + (s: String) -> CleartextPayloadJSON? in + return CleartextPayloadJSON(s) + } + + print(clientPayload, terminator: "\n") + + // Non-JSON malformed payloads don't even yield a value. + XCTAssertNil(Record.fromEnvelope(EnvelopeJSON(malformedPayload), payloadFactory: clearFactory)) + + // Only payloads that parse as JSON objects are valid. + XCTAssertNil(Record.fromEnvelope(EnvelopeJSON(invalidPayload), payloadFactory: clearFactory)) + + // Missing ID. + XCTAssertNil(Record.fromEnvelope(EnvelopeJSON(emptyPayload), payloadFactory: clearFactory)) + + // No ID in non-empty payload. + let noPayloadGUIDEnvelope = EnvelopeJSON(noPayloadGUIDRecordString) + + // The envelope is valid... + XCTAssertTrue(noPayloadGUIDEnvelope.isValid()) + + // ... but the payload is not. + let noID = Record.fromEnvelope(noPayloadGUIDEnvelope, payloadFactory: cleartextClientsFactory) + XCTAssertNil(noID) + + // Non-string ID in payload. + let badPayloadGUIDEnvelope = EnvelopeJSON(badPayloadGUIDRecordString) + + // The envelope is valid... + XCTAssertTrue(badPayloadGUIDEnvelope.isValid()) + + // ... but the payload is not. + let badID = Record.fromEnvelope(badPayloadGUIDEnvelope, payloadFactory: cleartextClientsFactory) + XCTAssertNil(badID) + + // Only valid ClientPayloads are valid. + XCTAssertNil(Record.fromEnvelope(EnvelopeJSON(invalidPayload), payloadFactory: cleartextClientsFactory)) + XCTAssertTrue(Record.fromEnvelope(EnvelopeJSON(clientPayload), payloadFactory: cleartextClientsFactory)!.payload.isValid()) + } + + func testEncryptedClientRecord() { + let b64E = "0A7mU5SZ/tu7ZqwXW1og4qHVHN+zgEi4Xwfwjw+vEJw=" + let b64H = "11GN34O9QWXkjR06g8t0gWE1sGgQeWL0qxxWwl8Dmxs=" + + let expectedGUID = "0-P9fabp9vJD" + let expectedSortIndex = 131 + let expectedLastModified: Timestamp = 1326254123650 + + let inputString = "{\"sortindex\": 131, \"payload\": \"{\\\"ciphertext\\\":\\\"YJB4dr0vZEIWPirfU2FCJvfzeSLiOP5QWasol2R6ILUxdHsJWuUuvTZVhxYQfTVNou6hVV67jfAvi5Cs+bqhhQsv7icZTiZhPTiTdVGt+uuMotxauVA5OryNGVEZgCCTvT3upzhDFdDbJzVd9O3/gU/b7r/CmAHykX8bTlthlbWeZ8oz6gwHJB5tPRU15nM/m/qW1vyKIw5pw/ZwtAy630AieRehGIGDk+33PWqsfyuT4EUFY9/Ly+8JlnqzxfiBCunIfuXGdLuqTjJOxgrK8mI4wccRFEdFEnmHvh5x7fjl1ID52qumFNQl8zkB75C8XK25alXqwvRR6/AQSP+BgQ==\\\",\\\"IV\\\":\\\"v/0BFgicqYQsd70T39rraA==\\\",\\\"hmac\\\":\\\"59605ed696f6e0e6e062a03510cff742bf6b50d695c042e8372a93f4c2d37dac\\\"}\", \"id\": \"0-P9fabp9vJD\", \"modified\": 1326254123.65}" + + let keyBundle = KeyBundle(encKeyB64: b64E, hmacKeyB64: b64H)! + let decryptClient = keyBundle.factory({ CleartextPayloadJSON($0) }) + let encryptClient = keyBundle.serializer({ $0.json }) // It's already a JSON. + + let toRecord = { + return Record.fromEnvelope($0, payloadFactory: decryptClient) + } + + let envelope = EnvelopeJSON(inputString) + if let r = toRecord(envelope) { + XCTAssertEqual(r.id, expectedGUID) + XCTAssertTrue(r.modified == expectedLastModified) //1326254123650 + XCTAssertEqual(r.sortindex, expectedSortIndex) + + if let ee = encryptClient(r) { + let envelopePrime = EnvelopeJSON(ee) + XCTAssertEqual(envelopePrime.id, expectedGUID) + XCTAssertEqual(envelopePrime.id, envelope.id) + XCTAssertEqual(envelopePrime.sortindex, envelope.sortindex) + XCTAssertTrue(envelopePrime.modified == 0) + + if let rPrime = toRecord(envelopePrime) { + // The payloads should be identical. + XCTAssertTrue(rPrime.equalPayloads(r)) + } else { + XCTFail("No record.") + } + } else { + XCTFail("No record.") + } + } else { + XCTFail("No record.") + } + + // Test invalid Base64. + let badInputString = "{\"sortindex\": 131, \"payload\": \"{\\\"ciphertext\\\":\\\"~~~YJB4dr0vZEIWPirfU2FCJvfzeSLiOP5QWasol2R6ILUxdHsJWuUuvTZVhxYQfTVNou6hVV67jfAvi5Cs+bqhhQsv7icZTiZhPTiTdVGt+uuMotxauVA5OryNGVEZgCCTvT3upzhDFdDbJzVd9O3/gU/b7r/CmAHykX8bTlthlbWeZ8oz6gwHJB5tPRU15nM/m/qW1vyKIw5pw/ZwtAy630AieRehGIGDk+33PWqsfyuT4EUFY9/Ly+8JlnqzxfiBCunIfuXGdLuqTjJOxgrK8mI4wccRFEdFEnmHvh5x7fjl1ID52qumFNQl8zkB75C8XK25alXqwvRR6/AQSP+BgQ==\\\",\\\"IV\\\":\\\"v/0BFgicqYQsd70T39rraA==\\\",\\\"hmac\\\":\\\"59605ed696f6e0e6e062a03510cff742bf6b50d695c042e8372a93f4c2d37dac\\\"}\", \"id\": \"0-P9fabp9vJD\", \"modified\": 1326254123.65}" + let badEnvelope = EnvelopeJSON(badInputString) + XCTAssertTrue(badEnvelope.isValid()) // It's a valid envelope containing nonsense ciphertext. + XCTAssertNil(toRecord(badEnvelope)) // Even though the envelope is valid, the payload is invalid, so we can't construct a record. + } + + func testMeta() { + let fullRecord = "{\"id\":\"global\"," + + "\"payload\":" + + "\"{\\\"syncID\\\":\\\"zPSQTm7WBVWB\\\"," + + "\\\"declined\\\":[\\\"bookmarks\\\"]," + + "\\\"storageVersion\\\":5," + + "\\\"engines\\\":{" + + "\\\"clients\\\":{\\\"version\\\":1,\\\"syncID\\\":\\\"fDg0MS5bDtV7\\\"}," + + "\\\"forms\\\":{\\\"version\\\":1,\\\"syncID\\\":\\\"GXF29AFprnvc\\\"}," + + "\\\"history\\\":{\\\"version\\\":1,\\\"syncID\\\":\\\"av75g4vm-_rp\\\"}," + + "\\\"passwords\\\":{\\\"version\\\":1,\\\"syncID\\\":\\\"LT_ACGpuKZ6a\\\"}," + + "\\\"prefs\\\":{\\\"version\\\":2,\\\"syncID\\\":\\\"-3nsksP9wSAs\\\"}," + + "\\\"tabs\\\":{\\\"version\\\":1,\\\"syncID\\\":\\\"W4H5lOMChkYA\\\"}}}\"," + + "\"username\":\"5817483\"," + + "\"modified\":1.32046073744E9}" + + let record = EnvelopeJSON(fullRecord) + XCTAssertTrue(record.isValid()) + + let global = MetaGlobal.fromJSON(JSON(parseJSON: record.payload)) + XCTAssertTrue(global != nil) + + if let global = global { + XCTAssertEqual(["bookmarks"], global.declined) + XCTAssertEqual(5, global.storageVersion) + let modified = record.modified + XCTAssertTrue(1320460737440 == modified) + let forms = global.engines["forms"] + let syncID = forms!.syncID + XCTAssertEqual("GXF29AFprnvc", syncID) + + let payload: JSON = global.asPayload().json + XCTAssertEqual("GXF29AFprnvc", payload["engines"]["forms"]["syncID"].stringValue) + XCTAssertEqual(1, payload["engines"]["forms"]["version"].intValue) + XCTAssertEqual("bookmarks", payload["declined"].arrayValue[0].stringValue) + } + } + + func testHistoryPayload() { + let payloadJSON = "{\"id\":\"--DzSJTCw-zb\",\"histUri\":\"https://bugzilla.mozilla.org/show_bug.cgi?id=1154549\",\"title\":\"1154549 – Encapsulate synced profile data within an account-centric object\",\"visits\":[{\"date\":1429061233163240,\"type\":1}]}" + let json = JSON(parseJSON: payloadJSON) + if let payload = HistoryPayload.fromJSON(json) { + XCTAssertEqual("--DzSJTCw-zb", payload["id"].stringValue) + XCTAssertEqual("1154549 – Encapsulate synced profile data within an account-centric object", payload["title"].stringValue) + XCTAssertEqual(1, payload.visits[0].type.rawValue) + XCTAssertEqual(1429061233163240, payload.visits[0].date) + + let v = payload.visits[0] + let j = v.toJSON() + XCTAssertEqual(1, j["type"] as! Int) + XCTAssertEqual(1429061233163240, j["date"] as! Int64) + } else { + XCTFail("Should have parsed.") + } + } + + func testHistoryPayloadWithNoURL() { + let payloadJSON = "{\"id\":\"--DzSJTCw-zb\",\"histUri\":null,\"visits\":[{\"date\":1429061233163240,\"type\":1}]}" + let json = JSON(parseJSON: payloadJSON) + XCTAssertNil(HistoryPayload.fromJSON(json)) + } + + func testHistoryPayloadWithNoTitle() { + let payloadJSON = "{\"id\":\"--DzSJTCw-zb\",\"histUri\":\"https://foo.com/\",\"visits\":[{\"date\":1429061233163240,\"type\":1}]}" + let json = JSON(parseJSON: payloadJSON) + if let payload = HistoryPayload.fromJSON(json) { + // Missing fields are null-valued in SwiftyJSON. + XCTAssertTrue(payload["title"].isNull()) + XCTAssertEqual("", payload.title) + } else { + XCTFail("Should have parsed.") + } + } + + func testHistoryPayloadWithNullTitle() { + let payloadJSON = "{\"id\":\"--DzSJTCw-zb\",\"histUri\":\"https://foo.com/\",\"title\":null,\"visits\":[{\"date\":1429061233163240,\"type\":1}]}" + let json = JSON(parseJSON: payloadJSON) + if let payload = HistoryPayload.fromJSON(json) { + XCTAssertEqual("", payload.title) + } else { + XCTFail("Should have parsed.") + } + } + + func testLoginPayload() { + let input = JSON([ + "id": "abcdefabcdef", + "hostname": "http://foo.com/", + "username": "foo", + "password": "bar", + "usernameField": "field", + "passwordField": "bar", + // No formSubmitURL. + "httpRealm": "", + ]) + + // fromJSON returns nil if not valid. + XCTAssertNotNil(LoginPayload.fromJSON(input)) + } + + func testSeparators() { + // Mistyped parentid. + let invalidSeparator = JSON(["type": "separator", "arentid": "toolbar", "parentName": "Bookmarks Toolbar", "pos": 3]) + let sep = BookmarkType.payloadFromJSON(invalidSeparator) + XCTAssertTrue(sep is SeparatorPayload) + XCTAssertFalse(sep!.isValid()) + + // This one's right. + let validSeparator = JSON(["id": "abcabcabcabc", "type": "separator", "parentid": "toolbar", "parentName": "Bookmarks Toolbar", "pos": 3]) + let separator = BookmarkType.payloadFromJSON(validSeparator)! + XCTAssertTrue(separator is SeparatorPayload) + XCTAssertTrue(separator.isValid()) + XCTAssertEqual(3, separator["pos"].intValue) + } + + func testFolders() { + let validFolder = JSON([ + "id": "abcabcabcabc", + "type": "folder", + "parentid": "toolbar", + "parentName": "Bookmarks Toolbar", + "title": "Sóme stüff", + "description": "", + "children": ["foo", "bar"], + ]) + let folder = BookmarkType.payloadFromJSON(validFolder)! + XCTAssertTrue(folder is FolderPayload) + XCTAssertTrue(folder.isValid()) + XCTAssertEqual((folder as! FolderPayload).children, ["foo", "bar"]) + } + +// swiftlint:disable line_length + func testMobileBookmarksFolder() { + let children = ["M87np9Vfh_2s", "-JxRyqNte-ue", "6lIQzUtbjE8O", "eOg3jPSslzXl", "1WJIi9EjQErp", "z5uRo45Rvfbd", "EK3lcNd0sUFN", "gFD3GTljgu12", "eRZGsbN1ew9-", "widfEdgGn9de", "l7eTOR4Uf6xq", "vPbxG-gpN4Rb", "4dwJ8CototFe", "zK-kw9Ii6ScW", "eDmDU-gtEFW6", "lKjqWQaL_syt", "ETVDvWgGT31Q", "3Z_bMIHPSZQ8", "Fqu4_bJOk7fT", "Uo_5K1QrA67j", "gDTXNg4m1AJZ", "zpds8P-9xews", "87zjNtVGPtEp", "ZJru8Sn3qhW7", "txVnzBBBOgLP", "JTnRqFaj_oNa", "soaMlfmM4kjR", "g8AcVBjo6IRf", "uPUDaiG4q637", "rfq2bUud_w4d", "XBGxsiuUG2UD", "-VQRnJlyAvMs", "6wu7TScKdTU7", "ZeFji2hLVpLj", "HpCn_TVizMWX", "IPR5HZwRdlwi", "00JFOGuWnhWB", "P1jb3qKt32Vg", "D6MQJ43V1Ir5", "qWSoXFteRfsq", "o2avfYqEdomL", "xRS0U0YnjK9G", "VgOgzE_xfP4w", "SwP3rMJGvoO3", "Hf2jEgI_-PWa", "AyhmBi7Cv598", "-PaMuzTJXxVk", "JMhYrg8SlY5K", "SQeySEjzyplL", "GTAwd2UkEQEe", "x3RsZj5Ilebr", "sRZWZqPi74FP", "amHR50TpygA6", "XSk782ceVNN6", "ipiMyYQzeypI", "ph2k3Nqfhau4", "m5JKC3hAEQ0H", "yTVerkmQbNxk", "7taA6FbbbUbH", "PZvpbSRuJLPs", "C8atoa25U94F", "KOfNJk_ISLc6", "Bt74lBG9tJq6", "BuHoY2rUhuKA", "XTmoWKnwfIPl", "ZATwa3oTD1m0", "e8TczN5It6Am", "6kCUYs8hQtKg", "jDD8s5aiKoex", "QmpmcrYwLU29", "nCRcekynuJ08", "resttaI4J9tu", "EKSX3HV55VU3", "2-yCz0EIsVls", "sSeeGw3VbBY-", "qfpCrU34w9y0", "RKDgzPWecD6m", "5SgXEKu_dICW", "R143WAeB5E5r", "8Ns4-NiKG62r", "4AHuZDvop5XX", "YCP1OsO1goFF", "CYYaU1mQ_N6t", "UGkzEOMK8cuU", "1RzZOarkzQBa", "qSW2Z3cZSI9c", "ooPlKEAfQsnn", "jIUScoKLiXQt", "bjNTKugzRRL1", "hR24ZVnHUZcs", "3j2IDAZgUyYi", "xnWcy-sQDJRu", "UCcgJqGk3bTV", "WSSRWeptH9tq", "4ugv47OGD2E2", "XboCZgUx-x3x", "HrmWqiqsuLrm", "OjdxvRJ3Jb6j"] + let json = JSON([ + "id": "UjAHxFOGEqU8", + "type": "folder", + "parentName": "", + "title": "mobile", + "description": JSON.null, + "children": children, + "parentid": "places", + ]) + + let bookmark = BookmarkType.payloadFromJSON(json) + XCTAssertTrue(bookmark is FolderPayload) + XCTAssertTrue(bookmark?.isValid() ?? false) + } +// swiftlint:enable line_length + + func testLivemarkMissingFields() { + let json = JSON([ + "id": "M5bwUKK8hPyF", + "type": "livemark", + "siteUri": "http://www.bbc.co.uk/go/rss/int/news/-/news/", + "feedUri": "http://fxfeeds.mozilla.com/en-US/firefox/headlines.xml", + "parentName": "Bookmarks Toolbar", + "parentid": "toolbar", + "children": ["3Qr13GucOtEh"]]) + + let bookmark = BookmarkType.payloadFromJSON(json) + XCTAssertTrue(bookmark is LivemarkPayload) + + let livemark = bookmark as! LivemarkPayload + XCTAssertTrue(livemark.isValid()) + let siteURI = "http://www.bbc.co.uk/go/rss/int/news/-/news/" + let feedURI = "http://fxfeeds.mozilla.com/en-US/firefox/headlines.xml" + XCTAssertEqual(feedURI, livemark.feedURI) + XCTAssertEqual(siteURI, livemark.siteURI) + + let m = (livemark as MirrorItemable).toMirrorItem(Date.now()) + XCTAssertEqual("http://fxfeeds.mozilla.com/en-US/firefox/headlines.xml", m.feedURI) + XCTAssertEqual("http://www.bbc.co.uk/go/rss/int/news/-/news/", m.siteURI) + } + + func testDeletedRecord() { + let json = JSON([ + "id": "abcdefghijkl", + "deleted": true, + "type": "bookmark", + ]) + + guard let payload = BookmarkType.payloadFromJSON(json) else { + XCTFail() + return + } + + XCTAssertFalse(payload is BookmarkPayload) // Only BookmarkBasePayload. + XCTAssertTrue(payload.isValid()) + } + + func testUnknownRecordType() { + // It'll return a base payload that's invalid because its type is unknown. + let json = JSON([ + "parentid": "mobile", + "tags": [], + "title": "Dispozitivul meu", + "id": "pQSMHiA7fD0Z", + "type": "something", + "parentName": "mobile", + ]) + + XCTAssertNil(BookmarkType.payloadFromJSON(json)) + + let payload = BookmarkType.somePayloadFromJSON(json) + XCTAssertFalse(payload.isValid()) // Not valid because type is unknown. + } + + func testInvalidRecordWithType() { + // It should still return the right payload type, even if it's not valid. + let json = JSON([ + "parentid": "mobile", + "bmkUri": JSON.null, + "tags": [], + "title": "Dispozitivul meu", + "id": "pQSMHiA7fD0Z", + "type": "bookmark", + "parentName": "mobile", + ]) + + guard let payload = BookmarkType.payloadFromJSON(json) else { + XCTFail() + return + } + + XCTAssertFalse(payload.isValid()) + XCTAssertTrue(payload is BookmarkPayload) + } + + func testLivemark() { + let json = JSON([ + "id": "M5bwUKK8hPyF", + "type": "livemark", + "siteUri": "http://www.bbc.co.uk/go/rss/int/news/-/news/", + "feedUri": "http://fxfeeds.mozilla.com/en-US/firefox/headlines.xml", + "parentName": "Bookmarks Toolbar", + "parentid": "toolbar", + "title": "Latest Headlines", + "description": "", + "children": + ["7oBdEZB-8BMO", "SUd1wktMNCTB", "eZe4QWzo1BcY", "YNBhGwhVnQsN", + "92Aw2SMEkFg0", "uw0uKqrVFwd-", "x7mx2P3--8FJ", "d-jVF8UuC9Ye", + "DV1XVtKLEiZ5", "g4mTaTjr837Z", "1Zi5W3lwBw8T", "FEYqlUHtbBWS", + "qQd2u7LjosCB", "VUs2djqYfbvn", "KuhYnHocu7eg", "u2gcg9ILRg-3", + "hfK_RP-EC7Ol", "Aq5qsa4E5msH", "6pZIbxuJTn-K", "k_fp0iN3yYMR", + "59YD3iNOYO8O", "01afpSdAk2iz", "Cq-kjXDEPIoP", "HtNTjt9UwWWg", + "IOU8QRSrTR--", "HJ5lSlBx6d1D", "j2dz5R5U6Khc", "5GvEjrNR0yJl", + "67ozIBF5pNVP", "r5YB0cUx6C_w", "FtmFDBNxDQ6J", "BTACeZq9eEtw", + "ll4ozQ-_VNJe", "HpImsA4_XuW7", "nJvCUQPLSXwA", "94LG-lh6TUYe", + "WHn_QoOL94Os", "l-RvjgsZYlej", "LipQ8abcRstN", "74TiLvarE3n_", + "8fCiLQpQGK1P", "Z6h4WkbwfQFa", "GgAzhqakoS6g", "qyt92T8vpMsK", + "RyOgVCe2EAOE", "bgSEhW3w6kk5", "hWODjHKGD7Ph", "Cky673aqOHbT", + "gZCYT7nx3Nwu", "iJzaJxxrM58L", "rUHCRv68aY5L", "6Jc1hNJiVrV9", + "lmNgoayZ-ym8", "R1lyXsDzlfOd", "pinrXwDnRk6g", "Sn7TmZV01vMM", + "qoXyU6tcS1dd", "TRLanED-QfBK", "xHbhMeX_FYEA", "aPqacdRlAtaW", + "E3H04Wn2RfSi", "eaSIMI6kSrcz", "rtkRxFoG5Vqi", "dectkUglV0Dz", + "B4vUE0BE15No", "qgQFW5AQrgB0", "SxAXvwOhu8Zi", "0S6cRPOg-5Z2", + "zcZZBGeLnaWW", "B0at8hkQqVZQ", "sgPtgGulbP66", "lwtwGHSCPYaQ", + "mNTdpgoRZMbW", "-L8Vci6CbkJY", "bVzudKSQERc1", "Gxl9lb4DXsmL", + "3Qr13GucOtEh"]]) + + let bookmark = BookmarkType.payloadFromJSON(json) + XCTAssertTrue(bookmark is LivemarkPayload) + + let livemark = bookmark as! LivemarkPayload + XCTAssertTrue(livemark.isValid()) + let siteURI = "http://www.bbc.co.uk/go/rss/int/news/-/news/" + let feedURI = "http://fxfeeds.mozilla.com/en-US/firefox/headlines.xml" + XCTAssertEqual(feedURI, livemark.feedURI) + XCTAssertEqual(siteURI, livemark.siteURI) + + let m = (livemark as MirrorItemable).toMirrorItem(Date.now()) + XCTAssertEqual("http://fxfeeds.mozilla.com/en-US/firefox/headlines.xml", m.feedURI) + XCTAssertEqual("http://www.bbc.co.uk/go/rss/int/news/-/news/", m.siteURI) + } + + func testMobileBookmark() { + let json = JSON([ + "id": "jIUScoKLiXQt", + "type": "bookmark", + "title": "Join the Engineering Leisure Class — Medium", + "parentName": "mobile", + "bmkUri": "https://medium.com/@chrisloer/join-the-engineering-leisure-class-b3083c09a78e", + "tags": [], + "keyword": JSON.null, + "description": JSON.null, + "loadInSidebar": false, + "parentid": "mobile", + ]) + + let bookmark = BookmarkType.payloadFromJSON(json) + XCTAssertTrue(bookmark is BookmarkPayload) + XCTAssertTrue(bookmark?.isValid() ?? false) + } + + func testQuery() { + let str = "{\"title\":\"Downloads\",\"parentName\":\"\",\"bmkUri\":\"place:transition=7&sort=4\",\"id\":\"7gdp9S1okhKf\",\"parentid\":\"rq6WHyfHkoUV\",\"type\":\"query\"}" + + let query = BookmarkType.payloadFromJSON(JSON(parseJSON: str)) + XCTAssertTrue(query is BookmarkQueryPayload) + let mirror = query?.toMirrorItem(Date.now()) + let roundtrip = mirror?.asPayload() + XCTAssertTrue(roundtrip! is BookmarkQueryPayload) + } + + func testBookmarks() { + let validBookmark = JSON([ + "id": "abcabcabcabc", + "type": "bookmark", + "parentid": "menu", + "parentName": "Bookmarks Menu", + "title": "Anøther", + "bmkUri": "http://terrible.sync/naming", + "description": "", + "tags": [], + "keyword": "", + ]) + let bookmark = BookmarkType.payloadFromJSON(validBookmark) + XCTAssertTrue(bookmark is BookmarkPayload) + + let query = JSON(parseJSON: "{\"id\":\"ShCZLGEFQMam\",\"type\":\"query\",\"title\":\"Downloads\",\"parentName\":\"\",\"bmkUri\":\"place:transition=7&sort=4\",\"tags\":[],\"keyword\":null,\"description\":null,\"loadInSidebar\":false,\"parentid\":\"T6XK5oJMU8ih\"}") + + guard let q = BookmarkType.payloadFromJSON(query) else { + XCTFail("Failed to generate payload from json: \(query)") + return + } + + XCTAssertTrue(q is BookmarkQueryPayload) + + let item = q.toMirrorItem(Date.now()) + XCTAssertEqual(6, item.type.rawValue) + XCTAssertEqual("ShCZLGEFQMam", item.guid) + + let places = JSON(parseJSON: "{\"id\":\"places\",\"type\":\"folder\",\"title\":\"\",\"description\":null,\"children\":[\"menu________\",\"toolbar_____\",\"tags________\",\"unfiled_____\",\"jKnyPDrBQSDg\",\"T6XK5oJMU8ih\"],\"parentid\":\"2hYxKgBwvkEH\"}") + guard let p = BookmarkType.payloadFromJSON(places) else { + XCTFail("Failed to generate payload from json: \(places)") + return + } + + XCTAssertTrue(p is FolderPayload) + + // Items keep their GUID until they're written into the mirror table. + XCTAssertEqual("places", p.id) + + let pMirror = p.toMirrorItem(Date.now()) + + XCTAssertEqual(2, pMirror.type.rawValue) + + // The mirror item has a translated GUID. + XCTAssertEqual(BookmarkRoots.RootGUID, pMirror.guid) + } +} diff --git a/mobile/ios/SyncTests/StateTests.swift b/mobile/ios/SyncTests/StateTests.swift new file mode 100644 index 0000000000..428c4870cd --- /dev/null +++ b/mobile/ios/SyncTests/StateTests.swift @@ -0,0 +1,68 @@ +/* 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 +@testable import Sync + +import XCTest + +func compareScratchpads(tuple: (lhs: Scratchpad, rhs: Scratchpad)) { + // This one is set in the constructor! + XCTAssertEqual(tuple.lhs.syncKeyBundle, tuple.rhs.syncKeyBundle) + + XCTAssertEqual(tuple.lhs.clientName, tuple.rhs.clientName) + XCTAssertEqual(tuple.lhs.clientGUID, tuple.rhs.clientGUID) + if let lkeys = tuple.lhs.keys { + if let rkeys = tuple.rhs.keys { + XCTAssertEqual(lkeys.timestamp, rkeys.timestamp) + XCTAssertEqual(lkeys.value, rkeys.value) + } else { + XCTAssertTrue(tuple.rhs.keys != nil) + } + } else { + XCTAssertTrue(tuple.rhs.keys == nil) + } + + XCTAssertTrue(tuple.lhs.global == tuple.rhs.global) + XCTAssertEqual(tuple.lhs.localCommands, tuple.rhs.localCommands) + XCTAssertEqual(tuple.lhs.engineConfiguration, tuple.rhs.engineConfiguration) +} + +func roundtrip(s: Scratchpad) -> (Scratchpad, rhs: Scratchpad) { + let prefs = MockProfilePrefs() + let _ = s.pickle(prefs) + return (s, rhs: Scratchpad.restoreFromPrefs(prefs, syncKeyBundle: s.syncKeyBundle)!) +} + +class StateTests: XCTestCase { + func getGlobal() -> Fetched { + let g = MetaGlobal(syncID: "abcdefghiklm", storageVersion: 5, engines: ["bookmarks": EngineMeta(version: 1, syncID: "dddddddddddd")], declined: ["tabs"]) + return Fetched(value: g, timestamp: Date.now()) + } + + func getEngineConfiguration() -> EngineConfiguration { + return EngineConfiguration(enabled: ["bookmarks", "clients"], declined: ["tabs"]) + } + + func baseScratchpad() -> Scratchpad { + let syncKeyBundle = KeyBundle.fromKB(Bytes.generateRandomBytes(32)) + let keys = Fetched(value: Keys(defaultBundle: syncKeyBundle), timestamp: 1001) + let b = Scratchpad(b: syncKeyBundle, persistingTo: MockProfilePrefs()).evolve() + let _ = b.setKeys(keys) + b.localCommands = Set([ + .enableEngine(engine: "tabs"), + .disableEngine(engine: "passwords"), + .resetAllEngines(except: Set(["bookmarks", "clients"])), + .resetEngine(engine: "clients")]) + return b.build() + } + + func testPickling() { + compareScratchpads(tuple: roundtrip(s: baseScratchpad())) + compareScratchpads(tuple: roundtrip(s: baseScratchpad().evolve().setGlobal(getGlobal()).build())) + compareScratchpads(tuple: roundtrip(s: baseScratchpad().evolve().clearLocalCommands().build())) + compareScratchpads(tuple: roundtrip(s: baseScratchpad().evolve().setEngineConfiguration(getEngineConfiguration()).build())) + } +} diff --git a/mobile/ios/SyncTests/StorageClientTests.swift b/mobile/ios/SyncTests/StorageClientTests.swift new file mode 100644 index 0000000000..2d7601a428 --- /dev/null +++ b/mobile/ios/SyncTests/StorageClientTests.swift @@ -0,0 +1,108 @@ +/* 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 +@testable import Sync + +import XCTest +import SwiftyJSON + +// Always return a gigantic encoded payload. +func massivify(record: Record) -> JSON? { + return JSON([ + "id": record.id, + "foo": String(repeating: "X", count: Sync15StorageClient.maxRecordSizeBytes + 1) + ]) +} + +class StorageClientTests: XCTestCase { + func testPartialJSON() { + let body = "0" + let o: Any? = try! JSONSerialization.jsonObject(with: body.data(using: String.Encoding.utf8)!, options: JSONSerialization.ReadingOptions.allowFragments) + XCTAssertTrue(JSON(object: o!).isInt()) + } + + func testPOSTResult() { + // Pulled straight from . + let r = "{" + + "\"success\": [\"GXS58IDC_12\", \"GXS58IDC_13\", \"GXS58IDC_15\"," + + "\"GXS58IDC_16\", \"GXS58IDC_18\", \"GXS58IDC_19\"]," + + "\"failed\": {\"GXS58IDC_11\": \"invalid ttl\"," + + "\"GXS58IDC_14\": \"invalid sortindex\"}" + + "}" + + let p = POSTResult.fromJSON(JSON(parseJSON: r)) + XCTAssertTrue(p != nil) + XCTAssertEqual(p!.success[0], "GXS58IDC_12") + XCTAssertEqual(p!.failed["GXS58IDC_14"]!, "invalid sortindex") + + XCTAssertTrue(nil == POSTResult.fromJSON(JSON(parseJSON: "{\"foo\": 5}"))) + } + + func testNumeric() { + let m = ResponseMetadata(status: 200, headers: [ + "X-Last-Modified": "2174380461.12", + ]) + XCTAssertTrue(m.lastModifiedMilliseconds == 2174380461120) + + XCTAssertEqual("2174380461.12", millisecondsToDecimalSeconds(2174380461120)) + } + + // Trivial test for struct semantics that we might want to pay attention to if they change, + // and for response header parsing. + func testResponseHeaders() { + let v: JSON = JSON(parseJSON: "{\"a:\": 2}") + let m = ResponseMetadata(status: 200, headers: [ + "X-Weave-Timestamp": "1274380461.12", + "X-Last-Modified": "2174380461.12", + "X-Weave-Next-Offset": "abdef", + ]) + + XCTAssertTrue(m.lastModifiedMilliseconds == 2174380461120) + XCTAssertTrue(m.timestampMilliseconds == 1274380461120) + XCTAssertTrue(m.nextOffset == "abdef") + + // Just to avoid consistent overflow allowing ==. + XCTAssertTrue(m.lastModifiedMilliseconds?.description == "2174380461120") + XCTAssertTrue(m.timestampMilliseconds.description == "1274380461120") + + let x: StorageResponse = StorageResponse(value: v, metadata: m) + + func doTesting(y: StorageResponse) { + // Make sure that reference fields in a struct are copies of the same reference, + // not references to a copy. + XCTAssertTrue(x.value == y.value) + + XCTAssertTrue(y.metadata.lastModifiedMilliseconds == x.metadata.lastModifiedMilliseconds, "lastModified is the same.") + + XCTAssertTrue(x.metadata.quotaRemaining == nil, "No quota.") + XCTAssertTrue(y.metadata.lastModifiedMilliseconds == 2174380461120, "lastModified is correct.") + XCTAssertTrue(x.metadata.timestampMilliseconds == 1274380461120, "timestamp is correct.") + XCTAssertTrue(x.metadata.nextOffset == "abdef", "nextOffset is correct.") + XCTAssertTrue(x.metadata.records == nil, "No X-Weave-Records.") + } + + doTesting(y: x) + } + + func testOverSizeRecords() { + let delegate = MockSyncDelegate() + + // We can use these useless values because we're directly injecting decrypted + // payloads; no need for real keys etc. + let prefs = MockProfilePrefs() + let scratchpad = Scratchpad(b: KeyBundle.random(), persistingTo: prefs) + + let synchronizer = IndependentRecordSynchronizer(scratchpad: scratchpad, delegate: delegate, basePrefs: prefs, why: .scheduled, collection: "foo") + let jA = "{\"id\":\"aaaaaa\",\"histUri\":\"http://foo.com/\",\"title\": \"ñ\",\"visits\":[{\"date\":1222222222222222,\"type\":1}]}" + let rA = Record(id: "aaaaaa", payload: CleartextPayloadJSON(JSON(parseJSON: jA)), modified: 10000, sortindex: 123, ttl: 1000000) + + let storageClient = Sync15StorageClient(serverURI: "http://example.com/".asURL!, authorizer: identity, workQueue: DispatchQueue.main, resultQueue: DispatchQueue.main, backoff: MockBackoffStorage()) + let collectionClient = storageClient.clientForCollection("foo", encrypter: RecordEncrypter(serializer: massivify, factory: { CleartextPayloadJSON($0) })) + let result = synchronizer.uploadRecords([rA], lastTimestamp: Date.now(), storageClient: collectionClient, onUpload: { _ in deferMaybe(Date.now()) }) + + XCTAssertTrue(result.value.failureValue is RecordTooLargeError) + } +} diff --git a/mobile/ios/SyncTests/SyncTests-Bridging-Header.h b/mobile/ios/SyncTests/SyncTests-Bridging-Header.h new file mode 100644 index 0000000000..a8ec25a0ed --- /dev/null +++ b/mobile/ios/SyncTests/SyncTests-Bridging-Header.h @@ -0,0 +1,9 @@ +#ifndef Client_SyncTests_Bridging_Header_h +#define Client_SyncTests_Bridging_Header_h + +#import +#import "Shared-Bridging-Header.h" +#import "Storage-Bridging-Header.h" +#import "Sync-Bridging-Header.h" + +#endif diff --git a/mobile/ios/SyncTests/TabsPayloadTests.swift b/mobile/ios/SyncTests/TabsPayloadTests.swift new file mode 100644 index 0000000000..daef0a86e6 --- /dev/null +++ b/mobile/ios/SyncTests/TabsPayloadTests.swift @@ -0,0 +1,90 @@ +/* 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 XCTest +@testable import Sync + +class TabsPayloadTests: XCTestCase { + + func testFromInvalidJSON() { + let tabsPayload1 = TabsPayload("") + XCTAssertFalse(tabsPayload1.isValid()) + + let tabsPayload2 = TabsPayload("null") + XCTAssertFalse(tabsPayload2.isValid()) + + let tabsPayload3 = TabsPayload("{}") + XCTAssertFalse(tabsPayload3.isValid()) + + let tabsPayload4 = TabsPayload("{\"id\": \"abc\"}") + XCTAssertFalse(tabsPayload4.isValid()) + } + + func testFromJSON() { + let tabsPayload = TabsPayload("{\"id\": \"abc\", \"deleted\": false, \"clientName\": \"Foo\", \"tabs\": []}") + XCTAssertTrue(tabsPayload.isValid()) + } + + func testFromJSONWithInvalidRecord() { + let tabsPayload = TabsPayload("{\"id\": \"abc\", \"deleted\": false, \"clientName\": \"Foo\", \"tabs\": null}") + XCTAssertFalse(tabsPayload.isValid()) + + let tabsPayload2 = TabsPayload("{\"id\": \"abc\", \"deleted\": false, \"clientName\": \"Foo\", \"tabs\": 1}") + XCTAssertFalse(tabsPayload2.isValid()) + + let tabsPayload3 = TabsPayload("{\"id\": \"abc\", \"deleted\": false, \"clientName\": \"Foo\", \"tabs\": {}}") + XCTAssertFalse(tabsPayload3.isValid()) + + let tabsPayload4 = TabsPayload("{\"id\": \"abc\", \"deleted\": false, \"clientName\": \"Foo\", \"tabs\": true}") + XCTAssertFalse(tabsPayload4.isValid()) + } + + func testTabWithBadTabs() { + let tabsPayload1 = TabsPayload("{\"id\": \"abc\", \"deleted\": false, \"clientName\": \"Foo\", \"tabs\": [{}]}") + XCTAssertTrue(tabsPayload1.isValid()) + let tabs1 = tabsPayload1.tabs + XCTAssert(tabs1.count == 0) + + let tabsPayload2 = TabsPayload("{\"id\": \"abc\", \"deleted\": false, \"clientName\": \"Foo\", \"tabs\": [null, {}, [], 123, false, true, \"\"]}") + XCTAssertTrue(tabsPayload2.isValid()) + let tabs2 = tabsPayload2.tabs + XCTAssert(tabs2.count == 0) + } + + func testTabWithCorrectTabLastUsed() { + let payloads = [ + "{\"id\": \"abc\", \"deleted\": false, \"clientName\": \"Foo\", \"tabs\": [{\"title\": \"Some Title\", \"urlHistory\": [\"http://www.example.com\"], \"icon\": null, \"lastUsed\": 1492649651}]}", + "{\"id\": \"abc\", \"deleted\": false, \"clientName\": \"Foo\", \"tabs\": [{\"title\": \"Some Title\", \"urlHistory\": [\"http://www.example.com\"], \"icon\": null, \"lastUsed\": \"1492316843992\"}]}" + ] + + for payload in payloads { + let tabsPayload = TabsPayload(payload) + XCTAssertTrue(tabsPayload.isValid()) + let tabs = tabsPayload.tabs + XCTAssert(tabs.count == 1) + } + } + + func testTabWithBadTabLastUsed() { + let payloads = [ + "{\"id\": \"abc\", \"deleted\": false, \"clientName\": \"Foo\", \"tabs\": [{\"title\": \"Some Title\", \"urlHistory\": [\"http://www.example.com\"], \"icon\": null, \"lastUsed\": null}]}", + "{\"id\": \"abc\", \"deleted\": false, \"clientName\": \"Foo\", \"tabs\": [{\"title\": \"Some Title\", \"urlHistory\": [\"http://www.example.com\"], \"icon\": null, \"lastUsed\": \"\"}]}", + "{\"id\": \"abc\", \"deleted\": false, \"clientName\": \"Foo\", \"tabs\": [{\"title\": \"Some Title\", \"urlHistory\": [\"http://www.example.com\"], \"icon\": null, \"lastUsed\": \"cheese\"}]}", + "{\"id\": \"abc\", \"deleted\": false, \"clientName\": \"Foo\", \"tabs\": [{\"title\": \"Some Title\", \"urlHistory\": [\"http://www.example.com\"], \"icon\": null, \"lastUsed\": true}]}", + "{\"id\": \"abc\", \"deleted\": false, \"clientName\": \"Foo\", \"tabs\": [{\"title\": \"Some Title\", \"urlHistory\": [\"http://www.example.com\"], \"icon\": null, \"lastUsed\": false}]}", + "{\"id\": \"abc\", \"deleted\": false, \"clientName\": \"Foo\", \"tabs\": [{\"title\": \"Some Title\", \"urlHistory\": [\"http://www.example.com\"], \"icon\": null, \"lastUsed\": 9223372036854775807}]}", + "{\"id\": \"abc\", \"deleted\": false, \"clientName\": \"Foo\", \"tabs\": [{\"title\": \"Some Title\", \"urlHistory\": [\"http://www.example.com\"], \"icon\": null, \"lastUsed\": 123456789012345678901234567890}]}", + "{\"id\": \"abc\", \"deleted\": false, \"clientName\": \"Foo\", \"tabs\": [{\"title\": \"Some Title\", \"urlHistory\": [\"http://www.example.com\"], \"icon\": null, \"lastUsed\": -1}]}", + "{\"id\": \"abc\", \"deleted\": false, \"clientName\": \"Foo\", \"tabs\": [{\"title\": \"Some Title\", \"urlHistory\": [\"http://www.example.com\"], \"icon\": null, \"lastUsed\": \"123456789012345678901234567890\"}]}", + "{\"id\": \"abc\", \"deleted\": false, \"clientName\": \"Foo\", \"tabs\": [{\"title\": \"Some Title\", \"urlHistory\": [\"http://www.example.com\"], \"icon\": null, \"lastUsed\": \"-1\"}]}" + ] + + for payload in payloads { + let tabsPayload = TabsPayload(payload) + XCTAssertTrue(tabsPayload.isValid(), "Should not be valid: \(payload)") + let tabs = tabsPayload.tabs + XCTAssert(tabs.count == 0, "Should not have valid tabs: \(payload)") + } + } +} diff --git a/mobile/ios/SyncTests/TestBookmarkModel.swift b/mobile/ios/SyncTests/TestBookmarkModel.swift new file mode 100644 index 0000000000..4cb31e9b3f --- /dev/null +++ b/mobile/ios/SyncTests/TestBookmarkModel.swift @@ -0,0 +1,201 @@ +/* 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 Deferred +import Foundation +import Shared +@testable import Storage +@testable import Sync +import XCTest + +// Thieved mercilessly from TestSQLiteBookmarks. +private func getBrowserDBForFile(filename: String, files: FileAccessor) -> BrowserDB? { + return BrowserDB(filename: filename, schema: BrowserSchema(), files: files) +} + +class TestBookmarkModel: FailFastTestCase { + let files = MockFiles() + + override func tearDown() { + do { + try self.files.removeFilesInDirectory() + } catch { + } + super.tearDown() + } + + private func getBrowserDB(name: String) -> BrowserDB? { + let file = "TBookmarkModel\(name).db" + print("DB file named: \(file)") + return getBrowserDBForFile(filename: file, files: self.files) + } + + func getSyncableBookmarks(name: String) -> MergedSQLiteBookmarks? { + guard let db = self.getBrowserDB(name: name) else { + XCTFail("Couldn't get prepared DB.") + return nil + } + + return MergedSQLiteBookmarks(db: db) + } + + func testBookmarkEditableIfNeverSyncedAndEmptyBuffer() { + guard let bookmarks = self.getSyncableBookmarks(name: "A") else { + XCTFail("Couldn't get bookmarks.") + return + } + + // Set a local bookmark + let bookmarkURL = "http://AAA.com".asURL! + bookmarks.local.insertBookmark(bookmarkURL, title: "AAA", favicon: nil, intoFolder: BookmarkRoots.MenuFolderGUID, withTitle: "").succeeded() + + XCTAssertTrue(bookmarks.isMirrorEmpty().value.successValue!) + XCTAssertTrue(bookmarks.buffer.isEmpty().value.successValue!) + + let menuFolder = bookmarks.menuFolder() + XCTAssertEqual(menuFolder.current.count, 1) + XCTAssertTrue(menuFolder.current[0]!.isEditable) + } + + func testBookmarkEditableIfNeverSyncedWithBufferedChanges() { + guard let bookmarks = self.getSyncableBookmarks(name: "B") else { + XCTFail("Couldn't get bookmarks.") + return + } + + let bookmarkURL = "http://AAA.com".asURL! + bookmarks.local.insertBookmark(bookmarkURL, title: "AAA", favicon: nil, intoFolder: BookmarkRoots.MenuFolderGUID, withTitle: "").succeeded() + + // Add a buffer into the buffer + let mirrorDate = Date.now() - 100000 + bookmarks.applyRecords([ + BookmarkMirrorItem.folder(BookmarkRoots.MenuFolderGUID, dateAdded: mirrorDate, modified: mirrorDate, hasDupe: false, parentID: BookmarkRoots.RootGUID, parentName: "", title: "Bookmarks Menu", description: "", children: ["BBB"]), + BookmarkMirrorItem.bookmark("BBB", dateAdded: mirrorDate, modified: mirrorDate, hasDupe: false, parentID: BookmarkRoots.MenuFolderGUID, parentName: "Bookmarks Menu", title: "BBB", description: nil, URI: "http://BBB.com", tags: "", keyword: nil) + ]).succeeded() + + XCTAssertFalse(bookmarks.buffer.isEmpty().value.successValue!) + XCTAssertTrue(bookmarks.isMirrorEmpty().value.successValue!) + + // Check to see if we're editable + let menuFolder = bookmarks.menuFolder() + XCTAssertEqual(menuFolder.current.count, 1) + XCTAssertTrue(menuFolder.current[0]!.isEditable) + } + + func testBookmarksEditableWithEmptyBufferAndRemoteBookmark() { + guard let bookmarks = self.getSyncableBookmarks(name: "C") else { + XCTFail("Couldn't get bookmarks.") + return + } + + // Add a bookmark to the menu folder in our mirror + let mirrorDate = Date.now() - 100000 + bookmarks.populateMirrorViaBuffer(items: [ + BookmarkMirrorItem.folder(BookmarkRoots.RootGUID, dateAdded: mirrorDate, modified: mirrorDate, hasDupe: false, parentID: BookmarkRoots.RootGUID, parentName: "", title: "", description: "", children: BookmarkRoots.RootChildren), + BookmarkMirrorItem.folder(BookmarkRoots.MenuFolderGUID, dateAdded: mirrorDate, modified: mirrorDate, hasDupe: false, parentID: BookmarkRoots.RootGUID, parentName: "", title: "Bookmarks Menu", description: "", children: ["CCC"]), + BookmarkMirrorItem.bookmark("CCC", dateAdded: mirrorDate, modified: mirrorDate, hasDupe: false, parentID: BookmarkRoots.MenuFolderGUID, parentName: "Bookmarks Menu", title: "CCC", description: nil, URI: "http://CCC.com", tags: "", keyword: nil) + ], atDate: mirrorDate) + + // Set a local bookmark + let bookmarkURL = "http://AAA.com".asURL! + bookmarks.local.insertBookmark(bookmarkURL, title: "AAA", favicon: nil, intoFolder: BookmarkRoots.MenuFolderGUID, withTitle: "").succeeded() + + XCTAssertTrue(bookmarks.buffer.isEmpty().value.successValue!) + XCTAssertFalse(bookmarks.isMirrorEmpty().value.successValue!) + + // Check to see if we're editable + let menuFolder = bookmarks.menuFolder() + XCTAssertEqual(menuFolder.current.count, 2) + XCTAssertTrue(menuFolder.current[0]!.isEditable) + XCTAssertTrue(menuFolder.current[1]!.isEditable) + } + + func testBookmarksNotEditableForUnmergedChanges() { + guard let bookmarks = self.getSyncableBookmarks(name: "D") else { + XCTFail("Couldn't get bookmarks.") + return + } + + // Add a bookmark to the menu folder in our mirror + let mirrorDate = Date.now() - 100000 + bookmarks.populateMirrorViaBuffer(items: [ + BookmarkMirrorItem.folder(BookmarkRoots.RootGUID, dateAdded: mirrorDate, modified: mirrorDate, hasDupe: false, parentID: BookmarkRoots.RootGUID, parentName: "", title: "", description: "", children: BookmarkRoots.RootChildren), + BookmarkMirrorItem.folder(BookmarkRoots.MenuFolderGUID, dateAdded: mirrorDate, modified: mirrorDate, hasDupe: false, parentID: BookmarkRoots.RootGUID, parentName: "", title: "Bookmarks Menu", description: "", children: ["EEE"]), + BookmarkMirrorItem.bookmark("EEE", dateAdded: mirrorDate, modified: mirrorDate, hasDupe: false, parentID: BookmarkRoots.MenuFolderGUID, parentName: "Bookmarks Menu", title: "EEE", description: nil, URI: "http://EEE.com", tags: "", keyword: nil) + ], atDate: mirrorDate) + + bookmarks.local.insertBookmark("http://AAA.com".asURL!, title: "AAA", favicon: nil, intoFolder: BookmarkRoots.MobileFolderGUID, withTitle: "Bookmarks Menu").succeeded() + + // Add some unmerged bookmarks into the menu folder in the buffer. + bookmarks.applyRecords([ + BookmarkMirrorItem.folder(BookmarkRoots.MenuFolderGUID, dateAdded: mirrorDate, modified: mirrorDate, hasDupe: false, parentID: BookmarkRoots.RootGUID, parentName: "", title: "Bookmarks Menu", description: "", children: ["EEE", "FFF"]), + BookmarkMirrorItem.bookmark("FFF", dateAdded: mirrorDate, modified: mirrorDate, hasDupe: false, parentID: BookmarkRoots.MenuFolderGUID, parentName: "Bookmarks Menu", title: "FFF", description: nil, URI: "http://FFF.com", tags: "", keyword: nil) + ]).succeeded() + + XCTAssertFalse(bookmarks.buffer.isEmpty().value.successValue!) + XCTAssertFalse(bookmarks.isMirrorEmpty().value.successValue!) + + // Check to see that we can't edit these bookmarks + let menuFolder = bookmarks.menuFolder() + XCTAssertEqual(menuFolder.current.count, 1) + XCTAssertFalse(menuFolder.current[0]!.isEditable) + + } + + func testLocalBookmarksEditableWhileHavingUnmergedChangesAndEmptyMirror() { + guard let bookmarks = self.getSyncableBookmarks(name: "D") else { + XCTFail("Couldn't get bookmarks.") + return + } + + bookmarks.local.insertBookmark("http://AAA.com".asURL!, title: "AAA", favicon: nil, intoFolder: BookmarkRoots.MobileFolderGUID, withTitle: "Bookmarks Menu").succeeded() + + // Add some unmerged bookmarks into the menu folder in the buffer. + let mirrorDate = Date.now() - 100000 + bookmarks.applyRecords([ + BookmarkMirrorItem.folder(BookmarkRoots.MenuFolderGUID, dateAdded: mirrorDate, modified: mirrorDate, hasDupe: false, parentID: BookmarkRoots.RootGUID, parentName: "", title: "Bookmarks Menu", description: "", children: ["EEE", "FFF"]), + BookmarkMirrorItem.bookmark("FFF", dateAdded: mirrorDate, modified: mirrorDate, hasDupe: false, parentID: BookmarkRoots.MenuFolderGUID, parentName: "Bookmarks Menu", title: "FFF", description: nil, URI: "http://FFF.com", tags: "", keyword: nil) + ]).succeeded() + + // Local bookmark should be editable + let mobileFolder = bookmarks.mobileFolder() + XCTAssertEqual(mobileFolder.current.count, 2) + XCTAssertTrue(mobileFolder.current[1]!.isEditable) + } +} + +private extension MergedSQLiteBookmarks { + func isMirrorEmpty() -> Deferred> { + return self.local.db.queryReturnsNoResults("SELECT 1 FROM \(TableBookmarksMirror)") + } + + func wipeLocal() { + self.local.db.run(["DELETE FROM \(TableBookmarksLocalStructure)", "DELETE FROM \(TableBookmarksLocal)"]).succeeded() + } + + func populateMirrorViaBuffer(items: [BookmarkMirrorItem], atDate mirrorDate: Timestamp) { + self.applyRecords(items).succeeded() + + // … and add the root relationships that will be missing (we don't do those for the buffer, + // so we need to manually add them and move them across). + self.buffer.db.run([ + "INSERT INTO \(TableBookmarksBufferStructure) (parent, child, idx) VALUES", + "('\(BookmarkRoots.RootGUID)', '\(BookmarkRoots.MenuFolderGUID)', 0),", + "('\(BookmarkRoots.RootGUID)', '\(BookmarkRoots.ToolbarFolderGUID)', 1),", + "('\(BookmarkRoots.RootGUID)', '\(BookmarkRoots.UnfiledFolderGUID)', 2),", + "('\(BookmarkRoots.RootGUID)', '\(BookmarkRoots.MobileFolderGUID)', 3)", + ].joined(separator: " ")).succeeded() + + // Move it all to the mirror. + self.local.db.moveBufferToMirrorForTesting() + } + + func menuFolder() -> BookmarksModel { + return modelFactory.value.successValue!.modelForFolder(BookmarkRoots.MenuFolderGUID).value.successValue! + } + + func mobileFolder() -> BookmarksModel { + return modelFactory.value.successValue!.modelForFolder(BookmarkRoots.MobileFolderGUID).value.successValue! + } +} diff --git a/mobile/ios/SyncTests/TestBookmarkTreeMerging.swift b/mobile/ios/SyncTests/TestBookmarkTreeMerging.swift new file mode 100644 index 0000000000..90d4873969 --- /dev/null +++ b/mobile/ios/SyncTests/TestBookmarkTreeMerging.swift @@ -0,0 +1,1100 @@ +/* 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 Deferred +import Foundation +import Shared +@testable import Storage +@testable import Sync +import XCTest + +extension Dictionary { + init(seq: S) where S.Iterator.Element == Element { + self.init() + for (k, v) in seq { + self[k] = v + } + } +} + +class MockLocalItemSource: LocalItemSource { + var local: [GUID: BookmarkMirrorItem] = [:] + + func getLocalItemWithGUID(_ guid: GUID) -> Deferred> { + guard let item = self.local[guid] else { + return deferMaybe(DatabaseError(description: "Couldn't find item \(guid).")) + } + return deferMaybe(item) + } + + func getLocalItemsWithGUIDs(_ guids: T) -> Deferred> where T.Iterator.Element == GUID { + var acc: [GUID: BookmarkMirrorItem] = [:] + guids.forEach { guid in + if let item = self.local[guid] { + acc[guid] = item + } + } + return deferMaybe(acc) + } + + func prefetchLocalItemsWithGUIDs(_ guids: T) -> Success where T.Iterator.Element == GUID { + return succeed() + } +} + +class MockMirrorItemSource: MirrorItemSource { + var mirror: [GUID: BookmarkMirrorItem] = [:] + + func getMirrorItemWithGUID(_ guid: GUID) -> Deferred> { + guard let item = self.mirror[guid] else { + return deferMaybe(DatabaseError(description: "Couldn't find item \(guid).")) + } + return deferMaybe(item) + } + + func getMirrorItemsWithGUIDs(_ guids: T) -> Deferred> where T.Iterator.Element == GUID { + var acc: [GUID: BookmarkMirrorItem] = [:] + guids.forEach { guid in + if let item = self.mirror[guid] { + acc[guid] = item + } + } + return deferMaybe(acc) + } + + func prefetchMirrorItemsWithGUIDs(_ guids: T) -> Success where T.Iterator.Element == GUID { + return succeed() + } +} + +class MockBufferItemSource: BufferItemSource { + var buffer: [GUID: BookmarkMirrorItem] = [:] + + func getBufferItemsWithGUIDs(_ guids: T) -> Deferred> where T.Iterator.Element == GUID { + var acc: [GUID: BookmarkMirrorItem] = [:] + guids.forEach { guid in + if let item = self.buffer[guid] { + acc[guid] = item + } + } + return deferMaybe(acc) + } + + func getBufferItemWithGUID(_ guid: GUID) -> Deferred> { + guard let item = self.buffer[guid] else { + return deferMaybe(DatabaseError(description: "Couldn't find item \(guid).")) + } + return deferMaybe(item) + } + + func getBufferChildrenGUIDsForParent(_ guid: GUID) -> Deferred> { + return deferMaybe(DatabaseError(description: "Not implemented")) + } + + func prefetchBufferItemsWithGUIDs(_ guids: T) -> Success where T.Iterator.Element == GUID { + return succeed() + } +} + +class MockUploader { + var deletions: Set = Set() + var added: Set = Set() + var records: [GUID: Record] = [:] + + func getStorer() -> TrivialBookmarkStorer { + return TrivialBookmarkStorer(uploader: self.doUpload) + } + + func doUpload(recs: [Record], lastTimestamp: Timestamp?, onUpload: (POSTResult, Timestamp) -> DeferredTimestamp) -> DeferredTimestamp { + var success: [GUID] = [] + recs.forEach { rec in + success.append(rec.id) + self.records[rec.id] = rec + if rec.payload.deleted { + self.deletions.insert(rec.id) + } else { + self.added.insert(rec.id) + } + } + + // Now pretend we did the upload. + return onUpload(POSTResult(success: success, failed: [:]), Date.now()) + } +} + +// Thieved mercilessly from TestSQLiteBookmarks. +private func getBrowserDBForFile(filename: String, files: FileAccessor) -> BrowserDB? { + let db = BrowserDB(filename: filename, schema: BrowserSchema(), files: files) + db.touch().succeeded() // Ensure the schema is created/updated in advance + return db +} + +class FailFastTestCase: XCTestCase { + // This is how to make an assertion failure stop the current test function + // but continue with other test functions in the same test case. + // See http://stackoverflow.com/a/27016786/22003 + override func invokeTest() { + self.continueAfterFailure = false + defer { self.continueAfterFailure = true } + super.invokeTest() + } +} + +class TestBookmarkTreeMerging: FailFastTestCase { + let files = MockFiles() + + override func tearDown() { + do { + try self.files.removeFilesInDirectory() + } catch { + } + super.tearDown() + } + + private func getBrowserDB(name: String) -> BrowserDB? { + let file = "TBookmarkTreeMerging\(name).db" + return getBrowserDBForFile(filename: file, files: self.files) + } + + private func mockStatsSessionForBookmarks() -> SyncEngineStatsSession { + let session = SyncEngineStatsSession(collection: "bookmarks") + session.start() + return session + } + + func getSyncableBookmarks(name: String) -> MergedSQLiteBookmarks? { + guard let db = self.getBrowserDB(name: name) else { + XCTFail("Couldn't get prepared DB.") + return nil + } + + return MergedSQLiteBookmarks(db: db) + } + + func getSQLiteBookmarks(name: String) -> SQLiteBookmarks? { + guard let db = self.getBrowserDB(name: name) else { + XCTFail("Couldn't get prepared DB.") + return nil + } + + return SQLiteBookmarks(db: db) + } + + func dbLocalTree(name: String) -> BookmarkTree? { + guard let bookmarks = self.getSQLiteBookmarks(name: name) else { + XCTFail("Couldn't get bookmarks.") + return nil + } + + return bookmarks.treeForLocal().value.successValue + } + + func localTree() -> BookmarkTree { + let roots = BookmarkRoots.RootChildren.map { BookmarkTreeNode.folder(guid: $0, children: []) } + let places = BookmarkTreeNode.folder(guid: BookmarkRoots.RootGUID, children: roots) + + var lookup: [GUID: BookmarkTreeNode] = [:] + var parents: [GUID: GUID] = [:] + + for n in roots { + lookup[n.recordGUID] = n + parents[n.recordGUID] = BookmarkRoots.RootGUID + } + lookup[BookmarkRoots.RootGUID] = places + + return BookmarkTree(subtrees: [places], lookup: lookup, parents: parents, orphans: Set(), deleted: Set(), modified: Set(lookup.keys), virtual: Set()) + } + + // Our synthesized tree is the same as the one we pull out of a brand new local DB. + func testLocalTreeAssumption() { + let constructed = self.localTree() + let fromDB = self.dbLocalTree(name: "A") + XCTAssertNotNil(fromDB) + XCTAssertTrue(fromDB!.isFullyRootedIn(constructed)) + XCTAssertTrue(constructed.isFullyRootedIn(fromDB!)) + XCTAssertTrue(fromDB!.virtual.isEmpty) + XCTAssertTrue(constructed.virtual.isEmpty) + } + + // This should never occur in the wild: local will never be empty. + func testMergingEmpty() { + let r = BookmarkTree.emptyTree() + let m = BookmarkTree.emptyMirrorTree() + let l = BookmarkTree.emptyTree() + let s = ItemSources(local: MockLocalItemSource(), mirror: MockMirrorItemSource(), buffer: MockBufferItemSource()) + + XCTAssertEqual(m.virtual, BookmarkRoots.Real) + let merger = ThreeWayTreeMerger(local: l, mirror: m, remote: r, itemSources: s) + guard let mergedTree = merger.produceMergedTree().value.successValue else { + XCTFail("Couldn't merge.") + return + } + + mergedTree.dump() + XCTAssertEqual(mergedTree.allGUIDs, BookmarkRoots.Real) + + guard let result = merger.produceMergeResultFromMergedTree(mergedTree).value.successValue else { + XCTFail("Couldn't produce result.") + return + } + + // We don't test the local override completion, because of course we saw mirror items. + XCTAssertTrue(result.uploadCompletion.isNoOp) + XCTAssertTrue(result.bufferCompletion.isNoOp) + } + + func getItemSourceIncludingEmptyRoots() -> ItemSources { + let local = MockLocalItemSource() + + func makeRoot(guid: GUID, _ name: String) { + local.local[guid] = BookmarkMirrorItem.folder(guid, dateAdded: Date.now(), modified: Date.now(), hasDupe: false, parentID: BookmarkRoots.RootGUID, parentName: "", title: name, description: nil, children: []) + } + + makeRoot(guid: BookmarkRoots.MenuFolderGUID, "Bookmarks Menu") + makeRoot(guid: BookmarkRoots.ToolbarFolderGUID, "Bookmarks Toolbar") + makeRoot(guid: BookmarkRoots.MobileFolderGUID, "Mobile Bookmarks") + makeRoot(guid: BookmarkRoots.UnfiledFolderGUID, "Unsorted Bookmarks") + makeRoot(guid: BookmarkRoots.RootGUID, "") + + return ItemSources(local: local, mirror: MockMirrorItemSource(), buffer: MockBufferItemSource()) + } + + func testMergingOnlyLocalRoots() { + let r = BookmarkTree.emptyTree() + let m = BookmarkTree.emptyMirrorTree() + let l = self.localTree() + let s = self.getItemSourceIncludingEmptyRoots() + + let merger = ThreeWayTreeMerger(local: l, mirror: m, remote: r, itemSources: s) + guard let mergedTree = merger.produceMergedTree().value.successValue else { + XCTFail("Couldn't merge.") + return + } + + mergedTree.dump() + XCTAssertEqual(mergedTree.allGUIDs, BookmarkRoots.Real) + + guard let result = merger.produceMergeResultFromMergedTree(mergedTree).value.successValue else { + XCTFail("Couldn't produce result.") + return + } + + XCTAssertFalse(result.isNoOp) + XCTAssertTrue(result.overrideCompletion.processedLocalChanges.isSuperset(of: Set(BookmarkRoots.RootChildren))) + + // We should be dropping the roots from local, and uploading roots to the server. + // The outgoing records should use Sync-style IDs. + // We never upload the places root. + let banned = Set(["places", BookmarkRoots.RootGUID]) + XCTAssertTrue(banned.isDisjoint(with: Set(result.uploadCompletion.records.map { $0.id }))) + XCTAssertTrue(banned.isDisjoint(with: result.uploadCompletion.amendChildrenFromBuffer.keys)) + XCTAssertTrue(banned.isDisjoint(with: result.uploadCompletion.amendChildrenFromMirror.keys)) + XCTAssertTrue(banned.isDisjoint(with: result.uploadCompletion.amendChildrenFromLocal.keys)) + XCTAssertEqual(Set(BookmarkRoots.RootChildren), Set(result.uploadCompletion.amendChildrenFromLocal.keys)) + XCTAssertEqual(Set(BookmarkRoots.Real), result.overrideCompletion.processedLocalChanges) + } + + func testMergingStorageLocalRootsEmptyServer() { + guard let bookmarks = self.getSyncableBookmarks(name: "B") else { + XCTFail("Couldn't get bookmarks.") + return + } + + // The mirror is never actually empty. + let mirrorTree = bookmarks.treeForMirror().value.successValue! + XCTAssertFalse(mirrorTree.isEmpty) + XCTAssertEqual(mirrorTree.lookup.keys.count, 5) // Root and four children. + XCTAssertEqual(1, mirrorTree.subtrees.count) + + let edgesBefore = bookmarks.treesForEdges().value.successValue! + XCTAssertFalse(edgesBefore.local.isEmpty) + XCTAssertTrue(edgesBefore.buffer.isEmpty) + + let uploader = MockUploader() + let storer = uploader.getStorer() + let applier = MergeApplier(buffer: bookmarks, storage: bookmarks, client: storer, statsSession: mockStatsSessionForBookmarks(), greenLight: { true }) + applier.go().succeeded() + + // Now the local contents are replicated into the mirror, and both the buffer and local are empty. + guard let mirror = bookmarks.treeForMirror().value.successValue else { + XCTFail("Couldn't get mirror!") + return + } + + XCTAssertFalse(mirror.isEmpty) + XCTAssertTrue(mirror.subtrees[0].recordGUID == BookmarkRoots.RootGUID) + let edgesAfter = bookmarks.treesForEdges().value.successValue! + XCTAssertTrue(edgesAfter.local.isEmpty) + XCTAssertTrue(edgesAfter.buffer.isEmpty) + + XCTAssertEqual(uploader.added, Set(BookmarkRoots.RootChildren.map(BookmarkRoots.translateOutgoingRootGUID))) + } + + func testComplexOrphaning() { + // This test describes a scenario like this: + // + // [] [] [] + // [M] [T] [M] [T] [M] [T] + // | | | | | | + // [C] [A] [C] [A] [C] [A] + // | | | | + // [D] [D] [B] [B] + // | | + // F E + // + // That is: we have locally added 'E' to folder B and deleted folder D, + // and remotely added 'F' to folder D and deleted folder B. + // + // This is a fundamental conflict that would ordinarily produce orphans. + // Our resolution for this is to put those orphans _somewhere_. + // + // That place is the lowest surviving parent: walk the tree until we find + // a folder that still exists, and put the orphans there. This is a little + // better than just dumping the records into Unsorted Bookmarks, and no + // more complex than dumping them into the closest root. + // + // We expect: + // + // [] + // [M] [T] + // | | + // [C] [A] + // | | + // F E + // + guard let bookmarks = self.getSyncableBookmarks(name: "G") else { + XCTFail("Couldn't get bookmarks.") + return + } + + // Set up the mirror. + let dateAdded = Date.now() - 200000 + let mirrorDate = Date.now() - 100000 + let records = [ + BookmarkMirrorItem.folder(BookmarkRoots.RootGUID, dateAdded: dateAdded, modified: mirrorDate, hasDupe: false, parentID: BookmarkRoots.RootGUID, parentName: "", title: "", description: "", children: BookmarkRoots.RootChildren), + BookmarkMirrorItem.folder(BookmarkRoots.MenuFolderGUID, dateAdded: dateAdded, modified: mirrorDate, hasDupe: false, parentID: BookmarkRoots.RootGUID, parentName: "", title: "Bookmarks Menu", description: "", children: ["folderCCCCCC"]), + BookmarkMirrorItem.folder(BookmarkRoots.UnfiledFolderGUID, dateAdded: dateAdded, modified: mirrorDate, hasDupe: false, parentID: BookmarkRoots.RootGUID, parentName: "", title: "Unsorted Bookmarks", description: "", children: []), + BookmarkMirrorItem.folder(BookmarkRoots.ToolbarFolderGUID, dateAdded: dateAdded, modified: mirrorDate, hasDupe: false, parentID: BookmarkRoots.RootGUID, parentName: "", title: "Bookmarks Toolbar", description: "", children: ["folderAAAAAA"]), + BookmarkMirrorItem.folder(BookmarkRoots.MobileFolderGUID, dateAdded: dateAdded, modified: mirrorDate, hasDupe: false, parentID: BookmarkRoots.RootGUID, parentName: "", title: "Mobile Bookmarks", description: "", children: []), + BookmarkMirrorItem.folder("folderAAAAAA", dateAdded: dateAdded, modified: mirrorDate, hasDupe: false, parentID: BookmarkRoots.ToolbarFolderGUID, parentName: "Bookmarks Toolbar", title: "A", description: "", children: ["folderBBBBBB"]), + BookmarkMirrorItem.folder("folderBBBBBB", dateAdded: dateAdded, modified: mirrorDate, hasDupe: false, parentID: "folderAAAAAA", parentName: "A", title: "B", description: "", children: []), + BookmarkMirrorItem.folder("folderCCCCCC", dateAdded: dateAdded, modified: mirrorDate, hasDupe: false, parentID: BookmarkRoots.MenuFolderGUID, parentName: "Bookmarks Menu", title: "C", description: "", children: ["folderDDDDDD"]), + BookmarkMirrorItem.folder("folderDDDDDD", dateAdded: dateAdded, modified: mirrorDate, hasDupe: false, parentID: "folderCCCCCC", parentName: "C", title: "D", description: "", children: []), + ] + + bookmarks.populateMirrorViaBuffer(items: records, atDate: mirrorDate) + bookmarks.wipeLocal() + + // Now the buffer is empty, and the mirror tree is what we expect. + let mirrorTree = bookmarks.treeForMirror().value.successValue! + XCTAssertFalse(mirrorTree.isEmpty) + + XCTAssertEqual(mirrorTree.lookup.keys.count, 9) + XCTAssertEqual(1, mirrorTree.subtrees.count) + XCTAssertEqual(mirrorTree.find("folderAAAAAA")!.children!.map { $0.recordGUID }, ["folderBBBBBB"]) + XCTAssertEqual(mirrorTree.find("folderCCCCCC")!.children!.map { $0.recordGUID }, ["folderDDDDDD"]) + + let edgesBefore = bookmarks.treesForEdges().value.successValue! + XCTAssertTrue(edgesBefore.local.isEmpty) // Because we're fully synced. + XCTAssertTrue(edgesBefore.buffer.isEmpty) + + // Set up the buffer. + let bufferDate = Date.now() + let changedBufferRecords = [ + BookmarkMirrorItem.deleted(BookmarkNodeType.folder, guid: "folderBBBBBB", modified: bufferDate), + BookmarkMirrorItem.folder("folderAAAAAA", dateAdded: dateAdded, modified: bufferDate, hasDupe: false, parentID: BookmarkRoots.ToolbarFolderGUID, parentName: "Bookmarks Toolbar", title: "A", description: "", children: []), + BookmarkMirrorItem.folder("folderDDDDDD", dateAdded: dateAdded, modified: bufferDate, hasDupe: false, parentID: "folderCCCCCC", parentName: "C", title: "D", description: "", children: ["bookmarkFFFF"]), + BookmarkMirrorItem.bookmark("bookmarkFFFF", dateAdded: dateAdded, modified: bufferDate, hasDupe: false, parentID: "folderDDDDDD", parentName: "D", title: "F", description: nil, URI: "http://example.com/f", tags: "", keyword: nil), + ] + + bookmarks.applyRecords(changedBufferRecords).succeeded() + + // Make local changes. + bookmarks.local.modelFactory.value.successValue!.removeByGUID("folderDDDDDD").succeeded() + bookmarks.local.insertBookmark("http://example.com/e".asURL!, title: "E", favicon: nil, intoFolder: "folderBBBBBB", withTitle: "B").succeeded() + let insertedGUID = bookmarks.local.db.getGUIDs("SELECT guid FROM \(TableBookmarksLocal) WHERE title IS 'E'")[0] + + let edges = bookmarks.treesForEdges().value.successValue! + + XCTAssertFalse(edges.local.isEmpty) + XCTAssertFalse(edges.buffer.isEmpty) + XCTAssertTrue(edges.local.isFullyRootedIn(mirrorTree)) + XCTAssertTrue(edges.buffer.isFullyRootedIn(mirrorTree)) + XCTAssertTrue(edges.buffer.deleted.contains("folderBBBBBB")) + XCTAssertFalse(edges.buffer.deleted.contains("folderDDDDDD")) + XCTAssertFalse(edges.local.deleted.contains("folderBBBBBB")) + XCTAssertTrue(edges.local.deleted.contains("folderDDDDDD")) + + // Now merge. + let storageMerger = ThreeWayBookmarksStorageMerger(buffer: bookmarks, storage: bookmarks) + let merger = storageMerger.getMerger().value.successValue! + guard let mergedTree = merger.produceMergedTree().value.successValue else { + XCTFail("Couldn't get merge result.") + return + } + + // Dump it so we can see it. + mergedTree.dump() + + XCTAssertTrue(mergedTree.deleteLocally.contains("folderBBBBBB")) + XCTAssertTrue(mergedTree.deleteFromMirror.contains("folderBBBBBB")) + XCTAssertTrue(mergedTree.deleteRemotely.contains("folderDDDDDD")) + XCTAssertTrue(mergedTree.deleteFromMirror.contains("folderDDDDDD")) + XCTAssertTrue(mergedTree.acceptLocalDeletion.contains("folderDDDDDD")) + XCTAssertTrue(mergedTree.acceptRemoteDeletion.contains("folderBBBBBB")) + + // E and F still exist, in Menu and Toolbar respectively. + // Note that the merge itself includes asserts for this; we shouldn't even get here if + // this part will fail. + XCTAssertTrue(mergedTree.allGUIDs.contains(insertedGUID)) + XCTAssertTrue(mergedTree.allGUIDs.contains("bookmarkFFFF")) + + let menu = mergedTree.root.mergedChildren![0] // menu, toolbar, unfiled, mobile, so 0. + let toolbar = mergedTree.root.mergedChildren![1] // menu, toolbar, unfiled, mobile, so 1. + XCTAssertEqual(BookmarkRoots.MenuFolderGUID, menu.guid) + XCTAssertEqual(BookmarkRoots.ToolbarFolderGUID, toolbar.guid) + + let folderC = menu.mergedChildren![0] + let folderA = toolbar.mergedChildren![0] + XCTAssertEqual("folderCCCCCC", folderC.guid) + XCTAssertEqual("folderAAAAAA", folderA.guid) + + XCTAssertEqual(insertedGUID, folderA.mergedChildren![0].guid) + XCTAssertEqual("bookmarkFFFF", folderC.mergedChildren![0].guid) + + guard let result = merger.produceMergeResultFromMergedTree(mergedTree).value.successValue else { + XCTFail("Couldn't get merge result.") + return + } + + XCTAssertFalse(result.isNoOp) + + // Everything in local is getting dropped. + let formerLocalIDs = Set(edges.local.lookup.keys) + XCTAssertTrue(result.overrideCompletion.processedLocalChanges.isSuperset(of: formerLocalIDs)) + + // Everything in the buffer is getting dropped. + let formerBufferIDs = Set(edges.buffer.lookup.keys) + XCTAssertTrue(result.bufferCompletion.processedBufferChanges.isSuperset(of: formerBufferIDs)) + + // The mirror will now contain everything added by each side, and not the deleted folders. + XCTAssertEqual(result.overrideCompletion.mirrorItemsToDelete, Set(["folderBBBBBB", "folderDDDDDD"])) + XCTAssertTrue(result.overrideCompletion.mirrorValuesToCopyFromLocal.isEmpty) + XCTAssertTrue(result.overrideCompletion.mirrorValuesToCopyFromBuffer.isEmpty) + XCTAssertTrue(result.overrideCompletion.mirrorItemsToInsert.keys.contains(insertedGUID)) // Because it was reparented. + XCTAssertTrue(result.overrideCompletion.mirrorItemsToInsert.keys.contains("bookmarkFFFF")) // Because it was reparented. + + // The mirror now has the right structure. + XCTAssertEqual(result.overrideCompletion.mirrorStructures["folderCCCCCC"]!, ["bookmarkFFFF"]) + XCTAssertEqual(result.overrideCompletion.mirrorStructures["folderAAAAAA"]!, [insertedGUID]) + + // We're going to upload new records for C (lost a child), F (changed parent), + // insertedGUID (new local record), A (new child), and D (deleted). + + // Anything that was in the mirror and only changed structure: + let expected: [GUID: [GUID]] = ["folderCCCCCC": ["bookmarkFFFF"], "folderAAAAAA": [insertedGUID]] + let actual: [GUID: [GUID]] = result.uploadCompletion.amendChildrenFromMirror + XCTAssertEqual(actual as NSDictionary, expected as NSDictionary) + + // Anything deleted: + XCTAssertTrue(result.uploadCompletion.records.contains(where: { (($0.id == "folderDDDDDD") && ($0.payload["deleted"].bool ?? false)) })) + + // Inserted: + let uploadE = result.uploadCompletion.records.find { $0.id == insertedGUID } + XCTAssertNotNil(uploadE) + XCTAssertEqual(uploadE!.payload["title"].stringValue, "E") + + // We fixed the parent before uploading. + XCTAssertEqual(uploadE!.payload["parentid"].stringValue, "folderAAAAAA") + XCTAssertEqual(uploadE!.payload["parentName"].string ?? "", "A") + + // Reparented: + let uploadF = result.uploadCompletion.records.find { $0.id == "bookmarkFFFF" } + XCTAssertNotNil(uploadF) + XCTAssertEqual(uploadF!.payload["title"].stringValue, "F") + + // We fixed the parent before uploading. + XCTAssertEqual(uploadF!.payload["parentid"].stringValue, "folderCCCCCC") + XCTAssertEqual(uploadF!.payload["parentName"].string ?? "", "C") + } + + func testComplexMoveWithAdditions() { + // This test describes a scenario like this: + // + // [] [] [] + // [X] [Y] [X] [Y] [X] [Y] + // | | | | + // [A] C [A] [A] + // / \ / \ / | \ + // B E B C B D C + // + // That is: we have locally added 'D' to folder A, and remotely moved + // A to a different root, added 'E', and moved 'C' back to the old root. + // + // Our expected result is: + // + // [] + // [X] [Y] + // | | + // [A] C + // / | \ + // B D E + // + // … but we'll settle for any order of children for [A] that preserves B < E + // and B < D -- in other words, (B E D) and (B D E) are both acceptable. + // + guard let bookmarks = self.getSyncableBookmarks(name: "F") else { + XCTFail("Couldn't get bookmarks.") + return + } + + // Set up the mirror. + let mirrorDate = Date.now() - 100000 + let records = [ + BookmarkMirrorItem.folder(BookmarkRoots.RootGUID, dateAdded: mirrorDate, modified: mirrorDate, hasDupe: false, parentID: BookmarkRoots.RootGUID, parentName: "", title: "", description: "", children: BookmarkRoots.RootChildren), + BookmarkMirrorItem.folder(BookmarkRoots.MenuFolderGUID, dateAdded: mirrorDate, modified: mirrorDate, hasDupe: false, parentID: BookmarkRoots.RootGUID, parentName: "", title: "Bookmarks Menu", description: "", children: ["folderAAAAAA"]), + BookmarkMirrorItem.folder(BookmarkRoots.UnfiledFolderGUID, dateAdded: mirrorDate, modified: mirrorDate, hasDupe: false, parentID: BookmarkRoots.RootGUID, parentName: "", title: "Unsorted Bookmarks", description: "", children: []), + BookmarkMirrorItem.folder(BookmarkRoots.ToolbarFolderGUID, dateAdded: mirrorDate, modified: mirrorDate, hasDupe: false, parentID: BookmarkRoots.RootGUID, parentName: "", title: "Bookmarks Toolbar", description: "", children: []), + BookmarkMirrorItem.folder(BookmarkRoots.MobileFolderGUID, dateAdded: mirrorDate, modified: mirrorDate, hasDupe: false, parentID: BookmarkRoots.RootGUID, parentName: "", title: "Mobile Bookmarks", description: "", children: []), + BookmarkMirrorItem.folder("folderAAAAAA", dateAdded: mirrorDate, modified: mirrorDate, hasDupe: false, parentID: BookmarkRoots.MenuFolderGUID, parentName: "Bookmarks Menu", title: "A", description: "", children: ["bookmarkBBBB", "bookmarkCCCC"]), + BookmarkMirrorItem.bookmark("bookmarkBBBB", dateAdded: mirrorDate, modified: mirrorDate, hasDupe: false, parentID: "folderAAAAAA", parentName: "A", title: "B", description: nil, URI: "http://example.com/b", tags: "", keyword: nil), + BookmarkMirrorItem.bookmark("bookmarkCCCC", dateAdded: mirrorDate, modified: mirrorDate, hasDupe: false, parentID: "folderAAAAAA", parentName: "A", title: "C", description: nil, URI: "http://example.com/c", tags: "", keyword: nil), + ] + + bookmarks.populateMirrorViaBuffer(items: records, atDate: mirrorDate) + bookmarks.wipeLocal() + + // Now the buffer is empty, and the mirror tree is what we expect. + let mirrorTree = bookmarks.treeForMirror().value.successValue! + XCTAssertFalse(mirrorTree.isEmpty) + + XCTAssertEqual(mirrorTree.lookup.keys.count, 8) + XCTAssertEqual(1, mirrorTree.subtrees.count) + XCTAssertEqual(mirrorTree.find("folderAAAAAA")!.children!.map { $0.recordGUID }, ["bookmarkBBBB", "bookmarkCCCC"]) + + let edgesBefore = bookmarks.treesForEdges().value.successValue! + XCTAssertTrue(edgesBefore.local.isEmpty) // Because we're fully synced. + XCTAssertTrue(edgesBefore.buffer.isEmpty) + + // Set up the buffer. + let bufferDate = Date.now() + let changedBufferRecords = [ + BookmarkMirrorItem.folder(BookmarkRoots.MenuFolderGUID, dateAdded: bufferDate, modified: bufferDate, hasDupe: false, parentID: BookmarkRoots.RootGUID, parentName: "", title: "Bookmarks Menu", description: "", children: ["bookmarkCCCC"]), + BookmarkMirrorItem.folder(BookmarkRoots.ToolbarFolderGUID, dateAdded: bufferDate, modified: bufferDate, hasDupe: false, parentID: BookmarkRoots.RootGUID, parentName: "", title: "Bookmarks Toolbar", description: "", children: ["folderAAAAAA"]), + BookmarkMirrorItem.folder("folderAAAAAA", dateAdded: bufferDate, modified: bufferDate, hasDupe: false, parentID: BookmarkRoots.ToolbarFolderGUID, parentName: "Bookmarks Toolbar", title: "A", description: "", children: ["bookmarkBBBB", "bookmarkEEEE"]), + BookmarkMirrorItem.bookmark("bookmarkCCCC", dateAdded: bufferDate, modified: bufferDate, hasDupe: false, parentID: BookmarkRoots.MenuFolderGUID, parentName: "A", title: "C", description: nil, URI: "http://example.com/c", tags: "", keyword: nil), + BookmarkMirrorItem.bookmark("bookmarkEEEE", dateAdded: bufferDate, modified: bufferDate, hasDupe: false, parentID: "folderAAAAAA", parentName: "A", title: "E", description: nil, URI: "http://example.com/e", tags: "", keyword: nil), + ] + + bookmarks.applyRecords(changedBufferRecords).succeeded() + let populatedBufferTree = bookmarks.treesForEdges().value.successValue!.buffer + XCTAssertFalse(populatedBufferTree.isEmpty) + XCTAssertTrue(populatedBufferTree.isFullyRootedIn(mirrorTree)) + XCTAssertFalse(populatedBufferTree.find("bookmarkEEEE")!.isUnknown) + XCTAssertFalse(populatedBufferTree.find("bookmarkCCCC")!.isUnknown) + XCTAssertTrue(populatedBufferTree.find("bookmarkBBBB")!.isUnknown) + + // Now let's make local changes with the API. + bookmarks.local.insertBookmark("http://example.com/d".asURL!, title: "D (local)", favicon: nil, intoFolder: "folderAAAAAA", withTitle: "A").succeeded() + + let populatedLocalTree = bookmarks.treesForEdges().value.successValue!.local + let newMirrorTree = bookmarks.treeForMirror().value.successValue! + XCTAssertEqual(1, newMirrorTree.subtrees.count) + + XCTAssertFalse(populatedLocalTree.isEmpty) + XCTAssertTrue(populatedLocalTree.isFullyRootedIn(mirrorTree)) + XCTAssertTrue(populatedLocalTree.isFullyRootedIn(newMirrorTree)) // It changed. + XCTAssertNil(populatedLocalTree.find("bookmarkEEEE")) + XCTAssertTrue(populatedLocalTree.find("bookmarkCCCC")!.isUnknown) + XCTAssertTrue(populatedLocalTree.find("bookmarkBBBB")!.isUnknown) + XCTAssertFalse(populatedLocalTree.find("folderAAAAAA")!.isUnknown) + + // Now merge. + let storageMerger = ThreeWayBookmarksStorageMerger(buffer: bookmarks, storage: bookmarks) + let merger = storageMerger.getMerger().value.successValue! + guard let mergedTree = merger.produceMergedTree().value.successValue else { + XCTFail("Couldn't get merge result.") + return + } + + // Dump it so we can see it. + mergedTree.dump() + + guard let menu = mergedTree.root.mergedChildren?[0] else { + XCTFail("Expected a child of the root.") + return + } + + XCTAssertEqual(menu.guid, BookmarkRoots.MenuFolderGUID) + XCTAssertEqual(menu.mergedChildren?[0].guid, "bookmarkCCCC") + + guard let toolbar = mergedTree.root.mergedChildren?[1] else { + XCTFail("Expected a second child of the root.") + return + } + + XCTAssertEqual(toolbar.guid, BookmarkRoots.ToolbarFolderGUID) + let toolbarChildren = toolbar.mergedChildren! + XCTAssertEqual(toolbarChildren.count, 1) // A. + let aaa = toolbarChildren[0] + XCTAssertEqual(aaa.guid, "folderAAAAAA") + let aaaChildren = aaa.mergedChildren! + + XCTAssertEqual(aaaChildren.count, 3) // B, E, new local. + XCTAssertFalse(aaaChildren.contains { $0.guid == "bookmarkCCCC" }) + } + + func testApplyingTwoEmptyFoldersDoesntSmush() { + guard let bookmarks = self.getSyncableBookmarks(name: "C") else { + XCTFail("Couldn't get bookmarks.") + return + } + + // Insert two identical folders. We mark them with hasDupe because that's the Syncy + // thing to do. + let now = Date.now() + let records = [ + BookmarkMirrorItem.folder(BookmarkRoots.MobileFolderGUID, dateAdded: now, modified: now, hasDupe: false, parentID: BookmarkRoots.RootGUID, parentName: "", title: "Mobile Bookmarks", description: "", children: ["emptyempty01", "emptyempty02"]), + BookmarkMirrorItem.folder("emptyempty01", dateAdded: now, modified: now, hasDupe: true, parentID: BookmarkRoots.MobileFolderGUID, parentName: "Mobile Bookmarks", title: "Empty", description: "", children: []), + BookmarkMirrorItem.folder("emptyempty02", dateAdded: now, modified: now, hasDupe: true, parentID: BookmarkRoots.MobileFolderGUID, parentName: "Mobile Bookmarks", title: "Empty", description: "", children: []), + ] + + bookmarks.buffer.applyRecords(records).succeeded() + + bookmarks.buffer.db.assertQueryReturns("SELECT COUNT(*) FROM \(TableBookmarksBuffer)", int: 3) + bookmarks.buffer.db.assertQueryReturns("SELECT COUNT(*) FROM \(TableBookmarksBufferStructure)", int: 2) + + let storageMerger = ThreeWayBookmarksStorageMerger(buffer: bookmarks, storage: bookmarks) + let merger = storageMerger.getMerger().value.successValue! + guard let mergedTree = merger.produceMergedTree().value.successValue else { + XCTFail("Couldn't get merge result.") + return + } + + // Dump it so we can see it. + mergedTree.dump() + + // Now let's look at the tree. + XCTAssertTrue(mergedTree.deleteFromMirror.isEmpty) + XCTAssertEqual(BookmarkRoots.RootGUID, mergedTree.root.guid) + XCTAssertEqual(BookmarkRoots.RootGUID, mergedTree.root.mirror?.recordGUID) + XCTAssertNil(mergedTree.root.remote) + + XCTAssertTrue(MergeState.unchanged == mergedTree.root.valueState) + XCTAssertTrue(MergeState.unchanged == mergedTree.root.structureState) + XCTAssertEqual(4, mergedTree.root.mergedChildren?.count) + + guard let mergedMobile = mergedTree.root.mergedChildren?[BookmarkRoots.RootChildren.index(of: BookmarkRoots.MobileFolderGUID) ?? -1] else { + XCTFail("Didn't get a merged mobile folder.") + return + } + + XCTAssertEqual(BookmarkRoots.MobileFolderGUID, mergedMobile.guid) + XCTAssertTrue(MergeState.remote == mergedMobile.valueState) + + // This ends up as Remote because we didn't change any of its structure + // when compared to the incoming record. + guard case MergeState.remote = mergedMobile.structureState else { + XCTFail("Didn't get expected Remote state.") + return + } + + XCTAssertEqual(["emptyempty01", "emptyempty02"], mergedMobile.remote!.children!.map { $0.recordGUID }) + XCTAssertEqual(["emptyempty01", "emptyempty02"], mergedMobile.asMergedTreeNode().children!.map { $0.recordGUID }) + XCTAssertEqual(["emptyempty01", "emptyempty02"], mergedMobile.mergedChildren!.map { $0.guid }) + let empty01 = mergedMobile.mergedChildren![0] + let empty02 = mergedMobile.mergedChildren![1] + XCTAssertNil(empty01.local) + XCTAssertNil(empty02.local) + XCTAssertNil(empty01.mirror) + XCTAssertNil(empty02.mirror) + XCTAssertNotNil(empty01.remote) + XCTAssertNotNil(empty02.remote) + XCTAssertEqual("emptyempty01", empty01.remote!.recordGUID) + XCTAssertEqual("emptyempty02", empty02.remote!.recordGUID) + XCTAssertTrue(MergeState.remote == empty01.structureState) + XCTAssertTrue(MergeState.remote == empty02.structureState) + XCTAssertTrue(MergeState.remote == empty01.valueState) + XCTAssertTrue(MergeState.remote == empty02.valueState) + XCTAssertTrue(empty01.mergedChildren?.isEmpty ?? false) + XCTAssertTrue(empty02.mergedChildren?.isEmpty ?? false) + + guard let result = merger.produceMergeResultFromMergedTree(mergedTree).value.successValue else { + XCTFail("Couldn't get merge result.") + return + } + + let uploader = MockUploader() + let storer = uploader.getStorer() + let applier = MergeApplier(buffer: bookmarks, storage: bookmarks, client: storer, statsSession: mockStatsSessionForBookmarks(), greenLight: { true }) + applier.applyResult(result).succeeded() + + guard let mirror = bookmarks.treeForMirror().value.successValue else { + XCTFail("Couldn't get mirror!") + return + } + + // After merge, the buffer and local are empty. + let edgesAfter = bookmarks.treesForEdges().value.successValue! + XCTAssertTrue(edgesAfter.local.isEmpty) + XCTAssertTrue(edgesAfter.buffer.isEmpty) + + // When merged in, we do not smush these two records together! + XCTAssertFalse(mirror.isEmpty) + XCTAssertTrue(mirror.subtrees[0].recordGUID == BookmarkRoots.RootGUID) + XCTAssertNotNil(mirror.find("emptyempty01")) + XCTAssertNotNil(mirror.find("emptyempty02")) + XCTAssertTrue(mirror.deleted.isEmpty) + guard let mobile = mirror.find(BookmarkRoots.MobileFolderGUID) else { + XCTFail("No mobile folder in mirror.") + return + } + + if case let .folder(_, children) = mobile { + XCTAssertEqual(children.map { $0.recordGUID }, ["emptyempty01", "emptyempty02"]) + } else { + XCTFail("Mobile isn't a folder.") + } + } + + /** + * Input: + * + * [M] [] [M] + * _______|_______ _____|_____ + * / | \ / \ + * [e01] [e02] [e03] [e02] [eL0] + * + * Expected output: + * + * Take remote, delete emptyemptyL0, upload the roots that + * weren't remotely present. + */ + func testApplyingTwoEmptyFoldersMatchesOnlyOne() { + guard let bookmarks = self.getSyncableBookmarks(name: "D") else { + XCTFail("Couldn't get bookmarks.") + return + } + + // Insert three identical folders. We mark them with hasDupe because that's the Syncy + // thing to do. + let now = Date.now() + let records = [ + BookmarkMirrorItem.folder(BookmarkRoots.MobileFolderGUID, dateAdded: now, modified: now, hasDupe: false, parentID: BookmarkRoots.RootGUID, parentName: "", title: "Mobile Bookmarks", description: "", children: ["emptyempty01", "emptyempty02", "emptyempty03"]), + BookmarkMirrorItem.folder("emptyempty01", dateAdded: now, modified: now, hasDupe: true, parentID: BookmarkRoots.MobileFolderGUID, parentName: "Mobile Bookmarks", title: "Empty", description: "", children: []), + BookmarkMirrorItem.folder("emptyempty02", dateAdded: now, modified: now, hasDupe: true, parentID: BookmarkRoots.MobileFolderGUID, parentName: "Mobile Bookmarks", title: "Empty", description: "", children: []), + BookmarkMirrorItem.folder("emptyempty03", dateAdded: now, modified: now, hasDupe: true, parentID: BookmarkRoots.MobileFolderGUID, parentName: "Mobile Bookmarks", title: "Empty", description: "", children: []), + ] + + bookmarks.buffer.validate().succeeded() // It's valid! Empty. + bookmarks.buffer.applyRecords(records).succeeded() + bookmarks.buffer.validate().succeeded() // It's valid! Rooted in mobile_______. + + bookmarks.buffer.db.assertQueryReturns("SELECT COUNT(*) FROM \(TableBookmarksBuffer)", int: 4) + bookmarks.buffer.db.assertQueryReturns("SELECT COUNT(*) FROM \(TableBookmarksBufferStructure)", int: 3) + + // Add one matching empty folder locally. + // Add one by GUID, too. This is the most complex possible case. + bookmarks.local.db.run("INSERT INTO \(TableBookmarksLocal) (guid, type, date_added, title, parentid, parentName, sync_status, local_modified) VALUES ('emptyempty02', \(BookmarkNodeType.folder.rawValue), \(now), 'Empty', '\(BookmarkRoots.MobileFolderGUID)', 'Mobile Bookmarks', \(SyncStatus.changed.rawValue), \(Date.now()))").succeeded() + bookmarks.local.db.run("INSERT INTO \(TableBookmarksLocal) (guid, type, date_added, title, parentid, parentName, sync_status, local_modified) VALUES ('emptyemptyL0', \(BookmarkNodeType.folder.rawValue), \(now), 'Empty', '\(BookmarkRoots.MobileFolderGUID)', 'Mobile Bookmarks', \(SyncStatus.new.rawValue), \(Date.now()))").succeeded() + bookmarks.local.db.run("INSERT INTO \(TableBookmarksLocalStructure) (parent, child, idx) VALUES ('\(BookmarkRoots.MobileFolderGUID)', 'emptyempty02', 0)").succeeded() + bookmarks.local.db.run("INSERT INTO \(TableBookmarksLocalStructure) (parent, child, idx) VALUES ('\(BookmarkRoots.MobileFolderGUID)', 'emptyemptyL0', 1)").succeeded() + + let uploader = MockUploader() + let storer = uploader.getStorer() + let applier = MergeApplier(buffer: bookmarks, storage: bookmarks, client: storer, statsSession: mockStatsSessionForBookmarks(), greenLight: { true }) + applier.go().succeeded() + + guard let mirror = bookmarks.treeForMirror().value.successValue else { + XCTFail("Couldn't get mirror!") + return + } + + // After merge, the buffer and local are empty. + let edgesAfter = bookmarks.treesForEdges().value.successValue! + + XCTAssertTrue(edgesAfter.local.isEmpty) + XCTAssertTrue(edgesAfter.buffer.isEmpty) + + // All of the incoming records exist. + XCTAssertFalse(mirror.isEmpty) + XCTAssertEqual(mirror.subtrees[0].recordGUID, BookmarkRoots.RootGUID) + XCTAssertNotNil(mirror.find("emptyempty01")) + XCTAssertNotNil(mirror.find("emptyempty02")) + XCTAssertNotNil(mirror.find("emptyempty03")) + + // The other roots are now in the mirror. + XCTAssertTrue(BookmarkRoots.Real.every({ mirror.find($0) != nil })) + + // And are queued for upload. + XCTAssertTrue(uploader.added.contains("toolbar")) + XCTAssertTrue(uploader.added.contains("menu")) + XCTAssertTrue(uploader.added.contains("unfiled")) + XCTAssertFalse(uploader.added.contains("mobile")) // Structure and value didn't change. + + // The local record that was smushed is not present… + XCTAssertNil(mirror.find("emptyemptyL0")) + + // … and because it was marked New, we don't bother trying to delete it. + XCTAssertFalse(uploader.deletions.contains("emptyemptyL0")) + + guard let mobile = mirror.find(BookmarkRoots.MobileFolderGUID) else { + XCTFail("No mobile folder in mirror.") + return + } + + if case let .folder(_, children) = mobile { + // This order isn't strictly specified, but try to preserve the remote order if we can. + XCTAssertEqual(children.map { $0.recordGUID }, ["emptyempty01", "emptyempty02", "emptyempty03"]) + } else { + XCTFail("Mobile isn't a folder.") + } + } + + // TODO: this test should be extended to also exercise the case of a conflict. + func testLocalRecordsKeepTheirFavicon() { + guard let bookmarks = self.getSyncableBookmarks(name: "E") else { + XCTFail("Couldn't get bookmarks.") + return + } + + bookmarks.buffer.db.assertQueryReturns("SELECT COUNT(*) FROM \(TableBookmarksBuffer)", int: 0) + bookmarks.buffer.db.assertQueryReturns("SELECT COUNT(*) FROM \(TableBookmarksBufferStructure)", int: 0) + bookmarks.buffer.db.assertQueryReturns("SELECT COUNT(*) FROM \(TableBookmarksLocal)", int: 5) + bookmarks.buffer.db.assertQueryReturns("SELECT COUNT(*) FROM \(TableBookmarksLocalStructure)", int: 4) + + bookmarks.local.db.run("INSERT INTO \(TableFavicons) (id, url, width, height, type, date) VALUES (11, 'http://example.org/favicon.ico', 16, 16, 0, \(Date.now()))").succeeded() + bookmarks.local.db.run("INSERT INTO \(TableBookmarksLocal) (guid, type, date_added, title, parentid, parentName, sync_status, bmkUri, faviconID) VALUES ('somebookmark', \(BookmarkNodeType.bookmark.rawValue), \(Date.now()), 'Some Bookmark', '\(BookmarkRoots.MobileFolderGUID)', 'Mobile Bookmarks', \(SyncStatus.new.rawValue), 'http://example.org/', 11)").succeeded() + bookmarks.local.db.run("INSERT INTO \(TableBookmarksLocalStructure) (parent, child, idx) VALUES ('\(BookmarkRoots.MobileFolderGUID)', 'somebookmark', 0)").succeeded() + + let uploader = MockUploader() + let storer = uploader.getStorer() + let applier = MergeApplier(buffer: bookmarks, storage: bookmarks, client: storer, statsSession: mockStatsSessionForBookmarks(), greenLight: { true }) + applier.go().succeeded() + + // After merge, the buffer and local are empty. + let edgesAfter = bookmarks.treesForEdges().value.successValue! + XCTAssertTrue(edgesAfter.local.isEmpty) + XCTAssertTrue(edgesAfter.buffer.isEmpty) + + // New record was uploaded. + XCTAssertTrue(uploader.added.contains("somebookmark")) + + // So were all the roots, with the Sync-native names. + XCTAssertTrue(uploader.added.contains("toolbar")) + XCTAssertTrue(uploader.added.contains("menu")) + XCTAssertTrue(uploader.added.contains("unfiled")) + XCTAssertTrue(uploader.added.contains("mobile")) + + // … but not the Places root. + XCTAssertFalse(uploader.added.contains("places")) + XCTAssertFalse(uploader.added.contains(BookmarkRoots.RootGUID)) + + // Their parent IDs are translated. + XCTAssertEqual(uploader.records["mobile"]?.payload["parentid"].string, "places") + XCTAssertEqual(uploader.records["somebookmark"]?.payload["parentid"].string, "mobile") + + XCTAssertTrue(uploader.deletions.isEmpty) + + // The record looks sane. + let bm = uploader.records["somebookmark"]! + XCTAssertEqual("bookmark", bm.payload["type"].string) + XCTAssertEqual("Some Bookmark", bm.payload["title"].string) + + // New record still has its icon ID in the local DB. + bookmarks.local.db.assertQueryReturns("SELECT faviconID FROM \(TableBookmarksMirror) WHERE bmkUri = 'http://example.org/'", int: 11) + } + + func testRemoteValueOnlyChange() { + guard let bookmarks = self.getSyncableBookmarks(name: "F") else { + XCTFail("Couldn't get bookmarks.") + return + } + + // Insert one folder and one child. + let now = Date.now() + let records = [ + BookmarkMirrorItem.folder(BookmarkRoots.UnfiledFolderGUID, dateAdded: Date.now(), modified: now, hasDupe: false, parentID: BookmarkRoots.RootGUID, parentName: "", title: "Unsorted Bookmarks", description: "", children: ["folderAAAAAA"]), + BookmarkMirrorItem.folder("folderAAAAAA", dateAdded: now, modified: now, hasDupe: false, parentID: BookmarkRoots.UnfiledFolderGUID, parentName: "Unsorted Bookmarks", title: "Folder A", description: "", children: ["bookmarkBBBB"]), + BookmarkMirrorItem.bookmark("bookmarkBBBB", dateAdded: now, modified: now, hasDupe: false, parentID: "folderAAAAAA", parentName: "Folder A", title: "Initial Title", description: "No desc.", URI: "http://example.org/foo", tags: "", keyword: nil), + ] + + bookmarks.buffer.validate().succeeded() // It's valid! Empty. + bookmarks.buffer.applyRecords(records).succeeded() + bookmarks.buffer.validate().succeeded() // It's valid! Rooted in mobile_______. + + bookmarks.buffer.db.assertQueryReturns("SELECT COUNT(*) FROM \(TableBookmarksBuffer)", int: 3) + bookmarks.buffer.db.assertQueryReturns("SELECT COUNT(*) FROM \(TableBookmarksBufferStructure)", int: 2) + + let uploader = MockUploader() + let storer = uploader.getStorer() + let applier = MergeApplier(buffer: bookmarks, storage: bookmarks, client: storer, statsSession: mockStatsSessionForBookmarks(), greenLight: { true }) + applier.go().succeeded() + + guard let _ = bookmarks.treeForMirror().value.successValue else { + XCTFail("Couldn't get mirror!") + return + } + + // After merge, the buffer and local are empty. + let edgesAfter = bookmarks.treesForEdges().value.successValue! + + XCTAssertTrue(edgesAfter.local.isEmpty) + XCTAssertTrue(edgesAfter.buffer.isEmpty) + + // Check the title. + let folder = bookmarks.modelFactory.value.successValue! + .modelForFolder("folderAAAAAA").value.successValue!.current[0]! + XCTAssertEqual(folder.title, "Initial Title") + + // Now process an incoming change. + let changed = [ + BookmarkMirrorItem.bookmark("bookmarkBBBB", dateAdded: now, modified: now, hasDupe: false, parentID: "folderAAAAAA", parentName: "Folder A", title: "New Title", description: "No desc.", URI: "http://example.org/foo", tags: "", keyword: nil), + ] + + bookmarks.buffer.validate().succeeded() // It's valid! Empty. + bookmarks.buffer.applyRecords(changed).succeeded() + bookmarks.buffer.validate().succeeded() // It's valid! One record. + + bookmarks.buffer.db.assertQueryReturns("SELECT COUNT(*) FROM \(TableBookmarksBuffer)", int: 1) + bookmarks.buffer.db.assertQueryReturns("SELECT COUNT(*) FROM \(TableBookmarksBufferStructure)", int: 0) + + let uu = MockUploader() + let ss = uu.getStorer() + let changeApplier = MergeApplier(buffer: bookmarks, storage: bookmarks, client: ss, statsSession: mockStatsSessionForBookmarks(), greenLight: { true }) + changeApplier.go().succeeded() + + // The title changed. + let updatedFolder = bookmarks.modelFactory.value.successValue! + .modelForFolder("folderAAAAAA").value.successValue!.current[0]! + XCTAssertEqual(updatedFolder.title, "New Title") + + // The buffer is empty. + bookmarks.buffer.db.assertQueryReturns("SELECT COUNT(*) FROM \(TableBookmarksBuffer)", int: 0) + bookmarks.buffer.db.assertQueryReturns("SELECT COUNT(*) FROM \(TableBookmarksBufferStructure)", int: 0) + } + + func testMergeDateAdded() { + guard let bookmarks = self.getSyncableBookmarks(name: "H") else { + XCTFail("Couldn't get bookmarks.") + return + } + + // Set up the mirror. + let mirrorDate = Date.now() - 100000 + let records = [ + BookmarkMirrorItem.folder(BookmarkRoots.RootGUID, dateAdded: mirrorDate, modified: mirrorDate, hasDupe: false, parentID: BookmarkRoots.RootGUID, parentName: "", title: "", description: "", children: BookmarkRoots.RootChildren), + BookmarkMirrorItem.folder(BookmarkRoots.MenuFolderGUID, dateAdded: mirrorDate, modified: mirrorDate, hasDupe: false, parentID: BookmarkRoots.RootGUID, parentName: "", title: "Bookmarks Menu", description: "", children: []), + BookmarkMirrorItem.folder(BookmarkRoots.UnfiledFolderGUID, dateAdded: mirrorDate, modified: mirrorDate, hasDupe: false, parentID: BookmarkRoots.RootGUID, parentName: "", title: "Unsorted Bookmarks", description: "", children: []), + BookmarkMirrorItem.folder(BookmarkRoots.ToolbarFolderGUID, dateAdded: mirrorDate, modified: mirrorDate, hasDupe: false, parentID: BookmarkRoots.RootGUID, parentName: "", title: "Bookmarks Toolbar", description: "", children: ["folderAAAAAA"]), + BookmarkMirrorItem.folder(BookmarkRoots.MobileFolderGUID, dateAdded: mirrorDate, modified: mirrorDate, hasDupe: false, parentID: BookmarkRoots.RootGUID, parentName: "", title: "Mobile Bookmarks", description: "", children: []), + BookmarkMirrorItem.folder("folderAAAAAA", dateAdded: mirrorDate, modified: mirrorDate, hasDupe: false, parentID: BookmarkRoots.ToolbarFolderGUID, parentName: "Bookmarks Toolbar", title: "A", description: "", children: []), + ] + + bookmarks.populateMirrorViaBuffer(items: records, atDate: mirrorDate) + bookmarks.wipeLocal() + + // Now the buffer is empty, and the mirror tree is what we expect. + let mirrorTree = bookmarks.treeForMirror().value.successValue! + XCTAssertFalse(mirrorTree.isEmpty) + + XCTAssertEqual(mirrorTree.lookup.keys.count, 6) + XCTAssertEqual(1, mirrorTree.subtrees.count) + + let edgesBefore = bookmarks.treesForEdges().value.successValue! + XCTAssertTrue(edgesBefore.local.isEmpty) // Because we're fully synced. + XCTAssertTrue(edgesBefore.buffer.isEmpty) + + // Make local changes. + bookmarks.local.insertBookmark("http://example.com/f".asURL!, title: "F", favicon: nil, intoFolder: "folderAAAAAA", withTitle: "A").succeeded() + let insertedGUID = bookmarks.local.db.getGUIDs("SELECT guid FROM \(TableBookmarksLocal) WHERE title IS 'F'")[0] + bookmarks.local.insertBookmark("http://example.com/g".asURL!, title: "G", favicon: nil, intoFolder: "folderAAAAAA", withTitle: "A").succeeded() + let insertedGUID2 = bookmarks.local.db.getGUIDs("SELECT guid FROM \(TableBookmarksLocal) WHERE title IS 'G'")[0] + + // Set up the buffer. + let bufferDate = Date.now() - 100000 + let dateAfter = Date.now() + let changedBufferRecords = [ + BookmarkMirrorItem.folder("folderAAAAAA", dateAdded: mirrorDate, modified: mirrorDate, hasDupe: false, parentID: BookmarkRoots.ToolbarFolderGUID, parentName: "Bookmarks Toolbar", title: "A", description: "", children: [insertedGUID, insertedGUID2]), + BookmarkMirrorItem.bookmark(insertedGUID, dateAdded: bufferDate, modified: bufferDate, hasDupe: false, parentID: "folderAAAAAA", parentName: "A", title: "F", description: nil, URI: "http://example.com/f", tags: "", keyword: nil), + BookmarkMirrorItem.bookmark(insertedGUID2, dateAdded: dateAfter, modified: bufferDate, hasDupe: false, parentID: "folderAAAAAA", parentName: "A", title: "G", description: nil, URI: "http://example.com/g", tags: "", keyword: nil), + ] + + bookmarks.applyRecords(changedBufferRecords).succeeded() + + // Now merge. + let storageMerger = ThreeWayBookmarksStorageMerger(buffer: bookmarks, storage: bookmarks) + let merger = storageMerger.getMerger().value.successValue! + guard let mergedTree = merger.produceMergedTree().value.successValue else { + XCTFail("Couldn't get merge result.") + return + } + + mergedTree.dump() + + let folderChildren = mergedTree.root.mergedChildren!.find{ $0.guid == BookmarkRoots.ToolbarFolderGUID }!.mergedChildren!.find { $0.guid == "folderAAAAAA" }!.mergedChildren! + + if case .new(let value) = folderChildren[0].valueState { + XCTAssertEqual(value.dateAdded, bufferDate) + } else { + XCTFail() + } + + guard case .local = folderChildren[1].valueState else { + XCTFail() + return + } + } +} + +class TestMergedTree: FailFastTestCase { + func testInitialState() { + let children = BookmarkRoots.RootChildren.map { BookmarkTreeNode.unknown(guid: $0) } + let root = BookmarkTreeNode.folder(guid: BookmarkRoots.RootGUID, children: children) + let tree = MergedTree(mirrorRoot: root) + XCTAssertTrue(tree.root.hasDecidedChildren) + + if case let .folder(guid, unmergedChildren) = tree.root.asUnmergedTreeNode() { + XCTAssertEqual(guid, BookmarkRoots.RootGUID) + XCTAssertEqual(unmergedChildren, children) + } else { + XCTFail("Root should start as Folder.") + } + + // We haven't processed the children. + XCTAssertNil(tree.root.mergedChildren) + XCTAssertTrue(tree.root.asMergedTreeNode().isUnknown) + + // Simulate a merge. + let mergedRoots = children.map { MergedTreeNode(guid: $0.recordGUID, mirror: $0, structureState: MergeState.unchanged) } + tree.root.mergedChildren = mergedRoots + + // Now we have processed children. + XCTAssertNotNil(tree.root.mergedChildren) + XCTAssertFalse(tree.root.asMergedTreeNode().isUnknown) + } +} + +private extension MergedSQLiteBookmarks { + func populateMirrorViaBuffer(items: [BookmarkMirrorItem], atDate mirrorDate: Timestamp) { + self.applyRecords(items).succeeded() + + // … and add the root relationships that will be missing (we don't do those for the buffer, + // so we need to manually add them and move them across). + self.buffer.db.run([ + "INSERT INTO \(TableBookmarksBufferStructure) (parent, child, idx) VALUES", + "('\(BookmarkRoots.RootGUID)', '\(BookmarkRoots.MenuFolderGUID)', 0),", + "('\(BookmarkRoots.RootGUID)', '\(BookmarkRoots.ToolbarFolderGUID)', 1),", + "('\(BookmarkRoots.RootGUID)', '\(BookmarkRoots.UnfiledFolderGUID)', 2),", + "('\(BookmarkRoots.RootGUID)', '\(BookmarkRoots.MobileFolderGUID)', 3)", + ].joined(separator: " ")).succeeded() + + // Move it all to the mirror. + self.local.db.moveBufferToMirrorForTesting() + } + + func wipeLocal() { + self.local.db.run(["DELETE FROM \(TableBookmarksLocalStructure)", "DELETE FROM \(TableBookmarksLocal)"]).succeeded() + } +} diff --git a/mobile/ios/SyncTests/TestBookmarksRepairRequestor.swift b/mobile/ios/SyncTests/TestBookmarksRepairRequestor.swift new file mode 100644 index 0000000000..8fe96b48da --- /dev/null +++ b/mobile/ios/SyncTests/TestBookmarksRepairRequestor.swift @@ -0,0 +1,519 @@ +/* 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 Deferred +import Foundation +import Shared +@testable import Storage +@testable import Sync +@testable import SyncTelemetry +import SwiftyJSON + +import XCTest + +class TestBookmarksRepairRequestor: XCTestCase { + private let MockHashedDeviceID = "4b66918e184c0a9a49c4a9dc7468d3495642141a08419e69c6cb107367366176" + + private func buildMockScratchpad(prefs: Prefs) -> Scratchpad { + var scratchpad = Scratchpad(b: KeyBundle.random(), persistingTo: prefs) + let b = Scratchpad.Builder(p: scratchpad) + b.hashedUID = "1234" + scratchpad = b.build() + + XCTAssertEqual(scratchpad.fxaDeviceId, "unknown_fxaDeviceId") + XCTAssertEqual(scratchpad.hashedDeviceID!, MockHashedDeviceID) + return scratchpad + } + + func testNoClients() { + let expectation = self.expectation(description: #function) + + let prefs = MockProfilePrefs() + let scratchpad = buildMockScratchpad(prefs: prefs) + let localClient = RemoteClient(guid: nil, name: "Test local client", modified: (Date.now() - OneMinuteInMilliseconds), type: "mobile", formfactor: "largetablet", os: "iOS", version: nil, fxaDeviceId: nil) + let remoteClients = MockRemoteClientsAndTabs([ClientAndTabs(client: localClient, tabs: [])]) + let validationInfo = [BufferInconsistency.missingValues: ["mock-guid1", "mock-guid2"]] + + let requestor = BookmarksRepairRequestor(scratchpad: scratchpad, basePrefs: prefs, remoteClients: remoteClients) + requestor.startRepairs(validationInfo: validationInfo) >>== { result in + XCTAssertTrue(result) + XCTAssertEqual(remoteClients.commands.count, 1) + XCTAssertEqual(remoteClients.commands["localID"]!.count, 0) + expectation.fulfill() + } + waitForExpectations(timeout: 10, handler: nil) + } + + func testOneClientNoResponse() { + let expectation = self.expectation(description: #function) + + let prefs = MockProfilePrefs() + let scratchpad = buildMockScratchpad(prefs: prefs) + + let localClient = RemoteClient(guid: nil, name: "Test local client", modified: (Date.now() - OneMinuteInMilliseconds), type: "mobile", formfactor: "largetablet", os: "iOS", version: nil, fxaDeviceId: nil) + let remoteClient = RemoteClient(guid: "client-a", name: "Test remote client", modified: (Date.now() - OneMinuteInMilliseconds), type: "desktop", formfactor: nil, os: nil, version: "55.0.1", fxaDeviceId: nil) + let remoteClients = MockRemoteClientsAndTabs([ClientAndTabs(client: localClient, tabs: []), ClientAndTabs(client: remoteClient, tabs: [])]) + let validationInfo = [BufferInconsistency.missingValues: ["mock-guid1", "mock-guid2"]] + + let mockFlowID = Bytes.generateGUID() + + // Mock telemetry events + let startedEvent = makeRepairEvent(["started", nil, ["flowID": mockFlowID, "numIDs": "2"]]) + let uploadEvent = makeRepairEvent(["request", "upload", ["flowID": mockFlowID, "deviceID": MockHashedDeviceID, "numIDs": "2"]]) + let finishedEvent = makeRepairEvent(["finished", nil, ["flowID": mockFlowID, "numIDs": "2"]]) + + let requestor = BookmarksRepairRequestor(scratchpad: scratchpad, basePrefs: prefs, remoteClients: remoteClients) + requestor.startRepairs(validationInfo: validationInfo, flowID: mockFlowID) >>== { result -> Deferred> in + XCTAssertTrue(result) + XCTAssertEqual(prefs.stringForKey("repairs.bookmark.state"), "repair.sent") + + checkRecordedEvents(fromPrefs: prefs, expected: startedEvent, uploadEvent) + checkOutgoingCommand(remoteClients: remoteClients, clientID: "client-a") + // asking it to continue stays in that state until we timeout or the command + // is removed. + return requestor.continueRepairs() + } >>== { result -> Deferred> in + XCTAssertTrue(result) + XCTAssertEqual(prefs.stringForKey("repairs.bookmark.state"), "repair.sent") + checkRecordedEvents(fromPrefs: prefs, expected: startedEvent, uploadEvent) + + // now pretend that client synced. + let _ = remoteClients.deleteCommands("client-a") + return requestor.continueRepairs() + } >>== { result -> Deferred> in + XCTAssertTrue(result) + XCTAssertEqual(prefs.stringForKey("repairs.bookmark.state"), "repair.sent-again") + checkRecordedEvents(fromPrefs: prefs, expected: startedEvent, uploadEvent, uploadEvent) + + // the command should be outgoing again. + checkOutgoingCommand(remoteClients: remoteClients, clientID: "client-a") + + // pretend that client synced again without writing a command. + let _ = remoteClients.deleteCommands("client-a") + return requestor.continueRepairs() + } >>== { result in + XCTAssertTrue(result) + XCTAssertEqual(prefs.stringForKey("repairs.bookmark.state"), nil) + checkRecordedEvents(fromPrefs: prefs, expected: startedEvent, uploadEvent, uploadEvent, finishedEvent) + + expectation.fulfill() + } + waitForExpectations(timeout: 10, handler: nil) + } + + func testOneClientTimeout() { + let expectation = self.expectation(description: #function) + + let prefs = MockProfilePrefs() + let scratchpad = buildMockScratchpad(prefs: prefs) + let localClient = RemoteClient(guid: nil, name: "Test local client", modified: (Date.now() - OneMinuteInMilliseconds), type: "mobile", formfactor: "largetablet", os: "iOS", version: nil, fxaDeviceId: nil) + let remoteClient = RemoteClient(guid: "client-a", name: "Test remote client", modified: (Date.now() - OneMinuteInMilliseconds), type: "desktop", formfactor: nil, os: nil, version: "55.0.1", fxaDeviceId: nil) + let remoteClients = MockRemoteClientsAndTabs([ClientAndTabs(client: localClient, tabs: []), ClientAndTabs(client: remoteClient, tabs: [])]) + let validationInfo = [BufferInconsistency.missingValues: ["mock-guid1", "mock-guid2"]] + + // Mock telemetry events + let mockFlowID = Bytes.generateGUID() + let startedEvent = makeRepairEvent(["started", nil, ["flowID": mockFlowID, "numIDs": "2"]]) + let uploadEvent = makeRepairEvent(["request", "upload", ["flowID": mockFlowID, "deviceID": MockHashedDeviceID, "numIDs": "2"]]) + let abandonEvent = makeRepairEvent(["abandon", "silent", ["flowID": mockFlowID, "deviceID": MockHashedDeviceID]]) + let finishedEvent = makeRepairEvent(["finished", nil, ["flowID": mockFlowID, "numIDs": "2"]]) + + let requestor = BookmarksRepairRequestor(scratchpad: scratchpad, basePrefs: prefs, remoteClients: remoteClients) + requestor.startRepairs(validationInfo: validationInfo, flowID: mockFlowID) >>== { result -> Deferred> in + XCTAssertTrue(result) + XCTAssertEqual(prefs.stringForKey("repairs.bookmark.state"), "repair.sent") + checkRecordedEvents(fromPrefs: prefs, expected: startedEvent, uploadEvent) + checkOutgoingCommand(remoteClients: remoteClients, clientID: "client-a") + // pretend we are now in the future (well actually, that the request was made a long time ago) + prefs.setTimestamp(0, forKey: "repairs.bookmark.when") + return requestor.continueRepairs() + } >>== { result in + // We should be finished as we gave up in disgust. + XCTAssertTrue(result) + XCTAssertEqual(prefs.stringForKey("repairs.bookmark.state"), nil) + checkRecordedEvents(fromPrefs: prefs, expected: startedEvent, uploadEvent, abandonEvent, finishedEvent) + expectation.fulfill() + } + waitForExpectations(timeout: 10, handler: nil) + } + + func testLatestClientUsed() { + let expectation = self.expectation(description: #function) + + let prefs = MockProfilePrefs() + let scratchpad = buildMockScratchpad(prefs: prefs) + let localClient = RemoteClient(guid: nil, name: "Test local client", modified: (Date.now() - OneMinuteInMilliseconds), type: "mobile", formfactor: "largetablet", os: "iOS", version: nil, fxaDeviceId: nil) + let clientEarly = RemoteClient(guid: "client-early", name: "Test remote client", modified: (Date.now() - OneWeekInMilliseconds), type: "desktop", formfactor: nil, os: nil, version: "55.0.1", fxaDeviceId: nil) + let clientLate = RemoteClient(guid: "client-late", name: "Test remote client", modified: (Date.now() - OneMinuteInMilliseconds), type: "desktop", formfactor: nil, os: nil, version: "55.0.1", fxaDeviceId: nil) + let remoteClients = MockRemoteClientsAndTabs([ClientAndTabs(client: localClient, tabs: []), ClientAndTabs(client: clientEarly, tabs: []), ClientAndTabs(client: clientLate, tabs: [])]) + let validationInfo = [BufferInconsistency.missingValues: ["mock-guid1", "mock-guid2"]] + + // Mock telemetry events + let mockFlowID = Bytes.generateGUID() + let startedEvent = makeRepairEvent(["started", nil, ["flowID": mockFlowID, "numIDs": "2"]]) + let uploadEvent = makeRepairEvent(["request", "upload", ["flowID": mockFlowID, "deviceID": MockHashedDeviceID, "numIDs": "2"]]) + + let requestor = BookmarksRepairRequestor(scratchpad: scratchpad, basePrefs: prefs, remoteClients: remoteClients) + requestor.startRepairs(validationInfo: validationInfo, flowID: mockFlowID) >>== { result in + XCTAssertTrue(result) + XCTAssertEqual(prefs.stringForKey("repairs.bookmark.state"), "repair.sent") + checkRecordedEvents(fromPrefs: prefs, expected: startedEvent, uploadEvent) + // the repair command should be outgoing to the most-recent client. + checkOutgoingCommand(remoteClients: remoteClients, clientID: "client-late") + expectation.fulfill() + } + waitForExpectations(timeout: 10, handler: nil) + } + + func testClientVanishes() { + let expectation = self.expectation(description: #function) + + let prefs = MockProfilePrefs() + let scratchpad = buildMockScratchpad(prefs: prefs) + let localClient = RemoteClient(guid: nil, name: "Test local client", modified: (Date.now() - OneMinuteInMilliseconds), type: "mobile", formfactor: "largetablet", os: "iOS", version: nil, fxaDeviceId: nil) + let remoteClientA = RemoteClient(guid: "client-a", name: "Test remote client", modified: Date.now(), type: "desktop", formfactor: nil, os: nil, version: "55.0.1", fxaDeviceId: nil) + let remoteClientB = RemoteClient(guid: "client-b", name: "Test remote client", modified: (Date.now() - OneMinuteInMilliseconds), type: "desktop", formfactor: nil, os: nil, version: "55.0.1", fxaDeviceId: nil) + let remoteClients = MockRemoteClientsAndTabs([ClientAndTabs(client: remoteClientA, tabs: []), ClientAndTabs(client: localClient, tabs: []), ClientAndTabs(client: remoteClientB, tabs: [])]) + let validationInfo = [BufferInconsistency.missingValues: ["mock-guid1", "mock-guid2"]] + + // Mock telemetry events + let flowID = Bytes.generateGUID() + let startedEvent = makeRepairEvent(["started", nil, ["flowID": flowID, "numIDs": "2"]]) + let uploadEvent = makeRepairEvent(["request", "upload", ["flowID": flowID, "deviceID": MockHashedDeviceID, "numIDs": "2"]]) + let missingEvent = makeRepairEvent(["abandon", "missing", ["flowID": flowID, "deviceID": MockHashedDeviceID]]) + let responseEvent = makeRepairEvent(["response", "upload", ["flowID": flowID, "deviceID": MockHashedDeviceID, "numIDs": "2"]]) + let finishedEvent = makeRepairEvent(["finished", nil, ["flowID": flowID, "numIDs": "0"]]) + + let requestor = BookmarksRepairRequestor(scratchpad: scratchpad, basePrefs: prefs, remoteClients: remoteClients) + requestor.startRepairs(validationInfo: validationInfo, flowID: flowID) >>== { result -> Deferred> in + XCTAssertTrue(result) + XCTAssertEqual(prefs.stringForKey("repairs.bookmark.state"), "repair.sent") + checkRecordedEvents(fromPrefs: prefs, expected: startedEvent, uploadEvent) + + checkOutgoingCommand(remoteClients: remoteClients, clientID: "client-a") + // asking it to continue stays in that state until we timeout or the command + // is removed. + return requestor.continueRepairs() + } >>== { result -> Deferred> in + XCTAssertTrue(result) + XCTAssertEqual(prefs.stringForKey("repairs.bookmark.state"), "repair.sent") + checkRecordedEvents(fromPrefs: prefs, expected: startedEvent, uploadEvent) + // the command should now be outgoing. + checkOutgoingCommand(remoteClients: remoteClients, clientID: "client-a") + + let _ = remoteClients.deleteCommands("client-a") + // Now let's pretend the client vanished. + remoteClients.clientsAndTabs.removeFirst() + return requestor.continueRepairs() + } >>== { result -> Deferred> in + XCTAssertTrue(result) + XCTAssertEqual(prefs.stringForKey("repairs.bookmark.state"), "repair.sent") + checkRecordedEvents(fromPrefs: prefs, expected: startedEvent, uploadEvent, missingEvent, uploadEvent) + + // We should have moved on to client-b. + checkOutgoingCommand(remoteClients: remoteClients, clientID: "client-b") + + // Now let's pretend client B wrote all missing IDs. + let repairResponse = RepairResponse(collection: "bookmarks", request: "upload", flowID: flowID, clientID: "client-b", ids: ["mock-guid1", "mock-guid2"]) + return requestor.continueRepairs(response: repairResponse) + } >>== { result in + XCTAssertTrue(result) + checkRecordedEvents(fromPrefs: prefs, + expected: startedEvent, uploadEvent, missingEvent, + uploadEvent, responseEvent, finishedEvent) + // We should be finished as we got all our IDs. + XCTAssertEqual(prefs.stringForKey("repairs.bookmark.state"), nil) + expectation.fulfill() + } + waitForExpectations(timeout: 10, handler: nil) + } + + func testMultiClients() { + let expectation = self.expectation(description: #function) + + let prefs = MockProfilePrefs() + let scratchpad = buildMockScratchpad(prefs: prefs) + let localClient = RemoteClient(guid: nil, name: "Test local client", modified: (Date.now() - OneMinuteInMilliseconds), type: "mobile", formfactor: "largetablet", os: "iOS", version: nil, fxaDeviceId: nil) + let remoteClientA = RemoteClient(guid: "client-a", name: "Test remote client", modified: Date.now(), type: "desktop", formfactor: nil, os: nil, version: "55.0.1", fxaDeviceId: nil) + let remoteClientB = RemoteClient(guid: "client-b", name: "Test remote client", modified: (Date.now() - OneMinuteInMilliseconds), type: "desktop", formfactor: nil, os: nil, version: "55.0.1", fxaDeviceId: nil) + let remoteClients = MockRemoteClientsAndTabs([ClientAndTabs(client: remoteClientA, tabs: []), ClientAndTabs(client: localClient, tabs: []), ClientAndTabs(client: remoteClientB, tabs: [])]) + let validationInfo = [BufferInconsistency.missingValues: ["mock-guid1", "mock-guid2", + "mock-guid3"]] + + let flowID = Bytes.generateGUID() + + // Mock telemetry events + let startedEvent = makeRepairEvent(["started", nil, ["flowID": flowID, "numIDs": "3"]]) + let firstUploadEvent = makeRepairEvent(["request", "upload", ["flowID": flowID, "deviceID": MockHashedDeviceID, "numIDs": "3"]]) + let secondUploadEvent = makeRepairEvent(["request", "upload", ["flowID": flowID, "deviceID": MockHashedDeviceID, "numIDs": "1"]]) + let firstResponseEvent = makeRepairEvent(["response", "upload", ["flowID": flowID, "deviceID": MockHashedDeviceID, "numIDs": "2"]]) + let secondResponseEvent = makeRepairEvent(["response", "upload", ["flowID": flowID, "deviceID": MockHashedDeviceID, "numIDs": "1"]]) + let finishedEvent = makeRepairEvent(["finished", nil, ["flowID": flowID, "numIDs": "0"]]) + + let requestor = BookmarksRepairRequestor(scratchpad: scratchpad, basePrefs: prefs, remoteClients: remoteClients) + requestor.startRepairs(validationInfo: validationInfo, flowID: flowID) >>== { result -> Deferred> in + XCTAssertTrue(result) + XCTAssertEqual(prefs.stringForKey("repairs.bookmark.state"), "repair.sent") + checkOutgoingCommand(remoteClients: remoteClients, clientID: "client-a") + // asking it to continue stays in that state until we timeout or the command + // is removed. + return requestor.continueRepairs() + } >>== { result -> Deferred> in + XCTAssertTrue(result) + XCTAssertEqual(prefs.stringForKey("repairs.bookmark.state"), "repair.sent") + checkRecordedEvents(fromPrefs: prefs, expected: startedEvent, firstUploadEvent) + + // the command should now be outgoing. + checkOutgoingCommand(remoteClients: remoteClients, clientID: "client-a") + + let _ = remoteClients.deleteCommands("client-a") + // Now let's pretend the client wrote a response. + let repairResponse = RepairResponse(collection: "bookmarks", request: "upload", flowID: flowID, clientID: "client-a", ids: ["mock-guid1", "mock-guid2"]) + return requestor.continueRepairs(response: repairResponse) + } >>== { result -> Deferred> in + XCTAssertTrue(result) + XCTAssertEqual(prefs.stringForKey("repairs.bookmark.state"), "repair.sent") + checkRecordedEvents(fromPrefs: prefs, + expected: startedEvent, firstUploadEvent, firstResponseEvent, secondUploadEvent) + + // We should have moved on to client-b. + checkOutgoingCommand(remoteClients: remoteClients, clientID: "client-b") + + let _ = remoteClients.deleteCommands("client-b") + // Now let's pretend client B write the missing ID. + let repairResponse = RepairResponse(collection: "bookmarks", request: "upload", flowID: flowID, clientID: "client-b", ids: ["mock-guid3"]) + return requestor.continueRepairs(response: repairResponse) + } >>== { result in + XCTAssertTrue(result) + + checkRecordedEvents(fromPrefs: prefs, + expected: startedEvent, firstUploadEvent, firstResponseEvent, secondUploadEvent, secondResponseEvent, finishedEvent) + // We should be finished as we got all our IDs. + XCTAssertEqual(prefs.stringForKey("repairs.bookmark.state"), nil) + expectation.fulfill() + } + waitForExpectations(timeout: 10, handler: nil) + } + + func testAlreadyRepairingContinue() { + let expectation = self.expectation(description: #function) + + let prefs = MockProfilePrefs() + let scratchpad = buildMockScratchpad(prefs: prefs) + let localClient = RemoteClient(guid: nil, name: "Test local client", modified: (Date.now() - OneMinuteInMilliseconds), type: "mobile", formfactor: "largetablet", os: "iOS", version: nil, fxaDeviceId: nil) + let remoteClientA = RemoteClient(guid: "client-a", name: "Test remote client", modified: Date.now(), type: "desktop", formfactor: nil, os: nil, version: "55.0.1", fxaDeviceId: nil) + let remoteClientB = RemoteClient(guid: "client-b", name: "Test remote client", modified: (Date.now() - OneMinuteInMilliseconds), type: "desktop", formfactor: nil, os: nil, version: "55.0.1", fxaDeviceId: nil) + let remoteClients = MockRemoteClientsAndTabs([ClientAndTabs(client: remoteClientA, tabs: []), ClientAndTabs(client: localClient, tabs: []), ClientAndTabs(client: remoteClientB, tabs: [])]) + let validationInfo = [BufferInconsistency.missingValues: ["mock-guid1", "mock-guid2", "mock-guid3"]] + + // Mock telemetry events + let flowID = Bytes.generateGUID() + let startedEvent = makeRepairEvent(["started", nil, ["flowID": flowID, "numIDs": "3"]]) + let uploadEvent = makeRepairEvent(["request", "upload", ["flowID": flowID, "deviceID": MockHashedDeviceID, "numIDs": "3"]]) + let abortedEvent = makeRepairEvent(["aborted", nil, ["flowID": flowID, "reason": "other clients repairing", "numIDs": "3"]]) + + let requestor = BookmarksRepairRequestor(scratchpad: scratchpad, basePrefs: prefs, remoteClients: remoteClients) + requestor.startRepairs(validationInfo: validationInfo, flowID: flowID) >>== { result -> Deferred> in + XCTAssertTrue(result) + XCTAssertEqual(prefs.stringForKey("repairs.bookmark.state"), "repair.sent") + checkOutgoingCommand(remoteClients: remoteClients, clientID: "client-a") + // asking it to continue stays in that state until we timeout or the command + // is removed. + return requestor.continueRepairs() + } >>== { result -> Deferred> in + XCTAssertTrue(result) + XCTAssertEqual(prefs.stringForKey("repairs.bookmark.state"), "repair.sent") + checkRecordedEvents(fromPrefs: prefs, expected: startedEvent, uploadEvent) + // the command should now be outgoing. + checkOutgoingCommand(remoteClients: remoteClients, clientID: "client-a") + + let _ = remoteClients.deleteCommands("client-a") + // Now let's pretend the client wrote a response (it doesn't matter what's in here) + let repairResponse = RepairResponse(collection: "bookmarks", request: "upload", flowID: flowID, clientID: "client-a", ids: ["mock-guid1", "mock-guid2"]) + + // and another client also started a request + let otherRequest = RepairRequest(collection: "bookmarks", request: "upload", flowID: "abdc", requestor: "client-c", ids: ["bogusid"]) + let _ = remoteClients.insertCommand(otherRequest.toSyncCommand(), forClients: [remoteClientB]) + + return requestor.continueRepairs(response: repairResponse) + } >>== { result in + XCTAssertTrue(result) + + // We should have aborted now + XCTAssertEqual(prefs.stringForKey("repairs.bookmark.state"), nil) + + checkRecordedEvents(fromPrefs: prefs, expected: startedEvent, uploadEvent, abortedEvent) + expectation.fulfill() + } + waitForExpectations(timeout: 10, handler: nil) + } + + func testSyncEventsPickledInPrefs() { + let prefs = MockProfilePrefs() + let scratchpad = Scratchpad(b: KeyBundle.random(), persistingTo: prefs) + let localClient = RemoteClient(guid: nil, name: "Test local client", modified: (Date.now() - OneMinuteInMilliseconds), type: "mobile", formfactor: "largetablet", os: "iOS", version: nil, fxaDeviceId: nil) + let remoteClientA = RemoteClient(guid: "client-a", name: "Test remote client", modified: Date.now(), type: "desktop", formfactor: nil, os: nil, version: "55.0.1", fxaDeviceId: nil) + let remoteClientB = RemoteClient(guid: "client-b", name: "Test remote client", modified: (Date.now() - OneMinuteInMilliseconds), type: "desktop", formfactor: nil, os: nil, version: "55.0.1", fxaDeviceId: nil) + let remoteClients = MockRemoteClientsAndTabs([ClientAndTabs(client: remoteClientA, tabs: []), ClientAndTabs(client: localClient, tabs: []), ClientAndTabs(client: remoteClientB, tabs: [])]) + let requestor = BookmarksRepairRequestor(scratchpad: scratchpad, basePrefs: prefs, remoteClients: remoteClients) + + let mockTimestamp = Date.now() + let mockEvent = Event(timestamp: mockTimestamp, category: "test", + method: "method", object: "object", value: "value", + extra: ["test": "value"]) + + requestor.recordTelemetry(event: mockEvent) + + let events = requestor.basePrefs.arrayForKey(PrefKeySyncEvents) as! [Data] + XCTAssertEqual(events.count, 1) + + let pickledEvent = Event.unpickle(events[0]) + XCTAssertEqual(pickledEvent!.category, mockEvent.category) + } +} + +func checkOutgoingCommand(remoteClients: MockRemoteClientsAndTabs, clientID: GUID) { + let outgoingCmds = remoteClients.commands[clientID]! + XCTAssertEqual(outgoingCmds.count, 1) + XCTAssertEqual(JSON.parse(outgoingCmds.first!.value)["command"].stringValue, "repairRequest") +} + +func checkRecordedEvents(fromPrefs prefs: Prefs, expected: Event...) { + let eventData = prefs.arrayForKey(PrefKeySyncEvents) as? [Data] ?? [] + let actualEvents = eventData.map(Event.unpickle) + XCTAssertEqual(actualEvents.count, expected.count) + expected.enumerated().forEach { offset, expected in + XCTAssertEqual(expected, actualEvents[offset]) + } +} + +func makeRepairEvent(_ values: [Any?]) -> Event { + return Event(timestamp: Date.now(), + category: "sync", + method: "repair", + object: values[0] as! String, + value: values[1] as? String, + extra: values[2] as? [String: String] + ) +} + +// Checks equivalence while ignoring the timestamp +extension Event: Equatable { + public static func ==(left: Event, right: Event) -> Bool { + let propsAreEqual = (left.category == right.category) && + (left.method == right.method) && + (left.object == right.object) && + (left.value ?? "" == right.value ?? "") && + (left.extra ?? [:] == right.extra ?? [:]) + return propsAreEqual + } +} + +open class MockRemoteClientsAndTabs: RemoteClientsAndTabs { + open var clientsAndTabs: [ClientAndTabs] + open var commands: [GUID: [SyncCommand]] + + public init(_ clientsAndTabs: [ClientAndTabs]) { + self.clientsAndTabs = clientsAndTabs + self.commands = clientsAndTabs.map { $0.client.guid ?? "localID" }.reduce([String: [SyncCommand]]()) { (dict, clientId) -> [String: [SyncCommand]] in + var dict = dict + dict[clientId] = [SyncCommand]() + return dict + } + } + + open func onRemovedAccount() -> Success { + return succeed() + } + + open func wipeClients() -> Success { + return succeed() + } + + open func wipeRemoteTabs() -> Deferred> { + return succeed() + } + + open func wipeTabs() -> Success { + return succeed() + } + + open func insertOrUpdateClients(_ clients: [RemoteClient]) -> Deferred> { + return deferMaybe(0) + } + + open func insertOrUpdateClient(_ client: RemoteClient) -> Deferred> { + return deferMaybe(0) + } + + open func insertOrUpdateTabs(_ tabs: [RemoteTab]) -> Deferred> { + return insertOrUpdateTabsForClientGUID(nil, tabs: [RemoteTab]()) + } + + open func insertOrUpdateTabsForClientGUID(_ clientGUID: String?, tabs: [RemoteTab]) -> Deferred> { + return deferMaybe(-1) + } + + open func getClientsAndTabs() -> Deferred> { + return deferMaybe(self.clientsAndTabs) + } + + open func getClients() -> Deferred> { + return deferMaybe(self.clientsAndTabs.map { $0.client }) + } + + open func getClientWithId(_ clientID: GUID) -> Deferred> { + return self.getClient(guid: clientID) + } + + open func getClient(fxaDeviceId: String) -> Deferred> { + return deferMaybe(self.clientsAndTabs.find { clientAndTabs in + return clientAndTabs.client.fxaDeviceId == fxaDeviceId + }?.client) + } + + open func getClient(guid: GUID) -> Deferred> { + return deferMaybe(self.clientsAndTabs.find { clientAndTabs in + return clientAndTabs.client.guid == guid + }?.client) + } + + open func deleteClient(guid: GUID) -> Success { + clientsAndTabs = clientsAndTabs.filter { $0.client.guid != guid } + return succeed() + } + + open func getClientGUIDs() -> Deferred>> { + return deferMaybe(Set(optFilter(self.clientsAndTabs.map { $0.client.guid }))) + } + + open func getTabsForClientWithGUID(_ guid: GUID?) -> Deferred> { + return deferMaybe(optFilter(self.clientsAndTabs.map { $0.client.guid == guid ? $0.tabs : nil })[0]) + } + + open func deleteCommands() -> Success { + self.commands = [GUID: [SyncCommand]]() + return succeed() + } + open func deleteCommands(_ clientGUID: GUID) -> Success { + self.commands[clientGUID] = [SyncCommand]() + return succeed() + } + + open func getCommands() -> Deferred> { + return deferMaybe(self.commands) + } + + open func insertCommand(_ command: SyncCommand, forClients clients: [RemoteClient]) -> Deferred> { + return self.insertCommands([command], forClients: clients) + } + open func insertCommands(_ commands: [SyncCommand], forClients clients: [RemoteClient]) -> Deferred> { + var numInserts = 0 + for client in clients where self.commands[client.guid ?? "localID"] != nil { + self.commands[client.guid ?? "localID"]! += commands + numInserts += commands.count + } + return deferMaybe(numInserts) + } +} diff --git a/mobile/ios/SyncTests/TestBookmarksSynchronizer.swift b/mobile/ios/SyncTests/TestBookmarksSynchronizer.swift new file mode 100644 index 0000000000..5b5316bef7 --- /dev/null +++ b/mobile/ios/SyncTests/TestBookmarksSynchronizer.swift @@ -0,0 +1,434 @@ +/* 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 Deferred +@testable import Sync +@testable import Storage + +import XCTest +import SwiftyJSON + +class MockStorage: LocalItemSource, MirrorItemSource, SyncableBookmarks { + var local: [GUID: BookmarkMirrorItem] = [:] + var localAdditions: [GUID] = [] + var localDeletions: [GUID] = [] + var lastBufferUpdatedCompletionOpApplied: BufferUpdatedCompletionOp? + + // LocalItemSource methods. + + func getLocalItemWithGUID(_ guid: GUID) -> Deferred> { + guard let item = self.local[guid] else { + return deferMaybe(DatabaseError(description: "Couldn't find item \(guid).")) + } + return deferMaybe(item) + } + + func getLocalItemsWithGUIDs(_ guids: T) -> Deferred> where T.Iterator.Element == GUID { + var acc: [GUID: BookmarkMirrorItem] = [:] + guids.forEach { guid in + if let item = self.local[guid] { + acc[guid] = item + } + } + return deferMaybe(acc) + } + + func prefetchLocalItemsWithGUIDs(_ guids: T) -> Success where T.Iterator.Element == GUID { + return succeed() + } + + // MirrorItemSource methods (not implemented!). + + func getMirrorItemWithGUID(_ guid: GUID) -> Deferred> { + return deferMaybe(DatabaseError(description: "Not implemented")) + } + + func getMirrorItemsWithGUIDs(_ guids: T) -> Deferred> where T : Collection, T.Iterator.Element == GUID { + return deferMaybe(DatabaseError(description: "Not implemented")) + } + + func prefetchMirrorItemsWithGUIDs(_ guids: T) -> Success where T.Iterator.Element == GUID { + return succeed() + } + + // SyncableBookmarks methods (partialy implemented!). + + func isUnchanged() -> Deferred> { + return deferMaybe(DatabaseError(description: "Not implemented")) + } + + func getLocalBookmarksModifications(limit: Int) -> Deferred> { + let deletions = Array(self.localDeletions.prefix(limit)) + let additions = Array(self.localAdditions.prefix(limit - deletions.count)).map { self.local[$0]! } + return deferMaybe((deletions: deletions, additions: additions)) + } + + func getLocalDeletions() -> Deferred> { + return deferMaybe(DatabaseError(description: "Not implemented")) + } + + func treesForEdges() -> Deferred> { + return deferMaybe(DatabaseError(description: "Not implemented")) + } + + func treeForMirror() -> Deferred> { + return deferMaybe(DatabaseError(description: "Not implemented")) + } + + func applyLocalOverrideCompletionOp(_ op: LocalOverrideCompletionOp, itemSources: ItemSources) -> Success { + return deferMaybe(DatabaseError(description: "Not implemented")) + } + + func applyBufferUpdatedCompletionOp(_ op: BufferUpdatedCompletionOp) -> Success { + self.lastBufferUpdatedCompletionOpApplied = op + return succeed() + } + + // Misc methods. + + func resetClient() -> Success { + return deferMaybe(DatabaseError(description: "Not implemented")) + } + + func onRemovedAccount() -> Success { + return deferMaybe(DatabaseError(description: "Not implemented")) + } +} + +class MockBuffer: BookmarkBufferStorage, BufferItemSource { + var buffer: [GUID: BookmarkMirrorItem] = [:] + var children: [GUID: [GUID]] = [:] + + // BufferItemSource methods. + + func getBufferItemWithGUID(_ guid: GUID) -> Deferred> { + guard let item = self.buffer[guid] else { + return deferMaybe(DatabaseError(description: "Couldn't find item \(guid).")) + } + return deferMaybe(item) + } + + func getBufferItemsWithGUIDs(_ guids: T) -> Deferred> where T : Collection, T.Iterator.Element == GUID { + return deferMaybe(DatabaseError(description: "Not implemented")) + } + + func getBufferChildrenGUIDsForParent(_ guid: GUID) -> Deferred> { + guard let children = self.children[guid] else { + return deferMaybe(DatabaseError(description: "Couldn't find children for \(guid).")) + } + return deferMaybe(children) + } + + func prefetchBufferItemsWithGUIDs(_ guids: T) -> Success where T : Collection, T.Iterator.Element == GUID { + return succeed() + } + + // BookmarkBufferStorage methods. + + func isEmpty() -> Deferred> { + return deferMaybe(DatabaseError(description: "Not implemented")) + } + + func applyRecords(_ records: [BookmarkMirrorItem]) -> Success { + return deferMaybe(DatabaseError(description: "Not implemented")) + } + + func doneApplyingRecordsAfterDownload() -> Success { + return deferMaybe(DatabaseError(description: "Not implemented")) + } + + func validate() -> Success { + return deferMaybe(DatabaseError(description: "Not implemented")) + } + + func getBufferedDeletions() -> Deferred> { + return deferMaybe(DatabaseError(description: "Not implemented")) + } + + func applyBufferCompletionOp(_ op: BufferCompletionOp, itemSources: ItemSources) -> Success { + return deferMaybe(DatabaseError(description: "Not implemented")) + } + + func synchronousBufferCount() -> Int? { + return nil + } + + func getUpstreamRecordCount() -> Deferred { + return Deferred(value: nil) + } +} + +class TestBookmarksSynchronizer: XCTestCase { + func testBuildMobileRootAndChildrenRecords_noMobileRootInBuffer() { + let delegate = MockSyncDelegate() + let prefs = MockProfilePrefs() + let scratchpad = Scratchpad(b: KeyBundle.random(), persistingTo: prefs) + + let buffer = MockBuffer() + let storage = MockStorage() + storage.local[BookmarkRoots.MobileFolderGUID] = BookmarkMirrorItem.folder(BookmarkRoots.MobileFolderGUID, dateAdded: Date.now(), modified: Date.now(), hasDupe: false, parentID: BookmarkRoots.RootGUID, parentName: nil, title: "Mobile Bookmarks", description: nil, children: ["bk1"]) + storage.local["bk1"] = BookmarkMirrorItem.bookmark("bk1", dateAdded: Date.now(), modified: Date.now(), hasDupe: false, parentID: BookmarkRoots.MobileFolderGUID, parentName: nil, title: "Bookmark 1", description: nil, URI: "https://example.com/1", tags: "", keyword: nil) + storage.local["bk1"] = BookmarkMirrorItem.bookmark("bk1", dateAdded: Date.now(), modified: Date.now(), hasDupe: false, parentID: BookmarkRoots.MobileFolderGUID, parentName: nil, title: "Bookmark 1", description: nil, URI: "https://example.com/1", tags: "", keyword: nil) + let bk2 = BookmarkMirrorItem.bookmark("bk2", dateAdded: Date.now(), modified: Date.now(), hasDupe: false, parentID: BookmarkRoots.MobileFolderGUID, parentName: nil, title: "Bookmark 2", description: nil, URI: "https://example.com/2", tags: "", keyword: nil) + let bk3 = BookmarkMirrorItem.bookmark("bk3", dateAdded: Date.now(), modified: Date.now(), hasDupe: false, parentID: BookmarkRoots.MobileFolderGUID, parentName: nil, title: "Bookmark 3", description: nil, URI: "https://example.com/3", tags: "", keyword: nil) + storage.local["bk2"] = bk2 + storage.local["bk3"] = bk3 + + let synchronizer = BufferingBookmarksSynchronizer(scratchpad: scratchpad, delegate: delegate, basePrefs: prefs, why: .scheduled) + let (mobileRootRecord, childrenRecords) = synchronizer.buildMobileRootAndChildrenRecords(storage, buffer, additionalChildren: [bk2, bk3], deletedChildren: []).value.successValue! + XCTAssertEqual(mobileRootRecord.id, BookmarkRoots.translateOutgoingRootGUID(BookmarkRoots.MobileFolderGUID)) + XCTAssertEqual(mobileRootRecord.payload.json["title"], "Mobile Bookmarks") + // We are not including bk1 in the call to buildMobileRootRecord() therefore it should NOT be included in the returned record + XCTAssertEqual(mobileRootRecord.payload.json["children"], ["bk2", "bk3"]) + + XCTAssertEqual(childrenRecords.count, 2) + XCTAssertEqual(childrenRecords[0].id, "bk2") + XCTAssertEqual(childrenRecords[1].id, "bk3") + } + + func testBuildMobileRootAndChildrenRecords_mobileRootInBuffer() { + let delegate = MockSyncDelegate() + let prefs = MockProfilePrefs() + let scratchpad = Scratchpad(b: KeyBundle.random(), persistingTo: prefs) + + let buffer = MockBuffer() + buffer.buffer[BookmarkRoots.MobileFolderGUID] = BookmarkMirrorItem.folder(BookmarkRoots.MobileFolderGUID, dateAdded: Date.now(), modified: Date.now(), hasDupe: false, parentID: BookmarkRoots.RootGUID, parentName: nil, title: "Mobile Bookmarks", description: nil, children: ["bk1", "bkmdeleteme"]) + buffer.children[BookmarkRoots.MobileFolderGUID] = ["bk1", "bkmdeleteme"] + buffer.buffer["bk1"] = BookmarkMirrorItem.bookmark("bk1", dateAdded: Date.now(), modified: Date.now(), hasDupe: false, parentID: BookmarkRoots.MobileFolderGUID, parentName: nil, title: "Bookmark 1", description: nil, URI: "https://example.com/1", tags: "", keyword: nil) + buffer.buffer["bkmdeleteme"] = BookmarkMirrorItem.bookmark("bkmdeleteme", dateAdded: Date.now(), modified: Date.now(), hasDupe: false, parentID: BookmarkRoots.MobileFolderGUID, parentName: nil, title: "Bookmark to delete", description: nil, URI: "https://example.com/todelete", tags: "", keyword: nil) + + let storage = MockStorage() + storage.local["bk1"] = BookmarkMirrorItem.bookmark("bk1", dateAdded: Date.now(), modified: Date.now(), hasDupe: false, parentID: BookmarkRoots.MobileFolderGUID, parentName: nil, title: "Bookmark 1", description: nil, URI: "https://example.com/1", tags: "", keyword: nil) + let bk2 = BookmarkMirrorItem.bookmark("bk2", dateAdded: Date.now(), modified: Date.now(), hasDupe: false, parentID: BookmarkRoots.MobileFolderGUID, parentName: nil, title: "Bookmark 2", description: nil, URI: "https://example.com/2", tags: "", keyword: nil) + let bk3 = BookmarkMirrorItem.bookmark("bk3", dateAdded: Date.now(), modified: Date.now(), hasDupe: false, parentID: BookmarkRoots.MobileFolderGUID, parentName: nil, title: "Bookmark 3", description: nil, URI: "https://example.com/3", tags: "", keyword: nil) + storage.local["bk2"] = bk2 + storage.local["bk3"] = bk3 + + let synchronizer = BufferingBookmarksSynchronizer(scratchpad: scratchpad, delegate: delegate, basePrefs: prefs, why: .scheduled) + let (mobileRootRecord, childrenRecords) = synchronizer.buildMobileRootAndChildrenRecords(storage, buffer, additionalChildren: [bk2, bk3], deletedChildren: ["bkmdeleteme"]).value.successValue! + XCTAssertEqual(mobileRootRecord.id, BookmarkRoots.translateOutgoingRootGUID(BookmarkRoots.MobileFolderGUID)) + XCTAssertEqual(mobileRootRecord.payload.json["title"], "Mobile Bookmarks") + // bk1 is a children of mobile root in the buffer, therefore it should be included here. + XCTAssertEqual(mobileRootRecord.payload.json["children"], ["bk1", "bk2", "bk3"]) + + // We are only sending the new/deleted records though! + XCTAssertEqual(childrenRecords.count, 3) + XCTAssertEqual(childrenRecords[0].id, "bk2") + XCTAssertEqual(childrenRecords[1].id, "bk3") + XCTAssertEqual(childrenRecords[2].id, "bkmdeleteme") + XCTAssertTrue(childrenRecords[2].payload.deleted) + } + + func testUploadSomeLocalRecords_batched_ok() { + let delegate = MockSyncDelegate() + let prefs = MockProfilePrefs() + let scratchpad = Scratchpad(b: KeyBundle.random(), persistingTo: prefs) + + let storage = MockStorage() + let buffer = MockBuffer() + + let records = createMobileRootAndChildrenRecords(numChildren: 80) + + var numPosts = 0 + let now = Date.now() + var firstPostLastModified: Timestamp? + var secondPostLastModified: Timestamp? + let uploader: BatchUploadFunction = { lines, ifUnmodifiedSince, queryParams in + numPosts += 1 + var headers = [String: Any]() + + let result: POSTResult + if numPosts == 1 { + result = POSTResult(success: records.childrenRecords.map { $0.id } + ["mobile"], failed: [:], batchToken: "toktok") + firstPostLastModified = now + headers["X-Last-Modified"] = firstPostLastModified + } else { + XCTAssertEqual(queryParams![0].name, "batch") + XCTAssertEqual(queryParams![0].value, "toktok") + XCTAssertEqual(queryParams![1].name, "commit") + XCTAssertEqual(queryParams![1].value, "true") + result = POSTResult(success: [], failed: [:]) + secondPostLastModified = now + 5000 + headers["X-Last-Modified"] = secondPostLastModified + } + let response = StorageResponse(value: result, metadata: ResponseMetadata(status: 200, headers: headers)) + return deferMaybe(response) + } + + let miniConfig = InfoConfiguration(maxRequestBytes: 1_048_576, maxPostRecords: 100, maxPostBytes: 1_048_576, maxTotalRecords: 250, maxTotalBytes: 104_857_600) + let collectionClient = MockSyncCollectionClient(uploader: uploader, infoConfig: miniConfig, collection: "bookmarks", encrypter: getBookmarksEncrypter()) + let mirrorer = BookmarksMirrorer(storage: buffer, client: collectionClient, basePrefs: prefs, collection: "bookmarks", statsSession: SyncEngineStatsSession(collection: "bookmarks")) + + let synchronizer = BufferingBookmarksSynchronizer(scratchpad: scratchpad, delegate: delegate, basePrefs: prefs, why: .scheduled) + synchronizer.uploadSomeLocalRecords(storage, mirrorer, collectionClient, mobileRootRecord: records.mobileRootRecord, childrenRecords: records.childrenRecords).succeeded() + XCTAssertEqual(storage.lastBufferUpdatedCompletionOpApplied?.bufferValuesToMoveFromLocal.count, 81) + XCTAssertEqual(numPosts, 2) + XCTAssertEqual(mirrorer.lastModified / 1000, secondPostLastModified) + } + + func testUploadSomeLocalRecords_batched_someFailed() { + let delegate = MockSyncDelegate() + let prefs = MockProfilePrefs() + let scratchpad = Scratchpad(b: KeyBundle.random(), persistingTo: prefs) + + let storage = MockStorage() + let buffer = MockBuffer() + + let records = createMobileRootAndChildrenRecords(numChildren: 80) + + var numPosts = 0 + let uploader: BatchUploadFunction = { lines, ifUnmodifiedSince, queryParams in + numPosts += 1 + var headers = [String: Any]() + headers["X-Last-Modified"] = Date.now() + + let result = POSTResult(success: records.childrenRecords.map { $0.id }.dropFirst(20) + ["mobile"], failed: [:], batchToken: "toktok") + let response = StorageResponse(value: result, metadata: ResponseMetadata(status: 200, headers: headers)) + return deferMaybe(response) + } + + let miniConfig = InfoConfiguration(maxRequestBytes: 1_048_576, maxPostRecords: 100, maxPostBytes: 1_048_576, maxTotalRecords: 250, maxTotalBytes: 104_857_600) + let collectionClient = MockSyncCollectionClient(uploader: uploader, infoConfig: miniConfig, collection: "bookmarks", encrypter: getBookmarksEncrypter()) + let mirrorer = BookmarksMirrorer(storage: buffer, client: collectionClient, basePrefs: prefs, collection: "bookmarks", statsSession: SyncEngineStatsSession(collection: "bookmarks")) + + let synchronizer = BufferingBookmarksSynchronizer(scratchpad: scratchpad, delegate: delegate, basePrefs: prefs, why: .scheduled) + let error = synchronizer.uploadSomeLocalRecords(storage, mirrorer, collectionClient, mobileRootRecord: records.mobileRootRecord, childrenRecords: records.childrenRecords).value.failureValue! + XCTAssertTrue(error is RecordsFailedToUpload) + XCTAssertNil(storage.lastBufferUpdatedCompletionOpApplied) + XCTAssertEqual(numPosts, 1) + } + + func testUploadSomeLocalRecords_nobatch_ok() { + let delegate = MockSyncDelegate() + let prefs = MockProfilePrefs() + let scratchpad = Scratchpad(b: KeyBundle.random(), persistingTo: prefs) + + let storage = MockStorage() + let buffer = MockBuffer() + + let records = createMobileRootAndChildrenRecords(numChildren: 80) + + var numPosts = 0 + let uploader: BatchUploadFunction = { lines, ifUnmodifiedSince, queryParams in + numPosts += 1 + var headers = [String: Any]() + headers["X-Last-Modified"] = Date.now() + + let result = POSTResult(success: records.childrenRecords.map { $0.id } + ["mobile"], failed: [:]) + let response = StorageResponse(value: result, metadata: ResponseMetadata(status: 200, headers: headers)) + return deferMaybe(response) + } + + let miniConfig = InfoConfiguration(maxRequestBytes: 1_048_576, maxPostRecords: 100, maxPostBytes: 1_048_576, maxTotalRecords: 250, maxTotalBytes: 104_857_600) + let collectionClient = MockSyncCollectionClient(uploader: uploader, infoConfig: miniConfig, collection: "bookmarks", encrypter: getBookmarksEncrypter()) + let mirrorer = BookmarksMirrorer(storage: buffer, client: collectionClient, basePrefs: prefs, collection: "bookmarks", statsSession: SyncEngineStatsSession(collection: "bookmarks")) + + let synchronizer = BufferingBookmarksSynchronizer(scratchpad: scratchpad, delegate: delegate, basePrefs: prefs, why: .scheduled) + synchronizer.uploadSomeLocalRecords(storage, mirrorer, collectionClient, mobileRootRecord: records.mobileRootRecord, childrenRecords: records.childrenRecords).succeeded() + XCTAssertEqual(storage.lastBufferUpdatedCompletionOpApplied?.bufferValuesToMoveFromLocal.count, 81) + XCTAssertEqual(numPosts, 1) + } + + func testUploadSomeLocalRecords_nobatch_someFailed() { + let delegate = MockSyncDelegate() + let prefs = MockProfilePrefs() + let scratchpad = Scratchpad(b: KeyBundle.random(), persistingTo: prefs) + + let storage = MockStorage() + let buffer = MockBuffer() + + let records = createMobileRootAndChildrenRecords(numChildren: 80) + + var numPosts = 0 + let uploader: BatchUploadFunction = { lines, ifUnmodifiedSince, queryParams in + numPosts += 1 + var headers = [String: Any]() + headers["X-Last-Modified"] = Date.now() + + let result = POSTResult(success: records.childrenRecords.map { $0.id }.dropFirst(20) + ["mobile"], failed: [:]) + let response = StorageResponse(value: result, metadata: ResponseMetadata(status: 200, headers: headers)) + return deferMaybe(response) + } + + let miniConfig = InfoConfiguration(maxRequestBytes: 1_048_576, maxPostRecords: 100, maxPostBytes: 1_048_576, maxTotalRecords: 250, maxTotalBytes: 104_857_600) + let collectionClient = MockSyncCollectionClient(uploader: uploader, infoConfig: miniConfig, collection: "bookmarks", encrypter: getBookmarksEncrypter()) + let mirrorer = BookmarksMirrorer(storage: buffer, client: collectionClient, basePrefs: prefs, collection: "bookmarks", statsSession: SyncEngineStatsSession(collection: "bookmarks")) + + let synchronizer = BufferingBookmarksSynchronizer(scratchpad: scratchpad, delegate: delegate, basePrefs: prefs, why: .scheduled) + synchronizer.uploadSomeLocalRecords(storage, mirrorer, collectionClient, mobileRootRecord: records.mobileRootRecord, childrenRecords: records.childrenRecords).succeeded() + XCTAssertEqual(storage.lastBufferUpdatedCompletionOpApplied?.bufferValuesToMoveFromLocal.count, 61) + XCTAssertEqual(numPosts, 1) + } + + func testUploadSomeLocalRecords_tooManyRecords() { + let delegate = MockSyncDelegate() + let prefs = MockProfilePrefs() + let scratchpad = Scratchpad(b: KeyBundle.random(), persistingTo: prefs) + + let storage = MockStorage() + let buffer = MockBuffer() + + let uploader: BatchUploadFunction = { _, _, _ in + // Should never happen + XCTFail() + return deferMaybe(NSError()) + } + + let miniConfig = InfoConfiguration(maxRequestBytes: 1_048_576, maxPostRecords: 100, maxPostBytes: 1_048_576, maxTotalRecords: 250, maxTotalBytes: 104_857_600) + let collectionClient = MockSyncCollectionClient(uploader: uploader, infoConfig: miniConfig, collection: "bookmarks", encrypter: getBookmarksEncrypter()) + let mirrorer = BookmarksMirrorer(storage: buffer, client: collectionClient, basePrefs: prefs, collection: "bookmarks", statsSession: SyncEngineStatsSession(collection: "bookmarks")) + + let records = createMobileRootAndChildrenRecords(numChildren: 120) + + let synchronizer = BufferingBookmarksSynchronizer(scratchpad: scratchpad, delegate: delegate, basePrefs: prefs, why: .scheduled) + let error = synchronizer.uploadSomeLocalRecords(storage, mirrorer, collectionClient, mobileRootRecord: records.mobileRootRecord, childrenRecords: records.childrenRecords).value.failureValue! + XCTAssertTrue(error is TooManyRecordsError) + } + + func testUploadSomeLocalRecords_tooBig() { + let delegate = MockSyncDelegate() + let prefs = MockProfilePrefs() + let scratchpad = Scratchpad(b: KeyBundle.random(), persistingTo: prefs) + + let storage = MockStorage() + let buffer = MockBuffer() + + let uploader: BatchUploadFunction = { _, _, _ in + // Should never happen + XCTFail() + return deferMaybe(NSError()) + } + + let miniConfig = InfoConfiguration(maxRequestBytes: 1_048_576, maxPostRecords: 100, maxPostBytes: 5000, maxTotalRecords: 250, maxTotalBytes: 104_857_600) + let collectionClient = MockSyncCollectionClient(uploader: uploader, infoConfig: miniConfig, collection: "bookmarks", encrypter: getBookmarksEncrypter()) + let mirrorer = BookmarksMirrorer(storage: buffer, client: collectionClient, basePrefs: prefs, collection: "bookmarks", statsSession: SyncEngineStatsSession(collection: "bookmarks")) + + let records = createMobileRootAndChildrenRecords(numChildren: 80) + + let synchronizer = BufferingBookmarksSynchronizer(scratchpad: scratchpad, delegate: delegate, basePrefs: prefs, why: .scheduled) + let error = synchronizer.uploadSomeLocalRecords(storage, mirrorer, collectionClient, mobileRootRecord: records.mobileRootRecord, childrenRecords: records.childrenRecords).value.failureValue! + XCTAssertTrue(error is TooManyRecordsError) + } + + func getBookmarksEncrypter() -> RecordEncrypter { + let serializer: (Record) -> JSON? = { $0.payload.json } + let factory: (String) -> BookmarkBasePayload = { BookmarkBasePayload($0) } + return RecordEncrypter(serializer: serializer, factory: factory) + } + + func createMobileRootAndChildrenRecords(numChildren: Int) -> (mobileRootRecord: Record, childrenRecords: [Record]) { + var childrenRecords: [Record] = [] + for i in 0...numChildren { + let item = BookmarkMirrorItem.bookmark("bk1\(i)", dateAdded: Date.now(), modified: Date.now(), hasDupe: false, parentID: BookmarkRoots.MobileFolderGUID, parentName: nil, title: "bk1\(i)", description: nil, URI: "https://example.com/\(i)", tags: "", keyword: nil) + let record = Record(id: item.guid, payload: item.asPayload()) + childrenRecords.append(record) + } + let mobileRoot = BookmarkMirrorItem.folder(BookmarkRoots.MobileFolderGUID, dateAdded: Date.now(), modified: Date.now(), hasDupe: false, parentID: BookmarkRoots.RootGUID, parentName: nil, title: "Mobile Bookmarks", description: nil, children: []) + let mobileRootRecord = Record(id: BookmarkRoots.translateOutgoingRootGUID(BookmarkRoots.MobileFolderGUID), payload: mobileRoot.asPayloadWithChildren(childrenRecords.map { $0.id })) + + return (mobileRootRecord: mobileRootRecord, childrenRecords: childrenRecords) + } +} diff --git a/mobile/ios/ThirdParty/Apple/UIImage+ImageEffects.h b/mobile/ios/ThirdParty/Apple/UIImage+ImageEffects.h new file mode 100644 index 0000000000..d9314fd3a3 --- /dev/null +++ b/mobile/ios/ThirdParty/Apple/UIImage+ImageEffects.h @@ -0,0 +1,116 @@ +/* + File: UIImage+ImageEffects.h + Abstract: This is a category of UIImage that adds methods to apply blur and tint effects to an image. This is the code you’ll want to look out to find out how to use vImage to efficiently calculate a blur. + Version: 1.0 + + Disclaimer: IMPORTANT: This Apple software is supplied to you by Apple + Inc. ("Apple") in consideration of your agreement to the following + terms, and your use, installation, modification or redistribution of + this Apple software constitutes acceptance of these terms. If you do + not agree with these terms, please do not use, install, modify or + redistribute this Apple software. + + In consideration of your agreement to abide by the following terms, and + subject to these terms, Apple grants you a personal, non-exclusive + license, under Apple's copyrights in this original Apple software (the + "Apple Software"), to use, reproduce, modify and redistribute the Apple + Software, with or without modifications, in source and/or binary forms; + provided that if you redistribute the Apple Software in its entirety and + without modifications, you must retain this notice and the following + text and disclaimers in all such redistributions of the Apple Software. + Neither the name, trademarks, service marks or logos of Apple Inc. may + be used to endorse or promote products derived from the Apple Software + without specific prior written permission from Apple. Except as + expressly stated in this notice, no other rights or licenses, express or + implied, are granted by Apple herein, including but not limited to any + patent rights that may be infringed by your derivative works or by other + works in which the Apple Software may be incorporated. + + The Apple Software is provided by Apple on an "AS IS" basis. APPLE + MAKES NO WARRANTIES, EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION + THE IMPLIED WARRANTIES OF NON-INFRINGEMENT, MERCHANTABILITY AND FITNESS + FOR A PARTICULAR PURPOSE, REGARDING THE APPLE SOFTWARE OR ITS USE AND + OPERATION ALONE OR IN COMBINATION WITH YOUR PRODUCTS. + + IN NO EVENT SHALL APPLE BE LIABLE FOR ANY SPECIAL, INDIRECT, INCIDENTAL + OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + INTERRUPTION) ARISING IN ANY WAY OUT OF THE USE, REPRODUCTION, + MODIFICATION AND/OR DISTRIBUTION OF THE APPLE SOFTWARE, HOWEVER CAUSED + AND WHETHER UNDER THEORY OF CONTRACT, TORT (INCLUDING NEGLIGENCE), + STRICT LIABILITY OR OTHERWISE, EVEN IF APPLE HAS BEEN ADVISED OF THE + POSSIBILITY OF SUCH DAMAGE. + + Copyright (C) 2013 Apple Inc. All Rights Reserved. + + + Copyright © 2013 Apple Inc. All rights reserved. + WWDC 2013 License + + NOTE: This Apple Software was supplied by Apple as part of a WWDC 2013 + Session. Please refer to the applicable WWDC 2013 Session for further + information. + + IMPORTANT: This Apple software is supplied to you by Apple Inc. + ("Apple") in consideration of your agreement to the following terms, and + your use, installation, modification or redistribution of this Apple + software constitutes acceptance of these terms. If you do not agree with + these terms, please do not use, install, modify or redistribute this + Apple software. + + In consideration of your agreement to abide by the following terms, and + subject to these terms, Apple grants you a non-exclusive license, under + Apple's copyrights in this original Apple software (the "Apple + Software"), to use, reproduce, modify and redistribute the Apple + Software, with or without modifications, in source and/or binary forms; + provided that if you redistribute the Apple Software in its entirety and + without modifications, you must retain this notice and the following + text and disclaimers in all such redistributions of the Apple Software. + Neither the name, trademarks, service marks or logos of Apple Inc. may + be used to endorse or promote products derived from the Apple Software + without specific prior written permission from Apple. Except as + expressly stated in this notice, no other rights or licenses, express or + implied, are granted by Apple herein, including but not limited to any + patent rights that may be infringed by your derivative works or by other + works in which the Apple Software may be incorporated. + + The Apple Software is provided by Apple on an "AS IS" basis. APPLE MAKES + NO WARRANTIES, EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION THE + IMPLIED WARRANTIES OF NON-INFRINGEMENT, MERCHANTABILITY AND FITNESS FOR + A PARTICULAR PURPOSE, REGARDING THE APPLE SOFTWARE OR ITS USE AND + OPERATION ALONE OR IN COMBINATION WITH YOUR PRODUCTS. + + IN NO EVENT SHALL APPLE BE LIABLE FOR ANY SPECIAL, INDIRECT, INCIDENTAL + OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + INTERRUPTION) ARISING IN ANY WAY OUT OF THE USE, REPRODUCTION, + MODIFICATION AND/OR DISTRIBUTION OF THE APPLE SOFTWARE, HOWEVER CAUSED + AND WHETHER UNDER THEORY OF CONTRACT, TORT (INCLUDING NEGLIGENCE), + STRICT LIABILITY OR OTHERWISE, EVEN IF APPLE HAS BEEN ADVISED OF THE + POSSIBILITY OF SUCH DAMAGE. + + EA1002 + 5/3/2013 + */ + +typedef enum { + NOBLUR, + BOXFILTER, + TENTFILTER +} BlurType; + + +@import UIKit; + +@interface UIImage (ImageEffects) + +- (UIImage *) applyLightEffect; +- (UIImage *) applyExtraLightEffect; +- (UIImage *) applyDarkEffect; +- (UIImage *) applyDarkEffectWithTent: (CGFloat) radius; +- (UIImage *) applyTintEffectWithColor:(UIColor *)tintColor; + +- (UIImage *)applyBlurWithRadius:(CGFloat)blurRadius blurType: (BlurType) blurType tintColor:(UIColor *)tintColor saturationDeltaFactor:(CGFloat)saturationDeltaFactor maskImage:(UIImage *)maskImage; + +@end + diff --git a/mobile/ios/ThirdParty/Apple/UIImage+ImageEffects.m b/mobile/ios/ThirdParty/Apple/UIImage+ImageEffects.m new file mode 100644 index 0000000000..343582852b --- /dev/null +++ b/mobile/ios/ThirdParty/Apple/UIImage+ImageEffects.m @@ -0,0 +1,288 @@ +/* + File: UIImage+ImageEffects.m + Abstract: This is a category of UIImage that adds methods to apply blur and tint effects to an image. This is the code you’ll want to look out to find out how to use vImage to efficiently calculate a blur. + Version: 1.0 + + Disclaimer: IMPORTANT: This Apple software is supplied to you by Apple + Inc. ("Apple") in consideration of your agreement to the following + terms, and your use, installation, modification or redistribution of + this Apple software constitutes acceptance of these terms. If you do + not agree with these terms, please do not use, install, modify or + redistribute this Apple software. + + In consideration of your agreement to abide by the following terms, and + subject to these terms, Apple grants you a personal, non-exclusive + license, under Apple's copyrights in this original Apple software (the + "Apple Software"), to use, reproduce, modify and redistribute the Apple + Software, with or without modifications, in source and/or binary forms; + provided that if you redistribute the Apple Software in its entirety and + without modifications, you must retain this notice and the following + text and disclaimers in all such redistributions of the Apple Software. + Neither the name, trademarks, service marks or logos of Apple Inc. may + be used to endorse or promote products derived from the Apple Software + without specific prior written permission from Apple. Except as + expressly stated in this notice, no other rights or licenses, express or + implied, are granted by Apple herein, including but not limited to any + patent rights that may be infringed by your derivative works or by other + works in which the Apple Software may be incorporated. + + The Apple Software is provided by Apple on an "AS IS" basis. APPLE + MAKES NO WARRANTIES, EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION + THE IMPLIED WARRANTIES OF NON-INFRINGEMENT, MERCHANTABILITY AND FITNESS + FOR A PARTICULAR PURPOSE, REGARDING THE APPLE SOFTWARE OR ITS USE AND + OPERATION ALONE OR IN COMBINATION WITH YOUR PRODUCTS. + + IN NO EVENT SHALL APPLE BE LIABLE FOR ANY SPECIAL, INDIRECT, INCIDENTAL + OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + INTERRUPTION) ARISING IN ANY WAY OUT OF THE USE, REPRODUCTION, + MODIFICATION AND/OR DISTRIBUTION OF THE APPLE SOFTWARE, HOWEVER CAUSED + AND WHETHER UNDER THEORY OF CONTRACT, TORT (INCLUDING NEGLIGENCE), + STRICT LIABILITY OR OTHERWISE, EVEN IF APPLE HAS BEEN ADVISED OF THE + POSSIBILITY OF SUCH DAMAGE. + + Copyright (C) 2013 Apple Inc. All Rights Reserved. + + + Copyright © 2013 Apple Inc. All rights reserved. + WWDC 2013 License + + NOTE: This Apple Software was supplied by Apple as part of a WWDC 2013 + Session. Please refer to the applicable WWDC 2013 Session for further + information. + + IMPORTANT: This Apple software is supplied to you by Apple Inc. + ("Apple") in consideration of your agreement to the following terms, and + your use, installation, modification or redistribution of this Apple + software constitutes acceptance of these terms. If you do not agree with + these terms, please do not use, install, modify or redistribute this + Apple software. + + In consideration of your agreement to abide by the following terms, and + subject to these terms, Apple grants you a non-exclusive license, under + Apple's copyrights in this original Apple software (the "Apple + Software"), to use, reproduce, modify and redistribute the Apple + Software, with or without modifications, in source and/or binary forms; + provided that if you redistribute the Apple Software in its entirety and + without modifications, you must retain this notice and the following + text and disclaimers in all such redistributions of the Apple Software. + Neither the name, trademarks, service marks or logos of Apple Inc. may + be used to endorse or promote products derived from the Apple Software + without specific prior written permission from Apple. Except as + expressly stated in this notice, no other rights or licenses, express or + implied, are granted by Apple herein, including but not limited to any + patent rights that may be infringed by your derivative works or by other + works in which the Apple Software may be incorporated. + + The Apple Software is provided by Apple on an "AS IS" basis. APPLE MAKES + NO WARRANTIES, EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION THE + IMPLIED WARRANTIES OF NON-INFRINGEMENT, MERCHANTABILITY AND FITNESS FOR + A PARTICULAR PURPOSE, REGARDING THE APPLE SOFTWARE OR ITS USE AND + OPERATION ALONE OR IN COMBINATION WITH YOUR PRODUCTS. + + IN NO EVENT SHALL APPLE BE LIABLE FOR ANY SPECIAL, INDIRECT, INCIDENTAL + OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + INTERRUPTION) ARISING IN ANY WAY OUT OF THE USE, REPRODUCTION, + MODIFICATION AND/OR DISTRIBUTION OF THE APPLE SOFTWARE, HOWEVER CAUSED + AND WHETHER UNDER THEORY OF CONTRACT, TORT (INCLUDING NEGLIGENCE), + STRICT LIABILITY OR OTHERWISE, EVEN IF APPLE HAS BEEN ADVISED OF THE + POSSIBILITY OF SUCH DAMAGE. + + EA1002 + 5/3/2013 + */ + +#import "UIImage+ImageEffects.h" + +@import Accelerate; +#import + + +@implementation UIImage (ImageEffects) + + +- (UIImage *)applyLightEffect +{ + UIColor *tintColor = [UIColor colorWithWhite:1.0 alpha:0.3]; + return [self applyBlurWithRadius:30 blurType: BOXFILTER tintColor:tintColor saturationDeltaFactor:1.8 maskImage:nil]; +} + + +- (UIImage *)applyExtraLightEffect +{ + UIColor *tintColor = [UIColor colorWithWhite:0.97 alpha:0.82]; + return [self applyBlurWithRadius:20 blurType: BOXFILTER tintColor:tintColor saturationDeltaFactor:1.8 maskImage:nil]; +} + + +- (UIImage *)applyDarkEffect +{ + UIColor *tintColor = [UIColor colorWithWhite:0.11 alpha:0.73]; + return [self applyBlurWithRadius:10 blurType: BOXFILTER tintColor:tintColor saturationDeltaFactor:1.8 maskImage:nil]; +} + +- (UIImage *) applyDarkEffectWithTent: (CGFloat) radius +{ + UIColor *tintColor = [UIColor colorWithWhite:0.11 alpha:0.5]; + return [self applyBlurWithRadius:radius blurType: TENTFILTER tintColor:tintColor saturationDeltaFactor:1.8 maskImage:nil]; +} + +- (UIImage *)applyTintEffectWithColor:(UIColor *)tintColor +{ + const CGFloat EffectColorAlpha = 0.6; + UIColor *effectColor = tintColor; + size_t componentCount = CGColorGetNumberOfComponents(tintColor.CGColor); + if (componentCount == 2) { + CGFloat b; + if ([tintColor getWhite:&b alpha:NULL]) { + effectColor = [UIColor colorWithWhite:b alpha:EffectColorAlpha]; + } + } + else { + CGFloat r, g, b; + if ([tintColor getRed:&r green:&g blue:&b alpha:NULL]) { + effectColor = [UIColor colorWithRed:r green:g blue:b alpha:EffectColorAlpha]; + } + } + return [self applyBlurWithRadius:10 blurType: BOXFILTER tintColor:effectColor saturationDeltaFactor:-1.0 maskImage:nil]; +} + + +- (UIImage *)applyBlurWithRadius:(CGFloat)blurRadius blurType: (BlurType) blurType tintColor:(UIColor *)tintColor saturationDeltaFactor:(CGFloat)saturationDeltaFactor maskImage:(UIImage *)maskImage +{ + // Check pre-conditions. + if (self.size.width < 1 || self.size.height < 1) { + NSLog (@"*** error: invalid size: (%.2f x %.2f). Both dimensions must be >= 1: %@", self.size.width, self.size.height, self); + return nil; + } + if (!self.CGImage) { + NSLog (@"*** error: image must be backed by a CGImage: %@", self); + return nil; + } + if (maskImage && !maskImage.CGImage) { + NSLog (@"*** error: maskImage must be backed by a CGImage: %@", maskImage); + return nil; + } + + CGRect imageRect = { CGPointZero, self.size }; + UIImage *effectImage = self; + + BOOL hasBlur = blurRadius > __FLT_EPSILON__ && blurType != NOBLUR; + BOOL hasSaturationChange = fabs(saturationDeltaFactor - 1.) > __FLT_EPSILON__; + if (hasBlur || hasSaturationChange) { + UIGraphicsBeginImageContextWithOptions(self.size, NO, [[UIScreen mainScreen] scale]); + CGContextRef effectInContext = UIGraphicsGetCurrentContext(); + CGContextScaleCTM(effectInContext, 1.0, -1.0); + CGContextTranslateCTM(effectInContext, 0, -self.size.height); + CGContextDrawImage(effectInContext, imageRect, self.CGImage); + + vImage_Buffer effectInBuffer; + effectInBuffer.data = CGBitmapContextGetData(effectInContext); + effectInBuffer.width = CGBitmapContextGetWidth(effectInContext); + effectInBuffer.height = CGBitmapContextGetHeight(effectInContext); + effectInBuffer.rowBytes = CGBitmapContextGetBytesPerRow(effectInContext); + + UIGraphicsBeginImageContextWithOptions(self.size, NO, [[UIScreen mainScreen] scale]); + CGContextRef effectOutContext = UIGraphicsGetCurrentContext(); + vImage_Buffer effectOutBuffer; + effectOutBuffer.data = CGBitmapContextGetData(effectOutContext); + effectOutBuffer.width = CGBitmapContextGetWidth(effectOutContext); + effectOutBuffer.height = CGBitmapContextGetHeight(effectOutContext); + effectOutBuffer.rowBytes = CGBitmapContextGetBytesPerRow(effectOutContext); + + if (hasBlur) { + // A description of how to compute the box kernel width from the Gaussian + // radius (aka standard deviation) appears in the SVG spec: + // http://www.w3.org/TR/SVG/filters.html#feGaussianBlurElement + // + // For larger values of 's' (s >= 2.0), an approximation can be used: Three + // successive box-blurs build a piece-wise quadratic convolution kernel, which + // approximates the Gaussian kernel to within roughly 3%. + // + // let d = floor(s * 3*sqrt(2*pi)/4 + 0.5) + // + // ... if d is odd, use three box-blurs of size 'd', centered on the output pixel. + // + CGFloat inputRadius = blurRadius * [[UIScreen mainScreen] scale]; + uint32_t radius = floor(inputRadius * 3. * sqrt(2 * M_PI) / 4 + 0.5); + if (radius % 2 != 1) { + radius += 1; // force radius to be odd so that the three box-blur methodology works. + } + + if (blurType == BOXFILTER) + { + vImageBoxConvolve_ARGB8888(&effectInBuffer, &effectOutBuffer, NULL, 0, 0, radius, radius, 0, kvImageEdgeExtend); + vImageBoxConvolve_ARGB8888(&effectOutBuffer, &effectInBuffer, NULL, 0, 0, radius, radius, 0, kvImageEdgeExtend); + vImageBoxConvolve_ARGB8888(&effectInBuffer, &effectOutBuffer, NULL, 0, 0, radius, radius, 0, kvImageEdgeExtend); + } + else + { + vImageTentConvolve_ARGB8888(&effectInBuffer, &effectOutBuffer, NULL, 0, 0, radius, radius, 0, kvImageEdgeExtend); + } + } + BOOL effectImageBuffersAreSwapped = NO; + if (hasSaturationChange) { + CGFloat s = saturationDeltaFactor; + CGFloat floatingPointSaturationMatrix[] = { + 0.0722 + 0.9278 * s, 0.0722 - 0.0722 * s, 0.0722 - 0.0722 * s, 0, + 0.7152 - 0.7152 * s, 0.7152 + 0.2848 * s, 0.7152 - 0.7152 * s, 0, + 0.2126 - 0.2126 * s, 0.2126 - 0.2126 * s, 0.2126 + 0.7873 * s, 0, + 0, 0, 0, 1, + }; + const int32_t divisor = 256; + NSUInteger matrixSize = sizeof(floatingPointSaturationMatrix)/sizeof(floatingPointSaturationMatrix[0]); + int16_t saturationMatrix[matrixSize]; + for (NSUInteger i = 0; i < matrixSize; ++i) { + saturationMatrix[i] = (int16_t)roundf(floatingPointSaturationMatrix[i] * divisor); + } + if (hasBlur) { + vImageMatrixMultiply_ARGB8888(&effectOutBuffer, &effectInBuffer, saturationMatrix, divisor, NULL, NULL, kvImageNoFlags); + effectImageBuffersAreSwapped = YES; + } + else { + vImageMatrixMultiply_ARGB8888(&effectInBuffer, &effectOutBuffer, saturationMatrix, divisor, NULL, NULL, kvImageNoFlags); + } + } + if (!effectImageBuffersAreSwapped) + effectImage = UIGraphicsGetImageFromCurrentImageContext(); + UIGraphicsEndImageContext(); + + if (effectImageBuffersAreSwapped) + effectImage = UIGraphicsGetImageFromCurrentImageContext(); + UIGraphicsEndImageContext(); + } + + // Set up output context. + UIGraphicsBeginImageContextWithOptions(self.size, NO, [[UIScreen mainScreen] scale]); + CGContextRef outputContext = UIGraphicsGetCurrentContext(); + CGContextScaleCTM(outputContext, 1.0, -1.0); + CGContextTranslateCTM(outputContext, 0, -self.size.height); + + // Draw effect image. + if (hasBlur) { + CGContextSaveGState(outputContext); + if (maskImage) { + CGContextClipToMask(outputContext, imageRect, maskImage.CGImage); + } + CGContextDrawImage(outputContext, imageRect, effectImage.CGImage); + CGContextRestoreGState(outputContext); + } + + // Add in color tint. + if (tintColor) { + CGContextSaveGState(outputContext); + CGContextSetFillColorWithColor(outputContext, tintColor.CGColor); + CGContextFillRect(outputContext, imageRect); + CGContextRestoreGState(outputContext); + } + + // Output image is ready. + UIImage *outputImage = UIGraphicsGetImageFromCurrentImageContext(); + UIGraphicsEndImageContext(); + + return outputImage; +} + +@end + diff --git a/mobile/ios/ThirdParty/Box/Box.swift b/mobile/ios/ThirdParty/Box/Box.swift new file mode 100644 index 0000000000..ce514379d4 --- /dev/null +++ b/mobile/ios/ThirdParty/Box/Box.swift @@ -0,0 +1,33 @@ +// Copyright (c) 2014 Rob Rix. All rights reserved. + +/// Wraps a type `T` in a reference type. +/// +/// Typically this is used to work around limitations of value types (for example, the lack of codegen for recursive value types and type-parameterized enums with >1 case). It is also useful for sharing a single (presumably large) value without copying it. +public final class Box: BoxType, CustomStringConvertible { + /// Initializes a `Box` with the given value. + public init(_ value: T) { + self.value = value + } + + + /// Constructs a `Box` with the given `value`. + public class func unit(_ value: T) -> Box { + return Box(value) + } + + + /// The (immutable) value wrapped by the receiver. + public let value: T + + /// Constructs a new Box by transforming `value` by `f`. + public func map(_ f: (T) -> U) -> Box { + return Box(f(value)) + } + + + // MARK: Printable + + public var description: String { + return String(describing: value) + } +} diff --git a/mobile/ios/ThirdParty/Box/BoxType.swift b/mobile/ios/ThirdParty/Box/BoxType.swift new file mode 100644 index 0000000000..a91e04d8d5 --- /dev/null +++ b/mobile/ios/ThirdParty/Box/BoxType.swift @@ -0,0 +1,46 @@ +// Copyright (c) 2014 Rob Rix. All rights reserved. + +// MARK: BoxType + +/// The type conformed to by all boxes. +public protocol BoxType { + /// The type of the wrapped value. + associatedtype Value + + /// Initializes an intance of the type with a value. + init(_ value: Value) + + /// The wrapped value. + var value: Value { get } +} + +/// The type conformed to by mutable boxes. +public protocol MutableBoxType: BoxType { + /// The (mutable) wrapped value. + var value: Value { get set } +} + + +// MARK: Equality + +/// Equality of `BoxType`s of `Equatable` types. +/// +/// We cannot declare that e.g. `Box` conforms to `Equatable`, so this is a relatively ad hoc definition. +public func == (lhs: B, rhs: B) -> Bool where B.Value: Equatable { + return lhs.value == rhs.value +} + +/// Inequality of `BoxType`s of `Equatable` types. +/// +/// We cannot declare that e.g. `Box` conforms to `Equatable`, so this is a relatively ad hoc definition. +public func != (lhs: B, rhs: B) -> Bool where B.Value: Equatable { + return lhs.value != rhs.value +} + + +// MARK: Map + +/// Maps the value of a box into a new box. +public func map(_ v: B, f: (B.Value) -> C.Value) -> C { + return C(f(v.value)) +} diff --git a/mobile/ios/ThirdParty/Box/LICENSE b/mobile/ios/ThirdParty/Box/LICENSE new file mode 100644 index 0000000000..3026ee1902 --- /dev/null +++ b/mobile/ios/ThirdParty/Box/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2014 Rob Rix + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/mobile/ios/ThirdParty/Box/MutableBox.swift b/mobile/ios/ThirdParty/Box/MutableBox.swift new file mode 100644 index 0000000000..2fe30b6c6d --- /dev/null +++ b/mobile/ios/ThirdParty/Box/MutableBox.swift @@ -0,0 +1,27 @@ +// Copyright (c) 2014 Rob Rix. All rights reserved. + +/// Wraps a type `T` in a mutable reference type. +/// +/// While this, like `Box` could be used to work around limitations of value types, it is much more useful for sharing a single mutable value such that mutations are shared. +/// +/// As with all mutable state, this should be used carefully, for example as an optimization, rather than a default design choice. Most of the time, `Box` will suffice where any `BoxType` is needed. +public final class MutableBox: MutableBoxType, CustomStringConvertible { + /// Initializes a `MutableBox` with the given value. + public init(_ value: T) { + self.value = value + } + + /// The (mutable) value wrapped by the receiver. + public var value: T + + /// Constructs a new MutableBox by transforming `value` by `f`. + public func map(_ f: (T) -> U) -> MutableBox { + return MutableBox(f(value)) + } + + // MARK: Printable + + public var description: String { + return String(describing: value) + } +} diff --git a/mobile/ios/ThirdParty/Box/README.md b/mobile/ios/ThirdParty/Box/README.md new file mode 100644 index 0000000000..b9948a020c --- /dev/null +++ b/mobile/ios/ThirdParty/Box/README.md @@ -0,0 +1,58 @@ +# Box + +This is a Swift microframework which implements `Box` & `MutableBox`, with implementations of `==`/`!=` where `T`: `Equatable`. + +`Box` is typically used to work around limitations of value types: + +- recursive `struct`s/`enum`s +- type-parameterized `enum`s where more than one `case` has a value + +## Use + +Wrapping & unwrapping a `Box`: + +```swift +// Wrap: +let box = Box(1) + +// Unwrap: +let value = box.value +``` + +Changing the value of a `MutableBox`: + +```swift +// Mutation: +let mutableBox = MutableBox(1) +mutableBox.value = 2 +``` + +Building a recursive value type: + +```swift +struct BinaryTree { + let value: Int + let left: Box? + let right: Box? +} +``` + +Building a parameterized `enum`: + +```swift +enum Result { + case Success(Box) + case Failure(NSError) +} +``` + +See the sources for more details. + +## Integration + +1. Add this repo as a submodule in e.g. `External/Box`: + + git submodule add https://github.com/robrix/Box.git External/Box +2. Drag `Box.xcodeproj` into your `.xcworkspace`/`.xcodeproj`. +3. Add `Box.framework` to your target’s `Link Binary With Libraries` build phase. +4. You may also want to add a `Copy Files` phase which copies `Box.framework` (and any other framework dependencies you need) into your bundle’s `Frameworks` directory. If your target is a framework, you may instead want the client app to include `Box.framework`. diff --git a/mobile/ios/ThirdParty/BuddyBuild/BuddyBuildSDK.framework/BuddyBuildSDK b/mobile/ios/ThirdParty/BuddyBuild/BuddyBuildSDK.framework/BuddyBuildSDK new file mode 100644 index 0000000000..f8a850dbc2 Binary files /dev/null and b/mobile/ios/ThirdParty/BuddyBuild/BuddyBuildSDK.framework/BuddyBuildSDK differ diff --git a/mobile/ios/ThirdParty/BuddyBuild/BuddyBuildSDK.framework/Headers/BuddyBuildSDK.h b/mobile/ios/ThirdParty/BuddyBuild/BuddyBuildSDK.framework/Headers/BuddyBuildSDK.h new file mode 100644 index 0000000000..ca31088ac5 --- /dev/null +++ b/mobile/ios/ThirdParty/BuddyBuild/BuddyBuildSDK.framework/Headers/BuddyBuildSDK.h @@ -0,0 +1,147 @@ +// Copyright (c) 2015 Doe Pics Hit, Inc. All rights reserved. + +#import +#import + +typedef NSString*(^BBReturnNSStringCallback)(); +typedef BOOL (^BBReturnBooleanCallback)(); +typedef void (^BBCallback)(); + +@interface BuddyBuildSDK : NSObject + +// Deprecated ++ (void)setup:(id)bbAppDelegate; + +/** + * Initialize the SDK + * + * This should be called at (or near) the start of the appdelegate + */ ++ (void)setup; + +/* + * Associate arbitrary key/value pairs with your crash reports + * which will be visible from the buddybuild dashboard + */ ++ (void) setCrashMetadataObject:(id)object forKey:(NSString*)key; + +/* + * Programatically trigger the screenshot feedback UI without pressing the screenshot buttons + * If you have screenshot feedback disabled through the buddybuild setting, + * you can still trigger it by calling this method + */ + ++ (void)takeScreenshotAndShowFeedbackScreen; + +/* + * If you distribute a build to someone with their email address, buddybuild can + * figure out who they are and attach their info to feedback and crash reports. + * + * However, if you send out a build to a mailing list, or through TestFlight or + * the App Store we are unable to infer who they are. If you see 'Unknown User' + * this is likely the cause. + + * Often you'll know the identity of your user, for example, after they've + * logged in. You can provide buddybuild a callback to identify the current user. + */ + ++ (void)setUserDisplayNameCallback:(BBReturnNSStringCallback)bbCallback; + +/* + * You might have API keys and other secrets that your app needs to consume. + * However, you may not want to check these secrets into the source code. + * + * You can provide your secrets to buddybuild. Buddybuild can then expose them + * to you at build time through environment variables. These secrets can also be + * configured to be included into built app. We obfuscate the device keys to + * prevent unauthorized access. + */ ++ (NSString*)valueForDeviceKey:(NSString*)bbKey; + +/* + * To temporarily disable screenshot interception you can provide a callback + * here. + * + * When screenshotting is turned on through a buddybuild setting, and no + * callback is provided then screenshotting is by default on. + * + * If screenshotting is disabled through the buddybuild setting, then this + * callback has no effect + * + */ ++ (void)setScreenshotAllowedCallback:(BBReturnBooleanCallback)bbCallback; + +/* + * Once a piece of feedback is sent this callback will be called + * so you can take additional actions if necessary + */ ++ (void)setScreenshotFeedbackSentCallback:(BBCallback)bbCallback; + +/* + * Once a crash report is sent this callback will be called + * so you can take additional actions if necessary + */ ++ (void)setCrashReportSentCallback:(BBCallback)bbCallback; + +/* + * Buddybuild Build Number + */ ++ (NSString*)buildNumber; + +/* + * Scheme + */ ++ (NSString*)scheme; + +/* + * App ID + */ ++ (NSString*)appID; + +/* + * Build ID + */ ++ (NSString*)buildID; + +/* + * Build Configuration + */ + ++ (NSString*)buildConfiguration; + +/* + * Branch name for this build + */ + ++ (NSString*)branchName; + +/* + * Returns the user's email or more specifically, the email that was used to download and deploy the build. + * Returns "Unknown User" in cases where buddybuild is unable to identify the user. + * This is the same email seen in crash instances and feedbacks in the dashboard + * NOTE: To be called after [BuddyBuildSDK setup] + * this is different than the one returned in the user display name callback. +*/ ++ (NSString*)userEmail; + +/* Manually invoke the screenshot tutorial + * If you don't want it to appear on app launch, disable it in the + * dashboard by going to settings -> buddybuildSDK -> Feature Settings and turning off the screenshot tutorial + * You will be able to show it at any time from anywhere in your app + */ ++ (void) showScreenshotTutorial; + + ++(void) crash; + +@end + +@interface UIView (BuddyBuildSDK) + +// Certain features of buddybuild involve capturing the screen (either through a static screenshot, or as a video for instant replays in crash reporting or video feedback. +// Your app may contain certain sensitive customer information that you do not want to be included in the video. +// If you set this property to be true, this view will be redacted from the screen capture and blacked out + +@property (nonatomic, assign) BOOL buddybuildViewIsPrivate; + +@end diff --git a/mobile/ios/ThirdParty/BuddyBuild/BuddyBuildSDK.framework/build.num b/mobile/ios/ThirdParty/BuddyBuild/BuddyBuildSDK.framework/build.num new file mode 100644 index 0000000000..6e4f7dfb64 --- /dev/null +++ b/mobile/ios/ThirdParty/BuddyBuild/BuddyBuildSDK.framework/build.num @@ -0,0 +1 @@ +d1039cf diff --git a/mobile/ios/ThirdParty/FilledPageControl.swift b/mobile/ios/ThirdParty/FilledPageControl.swift new file mode 100644 index 0000000000..7631f12c67 --- /dev/null +++ b/mobile/ios/ThirdParty/FilledPageControl.swift @@ -0,0 +1,137 @@ +// FilledPageControl +// +// Copyright (c) 2016 Kyle Zaragoza +// MIT License + +import UIKit + +@IBDesignable open class FilledPageControl: UIView { + + // MARK: - PageControl + + @IBInspectable open var pageCount: Int = 0 { + didSet { + updateNumberOfPages(pageCount) + } + } + @IBInspectable open var progress: CGFloat = 0 { + didSet { + updateActivePageIndicatorMasks(progress) + } + } + open var currentPage: Int { + return Int(round(progress)) + } + + + // MARK: - Appearance + + override open var tintColor: UIColor! { + didSet { + inactiveLayers.forEach() { $0.backgroundColor = tintColor.cgColor } + } + } + @IBInspectable open var inactiveRingWidth: CGFloat = 1 { + didSet { + updateActivePageIndicatorMasks(progress) + } + } + @IBInspectable open var indicatorPadding: CGFloat = 10 { + didSet { + layoutPageIndicators(inactiveLayers) + } + } + @IBInspectable open var indicatorRadius: CGFloat = 5 { + didSet { + layoutPageIndicators(inactiveLayers) + } + } + + fileprivate var indicatorDiameter: CGFloat { + return indicatorRadius * 2 + } + fileprivate var inactiveLayers = [CALayer]() + + + // MARK: - State Update + + fileprivate func updateNumberOfPages(_ count: Int) { + // no need to update + guard count != inactiveLayers.count else { return } + // reset current layout + inactiveLayers.forEach() { $0.removeFromSuperlayer() } + inactiveLayers = [CALayer]() + // add layers for new page count + inactiveLayers = stride(from: 0, to:count, by:1).map() { _ in + let layer = CALayer() + layer.backgroundColor = self.tintColor.cgColor + self.layer.addSublayer(layer) + return layer + } + layoutPageIndicators(inactiveLayers) + updateActivePageIndicatorMasks(progress) + self.invalidateIntrinsicContentSize() + } + + + // MARK: - Layout + + fileprivate func updateActivePageIndicatorMasks(_ progress: CGFloat) { + // ignore if progress is outside of page indicators' bounds + guard progress >= 0 && progress <= CGFloat(pageCount - 1) else { return } + + // mask rect w/ default stroke width + let insetRect = CGRect(x: 0, y: 0, width: indicatorDiameter, height: indicatorDiameter).insetBy(dx: inactiveRingWidth, dy: inactiveRingWidth) + let leftPageFloat = trunc(progress) + let leftPageInt = Int(progress) + + // inset right moving page indicator + let spaceToMove = insetRect.width / 2 + let percentPastLeftIndicator = progress - leftPageFloat + let additionalSpaceToInsetRight = spaceToMove * percentPastLeftIndicator + let closestRightInsetRect = insetRect.insetBy(dx: additionalSpaceToInsetRight, dy: additionalSpaceToInsetRight) + + // inset left moving page indicator + let additionalSpaceToInsetLeft = (1 - percentPastLeftIndicator) * spaceToMove + let closestLeftInsetRect = insetRect.insetBy(dx: additionalSpaceToInsetLeft, dy: additionalSpaceToInsetLeft) + + // adjust masks + for (idx, layer) in inactiveLayers.enumerated() { + let maskLayer = CAShapeLayer() + maskLayer.fillRule = kCAFillRuleEvenOdd + + let boundsPath = UIBezierPath(rect: layer.bounds) + let circlePath: UIBezierPath + if leftPageInt == idx { + circlePath = UIBezierPath(ovalIn: closestLeftInsetRect) + } else if leftPageInt + 1 == idx { + circlePath = UIBezierPath(ovalIn: closestRightInsetRect) + } else { + circlePath = UIBezierPath(ovalIn: insetRect) + } + boundsPath.append(circlePath) + maskLayer.path = boundsPath.cgPath + layer.mask = maskLayer + } + } + + fileprivate func layoutPageIndicators(_ layers: [CALayer]) { + let layerDiameter = indicatorRadius * 2 + var layerFrame = CGRect(x: 0, y: 0, width: layerDiameter, height: layerDiameter) + layers.forEach() { layer in + layer.cornerRadius = self.indicatorRadius + layer.frame = layerFrame + layerFrame.origin.x += layerDiameter + indicatorPadding + } + } + + override open var intrinsicContentSize: CGSize { + return sizeThatFits(CGSize.zero) + } + + override open func sizeThatFits(_ size: CGSize) -> CGSize { + let layerDiameter = indicatorRadius * 2 + return CGSize(width: CGFloat(inactiveLayers.count) * layerDiameter + CGFloat(inactiveLayers.count - 1) * indicatorPadding, + height: layerDiameter) + } +} diff --git a/mobile/ios/ThirdParty/Leanplum/Leanplum.framework/Headers/LPInbox.h b/mobile/ios/ThirdParty/Leanplum/Leanplum.framework/Headers/LPInbox.h new file mode 100644 index 0000000000..a2339e5d1a --- /dev/null +++ b/mobile/ios/ThirdParty/Leanplum/Leanplum.framework/Headers/LPInbox.h @@ -0,0 +1,193 @@ +// +// LPInbox.h +// Leanplum +// +// Created by Aleksandar Gyorev on 05/08/15. +// Copyright (c) 2015 Leanplum, Inc. All rights reserved. +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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 + +#pragma mark - LPInboxMessage interface + +@interface LPInboxMessage : NSObject + +#pragma mark - LPInboxMessage methods + +/** + * Returns the message identifier of the inbox message. + */ +- (NSString *)messageId; + +/** + * Returns the title of the inbox message. + */ +- (NSString *)title; + +/** + * Returns the subtitle of the inbox message. + */ +- (NSString *)subtitle; + +/** + * Returns the image path of the inbox message. Can be nil. + * Use with [UIImage contentsOfFile:]. + */ +- (NSString *)imageFilePath; + +/** + * Returns the image URL of the inbox message. + * You can safely use this with prefetching enabled. + * It will return the file URL path instead if the image is in cache. + */ +- (NSURL *)imageURL; + +/** + * Returns the data of the inbox message. Advanced use only. + */ +- (NSDictionary *)data; + +/** + * Returns the delivery timestamp of the inbox message. + */ +- (NSDate *)deliveryTimestamp; + +/** + * Return the expiration timestamp of the inbox message. + */ +- (NSDate *)expirationTimestamp; + +/** + * Returns YES if the inbox message is read. + */ +- (BOOL)isRead; + +/** + * Read the inbox message, marking it as read and invoking its open action. + */ +- (void)read; + +/** + * Remove the inbox message from the inbox. + */ +- (void)remove; + +@end + +#pragma mark - LPInbox interface + +/** + * This block is used when you define a callback. + */ +typedef void (^LeanplumInboxChangedBlock)(void); +typedef void (^LeanplumInboxSyncedBlock)(BOOL success); + +@interface LPInbox : NSObject + +#pragma mark - LPInbox methods + +/** + * Returns the number of all inbox messages on the device. + */ +- (NSUInteger)count; + +/** + * Returns the number of the unread inbox messages on the device. + */ +- (NSUInteger)unreadCount; + +/** + * Returns the identifiers of all inbox messages on the device sorted in ascending + * chronological order, i.e. the id of the oldest message is the first one, and the most + * recent one is the last one in the array. + */ +- (NSArray *)messagesIds; + +/** + * Returns an array containing all of the inbox messages (as LPInboxMessage objects) + * on the device, sorted in ascending chronological order, i.e. the oldest message is the + * first one, and the most recent one is the last one in the array. + */ +- (NSArray *)allMessages; + +/** + * Returns an array containing all of the unread inbox messages on the device, sorted + * in ascending chronological order, i.e. the oldest message is the first one, and the + * most recent one is the last one in the array. + */ +- (NSArray *)unreadMessages; + +/** + * Returns the inbox messages associated with the given messageId identifier. + */ +- (LPInboxMessage *)messageForId:(NSString *)messageId; + +/** + * Call this method if you don't want Inbox images to be prefetched. + * Useful if you only want to deal with image URL. + */ +- (void)disableImagePrefetching; + +/** + * Block to call when the inbox receive new values from the server. + * This will be called on start, and also later on if the user is in an experiment + * that can update in realtime. + */ +- (void)onChanged:(LeanplumInboxChangedBlock)block; + +/** + * Block to call when forceContentUpdate was called. + * Returns true if syncing was successful. + * Note: use onChanged: for UI. + */ +- (void)onForceContentUpdate:(LeanplumInboxSyncedBlock)block; + +/** + @{ + * Adds a responder to be executed when an event happens. + * Uses NSInvocation instead of blocks. + * @see [Leanplum onStartResponse:] + */ +- (void)addInboxChangedResponder:(id)responder withSelector:(SEL)selector; +- (void)removeInboxChangedResponder:(id)responder withSelector:(SEL)selector; +/**@}*/ + +@end + +#pragma mark - LPNewsfeed for backwards compatibility +@interface LPNewsfeedMessage : LPInboxMessage + +@end + +typedef void (^LeanplumNewsfeedChangedBlock)(void); + +@interface LPNewsfeed : NSObject + ++ (LPNewsfeed *)sharedState; +- (NSUInteger)count; +- (NSUInteger)unreadCount; +- (NSArray *)messagesIds; +- (NSArray *)allMessages; +- (NSArray *)unreadMessages; +- (void)onChanged:(LeanplumNewsfeedChangedBlock)block; +- (LPNewsfeedMessage *)messageForId:(NSString *)messageId; +- (void)addNewsfeedChangedResponder:(id)responder withSelector:(SEL)selector __attribute__((deprecated)); +- (void)removeNewsfeedChangedResponder:(id)responder withSelector:(SEL)selector __attribute__((deprecated)); + +@end diff --git a/mobile/ios/ThirdParty/Leanplum/Leanplum.framework/Headers/Leanplum.h b/mobile/ios/ThirdParty/Leanplum/Leanplum.framework/Headers/Leanplum.h new file mode 100644 index 0000000000..322df6dc04 --- /dev/null +++ b/mobile/ios/ThirdParty/Leanplum/Leanplum.framework/Headers/Leanplum.h @@ -0,0 +1,929 @@ +// +// Leanplum.h +// Leanplum iOS SDK Version 2.0.4 +// +// Copyright (c) 2012 Leanplum, Inc. All rights reserved. +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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 +#import +#import "LPInbox.h" + +#ifndef LP_NOT_TV +#define LP_NOT_TV (!defined(TARGET_OS_TV) || !TARGET_OS_TV) +#endif + +#define _LP_DEFINE_HELPER(name,val,type) LPVar* name; \ +static void __attribute__((constructor)) initialize_##name() { \ +@autoreleasepool { \ +name = [LPVar define:[@#name stringByReplacingOccurrencesOfString:@"_" withString:@"."] with##type:val]; \ +} \ +} + +/** + * @defgroup Macros Variable Macros + * Use these macros to define variables inside your app. + * Underscores within variable names will nest variables within groups. + * To define variables in a more custom way, copy and modify + * the template above in your own code. + * @see LPVar + * @{ + */ +#define DEFINE_VAR_INT(name,val) _LP_DEFINE_HELPER(name, val, Int) +#define DEFINE_VAR_BOOL(name,val) _LP_DEFINE_HELPER(name, val, Bool) +#define DEFINE_VAR_STRING(name,val) _LP_DEFINE_HELPER(name, val, String) +#define DEFINE_VAR_NUMBER(name,val) _LP_DEFINE_HELPER(name, val, Number) +#define DEFINE_VAR_FLOAT(name,val) _LP_DEFINE_HELPER(name, val, Float) +#define DEFINE_VAR_CGFLOAT(name,val) _LP_DEFINE_HELPER(name, val, CGFloat) +#define DEFINE_VAR_DOUBLE(name,val) _LP_DEFINE_HELPER(name, val, Double) +#define DEFINE_VAR_SHORT(name,val) _LP_DEFINE_HELPER(name, val, Short) +#define DEFINE_VAR_LONG(name,val) _LP_DEFINE_HELPER(name, val, Long) +#define DEFINE_VAR_CHAR(name,val) _LP_DEFINE_HELPER(name, val, Char) +#define DEFINE_VAR_LONG_LONG(name,val) _LP_DEFINE_HELPER(name, val, LongLong) +#define DEFINE_VAR_INTEGER(name,val) _LP_DEFINE_HELPER(name, val, Integer) +#define DEFINE_VAR_UINT(name,val) _LP_DEFINE_HELPER(name, val, UnsignedInt) +#define DEFINE_VAR_UCHAR(name,val) _LP_DEFINE_HELPER(name, val, UnsignedChar) +#define DEFINE_VAR_ULONG(name,val) _LP_DEFINE_HELPER(name, val, UnsignedLong) +#define DEFINE_VAR_UINTEGER(name,val) _LP_DEFINE_HELPER(name, val, UnsignedInteger) +#define DEFINE_VAR_USHORT(name,val) _LP_DEFINE_HELPER(name, val, UnsignedShort) +#define DEFINE_VAR_ULONGLONG(name,val) _LP_DEFINE_HELPER(name, val, UnsignedLongLong) +#define DEFINE_VAR_UNSIGNED_INT(name,val) _LP_DEFINE_HELPER(name, val, UnsignedInt) +#define DEFINE_VAR_UNSIGNED_INTEGER(name,val) _LP_DEFINE_HELPER(name, val, UnsignedInteger) +#define DEFINE_VAR_UNSIGNED_CHAR(name,val) _LP_DEFINE_HELPER(name, val, UnsignedChar) +#define DEFINE_VAR_UNSIGNED_LONG(name,val) _LP_DEFINE_HELPER(name, val, UnsignedLong) +#define DEFINE_VAR_UNSIGNED_LONG_LONG(name,val) _LP_DEFINE_HELPER(name, val, UnsignedLongLong) +#define DEFINE_VAR_UNSIGNED_SHORT(name,val) _LP_DEFINE_HELPER(name, val, UnsignedShort) +#define DEFINE_VAR_FILE(name,filename) _LP_DEFINE_HELPER(name, filename, File) +#define DEFINE_VAR_DICTIONARY(name,dict) _LP_DEFINE_HELPER(name, dict, Dictionary) +#define DEFINE_VAR_ARRAY(name,array) _LP_DEFINE_HELPER(name, array, Array) +#define DEFINE_VAR_COLOR(name,val) _LP_DEFINE_HELPER(name, val, Color) + +#define DEFINE_VAR_DICTIONARY_WITH_OBJECTS_AND_KEYS(name,...) LPVar* name; \ +static void __attribute__((constructor)) initialize_##name() { \ +@autoreleasepool { \ +name = [LPVar define:[@#name stringByReplacingOccurrencesOfString:@"_" withString:@"."] withDictionary:[NSDictionary dictionaryWithObjectsAndKeys:__VA_ARGS__]]; \ +} \ +} + +#define DEFINE_VAR_ARRAY_WITH_OBJECTS(name,...) LPVar* name; \ +static void __attribute__((constructor)) initialize_##name() { \ +@autoreleasepool { \ +name = [LPVar define:[@#name stringByReplacingOccurrencesOfString:@"_" withString:@"."] withArray:[NSArray arrayWithObjects:__VA_ARGS__]]; \ +} \ +} +/**@}*/ + +/** + * Use this code in development mode (or in production), to use the advertising ID. + * It's useful in development mode so that we remember your device even if you reinstall your app. + * Since it's a MACRO, this won't get compiled into your app in production, and will be safe + * to submit to Apple. + */ +#define LEANPLUM_USE_ADVERTISING_ID \ + _Pragma("clang diagnostic push") \ + _Pragma("clang diagnostic ignored \"-Warc-performSelector-leaks\"") \ + id LeanplumIdentifierManager = [NSClassFromString(@"ASIdentifierManager") \ + performSelector:NSSelectorFromString(@"sharedManager")]; \ + if (floor(NSFoundationVersionNumber) <= 1299 /* NSFoundationVersionNumber_iOS_9_x_Max */ || \ + [LeanplumIdentifierManager performSelector: \ + NSSelectorFromString(@"isAdvertisingTrackingEnabled")]) { \ + /* < iOS10 || isAdvertisingTrackingEnabled */ \ + [Leanplum setDeviceId:[[LeanplumIdentifierManager performSelector: \ + NSSelectorFromString(@"advertisingIdentifier")] \ + performSelector:NSSelectorFromString(@"UUIDString")]]; \ + } \ + _Pragma("clang diagnostic pop") + +@class LPActionContext; +@class SKPaymentTransaction; +#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 80000 +@class NSExtensionContext; +#endif + +/** + * @defgroup _ Callback Blocks + * Those blocks are used when you define callbacks. + * @{ + */ +typedef void (^LeanplumStartBlock)(BOOL success); +typedef void (^LeanplumVariablesChangedBlock)(void); +typedef void (^LeanplumInterfaceChangedBlock)(void); +typedef void (^LeanplumSetLocationBlock)(BOOL success); +// Returns whether the action was handled. +typedef BOOL (^LeanplumActionBlock)(LPActionContext* context); +typedef void (^LeanplumHandleNotificationBlock)(void); +typedef void (^LeanplumShouldHandleNotificationBlock)(NSDictionary *userInfo, LeanplumHandleNotificationBlock response); +typedef NSUInteger LeanplumUIBackgroundFetchResult; // UIBackgroundFetchResult +typedef void (^LeanplumFetchCompletionBlock)(LeanplumUIBackgroundFetchResult result); +typedef void (^LeanplumPushSetupBlock)(void); +/**@}*/ + +/** + * Leanplum Action Kind Message types + * This is a bit-field. To choose both kinds, use + * kLeanplumActionKindMessage | kLeanplumActionKindAction + */ +typedef enum { + kLeanplumActionKindMessage = 0b1, + kLeanplumActionKindAction = 0b10, +} LeanplumActionKind; + +#define LP_PURCHASE_EVENT @"Purchase" + +@interface Leanplum : NSObject + +/** + * Optional. Sets the API server. The API path is of the form http[s]://hostname/servletName + * @param hostName The name of the API host, such as api.leanplum.com + * @param servletName The name of the API servlet, such as api + * @param ssl Whether to use SSL + */ ++ (void)setApiHostName:(NSString *)hostName withServletName:(NSString *)servletName usingSsl:(BOOL)ssl; + +/** + * Optional. Adjusts the network timeouts. + * The default timeout is 10 seconds for requests, and 15 seconds for file downloads. + * @{ + */ ++ (void)setNetworkTimeoutSeconds:(int)seconds; ++ (void)setNetworkTimeoutSeconds:(int)seconds forDownloads:(int)downloadSeconds; +/**@}*/ + +/** + * Sets whether to show the network activity indicator in the status bar when making requests. + * Default: YES. + */ ++ (void)setNetworkActivityIndicatorEnabled:(BOOL)enabled; + +/** + * Advanced: Whether new variables can be downloaded mid-session. By default, this is disabled. + * Currently, if this is enabled, new variables can only be downloaded if a push notification is sent + * while the app is running, and the notification's metadata hasn't be downloaded yet. + */ ++ (void)setCanDownloadContentMidSessionInProductionMode:(BOOL)value; + +/** + * Modifies the file hashing setting in development mode. + * By default, Leanplum will hash file variables to determine if they're modified and need + * to be uploaded to the server if we're running in the simulator. + * Setting this to NO will reduce startup latency in development mode, but it's possible + * that Leanplum will not always have the most up-to-date versions of your resources. + */ ++ (void)setFileHashingEnabledInDevelopmentMode:(BOOL)enabled; + +/** + * Sets whether to enable verbose logging in development mode. Default: NO. + */ ++ (void)setVerboseLoggingInDevelopmentMode:(BOOL)enabled; + +/** + * Sets a custom event name for in-app purchase tracking. Default: Purchase. + */ ++ (void)setInAppPurchaseEventName:(NSString *)event; + +/** + * @{ + * Must call either this or {@link setAppId:withProductionKey:} + * before issuing any calls to the API, including start. + * @param appId Your app ID. + * @param accessKey Your development key. + */ ++ (void)setAppId:(NSString *)appId withDevelopmentKey:(NSString *)accessKey; + +/** + * Must call either this or {@link Leanplum::setAppId:withDevelopmentKey:} + * before issuing any calls to the API, including start. + * @param appId Your app ID. + * @param accessKey Your production key. + */ ++ (void)setAppId:(NSString *)appId withProductionKey:(NSString *)accessKey; +/**@}*/ + +#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 80000 +/** + * Apps running as extensions need to call this before start. + * @param context The current extensionContext. You can get this from UIViewController. + */ ++ (void)setExtensionContext:(NSExtensionContext *)context; +#endif + +/** + * @{ + * Call this before start to allow your interfaces to change on the fly. + * Needed in development mode to enable the interface editor, as well as in production to allow + * changes to be applied. + */ ++ (void)allowInterfaceEditing __attribute__((deprecated("Use LeanplumUIEditor pod instead."))); + +/** + * Check if interface editing is enabled. + */ ++ (BOOL)interfaceEditingEnabled __attribute__((deprecated("Use LeanplumUIEditor pod instead."))); + +/**@}*/ + +/** + * Sets a custom device ID. For example, you may want to pass the advertising ID to do attribution. + * By default, the device ID is the identifier for vendor. + */ ++ (void)setDeviceId:(NSString *)deviceId; + +/** + * By default, Leanplum reports the version of your app using CFBundleVersion, which + * can be used for reporting and targeting on the Leanplum dashboard. + * If you wish to use CFBundleShortVersionString or any other string as the version, + * you can call this before your call to [Leanplum start] + */ ++ (void)setAppVersion:(NSString *)appVersion; + +/** + * @{ + * Syncs resources between Leanplum and the current app. + * You should only call this once, and before {@link start}. + * Deprecated. Use {@link syncResourcesAsync:} instead. + */ ++ (void)syncResources __attribute__((deprecated)); + +/** + * Syncs resources between Leanplum and the current app. + * You should only call this once, and before {@link start}. + * @param async Whether the call should be asynchronous. Resource syncing can take 1-2 seconds to + * index the app's resources. If async is set, resources may not be available immediately + * when the app starts. + */ ++ (void)syncResourcesAsync:(BOOL)async; + +/** + * Syncs resources between Leanplum and the current app. + * You should only call this once, and before {@link start}. + * Deprecated. Use {@link syncResourcePaths:excluding:async} instead. + * @param async Whether the call should be asynchronous. Resource syncing can take 1-2 seconds to + * index the app's resources. If async is set, resources may not be available immediately + * when the app starts. + * @param patternsToIncludeOrNil Limit paths to only those matching at least one pattern in this + * list. Supply nil to indicate no inclusion patterns. Paths are relative to the app's bundle. + * @param patternsToExcludeOrNil Exclude paths matching at least one of these patterns. + * Supply nil to indicate no exclusion patterns. + */ ++ (void)syncResourcePaths:(NSArray *)patternsToIncludeOrNil + excluding:(NSArray *)patternsToExcludeOrNil __attribute__((deprecated)); + +/** + * Syncs resources between Leanplum and the current app. + * You should only call this once, and before {@link start}. + * @param async Whether the call should be asynchronous. Resource syncing can take 1-2 seconds to + * index the app's resources. If async is set, resources may not be available immediately + * when the app starts. + * @param patternsToIncludeOrNil Limit paths to only those matching at least one pattern in this + * list. Supply nil to indicate no inclusion patterns. Paths are relative to the app's bundle. + * @param patternsToExcludeOrNil Exclude paths matching at least one of these patterns. + * Supply nil to indicate no exclusion patterns. + * @param async Whether the call should be asynchronous. Resource syncing can take 1-2 seconds to + * index the app's resources. If async is set, resources may not be available immediately + * when the app starts. + */ ++ (void)syncResourcePaths:(NSArray *)patternsToIncludeOrNil + excluding:(NSArray *)patternsToExcludeOrNil + async:(BOOL)async; +/**@}*/ + +/** + * @{ + * Call this when your application starts. + * This will initiate a call to Leanplum's servers to get the values + * of the variables used in your app. + */ ++ (void)start; ++ (void)startWithResponseHandler:(LeanplumStartBlock)response; ++ (void)startWithUserAttributes:(NSDictionary *)attributes; ++ (void)startWithUserId:(NSString *)userId; ++ (void)startWithUserId:(NSString *)userId responseHandler:(LeanplumStartBlock)response; ++ (void)startWithUserId:(NSString *)userId userAttributes:(NSDictionary *)attributes; ++ (void)startWithUserId:(NSString *)userId userAttributes:(NSDictionary *)attributes + responseHandler:(LeanplumStartBlock)startResponse; +/**@}*/ + +/** + * @{ + * Returns whether or not Leanplum has finished starting. + */ ++ (BOOL)hasStarted; + +/** + * Returns whether or not Leanplum has finished starting and the device is registered + * as a developer. + */ ++ (BOOL)hasStartedAndRegisteredAsDeveloper; +/**@}*/ + +/** + * Block to call when the start call finishes, and variables are returned + * back from the server. Calling this multiple times will call each block + * in succession. + */ ++ (void)onStartResponse:(LeanplumStartBlock)block; + +/** + * Block to call when the variables receive new values from the server. + * This will be called on start, and also later on if the user is in an experiment + * that can update in realtime. + */ ++ (void)onVariablesChanged:(LeanplumVariablesChangedBlock)block; + +/** + * Block to call when the interface receive new values from the server. + * This will be called on start, and also later on if the user is in an experiment + * that can update in realtime. + */ ++ (void)onInterfaceChanged:(LeanplumInterfaceChangedBlock)block; + +/** + * Block to call when no more file downloads are pending (either when + * no files needed to be downloaded or all downloads have been completed). + */ ++ (void)onVariablesChangedAndNoDownloadsPending:(LeanplumVariablesChangedBlock)block; + +/** + * Block to call ONCE when no more file downloads are pending (either when + * no files needed to be downloaded or all downloads have been completed). + */ ++ (void)onceVariablesChangedAndNoDownloadsPending:(LeanplumVariablesChangedBlock)block; + +/** + * @{ + * Defines new action and message types to be performed at points set up on the Leanplum dashboard. + */ ++ (void)defineAction:(NSString *)name ofKind:(LeanplumActionKind)kind withArguments:(NSArray *)args; ++ (void)defineAction:(NSString *)name ofKind:(LeanplumActionKind)kind withArguments:(NSArray *)args + withOptions:(NSDictionary *)options; ++ (void)defineAction:(NSString *)name ofKind:(LeanplumActionKind)kind withArguments:(NSArray *)args + withResponder:(LeanplumActionBlock)responder; ++ (void)defineAction:(NSString *)name ofKind:(LeanplumActionKind)kind withArguments:(NSArray *)args + withOptions:(NSDictionary *)options + withResponder:(LeanplumActionBlock)responder; +/**@}*/ + +/** + * Block to call when an action is received, such as to show a message to the user. + */ ++ (void)onAction:(NSString *)actionName invoke:(LeanplumActionBlock)block; + +/** + * Handles a push notification for apps that use Background Notifications. + * Without background notifications, Leanplum handles them automatically. + * Deprecated. Leanplum calls handleNotification automatically now. If you + * implement application:didReceiveRemoteNotification:fetchCompletionHandler: + * in your app delegate, you should remove any calls to [Leanplum handleNotification] + * and call the completion handler yourself. + */ ++ (void)handleNotification:(NSDictionary *)userInfo + fetchCompletionHandler:(LeanplumFetchCompletionBlock)completionHandler + __attribute__((deprecated("Leanplum calls handleNotification automatically now. If you " + "implement application:didReceiveRemoteNotification:fetchCompletionHandler: in your app " + "delegate, you should remove any calls to [Leanplum handleNotification] and call the " + "completion handler yourself."))); + +#if LP_NOT_TV +/** + * Call this to handle custom actions for local notifications. + */ +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" +#pragma clang diagnostic ignored "-Wstrict-prototypes" ++ (void)handleActionWithIdentifier:(NSString *)identifier + forLocalNotification:(UILocalNotification *)notification + completionHandler:(void (^)())completionHandler; +#pragma clang diagnostic pop +#endif + +/** + * Call this to handle custom actions for remote notifications. + */ +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wstrict-prototypes" ++ (void)handleActionWithIdentifier:(NSString *)identifier + forRemoteNotification:(NSDictionary *)notification + completionHandler:(void (^)())completionHandler; +#pragma clang diagnostic pop + +/* + * Block to call that decides whether a notification should be displayed when it is + * received while the app is running, and the notification is not muted. + * Overrides the default behavior of showing an alert view with the notification message. + */ ++ (void)setShouldOpenNotificationHandler:(LeanplumShouldHandleNotificationBlock)block; + +/** + * @{ + * Adds a responder to be executed when an event happens. + * Similar to the methods above but uses NSInvocations instead of blocks. + * @see onStartResponse: + */ ++ (void)addStartResponseResponder:(id)responder withSelector:(SEL)selector; ++ (void)addVariablesChangedResponder:(id)responder withSelector:(SEL)selector; ++ (void)addInterfaceChangedResponder:(id)responder withSelector:(SEL)selector; ++ (void)addVariablesChangedAndNoDownloadsPendingResponder:(id)responder withSelector:(SEL)selector; ++ (void)addResponder:(id)responder withSelector:(SEL)selector forActionNamed:(NSString *)actionName; ++ (void)removeStartResponseResponder:(id)responder withSelector:(SEL)selector; ++ (void)removeVariablesChangedResponder:(id)responder withSelector:(SEL)selector; ++ (void)removeInterfaceChangedResponder:(id)responder withSelector:(SEL)selector; ++ (void)removeVariablesChangedAndNoDownloadsPendingResponder:(id)responder withSelector:(SEL)selector; ++ (void)removeResponder:(id)responder withSelector:(SEL)selector forActionNamed:(NSString *)actionName; +/**@}*/ + +/** + * Sets additional user attributes after the session has started. + * Variables retrieved by start won't be targeted based on these attributes, but + * they will count for the current session for reporting purposes. + * Only those attributes given in the dictionary will be updated. All other + * attributes will be preserved. + */ ++ (void)setUserAttributes:(NSDictionary *)attributes; + +/** + * Updates a user ID after session start. + */ ++ (void)setUserId:(NSString *)userId; + +/** + * Updates a user ID after session start with a dictionary of user attributes. + */ ++ (void)setUserId:(NSString *)userId withUserAttributes:(NSDictionary *)attributes; + +/** + * Sets the traffic source info for the current user. + * Keys in info must be one of: publisherId, publisherName, publisherSubPublisher, + * publisherSubSite, publisherSubCampaign, publisherSubAdGroup, publisherSubAd. + */ ++ (void)setTrafficSourceInfo:(NSDictionary *)info; + +/** + * @{ + * Advances to a particular state in your application. The string can be + * any value of your choosing, and will show up in the dashboard. + * A state is a section of your app that the user is currently in. + * @param state The name of the state. + */ ++ (void)advanceTo:(NSString *)state; + +/** + * Advances to a particular state in your application. The string can be + * any value of your choosing, and will show up in the dashboard. + * A state is a section of your app that the user is currently in. + * @param state The name of the state. + * @param info Anything else you want to log with the state. For example, if the state + * is watchVideo, info could be the video ID. + */ ++ (void)advanceTo:(NSString *)state withInfo:(NSString *)info; + +/** + * Advances to a particular state in your application. The string can be + * any value of your choosing, and will show up in the dashboard. + * A state is a section of your app that the user is currently in. + * You can specify up to 200 types of parameters per app across all events and state. + * The parameter keys must be strings, and values either strings or numbers. + * @param state The name of the state. + * @param params A dictionary with custom parameters. + */ ++ (void)advanceTo:(NSString *)state withParameters:(NSDictionary *)params; + +/** + * Advances to a particular state in your application. The string can be + * any value of your choosing, and will show up in the dashboard. + * A state is a section of your app that the user is currently in. + * You can specify up to 200 types of parameters per app across all events and state. + * The parameter keys must be strings, and values either strings or numbers. + * @param state The name of the state. (nullable) + * @param info Anything else you want to log with the state. For example, if the state + * is watchVideo, info could be the video ID. + * @param params A dictionary with custom parameters. + */ ++ (void)advanceTo:(NSString *)state withInfo:(NSString *)info andParameters:(NSDictionary *)params; + +/** + * Pauses the current state. + * You can use this if your game has a "pause" mode. You shouldn't call it + * when someone switches out of your app because that's done automatically. + */ ++ (void)pauseState; + +/** + * Resumes the current state. + */ ++ (void)resumeState; + +/** + * Automatically tracks all of the screens in the app as states. + * You should not use this in conjunction with advanceTo as the user can only be in + * 1 state at a time. This method requires LeanplumUIEditor module. + */ ++ (void)trackAllAppScreens; + +/** + * LPTrackScreenMode enum. + * LPTrackScreenModeDefault mans that states are the full view controller type name. + * LPTrackScreenModeStripViewController will cause the string "ViewController" to be stripped from + * the end of the state. + */ +typedef NS_ENUM(NSUInteger, LPTrackScreenMode) { + LPTrackScreenModeDefault = 0, + LPTrackScreenModeStripViewController +}; + +/** + * Automatically tracks all of the screens in the app as states. + * You should not use this in conjunction with advanceTo as the user can only be in + * 1 state at a time. This method requires LeanplumUIEditor module. + * @param trackScreenMode Choose mode for display. Default is the view controller type name. + */ ++ (void)trackAllAppScreensWithMode:(LPTrackScreenMode)trackScreenMode; + +/** + * Manually track purchase event with currency code in your application. It is advised to use + * trackInAppPurchases to automatically track IAPs. + */ ++ (void)trackPurchase:(NSString *)event withValue:(double)value + andCurrencyCode:(NSString *)currencyCode andParameters:(NSDictionary *)params; + +/** + * Automatically tracks InApp purchase and does server side receipt validation. + */ ++ (void)trackInAppPurchases; + +/** + * Manually tracks InApp purchase and does server side receipt validation. + */ ++ (void)trackInAppPurchase:(SKPaymentTransaction *)transaction; +/**@}*/ + +/** + * @{ + * Logs a particular event in your application. The string can be + * any value of your choosing, and will show up in the dashboard. + * To track a purchase, use LP_PURCHASE_EVENT. + */ ++ (void)track:(NSString *)event; ++ (void)track:(NSString *)event withValue:(double)value; ++ (void)track:(NSString *)event withInfo:(NSString *)info; ++ (void)track:(NSString *)event withValue:(double)value andInfo:(NSString *)info; + +// See above for the explanation of params. ++ (void)track:(NSString *)event withParameters:(NSDictionary *)params; ++ (void)track:(NSString *)event withValue:(double)value andParameters:(NSDictionary *)params; ++ (void)track:(NSString *)event withValue:(double)value andInfo:(NSString *)info andParameters:(NSDictionary *)params; +/**@}*/ + +/** + * @{ + * Gets the path for a particular resource. The resource can be overridden by the server. + */ ++ (NSString *)pathForResource:(NSString *)name ofType:(NSString *)extension; ++ (id)objectForKeyPath:(id)firstComponent, ... NS_REQUIRES_NIL_TERMINATION; ++ (id)objectForKeyPathComponents:(NSArray *)pathComponents; +/**@}*/ + +/** + * Gets a list of variants that are currently active for this user. + * Each variant is a dictionary containing an id. + */ ++ (NSArray *)variants; + +/** + * Returns metadata for all active in-app messages. + * Recommended only for debugging purposes and advanced use cases. + */ ++ (NSDictionary *)messageMetadata; + +/** + * Forces content to update from the server. If variables have changed, the + * appropriate callbacks will fire. Use sparingly as if the app is updated, + * you'll have to deal with potentially inconsistent state or user experience. + */ ++ (void)forceContentUpdate; + +/** + * Forces content to update from the server. If variables have changed, the + * appropriate callbacks will fire. Use sparingly as if the app is updated, + * you'll have to deal with potentially inconsistent state or user experience. + * The provided callback will always fire regardless + * of whether the variables have changed. + */ ++ (void)forceContentUpdate:(LeanplumVariablesChangedBlock)block; + +/** + * This should be your first statement in a unit test. This prevents + * Leanplum from communicating with the server. + */ ++ (void)enableTestMode; + +/** + * Used to enable or disable test mode. Test mode prevents Leanplum from + * communicating with the server. This is useful for unit tests. + */ ++ (void)setTestModeEnabled:(BOOL)isTestModeEnabled; + +/** + * Customize push setup. If this API should be called before [Leanplum start]. If this API is not + * used the default push setup from the docs will be used for "Push Ask to Ask" and + * "Register For Push". + */ ++ (void)setPushSetup:(LeanplumPushSetupBlock)block; + +/** + * Get the push setup block. + */ ++ (LeanplumPushSetupBlock)pushSetupBlock; + +/** + * Returns YES if the app existed on the device more than a day previous to a version built with + * Leanplum was installed. + */ ++ (BOOL)isPreLeanplumInstall; + +/** + * Returns the deviceId in the current Leanplum session. This should only be called after + * [Leanplum start]. + */ ++ (NSString *)deviceId; + +/** + * Returns the userId in the current Leanplum session. This should only be called after + * [Leanplum start]. + */ ++ (NSString *)userId; + +/** + * Returns an instance to the singleton LPInbox object. + */ ++ (LPInbox *)inbox; + +/** + * Returns an instance to the singleton LPNewsfeed object. + * Deprecated. Use {@link inbox} instead. + */ ++ (LPNewsfeed *)newsfeed __attribute__((deprecated("Use inbox instead."))); + +/** + * Types of location accuracy. Higher value implies better accuracy. + */ +typedef enum { + LPLocationAccuracyIP = 0, + LPLocationAccuracyCELL = 1, + LPLocationAccuracyGPS = 2 +} LPLocationAccuracyType; + +/** + * Set location manually. Calls setDeviceLocationWithLatitude:longitude:type: with cell type. + * Best if used in after calling setDeviceLocationWithLatitude:. + */ ++ (void)setDeviceLocationWithLatitude:(double)latitude + longitude:(double)longitude; + +/** + * Set location manually. Best if used in after calling setDeviceLocationWithLatitude:. + * Useful if you want to apply additional logic before sending in the location. + */ ++ (void)setDeviceLocationWithLatitude:(double)latitude + longitude:(double)longitude + type:(LPLocationAccuracyType)type; + +/** + * Set location manually. Best if used in after calling setDeviceLocationWithLatitude:. + * If you have the CLPlacemark info: city is locality, region is administrativeArea, + * and country is ISOcountryCode. + */ ++ (void)setDeviceLocationWithLatitude:(double)latitude + longitude:(double)longitude + city:(NSString *)city + region:(NSString *)region + country:(NSString *)country + type:(LPLocationAccuracyType)type; + +/** + * Disables collecting location automatically. Will do nothing if Leanplum-Location is not used. + */ ++ (void)disableLocationCollection; + +@end + +@interface LeanplumCompatibility : NSObject + +/** + * Used only for compatibility with Google Analytics. + */ ++ (void)gaTrack:(NSObject *)trackingObject; + +@end + +@class LPVar; + +/** + * Receives callbacks for {@link LPVar} + */ +@protocol LPVarDelegate +@optional +/** + * For file variables, called when the file is ready. + */ +- (void)fileIsReady:(LPVar *)var; +/** + * Called when the value of the variable changes. + */ +- (void)valueDidChange:(LPVar *)var; +@end + +/** + * A variable is any part of your application that can change from an experiment. + * Check out {@link Macros the macros} for defining variables more easily. + */ +@interface LPVar : NSObject +/** + * @{ + * Defines a {@link LPVar} + */ + ++ (LPVar *)define:(NSString *)name; ++ (LPVar *)define:(NSString *)name withInt:(int)defaultValue; ++ (LPVar *)define:(NSString *)name withFloat:(float)defaultValue; ++ (LPVar *)define:(NSString *)name withDouble:(double)defaultValue; ++ (LPVar *)define:(NSString *)name withCGFloat:(CGFloat)cgFloatValue; ++ (LPVar *)define:(NSString *)name withShort:(short)defaultValue; ++ (LPVar *)define:(NSString *)name withChar:(char)defaultValue; ++ (LPVar *)define:(NSString *)name withBool:(BOOL)defaultValue; ++ (LPVar *)define:(NSString *)name withString:(NSString *)defaultValue; ++ (LPVar *)define:(NSString *)name withNumber:(NSNumber *)defaultValue; ++ (LPVar *)define:(NSString *)name withInteger:(NSInteger)defaultValue; ++ (LPVar *)define:(NSString *)name withLong:(long)defaultValue; ++ (LPVar *)define:(NSString *)name withLongLong:(long long)defaultValue; ++ (LPVar *)define:(NSString *)name withUnsignedChar:(unsigned char)defaultValue; ++ (LPVar *)define:(NSString *)name withUnsignedInt:(unsigned int)defaultValue; ++ (LPVar *)define:(NSString *)name withUnsignedInteger:(NSUInteger)defaultValue; ++ (LPVar *)define:(NSString *)name withUnsignedLong:(unsigned long)defaultValue; ++ (LPVar *)define:(NSString *)name withUnsignedLongLong:(unsigned long long)defaultValue; ++ (LPVar *)define:(NSString *)name withUnsignedShort:(unsigned short)defaultValue; ++ (LPVar *)define:(NSString *)name withFile:(NSString *)defaultFilename; ++ (LPVar *)define:(NSString *)name withDictionary:(NSDictionary *)defaultValue; ++ (LPVar *)define:(NSString *)name withArray:(NSArray *)defaultValue; ++ (LPVar *)define:(NSString *)name withColor:(UIColor *)defaultValue; +/**@}*/ + +/** + * Returns the name of the variable. + */ +- (NSString *)name; + +/** + * Returns the components of the variable's name. + */ +- (NSArray *)nameComponents; + +/** + * Returns the default value of a variable. + */ +- (id)defaultValue; + +/** + * Returns the kind of the variable. + */ +- (NSString *)kind; + +/** + * Returns whether the variable has changed since the last time the app was run. + */ +- (BOOL)hasChanged; + +/** + * For file variables, called when the file is ready. + */ +- (void)onFileReady:(LeanplumVariablesChangedBlock)block; + +/** + * Called when the value of the variable changes. + */ +- (void)onValueChanged:(LeanplumVariablesChangedBlock)block; + +/** + * Sets the delegate of the variable in order to use + * {@link LPVarDelegate::fileIsReady:} and {@link LPVarDelegate::valueDidChange:} + */ +- (void)setDelegate:(id )delegate; + +/** + * @{ + * Accessess the value(s) of the variable + */ +- (id)objectForKey:(NSString *)key; +- (id)objectAtIndex:(NSUInteger )index; +- (id)objectForKeyPath:(id)firstComponent, ... NS_REQUIRES_NIL_TERMINATION; +- (id)objectForKeyPathComponents:(NSArray *)pathComponents; +- (NSUInteger)count; + +- (NSNumber *)numberValue; +- (NSString *)stringValue; +- (NSString *)fileValue; +- (UIImage *)imageValue; +- (int)intValue; +- (double)doubleValue; +- (CGFloat)cgFloatValue; +- (float)floatValue; +- (short)shortValue; +- (BOOL)boolValue; +- (char)charValue; +- (long)longValue; +- (long long)longLongValue; +- (NSInteger)integerValue; +- (unsigned char)unsignedCharValue; +- (unsigned short)unsignedShortValue; +- (unsigned int)unsignedIntValue; +- (NSUInteger)unsignedIntegerValue; +- (unsigned long)unsignedLongValue; +- (unsigned long long)unsignedLongLongValue; +- (UIColor *)colorValue; +/**@}*/ +@end + +@interface LPActionArg : NSObject +/** + * @{ + * Defines a Leanplum Action Argument + */ ++ (LPActionArg *)argNamed:(NSString *)name withNumber:(NSNumber *)defaultValue; ++ (LPActionArg *)argNamed:(NSString *)name withString:(NSString *)defaultValue; ++ (LPActionArg *)argNamed:(NSString *)name withBool:(BOOL)defaultValue; ++ (LPActionArg *)argNamed:(NSString *)name withFile:(NSString *)defaultValue; ++ (LPActionArg *)argNamed:(NSString *)name withDict:(NSDictionary *)defaultValue; ++ (LPActionArg *)argNamed:(NSString *)name withArray:(NSArray *)defaultValue; ++ (LPActionArg *)argNamed:(NSString *)name withAction:(NSString *)defaultValue; ++ (LPActionArg *)argNamed:(NSString *)name withColor:(UIColor *)defaultValue; +/**@}*/ +- (NSString *)name; +- (NSString *)kind; +- (id)defaultValue; + +@end + +@interface LPActionContext : NSObject + +- (NSString *)actionName; + +- (NSString *)stringNamed:(NSString *)name; +- (NSString *)fileNamed:(NSString *)name; +- (NSNumber *)numberNamed:(NSString *)name; +- (BOOL)boolNamed:(NSString *)name; +- (NSDictionary *)dictionaryNamed:(NSString *)name; +- (NSArray *)arrayNamed:(NSString *)name; +- (UIColor *)colorNamed:(NSString *)name; +- (NSString *)htmlWithTemplateNamed:(NSString *)templateName; + +/** + * Runs the action given by the "name" key. + */ +- (void)runActionNamed:(NSString *)name; + +/** + * Runs and tracks an event for the action given by the "name" key. + * This will track an event if no action is set. + */ +- (void)runTrackedActionNamed:(NSString *)name; + +/** + * Tracks an event in the context of the current message. + */ +- (void)track:(NSString *)event withValue:(double)value andParameters:(NSDictionary *)params; + +/** + * Tracks an event in the conext of the current message, with any parent actions prepended to the + * message event name. + */ +- (void)trackMessageEvent:(NSString *)event + withValue:(double)value + andInfo:(NSString *)info + andParameters:(NSDictionary *)params; + +/** + * Prevents the currently active message from appearing again in the future. + */ +- (void)muteFutureMessagesOfSameKind; + +/** + * Checks if the action context has any missing files that still need to be downloaded. + */ +- (BOOL)hasMissingFiles; + +@end diff --git a/mobile/ios/ThirdParty/Leanplum/Leanplum.framework/Info.plist b/mobile/ios/ThirdParty/Leanplum/Leanplum.framework/Info.plist new file mode 100644 index 0000000000..64b91413ba Binary files /dev/null and b/mobile/ios/ThirdParty/Leanplum/Leanplum.framework/Info.plist differ diff --git a/mobile/ios/ThirdParty/Leanplum/Leanplum.framework/Leanplum b/mobile/ios/ThirdParty/Leanplum/Leanplum.framework/Leanplum new file mode 100644 index 0000000000..d50c85e23d Binary files /dev/null and b/mobile/ios/ThirdParty/Leanplum/Leanplum.framework/Leanplum differ diff --git a/mobile/ios/ThirdParty/Leanplum/Leanplum.framework/Modules/module.modulemap b/mobile/ios/ThirdParty/Leanplum/Leanplum.framework/Modules/module.modulemap new file mode 100644 index 0000000000..a296b0cec2 --- /dev/null +++ b/mobile/ios/ThirdParty/Leanplum/Leanplum.framework/Modules/module.modulemap @@ -0,0 +1,6 @@ +framework module Leanplum { + umbrella header "Leanplum.h" + + export * + module * { export * } +} diff --git a/mobile/ios/ThirdParty/Reachability.swift b/mobile/ios/ThirdParty/Reachability.swift new file mode 100644 index 0000000000..ba526a14c2 --- /dev/null +++ b/mobile/ios/ThirdParty/Reachability.swift @@ -0,0 +1,115 @@ +// The MIT License (MIT) +// +// Copyright (c) 2015 Isuru Nanayakkara +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + + +import Foundation +import SystemConfiguration + + +let ReachabilityStatusChangedNotification = "ReachabilityStatusChangedNotification" + +enum ReachabilityType: CustomStringConvertible { + case wwan + case wiFi + + var description: String { + switch self { + case .wwan: return "WWAN" + case .wiFi: return "WiFi" + } + } +} + +enum ReachabilityStatus: CustomStringConvertible { + case offline + case online(ReachabilityType) + case unknown + + var description: String { + switch self { + case .offline: return "Offline" + case .online(let type): return "Online (\(type))" + case .unknown: return "Unknown" + } + } +} + +open class Reach { + + func connectionStatus() -> ReachabilityStatus { + var zeroAddress = sockaddr_in() + zeroAddress.sin_len = UInt8(MemoryLayout.size(ofValue: zeroAddress)) + zeroAddress.sin_family = sa_family_t(AF_INET) + + guard let defaultRouteReachability = withUnsafePointer(to: &zeroAddress, { + $0.withMemoryRebound(to: sockaddr.self, capacity: 1) { + SCNetworkReachabilityCreateWithAddress(kCFAllocatorDefault, $0) + } + }) else { + return .unknown + } + + var flags : SCNetworkReachabilityFlags = [] + if !SCNetworkReachabilityGetFlags(defaultRouteReachability, &flags) { + return .unknown + } + + return ReachabilityStatus(reachabilityFlags: flags) + } + + + func monitorReachabilityChanges() { + let host = "google.com" + var context = SCNetworkReachabilityContext(version: 0, info: nil, retain: nil, release: nil, copyDescription: nil) + let reachability = SCNetworkReachabilityCreateWithName(nil, host)! + + SCNetworkReachabilitySetCallback(reachability, { (_, flags, _) in + let status = ReachabilityStatus(reachabilityFlags: flags) + + NotificationCenter.default.post(name: Notification.Name(rawValue: ReachabilityStatusChangedNotification), + object: nil, + userInfo: ["Status": status.description]) + + }, &context) + + SCNetworkReachabilityScheduleWithRunLoop(reachability, CFRunLoopGetMain(), CFRunLoopMode.commonModes.rawValue) + } + +} + +extension ReachabilityStatus { + fileprivate init(reachabilityFlags flags: SCNetworkReachabilityFlags) { + let connectionRequired = flags.contains(.connectionRequired) + let isReachable = flags.contains(.reachable) + let isWWAN = flags.contains(.isWWAN) + + if !connectionRequired && isReachable { + if isWWAN { + self = .online(.wwan) + } else { + self = .online(.wiFi) + } + } else { + self = .offline + } + } +} diff --git a/mobile/ios/ThirdParty/Result/Error.swift b/mobile/ios/ThirdParty/Result/Error.swift new file mode 100644 index 0000000000..d2368e5c84 --- /dev/null +++ b/mobile/ios/ThirdParty/Result/Error.swift @@ -0,0 +1,11 @@ +// +// Error.swift +// Result +// +// Created by John Gallagher on 9/12/14. +// Copyright (c) 2014 Big Nerd Ranch. All rights reserved. +// + +import Foundation + +public typealias MaybeErrorType = Error & CustomStringConvertible diff --git a/mobile/ios/ThirdParty/Result/LICENSE b/mobile/ios/ThirdParty/Result/LICENSE new file mode 100644 index 0000000000..0bc4d34ae4 --- /dev/null +++ b/mobile/ios/ThirdParty/Result/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2014 John Gallagher + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/mobile/ios/ThirdParty/Result/README.md b/mobile/ios/ThirdParty/Result/README.md new file mode 100644 index 0000000000..788d118e14 --- /dev/null +++ b/mobile/ios/ThirdParty/Result/README.md @@ -0,0 +1,24 @@ +# Result + +`Result` is a Swift framework that includes the `Result` enum and an +`ErrorType` protocol. + +Both types are extremely small. I look forward to two changes in Swift's future: + +* A fix for the Swift compiler issue that requires the `Success` case of + `Result` to box its `T` type somehow. This repo uses + [Box](https://github.com/robrix/Box) as a workaround. +* (Hopefully) The inclusion of these types or their moral equivalents in the + Swift standard library, at which point this repo can be removed. + +## Integration + +Add this repository as a submodule, or use [Carthage](https://github.com/Carthage/Carthage/). + +## Author + +John Gallagher, jgallagher@bignerdranch.com + +## License + +Deferred is available under the MIT license. See the LICENSE file for more info. diff --git a/mobile/ios/ThirdParty/Result/Result.swift b/mobile/ios/ThirdParty/Result/Result.swift new file mode 100644 index 0000000000..fc47e8e527 --- /dev/null +++ b/mobile/ios/ThirdParty/Result/Result.swift @@ -0,0 +1,76 @@ +// +// Result.swift +// Result +// +// Created by John Gallagher on 9/12/14. +// Copyright (c) 2014 Big Nerd Ranch. All rights reserved. +// + +import Foundation +//import Box + +public enum Maybe { + case failure(MaybeErrorType) + + // TODO: Get rid of Box hack at some point after 6.3 + case success(Box) + + public init(failure: MaybeErrorType) { + self = .failure(failure) + } + + public init(success: T) { + self = .success(Box(success)) + } + + public var successValue: T? { + switch self { + case let .success(success): return success.value + case .failure: return nil + } + } + + public var failureValue: MaybeErrorType? { + switch self { + case .success: return nil + case let .failure(error): return error + } + } + + public var isSuccess: Bool { + switch self { + case .success: return true + case .failure: return false + } + } + + public var isFailure: Bool { + switch self { + case .success: return false + case .failure: return true + } + } + + public func map(_ f: (T) -> U) -> Maybe { + switch self { + case let .failure(error): return .failure(error) + case let .success(value): return .success(Box(f(value.value))) + } + } + + public func bind(_ f: (T) -> Maybe) -> Maybe { + switch self { + case let .failure(error): return .failure(error) + case let .success(value): return f(value.value) + } + } +} + +extension Maybe: CustomStringConvertible { + public var description: String { + switch self { + case let .failure(error): return "Result.Failure(\(error))" + case let .success(value): return "Result.Success(\(value.value))" + } + } +} diff --git a/mobile/ios/ThirdParty/SQLite.swift/.cocoadocs.yml b/mobile/ios/ThirdParty/SQLite.swift/.cocoadocs.yml new file mode 100644 index 0000000000..2784003281 --- /dev/null +++ b/mobile/ios/ThirdParty/SQLite.swift/.cocoadocs.yml @@ -0,0 +1,2 @@ +additional_guides: + - Documentation/Index.md diff --git a/mobile/ios/ThirdParty/SQLite.swift/.gitignore b/mobile/ios/ThirdParty/SQLite.swift/.gitignore new file mode 100644 index 0000000000..5882e0cb82 --- /dev/null +++ b/mobile/ios/ThirdParty/SQLite.swift/.gitignore @@ -0,0 +1,27 @@ +# OS X +.DS_Store + +# Xcode +build/ +*.pbxuser +!default.pbxuser +*.mode1v3 +!default.mode1v3 +*.mode2v3 +!default.mode2v3 +*.perspectivev3 +!default.perspectivev3 +xcuserdata +*.xccheckout +*.moved-aside +DerivedData +*.hmap +*.ipa +*.xcuserstate + +# Carthage +/Carthage/ + +# Swift Package Manager +.build +Packages/ diff --git a/mobile/ios/ThirdParty/SQLite.swift/.gitmodules b/mobile/ios/ThirdParty/SQLite.swift/.gitmodules new file mode 100644 index 0000000000..e69de29bb2 diff --git a/mobile/ios/ThirdParty/SQLite.swift/.swift-version b/mobile/ios/ThirdParty/SQLite.swift/.swift-version new file mode 100644 index 0000000000..9f55b2ccb5 --- /dev/null +++ b/mobile/ios/ThirdParty/SQLite.swift/.swift-version @@ -0,0 +1 @@ +3.0 diff --git a/mobile/ios/ThirdParty/SQLite.swift/.travis.yml b/mobile/ios/ThirdParty/SQLite.swift/.travis.yml new file mode 100644 index 0000000000..3747ac7b89 --- /dev/null +++ b/mobile/ios/ThirdParty/SQLite.swift/.travis.yml @@ -0,0 +1,26 @@ +language: objective-c +rvm: 2.2 +osx_image: xcode8.2 +env: + global: + - IOS_SIMULATOR="iPhone 6s" +matrix: + include: + - env: BUILD_SCHEME="SQLite iOS" + - env: BUILD_SCHEME="SQLite Mac" + - env: VALIDATOR_SUBSPEC="none" + - env: VALIDATOR_SUBSPEC="standard" + - env: VALIDATOR_SUBSPEC="standalone" + - env: VALIDATOR_SUBSPEC="SQLCipher" + - env: CARTHAGE_PLATFORM="iOS" + - env: CARTHAGE_PLATFORM="Mac" + - env: CARTHAGE_PLATFORM="watchOS" + - env: CARTHAGE_PLATFORM="tvOS" + - env: PACKAGE_MANAGER_COMMAND="test -Xlinker -lsqlite3" +before_install: + - gem update bundler + - gem install xcpretty --no-document + - brew update + - brew outdated carthage || brew upgrade carthage +script: + - ./run-tests.sh diff --git a/mobile/ios/ThirdParty/SQLite.swift/CHANGELOG.md b/mobile/ios/ThirdParty/SQLite.swift/CHANGELOG.md new file mode 100644 index 0000000000..f3298d68f9 --- /dev/null +++ b/mobile/ios/ThirdParty/SQLite.swift/CHANGELOG.md @@ -0,0 +1,34 @@ +0.11.2 (25-12-2016), [diff][diff-0.11.2] +======================================== + +* Fixed SQLCipher integration with read-only databases ([#559][]) +* Preliminary Swift Package Manager support ([#548][], [#560][]) +* Fixed null pointer when fetching an empty BLOB ([#561][]) +* Allow `where` as alias for `filter` ([#571][]) + +0.11.1 (06-12-2016), [diff][diff-0.11.1] +======================================== + +* Integrate SQLCipher via CocoaPods ([#546][], [#553][]) +* Made lastInsertRowid consistent with other SQLite wrappers ([#532][]) +* Fix for ~= operator used with Double ranges +* Various documentation updates + +0.11.0 (19-10-2016) +=================== + +* Swift3 migration ([diff][diff-0.11.0]) + + +[diff-0.11.0]: https://github.com/stephencelis/SQLite.swift/compare/0.10.1...0.11.0 +[diff-0.11.1]: https://github.com/stephencelis/SQLite.swift/compare/0.11.0...0.11.1 +[diff-0.11.2]: https://github.com/stephencelis/SQLite.swift/compare/0.11.1...0.11.2 + +[#532]: https://github.com/stephencelis/SQLite.swift/issues/532 +[#546]: https://github.com/stephencelis/SQLite.swift/issues/546 +[#548]: https://github.com/stephencelis/SQLite.swift/pull/548 +[#553]: https://github.com/stephencelis/SQLite.swift/pull/553 +[#559]: https://github.com/stephencelis/SQLite.swift/pull/559 +[#560]: https://github.com/stephencelis/SQLite.swift/pull/560 +[#561]: https://github.com/stephencelis/SQLite.swift/issues/561 +[#571]: https://github.com/stephencelis/SQLite.swift/issues/571 diff --git a/mobile/ios/ThirdParty/SQLite.swift/CONTRIBUTING.md b/mobile/ios/ThirdParty/SQLite.swift/CONTRIBUTING.md new file mode 100644 index 0000000000..60c1837048 --- /dev/null +++ b/mobile/ios/ThirdParty/SQLite.swift/CONTRIBUTING.md @@ -0,0 +1,108 @@ +# Contributing + +The where and when to open an [issue](#issues) or [pull +request](#pull-requests). + + +## Issues + +Issues are used to track **bugs** and **feature requests**. Need **help** or +have a **general question**? [Ask on Stack Overflow][] (tag `sqlite.swift`). + +Before reporting a bug or requesting a feature, [run a few searches][Search] to +see if a similar issue has already been opened and ensure you’re not submitting +a duplicate. + +If you find a similar issue, read the existing conversation and see if it +addresses everything. If it doesn’t, continue the conversation there. + +If your searches return empty, see the [bug](#bugs) or [feature +request](#feature-requests) guidelines below. + +[Ask on Stack Overflow]: http://stackoverflow.com/questions/tagged/sqlite.swift +[Search]: https://github.com/stephencelis/SQLite.swift/search?type=Issues + + +### Bugs + +Think you’ve discovered a new **bug**? Let’s try troubleshooting a few things +first. + + - **Is it an installation issue?** + + If this is your first time building SQLite.swift in your project, you may + encounter a build error, _e.g._: + + No such module 'SQLite' + + Please carefully re-read the [installation instructions][] to make sure + everything is in order. + + - **Have you read the documentation?** + + If you can’t seem to get something working, check + [the documentation][See Documentation] to see if the solution is there. + + - **Are you up-to-date?** + + If you’re perusing [the documentation][See Documentation] online and find + that an example is just not working, please upgrade to the latest version + of SQLite.swift and try again before continuing. + + - **Is it an unhelpful build error?** + + While Swift error messaging is improving with each release, complex + expressions still lend themselves to misleading errors. If you encounter an + error on a complex line, breaking it down into smaller pieces generally + yields a more understandable error. + + - **Is it an _even more_ unhelpful build error?** + + Have you updated Xcode recently? Did your project stop building out of the + blue? + + Hold down the **option** key and select **Clean Build Folder…** from the + **Product** menu (⌥⇧⌘K). + +Made it through everything above and still having trouble? Sorry! +[Open an issue][]! And _please_: + + - Be as descriptive as possible. + - Provide as much information needed to _reliably reproduce_ the issue. + - Attach screenshots if possible. + - Better yet: attach GIFs or link to video. + - Even better: link to a sample project exhibiting the issue. + - Include the SQLite.swift commit or branch experiencing the issue. + - Include devices and operating systems affected. + - Include build information: the Xcode and OS X versions affected. + +[installation instructions]: Documentation/Index.md#installation +[See Documentation]: Documentation/Index.md#sqliteswift-documentation +[Open an issue]: https://github.com/stephencelis/SQLite.swift/issues/new + + +### Feature Requests + +Have an innovative **feature request**? [Open an issue][]! Be thorough! Provide +context and examples. Be open to discussion. + + +## Pull Requests + +Interested in contributing but don’t know where to start? Try the [`help +wanted`][help wanted] label. + +Ready to submit a fix or a feature? [Submit a pull request][]! And _please_: + + - If code changes, run the tests and make sure everything still works. + - Write new tests for new functionality. + - Update documentation comments where applicable. + - Maintain the existing style. + - Don’t forget to have fun. + +While we cannot guarantee a merge to every pull request, we do read each one +and love your input. + + +[help wanted]: https://github.com/stephencelis/SQLite.swift/labels/help%20wanted +[Submit a pull request]: https://github.com/stephencelis/SQLite.swift/fork diff --git a/mobile/ios/ThirdParty/SQLite.swift/CocoaPods/appletvos/module.modulemap b/mobile/ios/ThirdParty/SQLite.swift/CocoaPods/appletvos/module.modulemap new file mode 100644 index 0000000000..637d993582 --- /dev/null +++ b/mobile/ios/ThirdParty/SQLite.swift/CocoaPods/appletvos/module.modulemap @@ -0,0 +1,4 @@ +module CSQLite [system] { + header "/Applications/Xcode.app/Contents/Developer/Platforms/AppleTVOS.platform/Developer/SDKs/AppleTVOS.sdk/usr/include/sqlite3.h" + export * +} diff --git a/mobile/ios/ThirdParty/SQLite.swift/CocoaPods/appletvsimulator/module.modulemap b/mobile/ios/ThirdParty/SQLite.swift/CocoaPods/appletvsimulator/module.modulemap new file mode 100644 index 0000000000..f8b9b671a7 --- /dev/null +++ b/mobile/ios/ThirdParty/SQLite.swift/CocoaPods/appletvsimulator/module.modulemap @@ -0,0 +1,4 @@ +module CSQLite [system] { + header "/Applications/Xcode.app/Contents/Developer/Platforms/AppleTVSimulator.platform/Developer/SDKs/AppleTVSimulator.sdk/usr/include/sqlite3.h" + export * +} diff --git a/mobile/ios/ThirdParty/SQLite.swift/CocoaPods/iphoneos-10.0/module.modulemap b/mobile/ios/ThirdParty/SQLite.swift/CocoaPods/iphoneos-10.0/module.modulemap new file mode 100644 index 0000000000..67a6c20309 --- /dev/null +++ b/mobile/ios/ThirdParty/SQLite.swift/CocoaPods/iphoneos-10.0/module.modulemap @@ -0,0 +1,4 @@ +module CSQLite [system] { + header "/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS10.0.sdk/usr/include/sqlite3.h" + export * +} diff --git a/mobile/ios/ThirdParty/SQLite.swift/CocoaPods/iphoneos/module.modulemap b/mobile/ios/ThirdParty/SQLite.swift/CocoaPods/iphoneos/module.modulemap new file mode 100644 index 0000000000..043db6c484 --- /dev/null +++ b/mobile/ios/ThirdParty/SQLite.swift/CocoaPods/iphoneos/module.modulemap @@ -0,0 +1,4 @@ +module CSQLite [system] { + header "/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk/usr/include/sqlite3.h" + export * +} diff --git a/mobile/ios/ThirdParty/SQLite.swift/CocoaPods/iphonesimulator-10.0/module.modulemap b/mobile/ios/ThirdParty/SQLite.swift/CocoaPods/iphonesimulator-10.0/module.modulemap new file mode 100644 index 0000000000..c8b84ab8f4 --- /dev/null +++ b/mobile/ios/ThirdParty/SQLite.swift/CocoaPods/iphonesimulator-10.0/module.modulemap @@ -0,0 +1,4 @@ +module CSQLite [system] { + header "/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator10.0.sdk/usr/include/sqlite3.h" + export * +} diff --git a/mobile/ios/ThirdParty/SQLite.swift/CocoaPods/iphonesimulator/module.modulemap b/mobile/ios/ThirdParty/SQLite.swift/CocoaPods/iphonesimulator/module.modulemap new file mode 100644 index 0000000000..a7b14cbb48 --- /dev/null +++ b/mobile/ios/ThirdParty/SQLite.swift/CocoaPods/iphonesimulator/module.modulemap @@ -0,0 +1,4 @@ +module CSQLite [system] { + header "/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk/usr/include/sqlite3.h" + export * +} diff --git a/mobile/ios/ThirdParty/SQLite.swift/CocoaPods/macosx-10.11/module.modulemap b/mobile/ios/ThirdParty/SQLite.swift/CocoaPods/macosx-10.11/module.modulemap new file mode 100644 index 0000000000..9e0912979d --- /dev/null +++ b/mobile/ios/ThirdParty/SQLite.swift/CocoaPods/macosx-10.11/module.modulemap @@ -0,0 +1,4 @@ +module CSQLite [system] { + header "/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.11.sdk/usr/include/sqlite3.h" + export * +} diff --git a/mobile/ios/ThirdParty/SQLite.swift/CocoaPods/macosx-10.12/module.modulemap b/mobile/ios/ThirdParty/SQLite.swift/CocoaPods/macosx-10.12/module.modulemap new file mode 100644 index 0000000000..8fc958e676 --- /dev/null +++ b/mobile/ios/ThirdParty/SQLite.swift/CocoaPods/macosx-10.12/module.modulemap @@ -0,0 +1,4 @@ +module CSQLite [system] { + header "/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.12.sdk/usr/include/sqlite3.h" + export * +} diff --git a/mobile/ios/ThirdParty/SQLite.swift/CocoaPods/macosx/module.modulemap b/mobile/ios/ThirdParty/SQLite.swift/CocoaPods/macosx/module.modulemap new file mode 100644 index 0000000000..cc8370ec56 --- /dev/null +++ b/mobile/ios/ThirdParty/SQLite.swift/CocoaPods/macosx/module.modulemap @@ -0,0 +1,4 @@ +module CSQLite [system] { + header "/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sqlite3.h" + export * +} diff --git a/mobile/ios/ThirdParty/SQLite.swift/CocoaPods/watchos/module.modulemap b/mobile/ios/ThirdParty/SQLite.swift/CocoaPods/watchos/module.modulemap new file mode 100644 index 0000000000..62a6c4eef0 --- /dev/null +++ b/mobile/ios/ThirdParty/SQLite.swift/CocoaPods/watchos/module.modulemap @@ -0,0 +1,4 @@ +module CSQLite [system] { + header "/Applications/Xcode.app/Contents/Developer/Platforms/WatchOS.platform/Developer/SDKs/WatchOS.sdk/usr/include/sqlite3.h" + export * +} diff --git a/mobile/ios/ThirdParty/SQLite.swift/CocoaPods/watchsimulator/module.modulemap b/mobile/ios/ThirdParty/SQLite.swift/CocoaPods/watchsimulator/module.modulemap new file mode 100644 index 0000000000..086fbab2de --- /dev/null +++ b/mobile/ios/ThirdParty/SQLite.swift/CocoaPods/watchsimulator/module.modulemap @@ -0,0 +1,4 @@ +module CSQLite [system] { + header "/Applications/Xcode.app/Contents/Developer/Platforms/WatchSimulator.platform/Developer/SDKs/WatchSimulator.sdk/usr/include/sqlite3.h" + export * +} diff --git a/mobile/ios/ThirdParty/SQLite.swift/Documentation/Index.md b/mobile/ios/ThirdParty/SQLite.swift/Documentation/Index.md new file mode 100644 index 0000000000..3e13ad4efe --- /dev/null +++ b/mobile/ios/ThirdParty/SQLite.swift/Documentation/Index.md @@ -0,0 +1,1587 @@ +# SQLite.swift Documentation + + - [Installation](#installation) + - [Carthage](#carthage) + - [CocoaPods](#cocoapods) + - [Swift Package Manager](#swift-package-manager) + - [Manual](#manual) + - [Getting Started](#getting-started) + - [Connecting to a Database](#connecting-to-a-database) + - [Read-Write Databases](#read-write-databases) + - [Read-Only Databases](#read-only-databases) + - [In-Memory Databases](#in-memory-databases) + - [Thread-Safety](#thread-safety) + - [Building Type-Safe SQL](#building-type-safe-sql) + - [Expressions](#expressions) + - [Compound Expressions](#compound-expressions) + - [Queries](#queries) + - [Creating a Table](#creating-a-table) + - [Create Table Options](#create-table-options) + - [Column Constraints](#column-constraints) + - [Table Constraints](#table-constraints) + - [Inserting Rows](#inserting-rows) + - [Setters](#setters) + - [Selecting Rows](#selecting-rows) + - [Iterating and Accessing Values](#iterating-and-accessing-values) + - [Plucking Rows](#plucking-rows) + - [Building Complex Queries](#building-complex-queries) + - [Selecting Columns](#selecting-columns) + - [Joining Other Tables](#joining-other-tables) + - [Column Namespacing](#column-namespacing) + - [Table Aliasing](#table-aliasing) + - [Filtering Rows](#filtering-rows) + - [Filter Operators and Functions](#filter-operators-and-functions) + - [Sorting Rows](#sorting-rows) + - [Limiting and Paging Results](#limiting-and-paging-results) + - [Aggregation](#aggregation) + - [Updating Rows](#updating-rows) + - [Deleting Rows](#deleting-rows) + - [Transactions and Savepoints](#transactions-and-savepoints) + - [Altering the Schema](#altering-the-schema) + - [Renaming Tables](#renaming-tables) + - [Adding Columns](#adding-columns) + - [Added Column Constraints](#added-column-constraints) + - [Indexes](#indexes) + - [Creating Indexes](#creating-indexes) + - [Dropping Indexes](#dropping-indexes) + - [Dropping Tables](#dropping-tables) + - [Migrations and Schema Versioning](#migrations-and-schema-versioning) + - [Custom Types](#custom-types) + - [Date-Time Values](#date-time-values) + - [Binary Data](#binary-data) + - [Custom Type Caveats](#custom-type-caveats) + - [Other Operators](#other-operators) + - [Core SQLite Functions](#core-sqlite-functions) + - [Aggregate SQLite Functions](#aggregate-sqlite-functions) + - [Custom SQL Functions](#custom-sql-functions) + - [Custom Collations](#custom-collations) + - [Full-text Search](#full-text-search) + - [Executing Arbitrary SQL](#executing-arbitrary-sql) + - [Logging](#logging) + + +[↩]: #sqliteswift-documentation + + +## Installation + +> _Note:_ SQLite.swift requires Swift 3 (and [Xcode 8](https://developer.apple.com/xcode/downloads/)) or greater. + + +### Carthage + +[Carthage][] is a simple, decentralized dependency manager for Cocoa. To +install SQLite.swift with Carthage: + + 1. Make sure Carthage is [installed][Carthage Installation]. + + 2. Update your Cartfile to include the following: + + ``` + github "stephencelis/SQLite.swift" ~> 0.11.2 + ``` + + 3. Run `carthage update` and [add the appropriate framework][Carthage Usage]. + + +[Carthage]: https://github.com/Carthage/Carthage +[Carthage Installation]: https://github.com/Carthage/Carthage#installing-carthage +[Carthage Usage]: https://github.com/Carthage/Carthage#adding-frameworks-to-an-application + + +### CocoaPods + +[CocoaPods][] is a dependency manager for Cocoa projects. To install SQLite.swift with CocoaPods: + + 1. Verify that your copy of Xcode is installed and active in the default location (`/Applications/Xcode.app`). + + ```sh + sudo xcode-select --switch /Applications/Xcode.app + ``` + + 2. Make sure CocoaPods is [installed][CocoaPods Installation] (SQLite.swift requires version 1.0.0 or greater). + + ``` sh + # Using the default Ruby install will require you to use sudo when + # installing and updating gems. + [sudo] gem install cocoapods + ``` + + 3. Update your Podfile to include the following: + + ``` ruby + use_frameworks! + + target 'YourAppTargetName' do + pod 'SQLite.swift', '~> 0.11.2' + end + ``` + + 4. Run `pod install --repo-update`. + + +#### Requiring a specific version of SQLite + + If you want to use a more recent version of SQLite than what is provided with the OS you can require the `standalone` subspec: + +``` ruby +target 'YourAppTargetName' do + pod 'SQLite.swift/standalone', '~> 0.11.2' +end +``` + +By default this will use the most recent version of SQLite without any extras. If you want you can further customize this by adding another dependency to sqlite3 or one of its subspecs: + +``` ruby +target 'YourAppTargetName' do + pod 'SQLite.swift/standalone', '~> 0.11.2' + pod 'sqlite3/fts5', '= 3.15.0' # SQLite 3.15.0 with FTS5 enabled +end +``` + +See the [sqlite3 podspec][sqlite3pod] for more details. + +#### Using SQLite.swift with SQLCipher + +If you want to use [SQLCipher][] with SQLite.swift you can require the `SQLCipher` +subspec in your Podfile: + +``` ruby +target 'YourAppTargetName' do + pod 'SQLite.swift/SQLCipher', '~> 0.11.2' +end +``` + +This will automatically add a dependency to the SQLCipher pod as well as extend +`Connection` with methods to change the database key: + +``` swift +import SQLite + +let db = try Connection("path/to/db.sqlite3") +try db.key("secret") +try db.rekey("another secret") +``` + +[CocoaPods]: https://cocoapods.org +[CocoaPods Installation]: https://guides.cocoapods.org/using/getting-started.html#getting-started +[sqlite3pod]: https://github.com/clemensg/sqlite3pod +[SQLCipher]: https://www.zetetic.net/sqlcipher/ + +### Swift Package Manager + +The [Swift Package Manager][] is a tool for managing the distribution of Swift code. +It’s integrated with the Swift build system to automate the process of +downloading, compiling, and linking dependencies. + +It is the recommended approach for using SQLite.swift in OSX CLI applications. + + 1. Add the following to your `Package.swift` file: + + ``` swift + dependencies: [ + .Package(url: "https://github.com/stephencelis/SQLite.swift.git", majorVersion: 0, minor: 11) + ] + ``` + + 2. Build your project: + + ``` sh + $ swift build -Xlinker -lsqlite3 + ``` + +[Swift Package Manager]: https://swift.org/package-manager + +### Manual + +To install SQLite.swift as an Xcode sub-project: + + 1. Drag the **SQLite.xcodeproj** file into your own project. ([Submodule](http://git-scm.com/book/en/Git-Tools-Submodules), clone, or [download](https://github.com/stephencelis/SQLite.swift/archive/master.zip) the project first.) + + ![Installation Screen Shot](Resources/installation@2x.png) + + 2. In your target’s **General** tab, click the **+** button under **Linked Frameworks and Libraries**. + + 3. Select the appropriate **SQLite.framework** for your platform. + + 4. **Add**. + +You should now be able to `import SQLite` from any of your target’s source files and begin using SQLite.swift. + +Some additional steps are required to install the application on an actual device: + + 5. In the **General** tab, click the **+** button under **Embedded Binaries**. + + 6. Select the appropriate **SQLite.framework** for your platform. + + 7. **Add**. + +## Getting Started + +To use SQLite.swift classes or structures in your target’s source file, first import the `SQLite` module. + +``` swift +import SQLite +``` + + +### Connecting to a Database + +Database connections are established using the `Connection` class. A connection is initialized with a path to a database. SQLite will attempt to create the database file if it does not already exist. + +``` swift +let db = try Connection("path/to/db.sqlite3") +``` + + +#### Read-Write Databases + +On iOS, you can create a writable database in your app’s **Documents** directory. + +``` swift +let path = NSSearchPathForDirectoriesInDomains( + .documentDirectory, .userDomainMask, true +).first! + +let db = try Connection("\(path)/db.sqlite3") +``` + +On OS X, you can use your app’s **Application Support** directory: + +``` swift +var path = NSSearchPathForDirectoriesInDomains( + .applicationSupportDirectory, .userDomainMask, true +).first! + Bundle.main.bundleIdentifier! + +// create parent directory iff it doesn’t exist +try FileManager.default.createDirectoryAtPath( + path, withIntermediateDirectories: true, attributes: nil +) + +let db = try Connection("\(path)/db.sqlite3") +``` + + +#### Read-Only Databases + +If you bundle a database with your app (_i.e._, you’ve copied a database file into your Xcode project and added it to your application target), you can establish a _read-only_ connection to it. + +``` swift +let path = Bundle.main.pathForResource("db", ofType: "sqlite3")! + +let db = try Connection(path, readonly: true) +``` + +> _Note:_ Signed applications cannot modify their bundle resources. If you bundle a database file with your app for the purpose of bootstrapping, copy it to a writable location _before_ establishing a connection (see [Read-Write Databases](#read-write-databases), above, for typical, writable locations). +> +> See these two Stack Overflow questions for more information about iOS apps with SQLite databases: [1](https://stackoverflow.com/questions/34609746/what-different-between-store-database-in-different-locations-in-ios), [2](https://stackoverflow.com/questions/34614968/ios-how-to-copy-pre-seeded-database-at-the-first-running-app-with-sqlite-swift). We welcome sample code to show how to successfully copy and use a bundled "seed" database for writing in an app. + +#### In-Memory Databases + +If you omit the path, SQLite.swift will provision an [in-memory database](https://www.sqlite.org/inmemorydb.html). + +``` swift +let db = try Connection() // equivalent to `Connection(.inMemory)` +``` + +To create a temporary, disk-backed database, pass an empty file name. + +``` swift +let db = try Connection(.temporary) +``` + +In-memory databases are automatically deleted when the database connection is closed. + + +#### Thread-Safety + +Every Connection comes equipped with its own serial queue for statement execution and can be safely accessed across threads. Threads that open transactions and savepoints will block other threads from executing statements while the transaction is open. + +If you maintain multiple connections for a single database, consider setting a timeout (in seconds) and/or a busy handler: + +```swift +db.busyTimeout = 5 + +db.busyHandler({ tries in + if tries >= 3 { + return false + } + return true +}) +``` + +> _Note:_ The default timeout is 0, so if you see `database is locked` errors, you may be trying to access the same database simultaneously from multiple connections. + + +## Building Type-Safe SQL + +SQLite.swift comes with a typed expression layer that directly maps [Swift types](https://developer.apple.com/library/prerelease/ios/documentation/General/Reference/SwiftStandardLibraryReference/) to their [SQLite counterparts](https://www.sqlite.org/datatype3.html). + +| Swift Type | SQLite Type | +| --------------- | ----------- | +| `Int64`* | `INTEGER` | +| `Double` | `REAL` | +| `String` | `TEXT` | +| `nil` | `NULL` | +| `SQLite.Blob`† | `BLOB` | + +> *While `Int64` is the basic, raw type (to preserve 64-bit integers on 32-bit platforms), `Int` and `Bool` work transparently. +> +> †SQLite.swift defines its own `Blob` structure, which safely wraps the underlying bytes. +> +> See [Custom Types](#custom-types) for more information about extending other classes and structures to work with SQLite.swift. +> +> See [Executing Arbitrary SQL](#executing-arbitrary-sql) to forego the typed layer and execute raw SQL, instead. + +These expressions (in the form of the structure, [`Expression`](#expressions)) build on one another and, with a query ([`QueryType`](#queries)), can create and execute SQL statements. + + +### Expressions + +Expressions are generic structures associated with a type ([built-in](#building-type-safe-sql) or [custom](#custom-types)), raw SQL, and (optionally) values to bind to that SQL. Typically, you will only explicitly create expressions to describe your columns, and typically only once per column. + +``` swift +let id = Expression("id") +let email = Expression("email") +let balance = Expression("balance") +let verified = Expression("verified") +``` + +Use optional generics for expressions that can evaluate to `NULL`. + +``` swift +let name = Expression("name") +``` + +> _Note:_ The default `Expression` initializer is for [quoted identifiers](https://www.sqlite.org/lang_keywords.html) (_i.e._, column names). To build a literal SQL expression, use `init(literal:)`. + + +### Compound Expressions + +Expressions can be combined with other expressions and types using [filter operators and functions](#filter-operators-and-functions) (as well as other [non-filter operators](#other-operators) and [functions](#core-sqlite-functions)). These building blocks can create complex SQLite statements. + + +### Queries + +Queries are structures that reference a database and table name, and can be used to build a variety of statements using expressions. We can create a query by initializing a `Table`, `View`, or `VirtualTable`. + +``` swift +let users = Table("users") +``` + +Assuming [the table exists](#creating-a-table), we can immediately [insert](#inserting-rows), [select](#selecting-rows), [update](#updating-rows), and [delete](#deleting-rows) rows. + + +## Creating a Table + +We can build [`CREATE TABLE` statements](https://www.sqlite.org/lang_createtable.html) by calling the `create` function on a `Table`. The following is a basic example of SQLite.swift code (using the [expressions](#expressions) and [query](#queries) above) and the corresponding SQL it generates. + +``` swift +try db.run(users.create { t in // CREATE TABLE "users" ( + t.column(id, primaryKey: true) // "id" INTEGER PRIMARY KEY NOT NULL, + t.column(email, unique: true) // "email" TEXT UNIQUE NOT NULL, + t.column(name) // "name" TEXT +}) // ) +``` + +> _Note:_ `Expression` structures (in this case, the `id` and `email` columns), generate `NOT NULL` constraints automatically, while `Expression` structures (`name`) do not. + + +### Create Table Options + +The `Table.create` function has several default parameters we can override. + + - `temporary` adds a `TEMPORARY` clause to the `CREATE TABLE` statement (to create a temporary table that will automatically drop when the database connection closes). Default: `false`. + + ``` swift + try db.run(users.create(temporary: true) { t in /* ... */ }) + // CREATE TEMPORARY TABLE "users" -- ... + ``` + + - `ifNotExists` adds an `IF NOT EXISTS` clause to the `CREATE TABLE` statement (which will bail out gracefully if the table already exists). Default: `false`. + + ``` swift + try db.run(users.create(ifNotExists: true) { t in /* ... */ }) + // CREATE TABLE "users" IF NOT EXISTS -- ... + ``` + +### Column Constraints + +The `column` function is used for a single column definition. It takes an [expression](#expressions) describing the column name and type, and accepts several parameters that map to various column constraints and clauses. + + - `primaryKey` adds a `PRIMARY KEY` constraint to a single column. + + ``` swift + t.column(id, primaryKey: true) + // "id" INTEGER PRIMARY KEY NOT NULL + + t.column(id, primaryKey: .autoincrement) + // "id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL + ``` + + > _Note:_ The `primaryKey` parameter cannot be used alongside `references`. If you need to create a column that has a default value and is also a primary and/or foreign key, use the `primaryKey` and `foreignKey` functions mentioned under [Table Constraints](#table-constraints). + > + > Primary keys cannot be optional (_e.g._, `Expression`). + > + > Only an `INTEGER PRIMARY KEY` can take `.autoincrement`. + + - `unique` adds a `UNIQUE` constraint to the column. (See the `unique` function under [Table Constraints](#table-constraints) for uniqueness over multiple columns). + + ``` swift + t.column(email, unique: true) + // "email" TEXT UNIQUE NOT NULL + ``` + + - `check` attaches a `CHECK` constraint to a column definition in the form of a boolean expression (`Expression`). Boolean expressions can be easily built using [filter operators and functions](#filter-operators-and-functions). (See also the `check` function under [Table Constraints](#table-constraints).) + + ``` swift + t.column(email, check: email.like("%@%")) + // "email" TEXT NOT NULL CHECK ("email" LIKE '%@%') + ``` + + - `defaultValue` adds a `DEFAULT` clause to a column definition and _only_ accepts a value (or expression) matching the column’s type. This value is used if none is explicitly provided during [an `INSERT`](#inserting-rows). + + ``` swift + t.column(name, defaultValue: "Anonymous") + // "name" TEXT DEFAULT 'Anonymous' + ``` + + > _Note:_ The `defaultValue` parameter cannot be used alongside `primaryKey` and `references`. If you need to create a column that has a default value and is also a primary and/or foreign key, use the `primaryKey` and `foreignKey` functions mentioned under [Table Constraints](#table-constraints). + + - `collate` adds a `COLLATE` clause to `Expression` (and `Expression`) column definitions with [a collating sequence](https://www.sqlite.org/datatype3.html#collation) defined in the `Collation` enumeration. + + ``` swift + t.column(email, collate: .nocase) + // "email" TEXT NOT NULL COLLATE "NOCASE" + + t.column(name, collate: .rtrim) + // "name" TEXT COLLATE "RTRIM" + ``` + + - `references` adds a `REFERENCES` clause to `Expression` (and `Expression`) column definitions and accepts a table (`SchemaType`) or namespaced column expression. (See the `foreignKey` function under [Table Constraints](#table-constraints) for non-integer foreign key support.) + + ``` swift + t.column(user_id, references: users, id) + // "user_id" INTEGER REFERENCES "users" ("id") + + + + > _Note:_ The `references` parameter cannot be used alongside `primaryKey` and `defaultValue`. If you need to create a column that has a default value and is also a primary and/or foreign key, use the `primaryKey` and `foreignKey` functions mentioned under [Table Constraints](#table-constraints). + + +### Table Constraints + +Additional constraints may be provided outside the scope of a single column using the following functions. + + - `primaryKey` adds a `PRIMARY KEY` constraint to the table. Unlike [the column constraint, above](#column-constraints), it supports all SQLite types, [ascending and descending orders](#sorting-rows), and composite (multiple column) keys. + + ``` swift + t.primaryKey(email.asc, name) + // PRIMARY KEY("email" ASC, "name") + ``` + + - `unique` adds a `UNIQUE` constraint to the table. Unlike [the column constraint, above](#column-constraints), it supports composite (multiple column) constraints. + + ``` swift + t.unique(local, domain) + // UNIQUE("local", "domain") + ``` + + - `check` adds a `CHECK` constraint to the table in the form of a boolean expression (`Expression`). Boolean expressions can be easily built using [filter operators and functions](#filter-operators-and-functions). (See also the `check` parameter under [Column Constraints](#column-constraints).) + + ``` swift + t.check(balance >= 0) + // CHECK ("balance" >= 0.0) + ``` + + - `foreignKey` adds a `FOREIGN KEY` constraint to the table. Unlike [the `references` constraint, above](#column-constraints), it supports all SQLite types, both [`ON UPDATE` and `ON DELETE` actions](https://www.sqlite.org/foreignkeys.html#fk_actions), and composite (multiple column) keys. + + ``` swift + t.foreignKey(user_id, references: users, id, delete: .setNull) + // FOREIGN KEY("user_id") REFERENCES "users"("id") ON DELETE SET NULL + ``` + + + + +## Inserting Rows + +We can insert rows into a table by calling a [query’s](#queries) `insert` function with a list of [setters](#setters)—typically [typed column expressions](#expressions) and values (which can also be expressions)—each joined by the `<-` operator. + +``` swift +try db.run(users.insert(email <- "alice@mac.com", name <- "Alice")) +// INSERT INTO "users" ("email", "name") VALUES ('alice@mac.com', 'Alice') + +try db.run(users.insert(or: .replace, email <- "alice@mac.com", name <- "Alice B.")) +// INSERT OR REPLACE INTO "users" ("email", "name") VALUES ('alice@mac.com', 'Alice B.') +``` + +The `insert` function, when run successfully, returns an `Int64` representing the inserted row’s [`ROWID`][ROWID]. + +``` swift +do { + let rowid = try db.run(users.insert(email <- "alice@mac.com")) + print("inserted id: \(rowid)") +} catch { + print("insertion failed: \(error)") +} +``` + +The [`update`](#updating-rows) and [`delete`](#deleting-rows) functions follow similar patterns. + +> _Note:_ If `insert` is called without any arguments, the statement will run with a `DEFAULT VALUES` clause. The table must not have any constraints that aren’t fulfilled by default values. +> +> ``` swift +> try db.run(timestamps.insert()) +> // INSERT INTO "timestamps" DEFAULT VALUES +> ``` + + +### Setters + +SQLite.swift typically uses the `<-` operator to set values during [inserts](#inserting-rows) and [updates](#updating-rows). + +``` swift +try db.run(counter.update(count <- 0)) +// UPDATE "counters" SET "count" = 0 WHERE ("id" = 1) +``` + +There are also a number of convenience setters that take the existing value into account using native Swift operators. + +For example, to atomically increment a column, we can use `++`: + +``` swift +try db.run(counter.update(count++)) // equivalent to `counter.update(count -> count + 1)` +// UPDATE "counters" SET "count" = "count" + 1 WHERE ("id" = 1) +``` + +To take an amount and “move” it via transaction, we can use `-=` and `+=`: + +``` swift +let amount = 100.0 +try db.transaction { + try db.run(alice.update(balance -= amount)) + try db.run(betty.update(balance += amount)) +} +// BEGIN DEFERRED TRANSACTION +// UPDATE "users" SET "balance" = "balance" - 100.0 WHERE ("id" = 1) +// UPDATE "users" SET "balance" = "balance" + 100.0 WHERE ("id" = 2) +// COMMIT TRANSACTION +``` + + +###### Infix Setters + +| Operator | Types | +| -------- | ------------------ | +| `<-` | `Value -> Value` | +| `+=` | `Number -> Number` | +| `-=` | `Number -> Number` | +| `*=` | `Number -> Number` | +| `/=` | `Number -> Number` | +| `%=` | `Int -> Int` | +| `<<=` | `Int -> Int` | +| `>>=` | `Int -> Int` | +| `&=` | `Int -> Int` | +| `||=` | `Int -> Int` | +| `^=` | `Int -> Int` | +| `+=` | `String -> String` | + + +###### Postfix Setters + +| Operator | Types | +| -------- | ------------ | +| `++` | `Int -> Int` | +| `--` | `Int -> Int` | + + +## Selecting Rows + +[Query structures](#queries) are `SELECT` statements waiting to happen. They execute via [iteration](#iterating-and-accessing-values) and [other means](#plucking-values) of sequence access. + + +### Iterating and Accessing Values + +Prepared [queries](#queries) execute lazily upon iteration. Each row is returned as a `Row` object, which can be subscripted with a [column expression](#expressions) matching one of the columns returned. + +``` swift +for user in try db.prepare(users) { + print("id: \(user[id]), email: \(user[email]), name: \(user[name])") + // id: 1, email: alice@mac.com, name: Optional("Alice") +} +// SELECT * FROM "users" +``` + +`Expression` column values are _automatically unwrapped_ (we’ve made a promise to the compiler that they’ll never be `NULL`), while `Expression` values remain wrapped. + + +### Plucking Rows + +We can pluck the first row by passing a query to the `pluck` function on a database connection. + +``` swift +if let user = try db.pluck(users) { /* ... */ } // Row +// SELECT * FROM "users" LIMIT 1 +``` + +To collect all rows into an array, we can simply wrap the sequence (though this is not always the most memory-efficient idea). + +``` swift +let all = Array(try db.prepare(users)) +// SELECT * FROM "users" +``` + + +### Building Complex Queries + +[Queries](#queries) have a number of chainable functions that can be used (with [expressions](#expressions)) to add and modify [a number of clauses](https://www.sqlite.org/lang_select.html) to the underlying statement. + +``` swift +let query = users.select(email) // SELECT "email" FROM "users" + .filter(name != nil) // WHERE "name" IS NOT NULL + .order(email.desc, name) // ORDER BY "email" DESC, "name" + .limit(5, offset: 1) // LIMIT 5 OFFSET 1 +``` + + +#### Selecting Columns + +By default, [queries](#queries) select every column of the result set (using `SELECT *`). We can use the `select` function with a list of [expressions](#expressions) to return specific columns instead. + +``` swift +for user in try db.prepare(users.select(id, email)) { + print("id: \(user[id]), email: \(user[email])") + // id: 1, email: alice@mac.com +} +// SELECT "id", "email" FROM "users" +``` + +We can access the results of more complex expressions by holding onto a reference of the expression itself. + +``` swift +let sentence = name + " is " + cast(age) as Expression + " years old!" +for user in users.select(sentence) { + print(user[sentence]) + // Optional("Alice is 30 years old!") +} +// SELECT ((("name" || ' is ') || CAST ("age" AS TEXT)) || ' years old!') FROM "users" +``` + + +#### Joining Other Tables + +We can join tables using a [query’s](#queries) `join` function. + +``` swift +users.join(posts, on: user_id == users[id]) +// SELECT * FROM "users" INNER JOIN "posts" ON ("user_id" = "users"."id") +``` + +The `join` function takes a [query](#queries) object (for the table being joined on), a join condition (`on`), and is prefixed with an optional join type (default: `.inner`). Join conditions can be built using [filter operators and functions](#filter-operators-and-functions), generally require [namespacing](#column-namespacing), and sometimes require [aliasing](#table-aliasing). + + +##### Column Namespacing + +When joining tables, column names can become ambiguous. _E.g._, both tables may have an `id` column. + +``` swift +let query = users.join(posts, on: user_id == id) +// assertion failure: ambiguous column 'id' +``` + +We can disambiguate by namespacing `id`. + +``` swift +let query = users.join(posts, on: user_id == users[id]) +// SELECT * FROM "users" INNER JOIN "posts" ON ("user_id" = "users"."id") +``` + +Namespacing is achieved by subscripting a [query](#queries) with a [column expression](#expressions) (_e.g._, `users[id]` above becomes `users.id`). + +> _Note:_ We can namespace all of a table’s columns using `*`. +> +> ``` swift +> let query = users.select(users[*]) +> // SELECT "users".* FROM "users" +> ``` + + +##### Table Aliasing + +Occasionally, we need to join a table to itself, in which case we must alias the table with another name. We can achieve this using the [query’s](#queries) `alias` function. + +``` swift +let managers = users.alias("managers") + +let query = users.join(managers, on: managers[id] == users[managerId]) +// SELECT * FROM "users" +// INNER JOIN ("users") AS "managers" ON ("managers"."id" = "users"."manager_id") +``` + +If query results can have ambiguous column names, row values should be accessed with namespaced [column expressions](#expressions). In the above case, `SELECT *` immediately namespaces all columns of the result set. + +``` swift +let user = try db.pluck(query) +user[id] // fatal error: ambiguous column 'id' + // (please disambiguate: ["users"."id", "managers"."id"]) + +user[users[id]] // returns "users"."id" +user[managers[id]] // returns "managers"."id" +``` + + +#### Filtering Rows + +SQLite.swift filters rows using a [query’s](#queries) `filter` function with a boolean [expression](#expressions) (`Expression`). + +``` swift +users.filter(id == 1) +// SELECT * FROM "users" WHERE ("id" = 1) + +users.filter([1, 2, 3, 4, 5].contains(id)) +// SELECT * FROM "users" WHERE ("id" IN (1, 2, 3, 4, 5)) + +users.filter(email.like("%@mac.com")) +// SELECT * FROM "users" WHERE ("email" LIKE '%@mac.com') + +users.filter(verified && name.lowercaseString == "alice") +// SELECT * FROM "users" WHERE ("verified" AND (lower("name") == 'alice')) + +users.filter(verified || balance >= 10_000) +// SELECT * FROM "users" WHERE ("verified" OR ("balance" >= 10000.0)) +``` + +We can build our own boolean expressions by using one of the many [filter operators and functions](#filter-operators-and-functions). + +Instead of `filter` we can also use the `where` function which is an alias: + +``` swift +users.where(id == 1) +// SELECT * FROM "users" WHERE ("id" = 1) +``` + +##### Filter Operators and Functions + +SQLite.swift defines a number of operators for building filtering predicates. Operators and functions work together in a type-safe manner, so attempting to equate or compare different types will prevent compilation. + + +###### Infix Filter Operators + +| Swift | Types | SQLite | +| ----- | -------------------------------- | -------------- | +| `==` | `Equatable -> Bool` | `=`/`IS`* | +| `!=` | `Equatable -> Bool` | `!=`/`IS NOT`* | +| `>` | `Comparable -> Bool` | `>` | +| `>=` | `Comparable -> Bool` | `>=` | +| `<` | `Comparable -> Bool` | `<` | +| `<=` | `Comparable -> Bool` | `<=` | +| `~=` | `(Interval, Comparable) -> Bool` | `BETWEEN` | +| `&&` | `Bool -> Bool` | `AND` | +| `||` | `Bool -> Bool` | `OR` | + +> *When comparing against `nil`, SQLite.swift will use `IS` and `IS NOT` accordingly. + + +###### Prefix Filter Operators + +| Swift | Types | SQLite | +| ----- | ------------------ | ------ | +| `!` | `Bool -> Bool` | `NOT` | + + +###### Filtering Functions + +| Swift | Types | SQLite | +| ---------- | ----------------------- | ------- | +| `like` | `String -> Bool` | `LIKE` | +| `glob` | `String -> Bool` | `GLOB` | +| `match` | `String -> Bool` | `MATCH` | +| `contains` | `(Array, T) -> Bool` | `IN` | + + + + + +#### Sorting Rows + +We can pre-sort returned rows using the [query’s](#queries) `order` function. + +_E.g._, to return users sorted by `email`, then `name`, in ascending order: + +``` swift +users.order(email, name) +// SELECT * FROM "users" ORDER BY "email", "name" +``` + +The `order` function takes a list of [column expressions](#expressions). + +`Expression` objects have two computed properties to assist sorting: `asc` and `desc`. These properties append the expression with `ASC` and `DESC` to mark ascending and descending order respectively. + +``` swift +users.order(email.desc, name.asc) +// SELECT * FROM "users" ORDER BY "email" DESC, "name" ASC +``` + + +#### Limiting and Paging Results + +We can limit and skip returned rows using a [query’s](#queries) `limit` function (and its optional `offset` parameter). + +``` swift +users.limit(5) +// SELECT * FROM "users" LIMIT 5 + +users.limit(5, offset: 5) +// SELECT * FROM "users" LIMIT 5 OFFSET 5 +``` + + +#### Aggregation + +[Queries](#queries) come with a number of functions that quickly return aggregate scalar values from the table. These mirror the [core aggregate functions](#aggregate-sqlite-functions) and are executed immediately against the query. + +``` swift +let count = try db.scalar(users.count) +// SELECT count(*) FROM "users" +``` + +Filtered queries will appropriately filter aggregate values. + +``` swift +let count = try db.scalar(users.filter(name != nil).count) +// SELECT count(*) FROM "users" WHERE "name" IS NOT NULL +``` + + - `count` as a computed property on a query (see examples above) returns the total number of rows matching the query. + + `count` as a computed property on a column expression returns the total number of rows where that column is not `NULL`. + + ``` swift + let count = try db.scalar(users.select(name.count)) // -> Int + // SELECT count("name") FROM "users" + ``` + + - `max` takes a comparable column expression and returns the largest value if any exists. + + ``` swift + let max = try db.scalar(users.select(id.max)) // -> Int64? + // SELECT max("id") FROM "users" + ``` + + - `min` takes a comparable column expression and returns the smallest value if any exists. + + ``` swift + let min = try db.scalar(users.select(id.min)) // -> Int64? + // SELECT min("id") FROM "users" + ``` + + - `average` takes a numeric column expression and returns the average row value (as a `Double`) if any exists. + + ``` swift + let average = try db.scalar(users.select(balance.average)) // -> Double? + // SELECT avg("balance") FROM "users" + ``` + + - `sum` takes a numeric column expression and returns the sum total of all rows if any exist. + + ``` swift + let sum = try db.scalar(users.select(balance.sum)) // -> Double? + // SELECT sum("balance") FROM "users" + ``` + + - `total`, like `sum`, takes a numeric column expression and returns the sum total of all rows, but in this case always returns a `Double`, and returns `0.0` for an empty query. + + ``` swift + let total = try db.scalar(users.select(balance.total)) // -> Double + // SELECT total("balance") FROM "users" + ``` + +> _Note:_ Expressions can be prefixed with a `DISTINCT` clause by calling the `distinct` computed property. +> +> ``` swift +> let count = try db.scalar(users.select(name.distinct.count) // -> Int +> // SELECT count(DISTINCT "name") FROM "users" +> ``` + + +## Updating Rows + +We can update a table’s rows by calling a [query’s](#queries) `update` function with a list of [setters](#setters)—typically [typed column expressions](#expressions) and values (which can also be expressions)—each joined by the `<-` operator. + +When an unscoped query calls `update`, it will update _every_ row in the table. + +``` swift +try db.run(users.update(email <- "alice@me.com")) +// UPDATE "users" SET "email" = 'alice@me.com' +``` + +Be sure to scope `UPDATE` statements beforehand using [the `filter` function](#filtering-rows). + +``` swift +let alice = users.filter(id == 1) +try db.run(alice.update(email <- "alice@me.com")) +// UPDATE "users" SET "email" = 'alice@me.com' WHERE ("id" = 1) +``` + +The `update` function returns an `Int` representing the number of updated rows. + +``` swift +do { + if try db.run(alice.update(email <- "alice@me.com")) > 0 { + print("updated alice") + } else { + print("alice not found") + } +} catch { + print("update failed: \(error)") +} +``` + + +## Deleting Rows + +We can delete rows from a table by calling a [query’s](#queries) `delete` function. + +When an unscoped query calls `delete`, it will delete _every_ row in the table. + +``` swift +try db.run(users.delete()) +// DELETE FROM "users" +``` + +Be sure to scope `DELETE` statements beforehand using [the `filter` function](#filtering-rows). + +``` swift +let alice = users.filter(id == 1) +try db.run(alice.delete()) +// DELETE FROM "users" WHERE ("id" = 1) +``` + +The `delete` function returns an `Int` representing the number of deleted rows. + +``` swift +do { + if try db.run(alice.delete()) > 0 { + print("deleted alice") + } else { + print("alice not found") + } +} catch { + print("delete failed: \(error)") +} +``` + + +## Transactions and Savepoints + +Using the `transaction` and `savepoint` functions, we can run a series of statements in a transaction. If a single statement fails or the block throws an error, the changes will be rolled back. + +``` swift +try db.transaction { + let rowid = try db.run(users.insert(email <- "betty@icloud.com")) + try db.run(users.insert(email <- "cathy@icloud.com", managerId <- rowid)) +} +// BEGIN DEFERRED TRANSACTION +// INSERT INTO "users" ("email") VALUES ('betty@icloud.com') +// INSERT INTO "users" ("email", "manager_id") VALUES ('cathy@icloud.com', 2) +// COMMIT TRANSACTION +``` + +> _Note:_ Transactions run in a serial queue. + + +## Altering the Schema + +SQLite.swift comes with several functions (in addition to `Table.create`) for altering a database schema in a type-safe manner. + + +### Renaming Tables + +We can build an `ALTER TABLE … RENAME TO` statement by calling the `rename` function on a `Table` or `VirtualTable`. + +``` swift +try db.run(users.rename(Table("users_old")) +// ALTER TABLE "users" RENAME TO "users_old" +``` + + +### Adding Columns + +We can add columns to a table by calling `addColumn` function on a `Table`. SQLite.swift enforces [the same limited subset](https://www.sqlite.org/lang_altertable.html) of `ALTER TABLE` that SQLite supports. + +``` swift +try db.run(users.addColumn(suffix)) +// ALTER TABLE "users" ADD COLUMN "suffix" TEXT +``` + + +#### Added Column Constraints + +The `addColumn` function shares several of the same [`column` function parameters](#column-constraints) used when [creating tables](#creating-a-table). + + - `check` attaches a `CHECK` constraint to a column definition in the form of a boolean expression (`Expression`). (See also the `check` function under [Table Constraints](#table-constraints).) + + ``` swift + try db.run(users.addColumn(suffix, check: ["JR", "SR"].contains(suffix))) + // ALTER TABLE "users" ADD COLUMN "suffix" TEXT CHECK ("suffix" IN ('JR', 'SR')) + ``` + + - `defaultValue` adds a `DEFAULT` clause to a column definition and _only_ accepts a value matching the column’s type. This value is used if none is explicitly provided during [an `INSERT`](#inserting-rows). + + ``` swift + try db.run(users.addColumn(suffix, defaultValue: "SR")) + // ALTER TABLE "users" ADD COLUMN "suffix" TEXT DEFAULT 'SR' + ``` + + > _Note:_ Unlike the [`CREATE TABLE` constraint](#table-constraints), default values may not be expression structures (including `CURRENT_TIME`, `CURRENT_DATE`, or `CURRENT_TIMESTAMP`). + + - `collate` adds a `COLLATE` clause to `Expression` (and `Expression`) column definitions with [a collating sequence](https://www.sqlite.org/datatype3.html#collation) defined in the `Collation` enumeration. + + ``` swift + try db.run(users.addColumn(email, collate: .nocase)) + // ALTER TABLE "users" ADD COLUMN "email" TEXT NOT NULL COLLATE "NOCASE" + + try db.run(users.addColumn(name, collate: .rtrim)) + // ALTER TABLE "users" ADD COLUMN "name" TEXT COLLATE "RTRIM" + ``` + + - `references` adds a `REFERENCES` clause to `Int64` (and `Int64?`) column definitions and accepts a table or namespaced column expression. (See the `foreignKey` function under [Table Constraints](#table-constraints) for non-integer foreign key support.) + + ``` swift + try db.run(posts.addColumn(userId, references: users, id) + // ALTER TABLE "posts" ADD COLUMN "user_id" INTEGER REFERENCES "users" ("id") + ``` + + +### Indexes + + +#### Creating Indexes + +We can build [`CREATE INDEX` statements](https://www.sqlite.org/lang_createindex.html) by calling the `createIndex` function on a `SchemaType`. + +``` swift +try db.run(users.createIndex(email)) +// CREATE INDEX "index_users_on_email" ON "users" ("email") +``` + +The index name is generated automatically based on the table and column names. + +The `createIndex` function has a couple default parameters we can override. + + - `unique` adds a `UNIQUE` constraint to the index. Default: `false`. + + ``` swift + try db.run(users.createIndex(email, unique: true)) + // CREATE UNIQUE INDEX "index_users_on_email" ON "users" ("email") + ``` + + - `ifNotExists` adds an `IF NOT EXISTS` clause to the `CREATE TABLE` statement (which will bail out gracefully if the table already exists). Default: `false`. + + ``` swift + try db.run(users.createIndex(email, ifNotExists: true)) + // CREATE INDEX IF NOT EXISTS "index_users_on_email" ON "users" ("email") + ``` + + +#### Dropping Indexes + +We can build [`DROP INDEX` statements](https://www.sqlite.org/lang_dropindex.html) by calling the `dropIndex` function on a `SchemaType`. + +``` swift +try db.run(users.dropIndex(email)) +// DROP INDEX "index_users_on_email" +``` + +The `dropIndex` function has one additional parameter, `ifExists`, which (when `true`) adds an `IF EXISTS` clause to the statement. + +``` swift +try db.run(users.dropIndex(email, ifExists: true)) +// DROP INDEX IF EXISTS "index_users_on_email" +``` + + +### Dropping Tables + +We can build [`DROP TABLE` statements](https://www.sqlite.org/lang_droptable.html) by calling the `dropTable` function on a `SchemaType`. + +``` swift +try db.run(users.drop()) +// DROP TABLE "users" +``` + +The `drop` function has one additional parameter, `ifExists`, which (when `true`) adds an `IF EXISTS` clause to the statement. + +``` swift +try db.run(users.drop(ifExists: true)) +// DROP TABLE IF EXISTS "users" +``` + + +### Migrations and Schema Versioning + +You can add a convenience property on `Connection` to query and set the [`PRAGMA user_version`](https://sqlite.org/pragma.html#pragma_user_version). + +This is a great way to manage your schema’s version over migrations. + +``` swift +extension Connection { + public var userVersion: Int32 { + get { return Int32(try! scalar("PRAGMA user_version") as! Int64)} + set { try! run("PRAGMA user_version = \(newValue)") } + } +} +``` + +Then you can conditionally run your migrations along the lines of: + +```swift +if db.userVersion == 0 { + // handle first migration + db.userVersion = 1 +} +if db.userVersion == 1 { + // handle second migration + db.userVersion = 2 +} +``` + +For more complex migration requirements check out the schema management system +[SQLiteMigrationManager.swift][]. + +## Custom Types + +SQLite.swift supports serializing and deserializing any custom type as long as it conforms to the `Value` protocol. + +> ``` swift +> protocol Value { +> typealias Datatype: Binding +> class var declaredDatatype: String { get } +> class func fromDatatypeValue(datatypeValue: Datatype) -> Self +> var datatypeValue: Datatype { get } +> } +> ``` + +The `Datatype` must be one of the basic Swift types that values are bridged through before serialization and deserialization (see [Building Type-Safe SQL](#building-type-safe-sql) for a list of types). + +> _Note:_ `Binding` is a protocol that SQLite.swift uses internally to directly map SQLite types to Swift types. **Do _not_** conform custom types to the `Binding` protocol. + +Once extended, the type can be used [_almost_](#custom-type-caveats) wherever typed expressions can be. + + +### Date-Time Values + +In SQLite, `DATETIME` columns can be treated as strings or numbers, so we can transparently bridge `Date` objects through Swift’s `String` or `Int` types. + +To serialize `Date` objects as `TEXT` values (in ISO 8601), we’ll use `String`. + +``` swift +extension Date: Value { + class var declaredDatatype: String { + return String.declaredDatatype + } + class func fromDatatypeValue(stringValue: String) -> Date { + return SQLDateFormatter.dateFromString(stringValue)! + } + var datatypeValue: String { + return SQLDateFormatter.stringFromDate(self) + } +} + +let SQLDateFormatter: DateFormatter = { + let formatter = DateFormatter() + formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSS" + formatter.locale = Locale(localeIdentifier: "en_US_POSIX") + formatter.timeZone = TimeZone(forSecondsFromGMT: 0) + return formatter +}() +``` + +We can also treat them as `INTEGER` values using `Int`. + +``` swift +extension Date: Value { + class var declaredDatatype: String { + return Int.declaredDatatype + } + class func fromDatatypeValue(intValue: Int) -> Self { + return self(timeIntervalSince1970: TimeInterval(intValue)) + } + var datatypeValue: Int { + return Int(timeIntervalSince1970) + } +} +``` + +> _Note:_ SQLite’s `CURRENT_DATE`, `CURRENT_TIME`, and `CURRENT_TIMESTAMP` helpers return `TEXT` values. Because of this (and the fact that Unix time is far less human-readable when we’re faced with the raw data), we recommend using the `TEXT` extension. + +Once defined, we can use these types directly in SQLite statements. + +``` swift +let published_at = Expression("published_at") + +let published = posts.filter(published_at <= Date()) +// extension where Datatype == String: +// SELECT * FROM "posts" WHERE "published_at" <= '2014-11-18 12:45:30' +// extension where Datatype == Int: +// SELECT * FROM "posts" WHERE "published_at" <= 1416314730 +``` + + +### Binary Data + +We can bridge any type that can be initialized from and encoded to `Data`. + +``` swift +extension UIImage: Value { + public class var declaredDatatype: String { + return Blob.declaredDatatype + } + public class func fromDatatypeValue(blobValue: Blob) -> UIImage { + return UIImage(data: Data.fromDatatypeValue(blobValue))! + } + public var datatypeValue: Blob { + return UIImagePNGRepresentation(self)!.datatypeValue + } + +} +``` + +> _Note:_ See the [Archives and Serializations Programming Guide](https://developer.apple.com/library/ios/documentation/Cocoa/Conceptual/Archiving/Archiving.html#//apple_ref/doc/uid/10000047i) for more information on encoding and decoding custom types. + + +### Custom Type Caveats + +Swift does _not_ currently support generic subscripting, which means we cannot, by default, subscript Expressions with custom types to: + + 1. **Namespace expressions**. Use the `namespace` function, instead: + + ``` swift + let avatar = Expression("avatar") + users[avatar] // fails to compile + users.namespace(avatar) // "users"."avatar" + ``` + + 2. **Access column data**. Use the `get` function, instead: + + ``` swift + let user = users.first! + user[avatar] // fails to compile + user.get(avatar) // UIImage? + ``` + +We can, of course, write extensions, but they’re rather wordy. + +``` swift +extension Query { + subscript(column: Expression) -> Expression { + return namespace(column) + } + subscript(column: Expression) -> Expression { + return namespace(column) + } +} + +extension Row { + subscript(column: Expression) -> UIImage { + return get(column) + } + subscript(column: Expression) -> UIImage? { + return get(column) + } +} +``` + + +## Other Operators + +In addition to [filter operators](#filtering-infix-operators), SQLite.swift defines a number of operators that can modify expression values with arithmetic, bitwise operations, and concatenation. + + +###### Other Infix Operators + +| Swift | Types | SQLite | +| ----- | -------------------------------- | -------- | +| `+` | `Number -> Number` | `+` | +| `-` | `Number -> Number` | `-` | +| `*` | `Number -> Number` | `*` | +| `/` | `Number -> Number` | `/` | +| `%` | `Int -> Int` | `%` | +| `<<` | `Int -> Int` | `<<` | +| `>>` | `Int -> Int` | `>>` | +| `&` | `Int -> Int` | `&` | +| `|` | `Int -> Int` | `|` | +| `+` | `String -> String` | `||` | + +> _Note:_ SQLite.swift also defines a bitwise XOR operator, `^`, which expands the expression `lhs ^ rhs` to `~(lhs & rhs) & (lhs | rhs)`. + + +###### Other Prefix Operators + +| Swift | Types | SQLite | +| ----- | ------------------ | ------ | +| `~` | `Int -> Int` | `~` | +| `-` | `Number -> Number` | `-` | + + +## Core SQLite Functions + +Many of SQLite’s [core functions](https://www.sqlite.org/lang_corefunc.html) have been surfaced in and type-audited for SQLite.swift. + +> _Note:_ SQLite.swift aliases the `??` operator to the `ifnull` function. +> +> ``` swift +> name ?? email // ifnull("name", "email") +> ``` + + +## Aggregate SQLite Functions + +Most of SQLite’s [aggregate functions](https://www.sqlite.org/lang_aggfunc.html) have been surfaced in and type-audited for SQLite.swift. + + +## Custom SQL Functions + +We can create custom SQL functions by calling `createFunction` on a database connection. + +For example, to give queries access to [`MobileCoreServices.UTTypeConformsTo`](https://developer.apple.com/library/ios/documentation/MobileCoreServices/Reference/UTTypeRef/index.html#//apple_ref/c/func/UTTypeConformsTo), we can write the following: + +``` swift +import MobileCoreServices + +let typeConformsTo: (Expression, String) -> Expression = ( + try db.createFunction("typeConformsTo", deterministic: true) { UTI, conformsToUTI in + return UTTypeConformsTo(UTI, conformsToUTI) + } +) +``` + +> _Note:_ The optional `deterministic` parameter is an optimization that causes the function to be created with [`SQLITE_DETERMINISTIC`](https://www.sqlite.org/c3ref/create_function.html). + +Note `typeConformsTo`’s signature: + +``` swift +(Expression, String) -> Expression +``` + +Because of this, `createFunction` expects a block with the following signature: + +``` swift +(String, String) -> Bool +``` + +Once assigned, the closure can be called wherever boolean expressions are accepted. + +``` swift +let attachments = Table("attachments") +let UTI = Expression("UTI") + +let images = attachments.filter(typeConformsTo(UTI, kUTTypeImage)) +// SELECT * FROM "attachments" WHERE "typeConformsTo"("UTI", 'public.image') +``` + +> _Note:_ The return type of a function must be [a core SQL type](#building-type-safe-sql) or [conform to `Value`](#custom-types). + +We can create loosely-typed functions by handling an array of raw arguments, instead. + +``` swift +db.createFunction("typeConformsTo", deterministic: true) { args in + guard let UTI = args[0] as? String, conformsToUTI = args[1] as? String else { return nil } + return UTTypeConformsTo(UTI, conformsToUTI) +} +``` + +Creating a loosely-typed function cannot return a closure and instead must be wrapped manually or executed [using raw SQL](#executing-arbitrary-sql). + +``` swift +let stmt = try db.prepare("SELECT * FROM attachments WHERE typeConformsTo(UTI, ?)") +for row in stmt.bind(kUTTypeImage) { /* ... */ } +``` + + +## Custom Collations + +We can create custom collating sequences by calling `createCollation` on a database connection. + +``` swift +try db.createCollation("NODIACRITIC") { lhs, rhs in + return lhs.compare(rhs, options: .diacriticInsensitiveSearch) +} +``` + +We can reference a custom collation using the `Custom` member of the `Collation` enumeration. + +``` swift +restaurants.order(collate(.custom("NODIACRITIC"), name)) +// SELECT * FROM "restaurants" ORDER BY "name" COLLATE "NODIACRITIC" +``` + + +## Full-text Search + +We can create a virtual table using the [FTS4 module](http://www.sqlite.org/fts3.html) by calling `create` on a `VirtualTable`. + +``` swift +let emails = VirtualTable("emails") +let subject = Expression("subject") +let body = Expression("body") + +try db.run(emails.create(.FTS4(subject, body))) +// CREATE VIRTUAL TABLE "emails" USING fts4("subject", "body") +``` + +We can specify a [tokenizer](http://www.sqlite.org/fts3.html#tokenizer) using the `tokenize` parameter. + +``` swift +try db.run(emails.create(.FTS4([subject, body], tokenize: .Porter))) +// CREATE VIRTUAL TABLE "emails" USING fts4("subject", "body", tokenize=porter) +``` + +We can set the full range of parameters by creating a `FTS4Config` object. + +``` swift +let emails = VirtualTable("emails") +let subject = Expression("subject") +let body = Expression("body") +let config = FTS4Config() + .column(subject) + .column(body, [.unindexed]) + .languageId("lid") + .order(.desc) + +try db.run(emails.create(.FTS4(config)) +// CREATE VIRTUAL TABLE "emails" USING fts4("subject", "body", notindexed="body", languageid="lid", order="desc") +``` + +Once we insert a few rows, we can search using the `match` function, which takes a table or column as its first argument and a query string as its second. + +``` swift +try db.run(emails.insert( + subject <- "Just Checking In", + body <- "Hey, I was just wondering...did you get my last email?" +)) + +let wonderfulEmails = emails.match("wonder*") +// SELECT * FROM "emails" WHERE "emails" MATCH 'wonder*' + +let replies = emails.filter(subject.match("Re:*")) +// SELECT * FROM "emails" WHERE "subject" MATCH 'Re:*' +``` + +### FTS5 + +When linking against a version of SQLite with [FTS5](http://www.sqlite.org/fts5.html) enabled we can create the virtual table +in a similar fashion. + +```swift +let emails = VirtualTable("emails") +let subject = Expression("subject") +let body = Expression("body") +let config = FTS5Config() + .column(subject) + .column(body, [.unindexed]) + +try db.run(emails.create(.FTS5(config)) +// CREATE VIRTUAL TABLE "emails" USING fts5("subject", "body" UNINDEXED) + +// Note that FTS5 uses a different syntax to select columns, so we need to rewrite +// the last FTS4 query above as: +let replies = emails.filter(emails.match("subject:\"Re:\"*)) +// SELECT * FROM "emails" WHERE "emails" MATCH 'subject:"Re:"*' + +// https://www.sqlite.org/fts5.html#_changes_to_select_statements_ +``` + +## Executing Arbitrary SQL + +Though we recommend you stick with SQLite.swift’s [type-safe system](#building-type-safe-sql) whenever possible, it is possible to simply and safely prepare and execute raw SQL statements via a `Database` connection using the following functions. + + - `execute` runs an arbitrary number of SQL statements as a convenience. + + ``` swift + try db.execute( + "BEGIN TRANSACTION;" + + "CREATE TABLE users (" + + "id INTEGER PRIMARY KEY NOT NULL," + + "email TEXT UNIQUE NOT NULL," + + "name TEXT" + + ");" + + "CREATE TABLE posts (" + + "id INTEGER PRIMARY KEY NOT NULL," + + "title TEXT NOT NULL," + + "body TEXT NOT NULL," + + "published_at DATETIME" + + ");" + + "PRAGMA user_version = 1;" + + "COMMIT TRANSACTION;" + ) + ``` + + - `prepare` prepares a single `Statement` object from a SQL string, optionally binds values to it (using the statement’s `bind` function), and returns the statement for deferred execution. + + ``` swift + let stmt = try db.prepare("INSERT INTO users (email) VALUES (?)") + ``` + + Once prepared, statements may be executed using `run`, binding any unbound parameters. + + ``` swift + try stmt.run("alice@mac.com") + db.changes // -> {Some 1} + ``` + + Statements with results may be iterated over, using the columnNames if useful. + + ``` swift + let stmt = try db.prepare("SELECT id, email FROM users") + for row in stmt { + for (index, name) in stmt.columnNames.enumerate() { + print ("\(name)=\(row[index]!)") + // id: Optional(1), email: Optional("alice@mac.com") + } + } + ``` + + - `run` prepares a single `Statement` object from a SQL string, optionally binds values to it (using the statement’s `bind` function), executes, and returns the statement. + + ``` swift + try db.run("INSERT INTO users (email) VALUES (?)", "alice@mac.com") + ``` + + - `scalar` prepares a single `Statement` object from a SQL string, optionally binds values to it (using the statement’s `bind` function), executes, and returns the first value of the first row. + + ``` swift + let count = try db.scalar("SELECT count(*) FROM users") as! Int64 + ``` + + Statements also have a `scalar` function, which can optionally re-bind values at execution. + + ``` swift + let stmt = try db.prepare("SELECT count (*) FROM users") + let count = try stmt.scalar() as! Int64 + ``` + + +## Logging + +We can log SQL using the database’s `trace` function. + +``` swift +#if DEBUG + db.trace(print) +#endif +``` + + +[ROWID]: https://sqlite.org/lang_createtable.html#rowid +[SQLiteMigrationManager.swift]: https://github.com/garriguv/SQLiteMigrationManager.swift diff --git a/mobile/ios/ThirdParty/SQLite.swift/Documentation/Planning.md b/mobile/ios/ThirdParty/SQLite.swift/Documentation/Planning.md new file mode 100644 index 0000000000..d814d26b06 --- /dev/null +++ b/mobile/ios/ThirdParty/SQLite.swift/Documentation/Planning.md @@ -0,0 +1,24 @@ +# SQLite.swift Planning + +This document captures both near term steps (aka Roadmap) and feature requests. +The goal is to add some visibility and guidance for future additions and Pull Requests, as well as to keep the Issues list clear of enhancement requests so that bugs are more visible. + +## Roadmap + +_Lists agreed upon next steps in approximate priority order._ + +## Feature Requests + +_A gathering point for ideas for new features. In general, the corresponding issue will be closed once it is added here, with the assumption that it will be referred to when it comes time to add the corresponding feature._ + +### Features + + * encapsulate ATTACH DATABASE / DETACH DATABASE as methods, per [#30](https://github.com/stephencelis/SQLite.swift/issues/30) + * provide separate threads for update vs read, so updates don't block reads, per [#236](https://github.com/stephencelis/SQLite.swift/issues/236) + * expose triggers, per [#164](https://github.com/stephencelis/SQLite.swift/issues/164) + +## Suspended Feature Requests + +_Features that are not actively being considered, perhaps because of no clean type-safe way to implement them with the current Swift, or bugs, or just general uncertainty._ + + * provide a mechanism for INSERT INTO multiple values, per [#168](https://github.com/stephencelis/SQLite.swift/issues/168) diff --git a/mobile/ios/ThirdParty/SQLite.swift/Documentation/Resources/installation@2x.png b/mobile/ios/ThirdParty/SQLite.swift/Documentation/Resources/installation@2x.png new file mode 100644 index 0000000000..6b31f45934 Binary files /dev/null and b/mobile/ios/ThirdParty/SQLite.swift/Documentation/Resources/installation@2x.png differ diff --git a/mobile/ios/ThirdParty/SQLite.swift/Documentation/Resources/playground@2x.png b/mobile/ios/ThirdParty/SQLite.swift/Documentation/Resources/playground@2x.png new file mode 100644 index 0000000000..32646d6aa4 Binary files /dev/null and b/mobile/ios/ThirdParty/SQLite.swift/Documentation/Resources/playground@2x.png differ diff --git a/mobile/ios/ThirdParty/SQLite.swift/LICENSE.txt b/mobile/ios/ThirdParty/SQLite.swift/LICENSE.txt new file mode 100644 index 0000000000..13303c1135 --- /dev/null +++ b/mobile/ios/ThirdParty/SQLite.swift/LICENSE.txt @@ -0,0 +1,21 @@ +(The MIT License) + +Copyright (c) 2014-2015 Stephen Celis () + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/mobile/ios/ThirdParty/SQLite.swift/Makefile b/mobile/ios/ThirdParty/SQLite.swift/Makefile new file mode 100644 index 0000000000..d4f51d53e3 --- /dev/null +++ b/mobile/ios/ThirdParty/SQLite.swift/Makefile @@ -0,0 +1,60 @@ +BUILD_TOOL = xcodebuild +BUILD_SCHEME = SQLite Mac +IOS_SIMULATOR = iPhone 6s +IOS_VERSION = 9.3 +ifeq ($(BUILD_SCHEME),SQLite iOS) + BUILD_ARGUMENTS = -scheme "$(BUILD_SCHEME)" -destination "platform=iOS Simulator,name=$(IOS_SIMULATOR),OS=$(IOS_VERSION)" +else + BUILD_ARGUMENTS = -scheme "$(BUILD_SCHEME)" +endif + +XCPRETTY := $(shell command -v xcpretty) +SWIFTCOV := $(shell command -v swiftcov) +GCOVR := $(shell command -v gcovr) +TEST_ACTIONS := clean build build-for-testing test-without-building + +default: test + +build: + $(BUILD_TOOL) $(BUILD_ARGUMENTS) + +test: +ifdef XCPRETTY + @set -o pipefail && $(BUILD_TOOL) $(BUILD_ARGUMENTS) $(TEST_ACTIONS) | $(XCPRETTY) -c +else + $(BUILD_TOOL) $(BUILD_ARGUMENTS) $(TEST_ACTIONS) +endif + +coverage: +ifdef SWIFTCOV + $(SWIFTCOV) generate --output coverage \ + $(BUILD_TOOL) $(BUILD_ARGUMENTS) -configuration Release test \ + -- ./SQLite/*.swift +ifdef GCOVR + $(GCOVR) \ + --root . \ + --use-gcov-files \ + --html \ + --html-details \ + --output coverage/index.html \ + --keep +else + @echo gcovr must be installed for HTML output: https://github.com/gcovr/gcovr +endif +else + @echo swiftcov must be installed for coverage: https://github.com/realm/SwiftCov + @exit 1 +endif + +clean: + $(BUILD_TOOL) $(BUILD_ARGUMENTS) clean + rm -r coverage + +repl: + @$(BUILD_TOOL) $(BUILD_ARGUMENTS) -derivedDataPath $(TMPDIR)/SQLite.swift > /dev/null && \ + swift -F '$(TMPDIR)/SQLite.swift/Build/Products/Debug' + +sloc: + @zsh -c "grep -vE '^ *//|^$$' SQLite/*/*.{swift,h,m} | wc -l" + +.PHONY: test coverage clean repl sloc diff --git a/mobile/ios/ThirdParty/SQLite.swift/Package.swift b/mobile/ios/ThirdParty/SQLite.swift/Package.swift new file mode 100644 index 0000000000..fc7c5a0ed9 --- /dev/null +++ b/mobile/ios/ThirdParty/SQLite.swift/Package.swift @@ -0,0 +1,17 @@ +import PackageDescription + +let package = Package( + name: "SQLite", + targets: [ + Target( + name: "SQLite", + dependencies: [ + .Target(name: "SQLiteObjc") + ]), + Target(name: "SQLiteObjc") + ], + dependencies: [ + .Package(url: "https://github.com/stephencelis/CSQLite.git", majorVersion: 0) + ], + exclude: ["Tests/CocoaPods", "Tests/Carthage"] +) diff --git a/mobile/ios/ThirdParty/SQLite.swift/README.md b/mobile/ios/ThirdParty/SQLite.swift/README.md new file mode 100644 index 0000000000..89917b36b3 --- /dev/null +++ b/mobile/ios/ThirdParty/SQLite.swift/README.md @@ -0,0 +1,263 @@ +# SQLite.swift + +[![Build Status][Badge]][Travis] [![CocoaPods Version](https://cocoapod-badges.herokuapp.com/v/SQLite.swift/badge.png)](http://cocoadocs.org/docsets/SQLite.swift) [![Swift](https://img.shields.io/badge/swift-3-orange.svg?style=flat)](https://developer.apple.com/swift/) [![Platform](https://cocoapod-badges.herokuapp.com/p/SQLite.swift/badge.png)](http://cocoadocs.org/docsets/SQLite.swift) [![Carthage compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat)](https://github.com/Carthage/Carthage) [![Join the chat at https://gitter.im/stephencelis/SQLite.swift](https://badges.gitter.im/stephencelis/SQLite.swift.svg)](https://gitter.im/stephencelis/SQLite.swift) + +A type-safe, [Swift][]-language layer over [SQLite3][]. + +[SQLite.swift][] provides compile-time confidence in SQL statement +syntax _and_ intent. + +[Badge]: https://img.shields.io/travis/stephencelis/SQLite.swift/master.svg?style=flat +[Travis]: https://travis-ci.org/stephencelis/SQLite.swift +[Swift]: https://developer.apple.com/swift/ +[SQLite3]: http://www.sqlite.org +[SQLite.swift]: https://github.com/stephencelis/SQLite.swift + + +## Features + + - A pure-Swift interface + - A type-safe, optional-aware SQL expression builder + - A flexible, chainable, lazy-executing query layer + - Automatically-typed data access + - A lightweight, uncomplicated query and parameter binding interface + - Developer-friendly error handling and debugging + - [Full-text search][] support + - [Well-documented][See Documentation] + - Extensively tested + - SQLCipher support via CocoaPods + - Active support at [StackOverflow](http://stackoverflow.com/questions/tagged/sqlite.swift), and [Gitter Chat Room](https://gitter.im/stephencelis/SQLite.swift) (_experimental_) + +[Full-text search]: Documentation/Index.md#full-text-search +[See Documentation]: Documentation/Index.md#sqliteswift-documentation + + +## Usage + +``` swift +import SQLite + +let db = try Connection("path/to/db.sqlite3") + +let users = Table("users") +let id = Expression("id") +let name = Expression("name") +let email = Expression("email") + +try db.run(users.create { t in + t.column(id, primaryKey: true) + t.column(name) + t.column(email, unique: true) +}) +// CREATE TABLE "users" ( +// "id" INTEGER PRIMARY KEY NOT NULL, +// "name" TEXT, +// "email" TEXT NOT NULL UNIQUE +// ) + +let insert = users.insert(name <- "Alice", email <- "alice@mac.com") +let rowid = try db.run(insert) +// INSERT INTO "users" ("name", "email") VALUES ('Alice', 'alice@mac.com') + +for user in try db.prepare(users) { + print("id: \(user[id]), name: \(user[name]), email: \(user[email])") + // id: 1, name: Optional("Alice"), email: alice@mac.com +} +// SELECT * FROM "users" + +let alice = users.filter(id == rowid) + +try db.run(alice.update(email <- email.replace("mac.com", with: "me.com"))) +// UPDATE "users" SET "email" = replace("email", 'mac.com', 'me.com') +// WHERE ("id" = 1) + +try db.run(alice.delete()) +// DELETE FROM "users" WHERE ("id" = 1) + +db.scalar(users.count) // 0 +// SELECT count(*) FROM "users" +``` + +SQLite.swift also works as a lightweight, Swift-friendly wrapper over the C +API. + +``` swift +let stmt = try db.prepare("INSERT INTO users (email) VALUES (?)") +for email in ["betty@icloud.com", "cathy@icloud.com"] { + try stmt.run(email) +} + +db.totalChanges // 3 +db.changes // 1 +db.lastInsertRowid // 3 + +for row in try db.prepare("SELECT id, email FROM users") { + print("id: \(row[0]), email: \(row[1])") + // id: Optional(2), email: Optional("betty@icloud.com") + // id: Optional(3), email: Optional("cathy@icloud.com") +} + +db.scalar("SELECT count(*) FROM users") // 2 +``` + +[Read the documentation][See Documentation] or explore more, +interactively, from the Xcode project’s playground. + +![SQLite.playground Screen Shot](Documentation/Resources/playground@2x.png) + +For a more comprehensive example, see [this article](http://masteringswift.blogspot.com/2015/09/create-data-access-layer-with.html) and the [companion repository](https://github.com/hoffmanjon/SQLiteDataAccessLayer2/tree/master). + +## Installation + +> _Note:_ SQLite.swift requires Swift 3 (and [Xcode][] 8) or greater. If you absolutely +> need compatibility with Swift 2.3 you can use the [swift-2.3][] branch or older +> released versions. New development will happen exclusively on the master/Swift 3 branch. + +### Carthage + +[Carthage][] is a simple, decentralized dependency manager for Cocoa. To +install SQLite.swift with Carthage: + + 1. Make sure Carthage is [installed][Carthage Installation]. + + 2. Update your Cartfile to include the following: + + ``` + github "stephencelis/SQLite.swift" ~> 0.11.2 + ``` + + 3. Run `carthage update` and [add the appropriate framework][Carthage Usage]. + + +[Carthage]: https://github.com/Carthage/Carthage +[Carthage Installation]: https://github.com/Carthage/Carthage#installing-carthage +[Carthage Usage]: https://github.com/Carthage/Carthage#adding-frameworks-to-an-application + + +### CocoaPods + +[CocoaPods][] is a dependency manager for Cocoa projects. To install +SQLite.swift with CocoaPods: + + 1. Verify that your copy of Xcode is installed and active in the default location (`/Applications/Xcode.app`). + + ```sh + sudo xcode-select --switch /Applications/Xcode.app + ``` + + 2. Make sure CocoaPods is [installed][CocoaPods Installation]. (SQLite.swift requires version 1.0.0 or greater.) + + ``` sh + # Using the default Ruby install will require you to use sudo when + # installing and updating gems. + [sudo] gem install cocoapods + ``` + + 3. Update your Podfile to include the following: + + ``` ruby + use_frameworks! + + target 'YourAppTargetName' do + pod 'SQLite.swift', '~> 0.11.2' + end + ``` + + 4. Run `pod install --repo-update`. + +[CocoaPods]: https://cocoapods.org +[CocoaPods Installation]: https://guides.cocoapods.org/using/getting-started.html#getting-started + +### Swift Package Manager + +The [Swift Package Manager][] is a tool for managing the distribution of Swift code. + +1. Add the following to your `Package.swift` file: + +```swift +dependencies: [ + .Package(url: "https://github.com/stephencelis/SQLite.swift.git", majorVersion: 0, minor: 11) +] +``` + +[Swift Package Manager]: https://swift.org/package-manager + +### Manual + +To install SQLite.swift as an Xcode sub-project: + + 1. Drag the **SQLite.xcodeproj** file into your own project. + ([Submodule][], clone, or [download][] the project first.) + + ![Installation Screen Shot](Documentation/Resources/installation@2x.png) + + 2. In your target’s **General** tab, click the **+** button under **Linked + Frameworks and Libraries**. + + 3. Select the appropriate **SQLite.framework** for your platform. + + 4. **Add**. + +Some additional steps are required to install the application on an actual device: + + 5. In the **General** tab, click the **+** button under **Embedded Binaries**. + + 6. Select the appropriate **SQLite.framework** for your platform. + + 7. **Add**. + + +[Xcode]: https://developer.apple.com/xcode/downloads/ +[Submodule]: http://git-scm.com/book/en/Git-Tools-Submodules +[download]: https://github.com/stephencelis/SQLite.swift/archive/master.zip + + +## Communication + +[See the planning document] for a roadmap and existing feature requests. + +[Read the contributing guidelines][]. The _TL;DR_ (but please; _R_): + + - Need **help** or have a **general question**? [Ask on Stack + Overflow][] (tag `sqlite.swift`). + - Found a **bug** or have a **feature request**? [Open an issue][]. + - Want to **contribute**? [Submit a pull request][]. + +[See the planning document]: /Documentation/Planning.md +[Read the contributing guidelines]: ./CONTRIBUTING.md#contributing +[Ask on Stack Overflow]: http://stackoverflow.com/questions/tagged/sqlite.swift +[Open an issue]: https://github.com/stephencelis/SQLite.swift/issues/new +[Submit a pull request]: https://github.com/stephencelis/SQLite.swift/fork + + +## Author + + - [Stephen Celis](mailto:stephen@stephencelis.com) + ([@stephencelis](https://twitter.com/stephencelis)) + + +## License + +SQLite.swift is available under the MIT license. See [the LICENSE +file](./LICENSE.txt) for more information. + +## Related + +These projects enhance or use SQLite.swift: + + - [SQLiteMigrationManager.swift](https://github.com/garriguv/SQLiteMigrationManager.swift) (inspired by [FMDBMigrationManager](https://github.com/layerhq/FMDBMigrationManager)) + + +## Alternatives + +Looking for something else? Try another Swift wrapper (or [FMDB][]): + + - [Camembert](https://github.com/remirobert/Camembert) + - [GRDB](https://github.com/groue/GRDB.swift) + - [SQLiteDB](https://github.com/FahimF/SQLiteDB) + - [Squeal](https://github.com/nerdyc/Squeal) + - [SwiftData](https://github.com/ryanfowler/SwiftData) + - [SwiftSQLite](https://github.com/chrismsimpson/SwiftSQLite) + +[FMDB]: https://github.com/ccgus/fmdb +[swift-2.3]: https://github.com/stephencelis/SQLite.swift/tree/swift-2.3 diff --git a/mobile/ios/ThirdParty/SQLite.swift/SQLite.playground/Contents.swift b/mobile/ios/ThirdParty/SQLite.swift/SQLite.playground/Contents.swift new file mode 100644 index 0000000000..11e13139f7 --- /dev/null +++ b/mobile/ios/ThirdParty/SQLite.swift/SQLite.playground/Contents.swift @@ -0,0 +1,43 @@ +import SQLite + +let db = try! Connection() + +db.trace { print($0) } + +let users = Table("users") + +let id = Expression("id") +let email = Expression("email") +let name = Expression("name") + +try! db.run(users.create { t in + t.column(id, primaryKey: true) + t.column(email, unique: true, check: email.like("%@%")) + t.column(name) +}) + +let rowid = try! db.run(users.insert(email <- "alice@mac.com")) +let alice = users.filter(id == rowid) + +for user in try! db.prepare(users) { + print("id: \(user[id]), email: \(user[email])") +} + +let emails = VirtualTable("emails") + +let subject = Expression("subject") +let body = Expression("body") + +try! db.run(emails.create(.FTS4(subject, body))) + +try! db.run(emails.insert( + subject <- "Hello, world!", + body <- "This is a hello world message." +)) + +let row = try! db.pluck(emails.match("hello")) + +let query = try! db.prepare(emails.match("hello")) +for row in query { + print(row[subject]) +} diff --git a/mobile/ios/ThirdParty/SQLite.swift/SQLite.playground/contents.xcplayground b/mobile/ios/ThirdParty/SQLite.swift/SQLite.playground/contents.xcplayground new file mode 100644 index 0000000000..fd676d5b46 --- /dev/null +++ b/mobile/ios/ThirdParty/SQLite.swift/SQLite.playground/contents.xcplayground @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/mobile/ios/ThirdParty/SQLite.swift/SQLite.swift.podspec b/mobile/ios/ThirdParty/SQLite.swift/SQLite.swift.podspec new file mode 100644 index 0000000000..3c61aa1fe6 --- /dev/null +++ b/mobile/ios/ThirdParty/SQLite.swift/SQLite.swift.podspec @@ -0,0 +1,70 @@ +Pod::Spec.new do |s| + s.name = "SQLite.swift" + s.version = "0.11.2" + s.summary = "A type-safe, Swift-language layer over SQLite3 for iOS and OS X." + + s.description = <<-DESC + SQLite.swift provides compile-time confidence in SQL statement syntax and + intent. + DESC + + s.homepage = "https://github.com/stephencelis/SQLite.swift" + s.license = 'MIT' + s.author = { "Stephen Celis" => "stephen@stephencelis.com" } + s.source = { :git => "https://github.com/stephencelis/SQLite.swift.git", :tag => s.version.to_s } + s.social_media_url = 'https://twitter.com/stephencelis' + + s.module_name = 'SQLite' + s.ios.deployment_target = "8.0" + s.tvos.deployment_target = "9.0" + s.osx.deployment_target = "10.9" + s.watchos.deployment_target = "2.0" + s.default_subspec = 'standard' + s.pod_target_xcconfig = { + 'SWIFT_VERSION' => '3.0', + } + + s.subspec 'standard' do |ss| + ss.source_files = 'Sources/{SQLite,SQLiteObjc}/**/*.{c,h,m,swift}' + ss.exclude_files = 'Sources/**/Cipher.swift' + ss.private_header_files = 'Sources/SQLiteObjc/*.h' + + ss.library = 'sqlite3' + ss.preserve_paths = 'CocoaPods/**/*' + ss.pod_target_xcconfig = { + 'SWIFT_INCLUDE_PATHS[sdk=macosx*]' => '$(SRCROOT)/SQLite.swift/CocoaPods/macosx', + 'SWIFT_INCLUDE_PATHS[sdk=macosx10.11]' => '$(SRCROOT)/SQLite.swift/CocoaPods/macosx-10.11', + 'SWIFT_INCLUDE_PATHS[sdk=macosx10.12]' => '$(SRCROOT)/SQLite.swift/CocoaPods/macosx-10.12', + 'SWIFT_INCLUDE_PATHS[sdk=iphoneos*]' => '$(SRCROOT)/SQLite.swift/CocoaPods/iphoneos', + 'SWIFT_INCLUDE_PATHS[sdk=iphoneos10.0]' => '$(SRCROOT)/SQLite.swift/CocoaPods/iphoneos-10.0', + 'SWIFT_INCLUDE_PATHS[sdk=iphonesimulator*]' => '$(SRCROOT)/SQLite.swift/CocoaPods/iphonesimulator', + 'SWIFT_INCLUDE_PATHS[sdk=iphonesimulator10.0]' => '$(SRCROOT)/SQLite.swift/CocoaPods/iphonesimulator-10.0', + 'SWIFT_INCLUDE_PATHS[sdk=appletvos*]' => '$(SRCROOT)/SQLite.swift/CocoaPods/appletvos', + 'SWIFT_INCLUDE_PATHS[sdk=appletvsimulator*]' => '$(SRCROOT)/SQLite.swift/CocoaPods/appletvsimulator', + 'SWIFT_INCLUDE_PATHS[sdk=watchos*]' => '$(SRCROOT)/SQLite.swift/CocoaPods/watchos', + 'SWIFT_INCLUDE_PATHS[sdk=watchsimulator*]' => '$(SRCROOT)/SQLite.swift/CocoaPods/watchsimulator' + } + end + + s.subspec 'standalone' do |ss| + ss.source_files = 'Sources/{SQLite,SQLiteObjc}/**/*.{c,h,m,swift}' + ss.exclude_files = 'Sources/**/Cipher.swift' + ss.private_header_files = 'Sources/SQLiteObjc/*.h' + ss.xcconfig = { + 'OTHER_SWIFT_FLAGS' => '$(inherited) -DSQLITE_SWIFT_STANDALONE' + } + + ss.dependency 'sqlite3', '>= 3.14.0' + end + + s.subspec 'SQLCipher' do |ss| + ss.source_files = 'Sources/{SQLite,SQLiteObjc}/**/*.{c,h,m,swift}' + ss.private_header_files = 'Sources/SQLiteObjc/*.h' + ss.xcconfig = { + 'OTHER_SWIFT_FLAGS' => '$(inherited) -DSQLITE_SWIFT_SQLCIPHER', + 'GCC_PREPROCESSOR_DEFINITIONS' => '$(inherited) SQLITE_HAS_CODEC=1' + } + + ss.dependency 'SQLCipher', '>= 3.4.0' + end +end diff --git a/mobile/ios/ThirdParty/SQLite.swift/SQLite.xcodeproj/project.pbxproj b/mobile/ios/ThirdParty/SQLite.swift/SQLite.xcodeproj/project.pbxproj new file mode 100644 index 0000000000..bd6421b88a --- /dev/null +++ b/mobile/ios/ThirdParty/SQLite.swift/SQLite.xcodeproj/project.pbxproj @@ -0,0 +1,1513 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + 03A65E641C6BB0F60062603F /* SQLite.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 03A65E5A1C6BB0F50062603F /* SQLite.framework */; }; + 03A65E711C6BB2CD0062603F /* usr/include/sqlite3.h in Headers */ = {isa = PBXBuildFile; fileRef = EE91808B1C46E34A0038162A /* usr/include/sqlite3.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 03A65E721C6BB2D30062603F /* SQLite.h in Headers */ = {isa = PBXBuildFile; fileRef = EE247AD61C3F04ED00AE3E12 /* SQLite.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 03A65E731C6BB2D80062603F /* Foundation.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247AF71C3F06E900AE3E12 /* Foundation.swift */; }; + 03A65E741C6BB2DA0062603F /* Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247AF81C3F06E900AE3E12 /* Helpers.swift */; }; + 03A65E751C6BB2DF0062603F /* SQLite-Bridging.h in Headers */ = {isa = PBXBuildFile; fileRef = EE91808D1C46E5230038162A /* SQLite-Bridging.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 03A65E761C6BB2E60062603F /* Blob.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247AEE1C3F06E900AE3E12 /* Blob.swift */; }; + 03A65E771C6BB2E60062603F /* Connection.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247AEF1C3F06E900AE3E12 /* Connection.swift */; }; + 03A65E781C6BB2EA0062603F /* fts3_tokenizer.h in Headers */ = {isa = PBXBuildFile; fileRef = EE247AF01C3F06E900AE3E12 /* fts3_tokenizer.h */; }; + 03A65E791C6BB2EF0062603F /* SQLite-Bridging.m in Sources */ = {isa = PBXBuildFile; fileRef = EE247AF11C3F06E900AE3E12 /* SQLite-Bridging.m */; }; + 03A65E7A1C6BB2F70062603F /* Statement.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247AF21C3F06E900AE3E12 /* Statement.swift */; }; + 03A65E7B1C6BB2F70062603F /* Value.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247AF31C3F06E900AE3E12 /* Value.swift */; }; + 03A65E7C1C6BB2F70062603F /* FTS4.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247AF51C3F06E900AE3E12 /* FTS4.swift */; }; + 03A65E7D1C6BB2F70062603F /* RTree.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247AF61C3F06E900AE3E12 /* RTree.swift */; }; + 03A65E7E1C6BB2FB0062603F /* AggregateFunctions.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247AFA1C3F06E900AE3E12 /* AggregateFunctions.swift */; }; + 03A65E7F1C6BB2FB0062603F /* Collation.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247AFB1C3F06E900AE3E12 /* Collation.swift */; }; + 03A65E801C6BB2FB0062603F /* CoreFunctions.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247AFC1C3F06E900AE3E12 /* CoreFunctions.swift */; }; + 03A65E811C6BB2FB0062603F /* CustomFunctions.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247AFD1C3F06E900AE3E12 /* CustomFunctions.swift */; }; + 03A65E821C6BB2FB0062603F /* Expression.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247AFE1C3F06E900AE3E12 /* Expression.swift */; }; + 03A65E831C6BB2FB0062603F /* Operators.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247AFF1C3F06E900AE3E12 /* Operators.swift */; }; + 03A65E841C6BB2FB0062603F /* Query.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247B001C3F06E900AE3E12 /* Query.swift */; }; + 03A65E851C6BB2FB0062603F /* Schema.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247B011C3F06E900AE3E12 /* Schema.swift */; }; + 03A65E861C6BB2FB0062603F /* Setter.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247B021C3F06E900AE3E12 /* Setter.swift */; }; + 03A65E871C6BB3030062603F /* AggregateFunctionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247B1A1C3F137700AE3E12 /* AggregateFunctionsTests.swift */; }; + 03A65E881C6BB3030062603F /* BlobTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247B1B1C3F137700AE3E12 /* BlobTests.swift */; }; + 03A65E891C6BB3030062603F /* ConnectionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247B1D1C3F137700AE3E12 /* ConnectionTests.swift */; }; + 03A65E8A1C6BB3030062603F /* CoreFunctionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247B1E1C3F137700AE3E12 /* CoreFunctionsTests.swift */; }; + 03A65E8B1C6BB3030062603F /* CustomFunctionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247B1F1C3F137700AE3E12 /* CustomFunctionsTests.swift */; }; + 03A65E8C1C6BB3030062603F /* ExpressionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247B201C3F137700AE3E12 /* ExpressionTests.swift */; }; + 03A65E8D1C6BB3030062603F /* FTS4Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247B211C3F137700AE3E12 /* FTS4Tests.swift */; }; + 03A65E8E1C6BB3030062603F /* OperatorsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247B2A1C3F141E00AE3E12 /* OperatorsTests.swift */; }; + 03A65E8F1C6BB3030062603F /* QueryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247B2B1C3F141E00AE3E12 /* QueryTests.swift */; }; + 03A65E901C6BB3030062603F /* RTreeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247B2C1C3F141E00AE3E12 /* RTreeTests.swift */; }; + 03A65E911C6BB3030062603F /* SchemaTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247B2D1C3F141E00AE3E12 /* SchemaTests.swift */; }; + 03A65E921C6BB3030062603F /* SetterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247B181C3F134A00AE3E12 /* SetterTests.swift */; }; + 03A65E931C6BB3030062603F /* StatementTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247B321C3F142E00AE3E12 /* StatementTests.swift */; }; + 03A65E941C6BB3030062603F /* ValueTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247B331C3F142E00AE3E12 /* ValueTests.swift */; }; + 03A65E951C6BB3030062603F /* TestHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247B161C3F127200AE3E12 /* TestHelpers.swift */; }; + 03A65E971C6BB3210062603F /* libsqlite3.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 03A65E961C6BB3210062603F /* libsqlite3.tbd */; }; + 19A1709C3E7A406E62293B2A /* Fixtures.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A17B93B48B5560E6E51791 /* Fixtures.swift */; }; + 19A1717B10CC941ACB5533D6 /* FTS5.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A1730E4390C775C25677D1 /* FTS5.swift */; }; + 19A171E6FA242F72A308C594 /* FTS5Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A1721B8984686B9963B45D /* FTS5Tests.swift */; }; + 19A171F12AB8B07F2FD7201A /* Cipher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A178A39ACA9667A62663CC /* Cipher.swift */; }; + 19A17254FBA7894891F7297B /* FTS5Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A1721B8984686B9963B45D /* FTS5Tests.swift */; }; + 19A17408007B182F884E3A53 /* Fixtures.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A17B93B48B5560E6E51791 /* Fixtures.swift */; }; + 19A174D78559CD30679BCCCB /* FTS5Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A1721B8984686B9963B45D /* FTS5Tests.swift */; }; + 19A1750CEE9B05267995CF3D /* FTS5.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A1730E4390C775C25677D1 /* FTS5.swift */; }; + 19A175DFF47B84757E547C62 /* fixtures in Resources */ = {isa = PBXBuildFile; fileRef = 19A17E2695737FAB5D6086E3 /* fixtures */; }; + 19A177CC33F2E6A24AF90B02 /* CipherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A17399EA9E61235D5D77BF /* CipherTests.swift */; }; + 19A178072B371489E6A1E839 /* FoundationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A1794CC4D7827E997E32A7 /* FoundationTests.swift */; }; + 19A17835FD5886FDC5A3228F /* Cipher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A178A39ACA9667A62663CC /* Cipher.swift */; }; + 19A179A0C45377CB09BB358C /* CipherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A17399EA9E61235D5D77BF /* CipherTests.swift */; }; + 19A179CCF9671E345E5A9811 /* Cipher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A178A39ACA9667A62663CC /* Cipher.swift */; }; + 19A179E76EA6207669B60C1B /* Cipher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A178A39ACA9667A62663CC /* Cipher.swift */; }; + 19A17C4B951CB054EE48AB1C /* CipherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A17399EA9E61235D5D77BF /* CipherTests.swift */; }; + 19A17E04C4C0956715C5676A /* FoundationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A1794CC4D7827E997E32A7 /* FoundationTests.swift */; }; + 19A17EC0D68BA8C03288ADF7 /* FTS5.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A1730E4390C775C25677D1 /* FTS5.swift */; }; + 19A17F3E1F7ACA33BD43E138 /* fixtures in Resources */ = {isa = PBXBuildFile; fileRef = 19A17E2695737FAB5D6086E3 /* fixtures */; }; + 19A17F60B685636D1F83C2DD /* Fixtures.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A17B93B48B5560E6E51791 /* Fixtures.swift */; }; + 19A17FB80B94E882050AA908 /* FoundationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A1794CC4D7827E997E32A7 /* FoundationTests.swift */; }; + 19A17FDA323BAFDEC627E76F /* fixtures in Resources */ = {isa = PBXBuildFile; fileRef = 19A17E2695737FAB5D6086E3 /* fixtures */; }; + 3D67B3E61DB2469200A4F4C6 /* libsqlite3.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 3D67B3E51DB2469200A4F4C6 /* libsqlite3.tbd */; }; + 3D67B3E71DB246BA00A4F4C6 /* Blob.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247AEE1C3F06E900AE3E12 /* Blob.swift */; }; + 3D67B3E81DB246BA00A4F4C6 /* Connection.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247AEF1C3F06E900AE3E12 /* Connection.swift */; }; + 3D67B3E91DB246D100A4F4C6 /* Statement.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247AF21C3F06E900AE3E12 /* Statement.swift */; }; + 3D67B3EA1DB246D100A4F4C6 /* Value.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247AF31C3F06E900AE3E12 /* Value.swift */; }; + 3D67B3EB1DB246D100A4F4C6 /* FTS4.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247AF51C3F06E900AE3E12 /* FTS4.swift */; }; + 3D67B3EC1DB246D100A4F4C6 /* RTree.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247AF61C3F06E900AE3E12 /* RTree.swift */; }; + 3D67B3ED1DB246D100A4F4C6 /* FTS5.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A1730E4390C775C25677D1 /* FTS5.swift */; }; + 3D67B3EE1DB246D100A4F4C6 /* AggregateFunctions.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247AFA1C3F06E900AE3E12 /* AggregateFunctions.swift */; }; + 3D67B3EF1DB246D100A4F4C6 /* Collation.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247AFB1C3F06E900AE3E12 /* Collation.swift */; }; + 3D67B3F01DB246D100A4F4C6 /* CoreFunctions.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247AFC1C3F06E900AE3E12 /* CoreFunctions.swift */; }; + 3D67B3F11DB246D100A4F4C6 /* CustomFunctions.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247AFD1C3F06E900AE3E12 /* CustomFunctions.swift */; }; + 3D67B3F21DB246D100A4F4C6 /* Expression.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247AFE1C3F06E900AE3E12 /* Expression.swift */; }; + 3D67B3F31DB246D100A4F4C6 /* Operators.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247AFF1C3F06E900AE3E12 /* Operators.swift */; }; + 3D67B3F41DB246D100A4F4C6 /* Query.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247B001C3F06E900AE3E12 /* Query.swift */; }; + 3D67B3F51DB246D100A4F4C6 /* Schema.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247B011C3F06E900AE3E12 /* Schema.swift */; }; + 3D67B3F61DB246D100A4F4C6 /* Setter.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247B021C3F06E900AE3E12 /* Setter.swift */; }; + 3D67B3F71DB246D700A4F4C6 /* Foundation.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247AF71C3F06E900AE3E12 /* Foundation.swift */; }; + 3D67B3F81DB246D700A4F4C6 /* Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247AF81C3F06E900AE3E12 /* Helpers.swift */; }; + 3D67B3F91DB246E700A4F4C6 /* SQLite-Bridging.m in Sources */ = {isa = PBXBuildFile; fileRef = EE247AF11C3F06E900AE3E12 /* SQLite-Bridging.m */; }; + 3D67B3FA1DB2470600A4F4C6 /* usr/include/sqlite3.h in Headers */ = {isa = PBXBuildFile; fileRef = EE91808B1C46E34A0038162A /* usr/include/sqlite3.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 3D67B3FB1DB2470600A4F4C6 /* SQLite-Bridging.h in Headers */ = {isa = PBXBuildFile; fileRef = EE91808D1C46E5230038162A /* SQLite-Bridging.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 3D67B3FC1DB2471B00A4F4C6 /* SQLite.h in Headers */ = {isa = PBXBuildFile; fileRef = EE247AD61C3F04ED00AE3E12 /* SQLite.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 3D67B3FD1DB2472D00A4F4C6 /* fts3_tokenizer.h in Headers */ = {isa = PBXBuildFile; fileRef = EE247AF01C3F06E900AE3E12 /* fts3_tokenizer.h */; }; + EE247AD71C3F04ED00AE3E12 /* SQLite.h in Headers */ = {isa = PBXBuildFile; fileRef = EE247AD61C3F04ED00AE3E12 /* SQLite.h */; settings = {ATTRIBUTES = (Public, ); }; }; + EE247ADE1C3F04ED00AE3E12 /* SQLite.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = EE247AD31C3F04ED00AE3E12 /* SQLite.framework */; }; + EE247B031C3F06E900AE3E12 /* Blob.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247AEE1C3F06E900AE3E12 /* Blob.swift */; }; + EE247B041C3F06E900AE3E12 /* Connection.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247AEF1C3F06E900AE3E12 /* Connection.swift */; }; + EE247B051C3F06E900AE3E12 /* fts3_tokenizer.h in Headers */ = {isa = PBXBuildFile; fileRef = EE247AF01C3F06E900AE3E12 /* fts3_tokenizer.h */; }; + EE247B061C3F06E900AE3E12 /* SQLite-Bridging.m in Sources */ = {isa = PBXBuildFile; fileRef = EE247AF11C3F06E900AE3E12 /* SQLite-Bridging.m */; }; + EE247B071C3F06E900AE3E12 /* Statement.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247AF21C3F06E900AE3E12 /* Statement.swift */; }; + EE247B081C3F06E900AE3E12 /* Value.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247AF31C3F06E900AE3E12 /* Value.swift */; }; + EE247B091C3F06E900AE3E12 /* FTS4.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247AF51C3F06E900AE3E12 /* FTS4.swift */; }; + EE247B0A1C3F06E900AE3E12 /* RTree.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247AF61C3F06E900AE3E12 /* RTree.swift */; }; + EE247B0B1C3F06E900AE3E12 /* Foundation.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247AF71C3F06E900AE3E12 /* Foundation.swift */; }; + EE247B0C1C3F06E900AE3E12 /* Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247AF81C3F06E900AE3E12 /* Helpers.swift */; }; + EE247B0D1C3F06E900AE3E12 /* AggregateFunctions.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247AFA1C3F06E900AE3E12 /* AggregateFunctions.swift */; }; + EE247B0E1C3F06E900AE3E12 /* Collation.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247AFB1C3F06E900AE3E12 /* Collation.swift */; }; + EE247B0F1C3F06E900AE3E12 /* CoreFunctions.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247AFC1C3F06E900AE3E12 /* CoreFunctions.swift */; }; + EE247B101C3F06E900AE3E12 /* CustomFunctions.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247AFD1C3F06E900AE3E12 /* CustomFunctions.swift */; }; + EE247B111C3F06E900AE3E12 /* Expression.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247AFE1C3F06E900AE3E12 /* Expression.swift */; }; + EE247B121C3F06E900AE3E12 /* Operators.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247AFF1C3F06E900AE3E12 /* Operators.swift */; }; + EE247B131C3F06E900AE3E12 /* Query.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247B001C3F06E900AE3E12 /* Query.swift */; }; + EE247B141C3F06E900AE3E12 /* Schema.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247B011C3F06E900AE3E12 /* Schema.swift */; }; + EE247B151C3F06E900AE3E12 /* Setter.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247B021C3F06E900AE3E12 /* Setter.swift */; }; + EE247B171C3F127200AE3E12 /* TestHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247B161C3F127200AE3E12 /* TestHelpers.swift */; }; + EE247B191C3F134A00AE3E12 /* SetterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247B181C3F134A00AE3E12 /* SetterTests.swift */; }; + EE247B221C3F137700AE3E12 /* AggregateFunctionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247B1A1C3F137700AE3E12 /* AggregateFunctionsTests.swift */; }; + EE247B231C3F137700AE3E12 /* BlobTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247B1B1C3F137700AE3E12 /* BlobTests.swift */; }; + EE247B251C3F137700AE3E12 /* ConnectionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247B1D1C3F137700AE3E12 /* ConnectionTests.swift */; }; + EE247B261C3F137700AE3E12 /* CoreFunctionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247B1E1C3F137700AE3E12 /* CoreFunctionsTests.swift */; }; + EE247B271C3F137700AE3E12 /* CustomFunctionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247B1F1C3F137700AE3E12 /* CustomFunctionsTests.swift */; }; + EE247B281C3F137700AE3E12 /* ExpressionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247B201C3F137700AE3E12 /* ExpressionTests.swift */; }; + EE247B291C3F137700AE3E12 /* FTS4Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247B211C3F137700AE3E12 /* FTS4Tests.swift */; }; + EE247B2E1C3F141E00AE3E12 /* OperatorsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247B2A1C3F141E00AE3E12 /* OperatorsTests.swift */; }; + EE247B2F1C3F141E00AE3E12 /* QueryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247B2B1C3F141E00AE3E12 /* QueryTests.swift */; }; + EE247B301C3F141E00AE3E12 /* RTreeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247B2C1C3F141E00AE3E12 /* RTreeTests.swift */; }; + EE247B311C3F141E00AE3E12 /* SchemaTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247B2D1C3F141E00AE3E12 /* SchemaTests.swift */; }; + EE247B341C3F142E00AE3E12 /* StatementTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247B321C3F142E00AE3E12 /* StatementTests.swift */; }; + EE247B351C3F142E00AE3E12 /* ValueTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247B331C3F142E00AE3E12 /* ValueTests.swift */; }; + EE247B461C3F3ED000AE3E12 /* SQLite.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = EE247B3C1C3F3ED000AE3E12 /* SQLite.framework */; }; + EE247B531C3F3FC700AE3E12 /* AggregateFunctionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247B1A1C3F137700AE3E12 /* AggregateFunctionsTests.swift */; }; + EE247B541C3F3FC700AE3E12 /* BlobTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247B1B1C3F137700AE3E12 /* BlobTests.swift */; }; + EE247B551C3F3FC700AE3E12 /* ConnectionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247B1D1C3F137700AE3E12 /* ConnectionTests.swift */; }; + EE247B561C3F3FC700AE3E12 /* CoreFunctionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247B1E1C3F137700AE3E12 /* CoreFunctionsTests.swift */; }; + EE247B571C3F3FC700AE3E12 /* CustomFunctionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247B1F1C3F137700AE3E12 /* CustomFunctionsTests.swift */; }; + EE247B581C3F3FC700AE3E12 /* ExpressionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247B201C3F137700AE3E12 /* ExpressionTests.swift */; }; + EE247B591C3F3FC700AE3E12 /* FTS4Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247B211C3F137700AE3E12 /* FTS4Tests.swift */; }; + EE247B5A1C3F3FC700AE3E12 /* OperatorsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247B2A1C3F141E00AE3E12 /* OperatorsTests.swift */; }; + EE247B5B1C3F3FC700AE3E12 /* QueryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247B2B1C3F141E00AE3E12 /* QueryTests.swift */; }; + EE247B5C1C3F3FC700AE3E12 /* RTreeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247B2C1C3F141E00AE3E12 /* RTreeTests.swift */; }; + EE247B5D1C3F3FC700AE3E12 /* SchemaTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247B2D1C3F141E00AE3E12 /* SchemaTests.swift */; }; + EE247B5E1C3F3FC700AE3E12 /* SetterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247B181C3F134A00AE3E12 /* SetterTests.swift */; }; + EE247B5F1C3F3FC700AE3E12 /* StatementTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247B321C3F142E00AE3E12 /* StatementTests.swift */; }; + EE247B601C3F3FC700AE3E12 /* ValueTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247B331C3F142E00AE3E12 /* ValueTests.swift */; }; + EE247B611C3F3FC700AE3E12 /* TestHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247B161C3F127200AE3E12 /* TestHelpers.swift */; }; + EE247B621C3F3FDB00AE3E12 /* SQLite.h in Headers */ = {isa = PBXBuildFile; fileRef = EE247AD61C3F04ED00AE3E12 /* SQLite.h */; settings = {ATTRIBUTES = (Public, ); }; }; + EE247B631C3F3FDB00AE3E12 /* Foundation.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247AF71C3F06E900AE3E12 /* Foundation.swift */; }; + EE247B641C3F3FDB00AE3E12 /* Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247AF81C3F06E900AE3E12 /* Helpers.swift */; }; + EE247B651C3F3FEC00AE3E12 /* Blob.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247AEE1C3F06E900AE3E12 /* Blob.swift */; }; + EE247B661C3F3FEC00AE3E12 /* Connection.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247AEF1C3F06E900AE3E12 /* Connection.swift */; }; + EE247B671C3F3FEC00AE3E12 /* fts3_tokenizer.h in Headers */ = {isa = PBXBuildFile; fileRef = EE247AF01C3F06E900AE3E12 /* fts3_tokenizer.h */; }; + EE247B681C3F3FEC00AE3E12 /* SQLite-Bridging.m in Sources */ = {isa = PBXBuildFile; fileRef = EE247AF11C3F06E900AE3E12 /* SQLite-Bridging.m */; }; + EE247B691C3F3FEC00AE3E12 /* Statement.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247AF21C3F06E900AE3E12 /* Statement.swift */; }; + EE247B6A1C3F3FEC00AE3E12 /* Value.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247AF31C3F06E900AE3E12 /* Value.swift */; }; + EE247B6B1C3F3FEC00AE3E12 /* FTS4.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247AF51C3F06E900AE3E12 /* FTS4.swift */; }; + EE247B6C1C3F3FEC00AE3E12 /* RTree.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247AF61C3F06E900AE3E12 /* RTree.swift */; }; + EE247B6D1C3F3FEC00AE3E12 /* AggregateFunctions.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247AFA1C3F06E900AE3E12 /* AggregateFunctions.swift */; }; + EE247B6E1C3F3FEC00AE3E12 /* Collation.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247AFB1C3F06E900AE3E12 /* Collation.swift */; }; + EE247B6F1C3F3FEC00AE3E12 /* CoreFunctions.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247AFC1C3F06E900AE3E12 /* CoreFunctions.swift */; }; + EE247B701C3F3FEC00AE3E12 /* CustomFunctions.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247AFD1C3F06E900AE3E12 /* CustomFunctions.swift */; }; + EE247B711C3F3FEC00AE3E12 /* Expression.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247AFE1C3F06E900AE3E12 /* Expression.swift */; }; + EE247B721C3F3FEC00AE3E12 /* Operators.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247AFF1C3F06E900AE3E12 /* Operators.swift */; }; + EE247B731C3F3FEC00AE3E12 /* Query.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247B001C3F06E900AE3E12 /* Query.swift */; }; + EE247B741C3F3FEC00AE3E12 /* Schema.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247B011C3F06E900AE3E12 /* Schema.swift */; }; + EE247B751C3F3FEC00AE3E12 /* Setter.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247B021C3F06E900AE3E12 /* Setter.swift */; }; + EE91808C1C46E34A0038162A /* usr/include/sqlite3.h in Headers */ = {isa = PBXBuildFile; fileRef = EE91808B1C46E34A0038162A /* usr/include/sqlite3.h */; settings = {ATTRIBUTES = (Public, ); }; }; + EE91808E1C46E5230038162A /* SQLite-Bridging.h in Headers */ = {isa = PBXBuildFile; fileRef = EE91808D1C46E5230038162A /* SQLite-Bridging.h */; settings = {ATTRIBUTES = (Public, ); }; }; + EE91808F1C46E76D0038162A /* SQLite-Bridging.h in Headers */ = {isa = PBXBuildFile; fileRef = EE91808D1C46E5230038162A /* SQLite-Bridging.h */; settings = {ATTRIBUTES = (Public, ); }; }; + EE9180901C46E8980038162A /* usr/include/sqlite3.h in Headers */ = {isa = PBXBuildFile; fileRef = EE91808B1C46E34A0038162A /* usr/include/sqlite3.h */; settings = {ATTRIBUTES = (Public, ); }; }; + EE9180941C46EA210038162A /* libsqlite3.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = EE9180931C46EA210038162A /* libsqlite3.tbd */; }; + EE9180951C46EBCC0038162A /* libsqlite3.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = EE9180911C46E9D30038162A /* libsqlite3.tbd */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 03A65E651C6BB0F60062603F /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = EE247ACA1C3F04ED00AE3E12 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 03A65E591C6BB0F50062603F; + remoteInfo = "SQLite tvOS"; + }; + EE247ADF1C3F04ED00AE3E12 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = EE247ACA1C3F04ED00AE3E12 /* Project object */; + proxyType = 1; + remoteGlobalIDString = EE247AD21C3F04ED00AE3E12; + remoteInfo = SQLite; + }; + EE247B471C3F3ED000AE3E12 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = EE247ACA1C3F04ED00AE3E12 /* Project object */; + proxyType = 1; + remoteGlobalIDString = EE247B3B1C3F3ED000AE3E12; + remoteInfo = SQLite; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXFileReference section */ + 03A65E5A1C6BB0F50062603F /* SQLite.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = SQLite.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 03A65E631C6BB0F60062603F /* SQLiteTests tvOS.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "SQLiteTests tvOS.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; + 03A65E961C6BB3210062603F /* libsqlite3.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libsqlite3.tbd; path = Platforms/AppleTVOS.platform/Developer/SDKs/AppleTVOS.sdk/usr/lib/libsqlite3.tbd; sourceTree = DEVELOPER_DIR; }; + 19A1721B8984686B9963B45D /* FTS5Tests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FTS5Tests.swift; sourceTree = ""; }; + 19A1730E4390C775C25677D1 /* FTS5.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FTS5.swift; sourceTree = ""; }; + 19A17399EA9E61235D5D77BF /* CipherTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CipherTests.swift; sourceTree = ""; }; + 19A178A39ACA9667A62663CC /* Cipher.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Cipher.swift; sourceTree = ""; }; + 19A1794CC4D7827E997E32A7 /* FoundationTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FoundationTests.swift; sourceTree = ""; }; + 19A17B93B48B5560E6E51791 /* Fixtures.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Fixtures.swift; sourceTree = ""; }; + 19A17E2695737FAB5D6086E3 /* fixtures */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = folder; path = fixtures; sourceTree = ""; }; + 39548A631CA63C740003E3B5 /* module.modulemap */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.module-map"; path = module.modulemap; sourceTree = ""; }; + 39548A651CA63C740003E3B5 /* module.modulemap */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.module-map"; path = module.modulemap; sourceTree = ""; }; + 39548A671CA63C740003E3B5 /* module.modulemap */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.module-map"; path = module.modulemap; sourceTree = ""; }; + 39548A691CA63C740003E3B5 /* module.modulemap */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.module-map"; path = module.modulemap; sourceTree = ""; }; + 39548A6B1CA63C740003E3B5 /* module.modulemap */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.module-map"; path = module.modulemap; sourceTree = ""; }; + 39548A6D1CA63C740003E3B5 /* module.modulemap */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.module-map"; path = module.modulemap; sourceTree = ""; }; + 39548A6F1CA63C740003E3B5 /* module.modulemap */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.module-map"; path = module.modulemap; sourceTree = ""; }; + 3D67B3E51DB2469200A4F4C6 /* libsqlite3.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libsqlite3.tbd; path = Platforms/WatchOS.platform/Developer/SDKs/WatchOS3.0.sdk/usr/lib/libsqlite3.tbd; sourceTree = DEVELOPER_DIR; }; + A121AC451CA35C79005A31D1 /* SQLite.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = SQLite.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + EE247AD31C3F04ED00AE3E12 /* SQLite.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = SQLite.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + EE247AD61C3F04ED00AE3E12 /* SQLite.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SQLite.h; sourceTree = ""; }; + EE247AD81C3F04ED00AE3E12 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + EE247ADD1C3F04ED00AE3E12 /* SQLiteTests iOS.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "SQLiteTests iOS.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; + EE247AE41C3F04ED00AE3E12 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + EE247AEE1C3F06E900AE3E12 /* Blob.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Blob.swift; sourceTree = ""; }; + EE247AEF1C3F06E900AE3E12 /* Connection.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Connection.swift; sourceTree = ""; }; + EE247AF01C3F06E900AE3E12 /* fts3_tokenizer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = fts3_tokenizer.h; path = ../../SQLiteObjc/fts3_tokenizer.h; sourceTree = ""; }; + EE247AF11C3F06E900AE3E12 /* SQLite-Bridging.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = "SQLite-Bridging.m"; path = "../../SQLiteObjc/SQLite-Bridging.m"; sourceTree = ""; }; + EE247AF21C3F06E900AE3E12 /* Statement.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Statement.swift; sourceTree = ""; }; + EE247AF31C3F06E900AE3E12 /* Value.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Value.swift; sourceTree = ""; }; + EE247AF51C3F06E900AE3E12 /* FTS4.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FTS4.swift; sourceTree = ""; }; + EE247AF61C3F06E900AE3E12 /* RTree.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RTree.swift; sourceTree = ""; }; + EE247AF71C3F06E900AE3E12 /* Foundation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Foundation.swift; sourceTree = ""; }; + EE247AF81C3F06E900AE3E12 /* Helpers.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Helpers.swift; sourceTree = ""; }; + EE247AFA1C3F06E900AE3E12 /* AggregateFunctions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AggregateFunctions.swift; sourceTree = ""; }; + EE247AFB1C3F06E900AE3E12 /* Collation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Collation.swift; sourceTree = ""; }; + EE247AFC1C3F06E900AE3E12 /* CoreFunctions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CoreFunctions.swift; sourceTree = ""; }; + EE247AFD1C3F06E900AE3E12 /* CustomFunctions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomFunctions.swift; sourceTree = ""; }; + EE247AFE1C3F06E900AE3E12 /* Expression.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Expression.swift; sourceTree = ""; }; + EE247AFF1C3F06E900AE3E12 /* Operators.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Operators.swift; sourceTree = ""; }; + EE247B001C3F06E900AE3E12 /* Query.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Query.swift; sourceTree = ""; }; + EE247B011C3F06E900AE3E12 /* Schema.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Schema.swift; sourceTree = ""; }; + EE247B021C3F06E900AE3E12 /* Setter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Setter.swift; sourceTree = ""; }; + EE247B161C3F127200AE3E12 /* TestHelpers.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestHelpers.swift; sourceTree = ""; }; + EE247B181C3F134A00AE3E12 /* SetterTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SetterTests.swift; sourceTree = ""; }; + EE247B1A1C3F137700AE3E12 /* AggregateFunctionsTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AggregateFunctionsTests.swift; sourceTree = ""; }; + EE247B1B1C3F137700AE3E12 /* BlobTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BlobTests.swift; sourceTree = ""; }; + EE247B1D1C3F137700AE3E12 /* ConnectionTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConnectionTests.swift; sourceTree = ""; }; + EE247B1E1C3F137700AE3E12 /* CoreFunctionsTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CoreFunctionsTests.swift; sourceTree = ""; }; + EE247B1F1C3F137700AE3E12 /* CustomFunctionsTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomFunctionsTests.swift; sourceTree = ""; }; + EE247B201C3F137700AE3E12 /* ExpressionTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ExpressionTests.swift; sourceTree = ""; }; + EE247B211C3F137700AE3E12 /* FTS4Tests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FTS4Tests.swift; sourceTree = ""; }; + EE247B2A1C3F141E00AE3E12 /* OperatorsTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OperatorsTests.swift; sourceTree = ""; }; + EE247B2B1C3F141E00AE3E12 /* QueryTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = QueryTests.swift; sourceTree = ""; }; + EE247B2C1C3F141E00AE3E12 /* RTreeTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RTreeTests.swift; sourceTree = ""; }; + EE247B2D1C3F141E00AE3E12 /* SchemaTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SchemaTests.swift; sourceTree = ""; }; + EE247B321C3F142E00AE3E12 /* StatementTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StatementTests.swift; sourceTree = ""; }; + EE247B331C3F142E00AE3E12 /* ValueTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ValueTests.swift; sourceTree = ""; }; + EE247B3C1C3F3ED000AE3E12 /* SQLite.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = SQLite.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + EE247B451C3F3ED000AE3E12 /* SQLiteTests Mac.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "SQLiteTests Mac.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; + EE247B771C3F40D700AE3E12 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; + EE247B8B1C3F820300AE3E12 /* CONTRIBUTING.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = CONTRIBUTING.md; sourceTree = ""; }; + EE247B8C1C3F821200AE3E12 /* .travis.yml */ = {isa = PBXFileReference; lastKnownFileType = text; path = .travis.yml; sourceTree = ""; }; + EE247B8D1C3F821200AE3E12 /* Makefile */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.make; path = Makefile; sourceTree = ""; }; + EE247B8F1C3F822500AE3E12 /* Index.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = Index.md; sourceTree = ""; }; + EE247B911C3F822500AE3E12 /* installation@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "installation@2x.png"; sourceTree = ""; }; + EE247B921C3F822600AE3E12 /* playground@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "playground@2x.png"; sourceTree = ""; }; + EE247B931C3F826100AE3E12 /* SQLite.swift.podspec */ = {isa = PBXFileReference; explicitFileType = text.script.ruby; path = SQLite.swift.podspec; sourceTree = ""; }; + EE91808B1C46E34A0038162A /* usr/include/sqlite3.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = usr/include/sqlite3.h; sourceTree = SDKROOT; }; + EE91808D1C46E5230038162A /* SQLite-Bridging.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "SQLite-Bridging.h"; path = "../../SQLiteObjc/include/SQLite-Bridging.h"; sourceTree = ""; }; + EE9180911C46E9D30038162A /* libsqlite3.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libsqlite3.tbd; path = Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.11.sdk/usr/lib/libsqlite3.tbd; sourceTree = DEVELOPER_DIR; }; + EE9180931C46EA210038162A /* libsqlite3.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libsqlite3.tbd; path = usr/lib/libsqlite3.tbd; sourceTree = SDKROOT; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 03A65E561C6BB0F50062603F /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 03A65E971C6BB3210062603F /* libsqlite3.tbd in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 03A65E601C6BB0F60062603F /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 03A65E641C6BB0F60062603F /* SQLite.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + A121AC411CA35C79005A31D1 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 3D67B3E61DB2469200A4F4C6 /* libsqlite3.tbd in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + EE247ACF1C3F04ED00AE3E12 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + EE9180941C46EA210038162A /* libsqlite3.tbd in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + EE247ADA1C3F04ED00AE3E12 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + EE247ADE1C3F04ED00AE3E12 /* SQLite.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + EE247B381C3F3ED000AE3E12 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + EE9180951C46EBCC0038162A /* libsqlite3.tbd in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + EE247B421C3F3ED000AE3E12 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + EE247B461C3F3ED000AE3E12 /* SQLite.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 39548A611CA63C740003E3B5 /* CocoaPods */ = { + isa = PBXGroup; + children = ( + 39548A621CA63C740003E3B5 /* appletvos */, + 39548A641CA63C740003E3B5 /* appletvsimulator */, + 39548A661CA63C740003E3B5 /* iphoneos */, + 39548A681CA63C740003E3B5 /* iphonesimulator */, + 39548A6A1CA63C740003E3B5 /* macosx */, + 39548A6C1CA63C740003E3B5 /* watchos */, + 39548A6E1CA63C740003E3B5 /* watchsimulator */, + ); + path = CocoaPods; + sourceTree = ""; + }; + 39548A621CA63C740003E3B5 /* appletvos */ = { + isa = PBXGroup; + children = ( + 39548A631CA63C740003E3B5 /* module.modulemap */, + ); + path = appletvos; + sourceTree = ""; + }; + 39548A641CA63C740003E3B5 /* appletvsimulator */ = { + isa = PBXGroup; + children = ( + 39548A651CA63C740003E3B5 /* module.modulemap */, + ); + path = appletvsimulator; + sourceTree = ""; + }; + 39548A661CA63C740003E3B5 /* iphoneos */ = { + isa = PBXGroup; + children = ( + 39548A671CA63C740003E3B5 /* module.modulemap */, + ); + path = iphoneos; + sourceTree = ""; + }; + 39548A681CA63C740003E3B5 /* iphonesimulator */ = { + isa = PBXGroup; + children = ( + 39548A691CA63C740003E3B5 /* module.modulemap */, + ); + path = iphonesimulator; + sourceTree = ""; + }; + 39548A6A1CA63C740003E3B5 /* macosx */ = { + isa = PBXGroup; + children = ( + 39548A6B1CA63C740003E3B5 /* module.modulemap */, + ); + path = macosx; + sourceTree = ""; + }; + 39548A6C1CA63C740003E3B5 /* watchos */ = { + isa = PBXGroup; + children = ( + 39548A6D1CA63C740003E3B5 /* module.modulemap */, + ); + path = watchos; + sourceTree = ""; + }; + 39548A6E1CA63C740003E3B5 /* watchsimulator */ = { + isa = PBXGroup; + children = ( + 39548A6F1CA63C740003E3B5 /* module.modulemap */, + ); + path = watchsimulator; + sourceTree = ""; + }; + 3D67B3E41DB2469200A4F4C6 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 3D67B3E51DB2469200A4F4C6 /* libsqlite3.tbd */, + ); + name = Frameworks; + sourceTree = ""; + }; + EE247AC91C3F04ED00AE3E12 = { + isa = PBXGroup; + children = ( + EE247AD51C3F04ED00AE3E12 /* SQLite */, + EE247AE11C3F04ED00AE3E12 /* SQLiteTests */, + EE247B8A1C3F81D000AE3E12 /* Metadata */, + EE247AD41C3F04ED00AE3E12 /* Products */, + 3D67B3E41DB2469200A4F4C6 /* Frameworks */, + ); + indentWidth = 4; + sourceTree = ""; + tabWidth = 4; + }; + EE247AD41C3F04ED00AE3E12 /* Products */ = { + isa = PBXGroup; + children = ( + EE247AD31C3F04ED00AE3E12 /* SQLite.framework */, + EE247ADD1C3F04ED00AE3E12 /* SQLiteTests iOS.xctest */, + EE247B3C1C3F3ED000AE3E12 /* SQLite.framework */, + EE247B451C3F3ED000AE3E12 /* SQLiteTests Mac.xctest */, + 03A65E5A1C6BB0F50062603F /* SQLite.framework */, + 03A65E631C6BB0F60062603F /* SQLiteTests tvOS.xctest */, + A121AC451CA35C79005A31D1 /* SQLite.framework */, + ); + name = Products; + sourceTree = ""; + }; + EE247AD51C3F04ED00AE3E12 /* SQLite */ = { + isa = PBXGroup; + children = ( + EE91808B1C46E34A0038162A /* usr/include/sqlite3.h */, + EE247AD61C3F04ED00AE3E12 /* SQLite.h */, + EE247AF71C3F06E900AE3E12 /* Foundation.swift */, + EE247AF81C3F06E900AE3E12 /* Helpers.swift */, + EE247AD81C3F04ED00AE3E12 /* Info.plist */, + EE247AED1C3F06E900AE3E12 /* Core */, + EE247AF41C3F06E900AE3E12 /* Extensions */, + EE247AF91C3F06E900AE3E12 /* Typed */, + ); + name = SQLite; + path = Sources/SQLite; + sourceTree = ""; + }; + EE247AE11C3F04ED00AE3E12 /* SQLiteTests */ = { + isa = PBXGroup; + children = ( + 19A17E2695737FAB5D6086E3 /* fixtures */, + EE247B1A1C3F137700AE3E12 /* AggregateFunctionsTests.swift */, + EE247B1B1C3F137700AE3E12 /* BlobTests.swift */, + EE247B1D1C3F137700AE3E12 /* ConnectionTests.swift */, + EE247B1E1C3F137700AE3E12 /* CoreFunctionsTests.swift */, + EE247B1F1C3F137700AE3E12 /* CustomFunctionsTests.swift */, + EE247B201C3F137700AE3E12 /* ExpressionTests.swift */, + EE247B211C3F137700AE3E12 /* FTS4Tests.swift */, + EE247B2A1C3F141E00AE3E12 /* OperatorsTests.swift */, + EE247B2B1C3F141E00AE3E12 /* QueryTests.swift */, + EE247B2C1C3F141E00AE3E12 /* RTreeTests.swift */, + EE247B2D1C3F141E00AE3E12 /* SchemaTests.swift */, + EE247B181C3F134A00AE3E12 /* SetterTests.swift */, + EE247B321C3F142E00AE3E12 /* StatementTests.swift */, + EE247B331C3F142E00AE3E12 /* ValueTests.swift */, + EE247B161C3F127200AE3E12 /* TestHelpers.swift */, + EE247AE41C3F04ED00AE3E12 /* Info.plist */, + 19A1721B8984686B9963B45D /* FTS5Tests.swift */, + 19A1794CC4D7827E997E32A7 /* FoundationTests.swift */, + 19A17399EA9E61235D5D77BF /* CipherTests.swift */, + 19A17B93B48B5560E6E51791 /* Fixtures.swift */, + ); + name = SQLiteTests; + path = Tests/SQLiteTests; + sourceTree = ""; + }; + EE247AED1C3F06E900AE3E12 /* Core */ = { + isa = PBXGroup; + children = ( + EE91808D1C46E5230038162A /* SQLite-Bridging.h */, + EE247AEE1C3F06E900AE3E12 /* Blob.swift */, + EE247AEF1C3F06E900AE3E12 /* Connection.swift */, + EE247AF01C3F06E900AE3E12 /* fts3_tokenizer.h */, + EE247AF11C3F06E900AE3E12 /* SQLite-Bridging.m */, + EE247AF21C3F06E900AE3E12 /* Statement.swift */, + EE247AF31C3F06E900AE3E12 /* Value.swift */, + ); + path = Core; + sourceTree = ""; + }; + EE247AF41C3F06E900AE3E12 /* Extensions */ = { + isa = PBXGroup; + children = ( + EE247AF51C3F06E900AE3E12 /* FTS4.swift */, + EE247AF61C3F06E900AE3E12 /* RTree.swift */, + 19A1730E4390C775C25677D1 /* FTS5.swift */, + 19A178A39ACA9667A62663CC /* Cipher.swift */, + ); + path = Extensions; + sourceTree = ""; + }; + EE247AF91C3F06E900AE3E12 /* Typed */ = { + isa = PBXGroup; + children = ( + EE247AFA1C3F06E900AE3E12 /* AggregateFunctions.swift */, + EE247AFB1C3F06E900AE3E12 /* Collation.swift */, + EE247AFC1C3F06E900AE3E12 /* CoreFunctions.swift */, + EE247AFD1C3F06E900AE3E12 /* CustomFunctions.swift */, + EE247AFE1C3F06E900AE3E12 /* Expression.swift */, + EE247AFF1C3F06E900AE3E12 /* Operators.swift */, + EE247B001C3F06E900AE3E12 /* Query.swift */, + EE247B011C3F06E900AE3E12 /* Schema.swift */, + EE247B021C3F06E900AE3E12 /* Setter.swift */, + ); + path = Typed; + sourceTree = ""; + }; + EE247B8A1C3F81D000AE3E12 /* Metadata */ = { + isa = PBXGroup; + children = ( + 39548A611CA63C740003E3B5 /* CocoaPods */, + EE247B771C3F40D700AE3E12 /* README.md */, + EE247B8B1C3F820300AE3E12 /* CONTRIBUTING.md */, + EE247B931C3F826100AE3E12 /* SQLite.swift.podspec */, + EE247B8C1C3F821200AE3E12 /* .travis.yml */, + EE247B8D1C3F821200AE3E12 /* Makefile */, + EE9180931C46EA210038162A /* libsqlite3.tbd */, + EE9180911C46E9D30038162A /* libsqlite3.tbd */, + 03A65E961C6BB3210062603F /* libsqlite3.tbd */, + EE247B8E1C3F822500AE3E12 /* Documentation */, + ); + name = Metadata; + sourceTree = ""; + }; + EE247B8E1C3F822500AE3E12 /* Documentation */ = { + isa = PBXGroup; + children = ( + EE247B8F1C3F822500AE3E12 /* Index.md */, + EE247B901C3F822500AE3E12 /* Resources */, + ); + path = Documentation; + sourceTree = ""; + }; + EE247B901C3F822500AE3E12 /* Resources */ = { + isa = PBXGroup; + children = ( + EE247B911C3F822500AE3E12 /* installation@2x.png */, + EE247B921C3F822600AE3E12 /* playground@2x.png */, + ); + path = Resources; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXHeadersBuildPhase section */ + 03A65E571C6BB0F50062603F /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + 03A65E781C6BB2EA0062603F /* fts3_tokenizer.h in Headers */, + 03A65E751C6BB2DF0062603F /* SQLite-Bridging.h in Headers */, + 03A65E711C6BB2CD0062603F /* usr/include/sqlite3.h in Headers */, + 03A65E721C6BB2D30062603F /* SQLite.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + A121AC421CA35C79005A31D1 /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + 3D67B3FA1DB2470600A4F4C6 /* usr/include/sqlite3.h in Headers */, + 3D67B3FB1DB2470600A4F4C6 /* SQLite-Bridging.h in Headers */, + 3D67B3FC1DB2471B00A4F4C6 /* SQLite.h in Headers */, + 3D67B3FD1DB2472D00A4F4C6 /* fts3_tokenizer.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + EE247AD01C3F04ED00AE3E12 /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + EE91808E1C46E5230038162A /* SQLite-Bridging.h in Headers */, + EE247B051C3F06E900AE3E12 /* fts3_tokenizer.h in Headers */, + EE91808C1C46E34A0038162A /* usr/include/sqlite3.h in Headers */, + EE247AD71C3F04ED00AE3E12 /* SQLite.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + EE247B391C3F3ED000AE3E12 /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + EE9180901C46E8980038162A /* usr/include/sqlite3.h in Headers */, + EE247B671C3F3FEC00AE3E12 /* fts3_tokenizer.h in Headers */, + EE247B621C3F3FDB00AE3E12 /* SQLite.h in Headers */, + EE91808F1C46E76D0038162A /* SQLite-Bridging.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXHeadersBuildPhase section */ + +/* Begin PBXNativeTarget section */ + 03A65E591C6BB0F50062603F /* SQLite tvOS */ = { + isa = PBXNativeTarget; + buildConfigurationList = 03A65E6F1C6BB0F60062603F /* Build configuration list for PBXNativeTarget "SQLite tvOS" */; + buildPhases = ( + 03A65E551C6BB0F50062603F /* Sources */, + 03A65E561C6BB0F50062603F /* Frameworks */, + 03A65E571C6BB0F50062603F /* Headers */, + 03A65E581C6BB0F50062603F /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = "SQLite tvOS"; + productName = "SQLite tvOS"; + productReference = 03A65E5A1C6BB0F50062603F /* SQLite.framework */; + productType = "com.apple.product-type.framework"; + }; + 03A65E621C6BB0F60062603F /* SQLiteTests tvOS */ = { + isa = PBXNativeTarget; + buildConfigurationList = 03A65E701C6BB0F60062603F /* Build configuration list for PBXNativeTarget "SQLiteTests tvOS" */; + buildPhases = ( + 03A65E5F1C6BB0F60062603F /* Sources */, + 03A65E601C6BB0F60062603F /* Frameworks */, + 03A65E611C6BB0F60062603F /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 03A65E661C6BB0F60062603F /* PBXTargetDependency */, + ); + name = "SQLiteTests tvOS"; + productName = "SQLite tvOSTests"; + productReference = 03A65E631C6BB0F60062603F /* SQLiteTests tvOS.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + A121AC441CA35C79005A31D1 /* SQLite watchOS */ = { + isa = PBXNativeTarget; + buildConfigurationList = A121AC4C1CA35C79005A31D1 /* Build configuration list for PBXNativeTarget "SQLite watchOS" */; + buildPhases = ( + A121AC401CA35C79005A31D1 /* Sources */, + A121AC411CA35C79005A31D1 /* Frameworks */, + A121AC421CA35C79005A31D1 /* Headers */, + A121AC431CA35C79005A31D1 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = "SQLite watchOS"; + productName = "SQLite watchOS"; + productReference = A121AC451CA35C79005A31D1 /* SQLite.framework */; + productType = "com.apple.product-type.framework"; + }; + EE247AD21C3F04ED00AE3E12 /* SQLite iOS */ = { + isa = PBXNativeTarget; + buildConfigurationList = EE247AE71C3F04ED00AE3E12 /* Build configuration list for PBXNativeTarget "SQLite iOS" */; + buildPhases = ( + EE247ACE1C3F04ED00AE3E12 /* Sources */, + EE247ACF1C3F04ED00AE3E12 /* Frameworks */, + EE247AD01C3F04ED00AE3E12 /* Headers */, + EE247AD11C3F04ED00AE3E12 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = "SQLite iOS"; + productName = SQLite; + productReference = EE247AD31C3F04ED00AE3E12 /* SQLite.framework */; + productType = "com.apple.product-type.framework"; + }; + EE247ADC1C3F04ED00AE3E12 /* SQLiteTests iOS */ = { + isa = PBXNativeTarget; + buildConfigurationList = EE247AEA1C3F04ED00AE3E12 /* Build configuration list for PBXNativeTarget "SQLiteTests iOS" */; + buildPhases = ( + EE247AD91C3F04ED00AE3E12 /* Sources */, + EE247ADA1C3F04ED00AE3E12 /* Frameworks */, + EE247ADB1C3F04ED00AE3E12 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + EE247AE01C3F04ED00AE3E12 /* PBXTargetDependency */, + ); + name = "SQLiteTests iOS"; + productName = SQLiteTests; + productReference = EE247ADD1C3F04ED00AE3E12 /* SQLiteTests iOS.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + EE247B3B1C3F3ED000AE3E12 /* SQLite Mac */ = { + isa = PBXNativeTarget; + buildConfigurationList = EE247B511C3F3ED000AE3E12 /* Build configuration list for PBXNativeTarget "SQLite Mac" */; + buildPhases = ( + EE247B371C3F3ED000AE3E12 /* Sources */, + EE247B381C3F3ED000AE3E12 /* Frameworks */, + EE247B391C3F3ED000AE3E12 /* Headers */, + EE247B3A1C3F3ED000AE3E12 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = "SQLite Mac"; + productName = SQLite; + productReference = EE247B3C1C3F3ED000AE3E12 /* SQLite.framework */; + productType = "com.apple.product-type.framework"; + }; + EE247B441C3F3ED000AE3E12 /* SQLiteTests Mac */ = { + isa = PBXNativeTarget; + buildConfigurationList = EE247B521C3F3ED000AE3E12 /* Build configuration list for PBXNativeTarget "SQLiteTests Mac" */; + buildPhases = ( + EE247B411C3F3ED000AE3E12 /* Sources */, + EE247B421C3F3ED000AE3E12 /* Frameworks */, + EE247B431C3F3ED000AE3E12 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + EE247B481C3F3ED000AE3E12 /* PBXTargetDependency */, + ); + name = "SQLiteTests Mac"; + productName = SQLiteTests; + productReference = EE247B451C3F3ED000AE3E12 /* SQLiteTests Mac.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + EE247ACA1C3F04ED00AE3E12 /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 0720; + LastUpgradeCheck = 0800; + TargetAttributes = { + 03A65E591C6BB0F50062603F = { + CreatedOnToolsVersion = 7.2; + }; + 03A65E621C6BB0F60062603F = { + CreatedOnToolsVersion = 7.2; + }; + A121AC441CA35C79005A31D1 = { + CreatedOnToolsVersion = 7.3; + }; + EE247AD21C3F04ED00AE3E12 = { + CreatedOnToolsVersion = 7.2; + LastSwiftMigration = 0820; + }; + EE247ADC1C3F04ED00AE3E12 = { + CreatedOnToolsVersion = 7.2; + LastSwiftMigration = 0800; + }; + EE247B3B1C3F3ED000AE3E12 = { + CreatedOnToolsVersion = 7.2; + LastSwiftMigration = 0800; + }; + EE247B441C3F3ED000AE3E12 = { + CreatedOnToolsVersion = 7.2; + LastSwiftMigration = 0800; + }; + }; + }; + buildConfigurationList = EE247ACD1C3F04ED00AE3E12 /* Build configuration list for PBXProject "SQLite" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + en, + ); + mainGroup = EE247AC91C3F04ED00AE3E12; + productRefGroup = EE247AD41C3F04ED00AE3E12 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + EE247AD21C3F04ED00AE3E12 /* SQLite iOS */, + EE247ADC1C3F04ED00AE3E12 /* SQLiteTests iOS */, + EE247B3B1C3F3ED000AE3E12 /* SQLite Mac */, + EE247B441C3F3ED000AE3E12 /* SQLiteTests Mac */, + 03A65E591C6BB0F50062603F /* SQLite tvOS */, + 03A65E621C6BB0F60062603F /* SQLiteTests tvOS */, + A121AC441CA35C79005A31D1 /* SQLite watchOS */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 03A65E581C6BB0F50062603F /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 03A65E611C6BB0F60062603F /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 19A17F3E1F7ACA33BD43E138 /* fixtures in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + A121AC431CA35C79005A31D1 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + EE247AD11C3F04ED00AE3E12 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + EE247ADB1C3F04ED00AE3E12 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 19A17FDA323BAFDEC627E76F /* fixtures in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + EE247B3A1C3F3ED000AE3E12 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + EE247B431C3F3ED000AE3E12 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 19A175DFF47B84757E547C62 /* fixtures in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 03A65E551C6BB0F50062603F /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 03A65E801C6BB2FB0062603F /* CoreFunctions.swift in Sources */, + 03A65E761C6BB2E60062603F /* Blob.swift in Sources */, + 03A65E7D1C6BB2F70062603F /* RTree.swift in Sources */, + 03A65E791C6BB2EF0062603F /* SQLite-Bridging.m in Sources */, + 03A65E7B1C6BB2F70062603F /* Value.swift in Sources */, + 03A65E821C6BB2FB0062603F /* Expression.swift in Sources */, + 03A65E731C6BB2D80062603F /* Foundation.swift in Sources */, + 03A65E7F1C6BB2FB0062603F /* Collation.swift in Sources */, + 03A65E861C6BB2FB0062603F /* Setter.swift in Sources */, + 03A65E811C6BB2FB0062603F /* CustomFunctions.swift in Sources */, + 03A65E7A1C6BB2F70062603F /* Statement.swift in Sources */, + 03A65E741C6BB2DA0062603F /* Helpers.swift in Sources */, + 03A65E831C6BB2FB0062603F /* Operators.swift in Sources */, + 03A65E851C6BB2FB0062603F /* Schema.swift in Sources */, + 03A65E841C6BB2FB0062603F /* Query.swift in Sources */, + 03A65E7C1C6BB2F70062603F /* FTS4.swift in Sources */, + 03A65E771C6BB2E60062603F /* Connection.swift in Sources */, + 03A65E7E1C6BB2FB0062603F /* AggregateFunctions.swift in Sources */, + 19A17EC0D68BA8C03288ADF7 /* FTS5.swift in Sources */, + 19A179E76EA6207669B60C1B /* Cipher.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 03A65E5F1C6BB0F60062603F /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 03A65E881C6BB3030062603F /* BlobTests.swift in Sources */, + 03A65E901C6BB3030062603F /* RTreeTests.swift in Sources */, + 03A65E941C6BB3030062603F /* ValueTests.swift in Sources */, + 03A65E8F1C6BB3030062603F /* QueryTests.swift in Sources */, + 03A65E8B1C6BB3030062603F /* CustomFunctionsTests.swift in Sources */, + 03A65E871C6BB3030062603F /* AggregateFunctionsTests.swift in Sources */, + 03A65E921C6BB3030062603F /* SetterTests.swift in Sources */, + 03A65E891C6BB3030062603F /* ConnectionTests.swift in Sources */, + 03A65E8A1C6BB3030062603F /* CoreFunctionsTests.swift in Sources */, + 03A65E931C6BB3030062603F /* StatementTests.swift in Sources */, + 03A65E911C6BB3030062603F /* SchemaTests.swift in Sources */, + 03A65E8D1C6BB3030062603F /* FTS4Tests.swift in Sources */, + 03A65E8C1C6BB3030062603F /* ExpressionTests.swift in Sources */, + 03A65E8E1C6BB3030062603F /* OperatorsTests.swift in Sources */, + 03A65E951C6BB3030062603F /* TestHelpers.swift in Sources */, + 19A17254FBA7894891F7297B /* FTS5Tests.swift in Sources */, + 19A17E04C4C0956715C5676A /* FoundationTests.swift in Sources */, + 19A179A0C45377CB09BB358C /* CipherTests.swift in Sources */, + 19A17F60B685636D1F83C2DD /* Fixtures.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + A121AC401CA35C79005A31D1 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 3D67B3F91DB246E700A4F4C6 /* SQLite-Bridging.m in Sources */, + 3D67B3F71DB246D700A4F4C6 /* Foundation.swift in Sources */, + 3D67B3F81DB246D700A4F4C6 /* Helpers.swift in Sources */, + 3D67B3E91DB246D100A4F4C6 /* Statement.swift in Sources */, + 3D67B3EA1DB246D100A4F4C6 /* Value.swift in Sources */, + 3D67B3EB1DB246D100A4F4C6 /* FTS4.swift in Sources */, + 3D67B3EC1DB246D100A4F4C6 /* RTree.swift in Sources */, + 3D67B3ED1DB246D100A4F4C6 /* FTS5.swift in Sources */, + 3D67B3EE1DB246D100A4F4C6 /* AggregateFunctions.swift in Sources */, + 3D67B3EF1DB246D100A4F4C6 /* Collation.swift in Sources */, + 3D67B3F01DB246D100A4F4C6 /* CoreFunctions.swift in Sources */, + 3D67B3F11DB246D100A4F4C6 /* CustomFunctions.swift in Sources */, + 3D67B3F21DB246D100A4F4C6 /* Expression.swift in Sources */, + 3D67B3F31DB246D100A4F4C6 /* Operators.swift in Sources */, + 3D67B3F41DB246D100A4F4C6 /* Query.swift in Sources */, + 3D67B3F51DB246D100A4F4C6 /* Schema.swift in Sources */, + 3D67B3F61DB246D100A4F4C6 /* Setter.swift in Sources */, + 3D67B3E71DB246BA00A4F4C6 /* Blob.swift in Sources */, + 3D67B3E81DB246BA00A4F4C6 /* Connection.swift in Sources */, + 19A179CCF9671E345E5A9811 /* Cipher.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + EE247ACE1C3F04ED00AE3E12 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + EE247B0F1C3F06E900AE3E12 /* CoreFunctions.swift in Sources */, + EE247B0A1C3F06E900AE3E12 /* RTree.swift in Sources */, + EE247B031C3F06E900AE3E12 /* Blob.swift in Sources */, + EE247B0B1C3F06E900AE3E12 /* Foundation.swift in Sources */, + EE247B041C3F06E900AE3E12 /* Connection.swift in Sources */, + EE247B111C3F06E900AE3E12 /* Expression.swift in Sources */, + EE247B0C1C3F06E900AE3E12 /* Helpers.swift in Sources */, + EE247B0E1C3F06E900AE3E12 /* Collation.swift in Sources */, + EE247B151C3F06E900AE3E12 /* Setter.swift in Sources */, + EE247B101C3F06E900AE3E12 /* CustomFunctions.swift in Sources */, + EE247B091C3F06E900AE3E12 /* FTS4.swift in Sources */, + EE247B081C3F06E900AE3E12 /* Value.swift in Sources */, + EE247B121C3F06E900AE3E12 /* Operators.swift in Sources */, + EE247B141C3F06E900AE3E12 /* Schema.swift in Sources */, + EE247B131C3F06E900AE3E12 /* Query.swift in Sources */, + EE247B061C3F06E900AE3E12 /* SQLite-Bridging.m in Sources */, + EE247B071C3F06E900AE3E12 /* Statement.swift in Sources */, + EE247B0D1C3F06E900AE3E12 /* AggregateFunctions.swift in Sources */, + 19A1717B10CC941ACB5533D6 /* FTS5.swift in Sources */, + 19A171F12AB8B07F2FD7201A /* Cipher.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + EE247AD91C3F04ED00AE3E12 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + EE247B261C3F137700AE3E12 /* CoreFunctionsTests.swift in Sources */, + EE247B291C3F137700AE3E12 /* FTS4Tests.swift in Sources */, + EE247B191C3F134A00AE3E12 /* SetterTests.swift in Sources */, + EE247B311C3F141E00AE3E12 /* SchemaTests.swift in Sources */, + EE247B171C3F127200AE3E12 /* TestHelpers.swift in Sources */, + EE247B281C3F137700AE3E12 /* ExpressionTests.swift in Sources */, + EE247B271C3F137700AE3E12 /* CustomFunctionsTests.swift in Sources */, + EE247B341C3F142E00AE3E12 /* StatementTests.swift in Sources */, + EE247B301C3F141E00AE3E12 /* RTreeTests.swift in Sources */, + EE247B231C3F137700AE3E12 /* BlobTests.swift in Sources */, + EE247B351C3F142E00AE3E12 /* ValueTests.swift in Sources */, + EE247B2F1C3F141E00AE3E12 /* QueryTests.swift in Sources */, + EE247B221C3F137700AE3E12 /* AggregateFunctionsTests.swift in Sources */, + EE247B2E1C3F141E00AE3E12 /* OperatorsTests.swift in Sources */, + EE247B251C3F137700AE3E12 /* ConnectionTests.swift in Sources */, + 19A171E6FA242F72A308C594 /* FTS5Tests.swift in Sources */, + 19A17FB80B94E882050AA908 /* FoundationTests.swift in Sources */, + 19A177CC33F2E6A24AF90B02 /* CipherTests.swift in Sources */, + 19A17408007B182F884E3A53 /* Fixtures.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + EE247B371C3F3ED000AE3E12 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + EE247B6F1C3F3FEC00AE3E12 /* CoreFunctions.swift in Sources */, + EE247B651C3F3FEC00AE3E12 /* Blob.swift in Sources */, + EE247B6C1C3F3FEC00AE3E12 /* RTree.swift in Sources */, + EE247B681C3F3FEC00AE3E12 /* SQLite-Bridging.m in Sources */, + EE247B6A1C3F3FEC00AE3E12 /* Value.swift in Sources */, + EE247B711C3F3FEC00AE3E12 /* Expression.swift in Sources */, + EE247B631C3F3FDB00AE3E12 /* Foundation.swift in Sources */, + EE247B6E1C3F3FEC00AE3E12 /* Collation.swift in Sources */, + EE247B751C3F3FEC00AE3E12 /* Setter.swift in Sources */, + EE247B701C3F3FEC00AE3E12 /* CustomFunctions.swift in Sources */, + EE247B691C3F3FEC00AE3E12 /* Statement.swift in Sources */, + EE247B641C3F3FDB00AE3E12 /* Helpers.swift in Sources */, + EE247B721C3F3FEC00AE3E12 /* Operators.swift in Sources */, + EE247B741C3F3FEC00AE3E12 /* Schema.swift in Sources */, + EE247B731C3F3FEC00AE3E12 /* Query.swift in Sources */, + EE247B6B1C3F3FEC00AE3E12 /* FTS4.swift in Sources */, + EE247B661C3F3FEC00AE3E12 /* Connection.swift in Sources */, + EE247B6D1C3F3FEC00AE3E12 /* AggregateFunctions.swift in Sources */, + 19A1750CEE9B05267995CF3D /* FTS5.swift in Sources */, + 19A17835FD5886FDC5A3228F /* Cipher.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + EE247B411C3F3ED000AE3E12 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + EE247B561C3F3FC700AE3E12 /* CoreFunctionsTests.swift in Sources */, + EE247B5A1C3F3FC700AE3E12 /* OperatorsTests.swift in Sources */, + EE247B541C3F3FC700AE3E12 /* BlobTests.swift in Sources */, + EE247B5D1C3F3FC700AE3E12 /* SchemaTests.swift in Sources */, + EE247B591C3F3FC700AE3E12 /* FTS4Tests.swift in Sources */, + EE247B531C3F3FC700AE3E12 /* AggregateFunctionsTests.swift in Sources */, + EE247B5F1C3F3FC700AE3E12 /* StatementTests.swift in Sources */, + EE247B5C1C3F3FC700AE3E12 /* RTreeTests.swift in Sources */, + EE247B571C3F3FC700AE3E12 /* CustomFunctionsTests.swift in Sources */, + EE247B601C3F3FC700AE3E12 /* ValueTests.swift in Sources */, + EE247B551C3F3FC700AE3E12 /* ConnectionTests.swift in Sources */, + EE247B611C3F3FC700AE3E12 /* TestHelpers.swift in Sources */, + EE247B581C3F3FC700AE3E12 /* ExpressionTests.swift in Sources */, + EE247B5E1C3F3FC700AE3E12 /* SetterTests.swift in Sources */, + EE247B5B1C3F3FC700AE3E12 /* QueryTests.swift in Sources */, + 19A174D78559CD30679BCCCB /* FTS5Tests.swift in Sources */, + 19A178072B371489E6A1E839 /* FoundationTests.swift in Sources */, + 19A17C4B951CB054EE48AB1C /* CipherTests.swift in Sources */, + 19A1709C3E7A406E62293B2A /* Fixtures.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 03A65E661C6BB0F60062603F /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 03A65E591C6BB0F50062603F /* SQLite tvOS */; + targetProxy = 03A65E651C6BB0F60062603F /* PBXContainerItemProxy */; + }; + EE247AE01C3F04ED00AE3E12 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = EE247AD21C3F04ED00AE3E12 /* SQLite iOS */; + targetProxy = EE247ADF1C3F04ED00AE3E12 /* PBXContainerItemProxy */; + }; + EE247B481C3F3ED000AE3E12 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = EE247B3B1C3F3ED000AE3E12 /* SQLite Mac */; + targetProxy = EE247B471C3F3ED000AE3E12 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin XCBuildConfiguration section */ + 03A65E6B1C6BB0F60062603F /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + APPLICATION_EXTENSION_API_ONLY = YES; + "CODE_SIGN_IDENTITY[sdk=appletvos*]" = ""; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + INFOPLIST_FILE = "$(SRCROOT)/Sources/SQLite/Info.plist"; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = com.stephencelis.SQLite; + PRODUCT_NAME = SQLite; + SDKROOT = appletvos; + SKIP_INSTALL = YES; + "SWIFT_INCLUDE_PATHS[sdk=appletvos*]" = "$(SRCROOT)/CocoaPods/appletvos"; + "SWIFT_INCLUDE_PATHS[sdk=appletvsimulator*]" = "$(SRCROOT)/CocoaPods/appletvsimulator"; + SWIFT_VERSION = 3.0; + TVOS_DEPLOYMENT_TARGET = 9.1; + }; + name = Debug; + }; + 03A65E6C1C6BB0F60062603F /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + APPLICATION_EXTENSION_API_ONLY = YES; + "CODE_SIGN_IDENTITY[sdk=appletvos*]" = ""; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + INFOPLIST_FILE = "$(SRCROOT)/Sources/SQLite/Info.plist"; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = com.stephencelis.SQLite; + PRODUCT_NAME = SQLite; + SDKROOT = appletvos; + SKIP_INSTALL = YES; + "SWIFT_INCLUDE_PATHS[sdk=appletvos*]" = "$(SRCROOT)/CocoaPods/appletvos"; + "SWIFT_INCLUDE_PATHS[sdk=appletvsimulator*]" = "$(SRCROOT)/CocoaPods/appletvsimulator"; + SWIFT_VERSION = 3.0; + TVOS_DEPLOYMENT_TARGET = 9.1; + }; + name = Release; + }; + 03A65E6D1C6BB0F60062603F /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + INFOPLIST_FILE = Tests/SQLite/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = com.stephencelis.SQLiteTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = appletvos; + SWIFT_VERSION = 3.0; + TVOS_DEPLOYMENT_TARGET = 9.1; + }; + name = Debug; + }; + 03A65E6E1C6BB0F60062603F /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + INFOPLIST_FILE = Tests/SQLite/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = com.stephencelis.SQLiteTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = appletvos; + SWIFT_VERSION = 3.0; + TVOS_DEPLOYMENT_TARGET = 9.1; + }; + name = Release; + }; + A121AC4A1CA35C79005A31D1 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + APPLICATION_EXTENSION_API_ONLY = YES; + CLANG_ANALYZER_NONNULL = YES; + "CODE_SIGN_IDENTITY[sdk=watchos*]" = ""; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + INFOPLIST_FILE = "$(SRCROOT)/Sources/SQLite/Info.plist"; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = com.stephencelis.SQLite; + PRODUCT_NAME = SQLite; + SDKROOT = watchos; + SKIP_INSTALL = YES; + "SWIFT_INCLUDE_PATHS[sdk=watchos*]" = "$(SRCROOT)/CocoaPods/watchos"; + "SWIFT_INCLUDE_PATHS[sdk=watchsimulator*]" = "$(SRCROOT)/CocoaPods/watchsimulator"; + SWIFT_VERSION = 3.0; + TARGETED_DEVICE_FAMILY = 4; + WATCHOS_DEPLOYMENT_TARGET = 2.2; + }; + name = Debug; + }; + A121AC4B1CA35C79005A31D1 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + APPLICATION_EXTENSION_API_ONLY = YES; + CLANG_ANALYZER_NONNULL = YES; + "CODE_SIGN_IDENTITY[sdk=watchos*]" = ""; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + INFOPLIST_FILE = "$(SRCROOT)/Sources/SQLite/Info.plist"; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = com.stephencelis.SQLite; + PRODUCT_NAME = SQLite; + SDKROOT = watchos; + SKIP_INSTALL = YES; + "SWIFT_INCLUDE_PATHS[sdk=watchos*]" = "$(SRCROOT)/CocoaPods/watchos"; + "SWIFT_INCLUDE_PATHS[sdk=watchsimulator*]" = "$(SRCROOT)/CocoaPods/watchsimulator"; + SWIFT_VERSION = 3.0; + TARGETED_DEVICE_FAMILY = 4; + WATCHOS_DEPLOYMENT_TARGET = 2.2; + }; + name = Release; + }; + EE247AE51C3F04ED00AE3E12 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + MACOSX_DEPLOYMENT_TARGET = 10.9; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + PRODUCT_NAME = ""; + SDKROOT = iphoneos; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + TARGETED_DEVICE_FAMILY = "1,2,3"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Debug; + }; + EE247AE61C3F04ED00AE3E12 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + MACOSX_DEPLOYMENT_TARGET = 10.9; + MTL_ENABLE_DEBUG_INFO = NO; + PRODUCT_NAME = ""; + SDKROOT = iphoneos; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + TARGETED_DEVICE_FAMILY = "1,2,3"; + VALIDATE_PRODUCT = YES; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Release; + }; + EE247AE81C3F04ED00AE3E12 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + APPLICATION_EXTENSION_API_ONLY = YES; + CLANG_ENABLE_MODULES = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + INFOPLIST_FILE = "$(SRCROOT)/Sources/SQLite/Info.plist"; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = com.stephencelis.SQLite; + PRODUCT_NAME = SQLite; + SKIP_INSTALL = YES; + "SWIFT_INCLUDE_PATHS[sdk=iphoneos*]" = "$(SRCROOT)/CocoaPods/iphoneos"; + "SWIFT_INCLUDE_PATHS[sdk=iphoneos10.0]" = "$(SRCROOT)/CocoaPods/iphoneos-10.0"; + "SWIFT_INCLUDE_PATHS[sdk=iphonesimulator*]" = "$(SRCROOT)/CocoaPods/iphonesimulator"; + "SWIFT_INCLUDE_PATHS[sdk=iphonesimulator10.0]" = "$(SRCROOT)/CocoaPods/iphonesimulator-10.0"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 3.0; + }; + name = Debug; + }; + EE247AE91C3F04ED00AE3E12 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + APPLICATION_EXTENSION_API_ONLY = YES; + CLANG_ENABLE_MODULES = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + INFOPLIST_FILE = "$(SRCROOT)/Sources/SQLite/Info.plist"; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = com.stephencelis.SQLite; + PRODUCT_NAME = SQLite; + SKIP_INSTALL = YES; + "SWIFT_INCLUDE_PATHS[sdk=iphoneos*]" = "$(SRCROOT)/CocoaPods/iphoneos"; + "SWIFT_INCLUDE_PATHS[sdk=iphoneos10.0]" = "$(SRCROOT)/CocoaPods/iphoneos-10.0"; + "SWIFT_INCLUDE_PATHS[sdk=iphonesimulator*]" = "$(SRCROOT)/CocoaPods/iphonesimulator"; + "SWIFT_INCLUDE_PATHS[sdk=iphonesimulator10.0]" = "$(SRCROOT)/CocoaPods/iphonesimulator-10.0"; + SWIFT_VERSION = 3.0; + }; + name = Release; + }; + EE247AEB1C3F04ED00AE3E12 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + INFOPLIST_FILE = Tests/SQLiteTests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = com.stephencelis.SQLiteTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 3.0; + }; + name = Debug; + }; + EE247AEC1C3F04ED00AE3E12 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + INFOPLIST_FILE = Tests/SQLiteTests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = com.stephencelis.SQLiteTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 3.0; + }; + name = Release; + }; + EE247B4D1C3F3ED000AE3E12 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + APPLICATION_EXTENSION_API_ONLY = YES; + CODE_SIGN_IDENTITY = ""; + COMBINE_HIDPI_IMAGES = YES; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + FRAMEWORK_VERSION = A; + INFOPLIST_FILE = "$(SRCROOT)/Sources/SQLite/Info.plist"; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/Frameworks"; + MACOSX_DEPLOYMENT_TARGET = 10.9; + PRODUCT_BUNDLE_IDENTIFIER = com.stephencelis.SQLite; + PRODUCT_NAME = SQLite; + SDKROOT = macosx; + SKIP_INSTALL = YES; + SWIFT_INCLUDE_PATHS = ""; + "SWIFT_INCLUDE_PATHS[sdk=macosx*]" = "$(SRCROOT)/CocoaPods/macosx"; + "SWIFT_INCLUDE_PATHS[sdk=macosx10.11]" = "$(SRCROOT)/CocoaPods/macosx-10.11"; + "SWIFT_INCLUDE_PATHS[sdk=macosx10.12]" = "$(SRCROOT)/CocoaPods/macosx-10.12"; + SWIFT_VERSION = 3.0; + }; + name = Debug; + }; + EE247B4E1C3F3ED000AE3E12 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + APPLICATION_EXTENSION_API_ONLY = YES; + CODE_SIGN_IDENTITY = ""; + COMBINE_HIDPI_IMAGES = YES; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + FRAMEWORK_VERSION = A; + INFOPLIST_FILE = "$(SRCROOT)/Sources/SQLite/Info.plist"; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/Frameworks"; + MACOSX_DEPLOYMENT_TARGET = 10.9; + PRODUCT_BUNDLE_IDENTIFIER = com.stephencelis.SQLite; + PRODUCT_NAME = SQLite; + SDKROOT = macosx; + SKIP_INSTALL = YES; + SWIFT_INCLUDE_PATHS = ""; + "SWIFT_INCLUDE_PATHS[sdk=macosx*]" = "$(SRCROOT)/CocoaPods/macosx"; + "SWIFT_INCLUDE_PATHS[sdk=macosx10.11]" = "$(SRCROOT)/CocoaPods/macosx-10.11"; + "SWIFT_INCLUDE_PATHS[sdk=macosx10.12]" = "$(SRCROOT)/CocoaPods/macosx-10.12"; + SWIFT_VERSION = 3.0; + }; + name = Release; + }; + EE247B4F1C3F3ED000AE3E12 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_IDENTITY = "-"; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Tests/SQLiteTests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks"; + MACOSX_DEPLOYMENT_TARGET = 10.11; + PRODUCT_BUNDLE_IDENTIFIER = com.stephencelis.SQLiteTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = macosx; + SWIFT_VERSION = 3.0; + }; + name = Debug; + }; + EE247B501C3F3ED000AE3E12 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_IDENTITY = "-"; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Tests/SQLiteTests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks"; + MACOSX_DEPLOYMENT_TARGET = 10.11; + PRODUCT_BUNDLE_IDENTIFIER = com.stephencelis.SQLiteTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = macosx; + SWIFT_VERSION = 3.0; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 03A65E6F1C6BB0F60062603F /* Build configuration list for PBXNativeTarget "SQLite tvOS" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 03A65E6B1C6BB0F60062603F /* Debug */, + 03A65E6C1C6BB0F60062603F /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 03A65E701C6BB0F60062603F /* Build configuration list for PBXNativeTarget "SQLiteTests tvOS" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 03A65E6D1C6BB0F60062603F /* Debug */, + 03A65E6E1C6BB0F60062603F /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + A121AC4C1CA35C79005A31D1 /* Build configuration list for PBXNativeTarget "SQLite watchOS" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + A121AC4A1CA35C79005A31D1 /* Debug */, + A121AC4B1CA35C79005A31D1 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + EE247ACD1C3F04ED00AE3E12 /* Build configuration list for PBXProject "SQLite" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + EE247AE51C3F04ED00AE3E12 /* Debug */, + EE247AE61C3F04ED00AE3E12 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + EE247AE71C3F04ED00AE3E12 /* Build configuration list for PBXNativeTarget "SQLite iOS" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + EE247AE81C3F04ED00AE3E12 /* Debug */, + EE247AE91C3F04ED00AE3E12 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + EE247AEA1C3F04ED00AE3E12 /* Build configuration list for PBXNativeTarget "SQLiteTests iOS" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + EE247AEB1C3F04ED00AE3E12 /* Debug */, + EE247AEC1C3F04ED00AE3E12 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + EE247B511C3F3ED000AE3E12 /* Build configuration list for PBXNativeTarget "SQLite Mac" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + EE247B4D1C3F3ED000AE3E12 /* Debug */, + EE247B4E1C3F3ED000AE3E12 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + EE247B521C3F3ED000AE3E12 /* Build configuration list for PBXNativeTarget "SQLiteTests Mac" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + EE247B4F1C3F3ED000AE3E12 /* Debug */, + EE247B501C3F3ED000AE3E12 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = EE247ACA1C3F04ED00AE3E12 /* Project object */; +} diff --git a/mobile/ios/ThirdParty/SQLite.swift/SQLite.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/mobile/ios/ThirdParty/SQLite.swift/SQLite.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000000..cd11dc9dd6 --- /dev/null +++ b/mobile/ios/ThirdParty/SQLite.swift/SQLite.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/mobile/ios/ThirdParty/SQLite.swift/Sources/SQLite/Core/Blob.swift b/mobile/ios/ThirdParty/SQLite.swift/Sources/SQLite/Core/Blob.swift new file mode 100644 index 0000000000..2f5d2a14f4 --- /dev/null +++ b/mobile/ios/ThirdParty/SQLite.swift/Sources/SQLite/Core/Blob.swift @@ -0,0 +1,60 @@ +// +// SQLite.swift +// https://github.com/stephencelis/SQLite.swift +// Copyright © 2014-2015 Stephen Celis. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +public struct Blob { + + public let bytes: [UInt8] + + public init(bytes: [UInt8]) { + self.bytes = bytes + } + + public init(bytes: UnsafeRawPointer, length: Int) { + let i8bufptr = UnsafeBufferPointer(start: bytes.assumingMemoryBound(to: UInt8.self), count: length) + self.init(bytes: [UInt8](i8bufptr)) + } + + public func toHex() -> String { + return bytes.map { + ($0 < 16 ? "0" : "") + String($0, radix: 16, uppercase: false) + }.joined(separator: "") + } + +} + +extension Blob : CustomStringConvertible { + + public var description: String { + return "x'\(toHex())'" + } + +} + +extension Blob : Equatable { + +} + +public func ==(lhs: Blob, rhs: Blob) -> Bool { + return lhs.bytes == rhs.bytes +} diff --git a/mobile/ios/ThirdParty/SQLite.swift/Sources/SQLite/Core/Connection.swift b/mobile/ios/ThirdParty/SQLite.swift/Sources/SQLite/Core/Connection.swift new file mode 100644 index 0000000000..bd9ce1aa0d --- /dev/null +++ b/mobile/ios/ThirdParty/SQLite.swift/Sources/SQLite/Core/Connection.swift @@ -0,0 +1,756 @@ +// +// SQLite.swift +// https://github.com/stephencelis/SQLite.swift +// Copyright © 2014-2015 Stephen Celis. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import Foundation.NSUUID +import Dispatch +#if SQLITE_SWIFT_STANDALONE +import sqlite3 +#elseif SQLITE_SWIFT_SQLCIPHER +import SQLCipher +#else +import SQLite3 +#endif + +/// A connection to SQLite. +public final class Connection { + + /// The location of a SQLite database. + public enum Location { + + /// An in-memory database (equivalent to `.URI(":memory:")`). + /// + /// See: + case inMemory + + /// A temporary, file-backed database (equivalent to `.URI("")`). + /// + /// See: + case temporary + + /// A database located at the given URI filename (or path). + /// + /// See: + /// + /// - Parameter filename: A URI filename + case uri(String) + } + + /// An SQL operation passed to update callbacks. + public enum Operation { + + /// An INSERT operation. + case insert + + /// An UPDATE operation. + case update + + /// A DELETE operation. + case delete + + fileprivate init(rawValue:Int32) { + switch rawValue { + case SQLITE_INSERT: + self = .insert + case SQLITE_UPDATE: + self = .update + case SQLITE_DELETE: + self = .delete + default: + fatalError("unhandled operation code: \(rawValue)") + } + } + } + + public var handle: OpaquePointer { return _handle! } + + fileprivate var _handle: OpaquePointer? = nil + + /// Initializes a new SQLite connection. + /// + /// - Parameters: + /// + /// - location: The location of the database. Creates a new database if it + /// doesn’t already exist (unless in read-only mode). + /// + /// Default: `.InMemory`. + /// + /// - readonly: Whether or not to open the database in a read-only state. + /// + /// Default: `false`. + /// + /// - Returns: A new database connection. + public init(_ location: Location = .inMemory, readonly: Bool = false) throws { + let flags = readonly ? SQLITE_OPEN_READONLY : SQLITE_OPEN_CREATE | SQLITE_OPEN_READWRITE + try check(sqlite3_open_v2(location.description, &_handle, flags | SQLITE_OPEN_FULLMUTEX, nil)) + queue.setSpecific(key: Connection.queueKey, value: queueContext) + } + + /// Initializes a new connection to a database. + /// + /// - Parameters: + /// + /// - filename: The location of the database. Creates a new database if + /// it doesn’t already exist (unless in read-only mode). + /// + /// - readonly: Whether or not to open the database in a read-only state. + /// + /// Default: `false`. + /// + /// - Throws: `Result.Error` iff a connection cannot be established. + /// + /// - Returns: A new database connection. + public convenience init(_ filename: String, readonly: Bool = false) throws { + try self.init(.uri(filename), readonly: readonly) + } + + deinit { + sqlite3_close(handle) + } + + // MARK: - + + /// Whether or not the database was opened in a read-only state. + public var readonly: Bool { return sqlite3_db_readonly(handle, nil) == 1 } + + /// The last rowid inserted into the database via this connection. + public var lastInsertRowid: Int64 { + return sqlite3_last_insert_rowid(handle) + } + + /// The last number of changes (inserts, updates, or deletes) made to the + /// database via this connection. + public var changes: Int { + return Int(sqlite3_changes(handle)) + } + + /// The total number of changes (inserts, updates, or deletes) made to the + /// database via this connection. + public var totalChanges: Int { + return Int(sqlite3_total_changes(handle)) + } + + // MARK: - Execute + + /// Executes a batch of SQL statements. + /// + /// - Parameter SQL: A batch of zero or more semicolon-separated SQL + /// statements. + /// + /// - Throws: `Result.Error` if query execution fails. + public func execute(_ SQL: String) throws { + _ = try sync { try self.check(sqlite3_exec(self.handle, SQL, nil, nil, nil)) } + } + + // MARK: - Prepare + + /// Prepares a single SQL statement (with optional parameter bindings). + /// + /// - Parameters: + /// + /// - statement: A single SQL statement. + /// + /// - bindings: A list of parameters to bind to the statement. + /// + /// - Returns: A prepared statement. + public func prepare(_ statement: String, _ bindings: Binding?...) throws -> Statement { + if !bindings.isEmpty { return try prepare(statement, bindings) } + return try Statement(self, statement) + } + + /// Prepares a single SQL statement and binds parameters to it. + /// + /// - Parameters: + /// + /// - statement: A single SQL statement. + /// + /// - bindings: A list of parameters to bind to the statement. + /// + /// - Returns: A prepared statement. + public func prepare(_ statement: String, _ bindings: [Binding?]) throws -> Statement { + return try prepare(statement).bind(bindings) + } + + /// Prepares a single SQL statement and binds parameters to it. + /// + /// - Parameters: + /// + /// - statement: A single SQL statement. + /// + /// - bindings: A dictionary of named parameters to bind to the statement. + /// + /// - Returns: A prepared statement. + public func prepare(_ statement: String, _ bindings: [String: Binding?]) throws -> Statement { + return try prepare(statement).bind(bindings) + } + + // MARK: - Run + + /// Runs a single SQL statement (with optional parameter bindings). + /// + /// - Parameters: + /// + /// - statement: A single SQL statement. + /// + /// - bindings: A list of parameters to bind to the statement. + /// + /// - Throws: `Result.Error` if query execution fails. + /// + /// - Returns: The statement. + @discardableResult public func run(_ statement: String, _ bindings: Binding?...) throws -> Statement { + return try run(statement, bindings) + } + + /// Prepares, binds, and runs a single SQL statement. + /// + /// - Parameters: + /// + /// - statement: A single SQL statement. + /// + /// - bindings: A list of parameters to bind to the statement. + /// + /// - Throws: `Result.Error` if query execution fails. + /// + /// - Returns: The statement. + @discardableResult public func run(_ statement: String, _ bindings: [Binding?]) throws -> Statement { + return try prepare(statement).run(bindings) + } + + /// Prepares, binds, and runs a single SQL statement. + /// + /// - Parameters: + /// + /// - statement: A single SQL statement. + /// + /// - bindings: A dictionary of named parameters to bind to the statement. + /// + /// - Throws: `Result.Error` if query execution fails. + /// + /// - Returns: The statement. + @discardableResult public func run(_ statement: String, _ bindings: [String: Binding?]) throws -> Statement { + return try prepare(statement).run(bindings) + } + + // MARK: - Scalar + + /// Runs a single SQL statement (with optional parameter bindings), + /// returning the first value of the first row. + /// + /// - Parameters: + /// + /// - statement: A single SQL statement. + /// + /// - bindings: A list of parameters to bind to the statement. + /// + /// - Returns: The first value of the first row returned. + public func scalar(_ statement: String, _ bindings: Binding?...) throws -> Binding? { + return try scalar(statement, bindings) + } + + /// Runs a single SQL statement (with optional parameter bindings), + /// returning the first value of the first row. + /// + /// - Parameters: + /// + /// - statement: A single SQL statement. + /// + /// - bindings: A list of parameters to bind to the statement. + /// + /// - Returns: The first value of the first row returned. + public func scalar(_ statement: String, _ bindings: [Binding?]) throws -> Binding? { + return try prepare(statement).scalar(bindings) + } + + /// Runs a single SQL statement (with optional parameter bindings), + /// returning the first value of the first row. + /// + /// - Parameters: + /// + /// - statement: A single SQL statement. + /// + /// - bindings: A dictionary of named parameters to bind to the statement. + /// + /// - Returns: The first value of the first row returned. + public func scalar(_ statement: String, _ bindings: [String: Binding?]) throws -> Binding? { + return try prepare(statement).scalar(bindings) + } + + // MARK: - Transactions + + /// The mode in which a transaction acquires a lock. + public enum TransactionMode : String { + + /// Defers locking the database till the first read/write executes. + case deferred = "DEFERRED" + + /// Immediately acquires a reserved lock on the database. + case immediate = "IMMEDIATE" + + /// Immediately acquires an exclusive lock on all databases. + case exclusive = "EXCLUSIVE" + + } + + // TODO: Consider not requiring a throw to roll back? + /// Runs a transaction with the given mode. + /// + /// - Note: Transactions cannot be nested. To nest transactions, see + /// `savepoint()`, instead. + /// + /// - Parameters: + /// + /// - mode: The mode in which a transaction acquires a lock. + /// + /// Default: `.Deferred` + /// + /// - block: A closure to run SQL statements within the transaction. + /// The transaction will be committed when the block returns. The block + /// must throw to roll the transaction back. + /// + /// - Throws: `Result.Error`, and rethrows. + public func transaction(_ mode: TransactionMode = .deferred, block: @escaping () throws -> Void) throws { + try transaction("BEGIN \(mode.rawValue) TRANSACTION", block, "COMMIT TRANSACTION", or: "ROLLBACK TRANSACTION") + } + + // TODO: Consider not requiring a throw to roll back? + // TODO: Consider removing ability to set a name? + /// Runs a transaction with the given savepoint name (if omitted, it will + /// generate a UUID). + /// + /// - SeeAlso: `transaction()`. + /// + /// - Parameters: + /// + /// - savepointName: A unique identifier for the savepoint (optional). + /// + /// - block: A closure to run SQL statements within the transaction. + /// The savepoint will be released (committed) when the block returns. + /// The block must throw to roll the savepoint back. + /// + /// - Throws: `SQLite.Result.Error`, and rethrows. + public func savepoint(_ name: String = UUID().uuidString, block: @escaping () throws -> Void) throws { + let name = name.quote("'") + let savepoint = "SAVEPOINT \(name)" + + try transaction(savepoint, block, "RELEASE \(savepoint)", or: "ROLLBACK TO \(savepoint)") + } + + fileprivate func transaction(_ begin: String, _ block: @escaping () throws -> Void, _ commit: String, or rollback: String) throws { + return try sync { + try self.run(begin) + do { + try block() + } catch { + try self.run(rollback) + throw error + } + try self.run(commit) + } + } + + /// Interrupts any long-running queries. + public func interrupt() { + sqlite3_interrupt(handle) + } + + // MARK: - Handlers + + /// The number of seconds a connection will attempt to retry a statement + /// after encountering a busy signal (lock). + public var busyTimeout: Double = 0 { + didSet { + sqlite3_busy_timeout(handle, Int32(busyTimeout * 1_000)) + } + } + + /// Sets a handler to call after encountering a busy signal (lock). + /// + /// - Parameter callback: This block is executed during a lock in which a + /// busy error would otherwise be returned. It’s passed the number of + /// times it’s been called for this lock. If it returns `true`, it will + /// try again. If it returns `false`, no further attempts will be made. + public func busyHandler(_ callback: ((_ tries: Int) -> Bool)?) { + guard let callback = callback else { + sqlite3_busy_handler(handle, nil, nil) + busyHandler = nil + return + } + + let box: BusyHandler = { callback(Int($0)) ? 1 : 0 } + sqlite3_busy_handler(handle, { callback, tries in + unsafeBitCast(callback, to: BusyHandler.self)(tries) + }, unsafeBitCast(box, to: UnsafeMutableRawPointer.self)) + busyHandler = box + } + fileprivate typealias BusyHandler = @convention(block) (Int32) -> Int32 + fileprivate var busyHandler: BusyHandler? + + /// Sets a handler to call when a statement is executed with the compiled + /// SQL. + /// + /// - Parameter callback: This block is invoked when a statement is executed + /// with the compiled SQL as its argument. + /// + /// db.trace { SQL in print(SQL) } + public func trace(_ callback: ((String) -> Void)?) { + #if SQLITE_SWIFT_SQLCIPHER + trace_v1(callback) + #else + if #available(iOS 10.0, OSX 10.12, tvOS 10.0, watchOS 3.0, *) { + trace_v2(callback) + } else { + trace_v1(callback) + } + #endif + } + + fileprivate func trace_v1(_ callback: ((String) -> Void)?) { + guard let callback = callback else { + sqlite3_trace(handle, nil /* xCallback */, nil /* pCtx */) + trace = nil + return + } + let box: Trace = { (pointer: UnsafeRawPointer) in + callback(String(cString: pointer.assumingMemoryBound(to: UInt8.self))) + } + sqlite3_trace(handle, + { + (C: UnsafeMutableRawPointer?, SQL: UnsafePointer?) in + if let C = C, let SQL = SQL { + unsafeBitCast(C, to: Trace.self)(SQL) + } + }, + unsafeBitCast(box, to: UnsafeMutableRawPointer.self) + ) + trace = box + } + + + + + fileprivate typealias Trace = @convention(block) (UnsafeRawPointer) -> Void + fileprivate var trace: Trace? + + /// Registers a callback to be invoked whenever a row is inserted, updated, + /// or deleted in a rowid table. + /// + /// - Parameter callback: A callback invoked with the `Operation` (one of + /// `.Insert`, `.Update`, or `.Delete`), database name, table name, and + /// rowid. + public func updateHook(_ callback: ((_ operation: Operation, _ db: String, _ table: String, _ rowid: Int64) -> Void)?) { + guard let callback = callback else { + sqlite3_update_hook(handle, nil, nil) + updateHook = nil + return + } + + let box: UpdateHook = { + callback( + Operation(rawValue: $0), + String(cString: $1), + String(cString: $2), + $3 + ) + } + sqlite3_update_hook(handle, { callback, operation, db, table, rowid in + unsafeBitCast(callback, to: UpdateHook.self)(operation, db!, table!, rowid) + }, unsafeBitCast(box, to: UnsafeMutableRawPointer.self)) + updateHook = box + } + fileprivate typealias UpdateHook = @convention(block) (Int32, UnsafePointer, UnsafePointer, Int64) -> Void + fileprivate var updateHook: UpdateHook? + + /// Registers a callback to be invoked whenever a transaction is committed. + /// + /// - Parameter callback: A callback invoked whenever a transaction is + /// committed. If this callback throws, the transaction will be rolled + /// back. + public func commitHook(_ callback: (() throws -> Void)?) { + guard let callback = callback else { + sqlite3_commit_hook(handle, nil, nil) + commitHook = nil + return + } + + let box: CommitHook = { + do { + try callback() + } catch { + return 1 + } + return 0 + } + sqlite3_commit_hook(handle, { callback in + unsafeBitCast(callback, to: CommitHook.self)() + }, unsafeBitCast(box, to: UnsafeMutableRawPointer.self)) + commitHook = box + } + fileprivate typealias CommitHook = @convention(block) () -> Int32 + fileprivate var commitHook: CommitHook? + + /// Registers a callback to be invoked whenever a transaction rolls back. + /// + /// - Parameter callback: A callback invoked when a transaction is rolled + /// back. + public func rollbackHook(_ callback: (() -> Void)?) { + guard let callback = callback else { + sqlite3_rollback_hook(handle, nil, nil) + rollbackHook = nil + return + } + + let box: RollbackHook = { callback() } + sqlite3_rollback_hook(handle, { callback in + unsafeBitCast(callback, to: RollbackHook.self)() + }, unsafeBitCast(box, to: UnsafeMutableRawPointer.self)) + rollbackHook = box + } + fileprivate typealias RollbackHook = @convention(block) () -> Void + fileprivate var rollbackHook: RollbackHook? + + /// Creates or redefines a custom SQL function. + /// + /// - Parameters: + /// + /// - function: The name of the function to create or redefine. + /// + /// - argumentCount: The number of arguments that the function takes. If + /// `nil`, the function may take any number of arguments. + /// + /// Default: `nil` + /// + /// - deterministic: Whether or not the function is deterministic (_i.e._ + /// the function always returns the same result for a given input). + /// + /// Default: `false` + /// + /// - block: A block of code to run when the function is called. The block + /// is called with an array of raw SQL values mapped to the function’s + /// parameters and should return a raw SQL value (or nil). + public func createFunction(_ function: String, argumentCount: UInt? = nil, deterministic: Bool = false, _ block: @escaping (_ args: [Binding?]) -> Binding?) { + let argc = argumentCount.map { Int($0) } ?? -1 + let box: Function = { context, argc, argv in + let arguments: [Binding?] = (0..?) -> Void + fileprivate var functions = [String: [Int: Function]]() + + /// Defines a new collating sequence. + /// + /// - Parameters: + /// + /// - collation: The name of the collation added. + /// + /// - block: A collation function that takes two strings and returns the + /// comparison result. + public func createCollation(_ collation: String, _ block: @escaping (_ lhs: String, _ rhs: String) -> ComparisonResult) throws { + let box: Collation = { (lhs: UnsafeRawPointer, rhs: UnsafeRawPointer) in + let lstr = String(cString: lhs.assumingMemoryBound(to: UInt8.self)) + let rstr = String(cString: rhs.assumingMemoryBound(to: UInt8.self)) + return Int32(block(lstr, rstr).rawValue) + } + try check(sqlite3_create_collation_v2(handle, collation, SQLITE_UTF8, + unsafeBitCast(box, to: UnsafeMutableRawPointer.self), + { (callback: UnsafeMutableRawPointer?, _, lhs: UnsafeRawPointer?, _, rhs: UnsafeRawPointer?) in /* xCompare */ + if let lhs = lhs, let rhs = rhs { + return unsafeBitCast(callback, to: Collation.self)(lhs, rhs) + } else { + fatalError("sqlite3_create_collation_v2 callback called with NULL pointer") + } + }, nil /* xDestroy */)) + collations[collation] = box + } + fileprivate typealias Collation = @convention(block) (UnsafeRawPointer, UnsafeRawPointer) -> Int32 + fileprivate var collations = [String: Collation]() + + // MARK: - Error Handling + + func sync(_ block: @escaping () throws -> T) rethrows -> T { + var success: T? + var failure: Error? + + let box: () -> Void = { + do { + success = try block() + } catch { + failure = error + } + } + + if DispatchQueue.getSpecific(key: Connection.queueKey) == queueContext { + box() + } else { + queue.sync(execute: box) // FIXME: rdar://problem/21389236 + } + + if let failure = failure { + try { () -> Void in throw failure }() + } + + return success! + } + + @discardableResult func check(_ resultCode: Int32, statement: Statement? = nil) throws -> Int32 { + guard let error = Result(errorCode: resultCode, connection: self, statement: statement) else { + return resultCode + } + + throw error + } + + fileprivate var queue = DispatchQueue(label: "SQLite.Database", attributes: []) + + fileprivate static let queueKey = DispatchSpecificKey() + + fileprivate lazy var queueContext: Int = unsafeBitCast(self, to: Int.self) + +} + +extension Connection : CustomStringConvertible { + + public var description: String { + return String(cString: sqlite3_db_filename(handle, nil)) + } + +} + +extension Connection.Location : CustomStringConvertible { + + public var description: String { + switch self { + case .inMemory: + return ":memory:" + case .temporary: + return "" + case .uri(let URI): + return URI + } + } + +} + +public enum Result : Error { + + fileprivate static let successCodes: Set = [SQLITE_OK, SQLITE_ROW, SQLITE_DONE] + + case error(message: String, code: Int32, statement: Statement?) + + init?(errorCode: Int32, connection: Connection, statement: Statement? = nil) { + guard !Result.successCodes.contains(errorCode) else { return nil } + + let message = String(cString: sqlite3_errmsg(connection.handle)) + self = .error(message: message, code: errorCode, statement: statement) + } + +} + +extension Result : CustomStringConvertible { + + public var description: String { + switch self { + case let .error(message, errorCode, statement): + if let statement = statement { + return "\(message) (\(statement)) (code: \(errorCode))" + } else { + return "\(message) (code: \(errorCode))" + } + } + } +} + +#if !SQLITE_SWIFT_SQLCIPHER +@available(iOS 10.0, OSX 10.12, tvOS 10.0, watchOS 3.0, *) +extension Connection { + fileprivate func trace_v2(_ callback: ((String) -> Void)?) { + guard let callback = callback else { + // If the X callback is NULL or if the M mask is zero, then tracing is disabled. + sqlite3_trace_v2(handle, 0 /* mask */, nil /* xCallback */, nil /* pCtx */) + trace = nil + return + } + + let box: Trace = { (pointer: UnsafeRawPointer) in + callback(String(cString: pointer.assumingMemoryBound(to: UInt8.self))) + } + sqlite3_trace_v2(handle, + UInt32(SQLITE_TRACE_STMT) /* mask */, + { + // A trace callback is invoked with four arguments: callback(T,C,P,X). + // The T argument is one of the SQLITE_TRACE constants to indicate why the + // callback was invoked. The C argument is a copy of the context pointer. + // The P and X arguments are pointers whose meanings depend on T. + (T: UInt32, C: UnsafeMutableRawPointer?, P: UnsafeMutableRawPointer?, X: UnsafeMutableRawPointer?) in + if let P = P, + let expandedSQL = sqlite3_expanded_sql(OpaquePointer(P)) { + unsafeBitCast(C, to: Trace.self)(expandedSQL) + sqlite3_free(expandedSQL) + } + return Int32(0) // currently ignored + }, + unsafeBitCast(box, to: UnsafeMutableRawPointer.self) /* pCtx */ + ) + trace = box + } +} +#endif diff --git a/mobile/ios/ThirdParty/SQLite.swift/Sources/SQLite/Core/Statement.swift b/mobile/ios/ThirdParty/SQLite.swift/Sources/SQLite/Core/Statement.swift new file mode 100644 index 0000000000..b14bc13c42 --- /dev/null +++ b/mobile/ios/ThirdParty/SQLite.swift/Sources/SQLite/Core/Statement.swift @@ -0,0 +1,297 @@ +// +// SQLite.swift +// https://github.com/stephencelis/SQLite.swift +// Copyright © 2014-2015 Stephen Celis. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +#if SQLITE_SWIFT_STANDALONE +import sqlite3 +#elseif SQLITE_SWIFT_SQLCIPHER +import SQLCipher +#else +import SQLite3 +#endif + +/// A single SQL statement. +public final class Statement { + + fileprivate var handle: OpaquePointer? = nil + + fileprivate let connection: Connection + + init(_ connection: Connection, _ SQL: String) throws { + self.connection = connection + try connection.check(sqlite3_prepare_v2(connection.handle, SQL, -1, &handle, nil)) + } + + deinit { + sqlite3_finalize(handle) + } + + public lazy var columnCount: Int = Int(sqlite3_column_count(self.handle)) + + public lazy var columnNames: [String] = (0.. Statement { + return bind(values) + } + + /// Binds a list of parameters to a statement. + /// + /// - Parameter values: A list of parameters to bind to the statement. + /// + /// - Returns: The statement object (useful for chaining). + public func bind(_ values: [Binding?]) -> Statement { + if values.isEmpty { return self } + reset() + guard values.count == Int(sqlite3_bind_parameter_count(handle)) else { + fatalError("\(sqlite3_bind_parameter_count(handle)) values expected, \(values.count) passed") + } + for idx in 1...values.count { bind(values[idx - 1], atIndex: idx) } + return self + } + + /// Binds a dictionary of named parameters to a statement. + /// + /// - Parameter values: A dictionary of named parameters to bind to the + /// statement. + /// + /// - Returns: The statement object (useful for chaining). + public func bind(_ values: [String: Binding?]) -> Statement { + reset() + for (name, value) in values { + let idx = sqlite3_bind_parameter_index(handle, name) + guard idx > 0 else { + fatalError("parameter not found: \(name)") + } + bind(value, atIndex: Int(idx)) + } + return self + } + + fileprivate func bind(_ value: Binding?, atIndex idx: Int) { + if value == nil { + sqlite3_bind_null(handle, Int32(idx)) + } else if let value = value as? Blob { + sqlite3_bind_blob(handle, Int32(idx), value.bytes, Int32(value.bytes.count), SQLITE_TRANSIENT) + } else if let value = value as? Double { + sqlite3_bind_double(handle, Int32(idx), value) + } else if let value = value as? Int64 { + sqlite3_bind_int64(handle, Int32(idx), value) + } else if let value = value as? String { + sqlite3_bind_text(handle, Int32(idx), value, -1, SQLITE_TRANSIENT) + } else if let value = value as? Int { + self.bind(value.datatypeValue, atIndex: idx) + } else if let value = value as? Bool { + self.bind(value.datatypeValue, atIndex: idx) + } else if let value = value { + fatalError("tried to bind unexpected value \(value)") + } + } + + /// - Parameter bindings: A list of parameters to bind to the statement. + /// + /// - Throws: `Result.Error` if query execution fails. + /// + /// - Returns: The statement object (useful for chaining). + @discardableResult public func run(_ bindings: Binding?...) throws -> Statement { + guard bindings.isEmpty else { + return try run(bindings) + } + + reset(clearBindings: false) + repeat {} while try step() + return self + } + + /// - Parameter bindings: A list of parameters to bind to the statement. + /// + /// - Throws: `Result.Error` if query execution fails. + /// + /// - Returns: The statement object (useful for chaining). + @discardableResult public func run(_ bindings: [Binding?]) throws -> Statement { + return try bind(bindings).run() + } + + /// - Parameter bindings: A dictionary of named parameters to bind to the + /// statement. + /// + /// - Throws: `Result.Error` if query execution fails. + /// + /// - Returns: The statement object (useful for chaining). + @discardableResult public func run(_ bindings: [String: Binding?]) throws -> Statement { + return try bind(bindings).run() + } + + /// - Parameter bindings: A list of parameters to bind to the statement. + /// + /// - Returns: The first value of the first row returned. + public func scalar(_ bindings: Binding?...) throws -> Binding? { + guard bindings.isEmpty else { + return try scalar(bindings) + } + + reset(clearBindings: false) + _ = try step() + return row[0] + } + + /// - Parameter bindings: A list of parameters to bind to the statement. + /// + /// - Returns: The first value of the first row returned. + public func scalar(_ bindings: [Binding?]) throws -> Binding? { + return try bind(bindings).scalar() + } + + + /// - Parameter bindings: A dictionary of named parameters to bind to the + /// statement. + /// + /// - Returns: The first value of the first row returned. + public func scalar(_ bindings: [String: Binding?]) throws -> Binding? { + return try bind(bindings).scalar() + } + + public func step() throws -> Bool { + return try connection.sync { try self.connection.check(sqlite3_step(self.handle)) == SQLITE_ROW } + } + + fileprivate func reset(clearBindings shouldClear: Bool = true) { + sqlite3_reset(handle) + if (shouldClear) { sqlite3_clear_bindings(handle) } + } + +} + +extension Statement : Sequence { + + public func makeIterator() -> Statement { + reset(clearBindings: false) + return self + } + +} + +extension Statement : IteratorProtocol { + + public func next() -> [Binding?]? { + return try! step() ? Array(row) : nil + } + +} + +extension Statement : CustomStringConvertible { + + public var description: String { + return String(cString: sqlite3_sql(handle)) + } + +} + +public struct Cursor { + + fileprivate let handle: OpaquePointer + + fileprivate let columnCount: Int + + fileprivate init(_ statement: Statement) { + handle = statement.handle! + columnCount = statement.columnCount + } + + public subscript(idx: Int) -> Double { + return sqlite3_column_double(handle, Int32(idx)) + } + + public subscript(idx: Int) -> Int64 { + return sqlite3_column_int64(handle, Int32(idx)) + } + + public subscript(idx: Int) -> String { + return String(cString: UnsafePointer(sqlite3_column_text(handle, Int32(idx)))) + } + + public subscript(idx: Int) -> Blob { + if let pointer = sqlite3_column_blob(handle, Int32(idx)) { + let length = Int(sqlite3_column_bytes(handle, Int32(idx))) + return Blob(bytes: pointer, length: length) + } else { + // The return value from sqlite3_column_blob() for a zero-length BLOB is a NULL pointer. + // https://www.sqlite.org/c3ref/column_blob.html + return Blob(bytes: []) + } + } + + // MARK: - + + public subscript(idx: Int) -> Bool { + return Bool.fromDatatypeValue(self[idx]) + } + + public subscript(idx: Int) -> Int { + return Int.fromDatatypeValue(self[idx]) + } + +} + +/// Cursors provide direct access to a statement’s current row. +extension Cursor : Sequence { + + public subscript(idx: Int) -> Binding? { + switch sqlite3_column_type(handle, Int32(idx)) { + case SQLITE_BLOB: + return self[idx] as Blob + case SQLITE_FLOAT: + return self[idx] as Double + case SQLITE_INTEGER: + return self[idx] as Int64 + case SQLITE_NULL: + return nil + case SQLITE_TEXT: + return self[idx] as String + case let type: + fatalError("unsupported column type: \(type)") + } + } + + public func makeIterator() -> AnyIterator { + var idx = 0 + return AnyIterator { + if idx >= self.columnCount { + return Optional.none + } else { + idx += 1 + return self[idx - 1] + } + } + } + +} diff --git a/mobile/ios/ThirdParty/SQLite.swift/Sources/SQLite/Core/Value.swift b/mobile/ios/ThirdParty/SQLite.swift/Sources/SQLite/Core/Value.swift new file mode 100644 index 0000000000..608f0ce6fe --- /dev/null +++ b/mobile/ios/ThirdParty/SQLite.swift/Sources/SQLite/Core/Value.swift @@ -0,0 +1,132 @@ +// +// SQLite.swift +// https://github.com/stephencelis/SQLite.swift +// Copyright © 2014-2015 Stephen Celis. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +/// - Warning: `Binding` is a protocol that SQLite.swift uses internally to +/// directly map SQLite types to Swift types. +/// +/// Do not conform custom types to the Binding protocol. See the `Value` +/// protocol, instead. +public protocol Binding {} + +public protocol Number : Binding {} + +public protocol Value : Expressible { // extensions cannot have inheritance clauses + + associatedtype ValueType = Self + + associatedtype Datatype : Binding + + static var declaredDatatype: String { get } + + static func fromDatatypeValue(_ datatypeValue: Datatype) -> ValueType + + var datatypeValue: Datatype { get } + +} + +extension Double : Number, Value { + + public static let declaredDatatype = "REAL" + + public static func fromDatatypeValue(_ datatypeValue: Double) -> Double { + return datatypeValue + } + + public var datatypeValue: Double { + return self + } + +} + +extension Int64 : Number, Value { + + public static let declaredDatatype = "INTEGER" + + public static func fromDatatypeValue(_ datatypeValue: Int64) -> Int64 { + return datatypeValue + } + + public var datatypeValue: Int64 { + return self + } + +} + +extension String : Binding, Value { + + public static let declaredDatatype = "TEXT" + + public static func fromDatatypeValue(_ datatypeValue: String) -> String { + return datatypeValue + } + + public var datatypeValue: String { + return self + } + +} + +extension Blob : Binding, Value { + + public static let declaredDatatype = "BLOB" + + public static func fromDatatypeValue(_ datatypeValue: Blob) -> Blob { + return datatypeValue + } + + public var datatypeValue: Blob { + return self + } + +} + +// MARK: - + +extension Bool : Binding, Value { + + public static var declaredDatatype = Int64.declaredDatatype + + public static func fromDatatypeValue(_ datatypeValue: Int64) -> Bool { + return datatypeValue != 0 + } + + public var datatypeValue: Int64 { + return self ? 1 : 0 + } + +} + +extension Int : Number, Value { + + public static var declaredDatatype = Int64.declaredDatatype + + public static func fromDatatypeValue(_ datatypeValue: Int64) -> Int { + return Int(datatypeValue) + } + + public var datatypeValue: Int64 { + return Int64(self) + } + +} diff --git a/mobile/ios/ThirdParty/SQLite.swift/Sources/SQLite/Extensions/Cipher.swift b/mobile/ios/ThirdParty/SQLite.swift/Sources/SQLite/Extensions/Cipher.swift new file mode 100644 index 0000000000..6c0d465781 --- /dev/null +++ b/mobile/ios/ThirdParty/SQLite.swift/Sources/SQLite/Extensions/Cipher.swift @@ -0,0 +1,61 @@ +#if SQLITE_SWIFT_SQLCIPHER +import SQLCipher + + +/// Extension methods for [SQLCipher](https://www.zetetic.net/sqlcipher/). +/// @see [sqlcipher api](https://www.zetetic.net/sqlcipher/sqlcipher-api/) +extension Connection { + + /// Specify the key for an encrypted database. This routine should be + /// called right after sqlite3_open(). + /// + /// @param key The key to use.The key itself can be a passphrase, which is converted to a key + /// using [PBKDF2](https://en.wikipedia.org/wiki/PBKDF2) key derivation. The result + /// is used as the encryption key for the database. + /// + /// Alternatively, it is possible to specify an exact byte sequence using a blob literal. + /// With this method, it is the calling application's responsibility to ensure that the data + /// provided is a 64 character hex string, which will be converted directly to 32 bytes (256 bits) + /// of key data. + /// e.g. x'2DD29CA851E7B56E4697B0E1F08507293D761A05CE4D1B628663F411A8086D99' + /// @param db name of the database, defaults to 'main' + public func key(_ key: String, db: String = "main") throws { + try _key_v2(db: db, keyPointer: key, keySize: key.utf8.count) + } + + public func key(_ key: Blob, db: String = "main") throws { + try _key_v2(db: db, keyPointer: key.bytes, keySize: key.bytes.count) + } + + + /// Change the key on an open database. If the current database is not encrypted, this routine + /// will encrypt it. + /// To change the key on an existing encrypted database, it must first be unlocked with the + /// current encryption key. Once the database is readable and writeable, rekey can be used + /// to re-encrypt every page in the database with a new key. + public func rekey(_ key: String, db: String = "main") throws { + try _rekey_v2(db: db, keyPointer: key, keySize: key.utf8.count) + } + + public func rekey(_ key: Blob, db: String = "main") throws { + try _rekey_v2(db: db, keyPointer: key.bytes, keySize: key.bytes.count) + } + + // MARK: - private + private func _key_v2(db: String, keyPointer: UnsafePointer, keySize: Int) throws { + try check(sqlite3_key_v2(handle, db, keyPointer, Int32(keySize))) + try cipher_key_check() + } + + private func _rekey_v2(db: String, keyPointer: UnsafePointer, keySize: Int) throws { + try check(sqlite3_rekey_v2(handle, db, keyPointer, Int32(keySize))) + } + + // When opening an existing database, sqlite3_key_v2 will not immediately throw an error if + // the key provided is incorrect. To test that the database can be successfully opened with the + // provided key, it is necessary to perform some operation on the database (i.e. read from it). + private func cipher_key_check() throws { + try scalar("SELECT count(*) FROM sqlite_master;") + } +} +#endif diff --git a/mobile/ios/ThirdParty/SQLite.swift/Sources/SQLite/Extensions/FTS4.swift b/mobile/ios/ThirdParty/SQLite.swift/Sources/SQLite/Extensions/FTS4.swift new file mode 100644 index 0000000000..152a29219e --- /dev/null +++ b/mobile/ios/ThirdParty/SQLite.swift/Sources/SQLite/Extensions/FTS4.swift @@ -0,0 +1,346 @@ +// +// SQLite.swift +// https://github.com/stephencelis/SQLite.swift +// Copyright © 2014-2015 Stephen Celis. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +#if SWIFT_PACKAGE +import SQLiteObjc +#endif + +extension Module { + + public static func FTS4(_ column: Expressible, _ more: Expressible...) -> Module { + return FTS4([column] + more) + } + + public static func FTS4(_ columns: [Expressible] = [], tokenize tokenizer: Tokenizer? = nil) -> Module { + return FTS4(FTS4Config().columns(columns).tokenizer(tokenizer)) + } + + public static func FTS4(_ config: FTS4Config) -> Module { + return Module(name: "fts4", arguments: config.arguments()) + } +} + +extension VirtualTable { + + /// Builds an expression appended with a `MATCH` query against the given + /// pattern. + /// + /// let emails = VirtualTable("emails") + /// + /// emails.filter(emails.match("Hello")) + /// // SELECT * FROM "emails" WHERE "emails" MATCH 'Hello' + /// + /// - Parameter pattern: A pattern to match. + /// + /// - Returns: An expression appended with a `MATCH` query against the given + /// pattern. + public func match(_ pattern: String) -> Expression { + return "MATCH".infix(tableName(), pattern) + } + + public func match(_ pattern: Expression) -> Expression { + return "MATCH".infix(tableName(), pattern) + } + + public func match(_ pattern: Expression) -> Expression { + return "MATCH".infix(tableName(), pattern) + } + + /// Builds a copy of the query with a `WHERE … MATCH` clause. + /// + /// let emails = VirtualTable("emails") + /// + /// emails.match("Hello") + /// // SELECT * FROM "emails" WHERE "emails" MATCH 'Hello' + /// + /// - Parameter pattern: A pattern to match. + /// + /// - Returns: A query with the given `WHERE … MATCH` clause applied. + public func match(_ pattern: String) -> QueryType { + return filter(match(pattern)) + } + + public func match(_ pattern: Expression) -> QueryType { + return filter(match(pattern)) + } + + public func match(_ pattern: Expression) -> QueryType { + return filter(match(pattern)) + } + +} + +public struct Tokenizer { + + public static let Simple = Tokenizer("simple") + + public static let Porter = Tokenizer("porter") + + public static func Unicode61(removeDiacritics: Bool? = nil, tokenchars: Set = [], separators: Set = []) -> Tokenizer { + var arguments = [String]() + + if let removeDiacritics = removeDiacritics { + arguments.append("removeDiacritics=\(removeDiacritics ? 1 : 0)".quote()) + } + + if !tokenchars.isEmpty { + let joined = tokenchars.map { String($0) }.joined(separator: "") + arguments.append("tokenchars=\(joined)".quote()) + } + + if !separators.isEmpty { + let joined = separators.map { String($0) }.joined(separator: "") + arguments.append("separators=\(joined)".quote()) + } + + return Tokenizer("unicode61", arguments) + } + + public static func Custom(_ name: String) -> Tokenizer { + return Tokenizer(Tokenizer.moduleName.quote(), [name.quote()]) + } + + public let name: String + + public let arguments: [String] + + fileprivate init(_ name: String, _ arguments: [String] = []) { + self.name = name + self.arguments = arguments + } + + fileprivate static let moduleName = "SQLite.swift" + +} + +extension Tokenizer : CustomStringConvertible { + + public var description: String { + return ([name] + arguments).joined(separator: " ") + } + +} + +extension Connection { + + public func registerTokenizer(_ submoduleName: String, next: @escaping (String) -> (String, Range)?) throws { + try check(_SQLiteRegisterTokenizer(handle, Tokenizer.moduleName, submoduleName) { input, offset, length in + let string = String(cString: input) + + guard let (token, range) = next(string) else { return nil } + + let view = string.utf8 + offset.pointee += string.substring(to: range.lowerBound).utf8.count + length.pointee = Int32(view.distance(from: range.lowerBound.samePosition(in: view), to: range.upperBound.samePosition(in: view))) + return token + }) + } + +} + +/// Configuration options shared between the [FTS4](https://www.sqlite.org/fts3.html) and +/// [FTS5](https://www.sqlite.org/fts5.html) extensions. +open class FTSConfig { + public enum ColumnOption { + /// [The notindexed= option](https://www.sqlite.org/fts3.html#section_6_5) + case unindexed + } + + typealias ColumnDefinition = (Expressible, options: [ColumnOption]) + var columnDefinitions = [ColumnDefinition]() + var tokenizer: Tokenizer? + var prefixes = [Int]() + var externalContentSchema: SchemaType? + var isContentless: Bool = false + + /// Adds a column definition + @discardableResult open func column(_ column: Expressible, _ options: [ColumnOption] = []) -> Self { + self.columnDefinitions.append((column, options)) + return self + } + + @discardableResult open func columns(_ columns: [Expressible]) -> Self { + for column in columns { + self.column(column) + } + return self + } + + /// [Tokenizers](https://www.sqlite.org/fts3.html#tokenizer) + open func tokenizer(_ tokenizer: Tokenizer?) -> Self { + self.tokenizer = tokenizer + return self + } + + /// [The prefix= option](https://www.sqlite.org/fts3.html#section_6_6) + open func prefix(_ prefix: [Int]) -> Self { + self.prefixes += prefix + return self + } + + /// [The content= option](https://www.sqlite.org/fts3.html#section_6_2) + open func externalContent(_ schema: SchemaType) -> Self { + self.externalContentSchema = schema + return self + } + + /// [Contentless FTS4 Tables](https://www.sqlite.org/fts3.html#section_6_2_1) + open func contentless() -> Self { + self.isContentless = true + return self + } + + func formatColumnDefinitions() -> [Expressible] { + return columnDefinitions.map { $0.0 } + } + + func arguments() -> [Expressible] { + return options().arguments + } + + func options() -> Options { + var options = Options() + options.append(formatColumnDefinitions()) + if let tokenizer = tokenizer { + options.append("tokenize", value: Expression(literal: tokenizer.description)) + } + options.appendCommaSeparated("prefix", values:prefixes.sorted().map { String($0) }) + if isContentless { + options.append("content", value: "") + } else if let externalContentSchema = externalContentSchema { + options.append("content", value: externalContentSchema.tableName()) + } + return options + } + + struct Options { + var arguments = [Expressible]() + + @discardableResult mutating func append(_ columns: [Expressible]) -> Options { + arguments.append(contentsOf: columns) + return self + } + + @discardableResult mutating func appendCommaSeparated(_ key: String, values: [String]) -> Options { + if values.isEmpty { + return self + } else { + return append(key, value: values.joined(separator: ",")) + } + } + + @discardableResult mutating func append(_ key: String, value: CustomStringConvertible?) -> Options { + return append(key, value: value?.description) + } + + @discardableResult mutating func append(_ key: String, value: String?) -> Options { + return append(key, value: value.map { Expression($0) }) + } + + @discardableResult mutating func append(_ key: String, value: Expressible?) -> Options { + if let value = value { + arguments.append("=".join([Expression(literal: key), value])) + } + return self + } + } +} + +/// Configuration for the [FTS4](https://www.sqlite.org/fts3.html) extension. +open class FTS4Config : FTSConfig { + /// [The matchinfo= option](https://www.sqlite.org/fts3.html#section_6_4) + public enum MatchInfo : CustomStringConvertible { + case fts3 + public var description: String { + return "fts3" + } + } + + /// [FTS4 options](https://www.sqlite.org/fts3.html#fts4_options) + public enum Order : CustomStringConvertible { + /// Data structures are optimized for returning results in ascending order by docid (default) + case asc + /// FTS4 stores its data in such a way as to optimize returning results in descending order by docid. + case desc + + public var description: String { + switch self { + case .asc: return "asc" + case .desc: return "desc" + } + } + } + + var compressFunction: String? + var uncompressFunction: String? + var languageId: String? + var matchInfo: MatchInfo? + var order: Order? + + override public init() { + } + + /// [The compress= and uncompress= options](https://www.sqlite.org/fts3.html#section_6_1) + open func compress(_ functionName: String) -> Self { + self.compressFunction = functionName + return self + } + + /// [The compress= and uncompress= options](https://www.sqlite.org/fts3.html#section_6_1) + open func uncompress(_ functionName: String) -> Self { + self.uncompressFunction = functionName + return self + } + + /// [The languageid= option](https://www.sqlite.org/fts3.html#section_6_3) + open func languageId(_ columnName: String) -> Self { + self.languageId = columnName + return self + } + + /// [The matchinfo= option](https://www.sqlite.org/fts3.html#section_6_4) + open func matchInfo(_ matchInfo: MatchInfo) -> Self { + self.matchInfo = matchInfo + return self + } + + /// [FTS4 options](https://www.sqlite.org/fts3.html#fts4_options) + open func order(_ order: Order) -> Self { + self.order = order + return self + } + + override func options() -> Options { + var options = super.options() + for (column, _) in (columnDefinitions.filter { $0.options.contains(.unindexed) }) { + options.append("notindexed", value: column) + } + options.append("languageid", value: languageId) + options.append("compress", value: compressFunction) + options.append("uncompress", value: uncompressFunction) + options.append("matchinfo", value: matchInfo) + options.append("order", value: order) + return options + } +} diff --git a/mobile/ios/ThirdParty/SQLite.swift/Sources/SQLite/Extensions/FTS5.swift b/mobile/ios/ThirdParty/SQLite.swift/Sources/SQLite/Extensions/FTS5.swift new file mode 100644 index 0000000000..763927ff48 --- /dev/null +++ b/mobile/ios/ThirdParty/SQLite.swift/Sources/SQLite/Extensions/FTS5.swift @@ -0,0 +1,97 @@ +// +// SQLite.swift +// https://github.com/stephencelis/SQLite.swift +// Copyright © 2014-2015 Stephen Celis. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +extension Module { + public static func FTS5(_ config: FTS5Config) -> Module { + return Module(name: "fts5", arguments: config.arguments()) + } +} + +/// Configuration for the [FTS5](https://www.sqlite.org/fts5.html) extension. +/// +/// **Note:** this is currently only applicable when using SQLite.swift together with a FTS5-enabled version +/// of SQLite. +open class FTS5Config : FTSConfig { + public enum Detail : CustomStringConvertible { + /// store rowid, column number, term offset + case full + /// store rowid, column number + case column + /// store rowid + case none + + public var description: String { + switch self { + case .full: return "full" + case .column: return "column" + case .none: return "none" + } + } + } + + var detail: Detail? + var contentRowId: Expressible? + var columnSize: Int? + + override public init() { + } + + /// [External Content Tables](https://www.sqlite.org/fts5.html#section_4_4_2) + open func contentRowId(_ column: Expressible) -> Self { + self.contentRowId = column + return self + } + + /// [The Columnsize Option](https://www.sqlite.org/fts5.html#section_4_5) + open func columnSize(_ size: Int) -> Self { + self.columnSize = size + return self + } + + /// [The Detail Option](https://www.sqlite.org/fts5.html#section_4_6) + open func detail(_ detail: Detail) -> Self { + self.detail = detail + return self + } + + override func options() -> Options { + var options = super.options() + options.append("content_rowid", value: contentRowId) + if let columnSize = columnSize { + options.append("columnsize", value: Expression(value: columnSize)) + } + options.append("detail", value: detail) + return options + } + + override func formatColumnDefinitions() -> [Expressible] { + return columnDefinitions.map { definition in + if definition.options.contains(.unindexed) { + return " ".join([definition.0, Expression(literal: "UNINDEXED")]) + } else { + return definition.0 + } + } + } +} diff --git a/mobile/ios/ThirdParty/SQLite.swift/Sources/SQLite/Extensions/RTree.swift b/mobile/ios/ThirdParty/SQLite.swift/Sources/SQLite/Extensions/RTree.swift new file mode 100644 index 0000000000..4fc1a23539 --- /dev/null +++ b/mobile/ios/ThirdParty/SQLite.swift/Sources/SQLite/Extensions/RTree.swift @@ -0,0 +1,37 @@ +// +// SQLite.swift +// https://github.com/stephencelis/SQLite.swift +// Copyright © 2014-2015 Stephen Celis. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +extension Module { + + public static func RTree(_ primaryKey: Expression, _ pairs: (Expression, Expression)...) -> Module where T.Datatype == Int64, U.Datatype == Double { + var arguments: [Expressible] = [primaryKey] + + for pair in pairs { + arguments.append(contentsOf: [pair.0, pair.1] as [Expressible]) + } + + return Module(name: "rtree", arguments: arguments) + } + +} diff --git a/mobile/ios/ThirdParty/SQLite.swift/Sources/SQLite/Foundation.swift b/mobile/ios/ThirdParty/SQLite.swift/Sources/SQLite/Foundation.swift new file mode 100644 index 0000000000..5e102297b9 --- /dev/null +++ b/mobile/ios/ThirdParty/SQLite.swift/Sources/SQLite/Foundation.swift @@ -0,0 +1,108 @@ +// +// SQLite.swift +// https://github.com/stephencelis/SQLite.swift +// Copyright © 2014-2015 Stephen Celis. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import Foundation + +extension Data : Value { + + public static var declaredDatatype: String { + return Blob.declaredDatatype + } + + public static func fromDatatypeValue(_ dataValue: Blob) -> Data { + return Data(bytes: dataValue.bytes) + } + + public var datatypeValue: Blob { + return withUnsafeBytes { (pointer: UnsafePointer) -> Blob in + return Blob(bytes: pointer, length: count) + } + } + +} + +extension Date : Value { + + public static var declaredDatatype: String { + return String.declaredDatatype + } + + public static func fromDatatypeValue(_ stringValue: String) -> Date { + return dateFormatter.date(from: stringValue)! + } + + public var datatypeValue: String { + return dateFormatter.string(from: self) + } + +} + +/// A global date formatter used to serialize and deserialize `NSDate` objects. +/// If multiple date formats are used in an application’s database(s), use a +/// custom `Value` type per additional format. +public var dateFormatter: DateFormatter = { + let formatter = DateFormatter() + formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSS" + formatter.locale = Locale(identifier: "en_US_POSIX") + formatter.timeZone = TimeZone(secondsFromGMT: 0) + return formatter +}() + +// FIXME: rdar://problem/18673897 // subscript… + +extension QueryType { + + public subscript(column: Expression) -> Expression { + return namespace(column) + } + public subscript(column: Expression) -> Expression { + return namespace(column) + } + + public subscript(column: Expression) -> Expression { + return namespace(column) + } + public subscript(column: Expression) -> Expression { + return namespace(column) + } + +} + +extension Row { + + public subscript(column: Expression) -> Data { + return get(column) + } + public subscript(column: Expression) -> Data? { + return get(column) + } + + public subscript(column: Expression) -> Date { + return get(column) + } + public subscript(column: Expression) -> Date? { + return get(column) + } + +} diff --git a/mobile/ios/ThirdParty/SQLite.swift/Sources/SQLite/Helpers.swift b/mobile/ios/ThirdParty/SQLite.swift/Sources/SQLite/Helpers.swift new file mode 100644 index 0000000000..27904793a2 --- /dev/null +++ b/mobile/ios/ThirdParty/SQLite.swift/Sources/SQLite/Helpers.swift @@ -0,0 +1,130 @@ +// +// SQLite.swift +// https://github.com/stephencelis/SQLite.swift +// Copyright © 2014-2015 Stephen Celis. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +#if SQLITE_SWIFT_STANDALONE +import sqlite3 +#elseif SQLITE_SWIFT_SQLCIPHER +import SQLCipher +#else +import SQLite3 +#endif + +public typealias Star = (Expression?, Expression?) -> Expression + +public func *(_: Expression?, _: Expression?) -> Expression { + return Expression(literal: "*") +} + +public protocol _OptionalType { + + associatedtype WrappedType + +} + +extension Optional : _OptionalType { + + public typealias WrappedType = Wrapped + +} + +// let SQLITE_STATIC = unsafeBitCast(0, sqlite3_destructor_type.self) +let SQLITE_TRANSIENT = unsafeBitCast(-1, to: sqlite3_destructor_type.self) + +extension String { + + func quote(_ mark: Character = "\"") -> String { + let escaped = characters.reduce("") { string, character in + string + (character == mark ? "\(mark)\(mark)" : "\(character)") + } + return "\(mark)\(escaped)\(mark)" + } + + func join(_ expressions: [Expressible]) -> Expressible { + var (template, bindings) = ([String](), [Binding?]()) + for expressible in expressions { + let expression = expressible.expression + template.append(expression.template) + bindings.append(contentsOf: expression.bindings) + } + return Expression(template.joined(separator: self), bindings) + } + + func infix(_ lhs: Expressible, _ rhs: Expressible, wrap: Bool = true) -> Expression { + let expression = Expression(" \(self) ".join([lhs, rhs]).expression) + guard wrap else { + return expression + } + return "".wrap(expression) + } + + func prefix(_ expressions: Expressible) -> Expressible { + return "\(self) ".wrap(expressions) as Expression + } + + func prefix(_ expressions: [Expressible]) -> Expressible { + return "\(self) ".wrap(expressions) as Expression + } + + func wrap(_ expression: Expressible) -> Expression { + return Expression("\(self)(\(expression.expression.template))", expression.expression.bindings) + } + + func wrap(_ expressions: [Expressible]) -> Expression { + return wrap(", ".join(expressions)) + } + +} + +func infix(_ lhs: Expressible, _ rhs: Expressible, wrap: Bool = true, function: String = #function) -> Expression { + return function.infix(lhs, rhs, wrap: wrap) +} + +func wrap(_ expression: Expressible, function: String = #function) -> Expression { + return function.wrap(expression) +} + +func wrap(_ expressions: [Expressible], function: String = #function) -> Expression { + return function.wrap(", ".join(expressions)) +} + +func transcode(_ literal: Binding?) -> String { + guard let literal = literal else { return "NULL" } + + switch literal { + case let blob as Blob: + return blob.description + case let string as String: + return string.quote("'") + case let binding: + return "\(binding)" + } +} + +func value(_ v: Binding) -> A { + return A.fromDatatypeValue(v as! A.Datatype) as! A +} + +func value(_ v: Binding?) -> A { + return value(v!) +} diff --git a/mobile/ios/ThirdParty/SQLite.swift/Sources/SQLite/Info.plist b/mobile/ios/ThirdParty/SQLite.swift/Sources/SQLite/Info.plist new file mode 100644 index 0000000000..588139c437 --- /dev/null +++ b/mobile/ios/ThirdParty/SQLite.swift/Sources/SQLite/Info.plist @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + FMWK + CFBundleShortVersionString + 0.11.2 + CFBundleSignature + ???? + CFBundleVersion + $(CURRENT_PROJECT_VERSION) + NSPrincipalClass + + + diff --git a/mobile/ios/ThirdParty/SQLite.swift/Sources/SQLite/SQLite.h b/mobile/ios/ThirdParty/SQLite.swift/Sources/SQLite/SQLite.h new file mode 100644 index 0000000000..693ce323cf --- /dev/null +++ b/mobile/ios/ThirdParty/SQLite.swift/Sources/SQLite/SQLite.h @@ -0,0 +1,6 @@ +@import Foundation; + +FOUNDATION_EXPORT double SQLiteVersionNumber; +FOUNDATION_EXPORT const unsigned char SQLiteVersionString[]; + +#import diff --git a/mobile/ios/ThirdParty/SQLite.swift/Sources/SQLite/Typed/AggregateFunctions.swift b/mobile/ios/ThirdParty/SQLite.swift/Sources/SQLite/Typed/AggregateFunctions.swift new file mode 100644 index 0000000000..249bbe60cd --- /dev/null +++ b/mobile/ios/ThirdParty/SQLite.swift/Sources/SQLite/Typed/AggregateFunctions.swift @@ -0,0 +1,251 @@ +// +// SQLite.swift +// https://github.com/stephencelis/SQLite.swift +// Copyright © 2014-2015 Stephen Celis. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +extension ExpressionType where UnderlyingType : Value { + + /// Builds a copy of the expression prefixed with the `DISTINCT` keyword. + /// + /// let name = Expression("name") + /// name.distinct + /// // DISTINCT "name" + /// + /// - Returns: A copy of the expression prefixed with the `DISTINCT` + /// keyword. + public var distinct: Expression { + return Expression("DISTINCT \(template)", bindings) + } + + /// Builds a copy of the expression wrapped with the `count` aggregate + /// function. + /// + /// let name = Expression("name") + /// name.count + /// // count("name") + /// name.distinct.count + /// // count(DISTINCT "name") + /// + /// - Returns: A copy of the expression wrapped with the `count` aggregate + /// function. + public var count: Expression { + return wrap(self) + } + +} + +extension ExpressionType where UnderlyingType : _OptionalType, UnderlyingType.WrappedType : Value { + + /// Builds a copy of the expression prefixed with the `DISTINCT` keyword. + /// + /// let name = Expression("name") + /// name.distinct + /// // DISTINCT "name" + /// + /// - Returns: A copy of the expression prefixed with the `DISTINCT` + /// keyword. + public var distinct: Expression { + return Expression("DISTINCT \(template)", bindings) + } + + /// Builds a copy of the expression wrapped with the `count` aggregate + /// function. + /// + /// let name = Expression("name") + /// name.count + /// // count("name") + /// name.distinct.count + /// // count(DISTINCT "name") + /// + /// - Returns: A copy of the expression wrapped with the `count` aggregate + /// function. + public var count: Expression { + return wrap(self) + } + +} + +extension ExpressionType where UnderlyingType : Value, UnderlyingType.Datatype : Comparable { + + /// Builds a copy of the expression wrapped with the `max` aggregate + /// function. + /// + /// let age = Expression("age") + /// age.max + /// // max("age") + /// + /// - Returns: A copy of the expression wrapped with the `max` aggregate + /// function. + public var max: Expression { + return wrap(self) + } + + /// Builds a copy of the expression wrapped with the `min` aggregate + /// function. + /// + /// let age = Expression("age") + /// age.min + /// // min("age") + /// + /// - Returns: A copy of the expression wrapped with the `min` aggregate + /// function. + public var min: Expression { + return wrap(self) + } + +} + +extension ExpressionType where UnderlyingType : _OptionalType, UnderlyingType.WrappedType : Value, UnderlyingType.WrappedType.Datatype : Comparable { + + /// Builds a copy of the expression wrapped with the `max` aggregate + /// function. + /// + /// let age = Expression("age") + /// age.max + /// // max("age") + /// + /// - Returns: A copy of the expression wrapped with the `max` aggregate + /// function. + public var max: Expression { + return wrap(self) + } + + /// Builds a copy of the expression wrapped with the `min` aggregate + /// function. + /// + /// let age = Expression("age") + /// age.min + /// // min("age") + /// + /// - Returns: A copy of the expression wrapped with the `min` aggregate + /// function. + public var min: Expression { + return wrap(self) + } + +} + +extension ExpressionType where UnderlyingType : Value, UnderlyingType.Datatype : Number { + + /// Builds a copy of the expression wrapped with the `avg` aggregate + /// function. + /// + /// let salary = Expression("salary") + /// salary.average + /// // avg("salary") + /// + /// - Returns: A copy of the expression wrapped with the `min` aggregate + /// function. + public var average: Expression { + return "avg".wrap(self) + } + + /// Builds a copy of the expression wrapped with the `sum` aggregate + /// function. + /// + /// let salary = Expression("salary") + /// salary.sum + /// // sum("salary") + /// + /// - Returns: A copy of the expression wrapped with the `min` aggregate + /// function. + public var sum: Expression { + return wrap(self) + } + + /// Builds a copy of the expression wrapped with the `total` aggregate + /// function. + /// + /// let salary = Expression("salary") + /// salary.total + /// // total("salary") + /// + /// - Returns: A copy of the expression wrapped with the `min` aggregate + /// function. + public var total: Expression { + return wrap(self) + } + +} + +extension ExpressionType where UnderlyingType : _OptionalType, UnderlyingType.WrappedType : Value, UnderlyingType.WrappedType.Datatype : Number { + + /// Builds a copy of the expression wrapped with the `avg` aggregate + /// function. + /// + /// let salary = Expression("salary") + /// salary.average + /// // avg("salary") + /// + /// - Returns: A copy of the expression wrapped with the `min` aggregate + /// function. + public var average: Expression { + return "avg".wrap(self) + } + + /// Builds a copy of the expression wrapped with the `sum` aggregate + /// function. + /// + /// let salary = Expression("salary") + /// salary.sum + /// // sum("salary") + /// + /// - Returns: A copy of the expression wrapped with the `min` aggregate + /// function. + public var sum: Expression { + return wrap(self) + } + + /// Builds a copy of the expression wrapped with the `total` aggregate + /// function. + /// + /// let salary = Expression("salary") + /// salary.total + /// // total("salary") + /// + /// - Returns: A copy of the expression wrapped with the `min` aggregate + /// function. + public var total: Expression { + return wrap(self) + } + +} + +extension ExpressionType where UnderlyingType == Int { + + static func count(_ star: Star) -> Expression { + return wrap(star(nil, nil)) + } + +} + +/// Builds an expression representing `count(*)` (when called with the `*` +/// function literal). +/// +/// count(*) +/// // count(*) +/// +/// - Returns: An expression returning `count(*)` (when called with the `*` +/// function literal). +public func count(_ star: Star) -> Expression { + return Expression.count(star) +} diff --git a/mobile/ios/ThirdParty/SQLite.swift/Sources/SQLite/Typed/Collation.swift b/mobile/ios/ThirdParty/SQLite.swift/Sources/SQLite/Typed/Collation.swift new file mode 100644 index 0000000000..e2ff9d1006 --- /dev/null +++ b/mobile/ios/ThirdParty/SQLite.swift/Sources/SQLite/Typed/Collation.swift @@ -0,0 +1,69 @@ +// +// SQLite.swift +// https://github.com/stephencelis/SQLite.swift +// Copyright © 2014-2015 Stephen Celis. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +/// A collating function used to compare to strings. +/// +/// - SeeAlso: +public enum Collation { + + /// Compares string by raw data. + case binary + + /// Like binary, but folds uppercase ASCII letters into their lowercase + /// equivalents. + case nocase + + /// Like binary, but strips trailing space. + case rtrim + + /// A custom collating sequence identified by the given string, registered + /// using `Database.create(collation:…)` + case custom(String) + +} + +extension Collation : Expressible { + + public var expression: Expression { + return Expression(literal: description) + } + +} + +extension Collation : CustomStringConvertible { + + public var description : String { + switch self { + case .binary: + return "BINARY" + case .nocase: + return "NOCASE" + case .rtrim: + return "RTRIM" + case .custom(let collation): + return collation.quote() + } + } + +} diff --git a/mobile/ios/ThirdParty/SQLite.swift/Sources/SQLite/Typed/CoreFunctions.swift b/mobile/ios/ThirdParty/SQLite.swift/Sources/SQLite/Typed/CoreFunctions.swift new file mode 100644 index 0000000000..9d17a326bc --- /dev/null +++ b/mobile/ios/ThirdParty/SQLite.swift/Sources/SQLite/Typed/CoreFunctions.swift @@ -0,0 +1,683 @@ +// +// SQLite.swift +// https://github.com/stephencelis/SQLite.swift +// Copyright © 2014-2015 Stephen Celis. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import Foundation.NSData + + +extension ExpressionType where UnderlyingType : Number { + + /// Builds a copy of the expression wrapped with the `abs` function. + /// + /// let x = Expression("x") + /// x.absoluteValue + /// // abs("x") + /// + /// - Returns: A copy of the expression wrapped with the `abs` function. + public var absoluteValue : Expression { + return "abs".wrap(self) + } + +} + +extension ExpressionType where UnderlyingType : _OptionalType, UnderlyingType.WrappedType : Number { + + /// Builds a copy of the expression wrapped with the `abs` function. + /// + /// let x = Expression("x") + /// x.absoluteValue + /// // abs("x") + /// + /// - Returns: A copy of the expression wrapped with the `abs` function. + public var absoluteValue : Expression { + return "abs".wrap(self) + } + +} + +extension ExpressionType where UnderlyingType == Double { + + /// Builds a copy of the expression wrapped with the `round` function. + /// + /// let salary = Expression("salary") + /// salary.round() + /// // round("salary") + /// salary.round(2) + /// // round("salary", 2) + /// + /// - Returns: A copy of the expression wrapped with the `round` function. + public func round(_ precision: Int? = nil) -> Expression { + guard let precision = precision else { + return wrap([self]) + } + return wrap([self, Int(precision)]) + } + +} + +extension ExpressionType where UnderlyingType == Double? { + + /// Builds a copy of the expression wrapped with the `round` function. + /// + /// let salary = Expression("salary") + /// salary.round() + /// // round("salary") + /// salary.round(2) + /// // round("salary", 2) + /// + /// - Returns: A copy of the expression wrapped with the `round` function. + public func round(_ precision: Int? = nil) -> Expression { + guard let precision = precision else { + return wrap(self) + } + return wrap([self, Int(precision)]) + } + +} + +extension ExpressionType where UnderlyingType : Value, UnderlyingType.Datatype == Int64 { + + /// Builds an expression representing the `random` function. + /// + /// Expression.random() + /// // random() + /// + /// - Returns: An expression calling the `random` function. + public static func random() -> Expression { + return "random".wrap([]) + } + +} + +extension ExpressionType where UnderlyingType == Data { + + /// Builds an expression representing the `randomblob` function. + /// + /// Expression.random(16) + /// // randomblob(16) + /// + /// - Parameter length: Length in bytes. + /// + /// - Returns: An expression calling the `randomblob` function. + public static func random(_ length: Int) -> Expression { + return "randomblob".wrap([]) + } + + /// Builds an expression representing the `zeroblob` function. + /// + /// Expression.allZeros(16) + /// // zeroblob(16) + /// + /// - Parameter length: Length in bytes. + /// + /// - Returns: An expression calling the `zeroblob` function. + public static func allZeros(_ length: Int) -> Expression { + return "zeroblob".wrap([]) + } + + /// Builds a copy of the expression wrapped with the `length` function. + /// + /// let data = Expression("data") + /// data.length + /// // length("data") + /// + /// - Returns: A copy of the expression wrapped with the `length` function. + public var length: Expression { + return wrap(self) + } + +} + +extension ExpressionType where UnderlyingType == Data? { + + /// Builds a copy of the expression wrapped with the `length` function. + /// + /// let data = Expression("data") + /// data.length + /// // length("data") + /// + /// - Returns: A copy of the expression wrapped with the `length` function. + public var length: Expression { + return wrap(self) + } + +} + +extension ExpressionType where UnderlyingType == String { + + /// Builds a copy of the expression wrapped with the `length` function. + /// + /// let name = Expression("name") + /// name.length + /// // length("name") + /// + /// - Returns: A copy of the expression wrapped with the `length` function. + public var length: Expression { + return wrap(self) + } + + /// Builds a copy of the expression wrapped with the `lower` function. + /// + /// let name = Expression("name") + /// name.lowercaseString + /// // lower("name") + /// + /// - Returns: A copy of the expression wrapped with the `lower` function. + public var lowercaseString: Expression { + return "lower".wrap(self) + } + + /// Builds a copy of the expression wrapped with the `upper` function. + /// + /// let name = Expression("name") + /// name.uppercaseString + /// // lower("name") + /// + /// - Returns: A copy of the expression wrapped with the `upper` function. + public var uppercaseString: Expression { + return "upper".wrap(self) + } + + /// Builds a copy of the expression appended with a `LIKE` query against the + /// given pattern. + /// + /// let email = Expression("email") + /// email.like("%@example.com") + /// // "email" LIKE '%@example.com' + /// email.like("99\\%@%", escape: "\\") + /// // "email" LIKE '99\%@%' ESCAPE '\' + /// + /// - Parameters: + /// + /// - pattern: A pattern to match. + /// + /// - escape: An (optional) character designated for escaping + /// pattern-matching characters (*i.e.*, the `%` and `_` characters). + /// + /// - Returns: A copy of the expression appended with a `LIKE` query against + /// the given pattern. + public func like(_ pattern: String, escape character: Character? = nil) -> Expression { + guard let character = character else { + return "LIKE".infix(self, pattern) + } + return Expression("(\(template) LIKE ? ESCAPE ?)", bindings + [pattern, String(character)]) + } + + /// Builds a copy of the expression appended with a `GLOB` query against the + /// given pattern. + /// + /// let path = Expression("path") + /// path.glob("*.png") + /// // "path" GLOB '*.png' + /// + /// - Parameter pattern: A pattern to match. + /// + /// - Returns: A copy of the expression appended with a `GLOB` query against + /// the given pattern. + public func glob(_ pattern: String) -> Expression { + return "GLOB".infix(self, pattern) + } + + /// Builds a copy of the expression appended with a `MATCH` query against + /// the given pattern. + /// + /// let title = Expression("title") + /// title.match("swift AND programming") + /// // "title" MATCH 'swift AND programming' + /// + /// - Parameter pattern: A pattern to match. + /// + /// - Returns: A copy of the expression appended with a `MATCH` query + /// against the given pattern. + public func match(_ pattern: String) -> Expression { + return "MATCH".infix(self, pattern) + } + + /// Builds a copy of the expression appended with a `REGEXP` query against + /// the given pattern. + /// + /// - Parameter pattern: A pattern to match. + /// + /// - Returns: A copy of the expression appended with a `REGEXP` query + /// against the given pattern. + public func regexp(_ pattern: String) -> Expression { + return "REGEXP".infix(self, pattern) + } + + /// Builds a copy of the expression appended with a `COLLATE` clause with + /// the given sequence. + /// + /// let name = Expression("name") + /// name.collate(.Nocase) + /// // "name" COLLATE NOCASE + /// + /// - Parameter collation: A collating sequence. + /// + /// - Returns: A copy of the expression appended with a `COLLATE` clause + /// with the given sequence. + public func collate(_ collation: Collation) -> Expression { + return "COLLATE".infix(self, collation) + } + + /// Builds a copy of the expression wrapped with the `ltrim` function. + /// + /// let name = Expression("name") + /// name.ltrim() + /// // ltrim("name") + /// name.ltrim([" ", "\t"]) + /// // ltrim("name", ' \t') + /// + /// - Parameter characters: A set of characters to trim. + /// + /// - Returns: A copy of the expression wrapped with the `ltrim` function. + public func ltrim(_ characters: Set? = nil) -> Expression { + guard let characters = characters else { + return wrap(self) + } + return wrap([self, String(characters)]) + } + + /// Builds a copy of the expression wrapped with the `rtrim` function. + /// + /// let name = Expression("name") + /// name.rtrim() + /// // rtrim("name") + /// name.rtrim([" ", "\t"]) + /// // rtrim("name", ' \t') + /// + /// - Parameter characters: A set of characters to trim. + /// + /// - Returns: A copy of the expression wrapped with the `rtrim` function. + public func rtrim(_ characters: Set? = nil) -> Expression { + guard let characters = characters else { + return wrap(self) + } + return wrap([self, String(characters)]) + } + + /// Builds a copy of the expression wrapped with the `trim` function. + /// + /// let name = Expression("name") + /// name.trim() + /// // trim("name") + /// name.trim([" ", "\t"]) + /// // trim("name", ' \t') + /// + /// - Parameter characters: A set of characters to trim. + /// + /// - Returns: A copy of the expression wrapped with the `trim` function. + public func trim(_ characters: Set? = nil) -> Expression { + guard let characters = characters else { + return wrap([self]) + } + return wrap([self, String(characters)]) + } + + /// Builds a copy of the expression wrapped with the `replace` function. + /// + /// let email = Expression("email") + /// email.replace("@mac.com", with: "@icloud.com") + /// // replace("email", '@mac.com', '@icloud.com') + /// + /// - Parameters: + /// + /// - pattern: A pattern to match. + /// + /// - replacement: The replacement string. + /// + /// - Returns: A copy of the expression wrapped with the `replace` function. + public func replace(_ pattern: String, with replacement: String) -> Expression { + return "replace".wrap([self, pattern, replacement]) + } + + public func substring(_ location: Int, length: Int? = nil) -> Expression { + guard let length = length else { + return "substr".wrap([self, location]) + } + return "substr".wrap([self, location, length]) + } + + public subscript(range: Range) -> Expression { + return substring(range.lowerBound, length: range.upperBound - range.lowerBound) + } + +} + +extension ExpressionType where UnderlyingType == String? { + + /// Builds a copy of the expression wrapped with the `length` function. + /// + /// let name = Expression("name") + /// name.length + /// // length("name") + /// + /// - Returns: A copy of the expression wrapped with the `length` function. + public var length: Expression { + return wrap(self) + } + + /// Builds a copy of the expression wrapped with the `lower` function. + /// + /// let name = Expression("name") + /// name.lowercaseString + /// // lower("name") + /// + /// - Returns: A copy of the expression wrapped with the `lower` function. + public var lowercaseString: Expression { + return "lower".wrap(self) + } + + /// Builds a copy of the expression wrapped with the `upper` function. + /// + /// let name = Expression("name") + /// name.uppercaseString + /// // lower("name") + /// + /// - Returns: A copy of the expression wrapped with the `upper` function. + public var uppercaseString: Expression { + return "upper".wrap(self) + } + + /// Builds a copy of the expression appended with a `LIKE` query against the + /// given pattern. + /// + /// let email = Expression("email") + /// email.like("%@example.com") + /// // "email" LIKE '%@example.com' + /// email.like("99\\%@%", escape: "\\") + /// // "email" LIKE '99\%@%' ESCAPE '\' + /// + /// - Parameters: + /// + /// - pattern: A pattern to match. + /// + /// - escape: An (optional) character designated for escaping + /// pattern-matching characters (*i.e.*, the `%` and `_` characters). + /// + /// - Returns: A copy of the expression appended with a `LIKE` query against + /// the given pattern. + public func like(_ pattern: String, escape character: Character? = nil) -> Expression { + guard let character = character else { + return "LIKE".infix(self, pattern) + } + return Expression("(\(template) LIKE ? ESCAPE ?)", bindings + [pattern, String(character)]) + } + + /// Builds a copy of the expression appended with a `GLOB` query against the + /// given pattern. + /// + /// let path = Expression("path") + /// path.glob("*.png") + /// // "path" GLOB '*.png' + /// + /// - Parameter pattern: A pattern to match. + /// + /// - Returns: A copy of the expression appended with a `GLOB` query against + /// the given pattern. + public func glob(_ pattern: String) -> Expression { + return "GLOB".infix(self, pattern) + } + + /// Builds a copy of the expression appended with a `MATCH` query against + /// the given pattern. + /// + /// let title = Expression("title") + /// title.match("swift AND programming") + /// // "title" MATCH 'swift AND programming' + /// + /// - Parameter pattern: A pattern to match. + /// + /// - Returns: A copy of the expression appended with a `MATCH` query + /// against the given pattern. + public func match(_ pattern: String) -> Expression { + return "MATCH".infix(self, pattern) + } + + /// Builds a copy of the expression appended with a `REGEXP` query against + /// the given pattern. + /// + /// - Parameter pattern: A pattern to match. + /// + /// - Returns: A copy of the expression appended with a `REGEXP` query + /// against the given pattern. + public func regexp(_ pattern: String) -> Expression { + return "REGEXP".infix(self, pattern) + } + + /// Builds a copy of the expression appended with a `COLLATE` clause with + /// the given sequence. + /// + /// let name = Expression("name") + /// name.collate(.Nocase) + /// // "name" COLLATE NOCASE + /// + /// - Parameter collation: A collating sequence. + /// + /// - Returns: A copy of the expression appended with a `COLLATE` clause + /// with the given sequence. + public func collate(_ collation: Collation) -> Expression { + return "COLLATE".infix(self, collation) + } + + /// Builds a copy of the expression wrapped with the `ltrim` function. + /// + /// let name = Expression("name") + /// name.ltrim() + /// // ltrim("name") + /// name.ltrim([" ", "\t"]) + /// // ltrim("name", ' \t') + /// + /// - Parameter characters: A set of characters to trim. + /// + /// - Returns: A copy of the expression wrapped with the `ltrim` function. + public func ltrim(_ characters: Set? = nil) -> Expression { + guard let characters = characters else { + return wrap(self) + } + return wrap([self, String(characters)]) + } + + /// Builds a copy of the expression wrapped with the `rtrim` function. + /// + /// let name = Expression("name") + /// name.rtrim() + /// // rtrim("name") + /// name.rtrim([" ", "\t"]) + /// // rtrim("name", ' \t') + /// + /// - Parameter characters: A set of characters to trim. + /// + /// - Returns: A copy of the expression wrapped with the `rtrim` function. + public func rtrim(_ characters: Set? = nil) -> Expression { + guard let characters = characters else { + return wrap(self) + } + return wrap([self, String(characters)]) + } + + /// Builds a copy of the expression wrapped with the `trim` function. + /// + /// let name = Expression("name") + /// name.trim() + /// // trim("name") + /// name.trim([" ", "\t"]) + /// // trim("name", ' \t') + /// + /// - Parameter characters: A set of characters to trim. + /// + /// - Returns: A copy of the expression wrapped with the `trim` function. + public func trim(_ characters: Set? = nil) -> Expression { + guard let characters = characters else { + return wrap(self) + } + return wrap([self, String(characters)]) + } + + /// Builds a copy of the expression wrapped with the `replace` function. + /// + /// let email = Expression("email") + /// email.replace("@mac.com", with: "@icloud.com") + /// // replace("email", '@mac.com', '@icloud.com') + /// + /// - Parameters: + /// + /// - pattern: A pattern to match. + /// + /// - replacement: The replacement string. + /// + /// - Returns: A copy of the expression wrapped with the `replace` function. + public func replace(_ pattern: String, with replacement: String) -> Expression { + return "replace".wrap([self, pattern, replacement]) + } + + /// Builds a copy of the expression wrapped with the `substr` function. + /// + /// let title = Expression("title") + /// title.substr(-100) + /// // substr("title", -100) + /// title.substr(0, length: 100) + /// // substr("title", 0, 100) + /// + /// - Parameters: + /// + /// - location: The substring’s start index. + /// + /// - length: An optional substring length. + /// + /// - Returns: A copy of the expression wrapped with the `substr` function. + public func substring(_ location: Int, length: Int? = nil) -> Expression { + guard let length = length else { + return "substr".wrap([self, location]) + } + return "substr".wrap([self, location, length]) + } + + /// Builds a copy of the expression wrapped with the `substr` function. + /// + /// let title = Expression("title") + /// title[0..<100] + /// // substr("title", 0, 100) + /// + /// - Parameter range: The character index range of the substring. + /// + /// - Returns: A copy of the expression wrapped with the `substr` function. + public subscript(range: Range) -> Expression { + return substring(range.lowerBound, length: range.upperBound - range.lowerBound) + } + +} + +extension Collection where Iterator.Element : Value, IndexDistance == Int { + + /// Builds a copy of the expression prepended with an `IN` check against the + /// collection. + /// + /// let name = Expression("name") + /// ["alice", "betty"].contains(name) + /// // "name" IN ('alice', 'betty') + /// + /// - Parameter pattern: A pattern to match. + /// + /// - Returns: A copy of the expression prepended with an `IN` check against + /// the collection. + public func contains(_ expression: Expression) -> Expression { + let templates = [String](repeating: "?", count: count).joined(separator: ", ") + return "IN".infix(expression, Expression("(\(templates))", map { $0.datatypeValue })) + } + + /// Builds a copy of the expression prepended with an `IN` check against the + /// collection. + /// + /// let name = Expression("name") + /// ["alice", "betty"].contains(name) + /// // "name" IN ('alice', 'betty') + /// + /// - Parameter pattern: A pattern to match. + /// + /// - Returns: A copy of the expression prepended with an `IN` check against + /// the collection. + public func contains(_ expression: Expression) -> Expression { + let templates = [String](repeating: "?", count: count).joined(separator: ", ") + return "IN".infix(expression, Expression("(\(templates))", map { $0.datatypeValue })) + } + +} + +/// Builds a copy of the given expressions wrapped with the `ifnull` function. +/// +/// let name = Expression("name") +/// name ?? "An Anonymous Coward" +/// // ifnull("name", 'An Anonymous Coward') +/// +/// - Parameters: +/// +/// - optional: An optional expression. +/// +/// - defaultValue: A fallback value for when the optional expression is +/// `nil`. +/// +/// - Returns: A copy of the given expressions wrapped with the `ifnull` +/// function. +public func ??(optional: Expression, defaultValue: V) -> Expression { + return "ifnull".wrap([optional, defaultValue]) +} + +/// Builds a copy of the given expressions wrapped with the `ifnull` function. +/// +/// let nick = Expression("nick") +/// let name = Expression("name") +/// nick ?? name +/// // ifnull("nick", "name") +/// +/// - Parameters: +/// +/// - optional: An optional expression. +/// +/// - defaultValue: A fallback expression for when the optional expression is +/// `nil`. +/// +/// - Returns: A copy of the given expressions wrapped with the `ifnull` +/// function. +public func ??(optional: Expression, defaultValue: Expression) -> Expression { + return "ifnull".wrap([optional, defaultValue]) +} + +/// Builds a copy of the given expressions wrapped with the `ifnull` function. +/// +/// let nick = Expression("nick") +/// let name = Expression("name") +/// nick ?? name +/// // ifnull("nick", "name") +/// +/// - Parameters: +/// +/// - optional: An optional expression. +/// +/// - defaultValue: A fallback expression for when the optional expression is +/// `nil`. +/// +/// - Returns: A copy of the given expressions wrapped with the `ifnull` +/// function. +public func ??(optional: Expression, defaultValue: Expression) -> Expression { + return "ifnull".wrap([optional, defaultValue]) +} diff --git a/mobile/ios/ThirdParty/SQLite.swift/Sources/SQLite/Typed/CustomFunctions.swift b/mobile/ios/ThirdParty/SQLite.swift/Sources/SQLite/Typed/CustomFunctions.swift new file mode 100644 index 0000000000..64b54edb75 --- /dev/null +++ b/mobile/ios/ThirdParty/SQLite.swift/Sources/SQLite/Typed/CustomFunctions.swift @@ -0,0 +1,136 @@ +// +// SQLite.swift +// https://github.com/stephencelis/SQLite.swift +// Copyright © 2014-2015 Stephen Celis. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +public extension Connection { + + /// Creates or redefines a custom SQL function. + /// + /// - Parameters: + /// + /// - function: The name of the function to create or redefine. + /// + /// - deterministic: Whether or not the function is deterministic (_i.e._ + /// the function always returns the same result for a given input). + /// + /// Default: `false` + /// + /// - block: A block of code to run when the function is called. + /// The assigned types must be explicit. + /// + /// - Returns: A closure returning an SQL expression to call the function. + public func createFunction(_ function: String, deterministic: Bool = false, _ block: @escaping () -> Z) throws -> (() -> Expression) { + let fn = try createFunction(function, 0, deterministic) { _ in block() } + return { fn([]) } + } + + public func createFunction(_ function: String, deterministic: Bool = false, _ block: @escaping () -> Z?) throws -> (() -> Expression) { + let fn = try createFunction(function, 0, deterministic) { _ in block() } + return { fn([]) } + } + + // MARK: - + + public func createFunction(_ function: String, deterministic: Bool = false, _ block: @escaping (A) -> Z) throws -> ((Expression) -> Expression) { + let fn = try createFunction(function, 1, deterministic) { args in block(value(args[0])) } + return { arg in fn([arg]) } + } + + public func createFunction(function: String, deterministic: Bool = false, _ block: @escaping (A?) -> Z) throws -> ((Expression) -> Expression) { + let fn = try createFunction(function, 1, deterministic) { args in block(args[0].map(value)) } + return { arg in fn([arg]) } + } + + public func createFunction(function: String, deterministic: Bool = false, _ block: @escaping (A) -> Z?) throws -> ((Expression) -> Expression) { + let fn = try createFunction(function, 1, deterministic) { args in block(value(args[0])) } + return { arg in fn([arg]) } + } + + public func createFunction(function: String, deterministic: Bool = false, _ block: @escaping (A?) -> Z?) throws -> ((Expression) -> Expression) { + let fn = try createFunction(function, 1, deterministic) { args in block(args[0].map(value)) } + return { arg in fn([arg]) } + } + + // MARK: - + + public func createFunction(_ function: String, deterministic: Bool = false, _ block: @escaping (A, B) -> Z) throws -> (Expression, Expression) -> Expression { + let fn = try createFunction(function, 1, deterministic) { args in block(value(args[0]), value(args[1])) } + return { a, b in fn([a, b]) } + } + + public func createFunction(_ function: String, deterministic: Bool = false, _ block: @escaping (A?, B) -> Z) throws -> (Expression, Expression) -> Expression { + let fn = try createFunction(function, 1, deterministic) { args in block(args[0].map(value), value(args[1])) } + return { a, b in fn([a, b]) } + } + + public func createFunction(_ function: String, deterministic: Bool = false, _ block: @escaping (A, B?) -> Z) throws -> (Expression, Expression) -> Expression { + let fn = try createFunction(function, 1, deterministic) { args in block(value(args[0]), args[1].map(value)) } + return { a, b in fn([a, b]) } + } + + public func createFunction(_ function: String, deterministic: Bool = false, _ block: @escaping (A, B) -> Z?) throws -> (Expression, Expression) -> Expression { + let fn = try createFunction(function, 1, deterministic) { args in block(value(args[0]), value(args[1])) } + return { a, b in fn([a, b]) } + } + + public func createFunction(_ function: String, deterministic: Bool = false, _ block: @escaping (A?, B?) -> Z) throws -> (Expression, Expression) -> Expression { + let fn = try createFunction(function, 1, deterministic) { args in block(args[0].map(value), args[1].map(value)) } + return { a, b in fn([a, b]) } + } + + public func createFunction(_ function: String, deterministic: Bool = false, _ block: @escaping (A?, B) -> Z?) throws -> (Expression, Expression) -> Expression { + let fn = try createFunction(function, 1, deterministic) { args in block(args[0].map(value), value(args[1])) } + return { a, b in fn([a, b]) } + } + + public func createFunction(_ function: String, deterministic: Bool = false, _ block: @escaping (A, B?) -> Z?) throws -> (Expression, Expression) -> Expression { + let fn = try createFunction(function, 1, deterministic) { args in block(value(args[0]), args[1].map(value)) } + return { a, b in fn([a, b]) } + } + + public func createFunction(_ function: String, deterministic: Bool = false, _ block: @escaping (A?, B?) -> Z?) throws -> (Expression, Expression) -> Expression { + let fn = try createFunction(function, 1, deterministic) { args in block(args[0].map(value), args[1].map(value)) } + return { a, b in fn([a, b]) } + } + + // MARK: - + + fileprivate func createFunction(_ function: String, _ argumentCount: UInt, _ deterministic: Bool, _ block: @escaping ([Binding?]) -> Z) throws -> (([Expressible]) -> Expression) { + createFunction(function, argumentCount: argumentCount, deterministic: deterministic) { arguments in + block(arguments).datatypeValue + } + return { arguments in + function.quote().wrap(", ".join(arguments)) + } + } + + fileprivate func createFunction(_ function: String, _ argumentCount: UInt, _ deterministic: Bool, _ block: @escaping ([Binding?]) -> Z?) throws -> (([Expressible]) -> Expression) { + createFunction(function, argumentCount: argumentCount, deterministic: deterministic) { arguments in + block(arguments)?.datatypeValue + } + return { arguments in + function.quote().wrap(", ".join(arguments)) + } + } + +} diff --git a/mobile/ios/ThirdParty/SQLite.swift/Sources/SQLite/Typed/Expression.swift b/mobile/ios/ThirdParty/SQLite.swift/Sources/SQLite/Typed/Expression.swift new file mode 100644 index 0000000000..3198901c55 --- /dev/null +++ b/mobile/ios/ThirdParty/SQLite.swift/Sources/SQLite/Typed/Expression.swift @@ -0,0 +1,147 @@ +// +// SQLite.swift +// https://github.com/stephencelis/SQLite.swift +// Copyright © 2014-2015 Stephen Celis. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +public protocol ExpressionType : Expressible { // extensions cannot have inheritance clauses + + associatedtype UnderlyingType = Void + + var template: String { get } + var bindings: [Binding?] { get } + + init(_ template: String, _ bindings: [Binding?]) + +} + +extension ExpressionType { + + public init(literal: String) { + self.init(literal, []) + } + + public init(_ identifier: String) { + self.init(literal: identifier.quote()) + } + + public init(_ expression: U) { + self.init(expression.template, expression.bindings) + } + +} + +/// An `Expression` represents a raw SQL fragment and any associated bindings. +public struct Expression : ExpressionType { + + public typealias UnderlyingType = Datatype + + public var template: String + public var bindings: [Binding?] + + public init(_ template: String, _ bindings: [Binding?]) { + self.template = template + self.bindings = bindings + } + +} + +public protocol Expressible { + + var expression: Expression { get } + +} + +extension Expressible { + + // naïve compiler for statements that can’t be bound, e.g., CREATE TABLE + // FIXME: use @testable and make internal + public func asSQL() -> String { + let expressed = expression + var idx = 0 + return expressed.template.characters.reduce("") { template, character in + let transcoded: String + + if character == "?" { + transcoded = transcode(expressed.bindings[idx]) + idx += 1 + } else { + transcoded = String(character) + } + return template + transcoded + } + } + +} + +extension ExpressionType { + + public var expression: Expression { + return Expression(template, bindings) + } + + public var asc: Expressible { + return " ".join([self, Expression(literal: "ASC")]) + } + + public var desc: Expressible { + return " ".join([self, Expression(literal: "DESC")]) + } + +} + +extension ExpressionType where UnderlyingType : Value { + + public init(value: UnderlyingType) { + self.init("?", [value.datatypeValue]) + } + +} + +extension ExpressionType where UnderlyingType : _OptionalType, UnderlyingType.WrappedType : Value { + + public static var null: Self { + return self.init(value: nil) + } + + public init(value: UnderlyingType.WrappedType?) { + self.init("?", [value?.datatypeValue]) + } + +} + +extension Value { + + public var expression: Expression { + return Expression(value: self).expression + } + +} + +public let rowid = Expression("ROWID") + +public func cast(_ expression: Expression) -> Expression { + return Expression("CAST (\(expression.template) AS \(U.declaredDatatype))", expression.bindings) +} + +public func cast(_ expression: Expression) -> Expression { + return Expression("CAST (\(expression.template) AS \(U.declaredDatatype))", expression.bindings) +} diff --git a/mobile/ios/ThirdParty/SQLite.swift/Sources/SQLite/Typed/Operators.swift b/mobile/ios/ThirdParty/SQLite.swift/Sources/SQLite/Typed/Operators.swift new file mode 100644 index 0000000000..fdd293be71 --- /dev/null +++ b/mobile/ios/ThirdParty/SQLite.swift/Sources/SQLite/Typed/Operators.swift @@ -0,0 +1,541 @@ +// +// SQLite.swift +// https://github.com/stephencelis/SQLite.swift +// Copyright © 2014-2015 Stephen Celis. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +// TODO: use `@warn_unused_result` by the time operator functions support it + +public func +(lhs: Expression, rhs: Expression) -> Expression { + return "||".infix(lhs, rhs) +} + +public func +(lhs: Expression, rhs: Expression) -> Expression { + return "||".infix(lhs, rhs) +} +public func +(lhs: Expression, rhs: Expression) -> Expression { + return "||".infix(lhs, rhs) +} +public func +(lhs: Expression, rhs: Expression) -> Expression { + return "||".infix(lhs, rhs) +} +public func +(lhs: Expression, rhs: String) -> Expression { + return "||".infix(lhs, rhs) +} +public func +(lhs: Expression, rhs: String) -> Expression { + return "||".infix(lhs, rhs) +} +public func +(lhs: String, rhs: Expression) -> Expression { + return "||".infix(lhs, rhs) +} +public func +(lhs: String, rhs: Expression) -> Expression { + return "||".infix(lhs, rhs) +} + +// MARK: - + +public func +(lhs: Expression, rhs: Expression) -> Expression where V.Datatype : Number { + return infix(lhs, rhs) +} +public func +(lhs: Expression, rhs: Expression) -> Expression where V.Datatype : Number { + return infix(lhs, rhs) +} +public func +(lhs: Expression, rhs: Expression) -> Expression where V.Datatype : Number { + return infix(lhs, rhs) +} +public func +(lhs: Expression, rhs: Expression) -> Expression where V.Datatype : Number { + return infix(lhs, rhs) +} +public func +(lhs: Expression, rhs: V) -> Expression where V.Datatype : Number { + return infix(lhs, rhs) +} +public func +(lhs: Expression, rhs: V) -> Expression where V.Datatype : Number { + return infix(lhs, rhs) +} +public func +(lhs: V, rhs: Expression) -> Expression where V.Datatype : Number { + return infix(lhs, rhs) +} +public func +(lhs: V, rhs: Expression) -> Expression where V.Datatype : Number { + return infix(lhs, rhs) +} + +public func -(lhs: Expression, rhs: Expression) -> Expression where V.Datatype : Number { + return infix(lhs, rhs) +} +public func -(lhs: Expression, rhs: Expression) -> Expression where V.Datatype : Number { + return infix(lhs, rhs) +} +public func -(lhs: Expression, rhs: Expression) -> Expression where V.Datatype : Number { + return infix(lhs, rhs) +} +public func -(lhs: Expression, rhs: Expression) -> Expression where V.Datatype : Number { + return infix(lhs, rhs) +} +public func -(lhs: Expression, rhs: V) -> Expression where V.Datatype : Number { + return infix(lhs, rhs) +} +public func -(lhs: Expression, rhs: V) -> Expression where V.Datatype : Number { + return infix(lhs, rhs) +} +public func -(lhs: V, rhs: Expression) -> Expression where V.Datatype : Number { + return infix(lhs, rhs) +} +public func -(lhs: V, rhs: Expression) -> Expression where V.Datatype : Number { + return infix(lhs, rhs) +} + +public func *(lhs: Expression, rhs: Expression) -> Expression where V.Datatype : Number { + return infix(lhs, rhs) +} +public func *(lhs: Expression, rhs: Expression) -> Expression where V.Datatype : Number { + return infix(lhs, rhs) +} +public func *(lhs: Expression, rhs: Expression) -> Expression where V.Datatype : Number { + return infix(lhs, rhs) +} +public func *(lhs: Expression, rhs: Expression) -> Expression where V.Datatype : Number { + return infix(lhs, rhs) +} +public func *(lhs: Expression, rhs: V) -> Expression where V.Datatype : Number { + return infix(lhs, rhs) +} +public func *(lhs: Expression, rhs: V) -> Expression where V.Datatype : Number { + return infix(lhs, rhs) +} +public func *(lhs: V, rhs: Expression) -> Expression where V.Datatype : Number { + return infix(lhs, rhs) +} +public func *(lhs: V, rhs: Expression) -> Expression where V.Datatype : Number { + return infix(lhs, rhs) +} + +public func /(lhs: Expression, rhs: Expression) -> Expression where V.Datatype : Number { + return infix(lhs, rhs) +} +public func /(lhs: Expression, rhs: Expression) -> Expression where V.Datatype : Number { + return infix(lhs, rhs) +} +public func /(lhs: Expression, rhs: Expression) -> Expression where V.Datatype : Number { + return infix(lhs, rhs) +} +public func /(lhs: Expression, rhs: Expression) -> Expression where V.Datatype : Number { + return infix(lhs, rhs) +} +public func /(lhs: Expression, rhs: V) -> Expression where V.Datatype : Number { + return infix(lhs, rhs) +} +public func /(lhs: Expression, rhs: V) -> Expression where V.Datatype : Number { + return infix(lhs, rhs) +} +public func /(lhs: V, rhs: Expression) -> Expression where V.Datatype : Number { + return infix(lhs, rhs) +} +public func /(lhs: V, rhs: Expression) -> Expression where V.Datatype : Number { + return infix(lhs, rhs) +} + +public prefix func -(rhs: Expression) -> Expression where V.Datatype : Number { + return wrap(rhs) +} +public prefix func -(rhs: Expression) -> Expression where V.Datatype : Number { + return wrap(rhs) +} + +// MARK: - + +public func %(lhs: Expression, rhs: Expression) -> Expression where V.Datatype == Int64 { + return infix(lhs, rhs) +} +public func %(lhs: Expression, rhs: Expression) -> Expression where V.Datatype == Int64 { + return infix(lhs, rhs) +} +public func %(lhs: Expression, rhs: Expression) -> Expression where V.Datatype == Int64 { + return infix(lhs, rhs) +} +public func %(lhs: Expression, rhs: Expression) -> Expression where V.Datatype == Int64 { + return infix(lhs, rhs) +} +public func %(lhs: Expression, rhs: V) -> Expression where V.Datatype == Int64 { + return infix(lhs, rhs) +} +public func %(lhs: Expression, rhs: V) -> Expression where V.Datatype == Int64 { + return infix(lhs, rhs) +} +public func %(lhs: V, rhs: Expression) -> Expression where V.Datatype == Int64 { + return infix(lhs, rhs) +} +public func %(lhs: V, rhs: Expression) -> Expression where V.Datatype == Int64 { + return infix(lhs, rhs) +} + +public func <<(lhs: Expression, rhs: Expression) -> Expression where V.Datatype == Int64 { + return infix(lhs, rhs) +} +public func <<(lhs: Expression, rhs: Expression) -> Expression where V.Datatype == Int64 { + return infix(lhs, rhs) +} +public func <<(lhs: Expression, rhs: Expression) -> Expression where V.Datatype == Int64 { + return infix(lhs, rhs) +} +public func <<(lhs: Expression, rhs: Expression) -> Expression where V.Datatype == Int64 { + return infix(lhs, rhs) +} +public func <<(lhs: Expression, rhs: V) -> Expression where V.Datatype == Int64 { + return infix(lhs, rhs) +} +public func <<(lhs: Expression, rhs: V) -> Expression where V.Datatype == Int64 { + return infix(lhs, rhs) +} +public func <<(lhs: V, rhs: Expression) -> Expression where V.Datatype == Int64 { + return infix(lhs, rhs) +} +public func <<(lhs: V, rhs: Expression) -> Expression where V.Datatype == Int64 { + return infix(lhs, rhs) +} + +public func >>(lhs: Expression, rhs: Expression) -> Expression where V.Datatype == Int64 { + return infix(lhs, rhs) +} +public func >>(lhs: Expression, rhs: Expression) -> Expression where V.Datatype == Int64 { + return infix(lhs, rhs) +} +public func >>(lhs: Expression, rhs: Expression) -> Expression where V.Datatype == Int64 { + return infix(lhs, rhs) +} +public func >>(lhs: Expression, rhs: Expression) -> Expression where V.Datatype == Int64 { + return infix(lhs, rhs) +} +public func >>(lhs: Expression, rhs: V) -> Expression where V.Datatype == Int64 { + return infix(lhs, rhs) +} +public func >>(lhs: Expression, rhs: V) -> Expression where V.Datatype == Int64 { + return infix(lhs, rhs) +} +public func >>(lhs: V, rhs: Expression) -> Expression where V.Datatype == Int64 { + return infix(lhs, rhs) +} +public func >>(lhs: V, rhs: Expression) -> Expression where V.Datatype == Int64 { + return infix(lhs, rhs) +} + +public func &(lhs: Expression, rhs: Expression) -> Expression where V.Datatype == Int64 { + return infix(lhs, rhs) +} +public func &(lhs: Expression, rhs: Expression) -> Expression where V.Datatype == Int64 { + return infix(lhs, rhs) +} +public func &(lhs: Expression, rhs: Expression) -> Expression where V.Datatype == Int64 { + return infix(lhs, rhs) +} +public func &(lhs: Expression, rhs: Expression) -> Expression where V.Datatype == Int64 { + return infix(lhs, rhs) +} +public func &(lhs: Expression, rhs: V) -> Expression where V.Datatype == Int64 { + return infix(lhs, rhs) +} +public func &(lhs: Expression, rhs: V) -> Expression where V.Datatype == Int64 { + return infix(lhs, rhs) +} +public func &(lhs: V, rhs: Expression) -> Expression where V.Datatype == Int64 { + return infix(lhs, rhs) +} +public func &(lhs: V, rhs: Expression) -> Expression where V.Datatype == Int64 { + return infix(lhs, rhs) +} + +public func |(lhs: Expression, rhs: Expression) -> Expression where V.Datatype == Int64 { + return infix(lhs, rhs) +} +public func |(lhs: Expression, rhs: Expression) -> Expression where V.Datatype == Int64 { + return infix(lhs, rhs) +} +public func |(lhs: Expression, rhs: Expression) -> Expression where V.Datatype == Int64 { + return infix(lhs, rhs) +} +public func |(lhs: Expression, rhs: Expression) -> Expression where V.Datatype == Int64 { + return infix(lhs, rhs) +} +public func |(lhs: Expression, rhs: V) -> Expression where V.Datatype == Int64 { + return infix(lhs, rhs) +} +public func |(lhs: Expression, rhs: V) -> Expression where V.Datatype == Int64 { + return infix(lhs, rhs) +} +public func |(lhs: V, rhs: Expression) -> Expression where V.Datatype == Int64 { + return infix(lhs, rhs) +} +public func |(lhs: V, rhs: Expression) -> Expression where V.Datatype == Int64 { + return infix(lhs, rhs) +} + +public func ^(lhs: Expression, rhs: Expression) -> Expression where V.Datatype == Int64 { + return (~(lhs & rhs)) & (lhs | rhs) +} +public func ^(lhs: Expression, rhs: Expression) -> Expression where V.Datatype == Int64 { + return (~(lhs & rhs)) & (lhs | rhs) +} +public func ^(lhs: Expression, rhs: Expression) -> Expression where V.Datatype == Int64 { + return (~(lhs & rhs)) & (lhs | rhs) +} +public func ^(lhs: Expression, rhs: Expression) -> Expression where V.Datatype == Int64 { + return (~(lhs & rhs)) & (lhs | rhs) +} +public func ^(lhs: Expression, rhs: V) -> Expression where V.Datatype == Int64 { + return (~(lhs & rhs)) & (lhs | rhs) +} +public func ^(lhs: Expression, rhs: V) -> Expression where V.Datatype == Int64 { + return (~(lhs & rhs)) & (lhs | rhs) +} +public func ^(lhs: V, rhs: Expression) -> Expression where V.Datatype == Int64 { + return (~(lhs & rhs)) & (lhs | rhs) +} +public func ^(lhs: V, rhs: Expression) -> Expression where V.Datatype == Int64 { + return (~(lhs & rhs)) & (lhs | rhs) +} + +public prefix func ~(rhs: Expression) -> Expression where V.Datatype == Int64 { + return wrap(rhs) +} +public prefix func ~(rhs: Expression) -> Expression where V.Datatype == Int64 { + return wrap(rhs) +} + +// MARK: - + +public func ==(lhs: Expression, rhs: Expression) -> Expression where V.Datatype : Equatable { + return "=".infix(lhs, rhs) +} +public func ==(lhs: Expression, rhs: Expression) -> Expression where V.Datatype : Equatable { + return "=".infix(lhs, rhs) +} +public func ==(lhs: Expression, rhs: Expression) -> Expression where V.Datatype : Equatable { + return "=".infix(lhs, rhs) +} +public func ==(lhs: Expression, rhs: Expression) -> Expression where V.Datatype : Equatable { + return "=".infix(lhs, rhs) +} +public func ==(lhs: Expression, rhs: V) -> Expression where V.Datatype : Equatable { + return "=".infix(lhs, rhs) +} +public func ==(lhs: Expression, rhs: V?) -> Expression where V.Datatype : Equatable { + guard let rhs = rhs else { return "IS".infix(lhs, Expression(value: nil)) } + return "=".infix(lhs, rhs) +} +public func ==(lhs: V, rhs: Expression) -> Expression where V.Datatype : Equatable { + return "=".infix(lhs, rhs) +} +public func ==(lhs: V?, rhs: Expression) -> Expression where V.Datatype : Equatable { + guard let lhs = lhs else { return "IS".infix(Expression(value: nil), rhs) } + return "=".infix(lhs, rhs) +} + +public func !=(lhs: Expression, rhs: Expression) -> Expression where V.Datatype : Equatable { + return infix(lhs, rhs) +} +public func !=(lhs: Expression, rhs: Expression) -> Expression where V.Datatype : Equatable { + return infix(lhs, rhs) +} +public func !=(lhs: Expression, rhs: Expression) -> Expression where V.Datatype : Equatable { + return infix(lhs, rhs) +} +public func !=(lhs: Expression, rhs: Expression) -> Expression where V.Datatype : Equatable { + return infix(lhs, rhs) +} +public func !=(lhs: Expression, rhs: V) -> Expression where V.Datatype : Equatable { + return infix(lhs, rhs) +} +public func !=(lhs: Expression, rhs: V?) -> Expression where V.Datatype : Equatable { + guard let rhs = rhs else { return "IS NOT".infix(lhs, Expression(value: nil)) } + return infix(lhs, rhs) +} +public func !=(lhs: V, rhs: Expression) -> Expression where V.Datatype : Equatable { + return infix(lhs, rhs) +} +public func !=(lhs: V?, rhs: Expression) -> Expression where V.Datatype : Equatable { + guard let lhs = lhs else { return "IS NOT".infix(Expression(value: nil), rhs) } + return infix(lhs, rhs) +} + +public func >(lhs: Expression, rhs: Expression) -> Expression where V.Datatype : Comparable { + return infix(lhs, rhs) +} +public func >(lhs: Expression, rhs: Expression) -> Expression where V.Datatype : Comparable { + return infix(lhs, rhs) +} +public func >(lhs: Expression, rhs: Expression) -> Expression where V.Datatype : Comparable { + return infix(lhs, rhs) +} +public func >(lhs: Expression, rhs: Expression) -> Expression where V.Datatype : Comparable { + return infix(lhs, rhs) +} +public func >(lhs: Expression, rhs: V) -> Expression where V.Datatype : Comparable { + return infix(lhs, rhs) +} +public func >(lhs: Expression, rhs: V) -> Expression where V.Datatype : Comparable { + return infix(lhs, rhs) +} +public func >(lhs: V, rhs: Expression) -> Expression where V.Datatype : Comparable { + return infix(lhs, rhs) +} +public func >(lhs: V, rhs: Expression) -> Expression where V.Datatype : Comparable { + return infix(lhs, rhs) +} + +public func >=(lhs: Expression, rhs: Expression) -> Expression where V.Datatype : Comparable { + return infix(lhs, rhs) +} +public func >=(lhs: Expression, rhs: Expression) -> Expression where V.Datatype : Comparable { + return infix(lhs, rhs) +} +public func >=(lhs: Expression, rhs: Expression) -> Expression where V.Datatype : Comparable { + return infix(lhs, rhs) +} +public func >=(lhs: Expression, rhs: Expression) -> Expression where V.Datatype : Comparable { + return infix(lhs, rhs) +} +public func >=(lhs: Expression, rhs: V) -> Expression where V.Datatype : Comparable { + return infix(lhs, rhs) +} +public func >=(lhs: Expression, rhs: V) -> Expression where V.Datatype : Comparable { + return infix(lhs, rhs) +} +public func >=(lhs: V, rhs: Expression) -> Expression where V.Datatype : Comparable { + return infix(lhs, rhs) +} +public func >=(lhs: V, rhs: Expression) -> Expression where V.Datatype : Comparable { + return infix(lhs, rhs) +} + +public func <(lhs: Expression, rhs: Expression) -> Expression where V.Datatype : Comparable { + return infix(lhs, rhs) +} +public func <(lhs: Expression, rhs: Expression) -> Expression where V.Datatype : Comparable { + return infix(lhs, rhs) +} +public func <(lhs: Expression, rhs: Expression) -> Expression where V.Datatype : Comparable { + return infix(lhs, rhs) +} +public func <(lhs: Expression, rhs: Expression) -> Expression where V.Datatype : Comparable { + return infix(lhs, rhs) +} +public func <(lhs: Expression, rhs: V) -> Expression where V.Datatype : Comparable { + return infix(lhs, rhs) +} +public func <(lhs: Expression, rhs: V) -> Expression where V.Datatype : Comparable { + return infix(lhs, rhs) +} +public func <(lhs: V, rhs: Expression) -> Expression where V.Datatype : Comparable { + return infix(lhs, rhs) +} +public func <(lhs: V, rhs: Expression) -> Expression where V.Datatype : Comparable { + return infix(lhs, rhs) +} + +public func <=(lhs: Expression, rhs: Expression) -> Expression where V.Datatype : Comparable { + return infix(lhs, rhs) +} +public func <=(lhs: Expression, rhs: Expression) -> Expression where V.Datatype : Comparable { + return infix(lhs, rhs) +} +public func <=(lhs: Expression, rhs: Expression) -> Expression where V.Datatype : Comparable { + return infix(lhs, rhs) +} +public func <=(lhs: Expression, rhs: Expression) -> Expression where V.Datatype : Comparable { + return infix(lhs, rhs) +} +public func <=(lhs: Expression, rhs: V) -> Expression where V.Datatype : Comparable { + return infix(lhs, rhs) +} +public func <=(lhs: Expression, rhs: V) -> Expression where V.Datatype : Comparable { + return infix(lhs, rhs) +} +public func <=(lhs: V, rhs: Expression) -> Expression where V.Datatype : Comparable { + return infix(lhs, rhs) +} +public func <=(lhs: V, rhs: Expression) -> Expression where V.Datatype : Comparable { + return infix(lhs, rhs) +} + +public func ~=(lhs: ClosedRange, rhs: Expression) -> Expression where V.Datatype : Binding & Comparable { + return Expression("\(rhs.template) BETWEEN ? AND ?", rhs.bindings + [lhs.lowerBound as? Binding, lhs.upperBound as? Binding]) +} +public func ~=(lhs: ClosedRange, rhs: Expression) -> Expression where V.Datatype : Binding & Comparable { + return Expression("\(rhs.template) BETWEEN ? AND ?", rhs.bindings + [lhs.lowerBound as? Binding, lhs.upperBound as? Binding]) +} + +// MARK: - + +public func &&(lhs: Expression, rhs: Expression) -> Expression { + return "AND".infix(lhs, rhs) +} +public func &&(lhs: Expression, rhs: Expression) -> Expression { + return "AND".infix(lhs, rhs) +} +public func &&(lhs: Expression, rhs: Expression) -> Expression { + return "AND".infix(lhs, rhs) +} +public func &&(lhs: Expression, rhs: Expression) -> Expression { + return "AND".infix(lhs, rhs) +} +public func &&(lhs: Expression, rhs: Bool) -> Expression { + return "AND".infix(lhs, rhs) +} +public func &&(lhs: Expression, rhs: Bool) -> Expression { + return "AND".infix(lhs, rhs) +} +public func &&(lhs: Bool, rhs: Expression) -> Expression { + return "AND".infix(lhs, rhs) +} +public func &&(lhs: Bool, rhs: Expression) -> Expression { + return "AND".infix(lhs, rhs) +} + +public func ||(lhs: Expression, rhs: Expression) -> Expression { + return "OR".infix(lhs, rhs) +} +public func ||(lhs: Expression, rhs: Expression) -> Expression { + return "OR".infix(lhs, rhs) +} +public func ||(lhs: Expression, rhs: Expression) -> Expression { + return "OR".infix(lhs, rhs) +} +public func ||(lhs: Expression, rhs: Expression) -> Expression { + return "OR".infix(lhs, rhs) +} +public func ||(lhs: Expression, rhs: Bool) -> Expression { + return "OR".infix(lhs, rhs) +} +public func ||(lhs: Expression, rhs: Bool) -> Expression { + return "OR".infix(lhs, rhs) +} +public func ||(lhs: Bool, rhs: Expression) -> Expression { + return "OR".infix(lhs, rhs) +} +public func ||(lhs: Bool, rhs: Expression) -> Expression { + return "OR".infix(lhs, rhs) +} + +public prefix func !(rhs: Expression) -> Expression { + return "NOT ".wrap(rhs) +} +public prefix func !(rhs: Expression) -> Expression { + return "NOT ".wrap(rhs) +} diff --git a/mobile/ios/ThirdParty/SQLite.swift/Sources/SQLite/Typed/Query.swift b/mobile/ios/ThirdParty/SQLite.swift/Sources/SQLite/Typed/Query.swift new file mode 100644 index 0000000000..c9d2ea9cc5 --- /dev/null +++ b/mobile/ios/ThirdParty/SQLite.swift/Sources/SQLite/Typed/Query.swift @@ -0,0 +1,1162 @@ +// +// SQLite.swift +// https://github.com/stephencelis/SQLite.swift +// Copyright © 2014-2015 Stephen Celis. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +public protocol QueryType : Expressible { + + var clauses: QueryClauses { get set } + + init(_ name: String, database: String?) + +} + +public protocol SchemaType : QueryType { + + static var identifier: String { get } + +} + +extension SchemaType { + + /// Builds a copy of the query with the `SELECT` clause applied. + /// + /// let users = Table("users") + /// let id = Expression("id") + /// let email = Expression("email") + /// + /// users.select(id, email) + /// // SELECT "id", "email" FROM "users" + /// + /// - Parameter all: A list of expressions to select. + /// + /// - Returns: A query with the given `SELECT` clause applied. + public func select(_ column1: Expressible, _ more: Expressible...) -> Self { + return select(false, [column1] + more) + } + + /// Builds a copy of the query with the `SELECT DISTINCT` clause applied. + /// + /// let users = Table("users") + /// let email = Expression("email") + /// + /// users.select(distinct: email) + /// // SELECT DISTINCT "email" FROM "users" + /// + /// - Parameter columns: A list of expressions to select. + /// + /// - Returns: A query with the given `SELECT DISTINCT` clause applied. + public func select(distinct column1: Expressible, _ more: Expressible...) -> Self { + return select(true, [column1] + more) + } + + /// Builds a copy of the query with the `SELECT` clause applied. + /// + /// let users = Table("users") + /// let id = Expression("id") + /// let email = Expression("email") + /// + /// users.select([id, email]) + /// // SELECT "id", "email" FROM "users" + /// + /// - Parameter all: A list of expressions to select. + /// + /// - Returns: A query with the given `SELECT` clause applied. + public func select(_ all: [Expressible]) -> Self { + return select(false, all) + } + + /// Builds a copy of the query with the `SELECT DISTINCT` clause applied. + /// + /// let users = Table("users") + /// let email = Expression("email") + /// + /// users.select(distinct: [email]) + /// // SELECT DISTINCT "email" FROM "users" + /// + /// - Parameter columns: A list of expressions to select. + /// + /// - Returns: A query with the given `SELECT DISTINCT` clause applied. + public func select(distinct columns: [Expressible]) -> Self { + return select(true, columns) + } + + /// Builds a copy of the query with the `SELECT *` clause applied. + /// + /// let users = Table("users") + /// + /// users.select(*) + /// // SELECT * FROM "users" + /// + /// - Parameter star: A star literal. + /// + /// - Returns: A query with the given `SELECT *` clause applied. + public func select(_ star: Star) -> Self { + return select([star(nil, nil)]) + } + + /// Builds a copy of the query with the `SELECT DISTINCT *` clause applied. + /// + /// let users = Table("users") + /// + /// users.select(distinct: *) + /// // SELECT DISTINCT * FROM "users" + /// + /// - Parameter star: A star literal. + /// + /// - Returns: A query with the given `SELECT DISTINCT *` clause applied. + public func select(distinct star: Star) -> Self { + return select(distinct: [star(nil, nil)]) + } + + /// Builds a scalar copy of the query with the `SELECT` clause applied. + /// + /// let users = Table("users") + /// let id = Expression("id") + /// + /// users.select(id) + /// // SELECT "id" FROM "users" + /// + /// - Parameter all: A list of expressions to select. + /// + /// - Returns: A query with the given `SELECT` clause applied. + public func select(_ column: Expression) -> ScalarQuery { + return select(false, [column]) + } + public func select(_ column: Expression) -> ScalarQuery { + return select(false, [column]) + } + + /// Builds a scalar copy of the query with the `SELECT DISTINCT` clause + /// applied. + /// + /// let users = Table("users") + /// let email = Expression("email") + /// + /// users.select(distinct: email) + /// // SELECT DISTINCT "email" FROM "users" + /// + /// - Parameter column: A list of expressions to select. + /// + /// - Returns: A query with the given `SELECT DISTINCT` clause applied. + public func select(distinct column: Expression) -> ScalarQuery { + return select(true, [column]) + } + public func select(distinct column: Expression) -> ScalarQuery { + return select(true, [column]) + } + + public var count: ScalarQuery { + return select(Expression.count(*)) + } + +} + +extension QueryType { + + fileprivate func select(_ distinct: Bool, _ columns: [Expressible]) -> Q { + var query = Q.init(clauses.from.name, database: clauses.from.database) + query.clauses = clauses + query.clauses.select = (distinct, columns) + return query + } + + // MARK: JOIN + + /// Adds a `JOIN` clause to the query. + /// + /// let users = Table("users") + /// let id = Expression("id") + /// let posts = Table("posts") + /// let userId = Expression("user_id") + /// + /// users.join(posts, on: posts[userId] == users[id]) + /// // SELECT * FROM "users" INNER JOIN "posts" ON ("posts"."user_id" = "users"."id") + /// + /// - Parameters: + /// + /// - table: A query representing the other table. + /// + /// - condition: A boolean expression describing the join condition. + /// + /// - Returns: A query with the given `JOIN` clause applied. + public func join(_ table: QueryType, on condition: Expression) -> Self { + return join(table, on: Expression(condition)) + } + + /// Adds a `JOIN` clause to the query. + /// + /// let users = Table("users") + /// let id = Expression("id") + /// let posts = Table("posts") + /// let userId = Expression("user_id") + /// + /// users.join(posts, on: posts[userId] == users[id]) + /// // SELECT * FROM "users" INNER JOIN "posts" ON ("posts"."user_id" = "users"."id") + /// + /// - Parameters: + /// + /// - table: A query representing the other table. + /// + /// - condition: A boolean expression describing the join condition. + /// + /// - Returns: A query with the given `JOIN` clause applied. + public func join(_ table: QueryType, on condition: Expression) -> Self { + return join(.inner, table, on: condition) + } + + /// Adds a `JOIN` clause to the query. + /// + /// let users = Table("users") + /// let id = Expression("id") + /// let posts = Table("posts") + /// let userId = Expression("user_id") + /// + /// users.join(.LeftOuter, posts, on: posts[userId] == users[id]) + /// // SELECT * FROM "users" LEFT OUTER JOIN "posts" ON ("posts"."user_id" = "users"."id") + /// + /// - Parameters: + /// + /// - type: The `JOIN` operator. + /// + /// - table: A query representing the other table. + /// + /// - condition: A boolean expression describing the join condition. + /// + /// - Returns: A query with the given `JOIN` clause applied. + public func join(_ type: JoinType, _ table: QueryType, on condition: Expression) -> Self { + return join(type, table, on: Expression(condition)) + } + + /// Adds a `JOIN` clause to the query. + /// + /// let users = Table("users") + /// let id = Expression("id") + /// let posts = Table("posts") + /// let userId = Expression("user_id") + /// + /// users.join(.LeftOuter, posts, on: posts[userId] == users[id]) + /// // SELECT * FROM "users" LEFT OUTER JOIN "posts" ON ("posts"."user_id" = "users"."id") + /// + /// - Parameters: + /// + /// - type: The `JOIN` operator. + /// + /// - table: A query representing the other table. + /// + /// - condition: A boolean expression describing the join condition. + /// + /// - Returns: A query with the given `JOIN` clause applied. + public func join(_ type: JoinType, _ table: QueryType, on condition: Expression) -> Self { + var query = self + query.clauses.join.append((type: type, query: table, condition: table.clauses.filters.map { condition && $0 } ?? condition as Expressible)) + return query + } + + // MARK: WHERE + + /// Adds a condition to the query’s `WHERE` clause. + /// + /// let users = Table("users") + /// let id = Expression("id") + /// + /// users.filter(id == 1) + /// // SELECT * FROM "users" WHERE ("id" = 1) + /// + /// - Parameter condition: A boolean expression to filter on. + /// + /// - Returns: A query with the given `WHERE` clause applied. + public func filter(_ predicate: Expression) -> Self { + return filter(Expression(predicate)) + } + + /// Adds a condition to the query’s `WHERE` clause. + /// + /// let users = Table("users") + /// let age = Expression("age") + /// + /// users.filter(age >= 35) + /// // SELECT * FROM "users" WHERE ("age" >= 35) + /// + /// - Parameter condition: A boolean expression to filter on. + /// + /// - Returns: A query with the given `WHERE` clause applied. + public func filter(_ predicate: Expression) -> Self { + var query = self + query.clauses.filters = query.clauses.filters.map { $0 && predicate } ?? predicate + return query + } + + /// Adds a condition to the query’s `WHERE` clause. + /// This is an alias for `filter(predicate)` + public func `where`(_ predicate: Expression) -> Self { + return `where`(Expression(predicate)) + } + + /// Adds a condition to the query’s `WHERE` clause. + /// This is an alias for `filter(predicate)` + public func `where`(_ predicate: Expression) -> Self { + return filter(predicate) + } + + // MARK: GROUP BY + + /// Sets a `GROUP BY` clause on the query. + /// + /// - Parameter by: A list of columns to group by. + /// + /// - Returns: A query with the given `GROUP BY` clause applied. + public func group(_ by: Expressible...) -> Self { + return group(by) + } + + /// Sets a `GROUP BY` clause on the query. + /// + /// - Parameter by: A list of columns to group by. + /// + /// - Returns: A query with the given `GROUP BY` clause applied. + public func group(_ by: [Expressible]) -> Self { + return group(by, nil) + } + + /// Sets a `GROUP BY`-`HAVING` clause on the query. + /// + /// - Parameters: + /// + /// - by: A column to group by. + /// + /// - having: A condition determining which groups are returned. + /// + /// - Returns: A query with the given `GROUP BY`–`HAVING` clause applied. + public func group(_ by: Expressible, having: Expression) -> Self { + return group([by], having: having) + } + + /// Sets a `GROUP BY`-`HAVING` clause on the query. + /// + /// - Parameters: + /// + /// - by: A column to group by. + /// + /// - having: A condition determining which groups are returned. + /// + /// - Returns: A query with the given `GROUP BY`–`HAVING` clause applied. + public func group(_ by: Expressible, having: Expression) -> Self { + return group([by], having: having) + } + + /// Sets a `GROUP BY`-`HAVING` clause on the query. + /// + /// - Parameters: + /// + /// - by: A list of columns to group by. + /// + /// - having: A condition determining which groups are returned. + /// + /// - Returns: A query with the given `GROUP BY`–`HAVING` clause applied. + public func group(_ by: [Expressible], having: Expression) -> Self { + return group(by, Expression(having)) + } + + /// Sets a `GROUP BY`-`HAVING` clause on the query. + /// + /// - Parameters: + /// + /// - by: A list of columns to group by. + /// + /// - having: A condition determining which groups are returned. + /// + /// - Returns: A query with the given `GROUP BY`–`HAVING` clause applied. + public func group(_ by: [Expressible], having: Expression) -> Self { + return group(by, having) + } + + fileprivate func group(_ by: [Expressible], _ having: Expression?) -> Self { + var query = self + query.clauses.group = (by, having) + return query + } + + // MARK: ORDER BY + + /// Sets an `ORDER BY` clause on the query. + /// + /// let users = Table("users") + /// let email = Expression("email") + /// let name = Expression("name") + /// + /// users.order(email.desc, name.asc) + /// // SELECT * FROM "users" ORDER BY "email" DESC, "name" ASC + /// + /// - Parameter by: An ordered list of columns and directions to sort by. + /// + /// - Returns: A query with the given `ORDER BY` clause applied. + public func order(_ by: Expressible...) -> Self { + return order(by) + } + + /// Sets an `ORDER BY` clause on the query. + /// + /// let users = Table("users") + /// let email = Expression("email") + /// let name = Expression("name") + /// + /// users.order([email.desc, name.asc]) + /// // SELECT * FROM "users" ORDER BY "email" DESC, "name" ASC + /// + /// - Parameter by: An ordered list of columns and directions to sort by. + /// + /// - Returns: A query with the given `ORDER BY` clause applied. + public func order(_ by: [Expressible]) -> Self { + var query = self + query.clauses.order = by + return query + } + + // MARK: LIMIT/OFFSET + + /// Sets the LIMIT clause (and resets any OFFSET clause) on the query. + /// + /// let users = Table("users") + /// + /// users.limit(20) + /// // SELECT * FROM "users" LIMIT 20 + /// + /// - Parameter length: The maximum number of rows to return (or `nil` to + /// return unlimited rows). + /// + /// - Returns: A query with the given LIMIT clause applied. + public func limit(_ length: Int?) -> Self { + return limit(length, nil) + } + + /// Sets LIMIT and OFFSET clauses on the query. + /// + /// let users = Table("users") + /// + /// users.limit(20, offset: 20) + /// // SELECT * FROM "users" LIMIT 20 OFFSET 20 + /// + /// - Parameters: + /// + /// - length: The maximum number of rows to return. + /// + /// - offset: The number of rows to skip. + /// + /// - Returns: A query with the given LIMIT and OFFSET clauses applied. + public func limit(_ length: Int, offset: Int) -> Self { + return limit(length, offset) + } + + // prevents limit(nil, offset: 5) + fileprivate func limit(_ length: Int?, _ offset: Int?) -> Self { + var query = self + query.clauses.limit = length.map { ($0, offset) } + return query + } + + // MARK: - Clauses + // + // MARK: SELECT + + // MARK: - + + fileprivate var selectClause: Expressible { + return " ".join([ + Expression(literal: clauses.select.distinct ? "SELECT DISTINCT" : "SELECT"), + ", ".join(clauses.select.columns), + Expression(literal: "FROM"), + tableName(alias: true) + ]) + } + + fileprivate var joinClause: Expressible? { + guard !clauses.join.isEmpty else { + return nil + } + + return " ".join(clauses.join.map { type, query, condition in + " ".join([ + Expression(literal: "\(type.rawValue) JOIN"), + query.tableName(alias: true), + Expression(literal: "ON"), + condition + ]) + }) + } + + fileprivate var whereClause: Expressible? { + guard let filters = clauses.filters else { + return nil + } + + return " ".join([ + Expression(literal: "WHERE"), + filters + ]) + } + + fileprivate var groupByClause: Expressible? { + guard let group = clauses.group else { + return nil + } + + let groupByClause = " ".join([ + Expression(literal: "GROUP BY"), + ", ".join(group.by) + ]) + + guard let having = group.having else { + return groupByClause + } + + return " ".join([ + groupByClause, + " ".join([ + Expression(literal: "HAVING"), + having + ]) + ]) + } + + fileprivate var orderClause: Expressible? { + guard !clauses.order.isEmpty else { + return nil + } + + return " ".join([ + Expression(literal: "ORDER BY"), + ", ".join(clauses.order) + ]) + } + + fileprivate var limitOffsetClause: Expressible? { + guard let limit = clauses.limit else { + return nil + } + + let limitClause = Expression(literal: "LIMIT \(limit.length)") + + guard let offset = limit.offset else { + return limitClause + } + + return " ".join([ + limitClause, + Expression(literal: "OFFSET \(offset)") + ]) + } + + // MARK: - + + public func alias(_ aliasName: String) -> Self { + var query = self + query.clauses.from = (clauses.from.name, aliasName, clauses.from.database) + return query + } + + // MARK: - Operations + // + // MARK: INSERT + + public func insert(_ value: Setter, _ more: Setter...) -> Insert { + return insert([value] + more) + } + + public func insert(_ values: [Setter]) -> Insert { + return insert(nil, values) + } + + public func insert(or onConflict: OnConflict, _ values: Setter...) -> Insert { + return insert(or: onConflict, values) + } + + public func insert(or onConflict: OnConflict, _ values: [Setter]) -> Insert { + return insert(onConflict, values) + } + + fileprivate func insert(_ or: OnConflict?, _ values: [Setter]) -> Insert { + let insert = values.reduce((columns: [Expressible](), values: [Expressible]())) { insert, setter in + (insert.columns + [setter.column], insert.values + [setter.value]) + } + + let clauses: [Expressible?] = [ + Expression(literal: "INSERT"), + or.map { Expression(literal: "OR \($0.rawValue)") }, + Expression(literal: "INTO"), + tableName(), + "".wrap(insert.columns) as Expression, + Expression(literal: "VALUES"), + "".wrap(insert.values) as Expression, + whereClause + ] + + return Insert(" ".join(clauses.flatMap { $0 }).expression) + } + + /// Runs an `INSERT` statement against the query with `DEFAULT VALUES`. + public func insert() -> Insert { + return Insert(" ".join([ + Expression(literal: "INSERT INTO"), + tableName(), + Expression(literal: "DEFAULT VALUES") + ]).expression) + } + + /// Runs an `INSERT` statement against the query with the results of another + /// query. + /// + /// - Parameter query: A query to `SELECT` results from. + /// + /// - Returns: The number of updated rows and statement. + public func insert(_ query: QueryType) -> Update { + return Update(" ".join([ + Expression(literal: "INSERT INTO"), + tableName(), + query.expression + ]).expression) + } + + // MARK: UPDATE + + public func update(_ values: Setter...) -> Update { + return update(values) + } + + public func update(_ values: [Setter]) -> Update { + let clauses: [Expressible?] = [ + Expression(literal: "UPDATE"), + tableName(), + Expression(literal: "SET"), + ", ".join(values.map { " = ".join([$0.column, $0.value]) }), + whereClause + ] + + return Update(" ".join(clauses.flatMap { $0 }).expression) + } + + // MARK: DELETE + + public func delete() -> Delete { + let clauses: [Expressible?] = [ + Expression(literal: "DELETE FROM"), + tableName(), + whereClause + ] + + return Delete(" ".join(clauses.flatMap { $0 }).expression) + } + + // MARK: EXISTS + + public var exists: Select { + return Select(" ".join([ + Expression(literal: "SELECT EXISTS"), + "".wrap(expression) as Expression + ]).expression) + } + + // MARK: - + + /// Prefixes a column expression with the query’s table name or alias. + /// + /// - Parameter column: A column expression. + /// + /// - Returns: A column expression namespaced with the query’s table name or + /// alias. + public func namespace(_ column: Expression) -> Expression { + return Expression(".".join([tableName(), column]).expression) + } + + // FIXME: rdar://problem/18673897 // subscript… + + public subscript(column: Expression) -> Expression { + return namespace(column) + } + public subscript(column: Expression) -> Expression { + return namespace(column) + } + + public subscript(column: Expression) -> Expression { + return namespace(column) + } + public subscript(column: Expression) -> Expression { + return namespace(column) + } + + public subscript(column: Expression) -> Expression { + return namespace(column) + } + public subscript(column: Expression) -> Expression { + return namespace(column) + } + + public subscript(column: Expression) -> Expression { + return namespace(column) + } + public subscript(column: Expression) -> Expression { + return namespace(column) + } + + public subscript(column: Expression) -> Expression { + return namespace(column) + } + public subscript(column: Expression) -> Expression { + return namespace(column) + } + + public subscript(column: Expression) -> Expression { + return namespace(column) + } + public subscript(column: Expression) -> Expression { + return namespace(column) + } + + /// Prefixes a star with the query’s table name or alias. + /// + /// - Parameter star: A literal `*`. + /// + /// - Returns: A `*` expression namespaced with the query’s table name or + /// alias. + public subscript(star: Star) -> Expression { + return namespace(star(nil, nil)) + } + + // MARK: - + + // TODO: alias support + func tableName(alias aliased: Bool = false) -> Expressible { + guard let alias = clauses.from.alias , aliased else { + return database(namespace: clauses.from.alias ?? clauses.from.name) + } + + return " ".join([ + database(namespace: clauses.from.name), + Expression(literal: "AS"), + Expression(alias) + ]) + } + + func tableName(qualified: Bool) -> Expressible { + if qualified { + return tableName() + } + return Expression(clauses.from.alias ?? clauses.from.name) + } + + func database(namespace name: String) -> Expressible { + let name = Expression(name) + + guard let database = clauses.from.database else { + return name + } + + return ".".join([Expression(database), name]) + } + + public var expression: Expression { + let clauses: [Expressible?] = [ + selectClause, + joinClause, + whereClause, + groupByClause, + orderClause, + limitOffsetClause + ] + + return " ".join(clauses.flatMap { $0 }).expression + } + +} + +// TODO: decide: simplify the below with a boxed type instead + +/// Queries a collection of chainable helper functions and expressions to build +/// executable SQL statements. +public struct Table : SchemaType { + + public static let identifier = "TABLE" + + public var clauses: QueryClauses + + public init(_ name: String, database: String? = nil) { + clauses = QueryClauses(name, alias: nil, database: database) + } + +} + +public struct View : SchemaType { + + public static let identifier = "VIEW" + + public var clauses: QueryClauses + + public init(_ name: String, database: String? = nil) { + clauses = QueryClauses(name, alias: nil, database: database) + } + +} + +public struct VirtualTable : SchemaType { + + public static let identifier = "VIRTUAL TABLE" + + public var clauses: QueryClauses + + public init(_ name: String, database: String? = nil) { + clauses = QueryClauses(name, alias: nil, database: database) + } + +} + +// TODO: make `ScalarQuery` work in `QueryType.select()`, `.filter()`, etc. + +public struct ScalarQuery : QueryType { + + public var clauses: QueryClauses + + public init(_ name: String, database: String? = nil) { + clauses = QueryClauses(name, alias: nil, database: database) + } + +} + +// TODO: decide: simplify the below with a boxed type instead + +public struct Select : ExpressionType { + + public var template: String + public var bindings: [Binding?] + + public init(_ template: String, _ bindings: [Binding?]) { + self.template = template + self.bindings = bindings + } + +} + +public struct Insert : ExpressionType { + + public var template: String + public var bindings: [Binding?] + + public init(_ template: String, _ bindings: [Binding?]) { + self.template = template + self.bindings = bindings + } + +} + +public struct Update : ExpressionType { + + public var template: String + public var bindings: [Binding?] + + public init(_ template: String, _ bindings: [Binding?]) { + self.template = template + self.bindings = bindings + } + +} + +public struct Delete : ExpressionType { + + public var template: String + public var bindings: [Binding?] + + public init(_ template: String, _ bindings: [Binding?]) { + self.template = template + self.bindings = bindings + } + +} + +extension Connection { + + public func prepare(_ query: QueryType) throws -> AnySequence { + let expression = query.expression + let statement = try prepare(expression.template, expression.bindings) + + let columnNames: [String: Int] = try { + var (columnNames, idx) = ([String: Int](), 0) + column: for each in query.clauses.select.columns { + var names = each.expression.template.characters.split { $0 == "." }.map(String.init) + let column = names.removeLast() + let namespace = names.joined(separator: ".") + + func expandGlob(_ namespace: Bool) -> ((QueryType) throws -> Void) { + return { (query: QueryType) throws -> (Void) in + var q = type(of: query).init(query.clauses.from.name, database: query.clauses.from.database) + q.clauses.select = query.clauses.select + let e = q.expression + var names = try self.prepare(e.template, e.bindings).columnNames.map { $0.quote() } + if namespace { names = names.map { "\(query.tableName().expression.template).\($0)" } } + for name in names { columnNames[name] = idx; idx += 1 } + } + } + + if column == "*" { + var select = query + select.clauses.select = (false, [Expression(literal: "*") as Expressible]) + let queries = [select] + query.clauses.join.map { $0.query } + if !namespace.isEmpty { + for q in queries { + if q.tableName().expression.template == namespace { + try expandGlob(true)(q) + continue column + } + } + fatalError("no such table: \(namespace)") + } + for q in queries { + try expandGlob(query.clauses.join.count > 0)(q) + } + continue + } + + columnNames[each.expression.template] = idx + idx += 1 + } + return columnNames + }() + + return AnySequence { + AnyIterator { statement.next().map { Row(columnNames, $0) } } + } + } + + public func scalar(_ query: ScalarQuery) throws -> V { + let expression = query.expression + return value(try scalar(expression.template, expression.bindings)) + } + + public func scalar(_ query: ScalarQuery) throws -> V.ValueType? { + let expression = query.expression + guard let value = try scalar(expression.template, expression.bindings) as? V.Datatype else { return nil } + return V.fromDatatypeValue(value) + } + + public func scalar(_ query: Select) throws -> V { + let expression = query.expression + return value(try scalar(expression.template, expression.bindings)) + } + + public func scalar(_ query: Select) throws -> V.ValueType? { + let expression = query.expression + guard let value = try scalar(expression.template, expression.bindings) as? V.Datatype else { return nil } + return V.fromDatatypeValue(value) + } + + public func pluck(_ query: QueryType) throws -> Row? { + return try prepare(query.limit(1, query.clauses.limit?.offset)).makeIterator().next() + } + + /// Runs an `Insert` query. + /// + /// - SeeAlso: `QueryType.insert(value:_:)` + /// - SeeAlso: `QueryType.insert(values:)` + /// - SeeAlso: `QueryType.insert(or:_:)` + /// - SeeAlso: `QueryType.insert()` + /// + /// - Parameter query: An insert query. + /// + /// - Returns: The insert’s rowid. + @discardableResult public func run(_ query: Insert) throws -> Int64 { + let expression = query.expression + return try sync { + try self.run(expression.template, expression.bindings) + return self.lastInsertRowid + } + } + + /// Runs an `Update` query. + /// + /// - SeeAlso: `QueryType.insert(query:)` + /// - SeeAlso: `QueryType.update(values:)` + /// + /// - Parameter query: An update query. + /// + /// - Returns: The number of updated rows. + @discardableResult public func run(_ query: Update) throws -> Int { + let expression = query.expression + return try sync { + try self.run(expression.template, expression.bindings) + return self.changes + } + } + + /// Runs a `Delete` query. + /// + /// - SeeAlso: `QueryType.delete()` + /// + /// - Parameter query: A delete query. + /// + /// - Returns: The number of deleted rows. + @discardableResult public func run(_ query: Delete) throws -> Int { + let expression = query.expression + return try sync { + try self.run(expression.template, expression.bindings) + return self.changes + } + } + +} + +public struct Row { + + fileprivate let columnNames: [String: Int] + + fileprivate let values: [Binding?] + + fileprivate init(_ columnNames: [String: Int], _ values: [Binding?]) { + self.columnNames = columnNames + self.values = values + } + + /// Returns a row’s value for the given column. + /// + /// - Parameter column: An expression representing a column selected in a Query. + /// + /// - Returns: The value for the given column. + public func get(_ column: Expression) -> V { + return get(Expression(column))! + } + public func get(_ column: Expression) -> V? { + func valueAtIndex(_ idx: Int) -> V? { + guard let value = values[idx] as? V.Datatype else { return nil } + return (V.fromDatatypeValue(value) as? V)! + } + + guard let idx = columnNames[column.template] else { + let similar = Array(columnNames.keys).filter { $0.hasSuffix(".\(column.template)") } + + switch similar.count { + case 0: + fatalError("no such column '\(column.template)' in columns: \(columnNames.keys.sorted())") + case 1: + return valueAtIndex(columnNames[similar[0]]!) + default: + fatalError("ambiguous column '\(column.template)' (please disambiguate: \(similar))") + } + } + + return valueAtIndex(idx) + } + + // FIXME: rdar://problem/18673897 // subscript… + + public subscript(column: Expression) -> Blob { + return get(column) + } + public subscript(column: Expression) -> Blob? { + return get(column) + } + + public subscript(column: Expression) -> Bool { + return get(column) + } + public subscript(column: Expression) -> Bool? { + return get(column) + } + + public subscript(column: Expression) -> Double { + return get(column) + } + public subscript(column: Expression) -> Double? { + return get(column) + } + + public subscript(column: Expression) -> Int { + return get(column) + } + public subscript(column: Expression) -> Int? { + return get(column) + } + + public subscript(column: Expression) -> Int64 { + return get(column) + } + public subscript(column: Expression) -> Int64? { + return get(column) + } + + public subscript(column: Expression) -> String { + return get(column) + } + public subscript(column: Expression) -> String? { + return get(column) + } + +} + +/// Determines the join operator for a query’s `JOIN` clause. +public enum JoinType : String { + + /// A `CROSS` join. + case cross = "CROSS" + + /// An `INNER` join. + case inner = "INNER" + + /// A `LEFT OUTER` join. + case leftOuter = "LEFT OUTER" + +} + +/// ON CONFLICT resolutions. +public enum OnConflict: String { + + case replace = "REPLACE" + + case rollback = "ROLLBACK" + + case abort = "ABORT" + + case fail = "FAIL" + + case ignore = "IGNORE" + +} + +// MARK: - Private + +public struct QueryClauses { + + var select = (distinct: false, columns: [Expression(literal: "*") as Expressible]) + + var from: (name: String, alias: String?, database: String?) + + var join = [(type: JoinType, query: QueryType, condition: Expressible)]() + + var filters: Expression? + + var group: (by: [Expressible], having: Expression?)? + + var order = [Expressible]() + + var limit: (length: Int, offset: Int?)? + + fileprivate init(_ name: String, alias: String?, database: String?) { + self.from = (name, alias, database) + } + +} diff --git a/mobile/ios/ThirdParty/SQLite.swift/Sources/SQLite/Typed/Schema.swift b/mobile/ios/ThirdParty/SQLite.swift/Sources/SQLite/Typed/Schema.swift new file mode 100644 index 0000000000..1388170e47 --- /dev/null +++ b/mobile/ios/ThirdParty/SQLite.swift/Sources/SQLite/Typed/Schema.swift @@ -0,0 +1,519 @@ +// +// SQLite.swift +// https://github.com/stephencelis/SQLite.swift +// Copyright © 2014-2015 Stephen Celis. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +extension SchemaType { + + // MARK: - DROP TABLE / VIEW / VIRTUAL TABLE + + public func drop(ifExists: Bool = false) -> String { + return drop("TABLE", tableName(), ifExists) + } + +} + +extension Table { + + // MARK: - CREATE TABLE + + public func create(temporary: Bool = false, ifNotExists: Bool = false, block: (TableBuilder) -> Void) -> String { + let builder = TableBuilder() + + block(builder) + + let clauses: [Expressible?] = [ + create(Table.identifier, tableName(), temporary ? .Temporary : nil, ifNotExists), + "".wrap(builder.definitions) as Expression + ] + + return " ".join(clauses.flatMap { $0 }).asSQL() + } + + public func create(_ query: QueryType, temporary: Bool = false, ifNotExists: Bool = false) -> String { + let clauses: [Expressible?] = [ + create(Table.identifier, tableName(), temporary ? .Temporary : nil, ifNotExists), + Expression(literal: "AS"), + query + ] + + return " ".join(clauses.flatMap { $0 }).asSQL() + } + + // MARK: - ALTER TABLE … ADD COLUMN + + public func addColumn(_ name: Expression, check: Expression? = nil, defaultValue: V) -> String { + return addColumn(definition(name, V.declaredDatatype, nil, false, false, check, defaultValue, nil, nil)) + } + + public func addColumn(_ name: Expression, check: Expression, defaultValue: V) -> String { + return addColumn(definition(name, V.declaredDatatype, nil, false, false, check, defaultValue, nil, nil)) + } + + public func addColumn(_ name: Expression, check: Expression? = nil, defaultValue: V? = nil) -> String { + return addColumn(definition(name, V.declaredDatatype, nil, true, false, check, defaultValue, nil, nil)) + } + + public func addColumn(_ name: Expression, check: Expression, defaultValue: V? = nil) -> String { + return addColumn(definition(name, V.declaredDatatype, nil, true, false, check, defaultValue, nil, nil)) + } + + public func addColumn(_ name: Expression, unique: Bool = false, check: Expression? = nil, references table: QueryType, _ other: Expression) -> String where V.Datatype == Int64 { + return addColumn(definition(name, V.declaredDatatype, nil, false, unique, check, nil, (table, other), nil)) + } + + public func addColumn(_ name: Expression, unique: Bool = false, check: Expression, references table: QueryType, _ other: Expression) -> String where V.Datatype == Int64 { + return addColumn(definition(name, V.declaredDatatype, nil, false, unique, check, nil, (table, other), nil)) + } + + public func addColumn(_ name: Expression, unique: Bool = false, check: Expression? = nil, references table: QueryType, _ other: Expression) -> String where V.Datatype == Int64 { + return addColumn(definition(name, V.declaredDatatype, nil, true, unique, check, nil, (table, other), nil)) + } + + public func addColumn(_ name: Expression, unique: Bool = false, check: Expression, references table: QueryType, _ other: Expression) -> String where V.Datatype == Int64 { + return addColumn(definition(name, V.declaredDatatype, nil, true, unique, check, nil, (table, other), nil)) + } + + public func addColumn(_ name: Expression, check: Expression? = nil, defaultValue: V, collate: Collation) -> String where V.Datatype == String { + return addColumn(definition(name, V.declaredDatatype, nil, false, false, check, defaultValue, nil, collate)) + } + + public func addColumn(_ name: Expression, check: Expression, defaultValue: V, collate: Collation) -> String where V.Datatype == String { + return addColumn(definition(name, V.declaredDatatype, nil, false, false, check, defaultValue, nil, collate)) + } + + public func addColumn(_ name: Expression, check: Expression? = nil, defaultValue: V? = nil, collate: Collation) -> String where V.Datatype == String { + return addColumn(definition(name, V.declaredDatatype, nil, true, false, check, defaultValue, nil, collate)) + } + + public func addColumn(_ name: Expression, check: Expression, defaultValue: V? = nil, collate: Collation) -> String where V.Datatype == String { + return addColumn(definition(name, V.declaredDatatype, nil, true, false, check, defaultValue, nil, collate)) + } + + fileprivate func addColumn(_ expression: Expressible) -> String { + return " ".join([ + Expression(literal: "ALTER TABLE"), + tableName(), + Expression(literal: "ADD COLUMN"), + expression + ]).asSQL() + } + + // MARK: - ALTER TABLE … RENAME TO + + public func rename(_ to: Table) -> String { + return rename(to: to) + } + + // MARK: - CREATE INDEX + + public func createIndex(_ columns: Expressible...) -> String { + return createIndex(columns) + } + + public func createIndex(_ columns: [Expressible], unique: Bool = false, ifNotExists: Bool = false) -> String { + let clauses: [Expressible?] = [ + create("INDEX", indexName(columns), unique ? .Unique : nil, ifNotExists), + Expression(literal: "ON"), + tableName(qualified: false), + "".wrap(columns) as Expression + ] + + return " ".join(clauses.flatMap { $0 }).asSQL() + } + + // MARK: - DROP INDEX + + public func dropIndex(_ columns: Expressible...) -> String { + return dropIndex(columns) + } + + public func dropIndex(_ columns: [Expressible], ifExists: Bool = false) -> String { + return drop("INDEX", indexName(columns), ifExists) + } + + fileprivate func indexName(_ columns: [Expressible]) -> Expressible { + let string = (["index", clauses.from.name, "on"] + columns.map { $0.expression.template }).joined(separator: " ").lowercased() + + let index = string.characters.reduce("") { underscored, character in + guard character != "\"" else { + return underscored + } + guard "a"..."z" ~= character || "0"..."9" ~= character else { + return underscored + "_" + } + return underscored + String(character) + } + + return database(namespace: index) + } + +} + +extension View { + + // MARK: - CREATE VIEW + + public func create(_ query: QueryType, temporary: Bool = false, ifNotExists: Bool = false) -> String { + let clauses: [Expressible?] = [ + create(View.identifier, tableName(), temporary ? .Temporary : nil, ifNotExists), + Expression(literal: "AS"), + query + ] + + return " ".join(clauses.flatMap { $0 }).asSQL() + } + + // MARK: - DROP VIEW + + public func drop(ifExists: Bool = false) -> String { + return drop("VIEW", tableName(), ifExists) + } + +} + +extension VirtualTable { + + // MARK: - CREATE VIRTUAL TABLE + + public func create(_ using: Module, ifNotExists: Bool = false) -> String { + let clauses: [Expressible?] = [ + create(VirtualTable.identifier, tableName(), nil, ifNotExists), + Expression(literal: "USING"), + using + ] + + return " ".join(clauses.flatMap { $0 }).asSQL() + } + + // MARK: - ALTER TABLE … RENAME TO + + public func rename(_ to: VirtualTable) -> String { + return rename(to: to) + } + +} + +public final class TableBuilder { + + fileprivate var definitions = [Expressible]() + + public func column(_ name: Expression, unique: Bool = false, check: Expression? = nil, defaultValue: Expression? = nil) { + column(name, V.declaredDatatype, nil, false, unique, check, defaultValue, nil, nil) + } + + public func column(_ name: Expression, unique: Bool = false, check: Expression? = nil, defaultValue: V) { + column(name, V.declaredDatatype, nil, false, unique, check, defaultValue, nil, nil) + } + + public func column(_ name: Expression, unique: Bool = false, check: Expression, defaultValue: Expression? = nil) { + column(name, V.declaredDatatype, nil, false, unique, check, defaultValue, nil, nil) + } + + public func column(_ name: Expression, unique: Bool = false, check: Expression, defaultValue: V) { + column(name, V.declaredDatatype, nil, false, unique, check, defaultValue, nil, nil) + } + + public func column(_ name: Expression, unique: Bool = false, check: Expression? = nil, defaultValue: Expression? = nil) { + column(name, V.declaredDatatype, nil, true, unique, check, defaultValue, nil, nil) + } + + public func column(_ name: Expression, unique: Bool = false, check: Expression? = nil, defaultValue: Expression) { + column(name, V.declaredDatatype, nil, true, unique, check, defaultValue, nil, nil) + } + + public func column(_ name: Expression, unique: Bool = false, check: Expression? = nil, defaultValue: V) { + column(name, V.declaredDatatype, nil, true, unique, check, defaultValue, nil, nil) + } + + public func column(_ name: Expression, unique: Bool = false, check: Expression, defaultValue: Expression? = nil) { + column(name, V.declaredDatatype, nil, true, unique, check, defaultValue, nil, nil) + } + + public func column(_ name: Expression, unique: Bool = false, check: Expression, defaultValue: Expression) { + column(name, V.declaredDatatype, nil, true, unique, check, defaultValue, nil, nil) + } + + public func column(_ name: Expression, unique: Bool = false, check: Expression, defaultValue: V) { + column(name, V.declaredDatatype, nil, true, unique, check, defaultValue, nil, nil) + } + + public func column(_ name: Expression, primaryKey: Bool, check: Expression? = nil, defaultValue: Expression? = nil) { + column(name, V.declaredDatatype, primaryKey ? .default : nil, false, false, check, defaultValue, nil, nil) + } + + public func column(_ name: Expression, primaryKey: Bool, check: Expression, defaultValue: Expression? = nil) { + column(name, V.declaredDatatype, primaryKey ? .default : nil, false, false, check, defaultValue, nil, nil) + } + + public func column(_ name: Expression, primaryKey: PrimaryKey, check: Expression? = nil) where V.Datatype == Int64 { + column(name, V.declaredDatatype, primaryKey, false, false, check, nil, nil, nil) + } + + public func column(_ name: Expression, primaryKey: PrimaryKey, check: Expression) where V.Datatype == Int64 { + column(name, V.declaredDatatype, primaryKey, false, false, check, nil, nil, nil) + } + + public func column(_ name: Expression, unique: Bool = false, check: Expression? = nil, references table: QueryType, _ other: Expression) where V.Datatype == Int64 { + column(name, V.declaredDatatype, nil, false, unique, check, nil, (table, other), nil) + } + + public func column(_ name: Expression, unique: Bool = false, check: Expression, references table: QueryType, _ other: Expression) where V.Datatype == Int64 { + column(name, V.declaredDatatype, nil, false, unique, check, nil, (table, other), nil) + } + + public func column(_ name: Expression, unique: Bool = false, check: Expression? = nil, references table: QueryType, _ other: Expression) where V.Datatype == Int64 { + column(name, V.declaredDatatype, nil, true, unique, check, nil, (table, other), nil) + } + + public func column(_ name: Expression, unique: Bool = false, check: Expression, references table: QueryType, _ other: Expression) where V.Datatype == Int64 { + column(name, V.declaredDatatype, nil, true, unique, check, nil, (table, other), nil) + } + + public func column(_ name: Expression, unique: Bool = false, check: Expression? = nil, defaultValue: Expression? = nil, collate: Collation) where V.Datatype == String { + column(name, V.declaredDatatype, nil, false, unique, check, defaultValue, nil, collate) + } + + public func column(_ name: Expression, unique: Bool = false, check: Expression? = nil, defaultValue: V, collate: Collation) where V.Datatype == String { + column(name, V.declaredDatatype, nil, false, unique, check, defaultValue, nil, collate) + } + + public func column(_ name: Expression, unique: Bool = false, check: Expression, defaultValue: Expression? = nil, collate: Collation) where V.Datatype == String { + column(name, V.declaredDatatype, nil, false, unique, check, defaultValue, nil, collate) + } + + public func column(_ name: Expression, unique: Bool = false, check: Expression, defaultValue: V, collate: Collation) where V.Datatype == String { + column(name, V.declaredDatatype, nil, false, unique, check, defaultValue, nil, collate) + } + + public func column(_ name: Expression, unique: Bool = false, check: Expression? = nil, defaultValue: Expression? = nil, collate: Collation) where V.Datatype == String { + column(name, V.declaredDatatype, nil, false, unique, check, defaultValue, nil, collate) + } + + public func column(_ name: Expression, unique: Bool = false, check: Expression? = nil, defaultValue: Expression, collate: Collation) where V.Datatype == String { + column(name, V.declaredDatatype, nil, false, unique, check, defaultValue, nil, collate) + } + + public func column(_ name: Expression, unique: Bool = false, check: Expression? = nil, defaultValue: V, collate: Collation) where V.Datatype == String { + column(name, V.declaredDatatype, nil, false, unique, check, defaultValue, nil, collate) + } + + public func column(_ name: Expression, unique: Bool = false, check: Expression, defaultValue: Expression? = nil, collate: Collation) where V.Datatype == String { + column(name, V.declaredDatatype, nil, false, unique, check, defaultValue, nil, collate) + } + + public func column(_ name: Expression, unique: Bool = false, check: Expression, defaultValue: Expression, collate: Collation) where V.Datatype == String { + column(name, V.declaredDatatype, nil, false, unique, check, defaultValue, nil, collate) + } + + public func column(_ name: Expression, unique: Bool = false, check: Expression, defaultValue: V, collate: Collation) where V.Datatype == String { + column(name, V.declaredDatatype, nil, false, unique, check, defaultValue, nil, collate) + } + + fileprivate func column(_ name: Expressible, _ datatype: String, _ primaryKey: PrimaryKey?, _ null: Bool, _ unique: Bool, _ check: Expressible?, _ defaultValue: Expressible?, _ references: (QueryType, Expressible)?, _ collate: Collation?) { + definitions.append(definition(name, datatype, primaryKey, null, unique, check, defaultValue, references, collate)) + } + + // MARK: - + + public func primaryKey(_ column: Expression) { + primaryKey([column]) + } + + public func primaryKey(_ compositeA: Expression, _ b: Expression) { + primaryKey([compositeA, b]) + } + + public func primaryKey(_ compositeA: Expression, _ b: Expression, _ c: Expression) { + primaryKey([compositeA, b, c]) + } + + fileprivate func primaryKey(_ composite: [Expressible]) { + definitions.append("PRIMARY KEY".prefix(composite)) + } + + public func unique(_ columns: Expressible...) { + unique(columns) + } + + public func unique(_ columns: [Expressible]) { + definitions.append("UNIQUE".prefix(columns)) + } + + public func check(_ condition: Expression) { + check(Expression(condition)) + } + + public func check(_ condition: Expression) { + definitions.append("CHECK".prefix(condition)) + } + + public enum Dependency: String { + + case noAction = "NO ACTION" + + case restrict = "RESTRICT" + + case setNull = "SET NULL" + + case setDefault = "SET DEFAULT" + + case cascade = "CASCADE" + + } + + public func foreignKey(_ column: Expression, references table: QueryType, _ other: Expression, update: Dependency? = nil, delete: Dependency? = nil) { + foreignKey(column, (table, other), update, delete) + } + + public func foreignKey(_ column: Expression, references table: QueryType, _ other: Expression, update: Dependency? = nil, delete: Dependency? = nil) { + foreignKey(column, (table, other), update, delete) + } + + public func foreignKey(_ composite: (Expression, Expression), references table: QueryType, _ other: (Expression, Expression), update: Dependency? = nil, delete: Dependency? = nil) { + let composite = ", ".join([composite.0, composite.1]) + let references = (table, ", ".join([other.0, other.1])) + + foreignKey(composite, references, update, delete) + } + + public func foreignKey(_ composite: (Expression, Expression, Expression), references table: QueryType, _ other: (Expression, Expression, Expression), update: Dependency? = nil, delete: Dependency? = nil) { + let composite = ", ".join([composite.0, composite.1, composite.2]) + let references = (table, ", ".join([other.0, other.1, other.2])) + + foreignKey(composite, references, update, delete) + } + + fileprivate func foreignKey(_ column: Expressible, _ references: (QueryType, Expressible), _ update: Dependency?, _ delete: Dependency?) { + let clauses: [Expressible?] = [ + "FOREIGN KEY".prefix(column), + reference(references), + update.map { Expression(literal: "ON UPDATE \($0.rawValue)") }, + delete.map { Expression(literal: "ON DELETE \($0.rawValue)") } + ] + + definitions.append(" ".join(clauses.flatMap { $0 })) + } + +} + +public enum PrimaryKey { + + case `default` + + case autoincrement + +} + +public struct Module { + + fileprivate let name: String + + fileprivate let arguments: [Expressible] + + public init(_ name: String, _ arguments: [Expressible]) { + self.init(name: name.quote(), arguments: arguments) + } + + init(name: String, arguments: [Expressible]) { + self.name = name + self.arguments = arguments + } + +} + +extension Module : Expressible { + + public var expression: Expression { + return name.wrap(arguments) + } + +} + +// MARK: - Private + +private extension QueryType { + + func create(_ identifier: String, _ name: Expressible, _ modifier: Modifier?, _ ifNotExists: Bool) -> Expressible { + let clauses: [Expressible?] = [ + Expression(literal: "CREATE"), + modifier.map { Expression(literal: $0.rawValue) }, + Expression(literal: identifier), + ifNotExists ? Expression(literal: "IF NOT EXISTS") : nil, + name + ] + + return " ".join(clauses.flatMap { $0 }) + } + + func rename(to: Self) -> String { + return " ".join([ + Expression(literal: "ALTER TABLE"), + tableName(), + Expression(literal: "RENAME TO"), + Expression(to.clauses.from.name) + ]).asSQL() + } + + func drop(_ identifier: String, _ name: Expressible, _ ifExists: Bool) -> String { + let clauses: [Expressible?] = [ + Expression(literal: "DROP \(identifier)"), + ifExists ? Expression(literal: "IF EXISTS") : nil, + name + ] + + return " ".join(clauses.flatMap { $0 }).asSQL() + } + +} + +private func definition(_ column: Expressible, _ datatype: String, _ primaryKey: PrimaryKey?, _ null: Bool, _ unique: Bool, _ check: Expressible?, _ defaultValue: Expressible?, _ references: (QueryType, Expressible)?, _ collate: Collation?) -> Expressible { + let clauses: [Expressible?] = [ + column, + Expression(literal: datatype), + primaryKey.map { Expression(literal: $0 == .autoincrement ? "PRIMARY KEY AUTOINCREMENT" : "PRIMARY KEY") }, + null ? nil : Expression(literal: "NOT NULL"), + unique ? Expression(literal: "UNIQUE") : nil, + check.map { " ".join([Expression(literal: "CHECK"), $0]) }, + defaultValue.map { "DEFAULT".prefix($0) }, + references.map(reference), + collate.map { " ".join([Expression(literal: "COLLATE"), $0]) } + ] + + return " ".join(clauses.flatMap { $0 }) +} + +private func reference(_ primary: (QueryType, Expressible)) -> Expressible { + return " ".join([ + Expression(literal: "REFERENCES"), + primary.0.tableName(qualified: false), + "".wrap(primary.1) as Expression + ]) +} + +private enum Modifier : String { + + case Unique = "UNIQUE" + + case Temporary = "TEMPORARY" + +} diff --git a/mobile/ios/ThirdParty/SQLite.swift/Sources/SQLite/Typed/Setter.swift b/mobile/ios/ThirdParty/SQLite.swift/Sources/SQLite/Typed/Setter.swift new file mode 100644 index 0000000000..86f16fcae3 --- /dev/null +++ b/mobile/ios/ThirdParty/SQLite.swift/Sources/SQLite/Typed/Setter.swift @@ -0,0 +1,277 @@ +// +// SQLite.swift +// https://github.com/stephencelis/SQLite.swift +// Copyright © 2014-2015 Stephen Celis. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +precedencegroup ColumnAssignment { + associativity: left + assignment: true + lowerThan: AssignmentPrecedence +} + +infix operator <- : ColumnAssignment + +public struct Setter { + + let column: Expressible + let value: Expressible + + fileprivate init(column: Expression, value: Expression) { + self.column = column + self.value = value + } + + fileprivate init(column: Expression, value: V) { + self.column = column + self.value = value + } + + fileprivate init(column: Expression, value: Expression) { + self.column = column + self.value = value + } + + fileprivate init(column: Expression, value: Expression) { + self.column = column + self.value = value + } + + fileprivate init(column: Expression, value: V?) { + self.column = column + self.value = Expression(value: value) + } + +} + +extension Setter : Expressible { + + public var expression: Expression { + return "=".infix(column, value, wrap: false) + } + +} + +public func <-(column: Expression, value: Expression) -> Setter { + return Setter(column: column, value: value) +} +public func <-(column: Expression, value: V) -> Setter { + return Setter(column: column, value: value) +} +public func <-(column: Expression, value: Expression) -> Setter { + return Setter(column: column, value: value) +} +public func <-(column: Expression, value: Expression) -> Setter { + return Setter(column: column, value: value) +} +public func <-(column: Expression, value: V?) -> Setter { + return Setter(column: column, value: value) +} + +public func +=(column: Expression, value: Expression) -> Setter { + return column <- column + value +} +public func +=(column: Expression, value: String) -> Setter { + return column <- column + value +} +public func +=(column: Expression, value: Expression) -> Setter { + return column <- column + value +} +public func +=(column: Expression, value: Expression) -> Setter { + return column <- column + value +} +public func +=(column: Expression, value: String) -> Setter { + return column <- column + value +} + +public func +=(column: Expression, value: Expression) -> Setter where V.Datatype : Number { + return column <- column + value +} +public func +=(column: Expression, value: V) -> Setter where V.Datatype : Number { + return column <- column + value +} +public func +=(column: Expression, value: Expression) -> Setter where V.Datatype : Number { + return column <- column + value +} +public func +=(column: Expression, value: Expression) -> Setter where V.Datatype : Number { + return column <- column + value +} +public func +=(column: Expression, value: V) -> Setter where V.Datatype : Number { + return column <- column + value +} + +public func -=(column: Expression, value: Expression) -> Setter where V.Datatype : Number { + return column <- column - value +} +public func -=(column: Expression, value: V) -> Setter where V.Datatype : Number { + return column <- column - value +} +public func -=(column: Expression, value: Expression) -> Setter where V.Datatype : Number { + return column <- column - value +} +public func -=(column: Expression, value: Expression) -> Setter where V.Datatype : Number { + return column <- column - value +} +public func -=(column: Expression, value: V) -> Setter where V.Datatype : Number { + return column <- column - value +} + +public func *=(column: Expression, value: Expression) -> Setter where V.Datatype : Number { + return column <- column * value +} +public func *=(column: Expression, value: V) -> Setter where V.Datatype : Number { + return column <- column * value +} +public func *=(column: Expression, value: Expression) -> Setter where V.Datatype : Number { + return column <- column * value +} +public func *=(column: Expression, value: Expression) -> Setter where V.Datatype : Number { + return column <- column * value +} +public func *=(column: Expression, value: V) -> Setter where V.Datatype : Number { + return column <- column * value +} + +public func /=(column: Expression, value: Expression) -> Setter where V.Datatype : Number { + return column <- column / value +} +public func /=(column: Expression, value: V) -> Setter where V.Datatype : Number { + return column <- column / value +} +public func /=(column: Expression, value: Expression) -> Setter where V.Datatype : Number { + return column <- column / value +} +public func /=(column: Expression, value: Expression) -> Setter where V.Datatype : Number { + return column <- column / value +} +public func /=(column: Expression, value: V) -> Setter where V.Datatype : Number { + return column <- column / value +} + +public func %=(column: Expression, value: Expression) -> Setter where V.Datatype == Int64 { + return column <- column % value +} +public func %=(column: Expression, value: V) -> Setter where V.Datatype == Int64 { + return column <- column % value +} +public func %=(column: Expression, value: Expression) -> Setter where V.Datatype == Int64 { + return column <- column % value +} +public func %=(column: Expression, value: Expression) -> Setter where V.Datatype == Int64 { + return column <- column % value +} +public func %=(column: Expression, value: V) -> Setter where V.Datatype == Int64 { + return column <- column % value +} + +public func <<=(column: Expression, value: Expression) -> Setter where V.Datatype == Int64 { + return column <- column << value +} +public func <<=(column: Expression, value: V) -> Setter where V.Datatype == Int64 { + return column <- column << value +} +public func <<=(column: Expression, value: Expression) -> Setter where V.Datatype == Int64 { + return column <- column << value +} +public func <<=(column: Expression, value: Expression) -> Setter where V.Datatype == Int64 { + return column <- column << value +} +public func <<=(column: Expression, value: V) -> Setter where V.Datatype == Int64 { + return column <- column << value +} + +public func >>=(column: Expression, value: Expression) -> Setter where V.Datatype == Int64 { + return column <- column >> value +} +public func >>=(column: Expression, value: V) -> Setter where V.Datatype == Int64 { + return column <- column >> value +} +public func >>=(column: Expression, value: Expression) -> Setter where V.Datatype == Int64 { + return column <- column >> value +} +public func >>=(column: Expression, value: Expression) -> Setter where V.Datatype == Int64 { + return column <- column >> value +} +public func >>=(column: Expression, value: V) -> Setter where V.Datatype == Int64 { + return column <- column >> value +} + +public func &=(column: Expression, value: Expression) -> Setter where V.Datatype == Int64 { + return column <- column & value +} +public func &=(column: Expression, value: V) -> Setter where V.Datatype == Int64 { + return column <- column & value +} +public func &=(column: Expression, value: Expression) -> Setter where V.Datatype == Int64 { + return column <- column & value +} +public func &=(column: Expression, value: Expression) -> Setter where V.Datatype == Int64 { + return column <- column & value +} +public func &=(column: Expression, value: V) -> Setter where V.Datatype == Int64 { + return column <- column & value +} + +public func |=(column: Expression, value: Expression) -> Setter where V.Datatype == Int64 { + return column <- column | value +} +public func |=(column: Expression, value: V) -> Setter where V.Datatype == Int64 { + return column <- column | value +} +public func |=(column: Expression, value: Expression) -> Setter where V.Datatype == Int64 { + return column <- column | value +} +public func |=(column: Expression, value: Expression) -> Setter where V.Datatype == Int64 { + return column <- column | value +} +public func |=(column: Expression, value: V) -> Setter where V.Datatype == Int64 { + return column <- column | value +} + +public func ^=(column: Expression, value: Expression) -> Setter where V.Datatype == Int64 { + return column <- column ^ value +} +public func ^=(column: Expression, value: V) -> Setter where V.Datatype == Int64 { + return column <- column ^ value +} +public func ^=(column: Expression, value: Expression) -> Setter where V.Datatype == Int64 { + return column <- column ^ value +} +public func ^=(column: Expression, value: Expression) -> Setter where V.Datatype == Int64 { + return column <- column ^ value +} +public func ^=(column: Expression, value: V) -> Setter where V.Datatype == Int64 { + return column <- column ^ value +} + +public postfix func ++(column: Expression) -> Setter where V.Datatype == Int64 { + return Expression(column) += 1 +} +public postfix func ++(column: Expression) -> Setter where V.Datatype == Int64 { + return Expression(column) += 1 +} + +public postfix func --(column: Expression) -> Setter where V.Datatype == Int64 { + return Expression(column) -= 1 +} +public postfix func --(column: Expression) -> Setter where V.Datatype == Int64 { + return Expression(column) -= 1 +} diff --git a/mobile/ios/ThirdParty/SQLite.swift/Sources/SQLiteObjc/SQLite-Bridging.m b/mobile/ios/ThirdParty/SQLite.swift/Sources/SQLiteObjc/SQLite-Bridging.m new file mode 100644 index 0000000000..d8fe6b683c --- /dev/null +++ b/mobile/ios/ThirdParty/SQLite.swift/Sources/SQLiteObjc/SQLite-Bridging.m @@ -0,0 +1,138 @@ +// +// SQLite.swift +// https://github.com/stephencelis/SQLite.swift +// Copyright © 2014-2015 Stephen Celis. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +#import "SQLite-Bridging.h" +#import "fts3_tokenizer.h" + +#pragma mark - FTS + +typedef struct __SQLiteTokenizer { + sqlite3_tokenizer base; + __unsafe_unretained _SQLiteTokenizerNextCallback callback; +} __SQLiteTokenizer; + +typedef struct __SQLiteTokenizerCursor { + void * base; + const char * input; + int inputOffset; + int inputLength; + int idx; +} __SQLiteTokenizerCursor; + +static NSMutableDictionary * __SQLiteTokenizerMap; + +static int __SQLiteTokenizerCreate(int argc, const char * const * argv, sqlite3_tokenizer ** ppTokenizer) { + __SQLiteTokenizer * tokenizer = (__SQLiteTokenizer *)sqlite3_malloc(sizeof(__SQLiteTokenizer)); + if (!tokenizer) { + return SQLITE_NOMEM; + } + memset(tokenizer, 0, sizeof(* tokenizer)); + + NSString * key = [NSString stringWithUTF8String:argv[0]]; + tokenizer->callback = [__SQLiteTokenizerMap objectForKey:key]; + if (!tokenizer->callback) { + return SQLITE_ERROR; + } + + *ppTokenizer = &tokenizer->base; + return SQLITE_OK; +} + +static int __SQLiteTokenizerDestroy(sqlite3_tokenizer * pTokenizer) { + sqlite3_free(pTokenizer); + return SQLITE_OK; +} + +static int __SQLiteTokenizerOpen(sqlite3_tokenizer * pTokenizer, const char * pInput, int nBytes, sqlite3_tokenizer_cursor ** ppCursor) { + __SQLiteTokenizerCursor * cursor = (__SQLiteTokenizerCursor *)sqlite3_malloc(sizeof(__SQLiteTokenizerCursor)); + if (!cursor) { + return SQLITE_NOMEM; + } + + cursor->input = pInput; + cursor->inputOffset = 0; + cursor->inputLength = 0; + cursor->idx = 0; + + *ppCursor = (sqlite3_tokenizer_cursor *)cursor; + return SQLITE_OK; +} + +static int __SQLiteTokenizerClose(sqlite3_tokenizer_cursor * pCursor) { + sqlite3_free(pCursor); + return SQLITE_OK; +} + +static int __SQLiteTokenizerNext(sqlite3_tokenizer_cursor * pCursor, const char ** ppToken, int * pnBytes, int * piStartOffset, int * piEndOffset, int * piPosition) { + __SQLiteTokenizerCursor * cursor = (__SQLiteTokenizerCursor *)pCursor; + __SQLiteTokenizer * tokenizer = (__SQLiteTokenizer *)cursor->base; + + cursor->inputOffset += cursor->inputLength; + const char * input = cursor->input + cursor->inputOffset; + const char * token = [tokenizer->callback(input, &cursor->inputOffset, &cursor->inputLength) cStringUsingEncoding:NSUTF8StringEncoding]; + if (!token) { + return SQLITE_DONE; + } + + *ppToken = token; + *pnBytes = (int)strlen(token); + *piStartOffset = cursor->inputOffset; + *piEndOffset = cursor->inputOffset + cursor->inputLength; + *piPosition = cursor->idx++; + return SQLITE_OK; +} + +static const sqlite3_tokenizer_module __SQLiteTokenizerModule = { + 0, + __SQLiteTokenizerCreate, + __SQLiteTokenizerDestroy, + __SQLiteTokenizerOpen, + __SQLiteTokenizerClose, + __SQLiteTokenizerNext +}; + +int _SQLiteRegisterTokenizer(SQLiteHandle * db, const char * moduleName, const char * submoduleName, _SQLiteTokenizerNextCallback callback) { + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + __SQLiteTokenizerMap = [NSMutableDictionary new]; + }); + + sqlite3_stmt * stmt; + int status = sqlite3_prepare_v2((sqlite3 *)db, "SELECT fts3_tokenizer(?, ?)", -1, &stmt, 0); + if (status != SQLITE_OK ){ + return status; + } + const sqlite3_tokenizer_module * pModule = &__SQLiteTokenizerModule; + sqlite3_bind_text(stmt, 1, moduleName, -1, SQLITE_STATIC); + sqlite3_bind_blob(stmt, 2, &pModule, sizeof(pModule), SQLITE_STATIC); + sqlite3_step(stmt); + status = sqlite3_finalize(stmt); + if (status != SQLITE_OK ){ + return status; + } + + [__SQLiteTokenizerMap setObject:[callback copy] forKey:[NSString stringWithUTF8String:submoduleName]]; + + return SQLITE_OK; +} diff --git a/mobile/ios/ThirdParty/SQLite.swift/Sources/SQLiteObjc/fts3_tokenizer.h b/mobile/ios/ThirdParty/SQLite.swift/Sources/SQLiteObjc/fts3_tokenizer.h new file mode 100644 index 0000000000..d8a1e44b47 --- /dev/null +++ b/mobile/ios/ThirdParty/SQLite.swift/Sources/SQLiteObjc/fts3_tokenizer.h @@ -0,0 +1,161 @@ +/* +** 2006 July 10 +** +** The author disclaims copyright to this source code. +** +************************************************************************* +** Defines the interface to tokenizers used by fulltext-search. There +** are three basic components: +** +** sqlite3_tokenizer_module is a singleton defining the tokenizer +** interface functions. This is essentially the class structure for +** tokenizers. +** +** sqlite3_tokenizer is used to define a particular tokenizer, perhaps +** including customization information defined at creation time. +** +** sqlite3_tokenizer_cursor is generated by a tokenizer to generate +** tokens from a particular input. +*/ +#ifndef _FTS3_TOKENIZER_H_ +#define _FTS3_TOKENIZER_H_ + +/* TODO(shess) Only used for SQLITE_OK and SQLITE_DONE at this time. +** If tokenizers are to be allowed to call sqlite3_*() functions, then +** we will need a way to register the API consistently. +*/ +#import "sqlite3.h" + +/* +** Structures used by the tokenizer interface. When a new tokenizer +** implementation is registered, the caller provides a pointer to +** an sqlite3_tokenizer_module containing pointers to the callback +** functions that make up an implementation. +** +** When an fts3 table is created, it passes any arguments passed to +** the tokenizer clause of the CREATE VIRTUAL TABLE statement to the +** sqlite3_tokenizer_module.xCreate() function of the requested tokenizer +** implementation. The xCreate() function in turn returns an +** sqlite3_tokenizer structure representing the specific tokenizer to +** be used for the fts3 table (customized by the tokenizer clause arguments). +** +** To tokenize an input buffer, the sqlite3_tokenizer_module.xOpen() +** method is called. It returns an sqlite3_tokenizer_cursor object +** that may be used to tokenize a specific input buffer based on +** the tokenization rules supplied by a specific sqlite3_tokenizer +** object. +*/ +typedef struct sqlite3_tokenizer_module sqlite3_tokenizer_module; +typedef struct sqlite3_tokenizer sqlite3_tokenizer; +typedef struct sqlite3_tokenizer_cursor sqlite3_tokenizer_cursor; + +struct sqlite3_tokenizer_module { + + /* + ** Structure version. Should always be set to 0 or 1. + */ + int iVersion; + + /* + ** Create a new tokenizer. The values in the argv[] array are the + ** arguments passed to the "tokenizer" clause of the CREATE VIRTUAL + ** TABLE statement that created the fts3 table. For example, if + ** the following SQL is executed: + ** + ** CREATE .. USING fts3( ... , tokenizer arg1 arg2) + ** + ** then argc is set to 2, and the argv[] array contains pointers + ** to the strings "arg1" and "arg2". + ** + ** This method should return either SQLITE_OK (0), or an SQLite error + ** code. If SQLITE_OK is returned, then *ppTokenizer should be set + ** to point at the newly created tokenizer structure. The generic + ** sqlite3_tokenizer.pModule variable should not be initialized by + ** this callback. The caller will do so. + */ + int (*xCreate)( + int argc, /* Size of argv array */ + const char *const*argv, /* Tokenizer argument strings */ + sqlite3_tokenizer **ppTokenizer /* OUT: Created tokenizer */ + ); + + /* + ** Destroy an existing tokenizer. The fts3 module calls this method + ** exactly once for each successful call to xCreate(). + */ + int (*xDestroy)(sqlite3_tokenizer *pTokenizer); + + /* + ** Create a tokenizer cursor to tokenize an input buffer. The caller + ** is responsible for ensuring that the input buffer remains valid + ** until the cursor is closed (using the xClose() method). + */ + int (*xOpen)( + sqlite3_tokenizer *pTokenizer, /* Tokenizer object */ + const char *pInput, int nBytes, /* Input buffer */ + sqlite3_tokenizer_cursor **ppCursor /* OUT: Created tokenizer cursor */ + ); + + /* + ** Destroy an existing tokenizer cursor. The fts3 module calls this + ** method exactly once for each successful call to xOpen(). + */ + int (*xClose)(sqlite3_tokenizer_cursor *pCursor); + + /* + ** Retrieve the next token from the tokenizer cursor pCursor. This + ** method should either return SQLITE_OK and set the values of the + ** "OUT" variables identified below, or SQLITE_DONE to indicate that + ** the end of the buffer has been reached, or an SQLite error code. + ** + ** *ppToken should be set to point at a buffer containing the + ** normalized version of the token (i.e. after any case-folding and/or + ** stemming has been performed). *pnBytes should be set to the length + ** of this buffer in bytes. The input text that generated the token is + ** identified by the byte offsets returned in *piStartOffset and + ** *piEndOffset. *piStartOffset should be set to the index of the first + ** byte of the token in the input buffer. *piEndOffset should be set + ** to the index of the first byte just past the end of the token in + ** the input buffer. + ** + ** The buffer *ppToken is set to point at is managed by the tokenizer + ** implementation. It is only required to be valid until the next call + ** to xNext() or xClose(). + */ + /* TODO(shess) current implementation requires pInput to be + ** nul-terminated. This should either be fixed, or pInput/nBytes + ** should be converted to zInput. + */ + int (*xNext)( + sqlite3_tokenizer_cursor *pCursor, /* Tokenizer cursor */ + const char **ppToken, int *pnBytes, /* OUT: Normalized text for token */ + int *piStartOffset, /* OUT: Byte offset of token in input buffer */ + int *piEndOffset, /* OUT: Byte offset of end of token in input buffer */ + int *piPosition /* OUT: Number of tokens returned before this one */ + ); + + /*********************************************************************** + ** Methods below this point are only available if iVersion>=1. + */ + + /* + ** Configure the language id of a tokenizer cursor. + */ + int (*xLanguageid)(sqlite3_tokenizer_cursor *pCsr, int iLangid); +}; + +struct sqlite3_tokenizer { + const sqlite3_tokenizer_module *pModule; /* The module for this tokenizer */ + /* Tokenizer implementations will typically add additional fields */ +}; + +struct sqlite3_tokenizer_cursor { + sqlite3_tokenizer *pTokenizer; /* Tokenizer for this cursor. */ + /* Tokenizer implementations will typically add additional fields */ +}; + +int fts3_global_term_cnt(int iTerm, int iCol); +int fts3_term_cnt(int iTerm, int iCol); + + +#endif /* _FTS3_TOKENIZER_H_ */ diff --git a/mobile/ios/ThirdParty/SQLite.swift/Sources/SQLiteObjc/include/SQLite-Bridging.h b/mobile/ios/ThirdParty/SQLite.swift/Sources/SQLiteObjc/include/SQLite-Bridging.h new file mode 100644 index 0000000000..d15e8d56d6 --- /dev/null +++ b/mobile/ios/ThirdParty/SQLite.swift/Sources/SQLiteObjc/include/SQLite-Bridging.h @@ -0,0 +1,37 @@ +// +// SQLite.swift +// https://github.com/stephencelis/SQLite.swift +// Copyright © 2014-2015 Stephen Celis. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +@import Foundation; + +#ifndef COCOAPODS +#import "sqlite3.h" +#endif + +typedef struct SQLiteHandle SQLiteHandle; // CocoaPods workaround + +NS_ASSUME_NONNULL_BEGIN +typedef NSString * _Nullable (^_SQLiteTokenizerNextCallback)(const char * input, int * inputOffset, int * inputLength); +int _SQLiteRegisterTokenizer(SQLiteHandle * db, const char * module, const char * tokenizer, _Nullable _SQLiteTokenizerNextCallback callback); +NS_ASSUME_NONNULL_END + diff --git a/mobile/ios/ThirdParty/SQLite.swift/Tests/CocoaPods/.gitignore b/mobile/ios/ThirdParty/SQLite.swift/Tests/CocoaPods/.gitignore new file mode 100644 index 0000000000..4cf24de2ce --- /dev/null +++ b/mobile/ios/ThirdParty/SQLite.swift/Tests/CocoaPods/.gitignore @@ -0,0 +1 @@ +gems/ diff --git a/mobile/ios/ThirdParty/SQLite.swift/Tests/CocoaPods/Gemfile b/mobile/ios/ThirdParty/SQLite.swift/Tests/CocoaPods/Gemfile new file mode 100644 index 0000000000..94018f18bd --- /dev/null +++ b/mobile/ios/ThirdParty/SQLite.swift/Tests/CocoaPods/Gemfile @@ -0,0 +1,4 @@ +source 'https://rubygems.org' + +gem 'cocoapods', '~> 1.1.0' +gem 'minitest' diff --git a/mobile/ios/ThirdParty/SQLite.swift/Tests/CocoaPods/Gemfile.lock b/mobile/ios/ThirdParty/SQLite.swift/Tests/CocoaPods/Gemfile.lock new file mode 100644 index 0000000000..e8879f13ca --- /dev/null +++ b/mobile/ios/ThirdParty/SQLite.swift/Tests/CocoaPods/Gemfile.lock @@ -0,0 +1,74 @@ +GEM + remote: https://rubygems.org/ + specs: + CFPropertyList (2.3.3) + activesupport (4.2.7.1) + i18n (~> 0.7) + json (~> 1.7, >= 1.7.7) + minitest (~> 5.1) + thread_safe (~> 0.3, >= 0.3.4) + tzinfo (~> 1.1) + claide (1.0.1) + cocoapods (1.1.1) + activesupport (>= 4.0.2, < 5) + claide (>= 1.0.1, < 2.0) + cocoapods-core (= 1.1.1) + cocoapods-deintegrate (>= 1.0.1, < 2.0) + cocoapods-downloader (>= 1.1.2, < 2.0) + cocoapods-plugins (>= 1.0.0, < 2.0) + cocoapods-search (>= 1.0.0, < 2.0) + cocoapods-stats (>= 1.0.0, < 2.0) + cocoapods-trunk (>= 1.1.1, < 2.0) + cocoapods-try (>= 1.1.0, < 2.0) + colored (~> 1.2) + escape (~> 0.0.4) + fourflusher (~> 2.0.1) + gh_inspector (~> 1.0) + molinillo (~> 0.5.1) + nap (~> 1.0) + xcodeproj (>= 1.3.3, < 2.0) + cocoapods-core (1.1.1) + activesupport (>= 4.0.2, < 5) + fuzzy_match (~> 2.0.4) + nap (~> 1.0) + cocoapods-deintegrate (1.0.1) + cocoapods-downloader (1.1.2) + cocoapods-plugins (1.0.0) + nap + cocoapods-search (1.0.0) + cocoapods-stats (1.0.0) + cocoapods-trunk (1.1.1) + nap (>= 0.8, < 2.0) + netrc (= 0.7.8) + cocoapods-try (1.1.0) + colored (1.2) + escape (0.0.4) + fourflusher (2.0.1) + fuzzy_match (2.0.4) + gh_inspector (1.0.2) + i18n (0.7.0) + json (1.8.3) + minitest (5.9.1) + molinillo (0.5.4) + nanaimo (0.2.2) + nap (1.1.0) + netrc (0.7.8) + thread_safe (0.3.5) + tzinfo (1.2.2) + thread_safe (~> 0.1) + xcodeproj (1.4.1) + CFPropertyList (~> 2.3.3) + activesupport (>= 3) + claide (>= 1.0.1, < 2.0) + colored (~> 1.2) + nanaimo (~> 0.2.0) + +PLATFORMS + ruby + +DEPENDENCIES + cocoapods (~> 1.1.0) + minitest + +BUNDLED WITH + 1.13.3 diff --git a/mobile/ios/ThirdParty/SQLite.swift/Tests/CocoaPods/Makefile b/mobile/ios/ThirdParty/SQLite.swift/Tests/CocoaPods/Makefile new file mode 100644 index 0000000000..26163fdbe1 --- /dev/null +++ b/mobile/ios/ThirdParty/SQLite.swift/Tests/CocoaPods/Makefile @@ -0,0 +1,13 @@ +test: install repo_update + @set -e; \ + for test in *_test.rb; do \ + bundle exec ./$$test; \ + done + +repo_update: + @bundle exec pod repo update --silent + +install: + @bundle install --path gems + +.PHONY: test install diff --git a/mobile/ios/ThirdParty/SQLite.swift/Tests/CocoaPods/integration_test.rb b/mobile/ios/ThirdParty/SQLite.swift/Tests/CocoaPods/integration_test.rb new file mode 100644 index 0000000000..192aff9032 --- /dev/null +++ b/mobile/ios/ThirdParty/SQLite.swift/Tests/CocoaPods/integration_test.rb @@ -0,0 +1,43 @@ +#!/usr/bin/env ruby + +require 'minitest/autorun' +require_relative 'test_running_validator' + +class IntegrationTest < Minitest::Test + + def test_validate_project + assert validator.validate, "validation failed: #{validator.failure_reason}" + end + + private + + def validator + @validator ||= TestRunningValidator.new(podspec, []).tap do |validator| + validator.test_files = Dir["#{project_test_dir}/**/*.swift"] + validator.test_resources = Dir["#{project_test_dir}/fixtures"] + validator.config.verbose = true + validator.no_clean = true + validator.use_frameworks = true + validator.fail_fast = true + validator.local = true + validator.allow_warnings = true + subspec = ENV['VALIDATOR_SUBSPEC'] + if subspec == 'none' + validator.no_subspecs = true + else + validator.only_subspec = subspec + end + if ENV['IOS_SIMULATOR'] + validator.ios_simulator = ENV['IOS_SIMULATOR'] + end + end + end + + def podspec + File.expand_path(File.dirname(__FILE__) + '/../../SQLite.swift.podspec') + end + + def project_test_dir + File.expand_path(File.dirname(__FILE__) + '/../SQLiteTests') + end +end diff --git a/mobile/ios/ThirdParty/SQLite.swift/Tests/CocoaPods/test_running_validator.rb b/mobile/ios/ThirdParty/SQLite.swift/Tests/CocoaPods/test_running_validator.rb new file mode 100644 index 0000000000..fcfd3c28ef --- /dev/null +++ b/mobile/ios/ThirdParty/SQLite.swift/Tests/CocoaPods/test_running_validator.rb @@ -0,0 +1,120 @@ +require 'cocoapods' +require 'cocoapods/validator' +require 'fileutils' + +class TestRunningValidator < Pod::Validator + APP_TARGET = 'App' + TEST_TARGET = 'Tests' + + attr_accessor :test_files + attr_accessor :test_resources + attr_accessor :ios_simulator + attr_accessor :tvos_simulator + attr_accessor :watchos_simulator + + def initialize(spec_or_path, source_urls) + super(spec_or_path, source_urls) + self.test_files = [] + self.test_resources = [] + self.ios_simulator = :oldest + self.tvos_simulator = :oldest + self.watchos_simulator = :oldest + end + + def create_app_project + super + project = Xcodeproj::Project.open(validation_dir + "#{APP_TARGET}.xcodeproj") + create_test_target(project) + project.save + end + + def add_app_project_import + super + project = Xcodeproj::Project.open(validation_dir + 'App.xcodeproj') + group = project.new_group(TEST_TARGET) + test_target = project.targets.last + test_target.add_resources(test_resources.map { |resource| group.new_file(resource) }) + test_target.add_file_references(test_files.map { |file| group.new_file(file) }) + add_swift_version(test_target) + project.save + end + + def install_pod + super + if local? + FileUtils.ln_s file.dirname, validation_dir + "Pods/#{spec.name}" + end + end + + def podfile_from_spec(*args) + super(*args).tap do |pod_file| + add_test_target(pod_file) + end + end + + def build_pod + super + Pod::UI.message "\Testing with xcodebuild.\n".yellow do + run_tests + end + end + + private + def create_test_target(project) + test_target = project.new_target(:unit_test_bundle, TEST_TARGET, consumer.platform_name, deployment_target) + create_test_scheme(project, test_target) + end + + def create_test_scheme(project, test_target) + project.recreate_user_schemes + test_scheme = Xcodeproj::XCScheme.new(test_scheme_path(project)) + test_scheme.add_test_target(test_target) + test_scheme.save! + end + + def test_scheme_path(project) + Xcodeproj::XCScheme.user_data_dir(project.path) + "#{TEST_TARGET}.xcscheme" + end + + def add_test_target(pod_file) + app_target = pod_file.target_definitions[APP_TARGET] + Pod::Podfile::TargetDefinition.new(TEST_TARGET, app_target) + end + + def run_tests + command = [ + 'clean', 'build', 'build-for-testing', 'test-without-building', + '-workspace', File.join(validation_dir, "#{APP_TARGET}.xcworkspace"), + '-scheme', TEST_TARGET, + '-configuration', 'Debug' + ] + case consumer.platform_name + when :ios + command += %w(CODE_SIGN_IDENTITY=- -sdk iphonesimulator) + command += Fourflusher::SimControl.new.destination(ios_simulator, 'iOS', deployment_target) + when :osx + command += %w(LD_RUNPATH_SEARCH_PATHS=@loader_path/../Frameworks) + when :tvos + command += %w(CODE_SIGN_IDENTITY=- -sdk appletvsimulator) + command += Fourflusher::SimControl.new.destination(tvos_simulator, 'tvOS', deployment_target) + when :watchos + # there's no XCTest on watchOS (https://openradar.appspot.com/21760513) + return + else + return + end + + output, status = _xcodebuild(command) + + unless status.success? + message = 'Returned an unsuccessful exit code.' + if config.verbose? + message += "\nXcode output: \n#{output}\n" + else + message += ' You can use `--verbose` for more information.' + end + error('xcodebuild', message) + end + output + end +end diff --git a/mobile/ios/ThirdParty/SQLite.swift/Tests/SQLiteTests/AggregateFunctionsTests.swift b/mobile/ios/ThirdParty/SQLite.swift/Tests/SQLiteTests/AggregateFunctionsTests.swift new file mode 100644 index 0000000000..6b583ccfa8 --- /dev/null +++ b/mobile/ios/ThirdParty/SQLite.swift/Tests/SQLiteTests/AggregateFunctionsTests.swift @@ -0,0 +1,68 @@ +import XCTest +import SQLite + +class AggregateFunctionsTests : XCTestCase { + + func test_distinct_prependsExpressionsWithDistinctKeyword() { + AssertSQL("DISTINCT \"int\"", int.distinct) + AssertSQL("DISTINCT \"intOptional\"", intOptional.distinct) + AssertSQL("DISTINCT \"double\"", double.distinct) + AssertSQL("DISTINCT \"doubleOptional\"", doubleOptional.distinct) + AssertSQL("DISTINCT \"string\"", string.distinct) + AssertSQL("DISTINCT \"stringOptional\"", stringOptional.distinct) + } + + func test_count_wrapsOptionalExpressionsWithCountFunction() { + AssertSQL("count(\"intOptional\")", intOptional.count) + AssertSQL("count(\"doubleOptional\")", doubleOptional.count) + AssertSQL("count(\"stringOptional\")", stringOptional.count) + } + + func test_max_wrapsComparableExpressionsWithMaxFunction() { + AssertSQL("max(\"int\")", int.max) + AssertSQL("max(\"intOptional\")", intOptional.max) + AssertSQL("max(\"double\")", double.max) + AssertSQL("max(\"doubleOptional\")", doubleOptional.max) + AssertSQL("max(\"string\")", string.max) + AssertSQL("max(\"stringOptional\")", stringOptional.max) + AssertSQL("max(\"date\")", date.max) + AssertSQL("max(\"dateOptional\")", dateOptional.max) + } + + func test_min_wrapsComparableExpressionsWithMinFunction() { + AssertSQL("min(\"int\")", int.min) + AssertSQL("min(\"intOptional\")", intOptional.min) + AssertSQL("min(\"double\")", double.min) + AssertSQL("min(\"doubleOptional\")", doubleOptional.min) + AssertSQL("min(\"string\")", string.min) + AssertSQL("min(\"stringOptional\")", stringOptional.min) + AssertSQL("min(\"date\")", date.min) + AssertSQL("min(\"dateOptional\")", dateOptional.min) + } + + func test_average_wrapsNumericExpressionsWithAvgFunction() { + AssertSQL("avg(\"int\")", int.average) + AssertSQL("avg(\"intOptional\")", intOptional.average) + AssertSQL("avg(\"double\")", double.average) + AssertSQL("avg(\"doubleOptional\")", doubleOptional.average) + } + + func test_sum_wrapsNumericExpressionsWithSumFunction() { + AssertSQL("sum(\"int\")", int.sum) + AssertSQL("sum(\"intOptional\")", intOptional.sum) + AssertSQL("sum(\"double\")", double.sum) + AssertSQL("sum(\"doubleOptional\")", doubleOptional.sum) + } + + func test_total_wrapsNumericExpressionsWithTotalFunction() { + AssertSQL("total(\"int\")", int.total) + AssertSQL("total(\"intOptional\")", intOptional.total) + AssertSQL("total(\"double\")", double.total) + AssertSQL("total(\"doubleOptional\")", doubleOptional.total) + } + + func test_count_withStar_wrapsStarWithCountFunction() { + AssertSQL("count(*)", count(*)) + } + +} diff --git a/mobile/ios/ThirdParty/SQLite.swift/Tests/SQLiteTests/BlobTests.swift b/mobile/ios/ThirdParty/SQLite.swift/Tests/SQLiteTests/BlobTests.swift new file mode 100644 index 0000000000..fbcca9bcd9 --- /dev/null +++ b/mobile/ios/ThirdParty/SQLite.swift/Tests/SQLiteTests/BlobTests.swift @@ -0,0 +1,23 @@ +import XCTest +import SQLite + +class BlobTests : XCTestCase { + + func test_toHex() { + let blob = Blob(bytes: [0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 150, 250, 255]) + + XCTAssertEqual(blob.toHex(), "000a141e28323c46505a6496faff") + } + + func test_init_array() { + let blob = Blob(bytes: [42, 42, 42]) + XCTAssertEqual(blob.bytes, [42, 42, 42]) + } + + func test_init_unsafeRawPointer() { + let pointer = UnsafeMutablePointer.allocate(capacity: 3) + pointer.initialize(to: 42, count: 3) + let blob = Blob(bytes: pointer, length: 3) + XCTAssertEqual(blob.bytes, [42, 42, 42]) + } +} diff --git a/mobile/ios/ThirdParty/SQLite.swift/Tests/SQLiteTests/CipherTests.swift b/mobile/ios/ThirdParty/SQLite.swift/Tests/SQLiteTests/CipherTests.swift new file mode 100644 index 0000000000..3ee0b13511 --- /dev/null +++ b/mobile/ios/ThirdParty/SQLite.swift/Tests/SQLiteTests/CipherTests.swift @@ -0,0 +1,98 @@ +#if SQLITE_SWIFT_SQLCIPHER +import XCTest +import SQLite +import SQLCipher + +class CipherTests: XCTestCase { + + let db1 = try! Connection() + let db2 = try! Connection() + + override func setUp() { + // db + + try! db1.key("hello") + + try! db1.run("CREATE TABLE foo (bar TEXT)") + try! db1.run("INSERT INTO foo (bar) VALUES ('world')") + + // db2 + let key2 = keyData() + try! db2.key(Blob(bytes: key2.bytes, length: key2.length)) + + try! db2.run("CREATE TABLE foo (bar TEXT)") + try! db2.run("INSERT INTO foo (bar) VALUES ('world')") + + super.setUp() + } + + func test_key() { + XCTAssertEqual(1, try! db1.scalar("SELECT count(*) FROM foo") as? Int64) + } + + func test_key_blob_literal() { + let db = try! Connection() + try! db.key("x'2DD29CA851E7B56E4697B0E1F08507293D761A05CE4D1B628663F411A8086D99'") + } + + func test_rekey() { + try! db1.rekey("goodbye") + XCTAssertEqual(1, try! db1.scalar("SELECT count(*) FROM foo") as? Int64) + } + + func test_data_key() { + XCTAssertEqual(1, try! db2.scalar("SELECT count(*) FROM foo") as? Int64) + } + + func test_data_rekey() { + let newKey = keyData() + try! db2.rekey(Blob(bytes: newKey.bytes, length: newKey.length)) + XCTAssertEqual(1, try! db2.scalar("SELECT count(*) FROM foo") as? Int64) + } + + func test_keyFailure() { + let path = "\(NSTemporaryDirectory())/db.sqlite3" + _ = try? FileManager.default.removeItem(atPath: path) + + let connA = try! Connection(path) + defer { try! FileManager.default.removeItem(atPath: path) } + + try! connA.key("hello") + try! connA.run("CREATE TABLE foo (bar TEXT)") + + let connB = try! Connection(path, readonly: true) + + do { + try connB.key("world") + XCTFail("expected exception") + } catch Result.error(_, let code, _) { + XCTAssertEqual(SQLITE_NOTADB, code) + } catch { + XCTFail("unexpected error: \(error)") + } + } + + func test_open_db_encrypted_with_sqlcipher() { + // $ sqlcipher SQLiteTests/fixtures/encrypted.sqlite + // sqlite> pragma key = 'sqlcipher-test'; + // sqlite> CREATE TABLE foo (bar TEXT); + // sqlite> INSERT INTO foo (bar) VALUES ('world'); + let encryptedFile = fixture("encrypted", withExtension: "sqlite") + + try! FileManager.default.setAttributes([FileAttributeKey.immutable : 1], ofItemAtPath: encryptedFile) + XCTAssertFalse(FileManager.default.isWritableFile(atPath: encryptedFile)) + + let conn = try! Connection(encryptedFile) + try! conn.key("sqlcipher-test") + XCTAssertEqual(1, try! conn.scalar("SELECT count(*) FROM foo") as? Int64) + } + + private func keyData(length: Int = 64) -> NSMutableData { + let keyData = NSMutableData(length: length)! + let result = SecRandomCopyBytes(kSecRandomDefault, length, + keyData.mutableBytes.assumingMemoryBound(to: UInt8.self)) + XCTAssertEqual(0, result) + return keyData + } +} +#endif diff --git a/mobile/ios/ThirdParty/SQLite.swift/Tests/SQLiteTests/ConnectionTests.swift b/mobile/ios/ThirdParty/SQLite.swift/Tests/SQLiteTests/ConnectionTests.swift new file mode 100644 index 0000000000..0db68b0d25 --- /dev/null +++ b/mobile/ios/ThirdParty/SQLite.swift/Tests/SQLiteTests/ConnectionTests.swift @@ -0,0 +1,384 @@ +import XCTest +@testable import SQLite + +#if SQLITE_SWIFT_STANDALONE +import sqlite3 +#elseif SQLITE_SWIFT_SQLCIPHER +import SQLCipher +#else +import SQLite3 +#endif + +class ConnectionTests : SQLiteTestCase { + + override func setUp() { + super.setUp() + + CreateUsersTable() + } + + func test_init_withInMemory_returnsInMemoryConnection() { + let db = try! Connection(.inMemory) + XCTAssertEqual("", db.description) + } + + func test_init_returnsInMemoryByDefault() { + let db = try! Connection() + XCTAssertEqual("", db.description) + } + + func test_init_withTemporary_returnsTemporaryConnection() { + let db = try! Connection(.temporary) + XCTAssertEqual("", db.description) + } + + func test_init_withURI_returnsURIConnection() { + let db = try! Connection(.uri("\(NSTemporaryDirectory())/SQLite.swift Tests.sqlite3")) + XCTAssertEqual("\(NSTemporaryDirectory())/SQLite.swift Tests.sqlite3", db.description) + } + + func test_init_withString_returnsURIConnection() { + let db = try! Connection("\(NSTemporaryDirectory())/SQLite.swift Tests.sqlite3") + XCTAssertEqual("\(NSTemporaryDirectory())/SQLite.swift Tests.sqlite3", db.description) + } + + func test_readonly_returnsFalseOnReadWriteConnections() { + XCTAssertFalse(db.readonly) + } + + func test_readonly_returnsTrueOnReadOnlyConnections() { + let db = try! Connection(readonly: true) + XCTAssertTrue(db.readonly) + } + + func test_changes_returnsZeroOnNewConnections() { + XCTAssertEqual(0, db.changes) + } + + func test_lastInsertRowid_returnsLastIdAfterInserts() { + try! InsertUser("alice") + XCTAssertEqual(1, db.lastInsertRowid) + } + + func test_lastInsertRowid_doesNotResetAfterError() { + XCTAssert(db.lastInsertRowid == 0) + try! InsertUser("alice") + XCTAssertEqual(1, db.lastInsertRowid) + XCTAssertThrowsError( + try db.run("INSERT INTO \"users\" (email, age, admin) values ('invalid@example.com', 12, 'invalid')") + ) { error in + if case SQLite.Result.error(_, let code, _) = error { + XCTAssertEqual(SQLITE_CONSTRAINT, code) + } else { + XCTFail("expected error") + } + } + XCTAssertEqual(1, db.lastInsertRowid) + } + + func test_changes_returnsNumberOfChanges() { + try! InsertUser("alice") + XCTAssertEqual(1, db.changes) + try! InsertUser("betsy") + XCTAssertEqual(1, db.changes) + } + + func test_totalChanges_returnsTotalNumberOfChanges() { + XCTAssertEqual(0, db.totalChanges) + try! InsertUser("alice") + XCTAssertEqual(1, db.totalChanges) + try! InsertUser("betsy") + XCTAssertEqual(2, db.totalChanges) + } + + func test_prepare_preparesAndReturnsStatements() { + _ = try! db.prepare("SELECT * FROM users WHERE admin = 0") + _ = try! db.prepare("SELECT * FROM users WHERE admin = ?", 0) + _ = try! db.prepare("SELECT * FROM users WHERE admin = ?", [0]) + _ = try! db.prepare("SELECT * FROM users WHERE admin = $admin", ["$admin": 0]) + } + + func test_run_preparesRunsAndReturnsStatements() { + try! db.run("SELECT * FROM users WHERE admin = 0") + try! db.run("SELECT * FROM users WHERE admin = ?", 0) + try! db.run("SELECT * FROM users WHERE admin = ?", [0]) + try! db.run("SELECT * FROM users WHERE admin = $admin", ["$admin": 0]) + AssertSQL("SELECT * FROM users WHERE admin = 0", 4) + } + + func test_scalar_preparesRunsAndReturnsScalarValues() { + XCTAssertEqual(0, try! db.scalar("SELECT count(*) FROM users WHERE admin = 0") as? Int64) + XCTAssertEqual(0, try! db.scalar("SELECT count(*) FROM users WHERE admin = ?", 0) as? Int64) + XCTAssertEqual(0, try! db.scalar("SELECT count(*) FROM users WHERE admin = ?", [0]) as? Int64) + XCTAssertEqual(0, try! db.scalar("SELECT count(*) FROM users WHERE admin = $admin", ["$admin": 0]) as? Int64) + AssertSQL("SELECT count(*) FROM users WHERE admin = 0", 4) + } + + func test_execute_comment() { + try! db.run("-- this is a comment\nSELECT 1") + AssertSQL("-- this is a comment", 0) + AssertSQL("SELECT 1", 0) + } + + func test_transaction_executesBeginDeferred() { + try! db.transaction(.deferred) {} + + AssertSQL("BEGIN DEFERRED TRANSACTION") + } + + func test_transaction_executesBeginImmediate() { + try! db.transaction(.immediate) {} + + AssertSQL("BEGIN IMMEDIATE TRANSACTION") + } + + func test_transaction_executesBeginExclusive() { + try! db.transaction(.exclusive) {} + + AssertSQL("BEGIN EXCLUSIVE TRANSACTION") + } + + func test_transaction_beginsAndCommitsTransactions() { + let stmt = try! db.prepare("INSERT INTO users (email) VALUES (?)", "alice@example.com") + + try! db.transaction { + try stmt.run() + } + + AssertSQL("BEGIN DEFERRED TRANSACTION") + AssertSQL("INSERT INTO users (email) VALUES ('alice@example.com')") + AssertSQL("COMMIT TRANSACTION") + AssertSQL("ROLLBACK TRANSACTION", 0) + } + + func test_transaction_beginsAndRollsTransactionsBack() { + let stmt = try! db.prepare("INSERT INTO users (email) VALUES (?)", "alice@example.com") + + do { + try db.transaction { + try stmt.run() + try stmt.run() + } + } catch { + } + + AssertSQL("BEGIN DEFERRED TRANSACTION") + AssertSQL("INSERT INTO users (email) VALUES ('alice@example.com')", 2) + AssertSQL("ROLLBACK TRANSACTION") + AssertSQL("COMMIT TRANSACTION", 0) + } + + func test_savepoint_beginsAndCommitsSavepoints() { + let db = self.db + + try! db.savepoint("1") { + try db.savepoint("2") { + try db.run("INSERT INTO users (email) VALUES (?)", "alice@example.com") + } + } + + AssertSQL("SAVEPOINT '1'") + AssertSQL("SAVEPOINT '2'") + AssertSQL("INSERT INTO users (email) VALUES ('alice@example.com')") + AssertSQL("RELEASE SAVEPOINT '2'") + AssertSQL("RELEASE SAVEPOINT '1'") + AssertSQL("ROLLBACK TO SAVEPOINT '2'", 0) + AssertSQL("ROLLBACK TO SAVEPOINT '1'", 0) + } + + func test_savepoint_beginsAndRollsSavepointsBack() { + let db = self.db + let stmt = try! db.prepare("INSERT INTO users (email) VALUES (?)", "alice@example.com") + + do { + try db.savepoint("1") { + try db.savepoint("2") { + try stmt.run() + try stmt.run() + try stmt.run() + } + try db.savepoint("2") { + try stmt.run() + try stmt.run() + try stmt.run() + } + } + } catch { + } + + AssertSQL("SAVEPOINT '1'") + AssertSQL("SAVEPOINT '2'") + AssertSQL("INSERT INTO users (email) VALUES ('alice@example.com')", 2) + AssertSQL("ROLLBACK TO SAVEPOINT '2'") + AssertSQL("ROLLBACK TO SAVEPOINT '1'") + AssertSQL("RELEASE SAVEPOINT '2'", 0) + AssertSQL("RELEASE SAVEPOINT '1'", 0) + } + + func test_updateHook_setsUpdateHook_withInsert() { + async { done in + db.updateHook { operation, db, table, rowid in + XCTAssertEqual(Connection.Operation.insert, operation) + XCTAssertEqual("main", db) + XCTAssertEqual("users", table) + XCTAssertEqual(1, rowid) + done() + } + try! InsertUser("alice") + } + } + + func test_updateHook_setsUpdateHook_withUpdate() { + try! InsertUser("alice") + async { done in + db.updateHook { operation, db, table, rowid in + XCTAssertEqual(Connection.Operation.update, operation) + XCTAssertEqual("main", db) + XCTAssertEqual("users", table) + XCTAssertEqual(1, rowid) + done() + } + try! db.run("UPDATE users SET email = 'alice@example.com'") + } + } + + func test_updateHook_setsUpdateHook_withDelete() { + try! InsertUser("alice") + async { done in + db.updateHook { operation, db, table, rowid in + XCTAssertEqual(Connection.Operation.delete, operation) + XCTAssertEqual("main", db) + XCTAssertEqual("users", table) + XCTAssertEqual(1, rowid) + done() + } + try! db.run("DELETE FROM users WHERE id = 1") + } + } + + func test_commitHook_setsCommitHook() { + async { done in + db.commitHook { + done() + } + try! db.transaction { + try self.InsertUser("alice") + } + XCTAssertEqual(1, try! db.scalar("SELECT count(*) FROM users") as? Int64) + } + } + + func test_rollbackHook_setsRollbackHook() { + async { done in + db.rollbackHook(done) + do { + try db.transaction { + try self.InsertUser("alice") + try self.InsertUser("alice") // throw + } + } catch { + } + XCTAssertEqual(0, try! db.scalar("SELECT count(*) FROM users") as? Int64) + } + } + + func test_commitHook_withRollback_rollsBack() { + async { done in + db.commitHook { + throw NSError(domain: "com.stephencelis.SQLiteTests", code: 1, userInfo: nil) + } + db.rollbackHook(done) + do { + try db.transaction { + try self.InsertUser("alice") + } + } catch { + } + XCTAssertEqual(0, try! db.scalar("SELECT count(*) FROM users") as? Int64) + } + } + + func test_createFunction_withArrayArguments() { + db.createFunction("hello") { $0[0].map { "Hello, \($0)!" } } + + XCTAssertEqual("Hello, world!", try! db.scalar("SELECT hello('world')") as? String) + XCTAssert(try! db.scalar("SELECT hello(NULL)") == nil) + } + + func test_createFunction_createsQuotableFunction() { + db.createFunction("hello world") { $0[0].map { "Hello, \($0)!" } } + + XCTAssertEqual("Hello, world!", try! db.scalar("SELECT \"hello world\"('world')") as? String) + XCTAssert(try! db.scalar("SELECT \"hello world\"(NULL)") == nil) + } + + func test_createCollation_createsCollation() { + try! db.createCollation("NODIACRITIC") { lhs, rhs in + return lhs.compare(rhs, options: .diacriticInsensitive) + } + XCTAssertEqual(1, try! db.scalar("SELECT ? = ? COLLATE NODIACRITIC", "cafe", "café") as? Int64) + } + + func test_createCollation_createsQuotableCollation() { + try! db.createCollation("NO DIACRITIC") { lhs, rhs in + return lhs.compare(rhs, options: .diacriticInsensitive) + } + XCTAssertEqual(1, try! db.scalar("SELECT ? = ? COLLATE \"NO DIACRITIC\"", "cafe", "café") as? Int64) + } + + func test_interrupt_interruptsLongRunningQuery() { + try! InsertUsers("abcdefghijklmnopqrstuvwxyz".characters.map { String($0) }) + db.createFunction("sleep") { args in + usleep(UInt32((args[0] as? Double ?? Double(args[0] as? Int64 ?? 1)) * 1_000_000)) + return nil + } + + let stmt = try! db.prepare("SELECT *, sleep(?) FROM users", 0.1) + try! stmt.run() + + let deadline = DispatchTime.now() + Double(Int64(10 * NSEC_PER_MSEC)) / Double(NSEC_PER_SEC) + _ = DispatchQueue.global(priority: .background).asyncAfter(deadline: deadline, execute: db.interrupt) + AssertThrows(try stmt.run()) + } + +} + + +class ResultTests : XCTestCase { + let connection = try! Connection(.inMemory) + + func test_init_with_ok_code_returns_nil() { + XCTAssertNil(Result(errorCode: SQLITE_OK, connection: connection, statement: nil) as Result?) + } + + func test_init_with_row_code_returns_nil() { + XCTAssertNil(Result(errorCode: SQLITE_ROW, connection: connection, statement: nil) as Result?) + } + + func test_init_with_done_code_returns_nil() { + XCTAssertNil(Result(errorCode: SQLITE_DONE, connection: connection, statement: nil) as Result?) + } + + func test_init_with_other_code_returns_error() { + if case .some(.error(let message, let code, let statement)) = + Result(errorCode: SQLITE_MISUSE, connection: connection, statement: nil) { + XCTAssertEqual("not an error", message) + XCTAssertEqual(SQLITE_MISUSE, code) + XCTAssertNil(statement) + XCTAssert(self.connection === connection) + } else { + XCTFail() + } + } + + func test_description_contains_error_code() { + XCTAssertEqual("not an error (code: 21)", + Result(errorCode: SQLITE_MISUSE, connection: connection, statement: nil)?.description) + } + + func test_description_contains_statement_and_error_code() { + let statement = try! Statement(connection, "SELECT 1") + XCTAssertEqual("not an error (SELECT 1) (code: 21)", + Result(errorCode: SQLITE_MISUSE, connection: connection, statement: statement)?.description) + } +} diff --git a/mobile/ios/ThirdParty/SQLite.swift/Tests/SQLiteTests/CoreFunctionsTests.swift b/mobile/ios/ThirdParty/SQLite.swift/Tests/SQLiteTests/CoreFunctionsTests.swift new file mode 100644 index 0000000000..db37ff7ff0 --- /dev/null +++ b/mobile/ios/ThirdParty/SQLite.swift/Tests/SQLiteTests/CoreFunctionsTests.swift @@ -0,0 +1,136 @@ +import XCTest +import SQLite + +class CoreFunctionsTests : XCTestCase { + + func test_round_wrapsDoubleExpressionsWithRoundFunction() { + AssertSQL("round(\"double\")", double.round()) + AssertSQL("round(\"doubleOptional\")", doubleOptional.round()) + + AssertSQL("round(\"double\", 1)", double.round(1)) + AssertSQL("round(\"doubleOptional\", 2)", doubleOptional.round(2)) + } + + func test_random_generatesExpressionWithRandomFunction() { + AssertSQL("random()", Expression.random()) + AssertSQL("random()", Expression.random()) + } + + func test_length_wrapsStringExpressionWithLengthFunction() { + AssertSQL("length(\"string\")", string.length) + AssertSQL("length(\"stringOptional\")", stringOptional.length) + } + + func test_lowercaseString_wrapsStringExpressionWithLowerFunction() { + AssertSQL("lower(\"string\")", string.lowercaseString) + AssertSQL("lower(\"stringOptional\")", stringOptional.lowercaseString) + } + + func test_uppercaseString_wrapsStringExpressionWithUpperFunction() { + AssertSQL("upper(\"string\")", string.uppercaseString) + AssertSQL("upper(\"stringOptional\")", stringOptional.uppercaseString) + } + + func test_like_buildsExpressionWithLikeOperator() { + AssertSQL("(\"string\" LIKE 'a%')", string.like("a%")) + AssertSQL("(\"stringOptional\" LIKE 'b%')", stringOptional.like("b%")) + + AssertSQL("(\"string\" LIKE '%\\%' ESCAPE '\\')", string.like("%\\%", escape: "\\")) + AssertSQL("(\"stringOptional\" LIKE '_\\_' ESCAPE '\\')", stringOptional.like("_\\_", escape: "\\")) + } + + func test_glob_buildsExpressionWithGlobOperator() { + AssertSQL("(\"string\" GLOB 'a*')", string.glob("a*")) + AssertSQL("(\"stringOptional\" GLOB 'b*')", stringOptional.glob("b*")) + } + + func test_match_buildsExpressionWithMatchOperator() { + AssertSQL("(\"string\" MATCH 'a*')", string.match("a*")) + AssertSQL("(\"stringOptional\" MATCH 'b*')", stringOptional.match("b*")) + } + + func test_regexp_buildsExpressionWithRegexpOperator() { + AssertSQL("(\"string\" REGEXP '^.+@.+\\.com$')", string.regexp("^.+@.+\\.com$")) + AssertSQL("(\"stringOptional\" REGEXP '^.+@.+\\.net$')", stringOptional.regexp("^.+@.+\\.net$")) + } + + func test_collate_buildsExpressionWithCollateOperator() { + AssertSQL("(\"string\" COLLATE BINARY)", string.collate(.binary)) + AssertSQL("(\"string\" COLLATE NOCASE)", string.collate(.nocase)) + AssertSQL("(\"string\" COLLATE RTRIM)", string.collate(.rtrim)) + AssertSQL("(\"string\" COLLATE \"CUSTOM\")", string.collate(.custom("CUSTOM"))) + + AssertSQL("(\"stringOptional\" COLLATE BINARY)", stringOptional.collate(.binary)) + AssertSQL("(\"stringOptional\" COLLATE NOCASE)", stringOptional.collate(.nocase)) + AssertSQL("(\"stringOptional\" COLLATE RTRIM)", stringOptional.collate(.rtrim)) + AssertSQL("(\"stringOptional\" COLLATE \"CUSTOM\")", stringOptional.collate(.custom("CUSTOM"))) + } + + func test_ltrim_wrapsStringWithLtrimFunction() { + AssertSQL("ltrim(\"string\")", string.ltrim()) + AssertSQL("ltrim(\"stringOptional\")", stringOptional.ltrim()) + + AssertSQL("ltrim(\"string\", ' ')", string.ltrim([" "])) + AssertSQL("ltrim(\"stringOptional\", ' ')", stringOptional.ltrim([" "])) + } + + func test_ltrim_wrapsStringWithRtrimFunction() { + AssertSQL("rtrim(\"string\")", string.rtrim()) + AssertSQL("rtrim(\"stringOptional\")", stringOptional.rtrim()) + + AssertSQL("rtrim(\"string\", ' ')", string.rtrim([" "])) + AssertSQL("rtrim(\"stringOptional\", ' ')", stringOptional.rtrim([" "])) + } + + func test_ltrim_wrapsStringWithTrimFunction() { + AssertSQL("trim(\"string\")", string.trim()) + AssertSQL("trim(\"stringOptional\")", stringOptional.trim()) + + AssertSQL("trim(\"string\", ' ')", string.trim([" "])) + AssertSQL("trim(\"stringOptional\", ' ')", stringOptional.trim([" "])) + } + + func test_replace_wrapsStringWithReplaceFunction() { + AssertSQL("replace(\"string\", '@example.com', '@example.net')", string.replace("@example.com", with: "@example.net")) + AssertSQL("replace(\"stringOptional\", '@example.net', '@example.com')", stringOptional.replace("@example.net", with: "@example.com")) + } + + func test_substring_wrapsStringWithSubstrFunction() { + AssertSQL("substr(\"string\", 1, 2)", string.substring(1, length: 2)) + AssertSQL("substr(\"stringOptional\", 2, 1)", stringOptional.substring(2, length: 1)) + } + + func test_subscriptWithRange_wrapsStringWithSubstrFunction() { + AssertSQL("substr(\"string\", 1, 2)", string[1..<3]) + AssertSQL("substr(\"stringOptional\", 2, 1)", stringOptional[2..<3]) + } + + func test_nilCoalescingOperator_wrapsOptionalsWithIfnullFunction() { + AssertSQL("ifnull(\"intOptional\", 1)", intOptional ?? 1) + // AssertSQL("ifnull(\"doubleOptional\", 1.0)", doubleOptional ?? 1) // rdar://problem/21677256 + XCTAssertEqual("ifnull(\"doubleOptional\", 1.0)", (doubleOptional ?? 1).asSQL()) + AssertSQL("ifnull(\"stringOptional\", 'literal')", stringOptional ?? "literal") + + AssertSQL("ifnull(\"intOptional\", \"int\")", intOptional ?? int) + AssertSQL("ifnull(\"doubleOptional\", \"double\")", doubleOptional ?? double) + AssertSQL("ifnull(\"stringOptional\", \"string\")", stringOptional ?? string) + + AssertSQL("ifnull(\"intOptional\", \"intOptional\")", intOptional ?? intOptional) + AssertSQL("ifnull(\"doubleOptional\", \"doubleOptional\")", doubleOptional ?? doubleOptional) + AssertSQL("ifnull(\"stringOptional\", \"stringOptional\")", stringOptional ?? stringOptional) + } + + func test_absoluteValue_wrapsNumberWithAbsFucntion() { + AssertSQL("abs(\"int\")", int.absoluteValue) + AssertSQL("abs(\"intOptional\")", intOptional.absoluteValue) + + AssertSQL("abs(\"double\")", double.absoluteValue) + AssertSQL("abs(\"doubleOptional\")", doubleOptional.absoluteValue) + } + + func test_contains_buildsExpressionWithInOperator() { + AssertSQL("(\"string\" IN ('hello', 'world'))", ["hello", "world"].contains(string)) + AssertSQL("(\"stringOptional\" IN ('hello', 'world'))", ["hello", "world"].contains(stringOptional)) + } + +} diff --git a/mobile/ios/ThirdParty/SQLite.swift/Tests/SQLiteTests/CustomFunctionsTests.swift b/mobile/ios/ThirdParty/SQLite.swift/Tests/SQLiteTests/CustomFunctionsTests.swift new file mode 100644 index 0000000000..67150ccf20 --- /dev/null +++ b/mobile/ios/ThirdParty/SQLite.swift/Tests/SQLiteTests/CustomFunctionsTests.swift @@ -0,0 +1,6 @@ +import XCTest +import SQLite + +class CustomFunctionsTests : XCTestCase { + +} diff --git a/mobile/ios/ThirdParty/SQLite.swift/Tests/SQLiteTests/ExpressionTests.swift b/mobile/ios/ThirdParty/SQLite.swift/Tests/SQLiteTests/ExpressionTests.swift new file mode 100644 index 0000000000..036e10ceb6 --- /dev/null +++ b/mobile/ios/ThirdParty/SQLite.swift/Tests/SQLiteTests/ExpressionTests.swift @@ -0,0 +1,6 @@ +import XCTest +import SQLite + +class ExpressionTests : XCTestCase { + +} diff --git a/mobile/ios/ThirdParty/SQLite.swift/Tests/SQLiteTests/FTS4Tests.swift b/mobile/ios/ThirdParty/SQLite.swift/Tests/SQLiteTests/FTS4Tests.swift new file mode 100644 index 0000000000..4373bf8b0e --- /dev/null +++ b/mobile/ios/ThirdParty/SQLite.swift/Tests/SQLiteTests/FTS4Tests.swift @@ -0,0 +1,208 @@ +import XCTest +import SQLite + +class FTS4Tests : XCTestCase { + + func test_create_onVirtualTable_withFTS4_compilesCreateVirtualTableExpression() { + XCTAssertEqual( + "CREATE VIRTUAL TABLE \"virtual_table\" USING fts4()", + virtualTable.create(.FTS4()) + ) + XCTAssertEqual( + "CREATE VIRTUAL TABLE \"virtual_table\" USING fts4(\"string\")", + virtualTable.create(.FTS4(string)) + ) + XCTAssertEqual( + "CREATE VIRTUAL TABLE \"virtual_table\" USING fts4(tokenize=simple)", + virtualTable.create(.FTS4(tokenize: .Simple)) + ) + XCTAssertEqual( + "CREATE VIRTUAL TABLE \"virtual_table\" USING fts4(\"string\", tokenize=porter)", + virtualTable.create(.FTS4([string], tokenize: .Porter)) + ) + XCTAssertEqual( + "CREATE VIRTUAL TABLE \"virtual_table\" USING fts4(tokenize=unicode61 \"removeDiacritics=0\")", + virtualTable.create(.FTS4(tokenize: .Unicode61(removeDiacritics: false))) + ) + XCTAssertEqual( + "CREATE VIRTUAL TABLE \"virtual_table\" USING fts4(tokenize=unicode61 \"removeDiacritics=1\" \"tokenchars=.\" \"separators=X\")", + virtualTable.create(.FTS4(tokenize: .Unicode61(removeDiacritics: true, tokenchars: ["."], separators: ["X"]))) + ) + } + + func test_match_onVirtualTableAsExpression_compilesMatchExpression() { + AssertSQL("(\"virtual_table\" MATCH 'string')", virtualTable.match("string") as Expression) + AssertSQL("(\"virtual_table\" MATCH \"string\")", virtualTable.match(string) as Expression) + AssertSQL("(\"virtual_table\" MATCH \"stringOptional\")", virtualTable.match(stringOptional) as Expression) + } + + func test_match_onVirtualTableAsQueryType_compilesMatchExpression() { + AssertSQL("SELECT * FROM \"virtual_table\" WHERE (\"virtual_table\" MATCH 'string')", virtualTable.match("string") as QueryType) + AssertSQL("SELECT * FROM \"virtual_table\" WHERE (\"virtual_table\" MATCH \"string\")", virtualTable.match(string) as QueryType) + AssertSQL("SELECT * FROM \"virtual_table\" WHERE (\"virtual_table\" MATCH \"stringOptional\")", virtualTable.match(stringOptional) as QueryType) + } + +} + +class FTS4ConfigTests : XCTestCase { + var config: FTS4Config! + + override func setUp() { + super.setUp() + config = FTS4Config() + } + + func test_empty_config() { + XCTAssertEqual( + "CREATE VIRTUAL TABLE \"virtual_table\" USING fts4()", + sql(config)) + } + + func test_config_column() { + XCTAssertEqual( + "CREATE VIRTUAL TABLE \"virtual_table\" USING fts4(\"string\")", + sql(config.column(string))) + } + + func test_config_columns() { + XCTAssertEqual( + "CREATE VIRTUAL TABLE \"virtual_table\" USING fts4(\"string\", \"int\")", + sql(config.columns([string, int]))) + } + + func test_config_unindexed_column() { + XCTAssertEqual( + "CREATE VIRTUAL TABLE \"virtual_table\" USING fts4(\"string\", notindexed=\"string\")", + sql(config.column(string, [.unindexed]))) + } + + func test_external_content_view() { + XCTAssertEqual( + "CREATE VIRTUAL TABLE \"virtual_table\" USING fts4(content=\"view\")", + sql(config.externalContent(_view ))) + } + + func test_external_content_virtual_table() { + XCTAssertEqual( + "CREATE VIRTUAL TABLE \"virtual_table\" USING fts4(content=\"virtual_table\")", + sql(config.externalContent(virtualTable))) + } + + func test_tokenizer_simple() { + XCTAssertEqual( + "CREATE VIRTUAL TABLE \"virtual_table\" USING fts4(tokenize=simple)", + sql(config.tokenizer(.Simple))) + } + + func test_tokenizer_porter() { + XCTAssertEqual( + "CREATE VIRTUAL TABLE \"virtual_table\" USING fts4(tokenize=porter)", + sql(config.tokenizer(.Porter))) + } + + func test_tokenizer_unicode61() { + XCTAssertEqual( + "CREATE VIRTUAL TABLE \"virtual_table\" USING fts4(tokenize=unicode61)", + sql(config.tokenizer(.Unicode61()))) + } + + func test_tokenizer_unicode61_with_options() { + XCTAssertEqual( + "CREATE VIRTUAL TABLE \"virtual_table\" USING fts4(tokenize=unicode61 \"removeDiacritics=1\" \"tokenchars=.\" \"separators=X\")", + sql(config.tokenizer(.Unicode61(removeDiacritics: true, tokenchars: ["."], separators: ["X"])))) + } + + func test_content_less() { + XCTAssertEqual( + "CREATE VIRTUAL TABLE \"virtual_table\" USING fts4(content=\"\")", + sql(config.contentless())) + } + + func test_config_matchinfo() { + XCTAssertEqual( + "CREATE VIRTUAL TABLE \"virtual_table\" USING fts4(matchinfo=\"fts3\")", + sql(config.matchInfo(.fts3))) + } + + func test_config_order_asc() { + XCTAssertEqual( + "CREATE VIRTUAL TABLE \"virtual_table\" USING fts4(order=\"asc\")", + sql(config.order(.asc))) + } + + func test_config_order_desc() { + XCTAssertEqual( + "CREATE VIRTUAL TABLE \"virtual_table\" USING fts4(order=\"desc\")", + sql(config.order(.desc))) + } + + func test_config_compress() { + XCTAssertEqual( + "CREATE VIRTUAL TABLE \"virtual_table\" USING fts4(compress=\"compress_foo\")", + sql(config.compress("compress_foo"))) + } + + func test_config_uncompress() { + XCTAssertEqual( + "CREATE VIRTUAL TABLE \"virtual_table\" USING fts4(uncompress=\"uncompress_foo\")", + sql(config.uncompress("uncompress_foo"))) + } + + func test_config_languageId() { + XCTAssertEqual( + "CREATE VIRTUAL TABLE \"virtual_table\" USING fts4(languageid=\"lid\")", + sql(config.languageId("lid"))) + } + + func test_config_all() { + XCTAssertEqual( + "CREATE VIRTUAL TABLE \"virtual_table\" USING fts4(\"int\", \"string\", \"date\", tokenize=porter, prefix=\"2,4\", content=\"table\", notindexed=\"string\", notindexed=\"date\", languageid=\"lid\", matchinfo=\"fts3\", order=\"desc\")", + sql(config + .tokenizer(.Porter) + .column(int) + .column(string, [.unindexed]) + .column(date, [.unindexed]) + .externalContent(table) + .matchInfo(.fts3) + .languageId("lid") + .order(.desc) + .prefix([2, 4])) + ) + } + + func sql(_ config: FTS4Config) -> String { + return virtualTable.create(.FTS4(config)) + } +} + +class FTS4IntegrationTests : SQLiteTestCase { +#if !SQLITE_SWIFT_STANDALONE && !SQLITE_SWIFT_SQLCIPHER + func test_registerTokenizer_registersTokenizer() { + let emails = VirtualTable("emails") + let subject = Expression("subject") + let body = Expression("body") + + let locale = CFLocaleCopyCurrent() + let tokenizerName = "tokenizer" + let tokenizer = CFStringTokenizerCreate(nil, "" as CFString!, CFRangeMake(0, 0), UInt(kCFStringTokenizerUnitWord), locale) + try! db.registerTokenizer(tokenizerName) { string in + CFStringTokenizerSetString(tokenizer, string as CFString, CFRangeMake(0, CFStringGetLength(string as CFString))) + if CFStringTokenizerAdvanceToNextToken(tokenizer).isEmpty { + return nil + } + let range = CFStringTokenizerGetCurrentTokenRange(tokenizer) + let input = CFStringCreateWithSubstring(kCFAllocatorDefault, string as CFString, range)! + let token = CFStringCreateMutableCopy(nil, range.length, input)! + CFStringLowercase(token, locale) + CFStringTransform(token, nil, kCFStringTransformStripDiacritics, false) + return (token as String, string.range(of: input as String)!) + } + + try! db.run(emails.create(.FTS4([subject, body], tokenize: .Custom(tokenizerName)))) + AssertSQL("CREATE VIRTUAL TABLE \"emails\" USING fts4(\"subject\", \"body\", tokenize=\"SQLite.swift\" \"tokenizer\")") + + try! _ = db.run(emails.insert(subject <- "Aún más cáfe!")) + XCTAssertEqual(1, try! db.scalar(emails.filter(emails.match("aun")).count)) + } +#endif +} diff --git a/mobile/ios/ThirdParty/SQLite.swift/Tests/SQLiteTests/FTS5Tests.swift b/mobile/ios/ThirdParty/SQLite.swift/Tests/SQLiteTests/FTS5Tests.swift new file mode 100644 index 0000000000..63d8dc4043 --- /dev/null +++ b/mobile/ios/ThirdParty/SQLite.swift/Tests/SQLiteTests/FTS5Tests.swift @@ -0,0 +1,124 @@ +import XCTest +import SQLite + +class FTS5Tests: XCTestCase { + var config: FTS5Config! + + override func setUp() { + super.setUp() + config = FTS5Config() + } + + func test_empty_config() { + XCTAssertEqual( + "CREATE VIRTUAL TABLE \"virtual_table\" USING fts5()", + sql(config)) + } + + func test_config_column() { + XCTAssertEqual( + "CREATE VIRTUAL TABLE \"virtual_table\" USING fts5(\"string\")", + sql(config.column(string))) + } + + func test_config_columns() { + XCTAssertEqual( + "CREATE VIRTUAL TABLE \"virtual_table\" USING fts5(\"string\", \"int\")", + sql(config.columns([string, int]))) + } + + func test_config_unindexed_column() { + XCTAssertEqual( + "CREATE VIRTUAL TABLE \"virtual_table\" USING fts5(\"string\" UNINDEXED)", + sql(config.column(string, [.unindexed]))) + } + + func test_external_content_table() { + XCTAssertEqual( + "CREATE VIRTUAL TABLE \"virtual_table\" USING fts5(content=\"table\")", + sql(config.externalContent(table))) + } + + func test_external_content_view() { + XCTAssertEqual( + "CREATE VIRTUAL TABLE \"virtual_table\" USING fts5(content=\"view\")", + sql(config.externalContent(_view))) + } + + func test_external_content_virtual_table() { + XCTAssertEqual( + "CREATE VIRTUAL TABLE \"virtual_table\" USING fts5(content=\"virtual_table\")", + sql(config.externalContent(virtualTable))) + } + + func test_content_less() { + XCTAssertEqual( + "CREATE VIRTUAL TABLE \"virtual_table\" USING fts5(content=\"\")", + sql(config.contentless())) + } + + func test_content_rowid() { + XCTAssertEqual( + "CREATE VIRTUAL TABLE \"virtual_table\" USING fts5(content_rowid=\"string\")", + sql(config.contentRowId(string))) + } + + func test_tokenizer_porter() { + XCTAssertEqual( + "CREATE VIRTUAL TABLE \"virtual_table\" USING fts5(tokenize=porter)", + sql(config.tokenizer(.Porter))) + } + + func test_tokenizer_unicode61() { + XCTAssertEqual( + "CREATE VIRTUAL TABLE \"virtual_table\" USING fts5(tokenize=unicode61)", + sql(config.tokenizer(.Unicode61()))) + } + + func test_tokenizer_unicode61_with_options() { + XCTAssertEqual( + "CREATE VIRTUAL TABLE \"virtual_table\" USING fts5(tokenize=unicode61 \"removeDiacritics=1\" \"tokenchars=.\" \"separators=X\")", + sql(config.tokenizer(.Unicode61(removeDiacritics: true, tokenchars: ["."], separators: ["X"])))) + } + + func test_column_size() { + XCTAssertEqual( + "CREATE VIRTUAL TABLE \"virtual_table\" USING fts5(columnsize=1)", + sql(config.columnSize(1))) + } + + func test_detail_full() { + XCTAssertEqual( + "CREATE VIRTUAL TABLE \"virtual_table\" USING fts5(detail=\"full\")", + sql(config.detail(.full))) + } + + func test_detail_column() { + XCTAssertEqual( + "CREATE VIRTUAL TABLE \"virtual_table\" USING fts5(detail=\"column\")", + sql(config.detail(.column))) + } + + func test_detail_none() { + XCTAssertEqual( + "CREATE VIRTUAL TABLE \"virtual_table\" USING fts5(detail=\"none\")", + sql(config.detail(.none))) + } + + func test_fts5_config_all() { + XCTAssertEqual( + "CREATE VIRTUAL TABLE \"virtual_table\" USING fts5(\"int\", \"string\" UNINDEXED, \"date\" UNINDEXED, tokenize=porter, prefix=\"2,4\", content=\"table\")", + sql(config + .tokenizer(.Porter) + .column(int) + .column(string, [.unindexed]) + .column(date, [.unindexed]) + .externalContent(table) + .prefix([2, 4])) + ) + } + + func sql(_ config: FTS5Config) -> String { + return virtualTable.create(.FTS5(config)) + } +} diff --git a/mobile/ios/ThirdParty/SQLite.swift/Tests/SQLiteTests/Fixtures.swift b/mobile/ios/ThirdParty/SQLite.swift/Tests/SQLiteTests/Fixtures.swift new file mode 100644 index 0000000000..13f83f7715 --- /dev/null +++ b/mobile/ios/ThirdParty/SQLite.swift/Tests/SQLiteTests/Fixtures.swift @@ -0,0 +1,8 @@ +import Foundation + +func fixture(_ name: String, withExtension: String?) -> String { + let testBundle = Bundle(for: SQLiteTestCase.self) + return testBundle.url( + forResource: URL(string: "fixtures")?.appendingPathComponent(name).path, + withExtension: withExtension)!.path +} diff --git a/mobile/ios/ThirdParty/SQLite.swift/Tests/SQLiteTests/FoundationTests.swift b/mobile/ios/ThirdParty/SQLite.swift/Tests/SQLiteTests/FoundationTests.swift new file mode 100644 index 0000000000..0df746d9e5 --- /dev/null +++ b/mobile/ios/ThirdParty/SQLite.swift/Tests/SQLiteTests/FoundationTests.swift @@ -0,0 +1,16 @@ +import XCTest +import SQLite + +class FoundationTests : XCTestCase { + func testDataFromBlob() { + let data = Data(bytes: [1, 2, 3]) + let blob = data.datatypeValue + XCTAssertEqual([1, 2, 3], blob.bytes) + } + + func testBlobToData() { + let blob = Blob(bytes: [1, 2, 3]) + let data = Data.fromDatatypeValue(blob) + XCTAssertEqual(Data(bytes: [1, 2, 3]), data) + } +} diff --git a/mobile/ios/ThirdParty/SQLite.swift/Tests/SQLiteTests/Info.plist b/mobile/ios/ThirdParty/SQLite.swift/Tests/SQLiteTests/Info.plist new file mode 100644 index 0000000000..ba72822e87 --- /dev/null +++ b/mobile/ios/ThirdParty/SQLite.swift/Tests/SQLiteTests/Info.plist @@ -0,0 +1,24 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + BNDL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1 + + diff --git a/mobile/ios/ThirdParty/SQLite.swift/Tests/SQLiteTests/OperatorsTests.swift b/mobile/ios/ThirdParty/SQLite.swift/Tests/SQLiteTests/OperatorsTests.swift new file mode 100644 index 0000000000..f0e585cd70 --- /dev/null +++ b/mobile/ios/ThirdParty/SQLite.swift/Tests/SQLiteTests/OperatorsTests.swift @@ -0,0 +1,296 @@ +import XCTest +import SQLite + +class OperatorsTests : XCTestCase { + + func test_stringExpressionPlusStringExpression_buildsConcatenatingStringExpression() { + AssertSQL("(\"string\" || \"string\")", string + string) + AssertSQL("(\"string\" || \"stringOptional\")", string + stringOptional) + AssertSQL("(\"stringOptional\" || \"string\")", stringOptional + string) + AssertSQL("(\"stringOptional\" || \"stringOptional\")", stringOptional + stringOptional) + AssertSQL("(\"string\" || 'literal')", string + "literal") + AssertSQL("(\"stringOptional\" || 'literal')", stringOptional + "literal") + AssertSQL("('literal' || \"string\")", "literal" + string) + AssertSQL("('literal' || \"stringOptional\")", "literal" + stringOptional) + } + + func test_numberExpression_plusNumberExpression_buildsAdditiveNumberExpression() { + AssertSQL("(\"int\" + \"int\")", int + int) + AssertSQL("(\"int\" + \"intOptional\")", int + intOptional) + AssertSQL("(\"intOptional\" + \"int\")", intOptional + int) + AssertSQL("(\"intOptional\" + \"intOptional\")", intOptional + intOptional) + AssertSQL("(\"int\" + 1)", int + 1) + AssertSQL("(\"intOptional\" + 1)", intOptional + 1) + AssertSQL("(1 + \"int\")", 1 + int) + AssertSQL("(1 + \"intOptional\")", 1 + intOptional) + + AssertSQL("(\"double\" + \"double\")", double + double) + AssertSQL("(\"double\" + \"doubleOptional\")", double + doubleOptional) + AssertSQL("(\"doubleOptional\" + \"double\")", doubleOptional + double) + AssertSQL("(\"doubleOptional\" + \"doubleOptional\")", doubleOptional + doubleOptional) + AssertSQL("(\"double\" + 1.0)", double + 1) + AssertSQL("(\"doubleOptional\" + 1.0)", doubleOptional + 1) + AssertSQL("(1.0 + \"double\")", 1 + double) + AssertSQL("(1.0 + \"doubleOptional\")", 1 + doubleOptional) + } + + func test_numberExpression_minusNumberExpression_buildsSubtractiveNumberExpression() { + AssertSQL("(\"int\" - \"int\")", int - int) + AssertSQL("(\"int\" - \"intOptional\")", int - intOptional) + AssertSQL("(\"intOptional\" - \"int\")", intOptional - int) + AssertSQL("(\"intOptional\" - \"intOptional\")", intOptional - intOptional) + AssertSQL("(\"int\" - 1)", int - 1) + AssertSQL("(\"intOptional\" - 1)", intOptional - 1) + AssertSQL("(1 - \"int\")", 1 - int) + AssertSQL("(1 - \"intOptional\")", 1 - intOptional) + + AssertSQL("(\"double\" - \"double\")", double - double) + AssertSQL("(\"double\" - \"doubleOptional\")", double - doubleOptional) + AssertSQL("(\"doubleOptional\" - \"double\")", doubleOptional - double) + AssertSQL("(\"doubleOptional\" - \"doubleOptional\")", doubleOptional - doubleOptional) + AssertSQL("(\"double\" - 1.0)", double - 1) + AssertSQL("(\"doubleOptional\" - 1.0)", doubleOptional - 1) + AssertSQL("(1.0 - \"double\")", 1 - double) + AssertSQL("(1.0 - \"doubleOptional\")", 1 - doubleOptional) + } + + func test_numberExpression_timesNumberExpression_buildsMultiplicativeNumberExpression() { + AssertSQL("(\"int\" * \"int\")", int * int) + AssertSQL("(\"int\" * \"intOptional\")", int * intOptional) + AssertSQL("(\"intOptional\" * \"int\")", intOptional * int) + AssertSQL("(\"intOptional\" * \"intOptional\")", intOptional * intOptional) + AssertSQL("(\"int\" * 1)", int * 1) + AssertSQL("(\"intOptional\" * 1)", intOptional * 1) + AssertSQL("(1 * \"int\")", 1 * int) + AssertSQL("(1 * \"intOptional\")", 1 * intOptional) + + AssertSQL("(\"double\" * \"double\")", double * double) + AssertSQL("(\"double\" * \"doubleOptional\")", double * doubleOptional) + AssertSQL("(\"doubleOptional\" * \"double\")", doubleOptional * double) + AssertSQL("(\"doubleOptional\" * \"doubleOptional\")", doubleOptional * doubleOptional) + AssertSQL("(\"double\" * 1.0)", double * 1) + AssertSQL("(\"doubleOptional\" * 1.0)", doubleOptional * 1) + AssertSQL("(1.0 * \"double\")", 1 * double) + AssertSQL("(1.0 * \"doubleOptional\")", 1 * doubleOptional) + } + + func test_numberExpression_dividedByNumberExpression_buildsDivisiveNumberExpression() { + AssertSQL("(\"int\" / \"int\")", int / int) + AssertSQL("(\"int\" / \"intOptional\")", int / intOptional) + AssertSQL("(\"intOptional\" / \"int\")", intOptional / int) + AssertSQL("(\"intOptional\" / \"intOptional\")", intOptional / intOptional) + AssertSQL("(\"int\" / 1)", int / 1) + AssertSQL("(\"intOptional\" / 1)", intOptional / 1) + AssertSQL("(1 / \"int\")", 1 / int) + AssertSQL("(1 / \"intOptional\")", 1 / intOptional) + + AssertSQL("(\"double\" / \"double\")", double / double) + AssertSQL("(\"double\" / \"doubleOptional\")", double / doubleOptional) + AssertSQL("(\"doubleOptional\" / \"double\")", doubleOptional / double) + AssertSQL("(\"doubleOptional\" / \"doubleOptional\")", doubleOptional / doubleOptional) + AssertSQL("(\"double\" / 1.0)", double / 1) + AssertSQL("(\"doubleOptional\" / 1.0)", doubleOptional / 1) + AssertSQL("(1.0 / \"double\")", 1 / double) + AssertSQL("(1.0 / \"doubleOptional\")", 1 / doubleOptional) + } + + func test_numberExpression_prefixedWithMinus_buildsInvertedNumberExpression() { + AssertSQL("-(\"int\")", -int) + AssertSQL("-(\"intOptional\")", -intOptional) + + AssertSQL("-(\"double\")", -double) + AssertSQL("-(\"doubleOptional\")", -doubleOptional) + } + + func test_integerExpression_moduloIntegerExpression_buildsModuloIntegerExpression() { + AssertSQL("(\"int\" % \"int\")", int % int) + AssertSQL("(\"int\" % \"intOptional\")", int % intOptional) + AssertSQL("(\"intOptional\" % \"int\")", intOptional % int) + AssertSQL("(\"intOptional\" % \"intOptional\")", intOptional % intOptional) + AssertSQL("(\"int\" % 1)", int % 1) + AssertSQL("(\"intOptional\" % 1)", intOptional % 1) + AssertSQL("(1 % \"int\")", 1 % int) + AssertSQL("(1 % \"intOptional\")", 1 % intOptional) + } + + func test_integerExpression_bitShiftLeftIntegerExpression_buildsLeftShiftedIntegerExpression() { + AssertSQL("(\"int\" << \"int\")", int << int) + AssertSQL("(\"int\" << \"intOptional\")", int << intOptional) + AssertSQL("(\"intOptional\" << \"int\")", intOptional << int) + AssertSQL("(\"intOptional\" << \"intOptional\")", intOptional << intOptional) + AssertSQL("(\"int\" << 1)", int << 1) + AssertSQL("(\"intOptional\" << 1)", intOptional << 1) + AssertSQL("(1 << \"int\")", 1 << int) + AssertSQL("(1 << \"intOptional\")", 1 << intOptional) + } + + func test_integerExpression_bitShiftRightIntegerExpression_buildsRightShiftedIntegerExpression() { + AssertSQL("(\"int\" >> \"int\")", int >> int) + AssertSQL("(\"int\" >> \"intOptional\")", int >> intOptional) + AssertSQL("(\"intOptional\" >> \"int\")", intOptional >> int) + AssertSQL("(\"intOptional\" >> \"intOptional\")", intOptional >> intOptional) + AssertSQL("(\"int\" >> 1)", int >> 1) + AssertSQL("(\"intOptional\" >> 1)", intOptional >> 1) + AssertSQL("(1 >> \"int\")", 1 >> int) + AssertSQL("(1 >> \"intOptional\")", 1 >> intOptional) + } + + func test_integerExpression_bitwiseAndIntegerExpression_buildsAndedIntegerExpression() { + AssertSQL("(\"int\" & \"int\")", int & int) + AssertSQL("(\"int\" & \"intOptional\")", int & intOptional) + AssertSQL("(\"intOptional\" & \"int\")", intOptional & int) + AssertSQL("(\"intOptional\" & \"intOptional\")", intOptional & intOptional) + AssertSQL("(\"int\" & 1)", int & 1) + AssertSQL("(\"intOptional\" & 1)", intOptional & 1) + AssertSQL("(1 & \"int\")", 1 & int) + AssertSQL("(1 & \"intOptional\")", 1 & intOptional) + } + + func test_integerExpression_bitwiseOrIntegerExpression_buildsOredIntegerExpression() { + AssertSQL("(\"int\" | \"int\")", int | int) + AssertSQL("(\"int\" | \"intOptional\")", int | intOptional) + AssertSQL("(\"intOptional\" | \"int\")", intOptional | int) + AssertSQL("(\"intOptional\" | \"intOptional\")", intOptional | intOptional) + AssertSQL("(\"int\" | 1)", int | 1) + AssertSQL("(\"intOptional\" | 1)", intOptional | 1) + AssertSQL("(1 | \"int\")", 1 | int) + AssertSQL("(1 | \"intOptional\")", 1 | intOptional) + } + + func test_integerExpression_bitwiseExclusiveOrIntegerExpression_buildsOredIntegerExpression() { + AssertSQL("(~((\"int\" & \"int\")) & (\"int\" | \"int\"))", int ^ int) + AssertSQL("(~((\"int\" & \"intOptional\")) & (\"int\" | \"intOptional\"))", int ^ intOptional) + AssertSQL("(~((\"intOptional\" & \"int\")) & (\"intOptional\" | \"int\"))", intOptional ^ int) + AssertSQL("(~((\"intOptional\" & \"intOptional\")) & (\"intOptional\" | \"intOptional\"))", intOptional ^ intOptional) + AssertSQL("(~((\"int\" & 1)) & (\"int\" | 1))", int ^ 1) + AssertSQL("(~((\"intOptional\" & 1)) & (\"intOptional\" | 1))", intOptional ^ 1) + AssertSQL("(~((1 & \"int\")) & (1 | \"int\"))", 1 ^ int) + AssertSQL("(~((1 & \"intOptional\")) & (1 | \"intOptional\"))", 1 ^ intOptional) + } + + func test_bitwiseNot_integerExpression_buildsComplementIntegerExpression() { + AssertSQL("~(\"int\")", ~int) + AssertSQL("~(\"intOptional\")", ~intOptional) + } + + func test_equalityOperator_withEquatableExpressions_buildsBooleanExpression() { + AssertSQL("(\"bool\" = \"bool\")", bool == bool) + AssertSQL("(\"bool\" = \"boolOptional\")", bool == boolOptional) + AssertSQL("(\"boolOptional\" = \"bool\")", boolOptional == bool) + AssertSQL("(\"boolOptional\" = \"boolOptional\")", boolOptional == boolOptional) + AssertSQL("(\"bool\" = 1)", bool == true) + AssertSQL("(\"boolOptional\" = 1)", boolOptional == true) + AssertSQL("(1 = \"bool\")", true == bool) + AssertSQL("(1 = \"boolOptional\")", true == boolOptional) + + AssertSQL("(\"boolOptional\" IS NULL)", boolOptional == nil) + AssertSQL("(NULL IS \"boolOptional\")", nil == boolOptional) + } + + func test_inequalityOperator_withEquatableExpressions_buildsBooleanExpression() { + AssertSQL("(\"bool\" != \"bool\")", bool != bool) + AssertSQL("(\"bool\" != \"boolOptional\")", bool != boolOptional) + AssertSQL("(\"boolOptional\" != \"bool\")", boolOptional != bool) + AssertSQL("(\"boolOptional\" != \"boolOptional\")", boolOptional != boolOptional) + AssertSQL("(\"bool\" != 1)", bool != true) + AssertSQL("(\"boolOptional\" != 1)", boolOptional != true) + AssertSQL("(1 != \"bool\")", true != bool) + AssertSQL("(1 != \"boolOptional\")", true != boolOptional) + + AssertSQL("(\"boolOptional\" IS NOT NULL)", boolOptional != nil) + AssertSQL("(NULL IS NOT \"boolOptional\")", nil != boolOptional) + } + + func test_greaterThanOperator_withComparableExpressions_buildsBooleanExpression() { + AssertSQL("(\"bool\" > \"bool\")", bool > bool) + AssertSQL("(\"bool\" > \"boolOptional\")", bool > boolOptional) + AssertSQL("(\"boolOptional\" > \"bool\")", boolOptional > bool) + AssertSQL("(\"boolOptional\" > \"boolOptional\")", boolOptional > boolOptional) + AssertSQL("(\"bool\" > 1)", bool > true) + AssertSQL("(\"boolOptional\" > 1)", boolOptional > true) + AssertSQL("(1 > \"bool\")", true > bool) + AssertSQL("(1 > \"boolOptional\")", true > boolOptional) + } + + func test_greaterThanOrEqualToOperator_withComparableExpressions_buildsBooleanExpression() { + AssertSQL("(\"bool\" >= \"bool\")", bool >= bool) + AssertSQL("(\"bool\" >= \"boolOptional\")", bool >= boolOptional) + AssertSQL("(\"boolOptional\" >= \"bool\")", boolOptional >= bool) + AssertSQL("(\"boolOptional\" >= \"boolOptional\")", boolOptional >= boolOptional) + AssertSQL("(\"bool\" >= 1)", bool >= true) + AssertSQL("(\"boolOptional\" >= 1)", boolOptional >= true) + AssertSQL("(1 >= \"bool\")", true >= bool) + AssertSQL("(1 >= \"boolOptional\")", true >= boolOptional) + } + + func test_lessThanOperator_withComparableExpressions_buildsBooleanExpression() { + AssertSQL("(\"bool\" < \"bool\")", bool < bool) + AssertSQL("(\"bool\" < \"boolOptional\")", bool < boolOptional) + AssertSQL("(\"boolOptional\" < \"bool\")", boolOptional < bool) + AssertSQL("(\"boolOptional\" < \"boolOptional\")", boolOptional < boolOptional) + AssertSQL("(\"bool\" < 1)", bool < true) + AssertSQL("(\"boolOptional\" < 1)", boolOptional < true) + AssertSQL("(1 < \"bool\")", true < bool) + AssertSQL("(1 < \"boolOptional\")", true < boolOptional) + } + + func test_lessThanOrEqualToOperator_withComparableExpressions_buildsBooleanExpression() { + AssertSQL("(\"bool\" <= \"bool\")", bool <= bool) + AssertSQL("(\"bool\" <= \"boolOptional\")", bool <= boolOptional) + AssertSQL("(\"boolOptional\" <= \"bool\")", boolOptional <= bool) + AssertSQL("(\"boolOptional\" <= \"boolOptional\")", boolOptional <= boolOptional) + AssertSQL("(\"bool\" <= 1)", bool <= true) + AssertSQL("(\"boolOptional\" <= 1)", boolOptional <= true) + AssertSQL("(1 <= \"bool\")", true <= bool) + AssertSQL("(1 <= \"boolOptional\")", true <= boolOptional) + } + + func test_patternMatchingOperator_withComparableCountableClosedRange_buildsBetweenBooleanExpression() { + AssertSQL("\"int\" BETWEEN 0 AND 5", 0...5 ~= int) + AssertSQL("\"intOptional\" BETWEEN 0 AND 5", 0...5 ~= intOptional) + } + + func test_patternMatchingOperator_withComparableClosedRange_buildsBetweenBooleanExpression() { + AssertSQL("\"double\" BETWEEN 1.2 AND 4.5", 1.2...4.5 ~= double) + AssertSQL("\"doubleOptional\" BETWEEN 1.2 AND 4.5", 1.2...4.5 ~= doubleOptional) + } + + func test_patternMatchingOperator_withomparableClosedRangeString_buildsBetweenBooleanExpression() { + AssertSQL("\"string\" BETWEEN 'a' AND 'b'", "a"..."b" ~= string) + AssertSQL("\"stringOptional\" BETWEEN 'a' AND 'b'", "a"..."b" ~= stringOptional) + } + + func test_doubleAndOperator_withBooleanExpressions_buildsCompoundExpression() { + AssertSQL("(\"bool\" AND \"bool\")", bool && bool) + AssertSQL("(\"bool\" AND \"boolOptional\")", bool && boolOptional) + AssertSQL("(\"boolOptional\" AND \"bool\")", boolOptional && bool) + AssertSQL("(\"boolOptional\" AND \"boolOptional\")", boolOptional && boolOptional) + AssertSQL("(\"bool\" AND 1)", bool && true) + AssertSQL("(\"boolOptional\" AND 1)", boolOptional && true) + AssertSQL("(1 AND \"bool\")", true && bool) + AssertSQL("(1 AND \"boolOptional\")", true && boolOptional) + } + + func test_doubleOrOperator_withBooleanExpressions_buildsCompoundExpression() { + AssertSQL("(\"bool\" OR \"bool\")", bool || bool) + AssertSQL("(\"bool\" OR \"boolOptional\")", bool || boolOptional) + AssertSQL("(\"boolOptional\" OR \"bool\")", boolOptional || bool) + AssertSQL("(\"boolOptional\" OR \"boolOptional\")", boolOptional || boolOptional) + AssertSQL("(\"bool\" OR 1)", bool || true) + AssertSQL("(\"boolOptional\" OR 1)", boolOptional || true) + AssertSQL("(1 OR \"bool\")", true || bool) + AssertSQL("(1 OR \"boolOptional\")", true || boolOptional) + } + + func test_unaryNotOperator_withBooleanExpressions_buildsNotExpression() { + AssertSQL("NOT (\"bool\")", !bool) + AssertSQL("NOT (\"boolOptional\")", !boolOptional) + } + + func test_precedencePreserved() { + let n = Expression(value: 1) + AssertSQL("(((1 = 1) AND (1 = 1)) OR (1 = 1))", (n == n && n == n) || n == n) + AssertSQL("((1 = 1) AND ((1 = 1) OR (1 = 1)))", n == n && (n == n || n == n)) + } + +} diff --git a/mobile/ios/ThirdParty/SQLite.swift/Tests/SQLiteTests/QueryTests.swift b/mobile/ios/ThirdParty/SQLite.swift/Tests/SQLiteTests/QueryTests.swift new file mode 100644 index 0000000000..2cf164c6d3 --- /dev/null +++ b/mobile/ios/ThirdParty/SQLite.swift/Tests/SQLiteTests/QueryTests.swift @@ -0,0 +1,365 @@ +import XCTest +import SQLite + +class QueryTests : XCTestCase { + + let users = Table("users") + let id = Expression("id") + let email = Expression("email") + let age = Expression("age") + let admin = Expression("admin") + let optionalAdmin = Expression("admin") + + let posts = Table("posts") + let userId = Expression("user_id") + let categoryId = Expression("category_id") + let published = Expression("published") + + let categories = Table("categories") + let tag = Expression("tag") + + func test_select_withExpression_compilesSelectClause() { + AssertSQL("SELECT \"email\" FROM \"users\"", users.select(email)) + } + + func test_select_withStarExpression_compilesSelectClause() { + AssertSQL("SELECT * FROM \"users\"", users.select(*)) + } + + func test_select_withNamespacedStarExpression_compilesSelectClause() { + AssertSQL("SELECT \"users\".* FROM \"users\"", users.select(users[*])) + } + + func test_select_withVariadicExpressions_compilesSelectClause() { + AssertSQL("SELECT \"email\", count(*) FROM \"users\"", users.select(email, count(*))) + } + + func test_select_withExpressions_compilesSelectClause() { + AssertSQL("SELECT \"email\", count(*) FROM \"users\"", users.select([email, count(*)])) + } + + func test_selectDistinct_withExpression_compilesSelectClause() { + AssertSQL("SELECT DISTINCT \"age\" FROM \"users\"", users.select(distinct: age)) + } + + func test_selectDistinct_withExpressions_compilesSelectClause() { + AssertSQL("SELECT DISTINCT \"age\", \"admin\" FROM \"users\"", users.select(distinct: [age, admin])) + } + + func test_selectDistinct_withStar_compilesSelectClause() { + AssertSQL("SELECT DISTINCT * FROM \"users\"", users.select(distinct: *)) + } + + func test_join_compilesJoinClause() { + AssertSQL( + "SELECT * FROM \"users\" INNER JOIN \"posts\" ON (\"posts\".\"user_id\" = \"users\".\"id\")", + users.join(posts, on: posts[userId] == users[id]) + ) + } + + func test_join_withExplicitType_compilesJoinClauseWithType() { + AssertSQL( + "SELECT * FROM \"users\" LEFT OUTER JOIN \"posts\" ON (\"posts\".\"user_id\" = \"users\".\"id\")", + users.join(.leftOuter, posts, on: posts[userId] == users[id]) + ) + + AssertSQL( + "SELECT * FROM \"users\" CROSS JOIN \"posts\" ON (\"posts\".\"user_id\" = \"users\".\"id\")", + users.join(.cross, posts, on: posts[userId] == users[id]) + ) + } + + func test_join_withTableCondition_compilesJoinClauseWithTableCondition() { + AssertSQL( + "SELECT * FROM \"users\" INNER JOIN \"posts\" ON ((\"posts\".\"user_id\" = \"users\".\"id\") AND \"published\")", + users.join(posts.filter(published), on: posts[userId] == users[id]) + ) + } + + func test_join_whenChained_compilesAggregateJoinClause() { + AssertSQL( + "SELECT * FROM \"users\" " + + "INNER JOIN \"posts\" ON (\"posts\".\"user_id\" = \"users\".\"id\") " + + "INNER JOIN \"categories\" ON (\"categories\".\"id\" = \"posts\".\"category_id\")", + users.join(posts, on: posts[userId] == users[id]).join(categories, on: categories[id] == posts[categoryId]) + ) + } + + func test_filter_compilesWhereClause() { + AssertSQL("SELECT * FROM \"users\" WHERE (\"admin\" = 1)", users.filter(admin == true)) + } + + func test_filter_compilesWhereClause_false() { + AssertSQL("SELECT * FROM \"users\" WHERE (\"admin\" = 0)", users.filter(admin == false)) + } + + func test_filter_compilesWhereClause_optional() { + AssertSQL("SELECT * FROM \"users\" WHERE (\"admin\" = 1)", users.filter(optionalAdmin == true)) + } + + func test_filter_compilesWhereClause_optional_false() { + AssertSQL("SELECT * FROM \"users\" WHERE (\"admin\" = 0)", users.filter(optionalAdmin == false)) + } + + func test_where_compilesWhereClause() { + AssertSQL("SELECT * FROM \"users\" WHERE (\"admin\" = 1)", users.where(admin == true)) + } + + func test_where_compilesWhereClause_false() { + AssertSQL("SELECT * FROM \"users\" WHERE (\"admin\" = 0)", users.where(admin == false)) + } + + func test_where_compilesWhereClause_optional() { + AssertSQL("SELECT * FROM \"users\" WHERE (\"admin\" = 1)", users.where(optionalAdmin == true)) + } + + func test_where_compilesWhereClause_optional_false() { + AssertSQL("SELECT * FROM \"users\" WHERE (\"admin\" = 0)", users.where(optionalAdmin == false)) + } + + func test_filter_whenChained_compilesAggregateWhereClause() { + AssertSQL( + "SELECT * FROM \"users\" WHERE ((\"age\" >= 35) AND \"admin\")", + users.filter(age >= 35).filter(admin) + ) + } + + func test_group_withSingleExpressionName_compilesGroupClause() { + AssertSQL("SELECT * FROM \"users\" GROUP BY \"age\"", + users.group(age)) + } + + func test_group_withVariadicExpressionNames_compilesGroupClause() { + AssertSQL("SELECT * FROM \"users\" GROUP BY \"age\", \"admin\"", users.group(age, admin)) + } + + func test_group_withExpressionNameAndHavingBindings_compilesGroupClause() { + AssertSQL("SELECT * FROM \"users\" GROUP BY \"age\" HAVING \"admin\"", users.group(age, having: admin)) + AssertSQL("SELECT * FROM \"users\" GROUP BY \"age\" HAVING (\"age\" >= 30)", users.group(age, having: age >= 30)) + } + + func test_group_withExpressionNamesAndHavingBindings_compilesGroupClause() { + AssertSQL( + "SELECT * FROM \"users\" GROUP BY \"age\", \"admin\" HAVING \"admin\"", + users.group([age, admin], having: admin) + ) + AssertSQL( + "SELECT * FROM \"users\" GROUP BY \"age\", \"admin\" HAVING (\"age\" >= 30)", + users.group([age, admin], having: age >= 30) + ) + } + + func test_order_withSingleExpressionName_compilesOrderClause() { + AssertSQL("SELECT * FROM \"users\" ORDER BY \"age\"", users.order(age)) + } + + func test_order_withVariadicExpressionNames_compilesOrderClause() { + AssertSQL("SELECT * FROM \"users\" ORDER BY \"age\", \"email\"", users.order(age, email)) + } + + func test_order_withArrayExpressionNames_compilesOrderClause() { + AssertSQL("SELECT * FROM \"users\" ORDER BY \"age\", \"email\"", users.order([age, email])) + } + + func test_order_withExpressionAndSortDirection_compilesOrderClause() { +// AssertSQL("SELECT * FROM \"users\" ORDER BY \"age\" DESC, \"email\" ASC", users.order(age.desc, email.asc)) + } + + func test_order_whenChained_resetsOrderClause() { + AssertSQL("SELECT * FROM \"users\" ORDER BY \"age\"", users.order(email).order(age)) + } + + func test_reverse_withoutOrder_ordersByRowIdDescending() { +// AssertSQL("SELECT * FROM \"users\" ORDER BY \"ROWID\" DESC", users.reverse()) + } + + func test_reverse_withOrder_reversesOrder() { +// AssertSQL("SELECT * FROM \"users\" ORDER BY \"age\" DESC, \"email\" ASC", users.order(age, email.desc).reverse()) + } + + func test_limit_compilesLimitClause() { + AssertSQL("SELECT * FROM \"users\" LIMIT 5", users.limit(5)) + } + + func test_limit_withOffset_compilesOffsetClause() { + AssertSQL("SELECT * FROM \"users\" LIMIT 5 OFFSET 5", users.limit(5, offset: 5)) + } + + func test_limit_whenChained_overridesLimit() { + let query = users.limit(5) + + AssertSQL("SELECT * FROM \"users\" LIMIT 10", query.limit(10)) + AssertSQL("SELECT * FROM \"users\"", query.limit(nil)) + } + + func test_limit_whenChained_withOffset_overridesOffset() { + let query = users.limit(5, offset: 5) + + AssertSQL("SELECT * FROM \"users\" LIMIT 10 OFFSET 20", query.limit(10, offset: 20)) + AssertSQL("SELECT * FROM \"users\"", query.limit(nil)) + } + + func test_alias_aliasesTable() { + let managerId = Expression("manager_id") + + let managers = users.alias("managers") + + AssertSQL( + "SELECT * FROM \"users\" " + + "INNER JOIN \"users\" AS \"managers\" ON (\"managers\".\"id\" = \"users\".\"manager_id\")", + users.join(managers, on: managers[id] == users[managerId]) + ) + } + + func test_insert_compilesInsertExpression() { + AssertSQL( + "INSERT INTO \"users\" (\"email\", \"age\") VALUES ('alice@example.com', 30)", + users.insert(email <- "alice@example.com", age <- 30) + ) + } + + func test_insert_withOnConflict_compilesInsertOrOnConflictExpression() { + AssertSQL( + "INSERT OR REPLACE INTO \"users\" (\"email\", \"age\") VALUES ('alice@example.com', 30)", + users.insert(or: .replace, email <- "alice@example.com", age <- 30) + ) + } + + func test_insert_compilesInsertExpressionWithDefaultValues() { + AssertSQL("INSERT INTO \"users\" DEFAULT VALUES", users.insert()) + } + + func test_insert_withQuery_compilesInsertExpressionWithSelectStatement() { + let emails = Table("emails") + + AssertSQL( + "INSERT INTO \"emails\" SELECT \"email\" FROM \"users\" WHERE \"admin\"", + emails.insert(users.select(email).filter(admin)) + ) + } + + func test_update_compilesUpdateExpression() { + AssertSQL( + "UPDATE \"users\" SET \"age\" = 30, \"admin\" = 1 WHERE (\"id\" = 1)", + users.filter(id == 1).update(age <- 30, admin <- true) + ) + } + + func test_delete_compilesDeleteExpression() { + AssertSQL( + "DELETE FROM \"users\" WHERE (\"id\" = 1)", + users.filter(id == 1).delete() + ) + } + + func test_delete_compilesExistsExpression() { + AssertSQL( + "SELECT EXISTS (SELECT * FROM \"users\")", + users.exists + ) + } + + func test_count_returnsCountExpression() { + AssertSQL("SELECT count(*) FROM \"users\"", users.count) + } + + func test_scalar_returnsScalarExpression() { + AssertSQL("SELECT \"int\" FROM \"table\"", table.select(int) as ScalarQuery) + AssertSQL("SELECT \"intOptional\" FROM \"table\"", table.select(intOptional) as ScalarQuery) + AssertSQL("SELECT DISTINCT \"int\" FROM \"table\"", table.select(distinct: int) as ScalarQuery) + AssertSQL("SELECT DISTINCT \"intOptional\" FROM \"table\"", table.select(distinct: intOptional) as ScalarQuery) + } + + func test_subscript_withExpression_returnsNamespacedExpression() { + let query = Table("query") + + AssertSQL("\"query\".\"blob\"", query[data]) + AssertSQL("\"query\".\"blobOptional\"", query[dataOptional]) + + AssertSQL("\"query\".\"bool\"", query[bool]) + AssertSQL("\"query\".\"boolOptional\"", query[boolOptional]) + + AssertSQL("\"query\".\"date\"", query[date]) + AssertSQL("\"query\".\"dateOptional\"", query[dateOptional]) + + AssertSQL("\"query\".\"double\"", query[double]) + AssertSQL("\"query\".\"doubleOptional\"", query[doubleOptional]) + + AssertSQL("\"query\".\"int\"", query[int]) + AssertSQL("\"query\".\"intOptional\"", query[intOptional]) + + AssertSQL("\"query\".\"int64\"", query[int64]) + AssertSQL("\"query\".\"int64Optional\"", query[int64Optional]) + + AssertSQL("\"query\".\"string\"", query[string]) + AssertSQL("\"query\".\"stringOptional\"", query[stringOptional]) + + AssertSQL("\"query\".*", query[*]) + } + + func test_tableNamespacedByDatabase() { + let table = Table("table", database: "attached") + + AssertSQL("SELECT * FROM \"attached\".\"table\"", table) + } + +} + +class QueryIntegrationTests : SQLiteTestCase { + + let id = Expression("id") + let email = Expression("email") + + override func setUp() { + super.setUp() + + CreateUsersTable() + } + + // MARK: - + + func test_select() { + for _ in try! db.prepare(users) { + // FIXME + } + + let managerId = Expression("manager_id") + let managers = users.alias("managers") + + let alice = try! db.run(users.insert(email <- "alice@example.com")) + _ = try! db.run(users.insert(email <- "betsy@example.com", managerId <- alice)) + + for user in try! db.prepare(users.join(managers, on: managers[id] == users[managerId])) { + _ = user[users[managerId]] + } + } + + func test_scalar() { + XCTAssertEqual(0, try! db.scalar(users.count)) + XCTAssertEqual(false, try! db.scalar(users.exists)) + + try! InsertUsers("alice") + XCTAssertEqual(1, try! db.scalar(users.select(id.average))) + } + + func test_pluck() { + let rowid = try! db.run(users.insert(email <- "alice@example.com")) + XCTAssertEqual(rowid, try! db.pluck(users)![id]) + } + + func test_insert() { + let id = try! db.run(users.insert(email <- "alice@example.com")) + XCTAssertEqual(1, id) + } + + func test_update() { + let changes = try! db.run(users.update(email <- "alice@example.com")) + XCTAssertEqual(0, changes) + } + + func test_delete() { + let changes = try! db.run(users.delete()) + XCTAssertEqual(0, changes) + } + +} diff --git a/mobile/ios/ThirdParty/SQLite.swift/Tests/SQLiteTests/RTreeTests.swift b/mobile/ios/ThirdParty/SQLite.swift/Tests/SQLiteTests/RTreeTests.swift new file mode 100644 index 0000000000..7147533ecb --- /dev/null +++ b/mobile/ios/ThirdParty/SQLite.swift/Tests/SQLiteTests/RTreeTests.swift @@ -0,0 +1,17 @@ +import XCTest +import SQLite + +class RTreeTests : XCTestCase { + + func test_create_onVirtualTable_withRTree_createVirtualTableExpression() { + XCTAssertEqual( + "CREATE VIRTUAL TABLE \"virtual_table\" USING rtree(\"int64\", \"double\", \"double\")", + virtualTable.create(.RTree(int64, (double, double))) + ) + XCTAssertEqual( + "CREATE VIRTUAL TABLE \"virtual_table\" USING rtree(\"int64\", \"double\", \"double\", \"double\", \"double\")", + virtualTable.create(.RTree(int64, (double, double), (double, double))) + ) + } + +} \ No newline at end of file diff --git a/mobile/ios/ThirdParty/SQLite.swift/Tests/SQLiteTests/SchemaTests.swift b/mobile/ios/ThirdParty/SQLite.swift/Tests/SQLiteTests/SchemaTests.swift new file mode 100644 index 0000000000..371459c757 --- /dev/null +++ b/mobile/ios/ThirdParty/SQLite.swift/Tests/SQLiteTests/SchemaTests.swift @@ -0,0 +1,775 @@ +import XCTest +import SQLite + +class SchemaTests : XCTestCase { + + func test_drop_compilesDropTableExpression() { + XCTAssertEqual("DROP TABLE \"table\"", table.drop()) + XCTAssertEqual("DROP TABLE IF EXISTS \"table\"", table.drop(ifExists: true)) + } + + func test_drop_compilesDropVirtualTableExpression() { + XCTAssertEqual("DROP TABLE \"virtual_table\"", virtualTable.drop()) + XCTAssertEqual("DROP TABLE IF EXISTS \"virtual_table\"", virtualTable.drop(ifExists: true)) + } + + func test_drop_compilesDropViewExpression() { + XCTAssertEqual("DROP VIEW \"view\"", _view.drop()) + XCTAssertEqual("DROP VIEW IF EXISTS \"view\"", _view.drop(ifExists: true)) + } + + func test_create_withBuilder_compilesCreateTableExpression() { + XCTAssertEqual( + "CREATE TABLE \"table\" (" + + "\"blob\" BLOB NOT NULL, " + + "\"blobOptional\" BLOB, " + + "\"double\" REAL NOT NULL, " + + "\"doubleOptional\" REAL, " + + "\"int64\" INTEGER NOT NULL, " + + "\"int64Optional\" INTEGER, " + + "\"string\" TEXT NOT NULL, " + + "\"stringOptional\" TEXT" + + ")", + table.create { t in + t.column(data) + t.column(dataOptional) + t.column(double) + t.column(doubleOptional) + t.column(int64) + t.column(int64Optional) + t.column(string) + t.column(stringOptional) + } + ) + XCTAssertEqual( + "CREATE TEMPORARY TABLE \"table\" (\"int64\" INTEGER NOT NULL)", + table.create(temporary: true) { $0.column(int64) } + ) + XCTAssertEqual( + "CREATE TABLE IF NOT EXISTS \"table\" (\"int64\" INTEGER NOT NULL)", + table.create(ifNotExists: true) { $0.column(int64) } + ) + XCTAssertEqual( + "CREATE TEMPORARY TABLE IF NOT EXISTS \"table\" (\"int64\" INTEGER NOT NULL)", + table.create(temporary: true, ifNotExists: true) { $0.column(int64) } + ) + } + + func test_create_withQuery_compilesCreateTableExpression() { + XCTAssertEqual( + "CREATE TABLE \"table\" AS SELECT \"int64\" FROM \"view\"", + table.create(_view.select(int64)) + ) + } + + // thoroughness test for ambiguity + func test_column_compilesColumnDefinitionExpression() { + XCTAssertEqual( + "CREATE TABLE \"table\" (\"int64\" INTEGER NOT NULL)", + table.create { t in t.column(int64) } + ) + XCTAssertEqual( + "CREATE TABLE \"table\" (\"int64\" INTEGER NOT NULL UNIQUE)", + table.create { t in t.column(int64, unique: true) } + ) + XCTAssertEqual( + "CREATE TABLE \"table\" (\"int64\" INTEGER NOT NULL CHECK (\"int64\" > 0))", + table.create { t in t.column(int64, check: int64 > 0) } + ) + XCTAssertEqual( + "CREATE TABLE \"table\" (\"int64\" INTEGER NOT NULL CHECK (\"int64Optional\" > 0))", + table.create { t in t.column(int64, check: int64Optional > 0) } + ) + XCTAssertEqual( + "CREATE TABLE \"table\" (\"int64\" INTEGER NOT NULL DEFAULT (\"int64\"))", + table.create { t in t.column(int64, defaultValue: int64) } + ) + XCTAssertEqual( + "CREATE TABLE \"table\" (\"int64\" INTEGER NOT NULL DEFAULT (0))", + table.create { t in t.column(int64, defaultValue: 0) } + ) + XCTAssertEqual( + "CREATE TABLE \"table\" (\"int64\" INTEGER NOT NULL UNIQUE CHECK (\"int64\" > 0))", + table.create { t in t.column(int64, unique: true, check: int64 > 0) } + ) + XCTAssertEqual( + "CREATE TABLE \"table\" (\"int64\" INTEGER NOT NULL UNIQUE CHECK (\"int64Optional\" > 0))", + table.create { t in t.column(int64, unique: true, check: int64Optional > 0) } + ) + XCTAssertEqual( + "CREATE TABLE \"table\" (\"int64\" INTEGER NOT NULL UNIQUE DEFAULT (\"int64\"))", + table.create { t in t.column(int64, unique: true, defaultValue: int64) } + ) + XCTAssertEqual( + "CREATE TABLE \"table\" (\"int64\" INTEGER NOT NULL UNIQUE DEFAULT (0))", + table.create { t in t.column(int64, unique: true, defaultValue: 0) } + ) + XCTAssertEqual( + "CREATE TABLE \"table\" (\"int64\" INTEGER NOT NULL UNIQUE CHECK (\"int64\" > 0))", + table.create { t in t.column(int64, unique: true, check: int64 > 0) } + ) + XCTAssertEqual( + "CREATE TABLE \"table\" (\"int64\" INTEGER NOT NULL UNIQUE CHECK (\"int64Optional\" > 0))", + table.create { t in t.column(int64, unique: true, check: int64Optional > 0) } + ) + XCTAssertEqual( + "CREATE TABLE \"table\" (\"int64\" INTEGER NOT NULL UNIQUE CHECK (\"int64\" > 0) DEFAULT (\"int64\"))", + table.create { t in t.column(int64, unique: true, check: int64 > 0, defaultValue: int64) } + ) + XCTAssertEqual( + "CREATE TABLE \"table\" (\"int64\" INTEGER NOT NULL UNIQUE CHECK (\"int64Optional\" > 0) DEFAULT (\"int64\"))", + table.create { t in t.column(int64, unique: true, check: int64Optional > 0, defaultValue: int64) } + ) + XCTAssertEqual( + "CREATE TABLE \"table\" (\"int64\" INTEGER NOT NULL UNIQUE CHECK (\"int64\" > 0) DEFAULT (0))", + table.create { t in t.column(int64, unique: true, check: int64 > 0, defaultValue: 0) } + ) + XCTAssertEqual( + "CREATE TABLE \"table\" (\"int64\" INTEGER NOT NULL UNIQUE CHECK (\"int64Optional\" > 0) DEFAULT (0))", + table.create { t in t.column(int64, unique: true, check: int64Optional > 0, defaultValue: 0) } + ) + XCTAssertEqual( + "CREATE TABLE \"table\" (\"int64\" INTEGER NOT NULL CHECK (\"int64\" > 0) DEFAULT (\"int64\"))", + table.create { t in t.column(int64, check: int64 > 0, defaultValue: int64) } + ) + XCTAssertEqual( + "CREATE TABLE \"table\" (\"int64\" INTEGER NOT NULL CHECK (\"int64Optional\" > 0) DEFAULT (\"int64\"))", + table.create { t in t.column(int64, check: int64Optional > 0, defaultValue: int64) } + ) + XCTAssertEqual( + "CREATE TABLE \"table\" (\"int64\" INTEGER NOT NULL CHECK (\"int64\" > 0) DEFAULT (0))", + table.create { t in t.column(int64, check: int64 > 0, defaultValue: 0) } + ) + XCTAssertEqual( + "CREATE TABLE \"table\" (\"int64\" INTEGER NOT NULL CHECK (\"int64Optional\" > 0) DEFAULT (0))", + table.create { t in t.column(int64, check: int64Optional > 0, defaultValue: 0) } + ) + + XCTAssertEqual( + "CREATE TABLE \"table\" (\"int64\" INTEGER PRIMARY KEY NOT NULL CHECK (\"int64\" > 0))", + table.create { t in t.column(int64, primaryKey: true, check: int64 > 0) } + ) + XCTAssertEqual( + "CREATE TABLE \"table\" (\"int64\" INTEGER PRIMARY KEY NOT NULL CHECK (\"int64Optional\" > 0))", + table.create { t in t.column(int64, primaryKey: true, check: int64Optional > 0) } + ) + XCTAssertEqual( + "CREATE TABLE \"table\" (\"int64\" INTEGER PRIMARY KEY NOT NULL DEFAULT (\"int64\"))", + table.create { t in t.column(int64, primaryKey: true, defaultValue: int64) } + ) + XCTAssertEqual( + "CREATE TABLE \"table\" (\"int64\" INTEGER PRIMARY KEY NOT NULL CHECK (\"int64\" > 0))", + table.create { t in t.column(int64, primaryKey: true, check: int64 > 0) } + ) + XCTAssertEqual( + "CREATE TABLE \"table\" (\"int64\" INTEGER PRIMARY KEY NOT NULL CHECK (\"int64Optional\" > 0))", + table.create { t in t.column(int64, primaryKey: true, check: int64Optional > 0) } + ) + XCTAssertEqual( + "CREATE TABLE \"table\" (\"int64\" INTEGER PRIMARY KEY NOT NULL CHECK (\"int64\" > 0) DEFAULT (\"int64\"))", + table.create { t in t.column(int64, primaryKey: true, check: int64 > 0, defaultValue: int64) } + ) + XCTAssertEqual( + "CREATE TABLE \"table\" (\"int64\" INTEGER PRIMARY KEY NOT NULL CHECK (\"int64Optional\" > 0) DEFAULT (\"int64\"))", + table.create { t in t.column(int64, primaryKey: true, check: int64Optional > 0, defaultValue: int64) } + ) + + XCTAssertEqual( + "CREATE TABLE \"table\" (\"int64Optional\" INTEGER)", + table.create { t in t.column(int64Optional) } + ) + XCTAssertEqual( + "CREATE TABLE \"table\" (\"int64Optional\" INTEGER UNIQUE)", + table.create { t in t.column(int64Optional, unique: true) } + ) + XCTAssertEqual( + "CREATE TABLE \"table\" (\"int64Optional\" INTEGER CHECK (\"int64\" > 0))", + table.create { t in t.column(int64Optional, check: int64 > 0) } + ) + XCTAssertEqual( + "CREATE TABLE \"table\" (\"int64Optional\" INTEGER CHECK (\"int64Optional\" > 0))", + table.create { t in t.column(int64Optional, check: int64Optional > 0) } + ) + XCTAssertEqual( + "CREATE TABLE \"table\" (\"int64Optional\" INTEGER DEFAULT (\"int64\"))", + table.create { t in t.column(int64Optional, defaultValue: int64) } + ) + XCTAssertEqual( + "CREATE TABLE \"table\" (\"int64Optional\" INTEGER DEFAULT (\"int64Optional\"))", + table.create { t in t.column(int64Optional, defaultValue: int64Optional) } + ) + XCTAssertEqual( + "CREATE TABLE \"table\" (\"int64Optional\" INTEGER DEFAULT (0))", + table.create { t in t.column(int64Optional, defaultValue: 0) } + ) + XCTAssertEqual( + "CREATE TABLE \"table\" (\"int64Optional\" INTEGER UNIQUE CHECK (\"int64\" > 0))", + table.create { t in t.column(int64Optional, unique: true, check: int64 > 0) } + ) + XCTAssertEqual( + "CREATE TABLE \"table\" (\"int64Optional\" INTEGER UNIQUE CHECK (\"int64Optional\" > 0))", + table.create { t in t.column(int64Optional, unique: true, check: int64Optional > 0) } + ) + XCTAssertEqual( + "CREATE TABLE \"table\" (\"int64Optional\" INTEGER UNIQUE DEFAULT (\"int64\"))", + table.create { t in t.column(int64Optional, unique: true, defaultValue: int64) } + ) + XCTAssertEqual( + "CREATE TABLE \"table\" (\"int64Optional\" INTEGER UNIQUE DEFAULT (\"int64Optional\"))", + table.create { t in t.column(int64Optional, unique: true, defaultValue: int64Optional) } + ) + XCTAssertEqual( + "CREATE TABLE \"table\" (\"int64Optional\" INTEGER UNIQUE DEFAULT (0))", + table.create { t in t.column(int64Optional, unique: true, defaultValue: 0) } + ) + XCTAssertEqual( + "CREATE TABLE \"table\" (\"int64Optional\" INTEGER UNIQUE CHECK (\"int64\" > 0))", + table.create { t in t.column(int64Optional, unique: true, check: int64 > 0) } + ) + XCTAssertEqual( + "CREATE TABLE \"table\" (\"int64Optional\" INTEGER UNIQUE CHECK (\"int64Optional\" > 0))", + table.create { t in t.column(int64Optional, unique: true, check: int64Optional > 0) } + ) + XCTAssertEqual( + "CREATE TABLE \"table\" (\"int64Optional\" INTEGER UNIQUE CHECK (\"int64\" > 0) DEFAULT (\"int64\"))", + table.create { t in t.column(int64Optional, unique: true, check: int64 > 0, defaultValue: int64) } + ) + XCTAssertEqual( + "CREATE TABLE \"table\" (\"int64Optional\" INTEGER UNIQUE CHECK (\"int64\" > 0) DEFAULT (\"int64Optional\"))", + table.create { t in t.column(int64Optional, unique: true, check: int64 > 0, defaultValue: int64Optional) } + ) + XCTAssertEqual( + "CREATE TABLE \"table\" (\"int64Optional\" INTEGER UNIQUE CHECK (\"int64Optional\" > 0) DEFAULT (\"int64\"))", + table.create { t in t.column(int64Optional, unique: true, check: int64Optional > 0, defaultValue: int64) } + ) + XCTAssertEqual( + "CREATE TABLE \"table\" (\"int64Optional\" INTEGER UNIQUE CHECK (\"int64Optional\" > 0) DEFAULT (\"int64Optional\"))", + table.create { t in t.column(int64Optional, unique: true, check: int64Optional > 0, defaultValue: int64Optional) } + ) + XCTAssertEqual( + "CREATE TABLE \"table\" (\"int64Optional\" INTEGER UNIQUE CHECK (\"int64\" > 0) DEFAULT (0))", + table.create { t in t.column(int64Optional, unique: true, check: int64 > 0, defaultValue: 0) } + ) + XCTAssertEqual( + "CREATE TABLE \"table\" (\"int64Optional\" INTEGER UNIQUE CHECK (\"int64Optional\" > 0) DEFAULT (0))", + table.create { t in t.column(int64Optional, unique: true, check: int64Optional > 0, defaultValue: 0) } + ) + XCTAssertEqual( + "CREATE TABLE \"table\" (\"int64Optional\" INTEGER CHECK (\"int64\" > 0) DEFAULT (\"int64\"))", + table.create { t in t.column(int64Optional, check: int64 > 0, defaultValue: int64) } + ) + XCTAssertEqual( + "CREATE TABLE \"table\" (\"int64Optional\" INTEGER CHECK (\"int64Optional\" > 0) DEFAULT (\"int64\"))", + table.create { t in t.column(int64Optional, check: int64Optional > 0, defaultValue: int64) } + ) + XCTAssertEqual( + "CREATE TABLE \"table\" (\"int64Optional\" INTEGER CHECK (\"int64\" > 0) DEFAULT (\"int64Optional\"))", + table.create { t in t.column(int64Optional, check: int64 > 0, defaultValue: int64Optional) } + ) + XCTAssertEqual( + "CREATE TABLE \"table\" (\"int64Optional\" INTEGER CHECK (\"int64Optional\" > 0) DEFAULT (\"int64Optional\"))", + table.create { t in t.column(int64Optional, check: int64Optional > 0, defaultValue: int64Optional) } + ) + XCTAssertEqual( + "CREATE TABLE \"table\" (\"int64Optional\" INTEGER CHECK (\"int64\" > 0) DEFAULT (0))", + table.create { t in t.column(int64Optional, check: int64 > 0, defaultValue: 0) } + ) + XCTAssertEqual( + "CREATE TABLE \"table\" (\"int64Optional\" INTEGER CHECK (\"int64Optional\" > 0) DEFAULT (0))", + table.create { t in t.column(int64Optional, check: int64Optional > 0, defaultValue: 0) } + ) + } + + func test_column_withIntegerExpression_compilesPrimaryKeyAutoincrementColumnDefinitionExpression() { + XCTAssertEqual( + "CREATE TABLE \"table\" (\"int64\" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + table.create { t in t.column(int64, primaryKey: .autoincrement) } + ) + XCTAssertEqual( + "CREATE TABLE \"table\" (\"int64\" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL CHECK (\"int64\" > 0))", + table.create { t in t.column(int64, primaryKey: .autoincrement, check: int64 > 0) } + ) + XCTAssertEqual( + "CREATE TABLE \"table\" (\"int64\" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL CHECK (\"int64Optional\" > 0))", + table.create { t in t.column(int64, primaryKey: .autoincrement, check: int64Optional > 0) } + ) + } + + func test_column_withIntegerExpression_compilesReferentialColumnDefinitionExpression() { + XCTAssertEqual( + "CREATE TABLE \"table\" (\"int64\" INTEGER NOT NULL REFERENCES \"table\" (\"int64\"))", + table.create { t in t.column(int64, references: table, int64) } + ) + XCTAssertEqual( + "CREATE TABLE \"table\" (\"int64\" INTEGER NOT NULL REFERENCES \"table\" (\"int64\"))", + table.create { t in t.column(int64, references: qualifiedTable, int64) } + ) + XCTAssertEqual( + "CREATE TABLE \"table\" (\"int64\" INTEGER NOT NULL UNIQUE REFERENCES \"table\" (\"int64\"))", + table.create { t in t.column(int64, unique: true, references: table, int64) } + ) + XCTAssertEqual( + "CREATE TABLE \"table\" (\"int64\" INTEGER NOT NULL CHECK (\"int64\" > 0) REFERENCES \"table\" (\"int64\"))", + table.create { t in t.column(int64, check: int64 > 0, references: table, int64) } + ) + XCTAssertEqual( + "CREATE TABLE \"table\" (\"int64\" INTEGER NOT NULL CHECK (\"int64Optional\" > 0) REFERENCES \"table\" (\"int64\"))", + table.create { t in t.column(int64, check: int64Optional > 0, references: table, int64) } + ) + XCTAssertEqual( + "CREATE TABLE \"table\" (\"int64\" INTEGER NOT NULL UNIQUE CHECK (\"int64\" > 0) REFERENCES \"table\" (\"int64\"))", + table.create { t in t.column(int64, unique: true, check: int64 > 0, references: table, int64) } + ) + XCTAssertEqual( + "CREATE TABLE \"table\" (\"int64\" INTEGER NOT NULL UNIQUE CHECK (\"int64Optional\" > 0) REFERENCES \"table\" (\"int64\"))", + table.create { t in t.column(int64, unique: true, check: int64Optional > 0, references: table, int64) } + ) + + XCTAssertEqual( + "CREATE TABLE \"table\" (\"int64Optional\" INTEGER REFERENCES \"table\" (\"int64\"))", + table.create { t in t.column(int64Optional, references: table, int64) } + ) + XCTAssertEqual( + "CREATE TABLE \"table\" (\"int64Optional\" INTEGER UNIQUE REFERENCES \"table\" (\"int64\"))", + table.create { t in t.column(int64Optional, unique: true, references: table, int64) } + ) + XCTAssertEqual( + "CREATE TABLE \"table\" (\"int64Optional\" INTEGER CHECK (\"int64\" > 0) REFERENCES \"table\" (\"int64\"))", + table.create { t in t.column(int64Optional, check: int64 > 0, references: table, int64) } + ) + XCTAssertEqual( + "CREATE TABLE \"table\" (\"int64Optional\" INTEGER CHECK (\"int64Optional\" > 0) REFERENCES \"table\" (\"int64\"))", + table.create { t in t.column(int64Optional, check: int64Optional > 0, references: table, int64) } + ) + XCTAssertEqual( + "CREATE TABLE \"table\" (\"int64Optional\" INTEGER UNIQUE CHECK (\"int64\" > 0) REFERENCES \"table\" (\"int64\"))", + table.create { t in t.column(int64Optional, unique: true, check: int64 > 0, references: table, int64) } + ) + XCTAssertEqual( + "CREATE TABLE \"table\" (\"int64Optional\" INTEGER UNIQUE CHECK (\"int64Optional\" > 0) REFERENCES \"table\" (\"int64\"))", + table.create { t in t.column(int64Optional, unique: true, check: int64Optional > 0, references: table, int64) } + ) + } + + func test_column_withStringExpression_compilesCollatedColumnDefinitionExpression() { + XCTAssertEqual( + "CREATE TABLE \"table\" (\"string\" TEXT NOT NULL COLLATE RTRIM)", + table.create { t in t.column(string, collate: .rtrim) } + ) + XCTAssertEqual( + "CREATE TABLE \"table\" (\"string\" TEXT NOT NULL UNIQUE COLLATE RTRIM)", + table.create { t in t.column(string, unique: true, collate: .rtrim) } + ) + XCTAssertEqual( + "CREATE TABLE \"table\" (\"string\" TEXT NOT NULL CHECK (\"string\" != '') COLLATE RTRIM)", + table.create { t in t.column(string, check: string != "", collate: .rtrim) } + ) + XCTAssertEqual( + "CREATE TABLE \"table\" (\"string\" TEXT NOT NULL CHECK (\"stringOptional\" != '') COLLATE RTRIM)", + table.create { t in t.column(string, check: stringOptional != "", collate: .rtrim) } + ) + XCTAssertEqual( + "CREATE TABLE \"table\" (\"string\" TEXT NOT NULL DEFAULT (\"string\") COLLATE RTRIM)", + table.create { t in t.column(string, defaultValue: string, collate: .rtrim) } + ) + XCTAssertEqual( + "CREATE TABLE \"table\" (\"string\" TEXT NOT NULL DEFAULT ('string') COLLATE RTRIM)", + table.create { t in t.column(string, defaultValue: "string", collate: .rtrim) } + ) + XCTAssertEqual( + "CREATE TABLE \"table\" (\"string\" TEXT NOT NULL UNIQUE CHECK (\"string\" != '') COLLATE RTRIM)", + table.create { t in t.column(string, unique: true, check: string != "", collate: .rtrim) } + ) + XCTAssertEqual( + "CREATE TABLE \"table\" (\"string\" TEXT NOT NULL UNIQUE CHECK (\"stringOptional\" != '') COLLATE RTRIM)", + table.create { t in t.column(string, unique: true, check: stringOptional != "", collate: .rtrim) } + ) + XCTAssertEqual( + "CREATE TABLE \"table\" (\"string\" TEXT NOT NULL UNIQUE DEFAULT (\"string\") COLLATE RTRIM)", + table.create { t in t.column(string, unique: true, defaultValue: string, collate: .rtrim) } + ) + XCTAssertEqual( + "CREATE TABLE \"table\" (\"string\" TEXT NOT NULL UNIQUE DEFAULT ('string') COLLATE RTRIM)", + table.create { t in t.column(string, unique: true, defaultValue: "string", collate: .rtrim) } + ) + XCTAssertEqual( + "CREATE TABLE \"table\" (\"string\" TEXT NOT NULL UNIQUE CHECK (\"string\" != '') DEFAULT (\"string\") COLLATE RTRIM)", + table.create { t in t.column(string, unique: true, check: string != "", defaultValue: string, collate: .rtrim) } + ) + XCTAssertEqual( + "CREATE TABLE \"table\" (\"string\" TEXT NOT NULL UNIQUE CHECK (\"stringOptional\" != '') DEFAULT (\"string\") COLLATE RTRIM)", + table.create { t in t.column(string, unique: true, check: stringOptional != "", defaultValue: string, collate: .rtrim) } + ) + XCTAssertEqual( + "CREATE TABLE \"table\" (\"string\" TEXT NOT NULL UNIQUE CHECK (\"string\" != '') DEFAULT ('string') COLLATE RTRIM)", + table.create { t in t.column(string, unique: true, check: string != "", defaultValue: "string", collate: .rtrim) } + ) + XCTAssertEqual( + "CREATE TABLE \"table\" (\"string\" TEXT NOT NULL UNIQUE CHECK (\"stringOptional\" != '') DEFAULT ('string') COLLATE RTRIM)", + table.create { t in t.column(string, unique: true, check: stringOptional != "", defaultValue: "string", collate: .rtrim) } + ) + XCTAssertEqual( + "CREATE TABLE \"table\" (\"string\" TEXT NOT NULL CHECK (\"string\" != '') DEFAULT (\"string\") COLLATE RTRIM)", + table.create { t in t.column(string, check: string != "", defaultValue: string, collate: .rtrim) } + ) + XCTAssertEqual( + "CREATE TABLE \"table\" (\"string\" TEXT NOT NULL CHECK (\"stringOptional\" != '') DEFAULT (\"string\") COLLATE RTRIM)", + table.create { t in t.column(string, check: stringOptional != "", defaultValue: string, collate: .rtrim) } + ) + XCTAssertEqual( + "CREATE TABLE \"table\" (\"string\" TEXT NOT NULL CHECK (\"string\" != '') DEFAULT ('string') COLLATE RTRIM)", + table.create { t in t.column(string, check: string != "", defaultValue: "string", collate: .rtrim) } + ) + XCTAssertEqual( + "CREATE TABLE \"table\" (\"string\" TEXT NOT NULL CHECK (\"stringOptional\" != '') DEFAULT ('string') COLLATE RTRIM)", + table.create { t in t.column(string, check: stringOptional != "", defaultValue: "string", collate: .rtrim) } + ) + + XCTAssertEqual( + "CREATE TABLE \"table\" (\"stringOptional\" TEXT NOT NULL COLLATE RTRIM)", + table.create { t in t.column(stringOptional, collate: .rtrim) } + ) + XCTAssertEqual( + "CREATE TABLE \"table\" (\"stringOptional\" TEXT NOT NULL UNIQUE COLLATE RTRIM)", + table.create { t in t.column(stringOptional, unique: true, collate: .rtrim) } + ) + XCTAssertEqual( + "CREATE TABLE \"table\" (\"stringOptional\" TEXT NOT NULL CHECK (\"string\" != '') COLLATE RTRIM)", + table.create { t in t.column(stringOptional, check: string != "", collate: .rtrim) } + ) + XCTAssertEqual( + "CREATE TABLE \"table\" (\"stringOptional\" TEXT NOT NULL CHECK (\"stringOptional\" != '') COLLATE RTRIM)", + table.create { t in t.column(stringOptional, check: stringOptional != "", collate: .rtrim) } + ) + XCTAssertEqual( + "CREATE TABLE \"table\" (\"stringOptional\" TEXT NOT NULL DEFAULT (\"string\") COLLATE RTRIM)", + table.create { t in t.column(stringOptional, defaultValue: string, collate: .rtrim) } + ) + XCTAssertEqual( + "CREATE TABLE \"table\" (\"stringOptional\" TEXT NOT NULL DEFAULT (\"stringOptional\") COLLATE RTRIM)", + table.create { t in t.column(stringOptional, defaultValue: stringOptional, collate: .rtrim) } + ) + XCTAssertEqual( + "CREATE TABLE \"table\" (\"stringOptional\" TEXT NOT NULL DEFAULT ('string') COLLATE RTRIM)", + table.create { t in t.column(stringOptional, defaultValue: "string", collate: .rtrim) } + ) + XCTAssertEqual( + "CREATE TABLE \"table\" (\"stringOptional\" TEXT NOT NULL UNIQUE CHECK (\"string\" != '') COLLATE RTRIM)", + table.create { t in t.column(stringOptional, unique: true, check: string != "", collate: .rtrim) } + ) + XCTAssertEqual( + "CREATE TABLE \"table\" (\"stringOptional\" TEXT NOT NULL UNIQUE CHECK (\"stringOptional\" != '') COLLATE RTRIM)", + table.create { t in t.column(stringOptional, unique: true, check: stringOptional != "", collate: .rtrim) } + ) + XCTAssertEqual( + "CREATE TABLE \"table\" (\"stringOptional\" TEXT NOT NULL UNIQUE DEFAULT (\"string\") COLLATE RTRIM)", + table.create { t in t.column(stringOptional, unique: true, defaultValue: string, collate: .rtrim) } + ) + XCTAssertEqual( + "CREATE TABLE \"table\" (\"stringOptional\" TEXT NOT NULL UNIQUE DEFAULT (\"stringOptional\") COLLATE RTRIM)", + table.create { t in t.column(stringOptional, unique: true, defaultValue: stringOptional, collate: .rtrim) } + ) + XCTAssertEqual( + "CREATE TABLE \"table\" (\"stringOptional\" TEXT NOT NULL UNIQUE DEFAULT ('string') COLLATE RTRIM)", + table.create { t in t.column(stringOptional, unique: true, defaultValue: "string", collate: .rtrim) } + ) + XCTAssertEqual( + "CREATE TABLE \"table\" (\"stringOptional\" TEXT NOT NULL UNIQUE CHECK (\"string\" != '') DEFAULT (\"string\") COLLATE RTRIM)", + table.create { t in t.column(stringOptional, unique: true, check: string != "", defaultValue: string, collate: .rtrim) } + ) + XCTAssertEqual( + "CREATE TABLE \"table\" (\"stringOptional\" TEXT NOT NULL UNIQUE CHECK (\"string\" != '') DEFAULT (\"stringOptional\") COLLATE RTRIM)", + table.create { t in t.column(stringOptional, unique: true, check: string != "", defaultValue: stringOptional, collate: .rtrim) } + ) + XCTAssertEqual( + "CREATE TABLE \"table\" (\"stringOptional\" TEXT NOT NULL UNIQUE CHECK (\"stringOptional\" != '') DEFAULT (\"string\") COLLATE RTRIM)", + table.create { t in t.column(stringOptional, unique: true, check: stringOptional != "", defaultValue: string, collate: .rtrim) } + ) + XCTAssertEqual( + "CREATE TABLE \"table\" (\"stringOptional\" TEXT NOT NULL UNIQUE CHECK (\"stringOptional\" != '') DEFAULT (\"stringOptional\") COLLATE RTRIM)", + table.create { t in t.column(stringOptional, unique: true, check: stringOptional != "", defaultValue: stringOptional, collate: .rtrim) } + ) + XCTAssertEqual( + "CREATE TABLE \"table\" (\"stringOptional\" TEXT NOT NULL UNIQUE CHECK (\"string\" != '') DEFAULT ('string') COLLATE RTRIM)", + table.create { t in t.column(stringOptional, unique: true, check: string != "", defaultValue: "string", collate: .rtrim) } + ) + XCTAssertEqual( + "CREATE TABLE \"table\" (\"stringOptional\" TEXT NOT NULL UNIQUE CHECK (\"stringOptional\" != '') DEFAULT ('string') COLLATE RTRIM)", + table.create { t in t.column(stringOptional, unique: true, check: stringOptional != "", defaultValue: "string", collate: .rtrim) } + ) + XCTAssertEqual( + "CREATE TABLE \"table\" (\"stringOptional\" TEXT NOT NULL CHECK (\"string\" != '') DEFAULT (\"string\") COLLATE RTRIM)", + table.create { t in t.column(stringOptional, check: string != "", defaultValue: string, collate: .rtrim) } + ) + XCTAssertEqual( + "CREATE TABLE \"table\" (\"stringOptional\" TEXT NOT NULL CHECK (\"stringOptional\" != '') DEFAULT (\"string\") COLLATE RTRIM)", + table.create { t in t.column(stringOptional, check: stringOptional != "", defaultValue: string, collate: .rtrim) } + ) + XCTAssertEqual( + "CREATE TABLE \"table\" (\"stringOptional\" TEXT NOT NULL CHECK (\"string\" != '') DEFAULT (\"stringOptional\") COLLATE RTRIM)", + table.create { t in t.column(stringOptional, check: string != "", defaultValue: stringOptional, collate: .rtrim) } + ) + XCTAssertEqual( + "CREATE TABLE \"table\" (\"stringOptional\" TEXT NOT NULL CHECK (\"stringOptional\" != '') DEFAULT (\"stringOptional\") COLLATE RTRIM)", + table.create { t in t.column(stringOptional, check: stringOptional != "", defaultValue: stringOptional, collate: .rtrim) } + ) + XCTAssertEqual( + "CREATE TABLE \"table\" (\"stringOptional\" TEXT NOT NULL CHECK (\"string\" != '') DEFAULT ('string') COLLATE RTRIM)", + table.create { t in t.column(stringOptional, check: string != "", defaultValue: "string", collate: .rtrim) } + ) + XCTAssertEqual( + "CREATE TABLE \"table\" (\"stringOptional\" TEXT NOT NULL CHECK (\"stringOptional\" != '') DEFAULT ('string') COLLATE RTRIM)", + table.create { t in t.column(stringOptional, check: stringOptional != "", defaultValue: "string", collate: .rtrim) } + ) + } + + func test_primaryKey_compilesPrimaryKeyExpression() { + XCTAssertEqual( + "CREATE TABLE \"table\" (PRIMARY KEY (\"int64\"))", + table.create { t in t.primaryKey(int64) } + ) + XCTAssertEqual( + "CREATE TABLE \"table\" (PRIMARY KEY (\"int64\", \"string\"))", + table.create { t in t.primaryKey(int64, string) } + ) + XCTAssertEqual( + "CREATE TABLE \"table\" (PRIMARY KEY (\"int64\", \"string\", \"double\"))", + table.create { t in t.primaryKey(int64, string, double) } + ) + } + + func test_unique_compilesUniqueExpression() { + XCTAssertEqual( + "CREATE TABLE \"table\" (UNIQUE (\"int64\"))", + table.create { t in t.unique(int64) } + ) + } + + func test_check_compilesCheckExpression() { + XCTAssertEqual( + "CREATE TABLE \"table\" (CHECK ((\"int64\" > 0)))", + table.create { t in t.check(int64 > 0) } + ) + XCTAssertEqual( + "CREATE TABLE \"table\" (CHECK ((\"int64Optional\" > 0)))", + table.create { t in t.check(int64Optional > 0) } + ) + } + + func test_foreignKey_compilesForeignKeyExpression() { + XCTAssertEqual( + "CREATE TABLE \"table\" (FOREIGN KEY (\"string\") REFERENCES \"table\" (\"string\"))", + table.create { t in t.foreignKey(string, references: table, string) } + ) + XCTAssertEqual( + "CREATE TABLE \"table\" (FOREIGN KEY (\"stringOptional\") REFERENCES \"table\" (\"string\"))", + table.create { t in t.foreignKey(stringOptional, references: table, string) } + ) + + XCTAssertEqual( + "CREATE TABLE \"table\" (FOREIGN KEY (\"string\") REFERENCES \"table\" (\"string\") ON UPDATE CASCADE ON DELETE SET NULL)", + table.create { t in t.foreignKey(string, references: table, string, update: .cascade, delete: .setNull) } + ) + + XCTAssertEqual( + "CREATE TABLE \"table\" (FOREIGN KEY (\"string\", \"string\") REFERENCES \"table\" (\"string\", \"string\"))", + table.create { t in t.foreignKey((string, string), references: table, (string, string)) } + ) + XCTAssertEqual( + "CREATE TABLE \"table\" (FOREIGN KEY (\"string\", \"string\", \"string\") REFERENCES \"table\" (\"string\", \"string\", \"string\"))", + table.create { t in t.foreignKey((string, string, string), references: table, (string, string, string)) } + ) + } + + func test_addColumn_compilesAlterTableExpression() { + XCTAssertEqual( + "ALTER TABLE \"table\" ADD COLUMN \"int64\" INTEGER NOT NULL DEFAULT (1)", + table.addColumn(int64, defaultValue: 1) + ) + XCTAssertEqual( + "ALTER TABLE \"table\" ADD COLUMN \"int64\" INTEGER NOT NULL CHECK (\"int64\" > 0) DEFAULT (1)", + table.addColumn(int64, check: int64 > 0, defaultValue: 1) + ) + XCTAssertEqual( + "ALTER TABLE \"table\" ADD COLUMN \"int64\" INTEGER NOT NULL CHECK (\"int64Optional\" > 0) DEFAULT (1)", + table.addColumn(int64, check: int64Optional > 0, defaultValue: 1) + ) + + XCTAssertEqual( + "ALTER TABLE \"table\" ADD COLUMN \"int64Optional\" INTEGER", + table.addColumn(int64Optional) + ) + XCTAssertEqual( + "ALTER TABLE \"table\" ADD COLUMN \"int64Optional\" INTEGER CHECK (\"int64\" > 0)", + table.addColumn(int64Optional, check: int64 > 0) + ) + XCTAssertEqual( + "ALTER TABLE \"table\" ADD COLUMN \"int64Optional\" INTEGER CHECK (\"int64Optional\" > 0)", + table.addColumn(int64Optional, check: int64Optional > 0) + ) + XCTAssertEqual( + "ALTER TABLE \"table\" ADD COLUMN \"int64Optional\" INTEGER DEFAULT (1)", + table.addColumn(int64Optional, defaultValue: 1) + ) + XCTAssertEqual( + "ALTER TABLE \"table\" ADD COLUMN \"int64Optional\" INTEGER CHECK (\"int64\" > 0) DEFAULT (1)", + table.addColumn(int64Optional, check: int64 > 0, defaultValue: 1) + ) + XCTAssertEqual( + "ALTER TABLE \"table\" ADD COLUMN \"int64Optional\" INTEGER CHECK (\"int64Optional\" > 0) DEFAULT (1)", + table.addColumn(int64Optional, check: int64Optional > 0, defaultValue: 1) + ) + } + + func test_addColumn_withIntegerExpression_compilesReferentialAlterTableExpression() { + XCTAssertEqual( + "ALTER TABLE \"table\" ADD COLUMN \"int64\" INTEGER NOT NULL REFERENCES \"table\" (\"int64\")", + table.addColumn(int64, references: table, int64) + ) + XCTAssertEqual( + "ALTER TABLE \"table\" ADD COLUMN \"int64\" INTEGER NOT NULL UNIQUE REFERENCES \"table\" (\"int64\")", + table.addColumn(int64, unique: true, references: table, int64) + ) + XCTAssertEqual( + "ALTER TABLE \"table\" ADD COLUMN \"int64\" INTEGER NOT NULL CHECK (\"int64\" > 0) REFERENCES \"table\" (\"int64\")", + table.addColumn(int64, check: int64 > 0, references: table, int64) + ) + XCTAssertEqual( + "ALTER TABLE \"table\" ADD COLUMN \"int64\" INTEGER NOT NULL CHECK (\"int64Optional\" > 0) REFERENCES \"table\" (\"int64\")", + table.addColumn(int64, check: int64Optional > 0, references: table, int64) + ) + XCTAssertEqual( + "ALTER TABLE \"table\" ADD COLUMN \"int64\" INTEGER NOT NULL UNIQUE CHECK (\"int64\" > 0) REFERENCES \"table\" (\"int64\")", + table.addColumn(int64, unique: true, check: int64 > 0, references: table, int64) + ) + XCTAssertEqual( + "ALTER TABLE \"table\" ADD COLUMN \"int64\" INTEGER NOT NULL UNIQUE CHECK (\"int64Optional\" > 0) REFERENCES \"table\" (\"int64\")", + table.addColumn(int64, unique: true, check: int64Optional > 0, references: table, int64) + ) + + XCTAssertEqual( + "ALTER TABLE \"table\" ADD COLUMN \"int64Optional\" INTEGER REFERENCES \"table\" (\"int64\")", + table.addColumn(int64Optional, references: table, int64) + ) + XCTAssertEqual( + "ALTER TABLE \"table\" ADD COLUMN \"int64Optional\" INTEGER UNIQUE REFERENCES \"table\" (\"int64\")", + table.addColumn(int64Optional, unique: true, references: table, int64) + ) + XCTAssertEqual( + "ALTER TABLE \"table\" ADD COLUMN \"int64Optional\" INTEGER CHECK (\"int64\" > 0) REFERENCES \"table\" (\"int64\")", + table.addColumn(int64Optional, check: int64 > 0, references: table, int64) + ) + XCTAssertEqual( + "ALTER TABLE \"table\" ADD COLUMN \"int64Optional\" INTEGER CHECK (\"int64Optional\" > 0) REFERENCES \"table\" (\"int64\")", + table.addColumn(int64Optional, check: int64Optional > 0, references: table, int64) + ) + XCTAssertEqual( + "ALTER TABLE \"table\" ADD COLUMN \"int64Optional\" INTEGER UNIQUE CHECK (\"int64\" > 0) REFERENCES \"table\" (\"int64\")", + table.addColumn(int64Optional, unique: true, check: int64 > 0, references: table, int64) + ) + XCTAssertEqual( + "ALTER TABLE \"table\" ADD COLUMN \"int64Optional\" INTEGER UNIQUE CHECK (\"int64Optional\" > 0) REFERENCES \"table\" (\"int64\")", + table.addColumn(int64Optional, unique: true, check: int64Optional > 0, references: table, int64) + ) + } + + func test_addColumn_withStringExpression_compilesCollatedAlterTableExpression() { + XCTAssertEqual( + "ALTER TABLE \"table\" ADD COLUMN \"string\" TEXT NOT NULL DEFAULT ('string') COLLATE RTRIM", + table.addColumn(string, defaultValue: "string", collate: .rtrim) + ) + XCTAssertEqual( + "ALTER TABLE \"table\" ADD COLUMN \"string\" TEXT NOT NULL CHECK (\"string\" != '') DEFAULT ('string') COLLATE RTRIM", + table.addColumn(string, check: string != "", defaultValue: "string", collate: .rtrim) + ) + XCTAssertEqual( + "ALTER TABLE \"table\" ADD COLUMN \"string\" TEXT NOT NULL CHECK (\"stringOptional\" != '') DEFAULT ('string') COLLATE RTRIM", + table.addColumn(string, check: stringOptional != "", defaultValue: "string", collate: .rtrim) + ) + + XCTAssertEqual( + "ALTER TABLE \"table\" ADD COLUMN \"stringOptional\" TEXT COLLATE RTRIM", + table.addColumn(stringOptional, collate: .rtrim) + ) + XCTAssertEqual( + "ALTER TABLE \"table\" ADD COLUMN \"stringOptional\" TEXT CHECK (\"string\" != '') COLLATE RTRIM", + table.addColumn(stringOptional, check: string != "", collate: .rtrim) + ) + XCTAssertEqual( + "ALTER TABLE \"table\" ADD COLUMN \"stringOptional\" TEXT CHECK (\"stringOptional\" != '') COLLATE RTRIM", + table.addColumn(stringOptional, check: stringOptional != "", collate: .rtrim) + ) + XCTAssertEqual( + "ALTER TABLE \"table\" ADD COLUMN \"stringOptional\" TEXT CHECK (\"string\" != '') DEFAULT ('string') COLLATE RTRIM", + table.addColumn(stringOptional, check: string != "", defaultValue: "string", collate: .rtrim) + ) + XCTAssertEqual( + "ALTER TABLE \"table\" ADD COLUMN \"stringOptional\" TEXT CHECK (\"stringOptional\" != '') DEFAULT ('string') COLLATE RTRIM", + table.addColumn(stringOptional, check: stringOptional != "", defaultValue: "string", collate: .rtrim) + ) + } + + func test_rename_compilesAlterTableRenameToExpression() { + XCTAssertEqual("ALTER TABLE \"old\" RENAME TO \"table\"", Table("old").rename(table)) + } + + func test_createIndex_compilesCreateIndexExpression() { + XCTAssertEqual("CREATE INDEX \"index_table_on_int64\" ON \"table\" (\"int64\")", table.createIndex(int64)) + + XCTAssertEqual( + "CREATE UNIQUE INDEX \"index_table_on_int64\" ON \"table\" (\"int64\")", + table.createIndex([int64], unique: true) + ) + XCTAssertEqual( + "CREATE INDEX IF NOT EXISTS \"index_table_on_int64\" ON \"table\" (\"int64\")", + table.createIndex([int64], ifNotExists: true) + ) + XCTAssertEqual( + "CREATE UNIQUE INDEX IF NOT EXISTS \"index_table_on_int64\" ON \"table\" (\"int64\")", + table.createIndex([int64], unique: true, ifNotExists: true) + ) + XCTAssertEqual( + "CREATE UNIQUE INDEX IF NOT EXISTS \"main\".\"index_table_on_int64\" ON \"table\" (\"int64\")", + qualifiedTable.createIndex([int64], unique: true, ifNotExists: true) + ) + } + + func test_dropIndex_compilesCreateIndexExpression() { + XCTAssertEqual("DROP INDEX \"index_table_on_int64\"", table.dropIndex(int64)) + XCTAssertEqual("DROP INDEX IF EXISTS \"index_table_on_int64\"", table.dropIndex([int64], ifExists: true)) + } + + func test_create_onView_compilesCreateViewExpression() { + XCTAssertEqual( + "CREATE VIEW \"view\" AS SELECT \"int64\" FROM \"table\"", + _view.create(table.select(int64)) + ) + XCTAssertEqual( + "CREATE TEMPORARY VIEW \"view\" AS SELECT \"int64\" FROM \"table\"", + _view.create(table.select(int64), temporary: true) + ) + XCTAssertEqual( + "CREATE VIEW IF NOT EXISTS \"view\" AS SELECT \"int64\" FROM \"table\"", + _view.create(table.select(int64), ifNotExists: true) + ) + XCTAssertEqual( + "CREATE TEMPORARY VIEW IF NOT EXISTS \"view\" AS SELECT \"int64\" FROM \"table\"", + _view.create(table.select(int64), temporary: true, ifNotExists: true) + ) + } + + func test_create_onVirtualTable_compilesCreateVirtualTableExpression() { + XCTAssertEqual( + "CREATE VIRTUAL TABLE \"virtual_table\" USING \"custom\"('foo', 'bar')", + virtualTable.create(Module("custom", ["foo", "bar"])) + ) + } + + func test_rename_onVirtualTable_compilesAlterTableRenameToExpression() { + XCTAssertEqual( + "ALTER TABLE \"old\" RENAME TO \"virtual_table\"", + VirtualTable("old").rename(virtualTable) + ) + } + +} diff --git a/mobile/ios/ThirdParty/SQLite.swift/Tests/SQLiteTests/SetterTests.swift b/mobile/ios/ThirdParty/SQLite.swift/Tests/SQLiteTests/SetterTests.swift new file mode 100644 index 0000000000..d4f189d7c5 --- /dev/null +++ b/mobile/ios/ThirdParty/SQLite.swift/Tests/SQLiteTests/SetterTests.swift @@ -0,0 +1,137 @@ +import XCTest +import SQLite + +class SetterTests : XCTestCase { + + func test_setterAssignmentOperator_buildsSetter() { + AssertSQL("\"int\" = \"int\"", int <- int) + AssertSQL("\"int\" = 1", int <- 1) + AssertSQL("\"intOptional\" = \"int\"", intOptional <- int) + AssertSQL("\"intOptional\" = \"intOptional\"", intOptional <- intOptional) + AssertSQL("\"intOptional\" = 1", intOptional <- 1) + AssertSQL("\"intOptional\" = NULL", intOptional <- nil) + } + + func test_plusEquals_withStringExpression_buildsSetter() { + AssertSQL("\"string\" = (\"string\" || \"string\")", string += string) + AssertSQL("\"string\" = (\"string\" || 'literal')", string += "literal") + AssertSQL("\"stringOptional\" = (\"stringOptional\" || \"string\")", stringOptional += string) + AssertSQL("\"stringOptional\" = (\"stringOptional\" || \"stringOptional\")", stringOptional += stringOptional) + AssertSQL("\"stringOptional\" = (\"stringOptional\" || 'literal')", stringOptional += "literal") + } + + func test_plusEquals_withNumberExpression_buildsSetter() { + AssertSQL("\"int\" = (\"int\" + \"int\")", int += int) + AssertSQL("\"int\" = (\"int\" + 1)", int += 1) + AssertSQL("\"intOptional\" = (\"intOptional\" + \"int\")", intOptional += int) + AssertSQL("\"intOptional\" = (\"intOptional\" + \"intOptional\")", intOptional += intOptional) + AssertSQL("\"intOptional\" = (\"intOptional\" + 1)", intOptional += 1) + + AssertSQL("\"double\" = (\"double\" + \"double\")", double += double) + AssertSQL("\"double\" = (\"double\" + 1.0)", double += 1) + AssertSQL("\"doubleOptional\" = (\"doubleOptional\" + \"double\")", doubleOptional += double) + AssertSQL("\"doubleOptional\" = (\"doubleOptional\" + \"doubleOptional\")", doubleOptional += doubleOptional) + AssertSQL("\"doubleOptional\" = (\"doubleOptional\" + 1.0)", doubleOptional += 1) + } + + func test_minusEquals_withNumberExpression_buildsSetter() { + AssertSQL("\"int\" = (\"int\" - \"int\")", int -= int) + AssertSQL("\"int\" = (\"int\" - 1)", int -= 1) + AssertSQL("\"intOptional\" = (\"intOptional\" - \"int\")", intOptional -= int) + AssertSQL("\"intOptional\" = (\"intOptional\" - \"intOptional\")", intOptional -= intOptional) + AssertSQL("\"intOptional\" = (\"intOptional\" - 1)", intOptional -= 1) + + AssertSQL("\"double\" = (\"double\" - \"double\")", double -= double) + AssertSQL("\"double\" = (\"double\" - 1.0)", double -= 1) + AssertSQL("\"doubleOptional\" = (\"doubleOptional\" - \"double\")", doubleOptional -= double) + AssertSQL("\"doubleOptional\" = (\"doubleOptional\" - \"doubleOptional\")", doubleOptional -= doubleOptional) + AssertSQL("\"doubleOptional\" = (\"doubleOptional\" - 1.0)", doubleOptional -= 1) + } + + func test_timesEquals_withNumberExpression_buildsSetter() { + AssertSQL("\"int\" = (\"int\" * \"int\")", int *= int) + AssertSQL("\"int\" = (\"int\" * 1)", int *= 1) + AssertSQL("\"intOptional\" = (\"intOptional\" * \"int\")", intOptional *= int) + AssertSQL("\"intOptional\" = (\"intOptional\" * \"intOptional\")", intOptional *= intOptional) + AssertSQL("\"intOptional\" = (\"intOptional\" * 1)", intOptional *= 1) + + AssertSQL("\"double\" = (\"double\" * \"double\")", double *= double) + AssertSQL("\"double\" = (\"double\" * 1.0)", double *= 1) + AssertSQL("\"doubleOptional\" = (\"doubleOptional\" * \"double\")", doubleOptional *= double) + AssertSQL("\"doubleOptional\" = (\"doubleOptional\" * \"doubleOptional\")", doubleOptional *= doubleOptional) + AssertSQL("\"doubleOptional\" = (\"doubleOptional\" * 1.0)", doubleOptional *= 1) + } + + func test_dividedByEquals_withNumberExpression_buildsSetter() { + AssertSQL("\"int\" = (\"int\" / \"int\")", int /= int) + AssertSQL("\"int\" = (\"int\" / 1)", int /= 1) + AssertSQL("\"intOptional\" = (\"intOptional\" / \"int\")", intOptional /= int) + AssertSQL("\"intOptional\" = (\"intOptional\" / \"intOptional\")", intOptional /= intOptional) + AssertSQL("\"intOptional\" = (\"intOptional\" / 1)", intOptional /= 1) + + AssertSQL("\"double\" = (\"double\" / \"double\")", double /= double) + AssertSQL("\"double\" = (\"double\" / 1.0)", double /= 1) + AssertSQL("\"doubleOptional\" = (\"doubleOptional\" / \"double\")", doubleOptional /= double) + AssertSQL("\"doubleOptional\" = (\"doubleOptional\" / \"doubleOptional\")", doubleOptional /= doubleOptional) + AssertSQL("\"doubleOptional\" = (\"doubleOptional\" / 1.0)", doubleOptional /= 1) + } + + func test_moduloEquals_withIntegerExpression_buildsSetter() { + AssertSQL("\"int\" = (\"int\" % \"int\")", int %= int) + AssertSQL("\"int\" = (\"int\" % 1)", int %= 1) + AssertSQL("\"intOptional\" = (\"intOptional\" % \"int\")", intOptional %= int) + AssertSQL("\"intOptional\" = (\"intOptional\" % \"intOptional\")", intOptional %= intOptional) + AssertSQL("\"intOptional\" = (\"intOptional\" % 1)", intOptional %= 1) + } + + func test_leftShiftEquals_withIntegerExpression_buildsSetter() { + AssertSQL("\"int\" = (\"int\" << \"int\")", int <<= int) + AssertSQL("\"int\" = (\"int\" << 1)", int <<= 1) + AssertSQL("\"intOptional\" = (\"intOptional\" << \"int\")", intOptional <<= int) + AssertSQL("\"intOptional\" = (\"intOptional\" << \"intOptional\")", intOptional <<= intOptional) + AssertSQL("\"intOptional\" = (\"intOptional\" << 1)", intOptional <<= 1) + } + + func test_rightShiftEquals_withIntegerExpression_buildsSetter() { + AssertSQL("\"int\" = (\"int\" >> \"int\")", int >>= int) + AssertSQL("\"int\" = (\"int\" >> 1)", int >>= 1) + AssertSQL("\"intOptional\" = (\"intOptional\" >> \"int\")", intOptional >>= int) + AssertSQL("\"intOptional\" = (\"intOptional\" >> \"intOptional\")", intOptional >>= intOptional) + AssertSQL("\"intOptional\" = (\"intOptional\" >> 1)", intOptional >>= 1) + } + + func test_bitwiseAndEquals_withIntegerExpression_buildsSetter() { + AssertSQL("\"int\" = (\"int\" & \"int\")", int &= int) + AssertSQL("\"int\" = (\"int\" & 1)", int &= 1) + AssertSQL("\"intOptional\" = (\"intOptional\" & \"int\")", intOptional &= int) + AssertSQL("\"intOptional\" = (\"intOptional\" & \"intOptional\")", intOptional &= intOptional) + AssertSQL("\"intOptional\" = (\"intOptional\" & 1)", intOptional &= 1) + } + + func test_bitwiseOrEquals_withIntegerExpression_buildsSetter() { + AssertSQL("\"int\" = (\"int\" | \"int\")", int |= int) + AssertSQL("\"int\" = (\"int\" | 1)", int |= 1) + AssertSQL("\"intOptional\" = (\"intOptional\" | \"int\")", intOptional |= int) + AssertSQL("\"intOptional\" = (\"intOptional\" | \"intOptional\")", intOptional |= intOptional) + AssertSQL("\"intOptional\" = (\"intOptional\" | 1)", intOptional |= 1) + } + + func test_bitwiseExclusiveOrEquals_withIntegerExpression_buildsSetter() { + AssertSQL("\"int\" = (~((\"int\" & \"int\")) & (\"int\" | \"int\"))", int ^= int) + AssertSQL("\"int\" = (~((\"int\" & 1)) & (\"int\" | 1))", int ^= 1) + AssertSQL("\"intOptional\" = (~((\"intOptional\" & \"int\")) & (\"intOptional\" | \"int\"))", intOptional ^= int) + AssertSQL("\"intOptional\" = (~((\"intOptional\" & \"intOptional\")) & (\"intOptional\" | \"intOptional\"))", intOptional ^= intOptional) + AssertSQL("\"intOptional\" = (~((\"intOptional\" & 1)) & (\"intOptional\" | 1))", intOptional ^= 1) + } + + func test_postfixPlus_withIntegerValue_buildsSetter() { + AssertSQL("\"int\" = (\"int\" + 1)", int++) + AssertSQL("\"intOptional\" = (\"intOptional\" + 1)", intOptional++) + } + + func test_postfixMinus_withIntegerValue_buildsSetter() { + AssertSQL("\"int\" = (\"int\" - 1)", int--) + AssertSQL("\"intOptional\" = (\"intOptional\" - 1)", intOptional--) + } + +} diff --git a/mobile/ios/ThirdParty/SQLite.swift/Tests/SQLiteTests/StatementTests.swift b/mobile/ios/ThirdParty/SQLite.swift/Tests/SQLiteTests/StatementTests.swift new file mode 100644 index 0000000000..326259b223 --- /dev/null +++ b/mobile/ios/ThirdParty/SQLite.swift/Tests/SQLiteTests/StatementTests.swift @@ -0,0 +1,26 @@ +import XCTest +import SQLite + +class StatementTests : SQLiteTestCase { + override func setUp() { + super.setUp() + CreateUsersTable() + } + + func test_cursor_to_blob() { + try! InsertUsers("alice") + let statement = try! db.prepare("SELECT email FROM users") + XCTAssert(try! statement.step()) + let blob = statement.row[0] as Blob + XCTAssertEqual("alice@example.com", String(bytes: blob.bytes, encoding: .utf8)!) + } + + func test_zero_sized_blob_returns_null() { + let blobs = Table("blobs") + let blobColumn = Expression("blob_column") + try! db.run(blobs.create { $0.column(blobColumn) }) + try! db.run(blobs.insert(blobColumn <- Blob(bytes: []))) + let blobValue = try! db.scalar(blobs.select(blobColumn).limit(1, offset: 0)) + XCTAssertEqual([], blobValue.bytes) + } +} diff --git a/mobile/ios/ThirdParty/SQLite.swift/Tests/SQLiteTests/TestHelpers.swift b/mobile/ios/ThirdParty/SQLite.swift/Tests/SQLiteTests/TestHelpers.swift new file mode 100644 index 0000000000..8c33bf6a07 --- /dev/null +++ b/mobile/ios/ThirdParty/SQLite.swift/Tests/SQLiteTests/TestHelpers.swift @@ -0,0 +1,115 @@ +import XCTest +import SQLite + +class SQLiteTestCase : XCTestCase { + + var trace = [String: Int]() + + let db = try! Connection() + + let users = Table("users") + + override func setUp() { + super.setUp() + + db.trace { SQL in + print(SQL) + self.trace[SQL] = (self.trace[SQL] ?? 0) + 1 + } + } + + func CreateUsersTable() { + try! db.execute( + "CREATE TABLE \"users\" (" + + "id INTEGER PRIMARY KEY, " + + "email TEXT NOT NULL UNIQUE, " + + "age INTEGER, " + + "salary REAL, " + + "admin BOOLEAN NOT NULL DEFAULT 0 CHECK (admin IN (0, 1)), " + + "manager_id INTEGER, " + + "FOREIGN KEY(manager_id) REFERENCES users(id)" + + ")" + ) + } + + func InsertUsers(_ names: String...) throws { + try InsertUsers(names) + } + + func InsertUsers(_ names: [String]) throws { + for name in names { try InsertUser(name) } + } + + @discardableResult func InsertUser(_ name: String, age: Int? = nil, admin: Bool = false) throws -> Statement { + return try db.run( + "INSERT INTO \"users\" (email, age, admin) values (?, ?, ?)", + "\(name)@example.com", age?.datatypeValue, admin.datatypeValue + ) + } + + func AssertSQL(_ SQL: String, _ executions: Int = 1, _ message: String? = nil, file: StaticString = #file, line: UInt = #line) { + XCTAssertEqual( + executions, trace[SQL] ?? 0, + message ?? SQL, + file: file, line: line + ) + } + + func AssertSQL(_ SQL: String, _ statement: Statement, _ message: String? = nil, file: StaticString = #file, line: UInt = #line) { + try! statement.run() + AssertSQL(SQL, 1, message, file: file, line: line) + if let count = trace[SQL] { trace[SQL] = count - 1 } + } + +// func AssertSQL(SQL: String, _ query: Query, _ message: String? = nil, file: String = __FILE__, line: UInt = __LINE__) { +// for _ in query {} +// AssertSQL(SQL, 1, message, file: file, line: line) +// if let count = trace[SQL] { trace[SQL] = count - 1 } +// } + + func async(expect description: String = "async", timeout: Double = 5, block: (@escaping () -> Void) -> Void) { + let expectation = self.expectation(description: description) + block(expectation.fulfill) + waitForExpectations(timeout: timeout, handler: nil) + } + +} + +let bool = Expression("bool") +let boolOptional = Expression("boolOptional") + +let data = Expression("blob") +let dataOptional = Expression("blobOptional") + +let date = Expression("date") +let dateOptional = Expression("dateOptional") + +let double = Expression("double") +let doubleOptional = Expression("doubleOptional") + +let int = Expression("int") +let intOptional = Expression("intOptional") + +let int64 = Expression("int64") +let int64Optional = Expression("int64Optional") + +let string = Expression("string") +let stringOptional = Expression("stringOptional") + +func AssertSQL(_ expression1: @autoclosure () -> String, _ expression2: @autoclosure () -> Expressible, file: StaticString = #file, line: UInt = #line) { + XCTAssertEqual(expression1(), expression2().asSQL(), file: file, line: line) +} + +func AssertThrows(_ expression: @autoclosure () throws -> T, file: StaticString = #file, line: UInt = #line) { + do { + _ = try expression() + XCTFail("expression expected to throw", file: file, line: line) + } catch { + XCTAssert(true, file: file, line: line) + } +} + +let table = Table("table") +let qualifiedTable = Table("table", database: "main") +let virtualTable = VirtualTable("virtual_table") +let _view = View("view") // avoid Mac XCTestCase collision diff --git a/mobile/ios/ThirdParty/SQLite.swift/Tests/SQLiteTests/ValueTests.swift b/mobile/ios/ThirdParty/SQLite.swift/Tests/SQLiteTests/ValueTests.swift new file mode 100644 index 0000000000..bda2b4b380 --- /dev/null +++ b/mobile/ios/ThirdParty/SQLite.swift/Tests/SQLiteTests/ValueTests.swift @@ -0,0 +1,6 @@ +import XCTest +import SQLite + +class ValueTests : XCTestCase { + +} diff --git a/mobile/ios/ThirdParty/SQLite.swift/Tests/SQLiteTests/fixtures/encrypted.sqlite b/mobile/ios/ThirdParty/SQLite.swift/Tests/SQLiteTests/fixtures/encrypted.sqlite new file mode 100644 index 0000000000..4b3c4d0ec0 Binary files /dev/null and b/mobile/ios/ThirdParty/SQLite.swift/Tests/SQLiteTests/fixtures/encrypted.sqlite differ diff --git a/mobile/ios/ThirdParty/SQLite.swift/run-tests.sh b/mobile/ios/ThirdParty/SQLite.swift/run-tests.sh new file mode 100644 index 0000000000..ddd7d67834 --- /dev/null +++ b/mobile/ios/ThirdParty/SQLite.swift/run-tests.sh @@ -0,0 +1,15 @@ +#!/bin/bash +set -ev +if [ -n "$BUILD_SCHEME" ]; then + if [ -n "$IOS_SIMULATOR" ]; then + make test BUILD_SCHEME="$BUILD_SCHEME" IOS_SIMULATOR="$IOS_SIMULATOR" + else + make test BUILD_SCHEME="$BUILD_SCHEME" + fi +elif [ -n "$VALIDATOR_SUBSPEC" ]; then + cd Tests/CocoaPods && make test +elif [ -n "$CARTHAGE_PLATFORM" ]; then + cd Tests/Carthage && make test CARTHAGE_PLATFORM="$CARTHAGE_PLATFORM" +elif [ -n "${PACKAGE_MANAGER_COMMAND}" ]; then + swift ${PACKAGE_MANAGER_COMMAND} +fi diff --git a/mobile/ios/ThirdParty/SWTableViewCell/AppDelegate.h b/mobile/ios/ThirdParty/SWTableViewCell/AppDelegate.h new file mode 100644 index 0000000000..c3982020c4 --- /dev/null +++ b/mobile/ios/ThirdParty/SWTableViewCell/AppDelegate.h @@ -0,0 +1,15 @@ +// +// AppDelegate.h +// SWTableViewCell +// +// Created by Chris Wendel on 9/10/13. +// Copyright (c) 2013 Chris Wendel. All rights reserved. +// + +#import + +@interface AppDelegate : UIResponder + +@property (strong, nonatomic) UIWindow *window; + +@end diff --git a/mobile/ios/ThirdParty/SWTableViewCell/AppDelegate.m b/mobile/ios/ThirdParty/SWTableViewCell/AppDelegate.m new file mode 100644 index 0000000000..0eb3256110 --- /dev/null +++ b/mobile/ios/ThirdParty/SWTableViewCell/AppDelegate.m @@ -0,0 +1,46 @@ +// +// AppDelegate.m +// SWTableViewCell +// +// Created by Chris Wendel on 9/10/13. +// Copyright (c) 2013 Chris Wendel. All rights reserved. +// + +#import "AppDelegate.h" + +@implementation AppDelegate + +- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions +{ + // Override point for customization after application launch. + return YES; +} + +- (void)applicationWillResignActive:(UIApplication *)application +{ + // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. + // Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game. +} + +- (void)applicationDidEnterBackground:(UIApplication *)application +{ + // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. + // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. +} + +- (void)applicationWillEnterForeground:(UIApplication *)application +{ + // Called as part of the transition from the background to the inactive state; here you can undo many of the changes made on entering the background. +} + +- (void)applicationDidBecomeActive:(UIApplication *)application +{ + // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. +} + +- (void)applicationWillTerminate:(UIApplication *)application +{ + // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. +} + +@end diff --git a/mobile/ios/ThirdParty/SWTableViewCell/Default-568h@2x.png b/mobile/ios/ThirdParty/SWTableViewCell/Default-568h@2x.png new file mode 100644 index 0000000000..0891b7aabf Binary files /dev/null and b/mobile/ios/ThirdParty/SWTableViewCell/Default-568h@2x.png differ diff --git a/mobile/ios/ThirdParty/SWTableViewCell/Default.png b/mobile/ios/ThirdParty/SWTableViewCell/Default.png new file mode 100644 index 0000000000..4c8ca6f693 Binary files /dev/null and b/mobile/ios/ThirdParty/SWTableViewCell/Default.png differ diff --git a/mobile/ios/ThirdParty/SWTableViewCell/Default@2x.png b/mobile/ios/ThirdParty/SWTableViewCell/Default@2x.png new file mode 100644 index 0000000000..35b84cffeb Binary files /dev/null and b/mobile/ios/ThirdParty/SWTableViewCell/Default@2x.png differ diff --git a/mobile/ios/ThirdParty/SWTableViewCell/MI.png b/mobile/ios/ThirdParty/SWTableViewCell/MI.png new file mode 100644 index 0000000000..d1a1c077e5 Binary files /dev/null and b/mobile/ios/ThirdParty/SWTableViewCell/MI.png differ diff --git a/mobile/ios/ThirdParty/SWTableViewCell/PodFiles/NSMutableArray+SWUtilityButtons.h b/mobile/ios/ThirdParty/SWTableViewCell/PodFiles/NSMutableArray+SWUtilityButtons.h new file mode 100644 index 0000000000..87edf77c31 --- /dev/null +++ b/mobile/ios/ThirdParty/SWTableViewCell/PodFiles/NSMutableArray+SWUtilityButtons.h @@ -0,0 +1,25 @@ +// +// NSMutableArray+SWUtilityButtons.h +// SWTableViewCell +// +// Created by Matt Bowman on 11/27/13. +// Copyright (c) 2013 Chris Wendel. All rights reserved. +// + +#import + +@interface NSMutableArray (SWUtilityButtons) + +- (void)sw_addUtilityButtonWithColor:(UIColor *)color title:(NSString *)title; +- (void)sw_addUtilityButtonWithColor:(UIColor *)color attributedTitle:(NSAttributedString *)title; +- (void)sw_addUtilityButtonWithColor:(UIColor *)color icon:(UIImage *)icon; +- (void)sw_addUtilityButtonWithColor:(UIColor *)color normalIcon:(UIImage *)normalIcon selectedIcon:(UIImage *)selectedIcon; + +@end + + +@interface NSArray (SWUtilityButtons) + +- (BOOL)sw_isEqualToButtons:(NSArray *)buttons; + +@end diff --git a/mobile/ios/ThirdParty/SWTableViewCell/PodFiles/NSMutableArray+SWUtilityButtons.m b/mobile/ios/ThirdParty/SWTableViewCell/PodFiles/NSMutableArray+SWUtilityButtons.m new file mode 100644 index 0000000000..2d32b75298 --- /dev/null +++ b/mobile/ios/ThirdParty/SWTableViewCell/PodFiles/NSMutableArray+SWUtilityButtons.m @@ -0,0 +1,92 @@ +// +// NSMutableArray+SWUtilityButtons.m +// SWTableViewCell +// +// Created by Matt Bowman on 11/27/13. +// Copyright (c) 2013 Chris Wendel. All rights reserved. +// + +#import "NSMutableArray+SWUtilityButtons.h" + +@implementation NSMutableArray (SWUtilityButtons) + +- (void)sw_addUtilityButtonWithColor:(UIColor *)color title:(NSString *)title +{ + UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom]; + button.backgroundColor = color; + [button setTitle:title forState:UIControlStateNormal]; + [button setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal]; + [button.titleLabel setAdjustsFontSizeToFitWidth:YES]; + [self addObject:button]; +} + +- (void)sw_addUtilityButtonWithColor:(UIColor *)color attributedTitle:(NSAttributedString *)title +{ + UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom]; + button.backgroundColor = color; + [button setAttributedTitle:title forState:UIControlStateNormal]; + [button setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal]; + [self addObject:button]; +} + +- (void)sw_addUtilityButtonWithColor:(UIColor *)color icon:(UIImage *)icon +{ + UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom]; + button.backgroundColor = color; + [button setImage:icon forState:UIControlStateNormal]; + [self addObject:button]; +} + +- (void)sw_addUtilityButtonWithColor:(UIColor *)color normalIcon:(UIImage *)normalIcon selectedIcon:(UIImage *)selectedIcon { + UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom]; + button.backgroundColor = color; + [button setImage:normalIcon forState:UIControlStateNormal]; + [button setImage:selectedIcon forState:UIControlStateHighlighted]; + [button setImage:selectedIcon forState:UIControlStateSelected]; + [self addObject:button]; +} + +@end + + +@implementation NSArray (SWUtilityButtons) + +- (BOOL)sw_isEqualToButtons:(NSArray *)buttons +{ + buttons = [buttons copy]; + if (!buttons || self.count != buttons.count) return NO; + + for (NSUInteger idx = 0; idx < self.count; idx++) { + id buttonA = self[idx]; + id buttonB = buttons[idx]; + if (![buttonA isKindOfClass:[UIButton class]] || ![buttonB isKindOfClass:[UIButton class]]) return NO; + if (![[self class] sw_button:buttonA isEqualToButton:buttonB]) return NO; + } + + return YES; +} + ++ (BOOL)sw_button:(UIButton *)buttonA isEqualToButton:(UIButton *)buttonB +{ + if (!buttonA || !buttonB) return NO; + + UIColor *backgroundColorA = buttonA.backgroundColor; + UIColor *backgroundColorB = buttonB.backgroundColor; + BOOL haveEqualBackgroundColors = (!backgroundColorA && !backgroundColorB) || [backgroundColorA isEqual:backgroundColorB]; + + NSString *titleA = [buttonA titleForState:UIControlStateNormal]; + NSString *titleB = [buttonB titleForState:UIControlStateNormal]; + BOOL haveEqualTitles = (!titleA && !titleB) || [titleA isEqualToString:titleB]; + + UIImage *normalIconA = [buttonA imageForState:UIControlStateNormal]; + UIImage *normalIconB = [buttonB imageForState:UIControlStateNormal]; + BOOL haveEqualNormalIcons = (!normalIconA && !normalIconB) || [normalIconA isEqual:normalIconB]; + + UIImage *selectedIconA = [buttonA imageForState:UIControlStateSelected]; + UIImage *selectedIconB = [buttonB imageForState:UIControlStateSelected]; + BOOL haveEqualSelectedIcons = (!selectedIconA && !selectedIconB) || [selectedIconA isEqual:selectedIconB]; + + return haveEqualBackgroundColors && haveEqualTitles && haveEqualNormalIcons && haveEqualSelectedIcons; +} + +@end diff --git a/mobile/ios/ThirdParty/SWTableViewCell/SWTableViewCell-Info.plist b/mobile/ios/ThirdParty/SWTableViewCell/SWTableViewCell-Info.plist new file mode 100644 index 0000000000..2f1980ef53 --- /dev/null +++ b/mobile/ios/ThirdParty/SWTableViewCell/SWTableViewCell-Info.plist @@ -0,0 +1,43 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleDisplayName + ${PRODUCT_NAME} + CFBundleExecutable + ${EXECUTABLE_NAME} + CFBundleIdentifier + com.ChrisWendel.${PRODUCT_NAME:rfc1034identifier} + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + ${PRODUCT_NAME} + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1.0 + LSRequiresIPhoneOS + + UILaunchStoryboardName + MainStoryboard + UIMainStoryboardFile + MainStoryboard + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + UIInterfaceOrientationPortraitUpsideDown + + + diff --git a/mobile/ios/ThirdParty/SWTableViewCell/SWTableViewCell-Prefix.pch b/mobile/ios/ThirdParty/SWTableViewCell/SWTableViewCell-Prefix.pch new file mode 100644 index 0000000000..4fad5d9f4f --- /dev/null +++ b/mobile/ios/ThirdParty/SWTableViewCell/SWTableViewCell-Prefix.pch @@ -0,0 +1,14 @@ +// +// Prefix header for all source files of the 'SWTableViewCell' target in the 'SWTableViewCell' project +// + +#import + +#ifndef __IPHONE_5_0 +#warning "This project uses features only available in iOS SDK 5.0 and later." +#endif + +#ifdef __OBJC__ + #import + #import +#endif diff --git a/mobile/ios/ThirdParty/SWTableViewCell/UMTableViewCell.h b/mobile/ios/ThirdParty/SWTableViewCell/UMTableViewCell.h new file mode 100644 index 0000000000..470258884a --- /dev/null +++ b/mobile/ios/ThirdParty/SWTableViewCell/UMTableViewCell.h @@ -0,0 +1,20 @@ +// +// UMTableViewCell.h +// SWTableViewCell +// +// Created by Matt Bowman on 12/2/13. +// Copyright (c) 2013 Chris Wendel. All rights reserved. +// + +#import +#import "SWTableViewCell.h" + +/* + * Example of a custom cell built in Storyboard + */ +@interface UMTableViewCell : SWTableViewCell + +@property (weak, nonatomic) IBOutlet UIImageView *image; +@property (weak, nonatomic) IBOutlet UILabel *label; + +@end diff --git a/mobile/ios/ThirdParty/SWTableViewCell/UMTableViewCell.m b/mobile/ios/ThirdParty/SWTableViewCell/UMTableViewCell.m new file mode 100644 index 0000000000..23b5591d98 --- /dev/null +++ b/mobile/ios/ThirdParty/SWTableViewCell/UMTableViewCell.m @@ -0,0 +1,13 @@ +// +// UMTableViewCell.m +// SWTableViewCell +// +// Created by Matt Bowman on 12/2/13. +// Copyright (c) 2013 Chris Wendel. All rights reserved. +// + +#import "UMTableViewCell.h" + +@implementation UMTableViewCell + +@end diff --git a/mobile/ios/ThirdParty/SWTableViewCell/ViewController.h b/mobile/ios/ThirdParty/SWTableViewCell/ViewController.h new file mode 100644 index 0000000000..474c0c2924 --- /dev/null +++ b/mobile/ios/ThirdParty/SWTableViewCell/ViewController.h @@ -0,0 +1,14 @@ +// +// ViewController.h +// SWTableViewCell +// +// Created by Chris Wendel on 9/10/13. +// Copyright (c) 2013 Chris Wendel. All rights reserved. +// + +#import +#import "SWTableViewCell.h" + +@interface ViewController : UITableViewController + +@end diff --git a/mobile/ios/ThirdParty/SWTableViewCell/ViewController.m b/mobile/ios/ThirdParty/SWTableViewCell/ViewController.m new file mode 100644 index 0000000000..3efe130e3f --- /dev/null +++ b/mobile/ios/ThirdParty/SWTableViewCell/ViewController.m @@ -0,0 +1,292 @@ +// +// ViewController.m +// SWTableViewCell +// +// Created by Chris Wendel on 9/10/13. +// Copyright (c) 2013 Chris Wendel. All rights reserved. +// + +#import "ViewController.h" +#import "SWTableViewCell.h" +#import "UMTableViewCell.h" + +@interface ViewController () { + NSArray *_sections; + NSMutableArray *_testArray; +} + +@property (nonatomic, weak) IBOutlet UITableView *tableView; +@property (nonatomic) BOOL useCustomCells; +@property (nonatomic, weak) UIRefreshControl *refreshControl; + +@end + +@implementation ViewController + +- (void)viewDidLoad { + [super viewDidLoad]; + + self.navigationItem.leftBarButtonItem = self.editButtonItem; + self.tableView.rowHeight = 90; + + self.navigationItem.title = @"Pull to Toggle Cell Type"; + + // Setup refresh control for example app + UIRefreshControl *refreshControl = [[UIRefreshControl alloc] init]; + [refreshControl addTarget:self action:@selector(toggleCells:) forControlEvents:UIControlEventValueChanged]; + refreshControl.tintColor = [UIColor blueColor]; + + self.refreshControl = refreshControl; + + // If you set the seperator inset on iOS 6 you get a NSInvalidArgumentException...weird + if ([[[UIDevice currentDevice] systemVersion] floatValue] >= 7) { + self.tableView.separatorInset = UIEdgeInsetsMake(0, 0, 0, 0); // Makes the horizontal row seperator stretch the entire length of the table view + } + + _sections = [[UILocalizedIndexedCollation currentCollation] sectionIndexTitles]; + + _testArray = [[NSMutableArray alloc] init]; + + self.useCustomCells = NO; + + for (int i = 0; i < _sections.count; ++i) { + [_testArray addObject:[NSMutableArray array]]; + } + + for (int i = 0; i < 100; ++i) { + NSString *string = [NSString stringWithFormat:@"%d", i]; + [_testArray[i % _sections.count] addObject:string]; + } +} + +#pragma mark UITableViewDataSource + +- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { + return _testArray.count; +} + +- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { + return [_testArray[section] count]; +} + +- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { + NSLog(@"cell selected at index path %ld:%ld", (long)indexPath.section, (long)indexPath.row); + NSLog(@"selected cell index path is %@", [self.tableView indexPathForSelectedRow]); + + if (!tableView.isEditing) { + [tableView deselectRowAtIndexPath:indexPath animated:YES]; + } +} + +- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section { + return _sections[section]; +} + +// Show index titles + +//- (NSArray *)sectionIndexTitlesForTableView:(UITableView *)tableView { +// return [[UILocalizedIndexedCollation currentCollation] sectionIndexTitles]; +//} +// +//- (NSInteger)tableView:(UITableView *)tableView sectionForSectionIndexTitle:(NSString *)title atIndex:(NSInteger)index { +// return [[UILocalizedIndexedCollation currentCollation] sectionForSectionIndexTitleAtIndex:index]; +//} + +#pragma mark - UIRefreshControl Selector + +- (void)toggleCells:(UIRefreshControl*)refreshControl +{ + [refreshControl beginRefreshing]; + self.useCustomCells = !self.useCustomCells; + if (self.useCustomCells) + { + self.refreshControl.tintColor = [UIColor yellowColor]; + } + else + { + self.refreshControl.tintColor = [UIColor blueColor]; + } + [self.tableView reloadData]; + [refreshControl endRefreshing]; +} + +#pragma mark - UIScrollViewDelegate + +- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { + + if (self.useCustomCells) + { + UMTableViewCell *cell = [self.tableView dequeueReusableCellWithIdentifier:@"UMCell" forIndexPath:indexPath]; + + // optionally specify a width that each set of utility buttons will share + [cell setLeftUtilityButtons:[self leftButtons] WithButtonWidth:32.0f]; + [cell setRightUtilityButtons:[self rightButtons] WithButtonWidth:58.0f]; + cell.delegate = self; + + cell.label.text = [NSString stringWithFormat:@"Section: %ld, Seat: %ld", (long)indexPath.section, (long)indexPath.row]; + + return cell; + } + else + { + static NSString *cellIdentifier = @"Cell"; + + SWTableViewCell *cell = (SWTableViewCell *)[tableView dequeueReusableCellWithIdentifier:cellIdentifier]; + + if (cell == nil) { + + cell = [[SWTableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:cellIdentifier]; + + cell.leftUtilityButtons = [self leftButtons]; + cell.rightUtilityButtons = [self rightButtons]; + cell.delegate = self; + } + + NSDate *dateObject = _testArray[indexPath.section][indexPath.row]; + cell.textLabel.text = [dateObject description]; + cell.textLabel.backgroundColor = [UIColor whiteColor]; + cell.detailTextLabel.backgroundColor = [UIColor whiteColor]; + cell.detailTextLabel.text = @"Some detail text"; + + return cell; + } + +} + +- (NSArray *)rightButtons +{ + NSMutableArray *rightUtilityButtons = [NSMutableArray new]; + [rightUtilityButtons sw_addUtilityButtonWithColor: + [UIColor colorWithRed:0.78f green:0.78f blue:0.8f alpha:1.0] + title:@"More"]; + [rightUtilityButtons sw_addUtilityButtonWithColor: + [UIColor colorWithRed:1.0f green:0.231f blue:0.188 alpha:1.0f] + title:@"Delete"]; + + return rightUtilityButtons; +} + +- (NSArray *)leftButtons +{ + NSMutableArray *leftUtilityButtons = [NSMutableArray new]; + + [leftUtilityButtons sw_addUtilityButtonWithColor: + [UIColor colorWithRed:0.07 green:0.75f blue:0.16f alpha:1.0] + icon:[UIImage imageNamed:@"check.png"]]; + [leftUtilityButtons sw_addUtilityButtonWithColor: + [UIColor colorWithRed:1.0f green:1.0f blue:0.35f alpha:1.0] + icon:[UIImage imageNamed:@"clock.png"]]; + [leftUtilityButtons sw_addUtilityButtonWithColor: + [UIColor colorWithRed:1.0f green:0.231f blue:0.188f alpha:1.0] + icon:[UIImage imageNamed:@"cross.png"]]; + [leftUtilityButtons sw_addUtilityButtonWithColor: + [UIColor colorWithRed:0.55f green:0.27f blue:0.07f alpha:1.0] + icon:[UIImage imageNamed:@"list.png"]]; + + return leftUtilityButtons; +} + +// Set row height on an individual basis + +//- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath{ +// return [self rowHeightForIndexPath:indexPath]; +//} +// +//- (CGFloat)rowHeightForIndexPath:(NSIndexPath *)indexPath { +// return ([indexPath row] * 10) + 60; +//} + +- (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath { + // Set background color of cell here if you don't want default white +} + +#pragma mark - SWTableViewDelegate + +- (void)swipeableTableViewCell:(SWTableViewCell *)cell scrollingToState:(SWCellState)state +{ + switch (state) { + case 0: + NSLog(@"utility buttons closed"); + break; + case 1: + NSLog(@"left utility buttons open"); + break; + case 2: + NSLog(@"right utility buttons open"); + break; + default: + break; + } +} + +- (void)swipeableTableViewCell:(SWTableViewCell *)cell didTriggerLeftUtilityButtonWithIndex:(NSInteger)index +{ + switch (index) { + case 0: + NSLog(@"left button 0 was pressed"); + break; + case 1: + NSLog(@"left button 1 was pressed"); + break; + case 2: + NSLog(@"left button 2 was pressed"); + break; + case 3: + NSLog(@"left btton 3 was pressed"); + default: + break; + } +} + +- (void)swipeableTableViewCell:(SWTableViewCell *)cell didTriggerRightUtilityButtonWithIndex:(NSInteger)index +{ + switch (index) { + case 0: + { + NSLog(@"More button was pressed"); + UIAlertView *alertTest = [[UIAlertView alloc] initWithTitle:@"Hello" message:@"More more more" delegate:nil cancelButtonTitle:@"cancel" otherButtonTitles: nil]; + [alertTest show]; + + [cell hideUtilityButtonsAnimated:YES]; + break; + } + case 1: + { + // Delete button was pressed + NSIndexPath *cellIndexPath = [self.tableView indexPathForCell:cell]; + + [_testArray[cellIndexPath.section] removeObjectAtIndex:cellIndexPath.row]; + [self.tableView deleteRowsAtIndexPaths:@[cellIndexPath] withRowAnimation:UITableViewRowAnimationLeft]; + break; + } + default: + break; + } +} + +- (BOOL)swipeableTableViewCellShouldHideUtilityButtonsOnSwipe:(SWTableViewCell *)cell +{ + // allow just one cell's utility button to be open at once + return YES; +} + +- (BOOL)swipeableTableViewCell:(SWTableViewCell *)cell canSwipeToState:(SWCellState)state +{ + switch (state) { + case 1: + // set to NO to disable all left utility buttons appearing + return YES; + break; + case 2: + // set to NO to disable all right utility buttons appearing + return YES; + break; + default: + break; + } + + return YES; +} + + +@end \ No newline at end of file diff --git a/mobile/ios/ThirdParty/SWTableViewCell/check@2x.png b/mobile/ios/ThirdParty/SWTableViewCell/check@2x.png new file mode 100644 index 0000000000..d3a3a6e23e Binary files /dev/null and b/mobile/ios/ThirdParty/SWTableViewCell/check@2x.png differ diff --git a/mobile/ios/ThirdParty/SWTableViewCell/clock@2x.png b/mobile/ios/ThirdParty/SWTableViewCell/clock@2x.png new file mode 100644 index 0000000000..184ad4c41d Binary files /dev/null and b/mobile/ios/ThirdParty/SWTableViewCell/clock@2x.png differ diff --git a/mobile/ios/ThirdParty/SWTableViewCell/cross@2x.png b/mobile/ios/ThirdParty/SWTableViewCell/cross@2x.png new file mode 100644 index 0000000000..053ee0d8e4 Binary files /dev/null and b/mobile/ios/ThirdParty/SWTableViewCell/cross@2x.png differ diff --git a/mobile/ios/ThirdParty/SWTableViewCell/en.lproj/InfoPlist.strings b/mobile/ios/ThirdParty/SWTableViewCell/en.lproj/InfoPlist.strings new file mode 100644 index 0000000000..477b28ff8f --- /dev/null +++ b/mobile/ios/ThirdParty/SWTableViewCell/en.lproj/InfoPlist.strings @@ -0,0 +1,2 @@ +/* Localized versions of Info.plist keys */ + diff --git a/mobile/ios/ThirdParty/SWTableViewCell/en.lproj/MainStoryboard.storyboard b/mobile/ios/ThirdParty/SWTableViewCell/en.lproj/MainStoryboard.storyboard new file mode 100644 index 0000000000..ce553c1619 --- /dev/null +++ b/mobile/ios/ThirdParty/SWTableViewCell/en.lproj/MainStoryboard.storyboard @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/mobile/ios/ThirdParty/SWTableViewCell/list@2x.png b/mobile/ios/ThirdParty/SWTableViewCell/list@2x.png new file mode 100644 index 0000000000..b8ad135f04 Binary files /dev/null and b/mobile/ios/ThirdParty/SWTableViewCell/list@2x.png differ diff --git a/mobile/ios/ThirdParty/SWTableViewCell/main.m b/mobile/ios/ThirdParty/SWTableViewCell/main.m new file mode 100644 index 0000000000..03d31e30dd --- /dev/null +++ b/mobile/ios/ThirdParty/SWTableViewCell/main.m @@ -0,0 +1,18 @@ +// +// main.m +// SWTableViewCell +// +// Created by Chris Wendel on 9/10/13. +// Copyright (c) 2013 Chris Wendel. All rights reserved. +// + +#import + +#import "AppDelegate.h" + +int main(int argc, char *argv[]) +{ + @autoreleasepool { + return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); + } +} diff --git a/mobile/ios/ThirdParty/SWTableViewCell/um.png b/mobile/ios/ThirdParty/SWTableViewCell/um.png new file mode 100644 index 0000000000..cd3b85e43c Binary files /dev/null and b/mobile/ios/ThirdParty/SWTableViewCell/um.png differ diff --git a/mobile/ios/ThirdParty/UIImageColors.swift b/mobile/ios/ThirdParty/UIImageColors.swift new file mode 100644 index 0000000000..2441a003af --- /dev/null +++ b/mobile/ios/ThirdParty/UIImageColors.swift @@ -0,0 +1,278 @@ +// +// UIImageColors.swift +// https://github.com/jathu/UIImageColors +// +// Created by Jathu Satkunarajah (@jathu) on 2015-06-11 - Toronto +// Original Cocoa version by Panic Inc. - Portland +// + +import UIKit + +public struct UIImageColors { + public var background: UIColor! + public var primary: UIColor! + public var secondary: UIColor! + public var detail: UIColor! +} + +class PCCountedColor { + let color: UIColor + let count: Int + + init(color: UIColor, count: Int) { + self.color = color + self.count = count + } +} + +extension CGColor { + var components: [CGFloat] { + get { + var red = CGFloat() + var green = CGFloat() + var blue = CGFloat() + var alpha = CGFloat() + UIColor(cgColor: self).getRed(&red, green: &green, blue: &blue, alpha: &alpha) + return [red,green,blue,alpha] + } + } +} + +extension UIColor { + + var isDarkColor: Bool { + let RGB = self.cgColor.components + return (0.2126 * RGB[0] + 0.7152 * RGB[1] + 0.0722 * RGB[2]) < 0.5 + } + + var isBlackOrWhite: Bool { + let RGB = self.cgColor.components + return (RGB[0] > 0.91 && RGB[1] > 0.91 && RGB[2] > 0.91) || (RGB[0] < 0.09 && RGB[1] < 0.09 && RGB[2] < 0.09) + } + + func isDistinct(compareColor: UIColor) -> Bool { + let bg = self.cgColor.components + let fg = compareColor.cgColor.components + let threshold: CGFloat = 0.25 + + if fabs(bg[0] - fg[0]) > threshold || fabs(bg[1] - fg[1]) > threshold || fabs(bg[2] - fg[2]) > threshold { + if fabs(bg[0] - bg[1]) < 0.03 && fabs(bg[0] - bg[2]) < 0.03 { + if fabs(fg[0] - fg[1]) < 0.03 && fabs(fg[0] - fg[2]) < 0.03 { + return false + } + } + return true + } + return false + } + + func colorWithMinimumSaturation(minSaturation: CGFloat) -> UIColor { + var hue: CGFloat = 0.0 + var saturation: CGFloat = 0.0 + var brightness: CGFloat = 0.0 + var alpha: CGFloat = 0.0 + self.getHue(&hue, saturation: &saturation, brightness: &brightness, alpha: &alpha) + + if saturation < minSaturation { + return UIColor(hue: hue, saturation: minSaturation, brightness: brightness, alpha: alpha) + } else { + return self + } + } + + func isContrastingColor(compareColor: UIColor) -> Bool { + let bg = self.cgColor.components + let fg = compareColor.cgColor.components + + let bgLum = 0.2126 * bg[0] + 0.7152 * bg[1] + 0.0722 * bg[2] + let fgLum = 0.2126 * fg[0] + 0.7152 * fg[1] + 0.0722 * fg[2] + + let bgGreater = bgLum > fgLum + let nom = bgGreater ? bgLum : fgLum + let denom = bgGreater ? fgLum : bgLum + let contrast = (nom + 0.05) / (denom + 0.05) + return 1.6 < contrast + } + +} + +extension UIImage { + private func resizeForUIImageColors(newSize: CGSize) -> UIImage { + UIGraphicsBeginImageContextWithOptions(newSize, false, 0) + defer { + UIGraphicsEndImageContext() + } + self.draw(in: CGRect(x: 0, y: 0, width: newSize.width, height: newSize.height)) + guard let result = UIGraphicsGetImageFromCurrentImageContext() else { + fatalError("UIImageColors.resizeForUIImageColors failed: UIGraphicsGetImageFromCurrentImageContext returned nil") + } + + return result + } + + /** + Get `UIImageColors` from the image asynchronously (in background thread). + Discussion: Use smaller sizes for better performance at the cost of quality colors. Use larger sizes for better color sampling and quality at the cost of performance. + + - parameter scaleDownSize: Downscale size of image for sampling, if `CGSize.zero` is provided, the sample image is rescaled to a width of 250px and the aspect ratio height. + - parameter completionHandler: `UIImageColors` for this image. + */ + public func getColors(scaleDownSize: CGSize = CGSize.zero, completionHandler: @escaping (UIImageColors) -> Void) { + DispatchQueue.global().async { + let result = self.getColors(scaleDownSize: scaleDownSize) + + DispatchQueue.main.async { + completionHandler(result) + } + } + } + + /** + Get `UIImageColors` from the image synchronously (in main thread). + Discussion: Use smaller sizes for better performance at the cost of quality colors. Use larger sizes for better color sampling and quality at the cost of performance. + + - parameter scaleDownSize: Downscale size of image for sampling, if `CGSize.zero` is provided, the sample image is rescaled to a width of 250px and the aspect ratio height. + + - returns: `UIImageColors` for this image. + */ + public func getColors(scaleDownSize: CGSize = CGSize.zero) -> UIImageColors { + + var scaleDownSize = scaleDownSize + + if scaleDownSize == CGSize.zero { + let ratio = self.size.width/self.size.height + let r_width: CGFloat = 250 + scaleDownSize = CGSize(width: r_width, height: r_width/ratio) + } + + var result = UIImageColors() + + let cgImage = self.resizeForUIImageColors(newSize: scaleDownSize).cgImage! + let width: Int = cgImage.width + let height: Int = cgImage.height + + let blackColor = UIColor(red: 0, green: 0, blue: 0, alpha: 1) + let whiteColor = UIColor(red: 1, green: 1, blue: 1, alpha: 1) + + let randomColorsThreshold = Int(CGFloat(height)*0.01) + let sortedColorComparator: Comparator = { (main, other) -> ComparisonResult in + let m = main as! PCCountedColor, o = other as! PCCountedColor + if m.count < o.count { + return ComparisonResult.orderedDescending + } else if m.count == o.count { + return ComparisonResult.orderedSame + } else { + return ComparisonResult.orderedAscending + } + } + + guard let data = CFDataGetBytePtr(cgImage.dataProvider!.data) else { + fatalError("UIImageColors.getColors failed: could not get cgImage data") + } + + // Filter out and collect pixels from image + let imageColors = NSCountedSet(capacity: width * height) + + for x in 0.. 0.3 { + if !nextProposedEdgeColor.color.isBlackOrWhite { + proposedEdgeColor = nextProposedEdgeColor + break + } + } else { + break + } + } + } + result.background = proposedEdgeColor.color + + // Get foreground colors + enumerator = imageColors.objectEnumerator() + sortedColors.removeAllObjects() + sortedColors = NSMutableArray(capacity: imageColors.count) + let findDarkTextColor = !result.background.isDarkColor + + while var kolor = enumerator.nextObject() as? UIColor { + kolor = kolor.colorWithMinimumSaturation(minSaturation: 0.15) + if kolor.isDarkColor == findDarkTextColor { + let colorCount = imageColors.count(for: kolor) + sortedColors.add(PCCountedColor(color: kolor, count: colorCount)) + } + } + sortedColors.sort(comparator: sortedColorComparator) + + for curContainer in sortedColors { + let kolor = (curContainer as! PCCountedColor).color + + if result.primary == nil { + if kolor.isContrastingColor(compareColor: result.background) { + result.primary = kolor + } + } else if result.secondary == nil { + if !result.primary.isDistinct(compareColor: kolor) || !kolor.isContrastingColor(compareColor: result.background) { + continue + } + + result.secondary = kolor + } else if result.detail == nil { + if !result.secondary.isDistinct(compareColor: kolor) || !result.primary.isDistinct(compareColor: kolor) || !kolor.isContrastingColor(compareColor: result.background) { + continue + } + + result.detail = kolor + break + } + } + + let isDarkBackgound = result.background.isDarkColor + + if result.primary == nil { + result.primary = isDarkBackgound ? whiteColor:blackColor + } + + if result.secondary == nil { + result.secondary = isDarkBackgound ? whiteColor:blackColor + } + + if result.detail == nil { + result.detail = isDarkBackgound ? whiteColor:blackColor + } + + return result + } +} diff --git a/mobile/ios/ThirdParty/UIImageViewAligned/Example/ImageAlignment.xcodeproj/project.pbxproj b/mobile/ios/ThirdParty/UIImageViewAligned/Example/ImageAlignment.xcodeproj/project.pbxproj new file mode 100644 index 0000000000..a39ca39ba3 --- /dev/null +++ b/mobile/ios/ThirdParty/UIImageViewAligned/Example/ImageAlignment.xcodeproj/project.pbxproj @@ -0,0 +1,496 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + C027041017A6FAEC0039CEC1 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C027040F17A6FAEC0039CEC1 /* Foundation.framework */; }; + C027041217A6FAEC0039CEC1 /* CoreGraphics.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C027041117A6FAEC0039CEC1 /* CoreGraphics.framework */; }; + C027041417A6FAEC0039CEC1 /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C027041317A6FAEC0039CEC1 /* UIKit.framework */; }; + C027041A17A6FAEC0039CEC1 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = C027041817A6FAEC0039CEC1 /* InfoPlist.strings */; }; + C027041C17A6FAEC0039CEC1 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = C027041B17A6FAEC0039CEC1 /* main.m */; }; + C027042017A6FAEC0039CEC1 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = C027041F17A6FAEC0039CEC1 /* AppDelegate.m */; }; + C027042317A6FAEC0039CEC1 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = C027042117A6FAEC0039CEC1 /* Main.storyboard */; }; + C027042617A6FAEC0039CEC1 /* ViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = C027042517A6FAEC0039CEC1 /* ViewController.m */; }; + C027042817A6FAEC0039CEC1 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = C027042717A6FAEC0039CEC1 /* Images.xcassets */; }; + C027042F17A6FAEC0039CEC1 /* XCTest.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C027042E17A6FAEC0039CEC1 /* XCTest.framework */; }; + C027043017A6FAEC0039CEC1 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C027040F17A6FAEC0039CEC1 /* Foundation.framework */; }; + C027043117A6FAEC0039CEC1 /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C027041317A6FAEC0039CEC1 /* UIKit.framework */; }; + C027043917A6FAEC0039CEC1 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = C027043717A6FAEC0039CEC1 /* InfoPlist.strings */; }; + C027043B17A6FAEC0039CEC1 /* ImageAlignmentTests.m in Sources */ = {isa = PBXBuildFile; fileRef = C027043A17A6FAEC0039CEC1 /* ImageAlignmentTests.m */; }; + C027044717A6FB170039CEC1 /* UIImageViewAligned.m in Sources */ = {isa = PBXBuildFile; fileRef = C027044617A6FB170039CEC1 /* UIImageViewAligned.m */; }; + C027044917A6FE200039CEC1 /* melinda_landscape.jpg in Resources */ = {isa = PBXBuildFile; fileRef = C027044817A6FE200039CEC1 /* melinda_landscape.jpg */; }; + C027044B17A6FE780039CEC1 /* melinda_portrait.jpg in Resources */ = {isa = PBXBuildFile; fileRef = C027044A17A6FE780039CEC1 /* melinda_portrait.jpg */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + C027043217A6FAEC0039CEC1 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = C027040417A6FAEC0039CEC1 /* Project object */; + proxyType = 1; + remoteGlobalIDString = C027040B17A6FAEC0039CEC1; + remoteInfo = ImageAlignment; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXFileReference section */ + C027040C17A6FAEC0039CEC1 /* ImageAlignment.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = ImageAlignment.app; sourceTree = BUILT_PRODUCTS_DIR; }; + C027040F17A6FAEC0039CEC1 /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = System/Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; }; + C027041117A6FAEC0039CEC1 /* CoreGraphics.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreGraphics.framework; path = System/Library/Frameworks/CoreGraphics.framework; sourceTree = SDKROOT; }; + C027041317A6FAEC0039CEC1 /* UIKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UIKit.framework; path = System/Library/Frameworks/UIKit.framework; sourceTree = SDKROOT; }; + C027041717A6FAEC0039CEC1 /* ImageAlignment-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "ImageAlignment-Info.plist"; sourceTree = ""; }; + C027041917A6FAEC0039CEC1 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = ""; }; + C027041B17A6FAEC0039CEC1 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; + C027041D17A6FAEC0039CEC1 /* ImageAlignment-Prefix.pch */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "ImageAlignment-Prefix.pch"; sourceTree = ""; }; + C027041E17A6FAEC0039CEC1 /* AppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; + C027041F17A6FAEC0039CEC1 /* AppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; + C027042217A6FAEC0039CEC1 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + C027042417A6FAEC0039CEC1 /* ViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ViewController.h; sourceTree = ""; }; + C027042517A6FAEC0039CEC1 /* ViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ViewController.m; sourceTree = ""; }; + C027042717A6FAEC0039CEC1 /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = ""; }; + C027042D17A6FAEC0039CEC1 /* ImageAlignmentTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ImageAlignmentTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + C027042E17A6FAEC0039CEC1 /* XCTest.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = XCTest.framework; path = Library/Frameworks/XCTest.framework; sourceTree = DEVELOPER_DIR; }; + C027043617A6FAEC0039CEC1 /* ImageAlignmentTests-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "ImageAlignmentTests-Info.plist"; sourceTree = ""; }; + C027043817A6FAEC0039CEC1 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = ""; }; + C027043A17A6FAEC0039CEC1 /* ImageAlignmentTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ImageAlignmentTests.m; sourceTree = ""; }; + C027044517A6FB170039CEC1 /* UIImageViewAligned.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = UIImageViewAligned.h; sourceTree = ""; }; + C027044617A6FB170039CEC1 /* UIImageViewAligned.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = UIImageViewAligned.m; sourceTree = ""; }; + C027044817A6FE200039CEC1 /* melinda_landscape.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = melinda_landscape.jpg; sourceTree = ""; }; + C027044A17A6FE780039CEC1 /* melinda_portrait.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = melinda_portrait.jpg; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + C027040917A6FAEC0039CEC1 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + C027041217A6FAEC0039CEC1 /* CoreGraphics.framework in Frameworks */, + C027041417A6FAEC0039CEC1 /* UIKit.framework in Frameworks */, + C027041017A6FAEC0039CEC1 /* Foundation.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + C027042A17A6FAEC0039CEC1 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + C027042F17A6FAEC0039CEC1 /* XCTest.framework in Frameworks */, + C027043117A6FAEC0039CEC1 /* UIKit.framework in Frameworks */, + C027043017A6FAEC0039CEC1 /* Foundation.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + C027040317A6FAEC0039CEC1 = { + isa = PBXGroup; + children = ( + C027041517A6FAEC0039CEC1 /* ImageAlignment */, + C027043417A6FAEC0039CEC1 /* ImageAlignmentTests */, + C027040E17A6FAEC0039CEC1 /* Frameworks */, + C027040D17A6FAEC0039CEC1 /* Products */, + ); + sourceTree = ""; + }; + C027040D17A6FAEC0039CEC1 /* Products */ = { + isa = PBXGroup; + children = ( + C027040C17A6FAEC0039CEC1 /* ImageAlignment.app */, + C027042D17A6FAEC0039CEC1 /* ImageAlignmentTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + C027040E17A6FAEC0039CEC1 /* Frameworks */ = { + isa = PBXGroup; + children = ( + C027040F17A6FAEC0039CEC1 /* Foundation.framework */, + C027041117A6FAEC0039CEC1 /* CoreGraphics.framework */, + C027041317A6FAEC0039CEC1 /* UIKit.framework */, + C027042E17A6FAEC0039CEC1 /* XCTest.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + C027041517A6FAEC0039CEC1 /* ImageAlignment */ = { + isa = PBXGroup; + children = ( + C027044417A6FAFD0039CEC1 /* UIImageViewAligned */, + C027041E17A6FAEC0039CEC1 /* AppDelegate.h */, + C027041F17A6FAEC0039CEC1 /* AppDelegate.m */, + C027042117A6FAEC0039CEC1 /* Main.storyboard */, + C027042417A6FAEC0039CEC1 /* ViewController.h */, + C027042517A6FAEC0039CEC1 /* ViewController.m */, + C027042717A6FAEC0039CEC1 /* Images.xcassets */, + C027044817A6FE200039CEC1 /* melinda_landscape.jpg */, + C027044A17A6FE780039CEC1 /* melinda_portrait.jpg */, + C027041617A6FAEC0039CEC1 /* Supporting Files */, + ); + path = ImageAlignment; + sourceTree = ""; + }; + C027041617A6FAEC0039CEC1 /* Supporting Files */ = { + isa = PBXGroup; + children = ( + C027041717A6FAEC0039CEC1 /* ImageAlignment-Info.plist */, + C027041817A6FAEC0039CEC1 /* InfoPlist.strings */, + C027041B17A6FAEC0039CEC1 /* main.m */, + C027041D17A6FAEC0039CEC1 /* ImageAlignment-Prefix.pch */, + ); + name = "Supporting Files"; + sourceTree = ""; + }; + C027043417A6FAEC0039CEC1 /* ImageAlignmentTests */ = { + isa = PBXGroup; + children = ( + C027043A17A6FAEC0039CEC1 /* ImageAlignmentTests.m */, + C027043517A6FAEC0039CEC1 /* Supporting Files */, + ); + path = ImageAlignmentTests; + sourceTree = ""; + }; + C027043517A6FAEC0039CEC1 /* Supporting Files */ = { + isa = PBXGroup; + children = ( + C027043617A6FAEC0039CEC1 /* ImageAlignmentTests-Info.plist */, + C027043717A6FAEC0039CEC1 /* InfoPlist.strings */, + ); + name = "Supporting Files"; + sourceTree = ""; + }; + C027044417A6FAFD0039CEC1 /* UIImageViewAligned */ = { + isa = PBXGroup; + children = ( + C027044517A6FB170039CEC1 /* UIImageViewAligned.h */, + C027044617A6FB170039CEC1 /* UIImageViewAligned.m */, + ); + name = UIImageViewAligned; + path = ../../UIImageViewAligned; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + C027040B17A6FAEC0039CEC1 /* ImageAlignment */ = { + isa = PBXNativeTarget; + buildConfigurationList = C027043E17A6FAEC0039CEC1 /* Build configuration list for PBXNativeTarget "ImageAlignment" */; + buildPhases = ( + C027040817A6FAEC0039CEC1 /* Sources */, + C027040917A6FAEC0039CEC1 /* Frameworks */, + C027040A17A6FAEC0039CEC1 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = ImageAlignment; + productName = ImageAlignment; + productReference = C027040C17A6FAEC0039CEC1 /* ImageAlignment.app */; + productType = "com.apple.product-type.application"; + }; + C027042C17A6FAEC0039CEC1 /* ImageAlignmentTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = C027044117A6FAEC0039CEC1 /* Build configuration list for PBXNativeTarget "ImageAlignmentTests" */; + buildPhases = ( + C027042917A6FAEC0039CEC1 /* Sources */, + C027042A17A6FAEC0039CEC1 /* Frameworks */, + C027042B17A6FAEC0039CEC1 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + C027043317A6FAEC0039CEC1 /* PBXTargetDependency */, + ); + name = ImageAlignmentTests; + productName = ImageAlignmentTests; + productReference = C027042D17A6FAEC0039CEC1 /* ImageAlignmentTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + C027040417A6FAEC0039CEC1 /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 0500; + ORGANIZATIONNAME = "Andrei Stanescu"; + TargetAttributes = { + C027042C17A6FAEC0039CEC1 = { + TestTargetID = C027040B17A6FAEC0039CEC1; + }; + }; + }; + buildConfigurationList = C027040717A6FAEC0039CEC1 /* Build configuration list for PBXProject "ImageAlignment" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = C027040317A6FAEC0039CEC1; + productRefGroup = C027040D17A6FAEC0039CEC1 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + C027040B17A6FAEC0039CEC1 /* ImageAlignment */, + C027042C17A6FAEC0039CEC1 /* ImageAlignmentTests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + C027040A17A6FAEC0039CEC1 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + C027044917A6FE200039CEC1 /* melinda_landscape.jpg in Resources */, + C027044B17A6FE780039CEC1 /* melinda_portrait.jpg in Resources */, + C027042817A6FAEC0039CEC1 /* Images.xcassets in Resources */, + C027041A17A6FAEC0039CEC1 /* InfoPlist.strings in Resources */, + C027042317A6FAEC0039CEC1 /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + C027042B17A6FAEC0039CEC1 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + C027043917A6FAEC0039CEC1 /* InfoPlist.strings in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + C027040817A6FAEC0039CEC1 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + C027042617A6FAEC0039CEC1 /* ViewController.m in Sources */, + C027042017A6FAEC0039CEC1 /* AppDelegate.m in Sources */, + C027041C17A6FAEC0039CEC1 /* main.m in Sources */, + C027044717A6FB170039CEC1 /* UIImageViewAligned.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + C027042917A6FAEC0039CEC1 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + C027043B17A6FAEC0039CEC1 /* ImageAlignmentTests.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + C027043317A6FAEC0039CEC1 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = C027040B17A6FAEC0039CEC1 /* ImageAlignment */; + targetProxy = C027043217A6FAEC0039CEC1 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + C027041817A6FAEC0039CEC1 /* InfoPlist.strings */ = { + isa = PBXVariantGroup; + children = ( + C027041917A6FAEC0039CEC1 /* en */, + ); + name = InfoPlist.strings; + sourceTree = ""; + }; + C027042117A6FAEC0039CEC1 /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + C027042217A6FAEC0039CEC1 /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + C027043717A6FAEC0039CEC1 /* InfoPlist.strings */ = { + isa = PBXVariantGroup; + children = ( + C027043817A6FAEC0039CEC1 /* en */, + ); + name = InfoPlist.strings; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + C027043C17A6FAEC0039CEC1 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_IDENTITY = "iPhone Developer: AG AG (4AJVDG7NFQ)"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer: AG AG (4AJVDG7NFQ)"; + COPY_PHASE_STRIP = NO; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_SYMBOLS_PRIVATE_EXTERN = NO; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 6.0; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + }; + name = Debug; + }; + C027043D17A6FAEC0039CEC1 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_IDENTITY = "iPhone Developer: AG AG (4AJVDG7NFQ)"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer: AG AG (4AJVDG7NFQ)"; + COPY_PHASE_STRIP = YES; + ENABLE_NS_ASSERTIONS = NO; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 6.0; + SDKROOT = iphoneos; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + C027043F17A6FAEC0039CEC1 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME = LaunchImage; + GCC_PRECOMPILE_PREFIX_HEADER = YES; + GCC_PREFIX_HEADER = "ImageAlignment/ImageAlignment-Prefix.pch"; + INFOPLIST_FILE = "ImageAlignment/ImageAlignment-Info.plist"; + PRODUCT_NAME = "$(TARGET_NAME)"; + WRAPPER_EXTENSION = app; + }; + name = Debug; + }; + C027044017A6FAEC0039CEC1 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME = LaunchImage; + GCC_PRECOMPILE_PREFIX_HEADER = YES; + GCC_PREFIX_HEADER = "ImageAlignment/ImageAlignment-Prefix.pch"; + INFOPLIST_FILE = "ImageAlignment/ImageAlignment-Info.plist"; + PRODUCT_NAME = "$(TARGET_NAME)"; + WRAPPER_EXTENSION = app; + }; + name = Release; + }; + C027044217A6FAEC0039CEC1 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(BUILT_PRODUCTS_DIR)/ImageAlignment.app/ImageAlignment"; + FRAMEWORK_SEARCH_PATHS = ( + "$(SDKROOT)/Developer/Library/Frameworks", + "$(inherited)", + "$(SYSTEM_APPS_DIR)/Xcode5-DP3.app/Contents/Developer/Library/Frameworks", + ); + GCC_PRECOMPILE_PREFIX_HEADER = YES; + GCC_PREFIX_HEADER = "ImageAlignment/ImageAlignment-Prefix.pch"; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + INFOPLIST_FILE = "ImageAlignmentTests/ImageAlignmentTests-Info.plist"; + PRODUCT_NAME = "$(TARGET_NAME)"; + TEST_HOST = "$(BUNDLE_LOADER)"; + WRAPPER_EXTENSION = xctest; + }; + name = Debug; + }; + C027044317A6FAEC0039CEC1 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(BUILT_PRODUCTS_DIR)/ImageAlignment.app/ImageAlignment"; + FRAMEWORK_SEARCH_PATHS = ( + "$(SDKROOT)/Developer/Library/Frameworks", + "$(inherited)", + "$(SYSTEM_APPS_DIR)/Xcode5-DP3.app/Contents/Developer/Library/Frameworks", + ); + GCC_PRECOMPILE_PREFIX_HEADER = YES; + GCC_PREFIX_HEADER = "ImageAlignment/ImageAlignment-Prefix.pch"; + INFOPLIST_FILE = "ImageAlignmentTests/ImageAlignmentTests-Info.plist"; + PRODUCT_NAME = "$(TARGET_NAME)"; + TEST_HOST = "$(BUNDLE_LOADER)"; + WRAPPER_EXTENSION = xctest; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + C027040717A6FAEC0039CEC1 /* Build configuration list for PBXProject "ImageAlignment" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + C027043C17A6FAEC0039CEC1 /* Debug */, + C027043D17A6FAEC0039CEC1 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + C027043E17A6FAEC0039CEC1 /* Build configuration list for PBXNativeTarget "ImageAlignment" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + C027043F17A6FAEC0039CEC1 /* Debug */, + C027044017A6FAEC0039CEC1 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + C027044117A6FAEC0039CEC1 /* Build configuration list for PBXNativeTarget "ImageAlignmentTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + C027044217A6FAEC0039CEC1 /* Debug */, + C027044317A6FAEC0039CEC1 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = C027040417A6FAEC0039CEC1 /* Project object */; +} diff --git a/mobile/ios/ThirdParty/UIImageViewAligned/Example/ImageAlignment/AppDelegate.h b/mobile/ios/ThirdParty/UIImageViewAligned/Example/ImageAlignment/AppDelegate.h new file mode 100644 index 0000000000..adac9a0ba8 --- /dev/null +++ b/mobile/ios/ThirdParty/UIImageViewAligned/Example/ImageAlignment/AppDelegate.h @@ -0,0 +1,14 @@ +// +// AppDelegate.h +// ImageAlignment +// +// Created by Andrei Stanescu on 7/29/13. +// + +#import + +@interface AppDelegate : UIResponder + +@property (strong, nonatomic) UIWindow *window; + +@end diff --git a/mobile/ios/ThirdParty/UIImageViewAligned/Example/ImageAlignment/AppDelegate.m b/mobile/ios/ThirdParty/UIImageViewAligned/Example/ImageAlignment/AppDelegate.m new file mode 100644 index 0000000000..27c9e72bfc --- /dev/null +++ b/mobile/ios/ThirdParty/UIImageViewAligned/Example/ImageAlignment/AppDelegate.m @@ -0,0 +1,45 @@ +// +// AppDelegate.m +// ImageAlignment +// +// Created by Andrei Stanescu on 7/29/13. +// + +#import "AppDelegate.h" + +@implementation AppDelegate + +- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions +{ + // Override point for customization after application launch. + return YES; +} + +- (void)applicationWillResignActive:(UIApplication *)application +{ + // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. + // Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game. +} + +- (void)applicationDidEnterBackground:(UIApplication *)application +{ + // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. + // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. +} + +- (void)applicationWillEnterForeground:(UIApplication *)application +{ + // Called as part of the transition from the background to the inactive state; here you can undo many of the changes made on entering the background. +} + +- (void)applicationDidBecomeActive:(UIApplication *)application +{ + // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. +} + +- (void)applicationWillTerminate:(UIApplication *)application +{ + // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. +} + +@end diff --git a/mobile/ios/ThirdParty/UIImageViewAligned/Example/ImageAlignment/Base.lproj/Main.storyboard b/mobile/ios/ThirdParty/UIImageViewAligned/Example/ImageAlignment/Base.lproj/Main.storyboard new file mode 100644 index 0000000000..1b1114e388 --- /dev/null +++ b/mobile/ios/ThirdParty/UIImageViewAligned/Example/ImageAlignment/Base.lproj/Main.storyboard @@ -0,0 +1,151 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/mobile/ios/ThirdParty/UIImageViewAligned/Example/ImageAlignment/ImageAlignment-Info.plist b/mobile/ios/ThirdParty/UIImageViewAligned/Example/ImageAlignment/ImageAlignment-Info.plist new file mode 100644 index 0000000000..3367744dd2 --- /dev/null +++ b/mobile/ios/ThirdParty/UIImageViewAligned/Example/ImageAlignment/ImageAlignment-Info.plist @@ -0,0 +1,40 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleDisplayName + ${PRODUCT_NAME} + CFBundleExecutable + ${EXECUTABLE_NAME} + CFBundleIdentifier + com.reydanro.${PRODUCT_NAME:rfc1034identifier} + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + ${PRODUCT_NAME} + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1.0 + LSRequiresIPhoneOS + + UIMainStoryboardFile + Main + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/mobile/ios/ThirdParty/UIImageViewAligned/Example/ImageAlignment/ImageAlignment-Prefix.pch b/mobile/ios/ThirdParty/UIImageViewAligned/Example/ImageAlignment/ImageAlignment-Prefix.pch new file mode 100644 index 0000000000..82a2bb4507 --- /dev/null +++ b/mobile/ios/ThirdParty/UIImageViewAligned/Example/ImageAlignment/ImageAlignment-Prefix.pch @@ -0,0 +1,16 @@ +// +// Prefix header +// +// The contents of this file are implicitly included at the beginning of every source file. +// + +#import + +#ifndef __IPHONE_5_0 +#warning "This project uses features only available in iOS SDK 5.0 and later." +#endif + +#ifdef __OBJC__ + #import + #import +#endif diff --git a/mobile/ios/ThirdParty/UIImageViewAligned/Example/ImageAlignment/Images.xcassets/AppIcon.appiconset/Contents.json b/mobile/ios/ThirdParty/UIImageViewAligned/Example/ImageAlignment/Images.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000000..2f35679e07 --- /dev/null +++ b/mobile/ios/ThirdParty/UIImageViewAligned/Example/ImageAlignment/Images.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,18 @@ +{ + "images" : [ + { + "idiom" : "iphone", + "size" : "29x29", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "60x60", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/mobile/ios/ThirdParty/UIImageViewAligned/Example/ImageAlignment/Images.xcassets/LaunchImage.launchimage/Contents.json b/mobile/ios/ThirdParty/UIImageViewAligned/Example/ImageAlignment/Images.xcassets/LaunchImage.launchimage/Contents.json new file mode 100644 index 0000000000..c79ebd3ada --- /dev/null +++ b/mobile/ios/ThirdParty/UIImageViewAligned/Example/ImageAlignment/Images.xcassets/LaunchImage.launchimage/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "orientation" : "portrait", + "idiom" : "iphone", + "extent" : "full-screen", + "minimum-system-version" : "7.0", + "scale" : "2x" + }, + { + "orientation" : "portrait", + "idiom" : "iphone", + "subtype" : "retina4", + "extent" : "full-screen", + "minimum-system-version" : "7.0", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/mobile/ios/ThirdParty/UIImageViewAligned/Example/ImageAlignment/ViewController.h b/mobile/ios/ThirdParty/UIImageViewAligned/Example/ImageAlignment/ViewController.h new file mode 100644 index 0000000000..bee6400276 --- /dev/null +++ b/mobile/ios/ThirdParty/UIImageViewAligned/Example/ImageAlignment/ViewController.h @@ -0,0 +1,30 @@ +// +// ViewController.h +// ImageAlignment +// +// Created by Andrei Stanescu on 7/29/13. +// + +#import +#import "UIImageViewAligned.h" + +@interface ViewController : UIViewController { + NSArray* _contentModeStrings; +} +@property (weak, nonatomic) IBOutlet UIImageViewAligned *alignedImageView; + +@property (weak, nonatomic) IBOutlet UISwitch *swLandscape; +@property (weak, nonatomic) IBOutlet UIButton *btnContentMode; +@property (weak, nonatomic) IBOutlet UISwitch *swAlignTop; +@property (weak, nonatomic) IBOutlet UISwitch *swAlignRight; +@property (weak, nonatomic) IBOutlet UISwitch *swAlignBottom; +@property (weak, nonatomic) IBOutlet UISwitch *swAlignLeft; + +- (IBAction)onSwitchLandscape:(id)sender; +- (IBAction)onButtonContentMode:(id)sender; +- (IBAction)onSwitchAlignTop:(id)sender; +- (IBAction)onSwitchAlignRight:(id)sender; +- (IBAction)onSwitchAlignBottom:(id)sender; +- (IBAction)onSwitchAlignLeft:(id)sender; + +@end diff --git a/mobile/ios/ThirdParty/UIImageViewAligned/Example/ImageAlignment/ViewController.m b/mobile/ios/ThirdParty/UIImageViewAligned/Example/ImageAlignment/ViewController.m new file mode 100644 index 0000000000..b536fd89fa --- /dev/null +++ b/mobile/ios/ThirdParty/UIImageViewAligned/Example/ImageAlignment/ViewController.m @@ -0,0 +1,79 @@ +// +// ViewController.m +// ImageAlignment +// +// Created by Andrei Stanescu on 7/29/13. +// + +#import "ViewController.h" + +@interface ViewController () + +@end + +@implementation ViewController + +- (void)viewDidLoad +{ + [super viewDidLoad]; + + _contentModeStrings = [NSArray arrayWithObjects: + @"UIViewContentModeScaleToFill", + @"UIViewContentModeScaleAspectFit", // contents scaled to fit with fixed aspect. remainder is transparent + @"UIViewContentModeScaleAspectFill", // contents scaled to fill with fixed aspect. some portion of content may be clipped. + @"UIViewContentModeRedraw", // redraw on bounds change (calls -setNeedsDisplay) + @"UIViewContentModeCenter", // contents remain same size. positioned adjusted. + @"UIViewContentModeTop", + @"UIViewContentModeBottom", + @"UIViewContentModeLeft", + @"UIViewContentModeRight", + @"UIViewContentModeTopLeft", + @"UIViewContentModeTopRight", + @"UIViewContentModeBottomLeft", + @"UIViewContentModeBottomRight", nil]; + + _swLandscape.on = (_alignedImageView.image != nil) && _alignedImageView.image.size.width >= _alignedImageView.image.size.height; + + _swAlignLeft.on = _alignedImageView.alignLeft; + _swAlignRight.on = _alignedImageView.alignRight; + _swAlignTop.on = _alignedImageView.alignTop; + _swAlignBottom.on = _alignedImageView.alignBottom; + + [self refreshContentModeButtonTitle]; +} + +- (IBAction)onSwitchLandscape:(id)sender { + if (_swLandscape.on) + _alignedImageView.image = [UIImage imageNamed:@"melinda_landscape.jpg"]; + else + _alignedImageView.image = [UIImage imageNamed:@"melinda_portrait.jpg"]; +} + +- (IBAction)onButtonContentMode:(id)sender +{ + _alignedImageView.contentMode = (_alignedImageView.contentMode + 1) % (UIViewContentModeBottomRight+1); // bit of hardcoding..but only for the purposes of this example + [self refreshContentModeButtonTitle]; +} + +- (IBAction)onSwitchAlignTop:(id)sender { + _alignedImageView.alignTop + = _swAlignTop.on; +} + +- (IBAction)onSwitchAlignRight:(id)sender { + _alignedImageView.alignRight = _swAlignRight.on; +} + +- (IBAction)onSwitchAlignBottom:(id)sender { + _alignedImageView.alignBottom = _swAlignBottom.on; +} + +- (IBAction)onSwitchAlignLeft:(id)sender { + _alignedImageView.alignLeft = _swAlignLeft.on; +} + +- (void)refreshContentModeButtonTitle +{ + [_btnContentMode setTitle:_contentModeStrings[_alignedImageView.contentMode] forState:UIControlStateNormal]; +} +@end diff --git a/mobile/ios/ThirdParty/UIImageViewAligned/Example/ImageAlignment/en.lproj/InfoPlist.strings b/mobile/ios/ThirdParty/UIImageViewAligned/Example/ImageAlignment/en.lproj/InfoPlist.strings new file mode 100644 index 0000000000..477b28ff8f --- /dev/null +++ b/mobile/ios/ThirdParty/UIImageViewAligned/Example/ImageAlignment/en.lproj/InfoPlist.strings @@ -0,0 +1,2 @@ +/* Localized versions of Info.plist keys */ + diff --git a/mobile/ios/ThirdParty/UIImageViewAligned/Example/ImageAlignment/main.m b/mobile/ios/ThirdParty/UIImageViewAligned/Example/ImageAlignment/main.m new file mode 100644 index 0000000000..a6392dbe6c --- /dev/null +++ b/mobile/ios/ThirdParty/UIImageViewAligned/Example/ImageAlignment/main.m @@ -0,0 +1,17 @@ +// +// main.m +// ImageAlignment +// +// Created by Andrei Stanescu on 7/29/13. +// + +#import + +#import "AppDelegate.h" + +int main(int argc, char * argv[]) +{ + @autoreleasepool { + return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); + } +} diff --git a/mobile/ios/ThirdParty/UIImageViewAligned/Example/ImageAlignment/melinda_landscape.jpg b/mobile/ios/ThirdParty/UIImageViewAligned/Example/ImageAlignment/melinda_landscape.jpg new file mode 100644 index 0000000000..959506b7f4 Binary files /dev/null and b/mobile/ios/ThirdParty/UIImageViewAligned/Example/ImageAlignment/melinda_landscape.jpg differ diff --git a/mobile/ios/ThirdParty/UIImageViewAligned/Example/ImageAlignment/melinda_portrait.jpg b/mobile/ios/ThirdParty/UIImageViewAligned/Example/ImageAlignment/melinda_portrait.jpg new file mode 100644 index 0000000000..d59367fe31 Binary files /dev/null and b/mobile/ios/ThirdParty/UIImageViewAligned/Example/ImageAlignment/melinda_portrait.jpg differ diff --git a/mobile/ios/ThirdParty/UIImageViewAligned/Example/ImageAlignmentTests/ImageAlignmentTests-Info.plist b/mobile/ios/ThirdParty/UIImageViewAligned/Example/ImageAlignmentTests/ImageAlignmentTests-Info.plist new file mode 100644 index 0000000000..cd573a4509 --- /dev/null +++ b/mobile/ios/ThirdParty/UIImageViewAligned/Example/ImageAlignmentTests/ImageAlignmentTests-Info.plist @@ -0,0 +1,22 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + ${EXECUTABLE_NAME} + CFBundleIdentifier + com.reydanro.${PRODUCT_NAME:rfc1034identifier} + CFBundleInfoDictionaryVersion + 6.0 + CFBundlePackageType + BNDL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1 + + diff --git a/mobile/ios/ThirdParty/UIImageViewAligned/Example/ImageAlignmentTests/ImageAlignmentTests.m b/mobile/ios/ThirdParty/UIImageViewAligned/Example/ImageAlignmentTests/ImageAlignmentTests.m new file mode 100644 index 0000000000..811adf2f77 --- /dev/null +++ b/mobile/ios/ThirdParty/UIImageViewAligned/Example/ImageAlignmentTests/ImageAlignmentTests.m @@ -0,0 +1,35 @@ +// +// ImageAlignmentTests.m +// ImageAlignmentTests +// +// Created by Andrei Stanescu on 7/29/13. +// + +#import + +@interface ImageAlignmentTests : XCTestCase + +@end + +@implementation ImageAlignmentTests + +- (void)setUp +{ + [super setUp]; + + // Set-up code here. +} + +- (void)tearDown +{ + // Tear-down code here. + + [super tearDown]; +} + +- (void)testExample +{ + XCTFail(@"No implementation for \"%s\"", __PRETTY_FUNCTION__); +} + +@end diff --git a/mobile/ios/ThirdParty/UIImageViewAligned/Example/ImageAlignmentTests/en.lproj/InfoPlist.strings b/mobile/ios/ThirdParty/UIImageViewAligned/Example/ImageAlignmentTests/en.lproj/InfoPlist.strings new file mode 100644 index 0000000000..477b28ff8f --- /dev/null +++ b/mobile/ios/ThirdParty/UIImageViewAligned/Example/ImageAlignmentTests/en.lproj/InfoPlist.strings @@ -0,0 +1,2 @@ +/* Localized versions of Info.plist keys */ + diff --git a/mobile/ios/ThirdParty/UIImageViewAligned/LICENSE b/mobile/ios/ThirdParty/UIImageViewAligned/LICENSE new file mode 100644 index 0000000000..09c327440e --- /dev/null +++ b/mobile/ios/ThirdParty/UIImageViewAligned/LICENSE @@ -0,0 +1,20 @@ +The MIT License (MIT) + +Copyright (c) 2013 Andrei Stanescu + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/mobile/ios/ThirdParty/UIImageViewAligned/README.md b/mobile/ios/ThirdParty/UIImageViewAligned/README.md new file mode 100644 index 0000000000..6c54117096 --- /dev/null +++ b/mobile/ios/ThirdParty/UIImageViewAligned/README.md @@ -0,0 +1,41 @@ +What is it? +=========== + +It is a subclass of `UIImageView` that allows you to customize the alignment of the displayed image inside the view's frame. +This works even if the `contentMode` is set to `AspectFit`, `AspectFill` or `ScaleToFill`. + + +Why a subclass of UIImageView, and not a standard UIView? +========================================================= +Because there are many cool categories built on top of UIImageView. Subclassing a standard UIView would mean losing them. + +For example, AFNetworking's async `UIImageView` category works perfectly using this container class, and you don't have to worry about a thing. + + +How does it work? +================= +When initialized, `UIImageViewAligned` will create a inner `UIImageView` which will actually hold the image displayed. +The main class then just repositions this inner `UIImageView` to achieve your desired alignment. + +At runtime, you can change the image, contentMode or alignment and the image will reposition itself correctly. + +The `image` property of UIImageViewAligned is overwritten to forward the calls to the inner `UIImageView`, so you can just drag-n-drop into your app. + +Compatibility +============= +From what I tested, it works on iOS 6.0+. +Also, it supports autorotation and will reposition the image correctly. + +Using Interface Builder +======================= +You can setup the alignment from Interface Builder using the `Identity Inspector` tab for the UIImageViewAligned. +There, you can add custom boolean properties that will be set when the view is instantiated. +The possible boolean names are: +`alignLeft` +`alignRight` +`alignTop` +`alignBottom` + +Below is a screenshot of an example. You will not get an immediate feedback in IB regarding your alignment, but it will work at runtime. + +![alt tag](https://raw.github.com/reydanro/UIImageViewAligned/master/docs/IBsetup.png) diff --git a/mobile/ios/ThirdParty/UIImageViewAligned/UIImageViewAligned.podspec b/mobile/ios/ThirdParty/UIImageViewAligned/UIImageViewAligned.podspec new file mode 100644 index 0000000000..27699948f7 --- /dev/null +++ b/mobile/ios/ThirdParty/UIImageViewAligned/UIImageViewAligned.podspec @@ -0,0 +1,21 @@ +Pod::Spec.new do |s| + + s.name = "UIImageViewAligned" + s.version = "0.0.1" + s.summary = "A UIImageView subclass which allows you to align the image left/right/top/bottom, even when contentMode is AspectFit." + + s.description = "It is a subclass of UIImageView that allows you to customize the alignment of the displayed image inside the view's frame. This works even if the contentMode is set to AspectFit, AspectFill or ScaleToFill." + + s.homepage = "https://github.com/reydanro/UIImageViewAligned" + s.license = { :type => "MIT", :file => "LICENSE" } + + s.author = "Andrei Stanescu" + s.social_media_url = "http://twitter.com/MTStudios_" + s.platform = :ios + + s.source = { :git => "https://github.com/reydanro/UIImageViewAligned.git", :tag => "[ADD THIS]" } + s.source_files = "UIImageViewAligned" + + s.framework = "UIKit" + +end diff --git a/mobile/ios/ThirdParty/UIImageViewAligned/UIImageViewAligned/UIImageViewAligned.h b/mobile/ios/ThirdParty/UIImageViewAligned/UIImageViewAligned/UIImageViewAligned.h new file mode 100644 index 0000000000..8280b93be2 --- /dev/null +++ b/mobile/ios/ThirdParty/UIImageViewAligned/UIImageViewAligned/UIImageViewAligned.h @@ -0,0 +1,49 @@ +// +// UIImageViewAligned.h +// awards +// +// Created by Andrei Stanescu on 7/29/13. +// + +#import + +typedef enum +{ + UIImageViewAlignmentMaskCenter = 0, + + UIImageViewAlignmentMaskLeft = 1, + UIImageViewAlignmentMaskRight = 2, + UIImageViewAlignmentMaskTop = 4, + UIImageViewAlignmentMaskBottom = 8, + + UIImageViewAlignmentMaskBottomLeft = UIImageViewAlignmentMaskBottom | UIImageViewAlignmentMaskLeft, + UIImageViewAlignmentMaskBottomRight = UIImageViewAlignmentMaskBottom | UIImageViewAlignmentMaskRight, + UIImageViewAlignmentMaskTopLeft = UIImageViewAlignmentMaskTop | UIImageViewAlignmentMaskLeft, + UIImageViewAlignmentMaskTopRight = UIImageViewAlignmentMaskTop | UIImageViewAlignmentMaskRight, + +}UIImageViewAlignmentMask; + +typedef UIImageViewAlignmentMask UIImageViewAignmentMask __attribute__((deprecated("Use UIImageViewAlignmentMask. Use of UIImageViewAignmentMask (misspelled) is deprecated."))); + + + +@interface UIImageViewAligned : UIImageView + +// This property holds the current alignment +@property (nonatomic) UIImageViewAlignmentMask alignment; + +// Properties needed for Interface Builder quick setup +@property (nonatomic) BOOL alignLeft; +@property (nonatomic) BOOL alignRight; +@property (nonatomic) BOOL alignTop; +@property (nonatomic) BOOL alignBottom; + +// Make the UIImageView scale only up or down +// This are used only if the content mode is Scaled +@property (nonatomic) BOOL enableScaleUp; +@property (nonatomic) BOOL enableScaleDown; + +// Just in case you need access to the inner image view +@property (nonatomic, readonly) UIImageView* realImageView; + +@end diff --git a/mobile/ios/ThirdParty/UIImageViewAligned/UIImageViewAligned/UIImageViewAligned.m b/mobile/ios/ThirdParty/UIImageViewAligned/UIImageViewAligned/UIImageViewAligned.m new file mode 100644 index 0000000000..d1531058f3 --- /dev/null +++ b/mobile/ios/ThirdParty/UIImageViewAligned/UIImageViewAligned/UIImageViewAligned.m @@ -0,0 +1,245 @@ +// +// UIImageViewAligned.m +// awards +// +// Created by Andrei Stanescu on 7/29/13. +// + +#import "UIImageViewAligned.h" + +@implementation UIImageViewAligned + +- (id)initWithFrame:(CGRect)frame +{ + self = [super initWithFrame:frame]; + if (self) + { + [self commonInit]; + } + return self; +} + + +- (id)initWithImage:(UIImage *)image +{ + self = [super initWithImage:image]; + if (self) + { + [self commonInit]; + } + return self; +} + +- (id)initWithImage:(UIImage *)image highlightedImage:(UIImage *)highlightedImage +{ + self = [super initWithImage:image highlightedImage:highlightedImage]; + if (self) + [self commonInit]; + + return self; +} + +- (id)initWithCoder:(NSCoder *)aDecoder +{ + self = [super initWithCoder:aDecoder]; + if (self) + [self commonInit]; + return self; +} + +- (void)commonInit +{ + _enableScaleDown = TRUE; + _enableScaleUp = TRUE; + + _alignment = UIImageViewAlignmentMaskCenter; + + _realImageView = [[UIImageView alloc] initWithFrame:self.bounds]; + _realImageView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; + _realImageView.contentMode = self.contentMode; + [self addSubview:_realImageView]; + + // Move any image we might have from this container to the real image view + if (super.image != nil) + { + UIImage* img = super.image; + super.image = nil; + self.image = img; + } +} + +- (UIImage*)image +{ + return _realImageView.image; +} + +- (void)setImage:(UIImage *)image +{ + [_realImageView setImage:image]; + [self setNeedsLayout]; +} + +- (void)setContentMode:(UIViewContentMode)contentMode +{ + [super setContentMode:contentMode]; + _realImageView.contentMode = contentMode; + [self setNeedsLayout]; +} + +- (void)setAlignment:(UIImageViewAlignmentMask)alignment +{ + if (_alignment == alignment) + return ; + + _alignment = alignment; + [self setNeedsLayout]; +} + +- (void)layoutSubviews +{ + CGSize realsize = [self realContentSize]; + + // Start centered + CGRect realframe = CGRectMake((self.bounds.size.width - realsize.width)/2, (self.bounds.size.height - realsize.height) / 2, realsize.width, realsize.height); + + if ((_alignment & UIImageViewAlignmentMaskLeft) != 0) + realframe.origin.x = 0; + else if ((_alignment & UIImageViewAlignmentMaskRight) != 0) + realframe.origin.x = CGRectGetMaxX(self.bounds) - realframe.size.width; + + if ((_alignment & UIImageViewAlignmentMaskTop) != 0) + realframe.origin.y = 0; + else if ((_alignment & UIImageViewAlignmentMaskBottom) != 0) + realframe.origin.y = CGRectGetMaxY(self.bounds) - realframe.size.height; + + _realImageView.frame = realframe; + + // Make sure we clear the contents of this container layer, since it refreshes from the image property once in a while. + self.layer.contents = nil; +} + +- (CGSize)realContentSize +{ + CGSize size = self.bounds.size; + + if (self.image == nil) + return size; + + switch (self.contentMode) + { + case UIViewContentModeScaleAspectFit: + { + float scalex = self.bounds.size.width / _realImageView.image.size.width; + float scaley = self.bounds.size.height / _realImageView.image.size.height; + float scale = MIN(scalex, scaley); + + if ((scale > 1.0f && !_enableScaleUp) || + (scale < 1.0f && !_enableScaleDown)) + scale = 1.0f; + size = CGSizeMake(_realImageView.image.size.width * scale, _realImageView.image.size.height * scale); + break; + } + + case UIViewContentModeScaleAspectFill: + { + float scalex = self.bounds.size.width / _realImageView.image.size.width; + float scaley = self.bounds.size.height / _realImageView.image.size.height; + float scale = MAX(scalex, scaley); + + if ((scale > 1.0f && !_enableScaleUp) || + (scale < 1.0f && !_enableScaleDown)) + scale = 1.0f; + + size = CGSizeMake(_realImageView.image.size.width * scale, _realImageView.image.size.height * scale); + break; + } + + case UIViewContentModeScaleToFill: + { + float scalex = self.bounds.size.width / _realImageView.image.size.width; + float scaley = self.bounds.size.height / _realImageView.image.size.height; + + if ((scalex > 1.0f && !_enableScaleUp) || + (scalex < 1.0f && !_enableScaleDown)) + scalex = 1.0f; + if ((scaley > 1.0f && !_enableScaleUp) || + (scaley < 1.0f && !_enableScaleDown)) + scaley = 1.0f; + + size = CGSizeMake(_realImageView.image.size.width * scalex, _realImageView.image.size.height * scaley); + break; + } + + default: + size = _realImageView.image.size; + break; + } + + return size; +} + + +#pragma mark - UIImageView overloads + +- (void)setHighlighted:(BOOL)highlighted { + [super setHighlighted:highlighted]; + self.layer.contents = nil; +} + +#pragma mark - Properties needed for Interface Builder + +- (BOOL)alignLeft +{ + return (_alignment & UIImageViewAlignmentMaskLeft) != 0; +} +- (void)setAlignLeft:(BOOL)alignLeft +{ + if (alignLeft) + self.alignment |= UIImageViewAlignmentMaskLeft; + else + self.alignment &= ~UIImageViewAlignmentMaskLeft; +} + +- (BOOL)alignRight +{ + return (_alignment & UIImageViewAlignmentMaskRight) != 0; +} +- (void)setAlignRight:(BOOL)alignRight +{ + if (alignRight) + self.alignment |= UIImageViewAlignmentMaskRight; + else + self.alignment &= ~UIImageViewAlignmentMaskRight; +} + + +- (BOOL)alignTop +{ + return (_alignment & UIImageViewAlignmentMaskTop) != 0; +} +- (void)setAlignTop:(BOOL)alignTop +{ + if (alignTop) + self.alignment |= UIImageViewAlignmentMaskTop; + else + self.alignment &= ~UIImageViewAlignmentMaskTop; +} + +- (BOOL)alignBottom +{ + return (_alignment & UIImageViewAlignmentMaskBottom) != 0; +} +- (void)setAlignBottom:(BOOL)alignBottom +{ + if (alignBottom) + self.alignment |= UIImageViewAlignmentMaskBottom; + else + self.alignment &= ~UIImageViewAlignmentMaskBottom; +} + +- (CGSize)sizeThatFits:(CGSize)size { + return [self.realImageView sizeThatFits:size]; +} + + +@end diff --git a/mobile/ios/ThirdParty/UIImageViewAligned/docs/IBsetup.png b/mobile/ios/ThirdParty/UIImageViewAligned/docs/IBsetup.png new file mode 100644 index 0000000000..9e076082bf Binary files /dev/null and b/mobile/ios/ThirdParty/UIImageViewAligned/docs/IBsetup.png differ diff --git a/mobile/ios/ThirdParty/ecec/CMakeLists.txt b/mobile/ios/ThirdParty/ecec/CMakeLists.txt new file mode 100644 index 0000000000..c678d9dc49 --- /dev/null +++ b/mobile/ios/ThirdParty/ecec/CMakeLists.txt @@ -0,0 +1,77 @@ +cmake_minimum_required(VERSION 3.1 FATAL_ERROR) +project(ece VERSION 0.9.0 LANGUAGES C) + +set(CMAKE_C_STANDARD 99) +set(CMAKE_C_EXTENSIONS OFF) +set(C_STANDARD_REQUIRED ON) + +include(GNUInstallDirs) + +find_package(OpenSSL 1.1.0 REQUIRED) + +enable_testing() + +set(ECE_SOURCES + src/base64url.c + src/encrypt.c + src/decrypt.c + src/keys.c + src/params.c + src/trailer.c) +add_library(ece ${ECE_SOURCES}) +set_target_properties(ece PROPERTIES + OUTPUT_NAME ece + VERSION "${ECE_VERSION}") +target_include_directories(ece + PUBLIC include + PRIVATE src + PRIVATE ${OPENSSL_INCLUDE_DIR}) +target_link_libraries(ece PRIVATE ${OPENSSL_LIBRARIES}) +if(DEFINED ENV{COVERAGE}) + target_compile_options(ece PUBLIC "-fprofile-arcs;-ftest-coverage") + target_link_libraries(ece PUBLIC --coverage) +endif() + +add_executable(ece-decrypt tool/decrypt.c) +set_target_properties(ece-decrypt PROPERTIES EXCLUDE_FROM_ALL 1) +target_include_directories(ece-decrypt PRIVATE tool) +target_link_libraries(ece-decrypt PRIVATE ece) + +add_executable(ece-keygen tool/keygen.c) +set_target_properties(ece-keygen PROPERTIES EXCLUDE_FROM_ALL 1) +target_include_directories(ece-keygen PRIVATE tool) +target_link_libraries(ece-keygen PRIVATE ece) + +set(ECE_TEST_SOURCES + test/decrypt/aes128gcm.c + test/decrypt/aesgcm.c + test/encrypt/aes128gcm.c + test/encrypt/aesgcm.c + test/base64url.c + test/e2e.c + test/params.c + test/test.c) +add_executable(ece-test ${ECE_TEST_SOURCES}) +set_target_properties(ece-test PROPERTIES EXCLUDE_FROM_ALL 1) +target_include_directories(ece-test + PRIVATE test + PRIVATE ${OPENSSL_INCLUDE_DIR}) +target_link_libraries(ece-test + PRIVATE ece + PRIVATE ${OPENSSL_LIBRARIES}) +add_test(NAME ece-test COMMAND ece-test) +add_custom_target(check COMMAND ${CMAKE_CTEST_COMMAND} + -C $ --output-on-failure) +add_dependencies(check ece-test) + +if(MSVC) + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} /W4 /WX") + target_compile_definitions(ece PUBLIC "_CRT_SECURE_NO_WARNINGS") +else() + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wall -pedantic -Werror") + if(${CMAKE_C_COMPILER_ID} MATCHES "(Apple)?Clang") + # GCC's `-Wconversion` reports too many false positives; Clang's is more + # sophisticated. + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wconversion") + endif() +endif() diff --git a/mobile/ios/ThirdParty/ecec/LICENSE b/mobile/ios/ThirdParty/ecec/LICENSE new file mode 100644 index 0000000000..b7d9f590aa --- /dev/null +++ b/mobile/ios/ThirdParty/ecec/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2017 Kit Cambridge + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/mobile/ios/ThirdParty/ecec/README.md b/mobile/ios/ThirdParty/ecec/README.md new file mode 100644 index 0000000000..57d5ec7e26 --- /dev/null +++ b/mobile/ios/ThirdParty/ecec/README.md @@ -0,0 +1,238 @@ +# ecec + +![GitHub version](https://badge.fury.io/gh/web-push-libs%2Fecec.svg) +[![Build Status](https://travis-ci.org/web-push-libs/ecec.svg?branch=master)](https://travis-ci.org/web-push-libs/ecec) +[![Coverage](https://img.shields.io/codecov/c/github/web-push-libs/ecec/master.svg)](https://codecov.io/github/web-push-libs/ecec) + +**ecec** is a C implementation of the [HTTP Encrypted Content-Encoding](http://httpwg.org/http-extensions/draft-ietf-httpbis-encryption-encoding.html) draft. It's a port of the reference [JavaScript implementation](https://github.com/martinthomson/encrypted-content-encoding). + +Encrypted content-coding is used to encrypt [Web Push messages](https://webpush-wg.github.io/webpush-encryption/), and can be used standalone. + +## Table of Contents + +- [Usage](#usage) + * [Generating subscription keys](#generating-subscription-keys) + * [`aes128gcm`](#aes128gcm) + * [`aesgcm`](#aesgcm) +- [Building](#building) + * [Dependencies](#dependencies) + * [macOS and \*nix](#macos-and-nix) + * [Windows](#windows) +- [What is encrypted content-coding?](#what-is-encrypted-content-coding) + * [Web Push](#web-push) + * [`aes128gcm`](#aes128gcm-1) + * [`aesgcm`](#aesgcm-1) +- [License](#license) + +## Usage + +### Generating subscription keys + +```c +#include + +int +main() { + // The subscription private key. This key should never be sent to the app + // server. It should be persisted with the endpoint and auth secret, and used + // to decrypt all messages sent to the subscription. + uint8_t rawRecvPrivKey[ECE_WEBPUSH_PRIVATE_KEY_LENGTH]; + + // The subscription public key. This key should be sent to the app server, + // and used to encrypt messages. The Push DOM API exposes the public key via + // `pushSubscription.getKey("p256dh")`. + uint8_t rawRecvPubKey[ECE_WEBPUSH_PUBLIC_KEY_LENGTH]; + + // The shared auth secret. This secret should be persisted with the + // subscription information, and sent to the app server. The DOM API exposes + // the auth secret via `pushSubscription.getKey("auth")`. + uint8_t authSecret[ECE_WEBPUSH_AUTH_SECRET_LENGTH]; + + int err = ece_webpush_generate_keys( + rawRecvPrivKey, ECE_WEBPUSH_PRIVATE_KEY_LENGTH, rawRecvPubKey, + ECE_WEBPUSH_PUBLIC_KEY_LENGTH, authSecret, ECE_WEBPUSH_AUTH_SECRET_LENGTH); + if (err) { + return 1; + } + return 0; +} +``` + +### `aes128gcm` + +This is the scheme from the latest version of the encrypted content-coding draft. It's not currently supported by any encryption library or browser, but will eventually replace `aesgcm`. This scheme removes the `Crypto-Key` and `Encryption` headers. Instead, the salt, record size, and sender public key are included in the payload as a binary header block. + +```c +// Assume `rawSubPrivKey` and `authSecret` contain the subscription private key +// and auth secret. +uint8_t rawSubPrivKey[ECE_WEBPUSH_PRIVATE_KEY_LENGTH] = {0}; +uint8_t authSecret[ECE_WEBPUSH_AUTH_SECRET_LENGTH] = {0}; + +// Assume `payload` points to the contents of the encrypted payload, and +// `payloadLen` specifies the length. +uint8_t* payload = NULL; +size_t payloadLen = 0; + +size_t plaintextLen = ece_aes128gcm_plaintext_max_length(payload, payloadLen); +assert(plaintextLen > 0); +uint8_t* plaintext = calloc(plaintextLen, sizeof(uint8_t)); +assert(plaintext); + +int err = + ece_webpush_aes128gcm_decrypt(rawSubPrivKey, ECE_WEBPUSH_PRIVATE_KEY_LENGTH, + authSecret, ECE_WEBPUSH_AUTH_SECRET_LENGTH, + payload, payloadLen, plaintext, &plaintextLen); +assert(err == ECE_OK); + +// `plaintext[0..plaintextLen]` contains the decrypted message. + +free(plaintext); +``` + +### `aesgcm` + +All [Web Push libraries](https://github.com/web-push-libs) support the "aesgcm" scheme, as well as Firefox 46+ and Chrome 50+. The app server includes its public key in the `Crypto-Key` HTTP header, the salt and record size in the `Encryption` header, and the encrypted payload in the body of the `POST` request. + +* The `Crypto-Key` header comprises one or more comma-delimited parameters. The first parameter must include a `dh` name-value pair, containing the sender's Base64url-encoded public key. +* The `Encryption` header must include a `salt` name-value pair containing the sender's Base64url-encoded salt, and an optional `rs` pair specifying the record size. + +If the `Crypto-Key` header contains multiple keys, the sender must also include a `keyid` to match the encryption parameters to the key. The drafts have examples for [a single key without a `keyid`](https://tools.ietf.org/html/draft-ietf-webpush-encryption-04#section-5), and [multiple keys with `keyid`s](https://tools.ietf.org/html/draft-ietf-httpbis-encryption-encoding-02#section-5.6). + +```c +uint8_t rawSubPrivKey[ECE_WEBPUSH_PRIVATE_KEY_LENGTH] = {0}; +uint8_t authSecret[ECE_WEBPUSH_AUTH_SECRET_LENGTH] = {0}; + +const char* cryptoKeyHeader = "dh=..."; +const char* encryptionHeader = "salt=...; rs=..."; + +uint8_t* ciphertext = NULL; +size_t ciphertextLen = 0; + +uint8_t salt[ECE_SALT_LENGTH]; +uint8_t rawSenderPubKey[ECE_WEBPUSH_PUBLIC_KEY_LENGTH]; +uint32_t rs = 0; +int err = + ece_webpush_aesgcm_headers_extract_params(cryptoKeyHeader, encryptionHeader, + salt, ECE_SALT_LENGTH, + rawSenderPubKey, + ECE_WEBPUSH_PUBLIC_KEY_LENGTH, &rs); +assert(err == ECE_OK); + +size_t plaintextLen = ece_aesgcm_plaintext_max_length(rs, ciphertextLen); +assert(plaintextLen > 0); +uint8_t* plaintext = calloc(plaintextLen, sizeof(uint8_t)); +assert(plaintext); + +err = ece_webpush_aesgcm_decrypt( + rawSubPrivKey, ECE_WEBPUSH_PRIVATE_KEY_LENGTH, authSecret, + ECE_WEBPUSH_AUTH_SECRET_LENGTH, salt, ECE_SALT_LENGTH, rawSenderPubKey, + ECE_WEBPUSH_PUBLIC_KEY_LENGTH, rs, ciphertext, ciphertextLen, plaintext, + &plaintextLen); +assert(err == ECE_OK); + +// `plaintext[0..plaintextLen]` contains the decrypted message. + +free(plaintext); +``` + +## Building + +### Dependencies + +* [OpenSSL](https://www.openssl.org/) 1.1.0 or higher +* [CMake](https://cmake.org/) 3.1 or higher +* A C99-capable compiler, like [Clang](https://clang.llvm.org/) 3.4, [GCC](https://gcc.gnu.org/) 4.6, or [Visual Studio](https://www.visualstudio.com/vs/community/) 2015 + +### macOS and \*nix + +OpenSSL 1.1.0 is new, and backward-incompatible with 1.0.x. If your package manager ([MacPorts](https://www.macports.org/), [Homebrew](https://brew.sh/), [APT](https://help.ubuntu.com/community/AptGet/Howto), [DNF](https://dnf.readthedocs.io/en/latest/), [yum](http://yum.baseurl.org/)) doesn't have 1.1.0 yet, you'll need to compile it yourself. **ecec** does this to run its tests on [Travis CI](https://docs.travis-ci.com/user/ci-environment/); please see `.travis/install.sh` for the commands. + +In particular, you'll need to set the `OPENSSL_ROOT_DIR` cache entry for CMake to find your compiled version. To build the library: + +```shell +> mkdir build +> cd build +> cmake -DOPENSSL_ROOT_DIR=/usr/local .. +> make +``` + +To build the decryption tool: + +```shell +> make ece-decrypt +> ./ece-decrypt +``` + +To run the tests: + +```shell +> make check +``` + +### Windows + +[Shining Light](https://slproweb.com/products/Win32OpenSSL.html) provides OpenSSL binaries for Windows. The installer will ask if you want to copy the OpenSSL DLLs into the system directory, or the OpenSSL binaries directory. If you choose the binaries directory, you'll need to add it to your `Path`. + +To do so, right-click the Start button, navigate to "System" > "Advanced system settings" > "Environment Variables...", find `Path` under "System variables", click "Edit" > "New", and enter the directory name. This will be `C:\OpenSSL-Win64\bin` if you've installed the 64-bit version in the default location. + +You can then build the library like so: + +```powershell +> mkdir build +> cd build +> cmake -G "Visual Studio 14 2015 Win64" -DOPENSSL_ROOT_DIR=C:\OpenSSL-Win64 .. +> cmake --build . [--config Debug|Release] +``` + +To build the decryption tool: + +```powershell +> cmake --build . --target ece-decrypt [--config Debug|Release] +> .\[Debug|Release]\ece-decrypt +``` + +To run the tests: + +```powershell +> cmake --build . --target check [--config Debug|Release] +``` + +## What is encrypted content-coding? + +Like [TLS](https://en.wikipedia.org/wiki/Transport_Layer_Security), encrypted content-coding uses Diffie-Hellman key exchange to derive a shared secret, which, in turn, is used to derive a symmetric encryption key for a block cipher. This encoding uses [ECDH](https://en.wikipedia.org/wiki/Elliptic_curve_Diffie-Hellman) for key exchange, and [AES](https://en.wikipedia.org/wiki/Advanced_Encryption_Standard) [GCM](https://en.wikipedia.org/wiki/Galois/Counter_Mode) for the block cipher. + +Key exchange is a process where a sender and a receiver generate public-private key pairs, then exchange public keys. The sender combines the receiver's public key with its own private key to obtain a secret. Meanwhile, the receiver combines the sender's public key with its private key to obtain the same secret. [Wikipedia](https://en.wikipedia.org/wiki/Diffie–Hellman_key_exchange) has a good visual explanation. + +The shared ECDH secret isn't directly usable as an encryption key. Instead, both the sender and receiver combine the shared ECDH secret with an [authentication secret](https://tools.ietf.org/html/draft-ietf-webpush-encryption-08#section-3.2), to produce a 32-byte pseudorandom key (PRK). The auth secret is a random 16-byte array generated by the receiver, and shared with the sender along with the receiver's public key. Both parties use [HKDF](https://tools.ietf.org/html/rfc5869) to derive the PRK from the ECDH secret, using the formula `PRK = HKDF-Expand(HKDF-Extract(authSecret, sharedSecret), prkInfo, 32)`. RFC 5869 describes the inputs to `HKDF-Expand` and `HKDF-Extract`, and how they work. `prkInfo` is different depending on the encryption scheme used; more on that later. + +Next, the sender and receiver combine the PRK with a random 16-byte salt. The salt is generated by the sender, and shared with the receiver as part of the message payload. The PRK undergoes two rounds of HKDF to derive the symmetric key and nonce: `key = HKDF-Expand(HKDF-Extract(salt, PRK), keyInfo, 16)`, and `nonce = HKDF-Expand(HKDF-Extract(salt, PRK), nonceInfo, 12)`. As with `prkInfo` above, `keyInfo` and `nonceInfo` are different depending on the exact scheme. + +Finally, the sender chunks the plaintext into fixed-size records, and includes this size in the message payload as the `rs`. The chunks are numbered 0 to N; this is called the sequence number (SEQ), and is used to derive the [IV](https://en.wikipedia.org/wiki/Initialization_vector). All chunks should be `rs` bytes long, but the final chunk can be smaller if needed. + +Each plaintext chunk is padded, then encrypted with AES using the 16-byte symmetric key and a 12-byte IV. The IV is [generated](https://tools.ietf.org/html/draft-ietf-httpbis-encryption-encoding-07#section-2.3) from the nonce by [XOR-ing](https://en.wikipedia.org/wiki/Exclusive_or) the last 6 bytes of the 12-byte nonce with the sequence number. Afterward, the sender appends the GCM authentication tag to the encrypted chunk, producing the final encrypted record. + +To decrypt the message, the receiver chunks the ciphertext into N encrypted records, decrypts each chunk, validates the auth tag, and removes the padding. + +### Web Push + +In Web Push, the app server is the sender, and the browser ("user agent") is the receiver. The browser generates a public-private ECDH key pair and 16-byte auth secret for each push subscription. These keys are static; they're used to decrypt all messages sent to this subscription. The browser exposes the subscription endpoint, public key, and auth secret to the web app via the [Push DOM API](https://w3c.github.io/push-api/). The web app then delivers the endpoint and keys to the app server. + +When the app server wants to send a push message, it generates its own public-private key pair, and computes the shared ECDH secret using the subscription public key. This key pair is ephemeral: it should be discarded after the message is sent, and a new key pair used for the next message. The app server encrypts the payload using the process outlined above, and includes the salt, sender public key, and ciphertext in a `POST` request to the endpoint. The push endpoint relays the encrypted payload to the browser. Finally, the browser decrypts the payload with the subscription private key, and delivers the plaintext to the web app. Because the endpoint doesn't know the private key, it can't decrypt or tamper with the message. + +### `aes128gcm` + +* `prkInfo` is the string `"WebPush: info\0"`, followed by the receiver and sender public keys in uncompressed form. Unlike `aesgcm`, these are not length-prefixed. +* `keyInfo` is the static string `"Content-Encoding: aes128gcm\0"`. +* `nonceInfo` is the static string `"Content-Encoding: nonce\0"`. +* Padding is at the end of each plaintext chunk. The padding block comprises the delimiter, which is `0x02` for the last chunk, and `0x01` for the other chunks. Up to `rs - 16` bytes of `0x0` padding can follow the delimiter. + +### `aesgcm` + +* `prkInfo` is the static string `"Content-Encoding: auth\0"`. +* `keyInfo` is `"Content-Encoding: aesgcm\0P-256\0"`, followed by the length-prefixed (unsigned 16-bit integers) receiver and sender public keys in uncompressed form. +* `nonceInfo` is `"Content-Encoding: nonce\0P-256\0"`, followed by the length-prefixed public keys in the same form as `keyInfo`. +* Padding is at the beginning of each plaintext chunk. The padding block comprises the number (unsigned 16-bit integer) of padding bytes, followed by that many `0x0`-valued bytes. + +## License + +MIT. diff --git a/mobile/ios/ThirdParty/ecec/include/ece.h b/mobile/ios/ThirdParty/ecec/include/ece.h new file mode 100644 index 0000000000..4b779b91b5 --- /dev/null +++ b/mobile/ios/ThirdParty/ecec/include/ece.h @@ -0,0 +1,625 @@ +#ifndef ECE_H +#define ECE_H +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include +#include + +#define ECE_SALT_LENGTH 16 +#define ECE_TAG_LENGTH 16 +#define ECE_WEBPUSH_PRIVATE_KEY_LENGTH 32 +#define ECE_WEBPUSH_PUBLIC_KEY_LENGTH 65 +#define ECE_WEBPUSH_AUTH_SECRET_LENGTH 16 + +#define ECE_AES128GCM_MIN_RS 18 +#define ECE_AES128GCM_HEADER_LENGTH 21 +#define ECE_AES128GCM_MAX_KEY_ID_LENGTH 255 +#define ECE_AES128GCM_PAD_SIZE 1 + +#define ECE_AESGCM_MIN_RS 3 +#define ECE_AESGCM_PAD_SIZE 2 + +#define ECE_OK 0 +#define ECE_ERROR_OUT_OF_MEMORY -1 +#define ECE_ERROR_INVALID_PRIVATE_KEY -2 +#define ECE_ERROR_INVALID_PUBLIC_KEY -3 +#define ECE_ERROR_COMPUTE_SECRET -4 +#define ECE_ERROR_ENCODE_PUBLIC_KEY -5 +#define ECE_ERROR_DECRYPT -6 +#define ECE_ERROR_DECRYPT_PADDING -7 +#define ECE_ERROR_ZERO_PLAINTEXT -8 +#define ECE_ERROR_SHORT_BLOCK -9 +#define ECE_ERROR_SHORT_HEADER -10 +#define ECE_ERROR_ZERO_CIPHERTEXT -11 +#define ECE_ERROR_HKDF -12 +#define ECE_ERROR_INVALID_ENCRYPTION_HEADER -13 +#define ECE_ERROR_INVALID_CRYPTO_KEY_HEADER -14 +#define ECE_ERROR_INVALID_RS -15 +#define ECE_ERROR_INVALID_SALT -16 +#define ECE_ERROR_INVALID_DH -17 +#define ECE_ERROR_ENCRYPT -18 +#define ECE_ERROR_ENCRYPT_PADDING -19 +#define ECE_ERROR_INVALID_AUTH_SECRET -20 +#define ECE_ERROR_GENERATE_KEYS -21 +#define ECE_ERROR_DECRYPT_TRUNCATED -22 + +// Annotates a variable or parameter as unused to avoid compiler warnings. +#define ECE_UNUSED(x) (void) (x) + +/*! + * The policy for appending trailing "=" characters to Base64url-encoded output. + */ +typedef enum ece_base64url_encode_policy_e { + /*! Omits padding, even if the input is not a multiple of 4. */ + ECE_BASE64URL_OMIT_PADDING, + + /*! Includes padding if the input is not a multiple of 4. */ + ECE_BASE64URL_INCLUDE_PADDING, +} ece_base64url_encode_policy_t; + +/*! + * The policy for handling trailing "=" characters in Base64url-encoded input. + */ +typedef enum ece_base64url_decode_policy_e { + /*! + * Fails decoding if the input is unpadded. RFC 4648, section 3.2 requires + * padding, unless the referring specification prohibits it. + */ + ECE_BASE64URL_REQUIRE_PADDING, + + /*! Tolerates padded and unpadded input. */ + ECE_BASE64URL_IGNORE_PADDING, + + /*! + * Fails decoding if the input is padded. This follows the strict Base64url + * variant used in JWS (RFC 7515, Appendix C) and + * draft-ietf-httpbis-encryption-encoding-03. */ + ECE_BASE64URL_REJECT_PADDING, +} ece_base64url_decode_policy_t; + +/*! + * Generates a public-private ECDH key pair and authentication secret for a Web + * Push subscription. + * + * \sa ece_webpush_aes128gcm_decrypt(), + * ece_webpush_aesgcm_decrypt() + * + * \param rawRecvPrivKey[in] The subscription private key. This key should + * be stored locally, and used to decrypt incoming + * messages. + * \param rawRecvPrivKeyLen[in] The length of the subscription private key. Must + * be `ECE_WEBPUSH_PRIVATE_KEY_LENGTH`. + * \param rawRecvPubKey[in] The subscription public key, in uncompressed + * form. This key should be shared with the app + * server, and used to encrypt outgoing messages. + * \param rawRecvPubKeyLen[in] The length of the subscription public key. Must + * be `ECE_WEBPUSH_PUBLIC_KEY_LENGTH`. + * \param authSecret[in] The authentication secret. This secret should + * be stored locally and shared with the app + * server. It's used to derive the content + * encryption key and nonce. + * \param authSecretLen[in] The length of the authentication secret. Must + * be `ECE_WEBPUSH_AUTH_SECRET_LENGTH`. + * + * \return `ECE_OK` on success, or an error code if key + * generation fails. + */ +int +ece_webpush_generate_keys(uint8_t* rawRecvPrivKey, size_t rawRecvPrivKeyLen, + uint8_t* rawRecvPubKey, size_t rawRecvPubKeyLen, + uint8_t* authSecret, size_t authSecretLen); + +/*! + * Calculates the maximum "aes128gcm" plaintext length. The caller should + * allocate and pass an array of this length to the "aes128gcm" decryption + * functions. + * + * \sa ece_aes128gcm_decrypt(), + * ece_webpush_aes128gcm_decrypt() + * + * \param payload[in] The encrypted payload. + * \param payloadLen[in] The length of the encrypted payload. + * + * \return The maximum plaintext length, or 0 if the payload + * header is truncated or invalid. + */ +size_t +ece_aes128gcm_plaintext_max_length(const uint8_t* payload, size_t payloadLen); + +/*! + * Decrypts a message encrypted using the "aes128gcm" scheme, with a symmetric + * key. The key is shared out of band, and identified by the `keyId` parameter + * in the payload header. + * + * \sa ece_aes128gcm_plaintext_max_length(), + * ece_aes128gcm_payload_extract_params() + * + * \param ikm[in] The input keying material (IKM) for the content + * encryption key and nonce. + * \param ikmLen[in] The length of the IKM. + * \param payload[in] The encrypted payload. + * \param payloadLen[in] The length of the encrypted payload. + * \param plaintext[in] An empty array. Must be large enough to hold the + * full plaintext. + * \param plaintextLen[in,out] The input is the length of the empty `plaintext` + * array. On success, the output is set to the + * actual plaintext length, and + * `[0..plaintextLen]` contains the plaintext. + * + * \return `ECE_OK` on success, or an error code if + * the payload is empty or malformed. + */ +int +ece_aes128gcm_decrypt(const uint8_t* ikm, size_t ikmLen, const uint8_t* payload, + size_t payloadLen, uint8_t* plaintext, + size_t* plaintextLen); + +/*! + * Decrypts a Web Push message encrypted using the "aes128gcm" scheme. + * + * \sa ece_aes128gcm_plaintext_max_length() + * + * \param rawRecvPrivKey[in] The subscription private key. + * \param rawRecvPrivKeyLen[in] The length of the subscription private key. Must + * be `ECE_WEBPUSH_PRIVATE_KEY_LENGTH`. + * \param authSecret[in] The authentication secret. + * \param authSecretLen[in] The length of the authentication secret. Must be + * `ECE_WEBPUSH_AUTH_SECRET_LENGTH`. + * \param payload[in] The encrypted payload. + * \param payloadLen[in] The length of the encrypted payload. + * \param plaintext[in] An empty array. Must be large enough to hold the + * full plaintext. + * \param plaintextLen[in,out] The input is the length of the empty `plaintext` + * array. On success, the output is set to the + * the actual plaintext length, and + * `[0..plaintextLen]` contains the plaintext. + * + * \return `ECE_OK` on success, or an error code if + * the payload is empty or malformed. + */ +int +ece_webpush_aes128gcm_decrypt(const uint8_t* rawRecvPrivKey, + size_t rawRecvPrivKeyLen, + const uint8_t* authSecret, size_t authSecretLen, + const uint8_t* payload, size_t payloadLen, + uint8_t* plaintext, size_t* plaintextLen); + +/*! + * Calculates the maximum "aes128gcm" encrypted payload length. The caller + * should allocate and pass an array of this length to the "aes128gcm" + * encryption functions. + * + * \param rs[in] The record size. This is the length of each encrypted + * plaintext chunk, including room for the padding + * delimiter and GCM authentication tag. Must be at + * least `ECE_AES128GCM_MIN_RS`. + * \param padLen[in] The length of additional padding, used to hide the + * plaintext length. Padding is added to the plaintext + * during encryption, and discarded during decryption. + * \param plaintextLen[in] The length of the plaintext. + * + * \return The maximum payload length, or 0 if `rs` is too + * small. + */ +size_t +ece_aes128gcm_payload_max_length(uint32_t rs, size_t padLen, + size_t plaintextLen); + +/*! + * Encrypts a Web Push message using the "aes128gcm" scheme. This function + * automatically generates an ephemeral ECDH key pair and a random salt. + * + * \sa ece_aes128gcm_payload_max_length() + * + * \param rawRecvPubKey[in] The subscription public key, in uncompressed + * form. + * \param rawRecvPubKeyLen[in] The length of the subscription public key. Must + * be `ECE_WEBPUSH_PUBLIC_KEY_LENGTH`. + * \param authSecret[in] The authentication secret. + * \param authSecretLen[in] The length of the authentication secret. Must be + * `ECE_WEBPUSH_AUTH_SECRET_LENGTH`. + * \param rs[in] The record size. Must be at least + * `ECE_AES128GCM_MIN_RS`. + * \param padLen[in] The length of additional padding to include in + * the ciphertext, if any. + * \param plaintext[in] The plaintext to encrypt. + * \param plaintextLen[in] The length of the plaintext. + * \param payload[in] An empty array. Must be large enough to hold the + * full payload. + * \param payloadLen[in,out] The input is the length of the empty `payload` + * array. On success, the output is set to the + * actual payload length, and + * `payload[0..payloadLen]` contains the payload. + * + * \return `ECE_OK` on success, or an error code if + * encryption fails. + */ +int +ece_webpush_aes128gcm_encrypt(const uint8_t* rawRecvPubKey, + size_t rawRecvPubKeyLen, + const uint8_t* authSecret, size_t authSecretLen, + uint32_t rs, size_t padLen, + const uint8_t* plaintext, size_t plaintextLen, + uint8_t* payload, size_t* payloadLen); + +/*! + * Encrypts a Web Push message using the "aes128gcm" scheme, with an explicit + * sender key and salt. The sender key can be reused, but the salt *must* be + * unique to avoid deriving the same content encryption key for multiple + * messages. + * + * \warning In general, you should only use this function + * for testing. `ece_webpush_aes128gcm_encrypt` + * is safer because it doesn't risk accidental + * salt reuse. + * + * \sa ece_aes128gcm_payload_max_length(), + * ece_webpush_aes128gcm_encrypt() + * + * \param rawSenderPrivKey[in] The sender private key. + * \param rawSenderPrivKeyLen[in] The length of the sender private key. Must be + * `ECE_WEBPUSH_PRIVATE_KEY_LENGTH`. + * \param authSecret[in] The authentication secret. + * \param authSecretLen[in] The length of the authentication secret. Must + * be `ECE_WEBPUSH_AUTH_SECRET_LENGTH`. + * \param salt[in] The encryption salt. + * \param saltLen[in] The length of the salt. Must be + * `ECE_SALT_LENGTH`. + * \param rawRecvPubKey[in] The subscription public key, in uncompressed + * form. Must be `ECE_WEBPUSH_PUBLIC_KEY_LENGTH`. + * \param rawRecvPubKeyLen[in] The length of the subscription public key. + * \param rs[in] The record size. Must be at least + * `ECE_AES128GCM_MIN_RS`. + * \param padLen[in] The length of additional padding to include in + * the ciphertext, if any. + * \param plaintext[in] The plaintext to encrypt. + * \param plaintextLen[in] The length of the plaintext. + * \param payload[in] An empty array. Must be large enough to hold + * the full payload. + * \param payloadLen[in,out] The input is the length of the empty `payload` + * array. On success, the output is set to the + * actual payload length, and + * `payload[0..payloadLen]` contains the payload. + * + * \return `ECE_OK` on success, or an error code if + * encryption fails. + */ +int +ece_webpush_aes128gcm_encrypt_with_keys( + const uint8_t* rawSenderPrivKey, size_t rawSenderPrivKeyLen, + const uint8_t* authSecret, size_t authSecretLen, const uint8_t* salt, + size_t saltLen, const uint8_t* rawRecvPubKey, size_t rawRecvPubKeyLen, + uint32_t rs, size_t padLen, const uint8_t* plaintext, size_t plaintextLen, + uint8_t* payload, size_t* payloadLen); + +/*! + * Calculates the maximum "aesgcm" ciphertext length. The caller should allocate + * and pass an array of this length to `ece_webpush_aesgcm_encrypt_with_keys`. + * + * \param rs[in] The record size. Must be least `ECE_AESGCM_MIN_RS`. + * \param padLen[in] The length of additional padding. + * \param plaintextLen[in] The length of the plaintext. + * + * \return The maximum ciphertext length, or 0 if `rs` is too + * small. + */ +size_t +ece_aesgcm_ciphertext_max_length(uint32_t rs, size_t padLen, + size_t plaintextLen); + +/*! + * Encrypts a Web Push message using the "aesgcm" scheme. Like + * `ece_webpush_aes128gcm_encrypt`, this function generates a sender key pair + * and salt. + * + * \sa ece_aesgcm_ciphertext_max_length() + * + * \param rawRecvPubKey[in] The subscription public key, in uncompressed + * form. + * \param rawRecvPubKeyLen[in] The length of the subscription public key. Must + * be `ECE_WEBPUSH_PUBLIC_KEY_LENGTH`. + * \param authSecret[in] The authentication secret. + * \param authSecretLen[in] The length of the authentication secret. Must + * be `ECE_WEBPUSH_AUTH_SECRET_LENGTH`. + * \param rs[in] The record size. Must be at least + * `ECE_AES128GCM_MIN_RS`. + * \param padLen[in] The length of additional padding to include in + * the ciphertext, if any. + * \param plaintext[in] The plaintext to encrypt. + * \param plaintextLen[in] The length of the plaintext. + * \param salt[in] An empty array to hold the salt. + * \param saltLen[in] The length of the empty `salt` array. Must be + * `ECE_SALT_LENGTH`. + * \param rawSenderPubKey[in] An empty array to hold the sender public key. + * \param rawSenderPubKeyLen[in] The length of the empty `rawSenderPubKey` + * array. Must be `ECE_WEBPUSH_PUBLIC_KEY_LENGTH`. + * \param ciphertext[in] An empty array to hold the ciphertext. + * \param ciphertextLen[in, out] The input is the length of the empty + * `ciphertext` array. On success, the output is + * set to the actual ciphertext length, and + * `ciphertext[0..ciphertextLen]` contains the + * ciphertext. + * + * \return `ECE_OK` on success, or an error code if + * encryption fails. + */ +int +ece_webpush_aesgcm_encrypt(const uint8_t* rawRecvPubKey, + size_t rawRecvPubKeyLen, const uint8_t* authSecret, + size_t authSecretLen, uint32_t rs, size_t padLen, + const uint8_t* plaintext, size_t plaintextLen, + uint8_t* salt, size_t saltLen, + uint8_t* rawSenderPubKey, size_t rawSenderPubKeyLen, + uint8_t* ciphertext, size_t* ciphertextLen); + +/*! + * Encrypts a Web Push message using the "aesgcm" scheme and explicit keys. + * + * \warning `ece_webpush_aesgcm_encrypt` is safer because + * it doesn't risk accidental salt reuse. + * + * \sa ece_aesgcm_ciphertext_max_length(), + * ece_webpush_aesgcm_encrypt() + * + * \param rawSenderPrivKey[in] The sender private key. + * \param rawSenderPrivKeyLen[in] The length of the sender private key. Must be + * `ECE_WEBPUSH_PRIVATE_KEY_LENGTH`. + * \param authSecret[in] The authentication secret. + * \param authSecretLen[in] The length of the authentication secret. Must + * be `ECE_WEBPUSH_AUTH_SECRET_LENGTH`. + * \param salt[in] The encryption salt. + * \param saltLen[in] The length of the salt. Must be + * `ECE_SALT_LENGTH`. + * \param rawRecvPubKey[in] The subscription public key, in uncompressed + * form. Must be `ECE_WEBPUSH_PUBLIC_KEY_LENGTH`. + * \param rawRecvPubKeyLen[in] The length of the subscription public key. + * \param rs[in] The record size. Must be at least + * `ECE_AES128GCM_MIN_RS`. + * \param padLen[in] The length of additional padding to include in + * the ciphertext, if any. + * \param plaintext[in] The plaintext to encrypt. + * \param plaintextLen[in] The length of the plaintext. + * \param ciphertext[in] An empty array. Must be large enough to hold + * the full ciphertext. + * \param ciphertextLen[in,out] The input is the length of the empty + * `ciphertext` array. On success, the output is + * set to the actual ciphertext length, and + * `ciphertext[0..ciphertextLen]` contains the + * ciphertext. + * + * \return `ECE_OK` on success, or an error code if + * encryption fails. + */ +int +ece_webpush_aesgcm_encrypt_with_keys( + const uint8_t* rawSenderPrivKey, size_t rawSenderPrivKeyLen, + const uint8_t* authSecret, size_t authSecretLen, const uint8_t* salt, + size_t saltLen, const uint8_t* rawRecvPubKey, size_t rawRecvPubKeyLen, + uint32_t rs, size_t padLen, const uint8_t* plaintext, size_t plaintextLen, + uint8_t* ciphertext, size_t* ciphertextLen); + +/*! + * Calculates the maximum "aesgcm" plaintext length. The caller should allocate + * and pass an array of this length to `ece_webpush_aesgcm_decrypt`. + * + * \sa ece_webpush_aesgcm_decrypt() + * + * \param rs[in] The record size. Must be at least + * `ECE_AESGCM_MIN_RS`. + * \param ciphertextLen[in] The ciphertext length. + * + * \return The maximum plaintext length. + */ +size_t +ece_aesgcm_plaintext_max_length(uint32_t rs, size_t ciphertextLen); + +/*! + * Decrypts a Web Push message encrypted using the "aesgcm" scheme. + * + * \sa ece_aesgcm_plaintext_max_length() + * + * \param rawRecvPrivKey[in] The subscription private key. + * \param rawRecvPrivKeyLen[in] The length of the subscription private key. + * Must be `ECE_WEBPUSH_PRIVATE_KEY_LENGTH`. + * \param authSecret[in] The authentication secret. + * \param authSecretLen[in] The length of the authentication secret. Must + * be `ECE_WEBPUSH_AUTH_SECRET_LENGTH`. + * \param salt[in] + * \param salt[in] The salt, from the `Encryption` header. + * \param saltLen[in] The length of the salt. Must be + * `ECE_SALT_LENGTH`. + * \param rawSenderPubKey[in] The sender public key, in uncompressed form, + * from the `Crypto-Key` header. + * \param rawSenderPubKeyLen[in] The length of the sender public key. Must be + * `ECE_WEBPUSH_PUBLIC_KEY_LENGTH`. + * \param rs[in] The record size. Must be at least + * `ECE_AESGCM_MIN_RS`. + * \param ciphertext[in] The ciphertext. + * \param ciphertextLen[in] The length of the ciphertext. + * \param plaintext[in] An empty array. Must be large enough to hold + * the full plaintext. + * \param plaintextLen[in,out] The input is the length of the empty + * `plaintext` array. On success, the output is + * set to the actual plaintext length, and + * `[0..plaintextLen]` contains the plaintext. + * + * \return `ECE_OK` on success, or an error code if the + * headers or ciphertext are malformed. + */ +int +ece_webpush_aesgcm_decrypt(const uint8_t* rawRecvPrivKey, + size_t rawRecvPrivKeyLen, const uint8_t* authSecret, + size_t authSecretLen, const uint8_t* salt, + size_t saltLen, const uint8_t* rawSenderPubKey, + size_t rawSenderPubKeyLen, uint32_t rs, + const uint8_t* ciphertext, size_t ciphertextLen, + uint8_t* plaintext, size_t* plaintextLen); + +/*! + * Extracts "aes128gcm" decryption parameters from an encrypted payload. + * `salt`, `keyId`, and `ciphertext` are pointers into `payload`, and must not + * outlive it. + * + * \sa ece_aes128gcm_decrypt() + * + * \param payload[in] The encrypted payload. + * \param payloadLen[in] The length of the encrypted payload. + * \param salt[out] The encryption salt. + * \param saltLen[out] The length of the salt. + * \param keyId[out] An identifier for the keying material. + * \param keyIdLen[out] The length of the key ID. + * \param rs[out] The record size. + * \param ciphertext[out] The ciphertext. + * \param ciphertextLen[out] The length of the ciphertext. + * + * \return `ECE_OK` on success, or an error code if the + * payload header is truncated or invalid. + */ +int +ece_aes128gcm_payload_extract_params(const uint8_t* payload, size_t payloadLen, + const uint8_t** salt, size_t* saltLen, + const uint8_t** keyId, size_t* keyIdLen, + uint32_t* rs, const uint8_t** ciphertext, + size_t* ciphertextLen); + +/*! + * Extracts "aesgcm" decryption parameters from the `Crypto-Key` and + * `Encryption` headers. + * + * \sa ece_webpush_aesgcm_decrypt(), + * ece_webpush_aesgcm_headers_from_params() + * + * \param cryptoKeyHeader[in] The value of the `Crypto-Key` HTTP header. + * \param encryptionHeader[in] The value of the `Encryption` HTTP header. + * \param salt[in] An empty array to hold the encryption salt, + * extracted from the `Encryption` header. + * \param saltLen[in] The length of the empty `salt` array. Must be + * `ECE_SALT_LENGTH`. + * \param rawSenderPubKey[in] An empty array to hold the sender public key, + * in uncompressed form, extracted from the + * `Crypto-Key` header. + * \param rawSenderPubKeyLen[in] The length of the empty `rawSenderPubKey` + * array. Must be `ECE_WEBPUSH_PUBLIC_KEY_LENGTH`. + * \param rs[out] The record size. + * + * \return `ECE_OK` on success, or an error code if the + * headers are malformed. + */ +int +ece_webpush_aesgcm_headers_extract_params(const char* cryptoKeyHeader, + const char* encryptionHeader, + uint8_t* salt, size_t saltLen, + uint8_t* rawSenderPubKey, + size_t rawSenderPubKeyLen, + uint32_t* rs); + +/*! + * Builds the `Crypto-Key` and `Encryption` headers from the "aesgcm" + * encryption parameters. + * + * \sa ece_webpush_aesgcm_encrypt_with_keys(), + * ece_webpush_aesgcm_headers_extract_params() + * + * \param salt[in] The encryption salt, to include in the + * `Encryption` header. + * \param saltLen[in] The length of the salt. Must be + * `ECE_SALT_LENGTH`. + * \param rawSenderPubKey[in] The sender public key, in uncompressed + * form, to include in the `Crypto-Key` + * header. + * \param rawSenderPubKeyLen[in] The length of the sender public key. Must + * be `ECE_WEBPUSH_PUBLIC_KEY_LENGTH`. + * \param rs[in] The record size, to include in the + * `Encryption` header. + * \param cryptoKeyHeader[in] An empty array to hold the `Crypto-Key` + * header. May be `NULL` if + * `cryptoKeyHeaderLen` is 0. The header is + * *not* null-terminated; you'll need to add + * a trailing `'\0'` if you want to treat + * `cryptoKeyHeader` as a C string. + * \param cryptoKeyHeaderLen[in, out] The input is the length of the empty + * `cryptoKeyHeader` array. If 0, the output + * is set to the length required to hold + * the result. On success, + * `[0..cryptoKeyHeaderLen]` contains the + * header. + * \param encryptionHeader[in] An empty array to hold the `Encryption` + * header. May be `NULL` if + * `encryptionHeaderLen` is 0. Like + * `cryptoKeyHeader`, this header is not + * null-terminated. + * \param encryptionHeaderLen[in, out] The input is the length of the empty + * `encryptionHeader` array. If 0, the + * output is set to the length required to + * hold the result. On success, + * `[0..encryptionHeaderLen]` contains the + * header. + * + * \return `ECE_OK` on success, or an error code if + * `cryptoKeyHeaderLen` or + * `encryptionHeaderLen` is too small. + */ +int +ece_webpush_aesgcm_headers_from_params(const void* salt, size_t saltLen, + const void* rawSenderPubKey, + size_t rawSenderPubKeyLen, uint32_t rs, + char* cryptoKeyHeader, + size_t* cryptoKeyHeaderLen, + char* encryptionHeader, + size_t* encryptionHeaderLen); + +/*! + * Converts a byte array to a Base64url-encoded (RFC 4648) string. + * + * \param binary[in] The byte array to encode. + * \param binaryLen[in] The length of the byte array. + * \param paddingPolicy[in] The policy for padding the encoded output. + * \param base64[in] An empty array to hold the encoded result. May be + * `NULL` if `base64Len` is 0. This function does + * *not* null-terminate `base64`. This makes it easier + * to include Base64url-encoded substrings in larger + * strings, but means you'll need to add a trailing + * `'\0'` if you want to treat `base64` as a C string. + * \param base64Len[in] The length of the empty `base64` array. On success, + * `base64[0..base64Len]` contains the result. + * + * \return The encoded length. If `binaryLen` is 0, returns the + * length of the array required to hold the result. If + * `binaryLen` is not large enough to hold the full + * result, returns 0. + */ +size_t +ece_base64url_encode(const void* binary, size_t binaryLen, + ece_base64url_encode_policy_t paddingPolicy, char* base64, + size_t base64Len); + +/*! + * Decodes a Base64url-encoded (RFC 4648) string. + * + * \param base64[in] The encoded string. + * \param base64Len[in] The length of the encoded string. + * \param paddingPolicy[in] The policy for handling "=" padding in the encoded + * input. + * \param binary[in] An empty array to hold the decoded result. May be + * `NULL` if `binaryLen` is 0. + * \param binaryLen[in] The length of the empty `binary` array. On success, + * `binary[0..binaryLen]` contains the result. + * + * \return The actual decoded length. If `binaryLen` is 0, + * returns the length of the array + * required to hold the result. If `base64` contains + * invalid characters, or `binaryLen` is not large + * enough to hold the full result, returns 0. + */ +size_t +ece_base64url_decode(const char* base64, size_t base64Len, + ece_base64url_decode_policy_t paddingPolicy, + uint8_t* binary, size_t binaryLen); + +#ifdef __cplusplus +} +#endif +#endif /* ECE_H */ diff --git a/mobile/ios/ThirdParty/ecec/include/ece/keys.h b/mobile/ios/ThirdParty/ecec/include/ece/keys.h new file mode 100644 index 0000000000..92046e22d4 --- /dev/null +++ b/mobile/ios/ThirdParty/ecec/include/ece/keys.h @@ -0,0 +1,91 @@ +#ifndef ECE_KEYS_H +#define ECE_KEYS_H +#ifdef __cplusplus +extern "C" { +#endif + +#include + +#define ECE_AES_KEY_LENGTH 16 +#define ECE_NONCE_LENGTH 12 + +#define ECE_WEBPUSH_IKM_LENGTH 32 + +// HKDF info strings for the "aes128gcm" scheme. Note that the lengths include +// the NUL terminator. +#define ECE_WEBPUSH_AES128GCM_IKM_INFO_PREFIX "WebPush: info\0" +#define ECE_WEBPUSH_AES128GCM_IKM_INFO_PREFIX_LENGTH 14 +#define ECE_WEBPUSH_AES128GCM_IKM_INFO_LENGTH 144 + +#define ECE_AES128GCM_KEY_INFO "Content-Encoding: aes128gcm\0" +#define ECE_AES128GCM_KEY_INFO_LENGTH 28 +#define ECE_AES128GCM_NONCE_INFO "Content-Encoding: nonce\0" +#define ECE_AES128GCM_NONCE_INFO_LENGTH 24 + +// HKDF info strings for the "aesgcm" scheme. +#define ECE_WEBPUSH_AESGCM_IKM_INFO "Content-Encoding: auth\0" +#define ECE_WEBPUSH_AESGCM_IKM_INFO_LENGTH 23 +#define ECE_WEBPUSH_AESGCM_KEY_INFO_PREFIX "Content-Encoding: aesgcm\0P-256\0" +#define ECE_WEBPUSH_AESGCM_KEY_INFO_PREFIX_LENGTH 31 +#define ECE_WEBPUSH_AESGCM_KEY_INFO_LENGTH 165 +#define ECE_WEBPUSH_AESGCM_NONCE_INFO_PREFIX "Content-Encoding: nonce\0P-256\0" +#define ECE_WEBPUSH_AESGCM_NONCE_INFO_PREFIX_LENGTH 30 +#define ECE_WEBPUSH_AESGCM_NONCE_INFO_LENGTH 164 + +// Key derivation modes. +typedef enum ece_mode_e { + ECE_MODE_ENCRYPT, + ECE_MODE_DECRYPT, +} ece_mode_t; + +typedef int (*derive_key_and_nonce_t)(ece_mode_t mode, EC_KEY* localKey, + EC_KEY* remoteKey, + const uint8_t* authSecret, + size_t authSecretLen, const uint8_t* salt, + size_t saltLen, uint8_t* key, + uint8_t* nonce); + +// Generates a 96-bit IV for decryption, 48 bits of which are populated. +void +ece_generate_iv(const uint8_t* nonce, uint64_t counter, uint8_t* iv); + +// Inflates a raw ECDH private key into an OpenSSL `EC_KEY` containing a +// private and public key pair. Returns `NULL` on error. +EC_KEY* +ece_import_private_key(const uint8_t* rawKey, size_t rawKeyLen); + +// Inflates a raw ECDH public key into an `EC_KEY` containing a public key. +// Returns `NULL` on error. +EC_KEY* +ece_import_public_key(const uint8_t* rawKey, size_t rawKeyLen); + +// Derives the "aes128gcm" content encryption key and nonce. +int +ece_aes128gcm_derive_key_and_nonce(const uint8_t* salt, size_t saltLen, + const uint8_t* ikm, size_t ikmLen, + uint8_t* key, uint8_t* nonce); + +// Derives the "aes128gcm" decryption key and nonce given the receiver private +// key, sender public key, authentication secret, and sender salt. +int +ece_webpush_aes128gcm_derive_key_and_nonce(ece_mode_t mode, EC_KEY* localKey, + EC_KEY* remoteKey, + const uint8_t* authSecret, + size_t authSecretLen, + const uint8_t* salt, size_t saltLen, + uint8_t* key, uint8_t* nonce); + +// Derives the "aesgcm" decryption key and nonce given the receiver private key, +// sender public key, authentication secret, and sender salt. +int +ece_webpush_aesgcm_derive_key_and_nonce(ece_mode_t mode, EC_KEY* recvPrivKey, + EC_KEY* senderPubKey, + const uint8_t* authSecret, + size_t authSecretLen, + const uint8_t* salt, size_t saltLen, + uint8_t* key, uint8_t* nonce); + +#ifdef __cplusplus +} +#endif +#endif /* ECE_KEYS_H */ diff --git a/mobile/ios/ThirdParty/ecec/include/ece/trailer.h b/mobile/ios/ThirdParty/ecec/include/ece/trailer.h new file mode 100644 index 0000000000..25246d8a94 --- /dev/null +++ b/mobile/ios/ThirdParty/ecec/include/ece/trailer.h @@ -0,0 +1,31 @@ +#ifndef ECE_TRAILER_H +#define ECE_TRAILER_H +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include +#include + +typedef bool (*needs_trailer_t)(uint32_t rs, size_t ciphertextLen); + +// Adjusts the aesgcm record size to account for the authentication tag. +// aesgcm includes the size of the padding delimiter, but not the tag. +uint32_t +ece_aesgcm_rs(uint32_t rs); + +// Indicates if an "aesgcm" ciphertext is a multiple of the record size, and +// needs a padding-only trailing block to prevent truncation attacks. +bool +ece_aesgcm_needs_trailer(uint32_t rs, size_t ciphertextLen); + +// Provided for completeness, but always returns false because "aes128gcm" uses +// a padding scheme that doesn't need a trailer. +bool +ece_aes128gcm_needs_trailer(uint32_t rs, size_t ciphertextLen); + +#ifdef __cplusplus +} +#endif +#endif /* ECE_TRAILER_H */ diff --git a/mobile/ios/ThirdParty/ecec/src/base64url.c b/mobile/ios/ThirdParty/ecec/src/base64url.c new file mode 100644 index 0000000000..6643994cc4 --- /dev/null +++ b/mobile/ios/ThirdParty/ecec/src/base64url.c @@ -0,0 +1,289 @@ +#include "ece.h" + +// This file implements Base64url encoding and decoding per RFC 4648. Originally +// implemented in https://bugzilla.mozilla.org/show_bug.cgi?id=1256488 and +// https://bugzilla.mozilla.org/show_bug.cgi?id=1205137. + +#include +#include + +#define ECE_BASE64URL_INVALID_CHAR 64 +#define ECE_BASE64URL_INVALID_PADDING 3 + +// Maps an index to a character in the Base64url alphabet. +static const char ece_base64url_encode_table[] = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"; + +// Maps a character in the Base64url alphabet to its index, per RFC 4648, +// Table 2. Invalid characters map to 64. +static const uint8_t ece_base64url_decode_table[] = { + 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, + 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, + 64, 64, 64, 64, 64, 64, 64, 62, 64, 64, 52, 53, 54, 55, 56, 57, 58, 59, 60, + 61, 64, 64, 64, 64, 64, 64, 64, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, + 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 64, 64, 64, 64, + 63, 64, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, + 43, 44, 45, 46, 47, 48, 49, 50, 51, 64, 64, 64, 64, +}; + +// Returns the size of the buffer required to hold the Base64url output, +// or 0 if `binaryLen` is too large. +static inline size_t +ece_base64url_base64_length(size_t binaryLen) { + if (binaryLen / 3 > SIZE_MAX / 4) { + return 0; + } + // Base64 expands each 3-byte quantum to 4 bytes. The final quantum can be + // 2 or 3 bytes. + size_t baseLen = (binaryLen / 3) * 4; + size_t finalLen = 0; + switch (binaryLen % 3) { + case 1: + finalLen = 2; + break; + + case 2: + finalLen = 3; + break; + } + if (finalLen > SIZE_MAX - baseLen) { + return 0; + } + return baseLen + finalLen; +} + +// Encodes a `binary` quantum into `base64`, and returns the number of bytes +// written. A 3-byte quantum encodes to 4 bytes, a 2-byte quantum encodes to +// 3 bytes, and a 1-byte quantum encodes to 2 bytes. +static inline int +ece_base64url_encode_quantum(const uint8_t* binary, size_t binaryLen, + char* base64) { + assert(binaryLen <= 3); + + uint32_t quantum = 0; + for (size_t i = 0; i < binaryLen; i++) { + quantum <<= 8; + quantum |= (uint32_t) binary[i]; + } + + switch (binaryLen) { + case 1: + base64[0] = ece_base64url_encode_table[(quantum >> 2) & 0x3f]; + base64[1] = ece_base64url_encode_table[(quantum << 4) & 0x3f]; + return 2; + + case 2: + base64[0] = ece_base64url_encode_table[(quantum >> 10) & 0x3f]; + base64[1] = ece_base64url_encode_table[(quantum >> 4) & 0x3f]; + base64[2] = ece_base64url_encode_table[(quantum << 2) & 0x3f]; + return 3; + + case 3: + base64[0] = ece_base64url_encode_table[(quantum >> 18) & 0x3f]; + base64[1] = ece_base64url_encode_table[(quantum >> 12) & 0x3f]; + base64[2] = ece_base64url_encode_table[(quantum >> 6) & 0x3f]; + base64[3] = ece_base64url_encode_table[quantum & 0x3f]; + return 4; + } + + return 0; +} + +// Returns the number of trailing `=` characters to remove from the end of +// `base64`, based on the `paddingPolicy`. Valid values are 0, 1, or 2; +// 3 means the input is invalid. +static inline size_t +ece_base64url_decode_pad_length(const char* base64, size_t base64Len, + ece_base64url_decode_policy_t paddingPolicy) { + // Determine whether to check for and ignore trailing padding. + bool maybePadded = false; + switch (paddingPolicy) { + case ECE_BASE64URL_REQUIRE_PADDING: + if (base64Len % 4) { + // Padded input length must be a multiple of 4. + return ECE_BASE64URL_INVALID_PADDING; + } + maybePadded = true; + break; + + case ECE_BASE64URL_IGNORE_PADDING: + // Check for padding only if the length is a multiple of 4. + maybePadded = !(base64Len % 4); + break; + + // If we're expecting unpadded input, no need for additional checks. + // `=` isn't in the decode table, so padded strings will fail to decode. + default: + // Invalid decode padding policy. + assert(false); + case ECE_BASE64URL_REJECT_PADDING: + break; + } + if (maybePadded && base64[base64Len - 1] == '=') { + base64Len--; + if (base64[base64Len - 1] == '=') { + return 2; + } + return 1; + } + return 0; +} + +// Returns the size of the buffer required to hold the binary output, or 0 if +// `base64Len` is truncated. +static inline size_t +ece_base64url_binary_length(size_t base64Len) { + size_t requiredBinaryLen = (base64Len / 4) * 3; + switch (base64Len % 4) { + case 1: + return 0; + + case 2: + requiredBinaryLen++; + break; + + case 3: + requiredBinaryLen += 2; + break; + } + return requiredBinaryLen; +} + +// Converts a Base64url character `c` to its index. +static inline uint8_t +ece_base64url_decode_byte(char b) { + return (b & ~0x7f) ? ECE_BASE64URL_INVALID_CHAR + : ece_base64url_decode_table[b & 0x7f]; +} + +// Decodes a `base64` encoded quantum into `binary`. A 4-byte quantum decodes to +// 3 bytes, a 3-byte quantum decodes to 2 bytes, and a 2-byte quantum decodes to +// 1 byte. +static inline bool +ece_base64url_decode_quantum(const char* base64, size_t base64Len, + uint8_t* binary) { + assert(base64Len <= 4); + + uint32_t quantum = 0; + for (size_t i = 0; i < base64Len; i++) { + uint8_t b = ece_base64url_decode_byte(base64[i]); + if (b == ECE_BASE64URL_INVALID_CHAR) { + return false; + } + quantum <<= 6; + quantum |= (uint32_t) b; + } + + switch (base64Len) { + case 0: + return true; + + case 2: + binary[0] = (quantum >> 4) & 0xff; + return true; + + case 3: + binary[0] = (quantum >> 10) & 0xff; + binary[1] = (quantum >> 2) & 0xff; + return true; + + case 4: + binary[0] = (quantum >> 16) & 0xff; + binary[1] = (quantum >> 8) & 0xff; + binary[2] = quantum & 0xff; + return true; + } + + return false; +} + +size_t +ece_base64url_encode(const void* binary, size_t binaryLen, + ece_base64url_encode_policy_t paddingPolicy, char* base64, + size_t base64Len) { + // Don't encode empty strings. + if (!binaryLen) { + return 0; + } + + // Ensure we have enough room to hold the output. + size_t requiredBase64Len = ece_base64url_base64_length(binaryLen); + if (!requiredBase64Len) { + return 0; + } + size_t padLen = 0; + if (paddingPolicy == ECE_BASE64URL_INCLUDE_PADDING) { + switch (requiredBase64Len % 4) { + case 2: + padLen = 2; + break; + + case 3: + padLen = 1; + break; + } + if (padLen > SIZE_MAX - requiredBase64Len) { + return 0; + } + requiredBase64Len += padLen; + } + + if (base64Len) { + if (base64Len < requiredBase64Len) { + return 0; + } + const uint8_t* input = binary; + for (; binaryLen >= 3; binaryLen -= 3) { + base64 += ece_base64url_encode_quantum(input, 3, base64); + input += 3; + } + base64 += ece_base64url_encode_quantum(input, binaryLen, base64); + if (paddingPolicy == ECE_BASE64URL_INCLUDE_PADDING) { + while (padLen) { + *base64++ = '='; + padLen--; + } + } else { + assert(paddingPolicy == ECE_BASE64URL_OMIT_PADDING); + } + } + + return requiredBase64Len; +} + +size_t +ece_base64url_decode(const char* base64, size_t base64Len, + ece_base64url_decode_policy_t paddingPolicy, + uint8_t* binary, size_t binaryLen) { + // Don't decode empty strings. + if (!base64Len) { + return 0; + } + + // Ensure we have enough room to hold the output. + size_t padLen = + ece_base64url_decode_pad_length(base64, base64Len, paddingPolicy); + if (padLen == ECE_BASE64URL_INVALID_PADDING) { + return 0; + } + base64Len -= padLen; + size_t requiredBinaryLen = ece_base64url_binary_length(base64Len); + + if (binaryLen) { + if (binaryLen < requiredBinaryLen) { + return 0; + } + for (; base64Len >= 4; base64Len -= 4) { + if (!ece_base64url_decode_quantum(base64, 4, binary)) { + return 0; + } + base64 += 4; + binary += 3; + } + if (!ece_base64url_decode_quantum(base64, base64Len, binary)) { + return 0; + } + } + + return requiredBinaryLen; +} diff --git a/mobile/ios/ThirdParty/ecec/src/decrypt.c b/mobile/ios/ThirdParty/ecec/src/decrypt.c new file mode 100644 index 0000000000..32425b7dac --- /dev/null +++ b/mobile/ios/ThirdParty/ecec/src/decrypt.c @@ -0,0 +1,425 @@ +#include "ece.h" +#include "ece/keys.h" +#include "ece/trailer.h" + +#include +#include + +#include +#include + +typedef int (*unpad_t)(uint8_t* block, bool lastRecord, size_t* blockLen); + +// Calculates the maximum plaintext length, including room for the padding +// delimiter and padding. +static inline size_t +ece_plaintext_max_length(uint32_t rs, size_t padSize, size_t ciphertextLen) { + assert(padSize <= 2); + size_t overhead = padSize + ECE_TAG_LENGTH; + if (rs <= overhead) { + return 0; + } + size_t numRecords = ciphertextLen / rs; + if (ciphertextLen % rs) { + // If the ciphertext length doesn't fall on a record boundary, we have + // a smaller final record. + numRecords++; + } + if (numRecords > ciphertextLen / ECE_TAG_LENGTH) { + // Each record includes a trailing auth tag. If the number of records + // exceeds the number of tags, the ciphertext is truncated. + return 0; + } + return ciphertextLen - (ECE_TAG_LENGTH * numRecords); +} + +// Extracts an unsigned 16-bit integer in network byte order. +static inline uint16_t +ece_read_uint16_be(const uint8_t* bytes) { + uint16_t value = (uint16_t) bytes[1]; + value |= bytes[0] << 8; + return value; +} + +// Converts an encrypted record to a decrypted block. +static int +ece_decrypt_record(EVP_CIPHER_CTX* ctx, const uint8_t* key, const uint8_t* iv, + const uint8_t* record, size_t recordLen, uint8_t* block) { + int chunkLen = -1; + + if (EVP_DecryptInit_ex(ctx, EVP_aes_128_gcm(), NULL, key, iv) != 1) { + return ECE_ERROR_DECRYPT; + } + + assert(recordLen > ECE_TAG_LENGTH); + size_t blockLen = recordLen - ECE_TAG_LENGTH; + + // The authentication tag is included at the end of the encrypted record. + uint8_t tag[ECE_TAG_LENGTH]; + memcpy(tag, &record[blockLen], ECE_TAG_LENGTH); + if (EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_TAG, ECE_TAG_LENGTH, tag) != + 1) { + return ECE_ERROR_DECRYPT; + } + + if (blockLen > INT_MAX || + EVP_DecryptUpdate(ctx, block, &chunkLen, record, (int) blockLen) != 1) { + return ECE_ERROR_DECRYPT; + } + + // Since we're using a stream cipher, finalization shouldn't write out any + // bytes. + assert(EVP_CIPHER_CTX_block_size(ctx) == 1); + if (EVP_DecryptFinal_ex(ctx, NULL, &chunkLen) != 1) { + return ECE_ERROR_DECRYPT; + } + + if (EVP_CIPHER_CTX_reset(ctx) != 1) { + return ECE_ERROR_DECRYPT; + } + + return ECE_OK; +} + +static int +ece_decrypt_records(const uint8_t* key, const uint8_t* nonce, uint32_t rs, + size_t padSize, const uint8_t* ciphertext, + size_t ciphertextLen, unpad_t unpad, uint8_t* plaintext, + size_t* plaintextLen) { + int err = ECE_OK; + + EVP_CIPHER_CTX* ctx = NULL; + + // Make sure the plaintext array is large enough to hold the full plaintext. + size_t maxPlaintextLen = ece_plaintext_max_length(rs, padSize, ciphertextLen); + if (!maxPlaintextLen) { + err = ECE_ERROR_DECRYPT; + goto end; + } + if (*plaintextLen < maxPlaintextLen) { + err = ECE_ERROR_OUT_OF_MEMORY; + goto end; + } + + ctx = EVP_CIPHER_CTX_new(); + if (!ctx) { + err = ECE_ERROR_OUT_OF_MEMORY; + goto end; + } + + // The offset at which to start reading the ciphertext. + size_t ciphertextStart = 0; + + // The offset at which to start writing the plaintext. + size_t plaintextStart = 0; + + for (size_t counter = 0; ciphertextStart < ciphertextLen; counter++) { + size_t ciphertextEnd; + if (rs > ciphertextLen - ciphertextStart) { + // This check is equivalent to `ciphertextStart + rs > ciphertextLen`; + // it's written this way to avoid an integer overflow. + ciphertextEnd = ciphertextLen; + } else { + ciphertextEnd = ciphertextStart + rs; + } + + assert(ciphertextEnd > ciphertextStart); + + // The full length of the encrypted record. + size_t recordLen = ciphertextEnd - ciphertextStart; + if (recordLen <= ECE_TAG_LENGTH) { + err = ECE_ERROR_SHORT_BLOCK; + goto end; + } + + // Generate the IV for this record using the nonce. + uint8_t iv[ECE_NONCE_LENGTH]; + ece_generate_iv(nonce, counter, iv); + + // Decrypt the record. + err = ece_decrypt_record(ctx, key, iv, &ciphertext[ciphertextStart], + recordLen, &plaintext[plaintextStart]); + if (err) { + goto end; + } + + // `unpad` sets `blockLen` to the actual plaintext block length, without + // the padding delimiter and padding. + bool lastRecord = ciphertextEnd >= ciphertextLen; + size_t blockLen = recordLen - ECE_TAG_LENGTH; + if (blockLen < padSize) { + err = ECE_ERROR_DECRYPT_PADDING; + goto end; + } + err = unpad(&plaintext[plaintextStart], lastRecord, &blockLen); + if (err) { + goto end; + } + + ciphertextStart = ciphertextEnd; + plaintextStart += blockLen; + } + + // Finally, set the actual plaintext length. + *plaintextLen = plaintextStart; + +end: + EVP_CIPHER_CTX_free(ctx); + return err; +} + +// A generic decryption function shared by "aesgcm" and "aes128gcm". +// `deriveKeyAndNonce` and `unpad` are function pointers that change based on +// the scheme. +static int +ece_webpush_decrypt(const uint8_t* rawRecvPrivKey, size_t rawRecvPrivKeyLen, + const uint8_t* authSecret, size_t authSecretLen, + const uint8_t* salt, size_t saltLen, + const uint8_t* rawSenderPubKey, size_t rawSenderPubKeyLen, + uint32_t rs, size_t padSize, const uint8_t* ciphertext, + size_t ciphertextLen, needs_trailer_t needsTrailer, + derive_key_and_nonce_t deriveKeyAndNonce, unpad_t unpad, + uint8_t* plaintext, size_t* plaintextLen) { + int err = ECE_OK; + + EC_KEY* recvPrivKey = NULL; + EC_KEY* senderPubKey = NULL; + + if (authSecretLen != ECE_WEBPUSH_AUTH_SECRET_LENGTH) { + err = ECE_ERROR_INVALID_AUTH_SECRET; + goto end; + } + if (saltLen != ECE_SALT_LENGTH) { + err = ECE_ERROR_INVALID_SALT; + goto end; + } + if (!ciphertextLen) { + err = ECE_ERROR_ZERO_CIPHERTEXT; + goto end; + } + if (needsTrailer(rs, ciphertextLen)) { + // If we're missing a trailing block, the ciphertext is truncated. This only + // applies to "aesgcm". + err = ECE_ERROR_DECRYPT_TRUNCATED; + goto end; + } + + recvPrivKey = ece_import_private_key(rawRecvPrivKey, rawRecvPrivKeyLen); + if (!recvPrivKey) { + err = ECE_ERROR_INVALID_PRIVATE_KEY; + goto end; + } + senderPubKey = ece_import_public_key(rawSenderPubKey, rawSenderPubKeyLen); + if (!senderPubKey) { + err = ECE_ERROR_INVALID_PUBLIC_KEY; + goto end; + } + + uint8_t key[ECE_AES_KEY_LENGTH]; + uint8_t nonce[ECE_NONCE_LENGTH]; + err = deriveKeyAndNonce(ECE_MODE_DECRYPT, recvPrivKey, senderPubKey, + authSecret, authSecretLen, salt, saltLen, key, nonce); + if (err) { + goto end; + } + + err = ece_decrypt_records(key, nonce, rs, padSize, ciphertext, ciphertextLen, + unpad, plaintext, plaintextLen); + +end: + EC_KEY_free(recvPrivKey); + EC_KEY_free(senderPubKey); + return err; +} + +// Removes padding from a decrypted "aesgcm" block. +static int +ece_aesgcm_unpad(uint8_t* block, bool lastRecord, size_t* blockLen) { + ECE_UNUSED(lastRecord); + + assert(*blockLen >= ECE_AESGCM_PAD_SIZE); + + uint16_t padLen = ece_read_uint16_be(block); + if (padLen > *blockLen - ECE_AESGCM_PAD_SIZE) { + return ECE_ERROR_DECRYPT_PADDING; + } + size_t plaintextStart = ECE_AESGCM_PAD_SIZE + padLen; + + for (size_t i = ECE_AESGCM_PAD_SIZE; i < plaintextStart; i++) { + if (block[i]) { + // All padding bytes must be zero. + return ECE_ERROR_DECRYPT_PADDING; + } + } + + // Move the unpadded plaintext to the start of the block. + *blockLen -= plaintextStart; + memmove(block, &block[plaintextStart], *blockLen); + return ECE_OK; +} + +// Removes padding from a decrypted "aes128gcm" block. +static int +ece_aes128gcm_unpad(uint8_t* block, bool lastRecord, size_t* blockLen) { + // Remove trailing padding. + while (*blockLen > 0) { + (*blockLen)--; + if (!block[*blockLen]) { + continue; + } + uint8_t padDelim = lastRecord ? 2 : 1; + if (block[*blockLen] != padDelim) { + // Last record needs to start padding with a 2; preceding records need + // to start padding with a 1. + return ECE_ERROR_DECRYPT_PADDING; + } + return ECE_OK; + } + + // All zero plaintext. + return ECE_ERROR_ZERO_PLAINTEXT; +} + +int +ece_webpush_generate_keys(uint8_t* rawRecvPrivKey, size_t rawRecvPrivKeyLen, + uint8_t* rawRecvPubKey, size_t rawRecvPubKeyLen, + uint8_t* authSecret, size_t authSecretLen) { + int err = ECE_OK; + EC_KEY* subKey = NULL; + + // Generate a public-private ECDH key pair for the push subscription. + subKey = EC_KEY_new_by_curve_name(NID_X9_62_prime256v1); + if (!subKey) { + err = ECE_ERROR_OUT_OF_MEMORY; + goto end; + } + if (EC_KEY_generate_key(subKey) != 1) { + err = ECE_ERROR_GENERATE_KEYS; + goto end; + } + + if (!EC_KEY_priv2oct(subKey, rawRecvPrivKey, rawRecvPrivKeyLen)) { + err = ECE_ERROR_INVALID_PRIVATE_KEY; + goto end; + } + const EC_GROUP* subGrp = EC_KEY_get0_group(subKey); + const EC_POINT* rawSubPubKeyPt = EC_KEY_get0_public_key(subKey); + if (!EC_POINT_point2oct(subGrp, rawSubPubKeyPt, POINT_CONVERSION_UNCOMPRESSED, + rawRecvPubKey, rawRecvPubKeyLen, NULL)) { + err = ECE_ERROR_INVALID_PUBLIC_KEY; + goto end; + } + + if (authSecretLen > INT_MAX || + RAND_bytes(authSecret, (int) authSecretLen) != 1) { + err = ECE_ERROR_INVALID_AUTH_SECRET; + goto end; + } + +end: + EC_KEY_free(subKey); + return err; +} + +size_t +ece_aes128gcm_plaintext_max_length(const uint8_t* payload, size_t payloadLen) { + const uint8_t* salt; + size_t saltLen; + const uint8_t* keyId; + size_t keyIdLen; + uint32_t rs; + const uint8_t* ciphertext; + size_t ciphertextLen; + int err = ece_aes128gcm_payload_extract_params( + payload, payloadLen, &salt, &saltLen, &keyId, &keyIdLen, &rs, &ciphertext, + &ciphertextLen); + if (err) { + return 0; + } + return ece_plaintext_max_length(rs, ECE_AES128GCM_PAD_SIZE, ciphertextLen); +} + +size_t +ece_aesgcm_plaintext_max_length(uint32_t rs, size_t ciphertextLen) { + rs = ece_aesgcm_rs(rs); + if (!rs) { + return 0; + } + return ece_plaintext_max_length(rs, ECE_AESGCM_PAD_SIZE, ciphertextLen); +} + +int +ece_aes128gcm_decrypt(const uint8_t* ikm, size_t ikmLen, const uint8_t* payload, + size_t payloadLen, uint8_t* plaintext, + size_t* plaintextLen) { + const uint8_t* salt; + size_t saltLen; + const uint8_t* keyId; + size_t keyIdLen; + uint32_t rs; + const uint8_t* ciphertext; + size_t ciphertextLen; + int err = ece_aes128gcm_payload_extract_params( + payload, payloadLen, &salt, &saltLen, &keyId, &keyIdLen, &rs, &ciphertext, + &ciphertextLen); + if (err) { + return err; + } + uint8_t key[ECE_AES_KEY_LENGTH]; + uint8_t nonce[ECE_NONCE_LENGTH]; + err = + ece_aes128gcm_derive_key_and_nonce(salt, saltLen, ikm, ikmLen, key, nonce); + if (err) { + return err; + } + return ece_decrypt_records(key, nonce, rs, ECE_AES128GCM_PAD_SIZE, ciphertext, + ciphertextLen, &ece_aes128gcm_unpad, plaintext, + plaintextLen); +} + +int +ece_webpush_aes128gcm_decrypt(const uint8_t* rawRecvPrivKey, + size_t rawRecvPrivKeyLen, + const uint8_t* authSecret, size_t authSecretLen, + const uint8_t* payload, size_t payloadLen, + uint8_t* plaintext, size_t* plaintextLen) { + const uint8_t* salt; + size_t saltLen; + const uint8_t* rawSenderPubKey; + size_t rawSenderPubKeyLen; + uint32_t rs; + const uint8_t* ciphertext; + size_t ciphertextLen; + int err = ece_aes128gcm_payload_extract_params( + payload, payloadLen, &salt, &saltLen, &rawSenderPubKey, &rawSenderPubKeyLen, + &rs, &ciphertext, &ciphertextLen); + if (err) { + return err; + } + return ece_webpush_decrypt( + rawRecvPrivKey, rawRecvPrivKeyLen, authSecret, authSecretLen, salt, saltLen, + rawSenderPubKey, rawSenderPubKeyLen, rs, ECE_AES128GCM_PAD_SIZE, ciphertext, + ciphertextLen, &ece_aes128gcm_needs_trailer, + &ece_webpush_aes128gcm_derive_key_and_nonce, &ece_aes128gcm_unpad, + plaintext, plaintextLen); +} + +int +ece_webpush_aesgcm_decrypt(const uint8_t* rawRecvPrivKey, + size_t rawRecvPrivKeyLen, const uint8_t* authSecret, + size_t authSecretLen, const uint8_t* salt, + size_t saltLen, const uint8_t* rawSenderPubKey, + size_t rawSenderPubKeyLen, uint32_t rs, + const uint8_t* ciphertext, size_t ciphertextLen, + uint8_t* plaintext, size_t* plaintextLen) { + rs = ece_aesgcm_rs(rs); + if (!rs) { + return 0; + } + return ece_webpush_decrypt( + rawRecvPrivKey, rawRecvPrivKeyLen, authSecret, authSecretLen, salt, saltLen, + rawSenderPubKey, rawSenderPubKeyLen, rs, ECE_AESGCM_PAD_SIZE, ciphertext, + ciphertextLen, &ece_aesgcm_needs_trailer, + &ece_webpush_aesgcm_derive_key_and_nonce, &ece_aesgcm_unpad, plaintext, + plaintextLen); +} diff --git a/mobile/ios/ThirdParty/ecec/src/encrypt.c b/mobile/ios/ThirdParty/ecec/src/encrypt.c new file mode 100644 index 0000000000..8428931b21 --- /dev/null +++ b/mobile/ios/ThirdParty/ecec/src/encrypt.c @@ -0,0 +1,617 @@ +#include "ece.h" +#include "ece/keys.h" +#include "ece/trailer.h" + +#include +#include +#include + +#include +#include +#include + +typedef size_t (*min_block_pad_length_t)(size_t padLen, size_t maxBlockLen); + +typedef int (*encrypt_block_t)(EVP_CIPHER_CTX* ctx, + const uint8_t* blockPlaintext, + size_t blockPlaintextLen, size_t blockPadLen, + bool lastRecord, uint8_t* record); + +static const uint8_t pad = 0; + +// Writes an unsigned 32-bit integer in network byte order. +static inline void +ece_write_uint32_be(uint8_t* bytes, uint32_t value) { + bytes[0] = (value >> 24) & 0xff; + bytes[1] = (value >> 16) & 0xff; + bytes[2] = (value >> 8) & 0xff; + bytes[3] = value & 0xff; +} + +// Writes an unsigned 16-bit integer in network byte order. +static inline void +ece_write_uint16_be(uint8_t* bytes, uint16_t value) { + bytes[0] = (value >> 8) & 0xff; + bytes[1] = value & 0xff; +} + +// Calculates the padding so that the block contains at least one plaintext +// byte. +static inline size_t +ece_min_block_pad_length(size_t padLen, size_t maxBlockLen) { + assert(maxBlockLen >= 1); + size_t blockPadLen = maxBlockLen - 1; + if (padLen && !blockPadLen) { + // If `maxBlockLen` is 1, we can only include 1 byte of data, so write + // the padding first. + blockPadLen++; + } + return blockPadLen > padLen ? padLen : blockPadLen; +} + +// Calculates the padding for an "aesgcm" block. We still want one plaintext +// byte per block, but the padding length must fit into a `uint16_t`. +static size_t +ece_aesgcm_min_block_pad_length(size_t padLen, size_t maxBlockLen) { + size_t blockPadLen = ece_min_block_pad_length(padLen, maxBlockLen); + return blockPadLen > UINT16_MAX ? UINT16_MAX : blockPadLen; +} + +// Calculates the maximum length of an encrypted ciphertext. This does not +// account for the "aes128gcm" header length. +static inline size_t +ece_ciphertext_max_length(uint32_t rs, size_t padSize, size_t padLen, + size_t plaintextLen, needs_trailer_t needsTrailer) { + // The per-record overhead for the padding delimiter and authentication tag. + // 17 for "aes128gcm", 18 for "aesgcm". + assert(padSize <= 2); + size_t overhead = padSize + ECE_TAG_LENGTH; + if (rs <= overhead) { + return 0; + } + if (padLen > SIZE_MAX - plaintextLen) { + return 0; + } + + // The total length of the data to encrypt, including the plaintext and + // padding. + size_t dataLen = plaintextLen + padLen; + + // The maximum length of data to include in each record, excluding the + // padding delimiter and authentication tag. + size_t maxBlockLen = rs - overhead; + + // The total number of encrypted records. + assert(maxBlockLen >= 1); + size_t numRecords = dataLen / maxBlockLen; + if (plaintextLen % rs || needsTrailer(rs, plaintextLen)) { + // If the plaintext length doesn't fall on a record boundary, or if + // we need to write an empty trailing record, allocate space to hold + // an extra padding delimiter and authentication tag. + numRecords++; + } + if (numRecords > (SIZE_MAX - dataLen) / overhead) { + return 0; + } + + return dataLen + (overhead * numRecords); +} + +// Encrypts an "aes128gcm" block into `record`. +static int +ece_aes128gcm_encrypt_block(EVP_CIPHER_CTX* ctx, const uint8_t* blockPlaintext, + size_t blockPlaintextLen, size_t blockPadLen, + bool lastRecord, uint8_t* record) { + int chunkLen = -1; + + // The plaintext block precedes the padding. + if (blockPlaintextLen > INT_MAX || + EVP_EncryptUpdate(ctx, record, &chunkLen, blockPlaintext, + (int) blockPlaintextLen) != 1) { + return ECE_ERROR_ENCRYPT; + } + + // The padding block comprises the delimiter, followed by zeros up to the end + // of the block. + uint8_t padDelim = lastRecord ? 2 : 1; + if (EVP_EncryptUpdate(ctx, &record[blockPlaintextLen], &chunkLen, &padDelim, + ECE_AES128GCM_PAD_SIZE) != 1) { + return ECE_ERROR_ENCRYPT; + } + + for (size_t i = 0; i < blockPadLen; i++) { + if (EVP_EncryptUpdate( + ctx, &record[blockPlaintextLen + ECE_AES128GCM_PAD_SIZE + i], + &chunkLen, &pad, 1) != 1) { + return ECE_ERROR_ENCRYPT; + } + } + + return ECE_OK; +} + +// Encrypts an "aesgcm" block into `record`. +static int +ece_aesgcm_encrypt_block(EVP_CIPHER_CTX* ctx, const uint8_t* blockPlaintext, + size_t plaintextLen, size_t blockPadLen, + bool lastRecord, uint8_t* record) { + ECE_UNUSED(lastRecord); + + int chunkLen = -1; + + // The padding block comprises the padding length as a 16-bit integer, + // followed by that many zeros. We checked that the length fits into a + // `uint16_t` in `ece_aesgcm_min_block_pad_length`, so this cast is safe. + uint8_t padDelim[ECE_AESGCM_PAD_SIZE]; + ece_write_uint16_be(padDelim, (uint16_t) blockPadLen); + if (EVP_EncryptUpdate(ctx, record, &chunkLen, padDelim, + ECE_AESGCM_PAD_SIZE) != 1) { + return ECE_ERROR_ENCRYPT; + } + + for (size_t i = 0; i < blockPadLen; i++) { + if (EVP_EncryptUpdate(ctx, &record[ECE_AESGCM_PAD_SIZE + i], &chunkLen, + &pad, 1) != 1) { + return ECE_ERROR_ENCRYPT; + } + } + + // The plaintext block follows the padding. + if (plaintextLen > INT_MAX || + EVP_EncryptUpdate(ctx, &record[ECE_AESGCM_PAD_SIZE + blockPadLen], + &chunkLen, blockPlaintext, (int) plaintextLen) != 1) { + return ECE_ERROR_ENCRYPT; + } + + return ECE_OK; +} + +// A generic encryption function shared by "aesgcm" and "aes128gcm". +// `deriveKeyAndNonce`, `minBlockPadLen`, `encryptBlock`, and `needsTrailer` +// change depending on the scheme. +static int +ece_webpush_encrypt_plaintext( + EC_KEY* senderPrivKey, EC_KEY* recvPubKey, const uint8_t* authSecret, + size_t authSecretLen, const uint8_t* salt, size_t saltLen, uint32_t rs, + size_t padSize, size_t padLen, const uint8_t* plaintext, size_t plaintextLen, + derive_key_and_nonce_t deriveKeyAndNonce, + min_block_pad_length_t minBlockPadLen, encrypt_block_t encryptBlock, + needs_trailer_t needsTrailer, uint8_t* ciphertext, size_t* ciphertextLen) { + + int err = ECE_OK; + + EVP_CIPHER_CTX* ctx = NULL; + + if (authSecretLen != ECE_WEBPUSH_AUTH_SECRET_LENGTH) { + err = ECE_ERROR_INVALID_AUTH_SECRET; + goto end; + } + if (saltLen != ECE_SALT_LENGTH) { + err = ECE_ERROR_INVALID_SALT; + goto end; + } + if (!plaintextLen) { + err = ECE_ERROR_ZERO_PLAINTEXT; + goto end; + } + + // Make sure the ciphertext buffer is large enough to hold the ciphertext. + size_t maxCiphertextLen = + ece_ciphertext_max_length(rs, padSize, padLen, plaintextLen, needsTrailer); + if (!maxCiphertextLen) { + err = ECE_ERROR_INVALID_RS; + goto end; + } + if (*ciphertextLen < maxCiphertextLen) { + err = ECE_ERROR_OUT_OF_MEMORY; + goto end; + } + + ctx = EVP_CIPHER_CTX_new(); + if (!ctx) { + err = ECE_ERROR_OUT_OF_MEMORY; + goto end; + } + + uint8_t key[ECE_AES_KEY_LENGTH]; + uint8_t nonce[ECE_NONCE_LENGTH]; + err = deriveKeyAndNonce(ECE_MODE_ENCRYPT, senderPrivKey, recvPubKey, + authSecret, ECE_WEBPUSH_AUTH_SECRET_LENGTH, salt, + ECE_SALT_LENGTH, key, nonce); + if (err) { + goto end; + } + + assert(padSize <= 2); + size_t overhead = padSize + ECE_TAG_LENGTH; + + // The maximum amount of plaintext and padding that will fit into a full + // block. The last block can be smaller. + assert(rs > overhead); + size_t maxBlockLen = rs - overhead; + + // The offset at which to start reading the plaintext. + size_t plaintextStart = 0; + + // The offset at which to start writing the ciphertext. + size_t ciphertextStart = 0; + + // The record sequence number, used to generate the IV. + size_t counter = 0; + + bool lastRecord = false; + while (!lastRecord) { + size_t blockPadLen = minBlockPadLen(padLen, maxBlockLen); + assert(blockPadLen <= padLen); + padLen -= blockPadLen; + + // Fill the rest of the block with plaintext. + assert(blockPadLen <= maxBlockLen); + size_t maxBlockPlaintextLen = maxBlockLen - blockPadLen; + size_t plaintextEnd; + if (maxBlockPlaintextLen >= plaintextLen - plaintextStart) { + // Equivalent to `plaintextStart + maxBlockPlaintextLen >= plaintextLen` + // without overflow. + plaintextEnd = plaintextLen; + } else { + plaintextEnd = plaintextStart + maxBlockPlaintextLen; + } + + // The length of the plaintext. + assert(plaintextEnd >= plaintextStart); + size_t blockPlaintextLen = plaintextEnd - plaintextStart; + + // The length of the plaintext and padding. This should never overflow + // because `maxBlockPlaintextLen` accounts for `blockPadLen`. + assert(blockPlaintextLen <= maxBlockPlaintextLen); + size_t blockLen = blockPlaintextLen + blockPadLen; + + // The length of the full encrypted record, including the plaintext, + // padding, padding delimiter, and auth tag. This should never overflow + // because `maxBlockLen` accounts for `overhead`. + assert(blockLen <= maxBlockLen); + size_t recordLen = blockLen + overhead; + + size_t ciphertextEnd; + if (recordLen >= maxCiphertextLen - ciphertextStart) { + // Equivalent to `ciphertextStart + recordLen >= maxCiphertextLen` + // without overflow. + ciphertextEnd = maxCiphertextLen; + } else { + ciphertextEnd = ciphertextStart + recordLen; + } + + assert(ciphertextEnd > ciphertextStart); + + bool plaintextExhausted = plaintextEnd >= plaintextLen; + if (!padLen && plaintextExhausted && !needsTrailer(rs, ciphertextEnd)) { + // We've reached the last record when the padding and plaintext are + // exhausted, and we don't need to write an empty trailing record. + lastRecord = true; + } + + if (!lastRecord && blockLen < maxBlockLen) { + // We have padding left, but not enough plaintext to form a full record. + // Writing trailing padding-only records will still leak size information, + // so we force the caller to pick a smaller padding length. + err = ECE_ERROR_ENCRYPT_PADDING; + goto end; + } + + // Generate the IV for this record using the nonce. + uint8_t iv[ECE_NONCE_LENGTH]; + ece_generate_iv(nonce, counter, iv); + + if (EVP_EncryptInit_ex(ctx, EVP_aes_128_gcm(), NULL, key, iv) != 1) { + err = ECE_ERROR_ENCRYPT; + goto end; + } + + // Encrypt and pad the block. + err = encryptBlock(ctx, &plaintext[plaintextStart], blockPlaintextLen, + blockPadLen, lastRecord, &ciphertext[ciphertextStart]); + if (err) { + err = ECE_ERROR_ENCRYPT; + goto end; + } + + // OpenSSL requires us to finalize the encryption, but, since we're using a + // stream cipher, finalization shouldn't write out any bytes. + int chunkLen = -1; + assert(EVP_CIPHER_CTX_block_size(ctx) == 1); + if (EVP_EncryptFinal_ex(ctx, NULL, &chunkLen) != 1) { + err = ECE_ERROR_ENCRYPT; + goto end; + } + + // Append the authentication tag. + if (EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_GET_TAG, ECE_TAG_LENGTH, + &ciphertext[ciphertextEnd - ECE_TAG_LENGTH]) != 1) { + err = ECE_ERROR_ENCRYPT; + goto end; + } + + if (EVP_CIPHER_CTX_reset(ctx) != 1) { + err = ECE_ERROR_ENCRYPT; + goto end; + } + + plaintextStart = plaintextEnd; + ciphertextStart = ciphertextEnd; + counter++; + } + + // Finally, set the actual ciphertext length. + *ciphertextLen = ciphertextStart; + +end: + EVP_CIPHER_CTX_free(ctx); + return err; +} + +// Encrypts a Web Push message using the "aes128gcm" scheme. +static int +ece_webpush_aes128gcm_encrypt_plaintext( + EC_KEY* senderPrivKey, EC_KEY* recvPubKey, const uint8_t* authSecret, + size_t authSecretLen, const uint8_t* salt, size_t saltLen, uint32_t rs, + size_t padLen, const uint8_t* plaintext, size_t plaintextLen, + uint8_t* payload, size_t* payloadLen) { + + size_t headerLen = + ECE_AES128GCM_HEADER_LENGTH + ECE_WEBPUSH_PUBLIC_KEY_LENGTH; + if (*payloadLen < headerLen) { + return ECE_ERROR_OUT_OF_MEMORY; + } + + // Write the header. + memcpy(payload, salt, ECE_SALT_LENGTH); + ece_write_uint32_be(&payload[ECE_SALT_LENGTH], rs); + payload[ECE_SALT_LENGTH + 4] = ECE_WEBPUSH_PUBLIC_KEY_LENGTH; + if (!EC_POINT_point2oct( + EC_KEY_get0_group(senderPrivKey), EC_KEY_get0_public_key(senderPrivKey), + POINT_CONVERSION_UNCOMPRESSED, &payload[ECE_AES128GCM_HEADER_LENGTH], + ECE_WEBPUSH_PUBLIC_KEY_LENGTH, NULL)) { + return ECE_ERROR_ENCODE_PUBLIC_KEY; + } + + // Write the ciphertext. + size_t ciphertextLen = *payloadLen - headerLen; + int err = ece_webpush_encrypt_plaintext( + senderPrivKey, recvPubKey, authSecret, authSecretLen, salt, saltLen, rs, + ECE_AES128GCM_PAD_SIZE, padLen, plaintext, plaintextLen, + &ece_webpush_aes128gcm_derive_key_and_nonce, &ece_min_block_pad_length, + &ece_aes128gcm_encrypt_block, &ece_aes128gcm_needs_trailer, + &payload[headerLen], &ciphertextLen); + if (err) { + return err; + } + + *payloadLen = headerLen + ciphertextLen; + return ECE_OK; +} + +size_t +ece_aes128gcm_payload_max_length(uint32_t rs, size_t padLen, + size_t plaintextLen) { + size_t ciphertextLen = + ece_ciphertext_max_length(rs, ECE_AES128GCM_PAD_SIZE, padLen, plaintextLen, + &ece_aes128gcm_needs_trailer); + if (!ciphertextLen) { + return 0; + } + size_t maxHeaderLen = + ECE_AES128GCM_HEADER_LENGTH + ECE_AES128GCM_MAX_KEY_ID_LENGTH; + if (ciphertextLen > SIZE_MAX - maxHeaderLen) { + return 0; + } + return maxHeaderLen + ciphertextLen; +} + +int +ece_webpush_aes128gcm_encrypt(const uint8_t* rawRecvPubKey, + size_t rawRecvPubKeyLen, + const uint8_t* authSecret, size_t authSecretLen, + uint32_t rs, size_t padLen, + const uint8_t* plaintext, size_t plaintextLen, + uint8_t* payload, size_t* payloadLen) { + int err = ECE_OK; + + EC_KEY* recvPubKey = NULL; + EC_KEY* senderPrivKey = NULL; + + // Generate a random salt. + uint8_t salt[ECE_SALT_LENGTH]; + if (RAND_bytes(salt, ECE_SALT_LENGTH) != 1) { + err = ECE_ERROR_INVALID_SALT; + goto end; + } + + // Import the receiver public key. + recvPubKey = ece_import_public_key(rawRecvPubKey, rawRecvPubKeyLen); + if (!recvPubKey) { + err = ECE_ERROR_INVALID_PUBLIC_KEY; + goto end; + } + + // Generate the sender ECDH key pair. + senderPrivKey = EC_KEY_new_by_curve_name(NID_X9_62_prime256v1); + if (!senderPrivKey) { + err = ECE_ERROR_OUT_OF_MEMORY; + goto end; + } + if (EC_KEY_generate_key(senderPrivKey) != 1) { + err = ECE_ERROR_INVALID_PRIVATE_KEY; + goto end; + } + + // Encrypt the message. + err = ece_webpush_aes128gcm_encrypt_plaintext( + senderPrivKey, recvPubKey, authSecret, authSecretLen, salt, ECE_SALT_LENGTH, + rs, padLen, plaintext, plaintextLen, payload, payloadLen); + +end: + EC_KEY_free(recvPubKey); + EC_KEY_free(senderPrivKey); + return err; +} + +int +ece_webpush_aes128gcm_encrypt_with_keys( + const uint8_t* rawSenderPrivKey, size_t rawSenderPrivKeyLen, + const uint8_t* authSecret, size_t authSecretLen, const uint8_t* salt, + size_t saltLen, const uint8_t* rawRecvPubKey, size_t rawRecvPubKeyLen, + uint32_t rs, size_t padLen, const uint8_t* plaintext, size_t plaintextLen, + uint8_t* payload, size_t* payloadLen) { + + int err = ECE_OK; + + EC_KEY* senderPrivKey = NULL; + EC_KEY* recvPubKey = NULL; + + senderPrivKey = ece_import_private_key(rawSenderPrivKey, rawSenderPrivKeyLen); + if (!senderPrivKey) { + err = ECE_ERROR_INVALID_PRIVATE_KEY; + goto end; + } + recvPubKey = ece_import_public_key(rawRecvPubKey, rawRecvPubKeyLen); + if (!recvPubKey) { + err = ECE_ERROR_INVALID_PUBLIC_KEY; + goto end; + } + + err = ece_webpush_aes128gcm_encrypt_plaintext( + senderPrivKey, recvPubKey, authSecret, authSecretLen, salt, saltLen, rs, + padLen, plaintext, plaintextLen, payload, payloadLen); + +end: + EC_KEY_free(senderPrivKey); + EC_KEY_free(recvPubKey); + return err; +} + +size_t +ece_aesgcm_ciphertext_max_length(uint32_t rs, size_t padLen, + size_t plaintextLen) { + rs = ece_aesgcm_rs(rs); + if (!rs) { + return 0; + } + return ece_ciphertext_max_length(rs, ECE_AESGCM_PAD_SIZE, padLen, + plaintextLen, &ece_aesgcm_needs_trailer); +} + +int +ece_webpush_aesgcm_encrypt(const uint8_t* rawRecvPubKey, + size_t rawRecvPubKeyLen, const uint8_t* authSecret, + size_t authSecretLen, uint32_t rs, size_t padLen, + const uint8_t* plaintext, size_t plaintextLen, + uint8_t* salt, size_t saltLen, + uint8_t* rawSenderPubKey, size_t rawSenderPubKeyLen, + uint8_t* ciphertext, size_t* ciphertextLen) { + int err = ECE_OK; + + EC_KEY* recvPubKey = NULL; + EC_KEY* senderPrivKey = NULL; + + rs = ece_aesgcm_rs(rs); + if (!rs) { + err = ECE_ERROR_INVALID_RS; + goto end; + } + + if (saltLen != ECE_SALT_LENGTH) { + err = ECE_ERROR_INVALID_SALT; + goto end; + } + if (rawSenderPubKeyLen != ECE_WEBPUSH_PUBLIC_KEY_LENGTH) { + err = ECE_ERROR_INVALID_DH; + goto end; + } + + // Generate a random salt. + if (saltLen > INT_MAX || RAND_bytes(salt, (int) saltLen) != 1) { + err = ECE_ERROR_INVALID_SALT; + goto end; + } + + // Import the receiver public key. + recvPubKey = ece_import_public_key(rawRecvPubKey, rawRecvPubKeyLen); + if (!recvPubKey) { + err = ECE_ERROR_INVALID_PUBLIC_KEY; + goto end; + } + + // Generate the sender ECDH key pair. + senderPrivKey = EC_KEY_new_by_curve_name(NID_X9_62_prime256v1); + if (!senderPrivKey) { + err = ECE_ERROR_OUT_OF_MEMORY; + goto end; + } + if (EC_KEY_generate_key(senderPrivKey) != 1) { + err = ECE_ERROR_INVALID_PRIVATE_KEY; + goto end; + } + + if (!EC_POINT_point2oct(EC_KEY_get0_group(senderPrivKey), + EC_KEY_get0_public_key(senderPrivKey), + POINT_CONVERSION_UNCOMPRESSED, rawSenderPubKey, + rawSenderPubKeyLen, NULL)) { + err = ECE_ERROR_ENCODE_PUBLIC_KEY; + goto end; + } + + err = ece_webpush_encrypt_plaintext( + senderPrivKey, recvPubKey, authSecret, authSecretLen, salt, saltLen, rs, + ECE_AESGCM_PAD_SIZE, padLen, plaintext, plaintextLen, + &ece_webpush_aesgcm_derive_key_and_nonce, &ece_aesgcm_min_block_pad_length, + &ece_aesgcm_encrypt_block, &ece_aesgcm_needs_trailer, ciphertext, + ciphertextLen); + +end: + EC_KEY_free(recvPubKey); + EC_KEY_free(senderPrivKey); + return err; +} + +int +ece_webpush_aesgcm_encrypt_with_keys( + const uint8_t* rawSenderPrivKey, size_t rawSenderPrivKeyLen, + const uint8_t* authSecret, size_t authSecretLen, const uint8_t* salt, + size_t saltLen, const uint8_t* rawRecvPubKey, size_t rawRecvPubKeyLen, + uint32_t rs, size_t padLen, const uint8_t* plaintext, size_t plaintextLen, + uint8_t* ciphertext, size_t* ciphertextLen) { + + int err = ECE_OK; + + EC_KEY* senderPrivKey = NULL; + EC_KEY* recvPubKey = NULL; + + rs = ece_aesgcm_rs(rs); + if (!rs) { + err = ECE_ERROR_INVALID_RS; + goto end; + } + + senderPrivKey = ece_import_private_key(rawSenderPrivKey, rawSenderPrivKeyLen); + if (!senderPrivKey) { + err = ECE_ERROR_INVALID_PRIVATE_KEY; + goto end; + } + recvPubKey = ece_import_public_key(rawRecvPubKey, rawRecvPubKeyLen); + if (!recvPubKey) { + err = ECE_ERROR_INVALID_PUBLIC_KEY; + goto end; + } + + err = ece_webpush_encrypt_plaintext( + senderPrivKey, recvPubKey, authSecret, authSecretLen, salt, saltLen, rs, + ECE_AESGCM_PAD_SIZE, padLen, plaintext, plaintextLen, + &ece_webpush_aesgcm_derive_key_and_nonce, &ece_aesgcm_min_block_pad_length, + &ece_aesgcm_encrypt_block, &ece_aesgcm_needs_trailer, ciphertext, + ciphertextLen); + +end: + EC_KEY_free(senderPrivKey); + EC_KEY_free(recvPubKey); + return err; +} diff --git a/mobile/ios/ThirdParty/ecec/src/keys.c b/mobile/ios/ThirdParty/ecec/src/keys.c new file mode 100644 index 0000000000..2fe03e31cd --- /dev/null +++ b/mobile/ios/ThirdParty/ecec/src/keys.c @@ -0,0 +1,410 @@ +#include "ece/keys.h" +#include "ece.h" + +#include +#include +#include + +#include +#include + +// Writes an unsigned 16-bit integer in network byte order. +static inline void +ece_write_uint16_be(uint8_t* bytes, uint16_t value) { + bytes[0] = (value >> 8) & 0xff; + bytes[1] = value & 0xff; +} + +// Extracts an unsigned 64-bit integer in network byte order. +static inline uint64_t +ece_read_uint64_be(const uint8_t* bytes) { + uint64_t value = bytes[7]; + value |= (uint64_t) bytes[6] << 8; + value |= (uint64_t) bytes[5] << 16; + value |= (uint64_t) bytes[4] << 24; + value |= (uint64_t) bytes[3] << 32; + value |= (uint64_t) bytes[2] << 40; + value |= (uint64_t) bytes[1] << 48; + value |= (uint64_t) bytes[0] << 56; + return value; +} + +// Writes an unsigned 64-bit integer in network byte order. +static inline void +ece_write_uint64_be(uint8_t* bytes, uint64_t value) { + bytes[0] = (value >> 56) & 0xff; + bytes[1] = (value >> 48) & 0xff; + bytes[2] = (value >> 40) & 0xff; + bytes[3] = (value >> 32) & 0xff; + bytes[4] = (value >> 24) & 0xff; + bytes[5] = (value >> 16) & 0xff; + bytes[6] = (value >> 8) & 0xff; + bytes[7] = value & 0xff; +} + +void +ece_generate_iv(const uint8_t* nonce, uint64_t counter, uint8_t* iv) { + // Copy the first 4 bytes as-is, since `(x ^ 0) == x`. + size_t offset = ECE_NONCE_LENGTH - 8; + memcpy(iv, nonce, offset); + // Combine the remaining unsigned 64-bit integer with the record sequence + // number using XOR. See the "nonce derivation" section of the draft. + uint64_t mask = ece_read_uint64_be(&nonce[offset]); + ece_write_uint64_be(&iv[offset], mask ^ counter); +} + +EC_KEY* +ece_import_private_key(const uint8_t* rawKey, size_t rawKeyLen) { + EC_KEY* key = NULL; + EC_POINT* pubKeyPt = NULL; + + key = EC_KEY_new_by_curve_name(NID_X9_62_prime256v1); + if (!key) { + goto error; + } + if (EC_KEY_oct2priv(key, rawKey, rawKeyLen) != 1) { + goto error; + } + const EC_GROUP* group = EC_KEY_get0_group(key); + pubKeyPt = EC_POINT_new(group); + if (!pubKeyPt) { + goto error; + } + const BIGNUM* privKey = EC_KEY_get0_private_key(key); + if (EC_POINT_mul(group, pubKeyPt, privKey, NULL, NULL, NULL) != 1) { + goto error; + } + if (EC_KEY_set_public_key(key, pubKeyPt) != 1) { + goto error; + } + goto end; + +error: + EC_KEY_free(key); + key = NULL; + +end: + EC_POINT_free(pubKeyPt); + return key; +} + +EC_KEY* +ece_import_public_key(const uint8_t* rawKey, size_t rawKeyLen) { + EC_KEY* key = EC_KEY_new_by_curve_name(NID_X9_62_prime256v1); + if (!key) { + return NULL; + } + if (EC_KEY_oct2key(key, rawKey, rawKeyLen, NULL) != 1) { + EC_KEY_free(key); + return NULL; + } + return key; +} + +// HKDF from RFC 5869: `HKDF-Expand(HKDF-Extract(salt, ikm), info, length)`. +static int +ece_hkdf_sha256(const void* salt, size_t saltLen, const void* ikm, + size_t ikmLen, const void* info, size_t infoLen, + uint8_t* output, size_t outputLen) { + int err = ECE_OK; + + EVP_PKEY_CTX* ctx = EVP_PKEY_CTX_new_id(EVP_PKEY_HKDF, NULL); + if (!ctx) { + err = ECE_ERROR_HKDF; + goto end; + } + if (EVP_PKEY_derive_init(ctx) != 1) { + err = ECE_ERROR_HKDF; + goto end; + } + if (EVP_PKEY_CTX_set_hkdf_md(ctx, EVP_sha256()) != 1) { + err = ECE_ERROR_HKDF; + goto end; + } + if (saltLen > INT_MAX || + EVP_PKEY_CTX_set1_hkdf_salt(ctx, salt, (int) saltLen) != 1) { + err = ECE_ERROR_HKDF; + goto end; + } + if (ikmLen > INT_MAX || + EVP_PKEY_CTX_set1_hkdf_key(ctx, ikm, (int) ikmLen) != 1) { + err = ECE_ERROR_HKDF; + goto end; + } + if (infoLen > INT_MAX || + EVP_PKEY_CTX_add1_hkdf_info(ctx, info, (int) infoLen) != 1) { + err = ECE_ERROR_HKDF; + goto end; + } + if (EVP_PKEY_derive(ctx, output, &outputLen) != 1) { + err = ECE_ERROR_HKDF; + goto end; + } + +end: + EVP_PKEY_CTX_free(ctx); + return err; +} + +// Computes the ECDH shared secret, used as the input key material (IKM) for +// HKDF. +static uint8_t* +ece_compute_secret(EC_KEY* privKey, EC_KEY* pubKey, size_t* sharedSecretLen) { + uint8_t* sharedSecret = NULL; + + const EC_GROUP* group = EC_KEY_get0_group(privKey); + const EC_POINT* pubKeyPt = EC_KEY_get0_public_key(pubKey); + *sharedSecretLen = (size_t)((EC_GROUP_get_degree(group) + 7) / 8); + sharedSecret = calloc(*sharedSecretLen, sizeof(uint8_t)); + if (!sharedSecret) { + goto error; + } + if (ECDH_compute_key(sharedSecret, *sharedSecretLen, pubKeyPt, privKey, + NULL) <= 0) { + goto error; + } + goto end; + +error: + free(sharedSecret); + sharedSecret = NULL; + *sharedSecretLen = 0; + +end: + return sharedSecret; +} + +// The "aes128gcm" IKM info string is "WebPush: info\0", followed by the +// receiver and sender public keys. +static int +ece_webpush_aes128gcm_generate_info(EC_KEY* recvKey, EC_KEY* senderKey, + const char* prefix, size_t prefixLen, + uint8_t* info) { + size_t offset = 0; + + // Copy the prefix. + memcpy(info, prefix, prefixLen); + offset += prefixLen; + + // Copy the receiver public key. + const EC_GROUP* recvGrp = EC_KEY_get0_group(recvKey); + const EC_POINT* recvPubKeyPt = EC_KEY_get0_public_key(recvKey); + size_t recvPubKeyLen = + EC_POINT_point2oct(recvGrp, recvPubKeyPt, POINT_CONVERSION_UNCOMPRESSED, + &info[offset], ECE_WEBPUSH_PUBLIC_KEY_LENGTH, NULL); + if (!recvPubKeyLen) { + return ECE_ERROR_ENCODE_PUBLIC_KEY; + } + offset += recvPubKeyLen; + + // Copy the sender public key. + const EC_GROUP* senderGrp = EC_KEY_get0_group(senderKey); + const EC_POINT* senderPubKeyPt = EC_KEY_get0_public_key(senderKey); + size_t senderPubKeyLen = + EC_POINT_point2oct(senderGrp, senderPubKeyPt, POINT_CONVERSION_UNCOMPRESSED, + &info[offset], ECE_WEBPUSH_PUBLIC_KEY_LENGTH, NULL); + if (!senderPubKeyLen) { + return ECE_ERROR_ENCODE_PUBLIC_KEY; + } + + return ECE_OK; +} + +int +ece_aes128gcm_derive_key_and_nonce(const uint8_t* salt, size_t saltLen, + const uint8_t* ikm, size_t ikmLen, + uint8_t* key, uint8_t* nonce) { + int err = + ece_hkdf_sha256(salt, saltLen, ikm, ikmLen, ECE_AES128GCM_KEY_INFO, + ECE_AES128GCM_KEY_INFO_LENGTH, key, ECE_AES_KEY_LENGTH); + if (err) { + return err; + } + return ece_hkdf_sha256(salt, saltLen, ikm, ikmLen, ECE_AES128GCM_NONCE_INFO, + ECE_AES128GCM_NONCE_INFO_LENGTH, nonce, + ECE_NONCE_LENGTH); +} + +int +ece_webpush_aes128gcm_derive_key_and_nonce(ece_mode_t mode, EC_KEY* localKey, + EC_KEY* remoteKey, + const uint8_t* authSecret, + size_t authSecretLen, + const uint8_t* salt, size_t saltLen, + uint8_t* key, uint8_t* nonce) { + int err = ECE_OK; + + uint8_t* sharedSecret = NULL; + + size_t sharedSecretLen = 0; + sharedSecret = ece_compute_secret(localKey, remoteKey, &sharedSecretLen); + if (!sharedSecret) { + err = ECE_ERROR_COMPUTE_SECRET; + goto end; + } + + // The new "aes128gcm" scheme includes the sender and receiver public keys in + // the info string when deriving the Web Push IKM. + uint8_t ikmInfo[ECE_WEBPUSH_AES128GCM_IKM_INFO_LENGTH]; + switch (mode) { + case ECE_MODE_ENCRYPT: + // For encryption, the remote static public key is the receiver key, and the + // local ephemeral private key is the sender key. + err = ece_webpush_aes128gcm_generate_info( + remoteKey, localKey, ECE_WEBPUSH_AES128GCM_IKM_INFO_PREFIX, + ECE_WEBPUSH_AES128GCM_IKM_INFO_PREFIX_LENGTH, ikmInfo); + break; + + case ECE_MODE_DECRYPT: + // For decryption, the local static private key is the receiver key, and the + // remote ephemeral public key is the sender key. + err = ece_webpush_aes128gcm_generate_info( + localKey, remoteKey, ECE_WEBPUSH_AES128GCM_IKM_INFO_PREFIX, + ECE_WEBPUSH_AES128GCM_IKM_INFO_PREFIX_LENGTH, ikmInfo); + break; + + default: + assert(false); + err = ECE_ERROR_DECRYPT; + } + if (err) { + goto end; + } + uint8_t ikm[ECE_WEBPUSH_IKM_LENGTH]; + err = ece_hkdf_sha256( + authSecret, authSecretLen, sharedSecret, sharedSecretLen, ikmInfo, + ECE_WEBPUSH_AES128GCM_IKM_INFO_LENGTH, ikm, ECE_WEBPUSH_IKM_LENGTH); + if (err) { + goto end; + } + + err = ece_aes128gcm_derive_key_and_nonce(salt, saltLen, ikm, + ECE_WEBPUSH_IKM_LENGTH, key, nonce); + +end: + free(sharedSecret); + return err; +} + +// The "aesgcm" info string is "Content-Encoding: \0P-256\0", +// followed by the length-prefixed (unsigned 16-bit integers) receiver and +// sender public keys. +static int +ece_webpush_aesgcm_generate_info(EC_KEY* recvKey, EC_KEY* senderKey, + const char* prefix, size_t prefixLen, + uint8_t* info) { + size_t offset = 0; + + // Copy the prefix. + memcpy(info, prefix, prefixLen); + offset += prefixLen; + + // Copy the length-prefixed receiver public key. + ece_write_uint16_be(&info[offset], ECE_WEBPUSH_PUBLIC_KEY_LENGTH); + offset += 2; + const EC_GROUP* recvGrp = EC_KEY_get0_group(recvKey); + const EC_POINT* recvPubKeyPt = EC_KEY_get0_public_key(recvKey); + size_t recvPubKeyLen = + EC_POINT_point2oct(recvGrp, recvPubKeyPt, POINT_CONVERSION_UNCOMPRESSED, + &info[offset], ECE_WEBPUSH_PUBLIC_KEY_LENGTH, NULL); + if (!recvPubKeyLen) { + return ECE_ERROR_ENCODE_PUBLIC_KEY; + } + offset += recvPubKeyLen; + + // Copy the length-prefixed sender public key. + ece_write_uint16_be(&info[offset], ECE_WEBPUSH_PUBLIC_KEY_LENGTH); + offset += 2; + const EC_GROUP* senderGrp = EC_KEY_get0_group(senderKey); + const EC_POINT* senderPubKeyPt = EC_KEY_get0_public_key(senderKey); + size_t senderPubKeyLen = + EC_POINT_point2oct(senderGrp, senderPubKeyPt, POINT_CONVERSION_UNCOMPRESSED, + &info[offset], ECE_WEBPUSH_PUBLIC_KEY_LENGTH, NULL); + if (!senderPubKeyLen) { + return ECE_ERROR_ENCODE_PUBLIC_KEY; + } + + return ECE_OK; +} + +int +ece_webpush_aesgcm_derive_key_and_nonce(ece_mode_t mode, EC_KEY* localKey, + EC_KEY* remoteKey, + const uint8_t* authSecret, + size_t authSecretLen, + const uint8_t* salt, size_t saltLen, + uint8_t* key, uint8_t* nonce) { + ECE_UNUSED(mode); + + int err = ECE_OK; + + uint8_t* sharedSecret = NULL; + + size_t sharedSecretLen = 0; + sharedSecret = ece_compute_secret(localKey, remoteKey, &sharedSecretLen); + if (!sharedSecret) { + err = ECE_ERROR_COMPUTE_SECRET; + goto end; + } + + // The old "aesgcm" scheme uses a static info string to derive the Web Push + // IKM. + uint8_t ikm[ECE_WEBPUSH_IKM_LENGTH]; + err = ece_hkdf_sha256(authSecret, authSecretLen, sharedSecret, + sharedSecretLen, ECE_WEBPUSH_AESGCM_IKM_INFO, + ECE_WEBPUSH_AESGCM_IKM_INFO_LENGTH, ikm, + ECE_WEBPUSH_IKM_LENGTH); + if (err) { + goto end; + } + + // Next, derive the AES decryption key and nonce. We include the sender and + // receiver public keys in the info strings. + uint8_t keyInfo[ECE_WEBPUSH_AESGCM_KEY_INFO_LENGTH]; + uint8_t nonceInfo[ECE_WEBPUSH_AESGCM_NONCE_INFO_LENGTH]; + switch (mode) { + case ECE_MODE_ENCRYPT: + err = ece_webpush_aesgcm_generate_info( + remoteKey, localKey, ECE_WEBPUSH_AESGCM_KEY_INFO_PREFIX, + ECE_WEBPUSH_AESGCM_KEY_INFO_PREFIX_LENGTH, keyInfo); + if (err) { + break; + } + err = ece_webpush_aesgcm_generate_info( + remoteKey, localKey, ECE_WEBPUSH_AESGCM_NONCE_INFO_PREFIX, + ECE_WEBPUSH_AESGCM_NONCE_INFO_PREFIX_LENGTH, nonceInfo); + break; + + case ECE_MODE_DECRYPT: + err = ece_webpush_aesgcm_generate_info( + localKey, remoteKey, ECE_WEBPUSH_AESGCM_KEY_INFO_PREFIX, + ECE_WEBPUSH_AESGCM_KEY_INFO_PREFIX_LENGTH, keyInfo); + if (err) { + break; + } + err = ece_webpush_aesgcm_generate_info( + localKey, remoteKey, ECE_WEBPUSH_AESGCM_NONCE_INFO_PREFIX, + ECE_WEBPUSH_AESGCM_NONCE_INFO_PREFIX_LENGTH, nonceInfo); + break; + + default: + assert(false); + err = ECE_ERROR_DECRYPT; + } + if (err) { + goto end; + } + err = ece_hkdf_sha256(salt, saltLen, ikm, ECE_WEBPUSH_IKM_LENGTH, keyInfo, + ECE_WEBPUSH_AESGCM_KEY_INFO_LENGTH, key, + ECE_AES_KEY_LENGTH); + if (err) { + goto end; + } + err = ece_hkdf_sha256(salt, saltLen, ikm, ECE_WEBPUSH_IKM_LENGTH, nonceInfo, + ECE_WEBPUSH_AESGCM_NONCE_INFO_LENGTH, nonce, + ECE_NONCE_LENGTH); + +end: + free(sharedSecret); + return err; +} diff --git a/mobile/ios/ThirdParty/ecec/src/params.c b/mobile/ios/ThirdParty/ecec/src/params.c new file mode 100644 index 0000000000..0366d9bdf2 --- /dev/null +++ b/mobile/ios/ThirdParty/ecec/src/params.c @@ -0,0 +1,574 @@ +#include "ece.h" + +// This file implements a parser for the `Crypto-Key` and `Encryption` HTTP +// headers, used by the older "aesgcm" encoding. The newer "aes128gcm" encoding +// includes the relevant information in a binary header, directly in the +// payload. + +#include +#include +#include +#include +#include + +#define ECE_HEADER_STATE_BEGIN_PARAM 1 +#define ECE_HEADER_STATE_BEGIN_NAME 2 +#define ECE_HEADER_STATE_NAME 3 +#define ECE_HEADER_STATE_END_NAME 4 +#define ECE_HEADER_STATE_BEGIN_VALUE 5 +#define ECE_HEADER_STATE_VALUE 6 +#define ECE_HEADER_STATE_BEGIN_QUOTED_VALUE 7 +#define ECE_HEADER_STATE_QUOTED_VALUE 8 +#define ECE_HEADER_STATE_END_VALUE 9 +#define ECE_HEADER_STATE_INVALID_HEADER 10 + +#define ECE_HEADER_DH_PREFIX "dh=" +#define ECE_HEADER_DH_PREFIX_LENGTH 3 + +// Extracts an unsigned 32-bit integer in network byte order. +static inline uint32_t +ece_read_uint32_be(const uint8_t* bytes) { + uint32_t value = bytes[3]; + value |= (uint32_t) bytes[2] << 8; + value |= (uint32_t) bytes[1] << 16; + value |= (uint32_t) bytes[0] << 24; + return value; +} + +// A linked list that holds name-value pairs for a parameter in a header +// value. For example, if the parameter is `a=b; c=d; e=f`, the parser will +// allocate three `ece_header_pairs_t` structures, one for each ;-delimited +// pair. "=" separates the name and value. +typedef struct ece_header_pairs_s { + struct ece_header_pairs_s* next; + // The name and value are pointers into the backing header value; the parser + // doesn't allocate new strings. Freeing the backing string will invalidate + // all `name` and `value` references. Also, because these are not true C + // strings, it's important to use them with functions that take a length, like + // `strncmp`. Functions that assume a NUL-terminated string will read until + // the end of the backing string. + const char* name; + const char* value; + size_t nameLen; + size_t valueLen; +} ece_header_pairs_t; + +// Initializes a name-value pair node at the head of the pair list. `head` may +// be `NULL`. +static ece_header_pairs_t* +ece_header_pairs_alloc(ece_header_pairs_t* head) { + ece_header_pairs_t* pairs = malloc(sizeof(ece_header_pairs_t)); + if (!pairs) { + return NULL; + } + pairs->next = head; + pairs->name = NULL; + pairs->value = NULL; + pairs->nameLen = 0; + pairs->valueLen = 0; + return pairs; +} + +// Indicates whether a name-value pair node matches the `name`. +static inline bool +ece_header_pairs_has_name(ece_header_pairs_t* pair, const char* name) { + return !strncmp(pair->name, name, pair->nameLen); +} + +// Indicates whether a name-value pair node matches the `value`. +static inline bool +ece_header_pairs_has_value(ece_header_pairs_t* pair, const char* value) { + return !strncmp(pair->value, value, pair->valueLen); +} + +// Copies a pair node's value into a C string. +static char* +ece_header_pairs_value_to_str(ece_header_pairs_t* pair) { + char* value = malloc(pair->valueLen + 1); + if (!value) { + return NULL; + } + strncpy(value, pair->value, pair->valueLen); + value[pair->valueLen] = '\0'; + return value; +} + +// Frees a name-value pair list and all its nodes. +static void +ece_header_pairs_free(ece_header_pairs_t* pairs) { + ece_header_pairs_t* pair = pairs; + while (pair) { + ece_header_pairs_t* next = pair->next; + free(pair); + pair = next; + } +} + +// A linked list that holds parameters extracted from a header value. For +// example, if the header value is `a=b; c=d, e=f; g=h`, the parser will +// allocate two `ece_header_params_t` structures: one to hold the parameter +// `a=b; c=d`, and the other to hold `e=f; g=h`. +typedef struct ece_header_params_s { + struct ece_header_params_s* next; + ece_header_pairs_t* pairs; +} ece_header_params_t; + +// Initializes a parameter node at the head of the parameter list. `head` may be +// `NULL`. +static ece_header_params_t* +ece_header_params_alloc(ece_header_params_t* head) { + ece_header_params_t* params = malloc(sizeof(ece_header_params_t)); + if (!params) { + return NULL; + } + params->next = head; + params->pairs = NULL; + return params; +} + +// Reverses a parameter list in-place and returns a pointer to the new head. +static ece_header_params_t* +ece_header_params_reverse(ece_header_params_t* params) { + ece_header_params_t* sibling = NULL; + while (params) { + ece_header_params_t* next = params->next; + params->next = sibling; + sibling = params; + params = next; + } + return sibling; +} + +// Frees a parameter list and all its nodes. +static void +ece_header_params_free(ece_header_params_t* params) { + ece_header_params_t* param = params; + while (param) { + ece_header_pairs_free(param->pairs); + ece_header_params_t* next = param->next; + free(param); + param = next; + } +} + +// Indicates whether `c` is whitespace, per `WSP` in RFC 5234, Appendix B.1. +static inline bool +ece_header_is_space(char c) { + return c == ' ' || c == '\t'; +} + +// Indicates whether `c` can appear in a pair name. Only lowercase letters and +// numbers are allowed. +static inline bool +ece_header_is_valid_pair_name(char c) { + return (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9'); +} + +// Indicates whether `c` can appear in a pair value. This includes all +// characters in the Base64url alphabet. +static inline bool +ece_header_is_valid_pair_value(char c) { + return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || + (c >= '0' && c <= '9') || c == '-' || c == '_'; +} + +// A header parameter parser. +typedef struct ece_header_parser_s { + int state; + ece_header_params_t* params; +} ece_header_parser_t; + +// Parses the next token in `input` and updates the parser state. Returns true +// if the caller should advance to the next character; false otherwise. +static bool +ece_header_parse(ece_header_parser_t* parser, const char* input) { + switch (parser->state) { + case ECE_HEADER_STATE_BEGIN_PARAM: { + ece_header_params_t* param = ece_header_params_alloc(parser->params); + if (!param) { + break; + } + parser->params = param; + parser->state = ECE_HEADER_STATE_BEGIN_NAME; + return false; + } + + case ECE_HEADER_STATE_BEGIN_NAME: + if (ece_header_is_space(*input)) { + return true; + } + if (ece_header_is_valid_pair_name(*input)) { + ece_header_pairs_t* pair = ece_header_pairs_alloc(parser->params->pairs); + if (!pair) { + break; + } + parser->params->pairs = pair; + pair->name = input; + parser->state = ECE_HEADER_STATE_NAME; + return false; + } + break; + + case ECE_HEADER_STATE_NAME: + if (ece_header_is_valid_pair_name(*input)) { + parser->params->pairs->nameLen++; + return true; + } + if (ece_header_is_space(*input) || *input == '=') { + parser->state = ECE_HEADER_STATE_END_NAME; + return false; + } + break; + + case ECE_HEADER_STATE_END_NAME: + if (ece_header_is_space(*input)) { + return true; + } + if (*input == '=') { + parser->state = ECE_HEADER_STATE_BEGIN_VALUE; + return true; + } + break; + + case ECE_HEADER_STATE_BEGIN_VALUE: + if (ece_header_is_space(*input)) { + return true; + } + if (ece_header_is_valid_pair_value(*input)) { + parser->params->pairs->value = input; + parser->state = ECE_HEADER_STATE_VALUE; + return false; + } + if (*input == '"') { + parser->state = ECE_HEADER_STATE_BEGIN_QUOTED_VALUE; + return true; + } + break; + + case ECE_HEADER_STATE_VALUE: + if (ece_header_is_space(*input) || *input == ';' || *input == ',') { + parser->state = ECE_HEADER_STATE_END_VALUE; + return false; + } + if (ece_header_is_valid_pair_value(*input)) { + parser->params->pairs->valueLen++; + return true; + } + break; + + case ECE_HEADER_STATE_BEGIN_QUOTED_VALUE: + if (ece_header_is_valid_pair_value(*input)) { + // Quoted strings allow spaces and escapes, but neither `Crypto-Key` nor + // `Encryption` accept them. We keep the parser simple by rejecting + // non-Base64url characters here. We also disallow empty quoted strings. + parser->params->pairs->value = input; + parser->params->pairs->valueLen++; + parser->state = ECE_HEADER_STATE_QUOTED_VALUE; + return true; + } + break; + + case ECE_HEADER_STATE_QUOTED_VALUE: + if (ece_header_is_valid_pair_value(*input)) { + parser->params->pairs->valueLen++; + return true; + } + if (*input == '"') { + parser->state = ECE_HEADER_STATE_END_VALUE; + return true; + } + break; + + case ECE_HEADER_STATE_END_VALUE: + if (ece_header_is_space(*input)) { + return true; + } + if (*input == ';') { + // New name-value pair for the same parameter. Advance the parser; + // `ECE_HEADER_STATE_BEGIN_NAME` will prepend a new node to the pairs + // list. + parser->state = ECE_HEADER_STATE_BEGIN_NAME; + return true; + } + if (*input == ',') { + // New parameter. Advance the parser; `ECE_HEADER_STATE_BEGIN_PARAM` will + // prepend a new node to the parameters list and begin parsing its pairs. + parser->state = ECE_HEADER_STATE_BEGIN_PARAM; + return true; + } + break; + + default: + // Unexpected parser state. + assert(false); + } + parser->state = ECE_HEADER_STATE_INVALID_HEADER; + return false; +} + +// Parses a `header` value of the form `a=b; c=d; e=f, g=h, i=j` into a +// parameter list. +static ece_header_params_t* +ece_header_extract_params(const char* header) { + ece_header_parser_t parser; + parser.state = ECE_HEADER_STATE_BEGIN_PARAM; + parser.params = NULL; + + const char* input = header; + while (*input) { + if (ece_header_parse(&parser, input)) { + input++; + } + if (parser.state == ECE_HEADER_STATE_INVALID_HEADER) { + goto error; + } + } + if (parser.state != ECE_HEADER_STATE_END_VALUE) { + // If the header ends with an unquoted value, the parser might still be in a + // non-terminal state. Try to parse an extra space to reach the terminal + // state. + ece_header_parse(&parser, " "); + if (parser.state != ECE_HEADER_STATE_END_VALUE) { + // If we're still in a non-terminal state, the header is incomplete. + goto error; + } + } + return ece_header_params_reverse(parser.params); + +error: + ece_header_params_free(parser.params); + return NULL; +} + +int +ece_aes128gcm_payload_extract_params(const uint8_t* payload, size_t payloadLen, + const uint8_t** salt, size_t* saltLen, + const uint8_t** keyId, size_t* keyIdLen, + uint32_t* rs, const uint8_t** ciphertext, + size_t* ciphertextLen) { + if (payloadLen < ECE_AES128GCM_HEADER_LENGTH) { + return ECE_ERROR_SHORT_HEADER; + } + + *saltLen = ECE_SALT_LENGTH; + + *keyIdLen = payload[ECE_SALT_LENGTH + 4]; + if (payloadLen < ECE_AES128GCM_HEADER_LENGTH + *keyIdLen) { + return ECE_ERROR_SHORT_HEADER; + } + + *rs = ece_read_uint32_be(&payload[ECE_SALT_LENGTH]); + if (*rs < ECE_AES128GCM_MIN_RS) { + return ECE_ERROR_INVALID_RS; + } + + size_t payloadStart = ECE_AES128GCM_HEADER_LENGTH + *keyIdLen; + *ciphertextLen = payloadLen - payloadStart; + if (!(*ciphertextLen)) { + return ECE_ERROR_ZERO_CIPHERTEXT; + } + + *salt = payload; + if (keyIdLen) { + *keyId = &payload[ECE_AES128GCM_HEADER_LENGTH]; + } else { + *keyId = NULL; + } + *ciphertext = &payload[payloadStart]; + + return ECE_OK; +} + +int +ece_webpush_aesgcm_headers_extract_params(const char* cryptoKeyHeader, + const char* encryptionHeader, + uint8_t* salt, size_t saltLen, + uint8_t* rawSenderPubKey, + size_t rawSenderPubKeyLen, + uint32_t* rs) { + int err = ECE_OK; + + ece_header_params_t* encryptionParams = NULL; + ece_header_params_t* cryptoKeyParams = NULL; + char* keyId = NULL; + uint32_t rsValue = 0; + size_t decodedSaltLen = 0; + size_t decodedKeyLen = 0; + + // First, extract the key ID, salt, and record size from the first key in the + // `Encryption` header. + encryptionParams = ece_header_extract_params(encryptionHeader); + if (!encryptionParams) { + err = ECE_ERROR_INVALID_ENCRYPTION_HEADER; + goto end; + } + for (ece_header_pairs_t* pair = encryptionParams->pairs; pair; + pair = pair->next) { + if (ece_header_pairs_has_name(pair, "keyid")) { + // The key ID is optional, and is used to identify the public key in the + // `Crypto-Key` header if multiple encryption keys are specified. + if (keyId) { + err = ECE_ERROR_INVALID_ENCRYPTION_HEADER; + goto end; + } + keyId = ece_header_pairs_value_to_str(pair); + if (!keyId) { + err = ECE_ERROR_OUT_OF_MEMORY; + goto end; + } + continue; + } + if (ece_header_pairs_has_name(pair, "rs")) { + // The record size is optional. + if (rsValue) { + err = ECE_ERROR_INVALID_ENCRYPTION_HEADER; + goto end; + } + char* value = ece_header_pairs_value_to_str(pair); + if (!value) { + err = ECE_ERROR_OUT_OF_MEMORY; + goto end; + } + int result = sscanf(value, "%" SCNu32, &rsValue); + free(value); + if (result <= 0 || rsValue < ECE_AESGCM_MIN_RS) { + err = ECE_ERROR_INVALID_RS; + goto end; + } + continue; + } + if (ece_header_pairs_has_name(pair, "salt")) { + // The salt is required, and must be Base64url-encoded without padding. + if (decodedSaltLen) { + err = ECE_ERROR_INVALID_ENCRYPTION_HEADER; + goto end; + } + decodedSaltLen = + ece_base64url_decode(pair->value, pair->valueLen, + ECE_BASE64URL_REJECT_PADDING, salt, saltLen); + if (!decodedSaltLen) { + break; + } + continue; + } + } + if (decodedSaltLen != saltLen) { + err = ECE_ERROR_INVALID_SALT; + goto end; + } + if (!rsValue) { + // The record size defaults to 4096 if unspecified. + rsValue = 4096; + } + *rs = rsValue; + + // Next, find the ephemeral public key in the `Crypto-Key` header. + cryptoKeyParams = ece_header_extract_params(cryptoKeyHeader); + if (!cryptoKeyParams) { + err = ECE_ERROR_INVALID_CRYPTO_KEY_HEADER; + goto end; + } + ece_header_params_t* cryptoKeyParam = cryptoKeyParams; + if (keyId) { + // If the sender specified a key ID in the `Encryption` header, find the + // matching parameter in the `Crypto-Key` header. Otherwise, we assume + // there's only one key, and use the first one we see. + while (cryptoKeyParam) { + bool keyIdMatches = false; + for (ece_header_pairs_t* pair = cryptoKeyParam->pairs; pair; + pair = pair->next) { + if (!ece_header_pairs_has_name(pair, "keyid")) { + continue; + } + keyIdMatches = ece_header_pairs_has_value(pair, keyId); + if (keyIdMatches) { + break; + } + } + if (keyIdMatches) { + break; + } + cryptoKeyParam = cryptoKeyParam->next; + } + if (!cryptoKeyParam) { + // We don't have a matching key ID with a `dh` name-value pair. + err = ECE_ERROR_INVALID_DH; + goto end; + } + } + for (ece_header_pairs_t* pair = cryptoKeyParam->pairs; pair; + pair = pair->next) { + if (!ece_header_pairs_has_name(pair, "dh")) { + continue; + } + // The sender's public key must be Base64url-encoded without padding. + decodedKeyLen = ece_base64url_decode(pair->value, pair->valueLen, + ECE_BASE64URL_REJECT_PADDING, + rawSenderPubKey, rawSenderPubKeyLen); + break; + } + if (decodedKeyLen != rawSenderPubKeyLen) { + err = ECE_ERROR_INVALID_DH; + goto end; + } + +end: + ece_header_params_free(encryptionParams); + ece_header_params_free(cryptoKeyParams); + free(keyId); + return err; +} + +int +ece_webpush_aesgcm_headers_from_params(const void* salt, size_t saltLen, + const void* rawSenderPubKey, + size_t rawSenderPubKeyLen, uint32_t rs, + char* cryptoKeyHeader, + size_t* cryptoKeyHeaderLen, + char* encryptionHeader, + size_t* encryptionHeaderLen) { + size_t b64SenderPubKeyLen = ece_base64url_encode( + rawSenderPubKey, rawSenderPubKeyLen, ECE_BASE64URL_OMIT_PADDING, NULL, 0); + if (!b64SenderPubKeyLen || + b64SenderPubKeyLen > SIZE_MAX - ECE_HEADER_DH_PREFIX_LENGTH) { + return ECE_ERROR_INVALID_DH; + } + size_t requiredCryptoKeyHeaderLen = + b64SenderPubKeyLen + ECE_HEADER_DH_PREFIX_LENGTH; + if (*cryptoKeyHeaderLen) { + if (*cryptoKeyHeaderLen < requiredCryptoKeyHeaderLen) { + return ECE_ERROR_OUT_OF_MEMORY; + } + memcpy(cryptoKeyHeader, ECE_HEADER_DH_PREFIX, ECE_HEADER_DH_PREFIX_LENGTH); + ece_base64url_encode( + rawSenderPubKey, rawSenderPubKeyLen, ECE_BASE64URL_OMIT_PADDING, + &cryptoKeyHeader[ECE_HEADER_DH_PREFIX_LENGTH], b64SenderPubKeyLen); + } + *cryptoKeyHeaderLen = requiredCryptoKeyHeaderLen; + + size_t b64SaltLen = + ece_base64url_encode(salt, saltLen, ECE_BASE64URL_OMIT_PADDING, NULL, 0); + if (!b64SaltLen) { + return ECE_ERROR_INVALID_SALT; + } + int maybeEncryptionPrefixLen = snprintf(NULL, 0, "rs=%" PRIu32 ";salt=", rs); + if (maybeEncryptionPrefixLen <= 0) { + return ECE_ERROR_INVALID_SALT; + } + size_t encryptionPrefixLen = (size_t) maybeEncryptionPrefixLen; + if (b64SaltLen > SIZE_MAX - encryptionPrefixLen) { + return ECE_ERROR_INVALID_SALT; + } + size_t requiredEncryptionHeaderLen = b64SaltLen + encryptionPrefixLen; + if (*encryptionHeaderLen) { + if (*encryptionHeaderLen < requiredEncryptionHeaderLen) { + return ECE_ERROR_OUT_OF_MEMORY; + } + sprintf(encryptionHeader, "rs=%" PRIu32 ";salt=", rs); + ece_base64url_encode(salt, saltLen, ECE_BASE64URL_OMIT_PADDING, + &encryptionHeader[encryptionPrefixLen], b64SaltLen); + } + *encryptionHeaderLen = requiredEncryptionHeaderLen; + + return ECE_OK; +} diff --git a/mobile/ios/ThirdParty/ecec/src/trailer.c b/mobile/ios/ThirdParty/ecec/src/trailer.c new file mode 100644 index 0000000000..6ab6abbcf4 --- /dev/null +++ b/mobile/ios/ThirdParty/ecec/src/trailer.c @@ -0,0 +1,19 @@ +#include "ece/trailer.h" +#include "ece.h" + +uint32_t +ece_aesgcm_rs(uint32_t rs) { + return rs > UINT32_MAX - ECE_TAG_LENGTH ? 0 : rs + ECE_TAG_LENGTH; +} + +bool +ece_aesgcm_needs_trailer(uint32_t rs, size_t ciphertextLen) { + return !(ciphertextLen % rs); +} + +bool +ece_aes128gcm_needs_trailer(uint32_t rs, size_t ciphertextLen) { + ECE_UNUSED(rs); + ECE_UNUSED(ciphertextLen); + return false; +} diff --git a/mobile/ios/ThirdParty/ecec/test/base64url.c b/mobile/ios/ThirdParty/ecec/test/base64url.c new file mode 100644 index 0000000000..f3da710fe2 --- /dev/null +++ b/mobile/ios/ThirdParty/ecec/test/base64url.c @@ -0,0 +1,178 @@ +#include "test.h" + +#include + +typedef struct base64url_encode_test_s { + const char* binary; + size_t binaryLen; + ece_base64url_encode_policy_t paddingPolicy; + const char* base64; + size_t base64Len; +} base64url_encode_test_t; + +static base64url_encode_test_t base64url_encode_tests[] = { + {"f", 1, ECE_BASE64URL_OMIT_PADDING, "Zg", 2}, + {"f", 1, ECE_BASE64URL_INCLUDE_PADDING, "Zg==", 4}, + + {"fo", 2, ECE_BASE64URL_OMIT_PADDING, "Zm8", 3}, + {"fo", 2, ECE_BASE64URL_INCLUDE_PADDING, "Zm8=", 4}, + + {"foo", 3, ECE_BASE64URL_OMIT_PADDING, "Zm9v", 4}, + {"foo", 3, ECE_BASE64URL_INCLUDE_PADDING, "Zm9v", 4}, + + {"foob", 4, ECE_BASE64URL_OMIT_PADDING, "Zm9vYg", 6}, + {"foob", 4, ECE_BASE64URL_INCLUDE_PADDING, "Zm9vYg==", 8}, + + {"fooba", 5, ECE_BASE64URL_OMIT_PADDING, "Zm9vYmE", 7}, + {"fooba", 5, ECE_BASE64URL_INCLUDE_PADDING, "Zm9vYmE=", 8}, + + {"foobar", 6, ECE_BASE64URL_OMIT_PADDING, "Zm9vYmFy", 8}, + {"foobar", 6, ECE_BASE64URL_INCLUDE_PADDING, "Zm9vYmFy", 8}, + + {"\x14\xfb\x9c\x03\xd9\x7e", 6, ECE_BASE64URL_OMIT_PADDING, "FPucA9l-", 8}, + {"\x14\xfb\x9c\x03\xd9\x7e", 6, ECE_BASE64URL_INCLUDE_PADDING, "FPucA9l-", 8}, + + {"\x14\xfb\x9c\x03\xd9", 5, ECE_BASE64URL_OMIT_PADDING, "FPucA9k", 7}, + {"\x14\xfb\x9c\x03\xd9", 5, ECE_BASE64URL_INCLUDE_PADDING, "FPucA9k=", 8}, + + {"\x14\xfb\x9c\x03", 4, ECE_BASE64URL_OMIT_PADDING, "FPucAw", 6}, + {"\x14\xfb\x9c\x03", 4, ECE_BASE64URL_INCLUDE_PADDING, "FPucAw==", 8}, +}; + +typedef struct base64url_decode_test_s { + const char* base64; + size_t base64Len; + ece_base64url_decode_policy_t paddingPolicy; + size_t requiredBinaryLen; + const char* binary; + size_t binaryLen; +} base64url_decode_test_t; + +static base64url_decode_test_t base64url_decode_tests[] = { + // Test vectors from RFC 4648, section 10. + {"", 0, ECE_BASE64URL_REQUIRE_PADDING, 0, NULL, 0}, + + {"Zg", 2, ECE_BASE64URL_REQUIRE_PADDING, 0, NULL, 0}, + {"Zg", 2, ECE_BASE64URL_IGNORE_PADDING, 1, "f", 1}, + {"Zg", 2, ECE_BASE64URL_REJECT_PADDING, 1, "f", 1}, + + {"Zg==", 4, ECE_BASE64URL_REQUIRE_PADDING, 1, "f", 1}, + {"Zg==", 4, ECE_BASE64URL_IGNORE_PADDING, 1, "f", 1}, + {"Zg==", 4, ECE_BASE64URL_REJECT_PADDING, 3, NULL, 0}, + + {"Zm8", 3, ECE_BASE64URL_REQUIRE_PADDING, 0, NULL, 0}, + {"Zm8", 3, ECE_BASE64URL_IGNORE_PADDING, 2, "fo", 2}, + {"Zm8", 3, ECE_BASE64URL_REJECT_PADDING, 2, "fo", 2}, + + {"Zm8=", 4, ECE_BASE64URL_REQUIRE_PADDING, 2, "fo", 2}, + {"Zm8=", 4, ECE_BASE64URL_IGNORE_PADDING, 2, "fo", 2}, + {"Zm8=", 4, ECE_BASE64URL_REJECT_PADDING, 3, NULL, 0}, + + {"Zm9v", 4, ECE_BASE64URL_REQUIRE_PADDING, 3, "foo", 3}, + {"Zm9v", 4, ECE_BASE64URL_IGNORE_PADDING, 3, "foo", 3}, + {"Zm9v", 4, ECE_BASE64URL_REJECT_PADDING, 3, "foo", 3}, + + {"Zm9vYg", 6, ECE_BASE64URL_REQUIRE_PADDING, 0, NULL, 0}, + {"Zm9vYg", 6, ECE_BASE64URL_IGNORE_PADDING, 4, "foob", 4}, + {"Zm9vYg", 6, ECE_BASE64URL_REJECT_PADDING, 4, "foob", 4}, + + {"Zm9vYg==", 8, ECE_BASE64URL_REQUIRE_PADDING, 4, "foob", 4}, + {"Zm9vYg==", 8, ECE_BASE64URL_IGNORE_PADDING, 4, "foob", 4}, + {"Zm9vYg==", 8, ECE_BASE64URL_REJECT_PADDING, 6, NULL, 0}, + + {"Zm9vYmE", 7, ECE_BASE64URL_REQUIRE_PADDING, 0, NULL, 0}, + {"Zm9vYmE", 7, ECE_BASE64URL_IGNORE_PADDING, 5, "fooba", 5}, + {"Zm9vYmE", 7, ECE_BASE64URL_REJECT_PADDING, 5, "fooba", 5}, + + {"Zm9vYmE=", 8, ECE_BASE64URL_REQUIRE_PADDING, 5, "fooba", 5}, + {"Zm9vYmE=", 8, ECE_BASE64URL_IGNORE_PADDING, 5, "fooba", 5}, + {"Zm9vYmE=", 8, ECE_BASE64URL_REJECT_PADDING, 6, NULL, 0}, + + {"Zm9vYmFy", 8, ECE_BASE64URL_REQUIRE_PADDING, 6, "foobar", 6}, + {"Zm9vYmFy", 8, ECE_BASE64URL_IGNORE_PADDING, 6, "foobar", 6}, + {"Zm9vYmFy", 8, ECE_BASE64URL_REJECT_PADDING, 6, "foobar", 6}, + + // Examples from RFC 4648, section 9. + {"FPucA9l-", 8, ECE_BASE64URL_REQUIRE_PADDING, 6, "\x14\xfb\x9c\x03\xd9\x7e", + 6}, + {"FPucA9l-", 8, ECE_BASE64URL_IGNORE_PADDING, 6, "\x14\xfb\x9c\x03\xd9\x7e", + 6}, + {"FPucA9l-", 8, ECE_BASE64URL_REJECT_PADDING, 6, "\x14\xfb\x9c\x03\xd9\x7e", + 6}, + + {"FPucA9k", 7, ECE_BASE64URL_REQUIRE_PADDING, 0, NULL, 0}, + {"FPucA9k", 7, ECE_BASE64URL_IGNORE_PADDING, 5, "\x14\xfb\x9c\x03\xd9", 5}, + {"FPucA9k", 7, ECE_BASE64URL_REJECT_PADDING, 5, "\x14\xfb\x9c\x03\xd9", 5}, + + {"FPucA9k=", 8, ECE_BASE64URL_REQUIRE_PADDING, 5, "\x14\xfb\x9c\x03\xd9", 5}, + {"FPucA9k=", 8, ECE_BASE64URL_IGNORE_PADDING, 5, "\x14\xfb\x9c\x03\xd9", 5}, + {"FPucA9k=", 8, ECE_BASE64URL_REJECT_PADDING, 6, NULL, 0}, + + {"FPucAw", 6, ECE_BASE64URL_REQUIRE_PADDING, 0, NULL, 0}, + {"FPucAw", 6, ECE_BASE64URL_IGNORE_PADDING, 4, "\x14\xfb\x9c\x03", 4}, + {"FPucAw", 6, ECE_BASE64URL_REJECT_PADDING, 4, "\x14\xfb\x9c\x03", 4}, + + {"FPucAw==", 8, ECE_BASE64URL_REQUIRE_PADDING, 4, "\x14\xfb\x9c\x03", 4}, + {"FPucAw==", 8, ECE_BASE64URL_IGNORE_PADDING, 4, "\x14\xfb\x9c\x03", 4}, + {"FPucAw==", 8, ECE_BASE64URL_REJECT_PADDING, 6, NULL, 0}, +}; + +void +test_base64url_encode(void) { + size_t tests = + sizeof(base64url_encode_tests) / sizeof(base64url_encode_test_t); + for (size_t i = 0; i < tests; i++) { + base64url_encode_test_t t = base64url_encode_tests[i]; + + size_t requiredBase64Len = + ece_base64url_encode(t.binary, t.binaryLen, t.paddingPolicy, NULL, 0); + ece_assert( + requiredBase64Len == t.base64Len, + "Got required length %zu for `%s` with padding policy %d; want %zu", + requiredBase64Len, t.base64, t.paddingPolicy, t.base64Len); + + char* base64 = malloc(requiredBase64Len + 1); + ece_assert(base64, + "Failed to allocate string for `%s` with padding policy %d", + t.base64, t.paddingPolicy); + + size_t actualBase64Len = ece_base64url_encode( + t.binary, t.binaryLen, t.paddingPolicy, base64, requiredBase64Len + 1); + ece_assert(actualBase64Len == t.base64Len, + "Got length %zu for `%s` with padding policy %d; want %zu", + actualBase64Len, t.base64, t.paddingPolicy, t.base64Len); + + free(base64); + } +} + +void +test_base64url_decode(void) { + size_t tests = + sizeof(base64url_decode_tests) / sizeof(base64url_decode_test_t); + for (size_t i = 0; i < tests; i++) { + base64url_decode_test_t t = base64url_decode_tests[i]; + + size_t requiredBinaryLen = + ece_base64url_decode(t.base64, t.base64Len, t.paddingPolicy, NULL, 0); + ece_assert(requiredBinaryLen == t.requiredBinaryLen, + "Got required length %zu for `%s` with padding %d; want %zu", + requiredBinaryLen, t.base64, t.paddingPolicy, + t.requiredBinaryLen); + + uint8_t* binary = calloc(requiredBinaryLen, sizeof(uint8_t)); + ece_assert(binary, "Failed to allocate buffer for `%s` with padding %d", + t.base64, t.paddingPolicy); + + size_t binaryLen = ece_base64url_decode( + t.base64, t.base64Len, t.paddingPolicy, binary, requiredBinaryLen); + ece_assert(binaryLen == t.binaryLen, + "Got length %zu for `%s` with padding %d; want %zu", binaryLen, + t.base64, t.paddingPolicy, t.binaryLen); + ece_assert(!memcmp(binary, t.binary, binaryLen), + "Wrong output for `%s` with padding %d", t.base64, + t.paddingPolicy); + + free(binary); + } +} diff --git a/mobile/ios/ThirdParty/ecec/test/decrypt/aes128gcm.c b/mobile/ios/ThirdParty/ecec/test/decrypt/aes128gcm.c new file mode 100644 index 0000000000..68521b68ac --- /dev/null +++ b/mobile/ios/ThirdParty/ecec/test/decrypt/aes128gcm.c @@ -0,0 +1,497 @@ +#include "test.h" + +#include + +typedef struct webpush_aes128gcm_decrypt_ok_test_s { + const char* desc; + const char* plaintext; + const char* recvPrivKey; + const char* authSecret; + const char* payload; + size_t payloadLen; + size_t maxPlaintextLen; + size_t plaintextLen; +} webpush_aes128gcm_decrypt_ok_test_t; + +static webpush_aes128gcm_decrypt_ok_test_t + webpush_aes128gcm_decrypt_ok_tests[] = { + { + .desc = "rs = 24, pad = 0", + .plaintext = "I am the walrus", + .recvPrivKey = "\xc8\x99\xd1\x1d\x32\xe2\xb7\xe6\xfe\x74\x98\x78\x6f\x50" + "\xf2\x3b\x98\xac\xe5\x39\x7a\xd2\x61\xde\x39\xba\x64\x49" + "\xec\xc1\x2c\xad", + .authSecret = + "\x99\x6f\xad\x8b\x50\xaa\x2d\x02\xb8\x3f\x26\x41\x2b\x2e\x2a\xee", + .payload = "\x49\x5c\xe6\xc8\xde\x93\xa4\x53\x9e\x86\x2e\x86\x34\x99\x3c" + "\xbb\x00\x00" + "\x00\x18\x41\x04\x3c\x33\x78\xa2\xc0\xab\x95\x4e\x14\x98\x71" + "\x8e\x85\xf0" + "\x8b\xb7\x23\xfb\x7d\x25\xe1\x35\xa6\x63\xfe\x38\x58\x84\xeb" + "\x81\x92\x33" + "\x6b\xf9\x0a\x54\xed\x72\x0f\x1c\x04\x5c\x0b\x40\x5e\x9b\xbc" + "\x3a\x21\x42" + "\xb1\x6c\x89\x08\x67\x34\xc3\x74\xeb\xaf\x70\x99\xe6\x42\x7e" + "\x2d\x32\xc8" + "\xad\xa5\x01\x87\x03\xc5\x4b\x10\xb4\x81\xe1\x02\x7d\x72\x09" + "\xd8\xc6\xb4" + "\x35\x53\xfa\x13\x3a\xfa\x59\x7f\x2d\xdc\x45\xa5\xba\x81\x40" + "\x94\x4e\x64" + "\x90\xbb\x8d\x6d\x99\xba\x1d\x02\xe6\x0d\x95\xf4\x8c\xe6\x44" + "\x47\x7c\x17" + "\x23\x1d\x95\xb9\x7a\x4f\x95\xdd", + .payloadLen = 152, + .maxPlaintextLen = 18, + .plaintextLen = 15, + }, + { + .desc = "rs = 49, pad = 84; ciphertext length falls on record boundary", + .plaintext = "Hello, world", + .recvPrivKey = "\x67\x00\x4a\x4e\xa8\x20\xde\xed\x8e\x49\xdb\x5e\x94\x80" + "\xe6\x3d\x3e\xa3\xcc\xe1\xae\x8e\x1a\x60\x60\x97\x13\xd5" + "\x27\xd0\x01\xef", + .authSecret = + "\x95\xf1\x75\x70\xe5\x08\xef\x6a\x2b\x2a\xd1\xb4\xf5\xca\xde\x33", + .payload = + "\xfb\x28\x83\xce\xc1\xc4\xfc\xad\xd6\xd1\x37\x1f\x6e\xa4\x91\xe0\x00" + "\x00\x00\x31\x41\x04\x2d\x44\x1e\xe7\xf9\xff\x6a\x03\x29\xa6\x49\x27" + "\xd0\x52\x4f\xdb\xe7\xb2\x2c\x6f\xb6\x5e\x10\xab\x4f\xdc\x03\x8f\x94" + "\x42\x0a\x0c\xa3\xfa\x28\xda\xd3\x6c\x84\xec\x91\xa1\x62\xea\xe0\x78" + "\xfa\xad\x2c\x1c\xed\x78\xde\x81\x13\xe1\x96\x02\xb2\x0e\x89\x4f\x49" + "\x76\xb9\x73\xe2\xfc\xf6\x82\xfa\x0c\x8c\xcd\x9a\xf3\xd5\xbf\xf1\xed" + "\xe1\x6f\xad\x5a\x31\xce\x19\xd3\x8b\x5e\x1f\xe1\xf7\x8a\x4f\xad\x84" + "\x2b\xbc\x10\x25\x4c\x2c\x6c\xdd\x96\xa2\xb5\x52\x84\xd9\x72\xc5\x3c" + "\xad\x8c\x3b\xac\xb1\x0f\x5f\x57\xeb\x0d\x4a\x43\x33\xb6\x04\x10\x2b" + "\xa1\x17\xca\xe2\x91\x08\xfb\xd9\xf6\x29\xa8\xba\x69\x60\xdd\x01\x94" + "\x5b\x39\xed\x37\xba\x70\x6c\x43\x4a\x10\xfd\x2b\xd2\x09\x4f\xf9\x24" + "\x9b\xcd\xad\x45\x13\x5f\x5f\xe4\x5f\xcd\x38\x07\x1f\x8b\x2d\x39\x41" + "\xaf\xda\x43\x98\x10\xd7\x7a\xac\xaf\x7c\xe5\x0b\x54\x32\x5b\xf5\x8c" + "\x95\x03\x33\x7d\x07\x37\x85\xa3\x23\xdf\xa3\x43", + .payloadLen = 233, + .maxPlaintextLen = 99, + .plaintextLen = 12, + }, + { + .desc = "Example from draft-ietf-webpush-encryption-latest", + .plaintext = "When I grow up, I want to be a watermelon", + .recvPrivKey = "\xab\x57\x57\xa7\x0d\xd4\xa5\x3e\x55\x3a\x6b\xbf\x71\xff" + "\xef\xea\x28\x74\xec\x07\xa6\xb3\x79\xe3\xc4\x8f\x89\x5a" + "\x02\xdc\x33\xde", + .authSecret = + "\x05\x30\x59\x32\xa1\xc7\xea\xbe\x13\xb6\xce\xc9\xfd\xa4\x88\x82", + .payload = "\x0c\x6b\xfa\xad\xad\x67\x95\x88\x03\x09\x2d\x45\x46\x76\xf3" + "\x97\x00\x00\x10\x00\x41\x04\xfe\x33\xf4\xab\x0d\xea\x71\x91" + "\x4d\xb5\x58\x23\xf7\x3b\x54\x94\x8f\x41\x30\x6d\x92\x07\x32" + "\xdb\xb9\xa5\x9a\x53\x28\x64\x82\x20\x0e\x59\x7a\x7b\x7b\xc2" + "\x60\xba\x1c\x22\x79\x98\x58\x09\x92\xe9\x39\x73\x00\x2f\x30" + "\x12\xa2\x8a\xe8\xf0\x6b\xbb\x78\xe5\xec\x0f\xf2\x97\xde\x5b" + "\x42\x9b\xba\x71\x53\xd3\xa4\xae\x0c\xaa\x09\x1f\xd4\x25\xf3" + "\xb4\xb5\x41\x4a\xdd\x8a\xb3\x7a\x19\xc1\xbb\xb0\x5c\xf5\xcb" + "\x5b\x2a\x2e\x05\x62\xd5\x58\x63\x56\x41\xec\x52\x81\x2c\x6c" + "\x8f\xf4\x2e\x95\xcc\xb8\x6b\xe7\xcd", + .payloadLen = 144, + .maxPlaintextLen = 42, + .plaintextLen = 41, + }, + { + .desc = "rs = 18, pad = 0", + .plaintext = "1", + .recvPrivKey = "\x27\x43\x3f\xab\x89\x70\xb3\xcb\x52\x84\xb6\x11\x83\xef" + "\xb4\x62\x86\x56\x2c\xd2\xa7\x33\x0d\x8c\xae\x96\x09\x11" + "\xa5\x57\x1d\x0c", + .authSecret = + "\xd6\x5a\x04\xdf\x95\xf2\xdb\x5e\x60\x48\x39\xf7\x17\xdc\xde\x79", + .payload = "\x7c\xae\xbd\xbc\x20\x93\x8e\xe3\x40\xa9\x46\xf1\xbd\x4f\x68" + "\xf1\x00\x00\x00\x12\x41\x04\x37\xcf\xdb\x52\x23\xd9\xf9\x5e" + "\xaa\x02\xf6\xed\x94\x0f\xf2\x2e\xaf\x05\xb3\x62\x2e\x94\x9d" + "\xc3\xce\x9f\x33\x5e\x6e\xf9\xb2\x6a\xea\xac\xca\x0f\x74\x08" + "\x0a\x8b\x36\x45\x92\xf2\xcc\xc6\xd5\xed\xdd\x43\x00\x4b\x70" + "\xb9\x18\x87\xd1\x44\xd9\xfa\x93\xf1\x6c\x3b\xc7\xea\x68\xf4" + "\xfd\x54\x7a\x94\xec\xa8\x4b\x16\xe1\x38\xa6\x08\x01\x77", + .payloadLen = 104, + .maxPlaintextLen = 2, + .plaintextLen = 1, + }, +}; + +typedef struct webpush_aes128gcm_err_decrypt_test_s { + const char* desc; + const char* recvPrivKey; + const char* authSecret; + const char* payload; + size_t payloadLen; + size_t maxPlaintextLen; + int err; +} webpush_aes128gcm_err_decrypt_test_t; + +static webpush_aes128gcm_err_decrypt_test_t + webpush_aes128gcm_err_decrypt_tests[] = { + { + // Header block shorter than 21 bytes. + .desc = "Missing header block", + .recvPrivKey = "\x1b\xe8\x3f\x38\x33\x2e\xf0\x96\x81\xfa\xf3\xf3\x07\xb1" + "\xff\x2e\x10\xca\xb7\x8c\xc7\xcd\xab\x68\x3a\xc0\xee\x92" + "\xac\x3f\x6e\xe1", + .authSecret = + "\x34\x71\xbb\x98\x48\x1e\x02\x53\x3b\xf3\x95\x42\xbc\xf3\xdb\xa4", + .payload = "\x45\xb7\x4d\x2b\x69\xbe\x9b\x07\x4d\xe3\xb3\x5a\xa8\x7e\x7c" + "\x15\x61\x1d", + .payloadLen = 18, + .maxPlaintextLen = 0, + .err = ECE_ERROR_SHORT_HEADER, + }, + { + // Sender key shorter than 65 bytes. + .desc = "Truncated sender key", + .recvPrivKey = "\xce\x88\xe8\xe0\xb3\x05\x7a\x47\x52\xeb\x4c\x8f\xa9\x31" + "\xeb\x62\x1c\x30\x2d\xa5\xad\x03\xb8\x1a\xf4\x59\xcf\x67" + "\x35\x56\x0c\xae", + .authSecret = + "\x5c\x31\xe0\xd9\x6d\x9a\x13\x98\x99\xac\x09\x69\xd3\x59\xf7\x40", + .payload = "\xde\x5b\x69\x6b\x87\xf1\xa1\x5c\xb6\xad\xeb\xdd\x79\xd6\xf9" + "\x9e\x00\x00\x00\x12\x01\x00\xb6\xbc\x18\x26\xc3\x7c\x9f\x73" + "\xdd\x6b\x48\x59\xc2\xb5\x05\x18\x19\x52", + .payloadLen = 40, + .maxPlaintextLen = 2, + .err = ECE_ERROR_COMPUTE_SECRET, + }, + { + // The payload is encrypted with only the first 12 bytes of the auth + // secret. + .desc = "Truncated auth secret", + .recvPrivKey = "\x60\xc7\x63\x6a\x51\x7d\xe7\x03\x9a\x0a\xc2\xd0\xe3\x06" + "\x44\x00\x79\x4c\x78\xe7\xe0\x49\x39\x81\x29\xa2\x27\xce" + "\xe0\xf9\xa8\x01", + .authSecret = + "\x35\x5a\x38\xcd\x6d\x9b\xef\x15\x99\x0e\x2d\x33\x08\xdb\xd6\x00", + .payload = "\x81\x15\xf4\x98\x8b\x8c\x39\x2a\x7b\xac\xb4\x3c\x8f\x1a\xc5" + "\x65\x00\x00\x00\x12\x41\x04\x19\x94\x48\x3c\x54\x1e\x9b\xc3" + "\x9a\x6a\xf0\x3f\xf7\x13\xaa\x77\x45\xc2\x84\xe1\x38\xa4\x2a" + "\x24\x35\xb7\x97\xb2\x0c\x4b\x69\x8c\xf5\x11\x8b\x4f\x85\x55" + "\x31\x7c\x19\x0e\xab\xeb\xfa\xb7\x49\xc1\x64\xd3\xf6\xbd\xeb" + "\xe0\xd4\x41\x71\x91\x31\xa3\x57\xd8\x89\x0a\x13\xc4\xdb\xd4" + "\xb1\x6f\xf3\xdd\x5a\x83\xf7\xc9\x1a\xd6\xe0\x40\xac\x42\x73" + "\x0a\x7f\x0b\x3c\xd3\x24\x5e\x9f\x8d\x6f\xf3\x1c\x75\x1d\x41" + "\x0c\xfd", + .payloadLen = 122, + .maxPlaintextLen = 4, + .err = ECE_ERROR_DECRYPT, + }, + { + .desc = "Early final record", + .recvPrivKey = "\x5d\xda\x1d\x91\x8b\xc4\x07\xba\x3c\xda\x12\xcb\x80\x14" + "\xd4\x9a\xa7\xe0\x26\x90\x02\x82\x03\x04\x46\x6b\xc8\x00" + "\x34\xca\x92\x40", + .authSecret = + "\x40\xc2\x41\xfd\xe4\x26\x9e\xe1\xe6\xd7\x25\x59\x2d\x98\x27\x18", + .payload = "\xdb\xe2\x15\x50\x7d\x1a\xd3\xd2\xea\xea\xbe\xae\x6e\x87\x4d" + "\x8f\x00\x00\x00\x12\x41\x04\x7b\xc4\x34\x3f\x34\xa8\x34\x8c" + "\xdc\x4e\x46\x2f\xfc\x7c\x40\xaa\x6a\x8c\x61\xa7\x39\xc4\xc4" + "\x1d\x45\x12\x55\x05\xf7\x0e\x9f\xc5\xf9\xef\xa8\x68\x52\xdd" + "\x48\x8d\xcf\x8e\x8e\xa2\xca\xfb\x75\xe0\x7a\xbd\x5e\xe7\xc9" + "\xd5\xc0\x38\xba\xfe\xf0\x79\x57\x1b\x0b\xda\x29\x44\x11\xce" + "\x98\xc7\x6d\xd0\x31\xc0\xe5\x80\x57\x7a\x49\x80\xa3\x75\xe4" + "\x5e\xd3\x04\x29\xbe\x0e\x2e\xe9\xda\x7e\x6d\xf8\x69\x6d\x01" + "\xb8\xec", + .payloadLen = 122, + .maxPlaintextLen = 4, + .err = ECE_ERROR_DECRYPT_PADDING, + }, +}; + +typedef struct aes128gcm_ok_decrypt_test_s { + const char* desc; + const char* plaintext; + const char* ikm; + const char* payload; + size_t payloadLen; + size_t maxPlaintextLen; + size_t plaintextLen; +} aes128gcm_ok_decrypt_test_t; + +static aes128gcm_ok_decrypt_test_t aes128gcm_ok_decrypt_tests[] = { + { + .desc = "rs = 18, pad = 8", + .plaintext = "When I grow up, I want to be a watermelon", + .ikm = "\x28\xc0\x66\x11\x4a\x2d\xa5\x21\xca\x89\xf4\x21\x9d\xa8\xac\xc0", + .payload = + "\x1f\xc2\xec\x59\x4d\xbd\xa8\xc8\xab\x26\x25\x47\x04\x65\xb8\xcd\x00\x00" + "\x00\x12\x00\x92\x56\xfe\x1c\x43\x4f\x71\x8e\x85\x16\x3a\x0f\x52\x69\xc1" + "\xb8\x24\x55\x73\x60\x7d\x06\x06\xc3\x97\xfc\xfd\xc3\x27\xd5\xf9\x0c\x44" + "\x8d\x6a\x11\xa0\xc4\xb8\xd0\x51\xc8\x54\x94\xb0\x0f\xb5\xeb\xb9\xe6\x85" + "\x38\x2f\x88\xee\x5a\xce\x19\x1b\xfa\x73\x1d\xa2\xc9\xb2\x3f\x0a\xe4\xfe" + "\x4b\x9a\xd5\xf5\x4d\xf0\xec\xc8\x17\x9f\xc6\xdb\xed\x3a\x94\x33\xbe\x4f" + "\x92\xa8\xdd\xf1\x0d\x5f\x29\x9f\x76\x73\xfb\x79\x33\x69\xc9\x6b\xf5\x20" + "\x5b\x4e\xa5\x47\xef\xa3\xd4\x4b\x6c\xaa\x47\xac\x97\x9a\xa1\x69\x45\x2a" + "\xf6\xf6\x84\x65\xda\xba\x9b\x8a\xb3\x9c\xed\x91\x15\xd4\x4f\xbb\x7c\xf6" + "\xc6\xfa\x0f\x86\x71\xa2\xa1\x2c\xf6\x18\x18\x86\x94\xf1\x7c\x2f\x63\xb7" + "\x46\xe0\x6e\x9a\x51\x20\x6a\x8c\x54\xc9\x91\x54\xb1\x84\xa9\xec\x8a\x29" + "\x71\x4e\xfd\xb6\x8f\xde\xe4\xc4\x2f\x57\xb3\x2e\x48\x8d\x5c\x47\x51\x05" + "\xd0\x57\xb6\x55\x15\xc4\xa0\xeb\x59\x5b\xd6\xe8\xa7\x11\x65\x18\xad\xfb" + "\xc5\xdf\xbc\x51\x71\x01\xae\x72\x2b\x19\x14\xa1\x47\x3e\x35\xbb\x52\xa7" + "\xc9\xad\x22\x09\xc6\xea\x8f\x2b\x60\x5f\x8d\xf9\x78\x65\x4e\xd5\x2c\x71" + "\x17\x5c\x12\x4e\xb3\xa5\x6e\xfa\xfe\x64\x77\xd8\x05\x07\x4d\xd0\x29\x15" + "\x65\x37\x4b\xb1\x02\x8c\x9b\xbd\x59\xd6\x4d\x56\x27\xe7\x28\x02\x5a\x30" + "\x59\x10\x0f\x48\xe8\x88\x5f\x7f\xe4\x4e\x0d\xec\xbb\x7b\x98\x08\x3d\x85" + "\xe1\xc8\x2b\x11\x8a\x8a\xf5\xb3\x8f\x33\xb3\xb6\x7b\xa6\xd5\x8e\x58\xc1" + "\x3a\xff\xaf\x8c\x2b\x99\x9d\x4f\xc2\x09\xed\x73\x7a\x04\x74\x93\x48\x1b" + "\xfa\xfd\x71\x9d\x49\x8d\xf0\xa9\x8c\x6f\x43\x48\xc7\x24\x3a\xa6\x78\x9a" + "\xf3\x36\x85\x4b\x7e\x87\xf9\x5a\x05\x47\xcf\x73\x5e\xc3\x83\xa8\x27\x4d" + "\xdc\xf5\xd9\x76\x43\x85\x37\x36\xb4\xc6\x06\x3f\x48\x95\xab\x38\x38\xc9" + "\x99\x9c\xec\x7b\x73\x1c\xda\xcb\xd5\x0f\x8c\x06\xde\x9f\xe8\x0a\xad\xaf" + "\x91\xd1\x1b\x9a\x35\xaa\xdf\x41\xa0\x5c\x6b\xae\xda\x0c\x6d\x00\xb4\xa8" + "\xc0\xc3\x69\xf9\x8f\x4c\x6e\x6a\x97\x76\xab\x41\x7e\x28\x39\x1b\x47\x5c" + "\xe7\xfc\x01\x65\xdb\xe4\x9e\xf9\x89\x1f\x9c\xef\x82\xe2\x86\x7e\xd6\xd6" + "\x7c\x4a\x5a\x71\xda\xa1\xf7\x5d\x26\x5f\x85\x92\xa8\x1d\xb4\x8c\xbc\x92" + "\xc3\x82\xd6\x3a\x96\xf5\x80\x0f\xa8\xec\xa9\xe2\x02\x7b\xaf\xb7\x4b\xc9" + "\xe3\x3b\xdc\xd3\xb8\xf4\xd8\xe0\x5f\x36\xdd\xa5\x44\xf8\x97\x5e\xcb\xea" + "\x47\x8d\xb8\x36\x61\xa1\xdb\xc5\xfc\xcb\x7f\xeb\x05\x57\xde\xd7\x3a\x37" + "\x90\xc3\x52\x69\xfa\x59\xe4\x75\x0e\x55\xc7\x29\xa0\x08\xc9\x8c\xe9\xee" + "\x88\x82\xe0\xc2\xae\xaf\x1e\xbe\x40\x3b\xe9\x6d\xaa\x25\xb4\x2a\xc0\x1b" + "\x6a\xd4\x35\x5b\xc3\x60\xcd\xd1\x31\x10\xe3\xff\xc7\x6a\xb4\x51\xf5\x9e" + "\x04\xa8\xab\x3f\x1a\x4a\x69\xdf\x21\x91\xab\x4b\x60\xfd\x31\x76\x13\x2b" + "\x8f\x99\x1d\x3a\xb2\x96\xa0\x36\x93\x36\xb5\x35\xaa\xcc\x15\x97\x7d\x50" + "\x5b\xe2\xc5\xd4\xb6\xb7\xbc\x55\xd8\x3c\xd1\x7c\x1e\x80\x35\x7f\x4a\x21" + "\x8d\x72\x93\x3f\xa6\x09\x74\x73\x29\x6d\x7d\xdc\x30\xd7\xa1\x7b\x73\x23" + "\xac\x17\x3e\xc3\x47\x72\x45\x1a\xa6\x70\x95\xca\xcf\xbc\x87\x6c\x05\x56" + "\xa7\xae\x2f\x2c\x64\x55\x50\xd8\xab\x05\xe6\xa7\x87\xa5\x5a\x1c\x0c\xe1" + "\x59\x7b\x95\x8e\xe7\xea\xff\x10\x29\x93\x02\xd9\x9c\x35\xca\xc9\x83\xb9" + "\x6c\x0f\xec\xaf\x61\x59\x3a\x17\x66\xd3\xbc\xc7\xc2\xd5\x00\x4a\x4c\x43" + "\x91\xcb\x41\xb1\x36\x79\x35\xe3\x9d\x73\xf3\xa2\xe0\x84\x59\xd2\x83\x2c" + "\x18\x34\xf2\x60\x6a\x87\x40\x10\xd7\xcf\x17\x7e\x7b\xcf\x61\xad\x41\x3d" + "\x0f\xfc\xe3\x3d\x6b\xef\x39\xb9\x61\x39\xda\x24\xaf\xc9\xac\xe7\x94\x28" + "\x8d\x79\x75\xac\x74\xc9\x86\x66\x1d\x40\x34\x42\x25\xf7\x99\xf5\x96\x35" + "\xa9\x1f\x98\x7b\x54\x42\x3a\x5e\x10\x60\x9b\x6d\x8e\xb4\xda\xe5\xc2\xc8" + "\x2e\x53\xae\xc3\xa3\xdd\xe5\xaf\xbf\x06\x2c\x42\xe2\x95\x91\xd9\x3e\x49" + "\xe4\x54\x80\x90\xb5\x22\x7e\x13\xda\x62\x70\x14\x7e\x5d\x9d\xee\x3f\x2e" + "\x8d\x2f\x7d\xe0\xaf\x1d\xd7\x61\x27\x9d\xd3\xf2\x8a\xce\x13\x74\x73\x15" + "\x11\xf2\x1a", + .payloadLen = 903, + .maxPlaintextLen = 98, + .plaintextLen = 41, + }, +}; + +typedef struct aes128gcm_err_decrypt_test_s { + const char* desc; + const char* ikm; + const char* payload; + size_t payloadLen; + size_t maxPlaintextLen; + int err; +} aes128gcm_err_decrypt_test_t; + +static aes128gcm_err_decrypt_test_t aes128gcm_err_decrypt_tests[] = { + { + .desc = "Truncated ciphertext, rs = 18", + .ikm = "\x28\xc0\x66\x11\x4a\x2d\xa5\x21\xca\x89\xf4\x21\x9d\xa8\xac\xc0", + .payload = "\x1f\xc2\xec\x59\x4d\xbd\xa8\xc8\xab\x26\x25\x47\x04\x65\xb8" + "\xcd\x00\x00\x00\x12\x00\x92\x56\xfe\x1c\x43\x4f\x71\x8e\x85" + "\x16\x3a\x0f\x52\x69\xc1\xb8\x24\x55", + .payloadLen = 40, + .maxPlaintextLen = 0, + .err = ECE_ERROR_DECRYPT, + }, + { + .desc = "rs <= block overhead", + .ikm = "\x2f\xb1\x75\xc2\x71\xb9\x2f\x6b\x55\xe4\xf2\xa2\x52\xd1\x45\x43", + .payload = "\x76\xf9\x1d\x48\x4e\x84\x91\xda\x55\xc5\xf7\xbf\xe6\xd3\x3e" + "\x89\x00\x00\x00\x02\x00", + .payloadLen = 21, + .maxPlaintextLen = 0, + .err = ECE_ERROR_INVALID_RS, + }, + { + .desc = "Zero plaintext", + .ikm = "\x64\xc7\x0e\x64\xa7\x25\x55\x14\x51\xf2\x08\xdf\xba\xa0\xb9\x72", + .payload = "\xaa\xd2\x05\x7d\x33\x53\xb7\xff\x37\xbd\xe4\x2a\xe1\xd5\x0f" + "\xda\x00\x00\x00\x20\x00\xbb\xc7\xb9\x65\x76\x0b\xf0\x66\x2b" + "\x93\xf4\xe5\xd6\x94\xb7\x65\xf0\xcd\x15\x9b\x28\x01\xa5", + .payloadLen = 44, + .maxPlaintextLen = 7, + .err = ECE_ERROR_ZERO_PLAINTEXT, + }, + { + .desc = "Bad early padding delimiter", + .ikm = "\x64\xc7\x0e\x64\xa7\x25\x55\x14\x51\xf2\x08\xdf\xba\xa0\xb9\x72", + .payload = "\xaa\xd2\x05\x7d\x33\x53\xb7\xff\x37\xbd\xe4\x2a\xe1\xd5\x0f" + "\xda\x00\x00\x00\x20\x00\xb9\xc7\xb9\x65\x76\x0b\xf0\x9e\x42" + "\xb1\x08\x43\x38\x75\xa3\x06\xc9\x78\x06\x0a\xfc\x7c\x7d\xe9" + "\x52\x85\x91\x8b\x58\x02\x60\xf3\x45\x38\x7a\x28\xe5\x25\x66" + "\x2f\x48\xc1\xc3\x32\x04\xb1\x95\xb5\x4e\x9e\x70\xd4\x0e\x3c" + "\xf3\xef\x0c\x67\x1b\xe0\x14\x49\x7e\xdc", + .payloadLen = 85, + .maxPlaintextLen = 32, + .err = ECE_ERROR_DECRYPT_PADDING, + }, + { + .desc = "Bad final padding delimiter", + .ikm = "\x64\xc7\x0e\x64\xa7\x25\x55\x14\x51\xf2\x08\xdf\xba\xa0\xb9\x72", + .payload = "\xaa\xd2\x05\x7d\x33\x53\xb7\xff\x37\xbd\xe4\x2a\xe1\xd5\x0f" + "\xda\x00\x00\x00\x20\x00\xba\xc7\xb9\x65\x76\x0b\xf0\x9e\x42" + "\xb1\x08\x4a\x69\xe4\x50\x1b\x8d\x49\xdb\xc6\x79\x23\x4d\x47" + "\xc2\x57\x16", + .payloadLen = 48, + .maxPlaintextLen = 11, + .err = ECE_ERROR_DECRYPT_PADDING, + }, + { + .desc = "Invalid auth tag", + .ikm = "\x64\xc7\x0e\x64\xa7\x25\x55\x14\x51\xf2\x08\xdf\xba\xa0\xb9\x72", + .payload = "\xaa\xd2\x05\x7d\x33\x53\xb7\xff\x37\xbd\xe4\x2a\xe1\xd5\x0f" + "\xda\x00\x00\x00\x20\x00\xbb\xc6\xb1\x1d\x46\x3a\x7e\x0f\x07" + "\x2b\xbe\xaa\x44\xe0\xd6\x2e\x4b\xe5\xf9\x5d\x25\xe3\x86\x71" + "\xe0\x7d", + .payloadLen = 47, + .maxPlaintextLen = 10, + .err = ECE_ERROR_DECRYPT, + }, + { + // 2 records; last record is "\x00" without a delimiter. + .desc = "rs = 21, truncated padding for last record", + .ikm = "\x1a\x5c\x05\x64\x16\xdf\x83\x73\x87\x51\x01\xd1\x11\x98\x47\x83", + .payload = "\x53\x06\xdc\x45\xdd\x8e\x51\x00\x16\x53\x3c\x1e\xba\xe5\x50" + "\x53\x00\x00\x00\x15\x00\xa7\x0d\x92\x4e\xe6\x08\xd0\xc1\xc1" + "\x00\x88\x5a\xe8\x78\x1d\xd1\x47\x67\x02\x12\x63\xf7\x9d\x22" + "\xa9\x44\x8d\xb2\x33\x6e\xe0\xe5\x72\xe2\x3c\x38\x49\x70", + .payloadLen = 59, + .maxPlaintextLen = 6, + .err = ECE_ERROR_ZERO_PLAINTEXT, + }, + { + // 2 records; last record is just the auth tag. + .desc = "rs = 21, auth tag for last record", + .ikm = "\xc1\xc9\xc0\x91\x9d\x81\x0a\xe7\xd9\xe8\x0c\x45\xbc\x21\xa9\xfa", + .payload = "\xc1\xaf\x29\x07\x6f\x69\x25\x60\xde\x6d\x1f\xde\x02\x11\x69" + "\x79\x00\x00\x00\x15\x00\x46\x9f\xde\x73\xa7\x8a\x2a\x66\x1d" + "\xb0\xf1\xae\x55\xec\xec\x86\x6a\xaa\xe5\xf3\x04\xa3\x3e\xc3" + "\xb0\xbb\x16\xe9\x0a\xab\xc4\xba\xe0\xed\xbb\x73\x46", + .payloadLen = 58, + .maxPlaintextLen = 5, + .err = ECE_ERROR_SHORT_BLOCK, + }, +}; + +void +test_webpush_aes128gcm_decrypt_ok(void) { + size_t tests = sizeof(webpush_aes128gcm_decrypt_ok_tests) / + sizeof(webpush_aes128gcm_decrypt_ok_test_t); + for (size_t i = 0; i < tests; i++) { + webpush_aes128gcm_decrypt_ok_test_t t = + webpush_aes128gcm_decrypt_ok_tests[i]; + + const void* recvPrivKey = t.recvPrivKey; + const void* authSecret = t.authSecret; + const void* payload = t.payload; + + size_t plaintextLen = + ece_aes128gcm_plaintext_max_length(payload, t.payloadLen); + ece_assert(plaintextLen == t.maxPlaintextLen, + "Got plaintext max length %zu for `%s`; want %zu", plaintextLen, + t.desc, t.maxPlaintextLen); + + uint8_t* plaintext = calloc(plaintextLen, sizeof(uint8_t)); + + int err = ece_webpush_aes128gcm_decrypt( + recvPrivKey, ECE_WEBPUSH_PRIVATE_KEY_LENGTH, authSecret, + ECE_WEBPUSH_AUTH_SECRET_LENGTH, payload, t.payloadLen, plaintext, + &plaintextLen); + ece_assert(!err, "Got %d decrypting payload for `%s`", err, t.desc); + + ece_assert(plaintextLen == t.plaintextLen, + "Got plaintext length %zu for `%s`; want %zu", plaintextLen, + t.desc, t.plaintextLen); + ece_assert(!memcmp(plaintext, t.plaintext, plaintextLen), + "Wrong plaintext for `%s`", t.desc); + + free(plaintext); + } +} + +void +test_webpush_aes128gcm_decrypt_err(void) { + size_t tests = sizeof(webpush_aes128gcm_err_decrypt_tests) / + sizeof(webpush_aes128gcm_err_decrypt_test_t); + for (size_t i = 0; i < tests; i++) { + webpush_aes128gcm_err_decrypt_test_t t = + webpush_aes128gcm_err_decrypt_tests[i]; + + const void* recvPrivKey = t.recvPrivKey; + const void* authSecret = t.authSecret; + const void* payload = t.payload; + + size_t plaintextLen = + ece_aes128gcm_plaintext_max_length(payload, t.payloadLen); + ece_assert(plaintextLen == t.maxPlaintextLen, + "Got plaintext max length %zu for `%s`; want %zu", plaintextLen, + t.desc, t.maxPlaintextLen); + + uint8_t* plaintext = calloc(plaintextLen, sizeof(uint8_t)); + + int err = ece_webpush_aes128gcm_decrypt( + recvPrivKey, ECE_WEBPUSH_PRIVATE_KEY_LENGTH, authSecret, + ECE_WEBPUSH_AUTH_SECRET_LENGTH, payload, t.payloadLen, plaintext, + &plaintextLen); + ece_assert(err == t.err, "Got %d decrypting payload for `%s`; want %d", err, + t.desc, t.err); + + free(plaintext); + } +} + +void +test_aes128gcm_decrypt_ok(void) { + size_t tests = + sizeof(aes128gcm_ok_decrypt_tests) / sizeof(aes128gcm_ok_decrypt_test_t); + for (size_t i = 0; i < tests; i++) { + aes128gcm_ok_decrypt_test_t t = aes128gcm_ok_decrypt_tests[i]; + + size_t plaintextLen = ece_aes128gcm_plaintext_max_length( + (const uint8_t*) t.payload, t.payloadLen); + ece_assert(plaintextLen == t.maxPlaintextLen, + "Got plaintext max length %zu for `%s`; want %zu", plaintextLen, + t.desc, t.maxPlaintextLen); + + uint8_t* plaintext = calloc(plaintextLen, sizeof(uint8_t)); + + int err = ece_aes128gcm_decrypt((const uint8_t*) t.ikm, 16, + (const uint8_t*) t.payload, t.payloadLen, + plaintext, &plaintextLen); + ece_assert(!err, "Got %d decrypting payload for `%s`", err, t.desc); + + ece_assert(plaintextLen == t.plaintextLen, + "Got plaintext length %zu for `%s`; want %zu", plaintextLen, + t.desc, t.plaintextLen); + ece_assert(!memcmp(plaintext, t.plaintext, plaintextLen), + "Wrong plaintext for `%s`", t.desc); + + free(plaintext); + } +} + +void +test_aes128gcm_decrypt_err(void) { + size_t tests = + sizeof(aes128gcm_err_decrypt_tests) / sizeof(aes128gcm_err_decrypt_test_t); + for (size_t i = 0; i < tests; i++) { + aes128gcm_err_decrypt_test_t t = aes128gcm_err_decrypt_tests[i]; + + const void* ikm = t.ikm; + const void* payload = t.payload; + + size_t plaintextLen = + ece_aes128gcm_plaintext_max_length(payload, t.payloadLen); + ece_assert(plaintextLen == t.maxPlaintextLen, + "Got plaintext max length %zu for `%s`; want %zu", plaintextLen, + t.desc, t.maxPlaintextLen); + + uint8_t* plaintext = calloc(plaintextLen, sizeof(uint8_t)); + + int err = ece_aes128gcm_decrypt(ikm, 16, payload, t.payloadLen, plaintext, + &plaintextLen); + ece_assert(err == t.err, "Got %d decrypting payload for `%s`; want %d", err, + t.desc, t.err); + + free(plaintext); + } +} diff --git a/mobile/ios/ThirdParty/ecec/test/decrypt/aesgcm.c b/mobile/ios/ThirdParty/ecec/test/decrypt/aesgcm.c new file mode 100644 index 0000000000..fe641a64ab --- /dev/null +++ b/mobile/ios/ThirdParty/ecec/test/decrypt/aesgcm.c @@ -0,0 +1,306 @@ +#include "test.h" + +#include + +typedef struct webpush_aesgcm_decrypt_ok_test_s { + const char* desc; + const char* plaintext; + const char* recvPrivKey; + const char* authSecret; + const char* ciphertext; + const char* cryptoKey; + const char* encryption; + size_t ciphertextLen; + size_t maxPlaintextLen; + size_t plaintextLen; +} webpush_aesgcm_decrypt_ok_test_t; + +typedef struct webpush_aesgcm_decrypt_err_test_s { + const char* desc; + const char* recvPrivKey; + const char* authSecret; + const char* ciphertext; + const char* cryptoKey; + const char* encryption; + size_t maxPlaintextLen; + size_t ciphertextLen; + int err; +} webpush_aesgcm_decrypt_err_test_t; + +static webpush_aesgcm_decrypt_ok_test_t webpush_aesgcm_decrypt_ok_tests[] = { + { + .desc = "rs = 24, pad = 0", + .plaintext = "Some message", + .recvPrivKey = "\xe2\x1d\xb7\x1b\xf2\xa4\x5c\x2f\x53\xbc\x14\x8a\xda\xfd" + "\x10\xec\x89\xa9\x4b\x66\x00\xb9\x17\x7c\x85\x0c\x8d\xd2" + "\xb1\x40\xc0\x18", + .authSecret = + "\x69\x30\xdc\xe8\x97\x9b\xcd\x1e\x9e\x49\xcc\xb6\xa0\xba\x38\x45", + .ciphertext = "\x3a\x8d\xf8\xc3\x61\x7d\x55\x59\xd3\x30\x57\xca\xb5\xdc" + "\x78\xf0\x06\x56\x43\xd2\xe2\xf4\xce\x83\x6a\xe5\x89\x56" + "\x05\xd4", + .cryptoKey = "dh=" + "BCHFVrflyxibGLlgztLwKelsRZp4gqX3tNfAKFaxAcBhpvYeN1yIUMrxa" + "DKiLh4LNKPtj0BOXGdr-IQ-QP82Wjo", + .encryption = "salt=zCU18Rw3A5aB_Xi-vfixmA; rs=24", + .ciphertextLen = 30, + .maxPlaintextLen = 14, + .plaintextLen = 12, + }, + { + .desc = "rs = 8, pad = 16", + .plaintext = "Yet another message", + .recvPrivKey = "\xe2\x1d\xb7\x1b\xf2\xa4\x5c\x2f\x53\xbc\x14\x8a\xda\xfd" + "\x10\xec\x89\xa9\x4b\x66\x00\xb9\x17\x7c\x85\x0c\x8d\xd2" + "\xb1\x40\xc0\x18", + .authSecret = + "\xea\x99\x70\x66\x74\xa9\x55\x46\xc5\xec\x03\xc3\x5e\xeb\x37\x51", + .ciphertext = + "\xb8\x40\xb9\x07\xfb\x51\xf9\xfb\x90\xdd\xd7\xa5\x41\xca\xf3\xac\x30" + "\xa9\xe3\x45\xba\x8a\x93\x19\x8c\x66\x7b\xf1\x44\x83\x27\x9b\x0c\x8f" + "\xee\x9b\x00\xe5\x46\xdc\x02\xba\x26\xa1\x65\xf4\x4e\x80\xa1\x68\x81" + "\x61\x8b\xcc\x65\xfc\x13\x85\x5c\x66\x0e\x7c\x3a\x44\x7b\x55\x78\xb2" + "\x85\x33\x90\xd6\x82\x5d\x44\xc2\x43\xa8\x87\x01\xf6\x12\x18\x83\x0f" + "\x48\x0c\xde\x2a\x3e\x77\xbf\x56\x2b\x33\x8f\x64\x4f\x6b\x0d\x65\x12" + "\xbf\xae\x09\xb2\xd2\x26\x49\xf5\xff\x00\x34\x1f\x00\x1a\xef\x6d\x99" + "\x1e\x69\x6c\x61\xe7\x71\x06\xe7\xd4\x0c\x38\x49\x45\x26\xa1\xee\x7b" + "\x87\x4c\x51\x1a\x6c\x31\x78", + .cryptoKey = "dh=BEaA4gzA3i0JDuirGhiLgymS4hfFX7TNTdEhSk_" + "HBlLpkjgCpjPL5c-GL9uBGIfa_fhGNKKFhXz1k9Kyens2ZpQ", + .encryption = "salt=ZFhzj0S-n29g9P2p4-I7tA; rs=8", + .ciphertextLen = 143, + .maxPlaintextLen = 47, + .plaintextLen = 19, + }, + { + .desc = "rs = 3, pad = 0", + .plaintext = "Small record size", + .recvPrivKey = "\xe2\x1d\xb7\x1b\xf2\xa4\x5c\x2f\x53\xbc\x14\x8a\xda\xfd" + "\x10\xec\x89\xa9\x4b\x66\x00\xb9\x17\x7c\x85\x0c\x8d\xd2" + "\xb1\x40\xc0\x18", + .authSecret = + "\x83\x6a\xd6\x54\x75\x02\xa5\x4c\x60\x70\xbf\x53\xcf\xbb\xf2\x79", + .ciphertext = + "\xa1\x8e\x1e\xe5\xe0\xda\xb4\x35\x6d\xd9\xfa\x50\xca\x5c\x5b\x3c\x93" + "\x3e\xde\xfa\xdf\x84\x36\xac\x7c\xf7\x3c\x43\x53\xd6\xb7\x8b\x4f\x7c" + "\xc5\x5b\xcf\xfb\x03\x34\xbf\xdc\xbe\xbd\x03\x5d\x79\x1d\x17\x34\xb5" + "\x97\x1b\x09\xb2\x3e\x79\xd1\x44\xb1\xe0\xc3\x25\xd3\x58\xa1\x8c\x89" + "\x97\x0a\x3a\xf0\xf5\x1e\x71\x16\x01\x6b\x08\x0a\x89\x0f\x71\xb0\x5c" + "\x0d\x6f\xcd\x2e\x13\xe7\x2b\xc6\x55\xb5\x29\xdd\x29\xdd\x30\x37\x53" + "\xd4\xe0\x9c\x12\x87\xd3\x9f\xb5\x42\xeb\x16\x7e\xcc\xff\x2b\xcd\x24" + "\x1f\x0c\x20\x41\xc6\x63\xd4\xec\xe2\x12\xce\xbf\x19\xe0\x7c\xb3\x14" + "\x21\x89\x78\x17\xea\x89\x9e\xf2\x51\xf3\x65\x28\x5d\x71\xe5\x46\x99" + "\xe1\x7e\xa2\x53\xf4\xd7\xc7\x92\xa5\x43\x2f\xed\xa1\x5d\x5d\x5d\x9d" + "\x9e\x10\x03\x07\x25\x47\x12\x74\x0a\xef\x07\xac\xd1\xa4\x57\x44\x23" + "\x17\x5c\x1d\xc2\x72\xa3\x50\x48\x62\x00\x18\x4c\x37\x40\x23\x2f\xff" + "\x20\x95\x42\x3d\x8f\xdb\xd8\x21\x39\x5b\xfe\xa8\x86\x56\x0a\x07\xbc" + "\x43\xc8\x7f\x20\x88\xdb\x96\x59\x70\x20\x06\x8c\xd4\x7d\x27\x0b\x51" + "\x58\xcf\x0b\x7f\x32\x25\x3a\x40\x52\x2e\xb0\xea\x95\xe0\x5d\x45\x9f" + "\x7d\xe9\xdd\x7b\x91\x6b\x79\x0a\x67\x30\x52\xf5\x2f\x81\xbe\xdf\xcc" + "\x2c\x23\xff\x88\x48\x83\xb3\x74\xef\x10\x22\xb5\xc7\xc6\x6b\x5c\xd5" + "\x2e\x71\xcd\x8b\xbc\x6c\xca\x92\x80\x28\xa7\xbc\x21\x30\x75\xe1\x85" + "\x0a\x68\xa0\x85\x18\xe3\xf2\xdd\x73\x45\x6a\x34\x0f\x5e\x2b\x1d\xb4" + "\x0b\x3d\x1f\xdb\xd4\xfe\x52\xbc\x47\x89\x4e\xd4\xbd\x36\x6d\xe2\x89" + "\xb3", + .cryptoKey = "dh=BCg6ZIGuE2ZNm2ti6Arf4CDVD_8--" + "aLXAGLYhpghwjl1xxVjTLLpb7zihuEOGGbyt8Qj0_" + "fYHBP4ObxwJNl56bk", + .encryption = "salt=5LIDBXbvkBvvb7ZdD-T4PQ; rs=3", + .ciphertextLen = 341, + .maxPlaintextLen = 53, + .plaintextLen = 17, + }, + { + .desc = "Example from draft-ietf-httpbis-encryption-encoding-02", + .plaintext = "I am the walrus", + .recvPrivKey = "\xf4\x55\xa5\xd7\x9f\xd0\x51\x00\x16\x0d\xa0\xf7\x93\x79" + "\x79\xd1\x90\x59\x40\x9e\x1a\xbb\x6e\xc5\xd5\x5e\x05\xd2" + "\xe2\xd2\x0f\xf3", + .authSecret = + "\x47\x6f\x6f\x20\x67\x6f\x6f\x20\x67\x27\x20\x6a\x6f\x6f\x62\x21", + .ciphertext = "\xea\x7a\x80\x41\x43\x04\xf2\x13\x6a\xc3\x92\x77\x92\x5f" + "\x1c\xa5\x55\x49\xca\x55\xca\x62\xa6\x4e\x7a\xc7\x99\x1b" + "\xc5\x2e\x78\xaa\x40", + .cryptoKey = "keyid=\"dhkey\"; " + "dh=" + "\"BNoRDbb84JGm8g5Z5CFxurSqsXWJ11ItfXEWYVLE85Y7CYkDjXsIEc4" + "aqxYaQ1G8BqkXCJ6DPpDrWtdWj_mugHU\"", + .encryption = "keyid=\"dhkey\"; salt=\"lngarbyKfMoi9Z75xYXmkg\"", + .ciphertextLen = 33, + .maxPlaintextLen = 17, + .plaintextLen = 15, + }, +}; + +static webpush_aesgcm_decrypt_err_test_t webpush_aesgcm_decrypt_err_tests[] = { + { + .desc = "rs = 7, no trailer", + .recvPrivKey = "\xd9\xbb\xb8\xa5\xa3\x80\x65\xb2\xf6\x79\xfd\x6e\xfb\x04" + "\xf3\x38\xdb\x93\x21\xc0\xcf\x73\x4d\x28\xd3\x35\x09\x82" + "\x0e\x3a\x5d\x37", + .authSecret = + "\x42\x80\xe2\xd2\xee\xaf\x72\xc9\x48\x54\x92\xa2\xa2\xe5\xcc\x5f", + // "O hai". The ciphertext is exactly `rs + 16`, without a trailer. + .ciphertext = "\x60\x6e\x05\xf9\xbd\x3a\xcb\x9f\x74\x85\x19\x67\x4a\xcc\x3f" + "\xbe\xe3\xb0\xeb\x65\x7d\x23\x3f", + .cryptoKey = "dh=BD_" + "bsTUpxBMvSv8eksith3vijMLj44D4jhJjO51y7wK1ytbUlsyYBBYYyB5AAe5b" + "nREA_WipTgemDVz00LiWcfM", + .encryption = "salt=xKWvs_jWWeg4KOsot_uBhA; rs=7", + .ciphertextLen = 23, + .maxPlaintextLen = 7, + .err = ECE_ERROR_DECRYPT_TRUNCATED, + }, + { + // Last block is only 1 byte; pad length prefix is 2 bytes. + .desc = "Pad size > last block length", + .recvPrivKey = "\x0a\x8b\x04\x44\x05\x57\x82\xf4\xef\xa2\x1e\xd4\x92\xb4" + "\x42\xd9\x5f\xa2\x5e\x83\x6c\xd1\xb5\xe5\x7b\xd2\x3a\xf2" + "\xac\xe4\x95\xeb", + .authSecret = + "\x42\xd1\x99\x79\x8f\x0c\x41\xf0\x9a\xab\xe5\xf0\x28\xe5\x46\x05", + .ciphertext = "\x26\xf5\xfd\x1e\xc2\x78\x94\xbe\x60\xcc\xff\x3f\xb8\x22\x9c" + "\xea\xcd\x79\x89\x12\x1a\x36\x10\xf8\xa4\x50\xa0\xab\x9f\x9d" + "\x7f\x06\xd4\xa8\x47\x0d\x52\x4a\xaf", + .cryptoKey = "dh=BBNZNEi5Ew_ID5S4Y9jWBi1NeVDje6Mjs7SDLViUn6A8VAZj-" + "6X3QAuYQ3j20BblqjwTgYst7PRnY6UGrKyLbmU", + .encryption = "salt=ot8hzbwOo6CYe6ZhdlwKtg; rs=6", + .ciphertextLen = 39, + .maxPlaintextLen = 7, + .err = ECE_ERROR_DECRYPT_PADDING, + }, + { + // Last block is 1 byte, but claims its pad length is 2. + .desc = "Padding length > last block length", + .recvPrivKey = "\xf6\x5f\x9a\x85\xc0\x4c\xf8\x8d\x32\x93\x06\xd6\x88\x34" + "\xbd\x29\x1a\xcf\x76\x1c\xaf\x4d\x9d\x12\xc4\xa8\x8f\xa6" + "\x3d\x9a\x79\xa2", + .authSecret = + "\x80\xa1\xbf\x3f\xaf\x9d\x7b\x9a\x72\xcd\x2f\x21\xc8\x7f\xcd\xc9", + .ciphertext = "\xa1\x64\x8e\x14\x0f\x94\x3b\x9a\x16\xab\xe9\x08\xef\xd4\x47" + "\x68\x57\xf0\x01\xe8\xcb\x89\x02\x78\x0b\xb7\x93\x9a\xb4\x93" + "\x06\x5e\x20\x02\xb2\xd7\x7f\x1e\xe5\x67\xe6", + .cryptoKey = "dh=BKe2IBO_cwmEzQyTVscSbQcj0Y3uBSzGZ_mHlANMciS8uGpb7U8_" + "Bw7TNdlYfpwWDLd0cxM8YYWNDbNJ_p2Rp4o", + .encryption = "salt=z7QJ6UR89SiFRkd4RsC4Vg; rs=6", + .ciphertextLen = 41, + .maxPlaintextLen = 9, + .err = ECE_ERROR_DECRYPT_PADDING, + }, + { + // First block has no padding, but claims its pad length is 1. + .desc = "Non-zero padding", + .recvPrivKey = "\x23\x3b\x9a\xc4\xba\x85\x26\x68\xd2\xbb\xc1\xa3\x2c\x2a" + "\x36\xa0\x46\x83\x66\x30\xc1\xba\xd5\xb8\x9b\x84\xf4\xab" + "\x1d\x36\x5e\xc3", + .authSecret = + "\x70\xca\x56\x41\x6e\x7c\x06\xba\x43\x6c\x9f\x0a\xa9\xb4\xbd\x8a", + .ciphertext = "\x41\xdb\xe3\x87\x42\xe4\x65\x72\xae\xff\x51\xef\xbf\x9e\x83" + "\xd2\xb3\x92\x17\xa3\x30\xc3\x7c\xb4\x17\xcc\x64\xc5\x43\x65" + "\xc1\x5b\xb6\x53\x58\x9a\x90\xe5\x14\x19\x1b", + .cryptoKey = "dh=BBicj01QI0ryiFzAaty9VpW_crgq9XbU1bOCtEZI9UNE6tuOgp4lyN_" + "UN0N905ECnLWK5v_sCPUIxnQgOuCseSo", + .encryption = "salt=SbkGHONbQBBsBcj9dLyIUw; rs=6", + .ciphertextLen = 41, + .maxPlaintextLen = 9, + .err = ECE_ERROR_DECRYPT_PADDING, + }, + { + .desc = "rs = 6, auth tag for last record", + .recvPrivKey = "\x9e\x13\x93\xf7\x5e\xf5\xc6\xea\x10\x04\x91\xa4\x89\x9d" + "\xda\xa9\x3e\x6a\xc3\xf2\x0b\x27\xde\x3f\x3c\xf8\x95\x36" + "\xed\x4b\x15\x26", + .authSecret = + "\xde\xb5\xa1\xb1\x10\x94\xfc\xa7\x5a\xa9\xf2\x8f\x6d\xdd\xf3\x05", + .ciphertext = "\x0b\xbb\xb7\x8f\x90\x0b\xe1\x8c\xe1\xdb\x26\x01\xfe\xe9\x8d" + "\xea\xdc\xeb\x54\x7c\x6b\xb7\xb0\xf9\x6d\xa4\xc4\x5b\xd0\xc4" + "\xd4\x19\x37\xba\x9f\x5f\x63\x8c", + .cryptoKey = "dh=BI38Qs_OhDmQIxbszc6Nako-MrX3FzAE_8HzxM1wgoEIG4ocxyF-" + "YAAVhfkpJUvDpRyKW2LDHIaoylaZuxQfRhE", + .encryption = "salt=QClh48OlvGpSjZ0Mg0e8rg; rs=6", + .ciphertextLen = 38, + .maxPlaintextLen = 6, + .err = ECE_ERROR_SHORT_BLOCK, + }, +}; + +void +test_webpush_aesgcm_decrypt_ok(void) { + size_t length = sizeof(webpush_aesgcm_decrypt_ok_tests) / + sizeof(webpush_aesgcm_decrypt_ok_test_t); + for (size_t i = 0; i < length; i++) { + webpush_aesgcm_decrypt_ok_test_t t = webpush_aesgcm_decrypt_ok_tests[i]; + + const void* recvPrivKey = t.recvPrivKey; + const void* authSecret = t.authSecret; + const void* ciphertext = t.ciphertext; + + uint8_t salt[ECE_SALT_LENGTH]; + uint8_t rawSenderPubKey[ECE_WEBPUSH_PUBLIC_KEY_LENGTH]; + uint32_t rs; + int err = ece_webpush_aesgcm_headers_extract_params( + t.cryptoKey, t.encryption, salt, ECE_SALT_LENGTH, rawSenderPubKey, + ECE_WEBPUSH_PUBLIC_KEY_LENGTH, &rs); + ece_assert(!err, "Got %d parsing crypto headers", err); + + size_t plaintextLen = ece_aesgcm_plaintext_max_length(rs, t.ciphertextLen); + ece_assert(plaintextLen == t.maxPlaintextLen, + "Got plaintext max length %zu for `%s`; want %zu", plaintextLen, + t.desc, t.maxPlaintextLen); + + uint8_t* plaintext = calloc(plaintextLen, sizeof(uint8_t)); + + err = ece_webpush_aesgcm_decrypt( + recvPrivKey, ECE_WEBPUSH_PRIVATE_KEY_LENGTH, authSecret, + ECE_WEBPUSH_AUTH_SECRET_LENGTH, salt, ECE_SALT_LENGTH, rawSenderPubKey, + ECE_WEBPUSH_PUBLIC_KEY_LENGTH, rs, ciphertext, t.ciphertextLen, plaintext, + &plaintextLen); + ece_assert(!err, "Got %d decrypting ciphertext for `%s`", err, t.desc); + + ece_assert(plaintextLen == t.plaintextLen, + "Got plaintext length %zu for `%s`; want %zu", plaintextLen, + t.desc, t.plaintextLen); + ece_assert(!memcmp(plaintext, t.plaintext, plaintextLen), + "Wrong plaintext for `%s`", t.desc); + + free(plaintext); + } +} + +void +test_webpush_aesgcm_decrypt_err(void) { + size_t tests = sizeof(webpush_aesgcm_decrypt_err_tests) / + sizeof(webpush_aesgcm_decrypt_err_test_t); + for (size_t i = 0; i < tests; i++) { + webpush_aesgcm_decrypt_err_test_t t = webpush_aesgcm_decrypt_err_tests[i]; + + const void* recvPrivKey = t.recvPrivKey; + const void* authSecret = t.authSecret; + const void* ciphertext = t.ciphertext; + + uint8_t salt[ECE_SALT_LENGTH]; + uint8_t rawSenderPubKey[ECE_WEBPUSH_PUBLIC_KEY_LENGTH]; + uint32_t rs; + int err = ece_webpush_aesgcm_headers_extract_params( + t.cryptoKey, t.encryption, salt, ECE_SALT_LENGTH, rawSenderPubKey, + ECE_WEBPUSH_PUBLIC_KEY_LENGTH, &rs); + ece_assert(!err, "Got %d parsing crypto headers", err); + + size_t plaintextLen = ece_aesgcm_plaintext_max_length(rs, t.ciphertextLen); + ece_assert(plaintextLen == t.maxPlaintextLen, + "Got plaintext max length %zu for `%s`; want %zu", plaintextLen, + t.desc, t.maxPlaintextLen); + + uint8_t* plaintext = calloc(plaintextLen, sizeof(uint8_t)); + + err = ece_webpush_aesgcm_decrypt( + recvPrivKey, ECE_WEBPUSH_PRIVATE_KEY_LENGTH, authSecret, + ECE_WEBPUSH_AUTH_SECRET_LENGTH, salt, ECE_SALT_LENGTH, rawSenderPubKey, + ECE_WEBPUSH_PUBLIC_KEY_LENGTH, rs, ciphertext, t.ciphertextLen, plaintext, + &plaintextLen); + ece_assert(err == t.err, "Got %d decrypting ciphertext for `%s`; want %d", + err, t.desc, t.err); + + free(plaintext); + } +} diff --git a/mobile/ios/ThirdParty/ecec/test/e2e.c b/mobile/ios/ThirdParty/ecec/test/e2e.c new file mode 100644 index 0000000000..4acdf251aa --- /dev/null +++ b/mobile/ios/ThirdParty/ecec/test/e2e.c @@ -0,0 +1,134 @@ +#include "test.h" + +#include +#include + +void +test_webpush_aes128gcm_e2e(void) { + uint8_t rawRecvPrivKey[ECE_WEBPUSH_PRIVATE_KEY_LENGTH]; + uint8_t rawRecvPubKey[ECE_WEBPUSH_PUBLIC_KEY_LENGTH]; + uint8_t authSecret[ECE_WEBPUSH_AUTH_SECRET_LENGTH]; + int err = ece_webpush_generate_keys( + rawRecvPrivKey, ECE_WEBPUSH_PRIVATE_KEY_LENGTH, rawRecvPubKey, + ECE_WEBPUSH_PUBLIC_KEY_LENGTH, authSecret, ECE_WEBPUSH_AUTH_SECRET_LENGTH); + ece_assert(!err, "Got %d generating keys", err); + + const void* input = "When I grow up, I want to be a watermelon"; + size_t inputLen = strlen(input); + + size_t payloadLen = ece_aes128gcm_payload_max_length(4096, 0, inputLen); + ece_assert(payloadLen == 334, "Got %zu for payload max length; want 334", + payloadLen); + uint8_t* payload = calloc(payloadLen, sizeof(uint8_t)); + + err = ece_webpush_aes128gcm_encrypt(rawRecvPubKey, + ECE_WEBPUSH_PUBLIC_KEY_LENGTH, authSecret, + ECE_WEBPUSH_AUTH_SECRET_LENGTH, 4096, 0, + input, inputLen, payload, &payloadLen); + ece_assert(!err, "Got %d encrypting plaintext", err); + ece_assert(payloadLen == 144, "Got %zu for payload length; want 144", + payloadLen); + + size_t plaintextLen = ece_aes128gcm_plaintext_max_length(payload, payloadLen); + ece_assert(plaintextLen == 42, "Got %zu for plaintext max length; want 42", + plaintextLen); + uint8_t* plaintext = calloc(plaintextLen, sizeof(uint8_t)); + + err = ece_webpush_aes128gcm_decrypt( + rawRecvPrivKey, ECE_WEBPUSH_PRIVATE_KEY_LENGTH, authSecret, + ECE_WEBPUSH_AUTH_SECRET_LENGTH, payload, payloadLen, plaintext, + &plaintextLen); + ece_assert(!err, "Got %d decrypting payload", err); + ece_assert(plaintextLen == inputLen, "Got %zu for plaintext length; want %zu", + plaintextLen, inputLen); + ece_assert(!memcmp(plaintext, input, inputLen), + "Got `%s` for plaintext; want `%s`", plaintext, input); + + free(payload); + free(plaintext); +} + +void +test_webpush_aesgcm_e2e(void) { + uint8_t rawRecvPrivKey[ECE_WEBPUSH_PRIVATE_KEY_LENGTH]; + uint8_t rawRecvPubKey[ECE_WEBPUSH_PUBLIC_KEY_LENGTH]; + uint8_t authSecret[ECE_WEBPUSH_AUTH_SECRET_LENGTH]; + int err = ece_webpush_generate_keys( + rawRecvPrivKey, ECE_WEBPUSH_PRIVATE_KEY_LENGTH, rawRecvPubKey, + ECE_WEBPUSH_PUBLIC_KEY_LENGTH, authSecret, ECE_WEBPUSH_AUTH_SECRET_LENGTH); + ece_assert(!err, "Got %d generating keys", err); + + const void* input = "If the wind in my sail on the sea stays behind me, one " + "day I'll know how far I'll go"; + size_t inputLen = strlen(input); + + size_t ciphertextLen = ece_aesgcm_ciphertext_max_length(26, 6, inputLen); + ece_assert(ciphertextLen == 162, + "Got %zu for ciphertext max length; want 162", ciphertextLen); + + uint8_t* ciphertext = calloc(ciphertextLen, sizeof(uint8_t)); + + uint8_t encryptSalt[ECE_SALT_LENGTH]; + uint8_t encryptRawSenderPubKey[ECE_WEBPUSH_PUBLIC_KEY_LENGTH]; + err = ece_webpush_aesgcm_encrypt( + rawRecvPubKey, ECE_WEBPUSH_PUBLIC_KEY_LENGTH, authSecret, + ECE_WEBPUSH_AUTH_SECRET_LENGTH, 26, 6, input, inputLen, encryptSalt, + ECE_SALT_LENGTH, encryptRawSenderPubKey, ECE_WEBPUSH_PUBLIC_KEY_LENGTH, + ciphertext, &ciphertextLen); + ece_assert(!err, "Got %d encrypting plaintext", err); + ece_assert(ciphertextLen == 162, "Got %zu for ciphertext length; want 162", + ciphertextLen); + + size_t cryptoKeyHeaderLen = 0; + size_t encryptionHeaderLen = 0; + err = ece_webpush_aesgcm_headers_from_params( + encryptSalt, ECE_SALT_LENGTH, encryptRawSenderPubKey, + ECE_WEBPUSH_PUBLIC_KEY_LENGTH, 14, NULL, &cryptoKeyHeaderLen, NULL, + &encryptionHeaderLen); + ece_assert(!err, "Got %d determining crypto header lengths", err); + + char* cryptoKeyHeader = malloc(cryptoKeyHeaderLen + 1); + char* encryptionHeader = malloc(encryptionHeaderLen + 1); + err = ece_webpush_aesgcm_headers_from_params( + encryptSalt, ECE_SALT_LENGTH, encryptRawSenderPubKey, + ECE_WEBPUSH_PUBLIC_KEY_LENGTH, 26, cryptoKeyHeader, &cryptoKeyHeaderLen, + encryptionHeader, &encryptionHeaderLen); + ece_assert(!err, "Got %d formatting crypto headers", err); + cryptoKeyHeader[cryptoKeyHeaderLen] = '\0'; + encryptionHeader[encryptionHeaderLen] = '\0'; + + uint8_t decryptSalt[ECE_SALT_LENGTH]; + uint8_t decryptRawSenderPubKey[ECE_WEBPUSH_PUBLIC_KEY_LENGTH]; + uint32_t rs; + err = ece_webpush_aesgcm_headers_extract_params( + cryptoKeyHeader, encryptionHeader, decryptSalt, ECE_SALT_LENGTH, + decryptRawSenderPubKey, ECE_WEBPUSH_PUBLIC_KEY_LENGTH, &rs); + ece_assert(!err, "Got %d extracting crypto params", err); + ece_assert(!memcmp(encryptSalt, decryptSalt, ECE_SALT_LENGTH), + "Wrong salt for `%s`", input); + ece_assert(!memcmp(encryptRawSenderPubKey, decryptRawSenderPubKey, + ECE_WEBPUSH_PUBLIC_KEY_LENGTH), + "Wrong sender public key for `%s`", input); + ece_assert(rs == 26, "Got rs = %" PRIu32 "; want 26", rs); + + size_t plaintextLen = ece_aesgcm_plaintext_max_length(rs, ciphertextLen); + ece_assert(plaintextLen == 98, "Got %zu for plaintext max length; want 98", + plaintextLen); + uint8_t* plaintext = calloc(plaintextLen, sizeof(uint8_t)); + + err = ece_webpush_aesgcm_decrypt( + rawRecvPrivKey, ECE_WEBPUSH_PRIVATE_KEY_LENGTH, authSecret, + ECE_WEBPUSH_AUTH_SECRET_LENGTH, decryptSalt, ECE_SALT_LENGTH, + decryptRawSenderPubKey, ECE_WEBPUSH_PUBLIC_KEY_LENGTH, rs, ciphertext, + ciphertextLen, plaintext, &plaintextLen); + ece_assert(!err, "Got %d decrypting ciphertext", err); + ece_assert(plaintextLen == inputLen, "Got %zu for plaintext length; want %zu", + plaintextLen, inputLen); + ece_assert(!memcmp(plaintext, input, inputLen), + "Got `%s` for plaintext; want `%s`", plaintext, input); + + free(ciphertext); + free(cryptoKeyHeader); + free(encryptionHeader); + free(plaintext); +} diff --git a/mobile/ios/ThirdParty/ecec/test/encrypt/aes128gcm.c b/mobile/ios/ThirdParty/ecec/test/encrypt/aes128gcm.c new file mode 100644 index 0000000000..ad19acdea4 --- /dev/null +++ b/mobile/ios/ThirdParty/ecec/test/encrypt/aes128gcm.c @@ -0,0 +1,372 @@ +#include "test.h" + +#include + +#include + +typedef struct webpush_aes128gcm_encrypt_ok_test_s { + const char* desc; + const char* payload; + const char* senderPrivKey; + const char* recvPubKey; + const char* authSecret; + const char* salt; + const char* plaintext; + size_t plaintextLen; + size_t padLen; + size_t maxPayloadLen; + size_t payloadLen; + uint32_t rs; +} webpush_aes128gcm_encrypt_ok_test_t; + +static webpush_aes128gcm_encrypt_ok_test_t + webpush_aes128gcm_encrypt_ok_tests[] = { + { + .desc = "Example from draft-ietf-webpush-encryption-latest", + .payload = "\x0c\x6b\xfa\xad\xad\x67\x95\x88\x03\x09\x2d\x45\x46\x76\xf3" + "\x97\x00\x00\x10\x00\x41\x04\xfe\x33\xf4\xab\x0d\xea\x71\x91" + "\x4d\xb5\x58\x23\xf7\x3b\x54\x94\x8f\x41\x30\x6d\x92\x07\x32" + "\xdb\xb9\xa5\x9a\x53\x28\x64\x82\x20\x0e\x59\x7a\x7b\x7b\xc2" + "\x60\xba\x1c\x22\x79\x98\x58\x09\x92\xe9\x39\x73\x00\x2f\x30" + "\x12\xa2\x8a\xe8\xf0\x6b\xbb\x78\xe5\xec\x0f\xf2\x97\xde\x5b" + "\x42\x9b\xba\x71\x53\xd3\xa4\xae\x0c\xaa\x09\x1f\xd4\x25\xf3" + "\xb4\xb5\x41\x4a\xdd\x8a\xb3\x7a\x19\xc1\xbb\xb0\x5c\xf5\xcb" + "\x5b\x2a\x2e\x05\x62\xd5\x58\x63\x56\x41\xec\x52\x81\x2c\x6c" + "\x8f\xf4\x2e\x95\xcc\xb8\x6b\xe7\xcd", + .senderPrivKey = + "\xc9\xf5\x8f\x89\x81\x3e\x9f\x8e\x87\x2e\x71\xf4\x2a\xa6" + "\x4e\x17\x57\xc9\x25\x4d\xcc\x62\xb7\x2d\xdc\x01\x0b\xb4" + "\x04\x3e\xa1\x1c", + .recvPubKey = + "\x04\x25\x71\xb2\xbe\xcd\xfd\xe3\x60\x55\x1a\xaf\x1e\xd0\xf4" + "\xcd\x36\x6c\x11\xce\xbe\x55\x5f\x89\xbc\xb7\xb1\x86\xa5\x33" + "\x39\x17\x31\x68\xec\xe2\xeb\xe0\x18\x59\x7b\xd3\x04\x79\xb8" + "\x6e\x3c\x8f\x8e\xce\xd5\x77\xca\x59\x18\x7e\x92\x46\x99\x0d" + "\xb6\x82\x00\x8b\x0e", + .authSecret = + "\x05\x30\x59\x32\xa1\xc7\xea\xbe\x13\xb6\xce\xc9\xfd\xa4\x88\x82", + .salt = + "\x0c\x6b\xfa\xad\xad\x67\x95\x88\x03\x09\x2d\x45\x46\x76\xf3\x97", + .plaintext = "When I grow up, I want to be a watermelon", + .plaintextLen = 41, + .padLen = 0, + .maxPayloadLen = 334, + .payloadLen = 144, + .rs = 4096, + }, + { + .desc = "rs = 24, pad = 6", + .payload = "\xff\x80\x50\x30\xa1\x08\xe1\x14\xe6\xc1\x7f\xad\x61\x86\xa1" + "\xa6\x00\x00" + "\x00\x18\x41\x04\x30\xef\xcb\x1e\xb0\x43\xb8\x05\xe4\xe4\x4b" + "\xab\x35\xf8" + "\x25\x13\xc3\x3f\xed\xb2\x87\x00\xf7\xe5\x68\xac\x8b\x61\xe8" + "\xd8\x35\x66" + "\x5a\x51\xeb\x66\x79\xb2\xdb\x22\x8a\x10\xc0\xc3\xfe\x50\x77" + "\x06\x28\x48" + "\xd9\xbb\x3d\x60\x27\x9f\x93\xce\x35\x48\x47\x28\xaa\x1f\xd2" + "\xc1\x71\x39" + "\x49\xae\xc9\x8f\x05\x09\x6c\x72\x98\xfd\x3f\x51\xc4\xf8\x18" + "\xfa\xfa\x1f" + "\xe6\x15\xd8\x44\x7b\x3a\x05\x40\x60\x31\xf6\x40\x1a\xc2\x4f" + "\x2a\x77\x5c" + "\xa5\x24\x56\xa9\x21\xb8\x3b\x9e\x00\x42\xc3\xa6\x3e\x1a\xfa" + "\x1a\xe0\x12" + "\x77\x4d\x9d\x77\x5b\xe8\xd1\x94\x19\x45\x1d\x37\xff\x59\xff" + "\x59\x2e\x84" + "\xf0\x74\x40\xa6\x3f\xc1\x7f\x5c\xab\xcb\x9a\x50\xed\xda\xf7" + "\x53\x70\xdb" + "\x64\x7f\x94\x44\x7d\x3f\x16\x62\x69\xd8\x71\x1d\xf0\xf5\x7e" + "\x56\x04\x95" + "\x76\xe1\x13\x0a\x5a\x5e\x1f\x94\xba\x8a\x5d\x0b\x00\x07\xc6" + "\xc0\xfd\x29" + "\x98\x42\x9e\x7d\x63\xd4\xef\x91\x97\x98\xf4\x6e\xcf\x5f\x0b" + "\x28\xfb\x80" + "\xf5\xb2\x43\x9d\xe2\x6b\x8a\x52\x20\x0b\xc7\xd6\xaf\x7a\x48" + "\x40\x72\x1f" + "\xe8\xbe\x85\x24\xa6\x91\xb6\xef\x0e\xda\xe9\x0b\xb6\xf5\x92" + "\x78\x94\x81" + "\x9b\x83\x1b\x45\xb5\x3f\x84\x01\xfe\x02\x2d\xbb\x64\xed\x75" + "\x65\x35\x09" + "\x04\xac\x0b\x51\x71\x35\xd7\xf8\xab\xbc\x98\x12\x7f\xb1\x63" + "\x86\x4d\x4d" + "\x4a\x30\x74\x25\xb2\xcd\x43\xdb\x22\xaf\x26\x7d\x71\xc3\x71" + "\x46\x99\x4a" + "\x8c\x48\x05\xad\xc3\x41\xbf\xba\x27\xaf\x09\xfd\x80\xbd\x5e" + "\xff\x51\xd8" + "\x77\x28\x2a\x2f\xbf\xbf\xeb\x10\x19\x9e\x78\x79\xe4\xb9\xd1" + "\x3a\x46\xd5" + "\x7f\xb7\xd7\x86\x82\x48\x53\xe1\xcc\x89\xca\xfb\xaf\x14\xde" + "\x1e\x92\x4c" + "\x94\x4f\xeb\x8b\x62\x6c\xe0\x20\x7d\x6f\x9f\xa9\xd8\x49\xee" + "\xca\xc6\x9b" + "\x42\xd6\xe7\xa2\x3b\xd5\x12\x4d\x49\x62\x2b\x44\xb3\x5c\x5b" + "\x15\xfb\x0e" + "\x6a\x77\x81\xa5\x03\xf1\xa4\xe0\x62\xe0\x15\xd5\x57\xd9\x5d" + "\x44\xd9\xd8" + "\xb0\x79\x9b\x3a\xaf\xce\x83\xd5\xd4", + .senderPrivKey = + "\x0f\x28\xbe\xaf\x7e\x27\x79\x3c\x03\x63\x8d\xc2\x97\x3a" + "\x15\xb0\x01\x6e\x1b\x36\x7c\xbf\xfd\xa8\x86\x1a\xb1\x75" + "\xf3\x1b\xce\x02", + .recvPubKey = + "\x04\xc0\xd1\xa8\x12\xb2\x91\x29\x1d\xd7\xbe\xee\x35\x87\x13" + "\xc1\x26\xc5\x89\xf3\x63\x3c\x26\xd1\xa2\x01\x31\x1d\xe0\x36" + "\xdc\x10\x93\x1e\x4e\xe1\x42\xf6\x19\x21\xa3\xea\x58\x64\xe8" + "\x72\xa9\x38\x41\xa5\x29\x44\xe5\xb3\xf6\xac\xce\xcc\xe8\xc8" + "\x28\xfb\x04\xa4\xcd", + .authSecret = + "\x9d\x77\x35\xd8\xde\x19\x62\xb9\x83\x94\xb0\x7f\xfe\x28\x7e\x20", + .salt = + "\xff\x80\x50\x30\xa1\x08\xe1\x14\xe6\xc1\x7f\xad\x61\x86\xa1\xa6", + .plaintext = "I am the very model of a modern Major-General, I've " + "information vegetable, animal, and mineral", + .plaintextLen = 94, + .padLen = 6, + .maxPayloadLen = 631, + .payloadLen = 441, + .rs = 24, + }, + { + // This test is also interesting because the data length (54) is a + // multiple of rs (18). We'll allocate memory to hold 4 records, but only + // write 3. + .desc = "rs = 18, pad = 31", + .payload = "\xe4\x98\x88\xd2\xb2\x8f\x27\x7f\x84\x7b\xc5\xde\x96\xf0\xf8" + "\x1b\x00\x00" + "\x00\x12\x41\x04\x00\xb8\x33\xe4\x81\xa9\x9a\xa3\x30\xdc\xb2" + "\x77\x92\x2d" + "\x5f\x84\xaf\x2e\x9c\xe6\x11\xad\x2a\xd3\xed\x0f\x5b\x43\x19" + "\x12\xd3\x5e" + "\xa7\x2f\xc5\xbf\x76\xb7\x69\xd9\x52\x67\x78\xf5\xab\xfa\x05" + "\x86\x50\x98" + "\x8d\xa5\xe5\x31\xff\x82\xd1\xa7\x04\x37\x94\xc7\x17\x06\x3a" + "\xeb\x95\x8b" + "\xf1\x16\xbc\xcf\x50\x74\x2f\xd4\xd6\x9b\xd0\xea\x7e\x3f\x61" + "\x1c\x70\x9b" + "\xf2\xcd\xf5\xcd\x47\xc6\x42\x6c\xb8\x32\x3b\x53\x98\xc4\x3c" + "\x0d\x0b\x92" + "\xcc\x98\x2d\xa1\xc2\x4c\xe5\xfe\xe2\xb2\x03\xf7\xad\x78\xca" + "\x44\xf0\x49" + "\x0f\x34\x07\xf5\xfe\xe8\x83\x26\x6e\xe4\x70\x35\x19\x5d\xe0" + "\xfe\x6d\x8a" + "\x75\xe4\x87\xdf\x25\x6d\xb5\x97\xa7\x5e\x45\xae\x4f\xb5\x5b" + "\x82\x59\xcb" + "\x0b\x2d\x19\xe7\xb0\x57\x14\x26\x7e\xb5\x60\xae\x07\x2b\x7a" + "\x66\x59\x51" + "\x91\x7a\x06\x87\x32\xdf\x30\x9b\xe2\x56\xf9\x0f\x2a\xdd\xa3" + "\x2f\x05\xfe" + "\xaa\x5e\x9b\x06\x95\xbc\xa2\xcc\xf2\x2a\xae\xfc\x7d\xa9\xce" + "\xeb\xc5\xd4" + "\x0c\x12\xd3\x2a\xdb\x5c\x84\xcb\x32\x0a\xf9\x44\x01\x60\x95" + "\x36\x2f\xeb" + "\xba\x4f\xfa\x4a\x99\x83\x0e\x49\x58\xea\x2b\xba\x50\x8c\xb6" + "\x83\xa5\x8d" + "\x20\x27\xd4\xb7\x47\x26\xa8\x53\xb2\x4b\x47\xcc\xba\x75\x1a" + "\xbe\x9d\x9a" + "\xb2\xda\x9e\xc2\xba\x9c\x7c\xcf\x0c\xf1\x73\x05\xba\xe3\x14" + "\xd3\x8a\x68" + "\x76\x18\xb0\x77\x2f\xcb\x71\xd4\x41\x90\x27\xa4\xbf\x43\x5c" + "\xb7\x21\xaa" + "\xd7\x4e\xfc\x17\x99\x81\xb7\x16\x96\x04\xbf\x97\xec\xac\x41" + "\xe7\x38\x84" + "\x45\x69\x33\x73\x48\x18\x13\x29\x23\xb5\x6c\x15\x2d\x6c\x9e" + "\x59\xae\xf9" + "\x95\xac\xa5\x9d\xe0\xbf\x2c\x80\x3a\x07\x18\x08\x89\x67\x0a" + "\x08\xe6\x4a" + "\x20\xd2\xbf\xa8\x53\xe0\x11\x28\x72\x94\x7b\xaa\xaf\xfb\x51" + "\x0c\xc9\xe7" + "\x5d\x63\x10\xed\x6a\xac\xbd\x2e\x0b\xa3\xa2\x9b\xe4\x2c\x65" + "\x32\xea\x4e" + "\x33\x46\xe1\xf0\x57\x16\x46\x37\x1c\x71\x66\x5e\x3f\xac\x9d" + "\x76\xfa\xee" + "\x1f\x12\x2e\x64\xd4\x90\xdd\x2a\x3e\x31\x81\x6e\xab\x58\x3f" + "\x17\x28\x41" + "\xa0\x75\xd2\x05\xf3\x18\x71\x4a\x8c\x70\xce\x0f\x32\x7f\x4d" + "\x92\xb8\xc9" + "\xdc\xb8\x13\xe6\xd2\x4f\xe8\x56\x33\xf1\xa9\xc7\xc1\xe4\xa1" + "\xfb\x31\x4d" + "\xd5\xfe\x3e\x28\x0e\x39\x08\xf3\x6c\x8c\xbf\xb8\x0b\x7d\x92" + "\x43\xab\xaf" + "\xfa\x65\xc2\x16\xcf\x1a\xa8\xb8\xd6\x26\xa6\x30\xdf\xe8\x18" + "\x6c\xe9\x77" + "\xa5\xb8\xf3\x64\x9d\x37\x53\xb9\x17\x6c\x36\x7e\x4e\x07\xf2" + "\x20\xa1\x75" + "\x80\x61\x38\xe8\x88\x25\xa2\xf3\x49\x84\x20\x58\x2b\x96\x20" + "\x96\x58\xbb" + "\xfa\x8f\x2b\xa6\x93\x3a\x83\xc2\x5e\xdb\x26\x91\x87\x79\x65" + "\x42\xe2\xac" + "\x49\xb8\x07\x86\x36\xbd\xdc\x26\x8e\x11\x62\x5e\x8b\xff\x9f" + "\x0a\x34\x3d" + "\x3a\x4c\x06\x08\x0e\xf0\x80\x3b\x8d\xcd\x8e\x84\x1d\x0e\x27" + "\x59\xe4\x83" + "\xea\x19\xb9\x03\x32\x4d\x9e\xc4\xd5\x2f\x49\x1a\xce\xf3\xee" + "\xff\x44\x1c" + "\x37\x88\x1c\x75\x93\xea\xc3\x16\x21\x33\x7a\x5e\x86\x59\xf9" + "\x3e\x20\x07" + "\x9b\x0e\x26\xeb\xfe\x56\xc1\x04\x55\xd1\x09\x71\x13\x0b\xd2" + "\xa2\xc1\x59" + "\xc7\x4f\x48\xb2\xe5\x26\x53\x0a\x76\xf6\x4c\xca\x2e\xfb\x24" + "\x6e\x79\x3d" + "\x11\xfb\x75\xa6\x68\x01\x8e\x70\xc3\x10\x71\x00\xf8\x1b\xa3" + "\xb1\x6a\xe4" + "\x0a\x83\x8f\x18\xd4\xc4\x7f\x1d\x71\x32\xf1\x74\x68\x8e\xc5" + "\x38\x23\x94" + "\xe0\x11\x99\x21\x73\x1a\x16\x87\x9b\x85\x8f\xf3\x8f\x72\x85" + "\x1e\xa3\xd9" + "\xf5\x26\x3f\xec\x5a\x60\x6d\x12\x71\xa8\x9b\x84\xcc\xa5\x3e" + "\xd7\x3c\x52" + "\x54\xe2\x45\xbf\x8f\x2f\x27\xc2\xc1\xc8\x7f\x39\xee\xa7\x8c" + "\x70\x17\xc8" + "\xc6\xb5\xab\x01\x66\x30\x32\xb5\x8d\xa3\x10\x57\x28\x5e\x56" + "\xc2\x03\xf4" + "\xe4\x8d\x67\x89\xc6\x6b\x26\x95\xa9\x00\xe0\x04\x82\xbd\x84" + "\x65\x59\xec" + "\xdd\xd4\x02\x64\xb3\x8e\x27\x96\x47\xd1\xec\x0f\xcc\xdc\x18" + "\x81\x83\x8b" + "\xbe\x0c\x83\x5e\x26\x90\xef\x05\x8b\x8f\x6a\x03\xe2\x9c\xd9" + "\xeb\x95\x84" + "\xe9\x7f\xbc\x30\x97\x73\xc3\x68\x8e\x5e\x03\xf9\xd3\x8e\x3e" + "\x45\x48\x73" + "\x8a\x5f\x56\x9c\x59\x14\x7d\x3e\x82\x3c\xcc\xac\x71\xd5\xe8" + "\x82\x5d\x51" + "\x34\xce\x98\x13\xcd\x0b\x8f\x96\x27\xa3\xdb\xfa\x45\xb8\x3a" + "\x59\xc8\x3d" + "\x2b\x4d\x3a\xd4\x37\x77\x8a\x3c\xb1\xbc\x77\xba\x16\xc9\x23" + "\x06\xf4\x26" + "\x1a\x2a\x1f\x0d\x5c\x7e\xda\xec\xf9\x26\xf9\x2d\x7c\x9d\xfc" + "\xae\x87\x51" + "\x3a\x68\xb8\xc7\xef\x7c\x63\x26\x4b\x85\x87\x67\xc1\x1a\xaa" + "\x41\xd2\x7c" + "\x63\x6f\x52\xe2\x85\x51\xe9\x3a\x96\x9c\xdc\x96\xd4\x38\x67" + "\xb7\xcb\xd6" + "\x8f\xe0\x35\x7b\xd3\x34\x15\xfa\xf2\x2a\xae\xeb\xc9\x57\xf4" + "\xb5\x73\x7a" + "\x04\xab\x72\x77\xb4\xed\x40\x08\xf0\x9e\xda\xff\x5a\x6d\xb6" + "\x9f\x6c\xb0" + "\x6f\x3d\x0b\x76\x68\x89\x06\xb2\xf5\x3b\x27\xe6\x3f\x37\x28" + "\xba\x2e\xda" + "\x50\x5f\xb1\xb3\x2f\x81\xdd\xdc\x6d\x30\x5f\xd5\x94\x9e\xdd" + "\x05\x49\x0c" + "\xb1\x61\x8f\x0c\xe1\x43\x0e\x9f\x5e\xdf\x50\x01\x2d\xc3", + .senderPrivKey = + "\x78\x30\x57\x7b\xaf\xcf\xc4\x58\x28\xda\x0c\x40\xaa\xb0" + "\x9f\xb2\x27\xbf\xea\xe0\x68\xaa\xb8\xc0\x64\x22\x2a\xcb" + "\xe6\xef\xfd\x34", + .recvPubKey = + "\x04\xc3\xd7\x14\xcb\x42\xe2\xb0\xa1\xd6\xf9\x85\x99\xe2\xf1" + "\x86\xb8\xc2\xba\x6f\x6f\xab\x5e\x09\xa2\xab\xca\x86\x5c\x08" + "\x05\x89\x2b\x2c\x37\x29\x33\x0e\xf8\x3d\xc9\xdf\x4b\x44\x36" + "\x2b\x03\x9a\x06\x09\xd3\x6b\xeb\x93\x21\xa4\x31\xec\x12\x35" + "\x06\xdd\xd9\x0f\x24", + .authSecret = + "\xe4\xd7\xb7\x9d\xec\xde\xde\x12\xc3\xe9\xd9\x0d\x3e\x05\x73\x0f", + .salt = + "\xe4\x98\x88\xd2\xb2\x8f\x27\x7f\x84\x7b\xc5\xde\x96\xf0\xf8\x1b", + .plaintext = "Push the button, Frank!", + .plaintextLen = 23, + .padLen = 31, + .maxPayloadLen = 1265, + .payloadLen = 1058, + .rs = 18, + }, +}; + +void +test_webpush_aes128gcm_encrypt_ok(void) { + size_t tests = sizeof(webpush_aes128gcm_encrypt_ok_tests) / + sizeof(webpush_aes128gcm_encrypt_ok_test_t); + for (size_t i = 0; i < tests; i++) { + webpush_aes128gcm_encrypt_ok_test_t t = + webpush_aes128gcm_encrypt_ok_tests[i]; + + const void* senderPrivKey = t.senderPrivKey; + const void* authSecret = t.authSecret; + const void* salt = t.salt; + const void* recvPubKey = t.recvPubKey; + const void* plaintext = t.plaintext; + + size_t payloadLen = + ece_aes128gcm_payload_max_length(t.rs, t.padLen, t.plaintextLen); + ece_assert(payloadLen == t.maxPayloadLen, + "Got payload max length %zu for `%s`; want %zu", payloadLen, + t.desc, t.maxPayloadLen); + + uint8_t* payload = calloc(payloadLen, sizeof(uint8_t)); + + int err = ece_webpush_aes128gcm_encrypt_with_keys( + senderPrivKey, ECE_WEBPUSH_PRIVATE_KEY_LENGTH, authSecret, + ECE_WEBPUSH_AUTH_SECRET_LENGTH, salt, ECE_SALT_LENGTH, recvPubKey, + ECE_WEBPUSH_PUBLIC_KEY_LENGTH, t.rs, t.padLen, plaintext, t.plaintextLen, + payload, &payloadLen); + ece_assert(!err, "Got %d encrypting payload for `%s`", err, t.desc); + + ece_assert(payloadLen == t.payloadLen, + "Got actual payload length %zu for `%s`; want %zu", payloadLen, + t.desc, t.payloadLen); + ece_assert(!memcmp(payload, t.payload, payloadLen), + "Wrong payload for `%s`", t.desc); + + free(payload); + } +} + +void +test_webpush_aes128gcm_encrypt_pad(void) { + static const uint32_t maxRs = 128; + + const void* senderPrivKey = "\xac\xae\xc1\xc3\x7c\x30\x7c\xb9\x02\x8f\xbb\xd9" + "\xc7\xf3\xc6\x89\x26\x60\x08\x95\x9a\x5e\xd4\x03" + "\x42\x21\xb2\xda\x72\x01\x82\x8f"; + const void* authSecret = + "\x44\x29\x81\x2d\x53\x5f\xbf\xdb\xea\xc8\x6d\xb7\x14\x5c\x6a\xf2"; + const void* salt = + "\x45\x2b\xfb\xea\x8c\xc7\xa7\x57\x14\xd2\x03\xcf\xf1\x02\xe8\x76"; + const void* recvPubKey = + "\x04\x2d\x78\x8d\x3e\x8e\x82\xf2\xd7\xea\xef\xbd\xe3\xa1\xbe\xde\xa2\x1f" + "\x3b\xc9\x60\x33\x15\x73\x22\xa0\x9e\x14\x46\x55\xa3\xdf\x78\xfd\xca\xc8" + "\x10\xe3\x02\x2a\xb5\x6a\x0e\xa9\xb8\xec\x06\x73\x8a\xce\x41\x1f\x49\x54" + "\x7b\xc0\x0d\x1a\x1c\xde\x97\xce\x7b\xdd\x26"; + + // For rs = `ECE_AES128GCM_MIN_RS`, we allow any amount of padding, because we + // can only include one byte of data per record. + for (uint32_t rs = ECE_AES128GCM_MIN_RS + 1; rs <= maxRs; rs++) { + // Generate a random plaintext. + size_t plaintextLen = (size_t)(rand() % (int) (maxRs - 1) + 1); + uint8_t* plaintext = calloc(plaintextLen, sizeof(uint8_t)); + int ok = RAND_bytes(plaintext, (int) plaintextLen); + ece_assert(ok == 1, "Got %d generating plaintext for rs = %d", ok, rs); + + size_t maxPadLen = (rs - ECE_AES128GCM_MIN_RS) * (plaintextLen + 1); + + // Encrypting with the maximum padding length should succeed. + size_t payloadLen = + ece_aes128gcm_payload_max_length(rs, maxPadLen, plaintextLen); + uint8_t* payload = calloc(payloadLen, sizeof(uint8_t)); + + int err = ece_webpush_aes128gcm_encrypt_with_keys( + senderPrivKey, ECE_WEBPUSH_PRIVATE_KEY_LENGTH, authSecret, + ECE_WEBPUSH_AUTH_SECRET_LENGTH, salt, ECE_SALT_LENGTH, recvPubKey, + ECE_WEBPUSH_PUBLIC_KEY_LENGTH, rs, maxPadLen, plaintext, plaintextLen, + payload, &payloadLen); + ece_assert(!err, "Got %d encrypting with rs = %d, padLen = %zu", err, rs, + maxPadLen); + + // Adding more padding should fail. + size_t badPadLen = maxPadLen + 1; + payloadLen = ece_aes128gcm_payload_max_length(rs, badPadLen, plaintextLen); + payload = realloc(payload, payloadLen); + + err = ece_webpush_aes128gcm_encrypt_with_keys( + senderPrivKey, ECE_WEBPUSH_PRIVATE_KEY_LENGTH, authSecret, + ECE_WEBPUSH_AUTH_SECRET_LENGTH, salt, ECE_SALT_LENGTH, recvPubKey, + ECE_WEBPUSH_PUBLIC_KEY_LENGTH, rs, badPadLen, plaintext, plaintextLen, + payload, &payloadLen); + ece_assert(err == ECE_ERROR_ENCRYPT_PADDING, + "Want error encrypting with rs = %d, padLen = %zu", rs, + badPadLen); + + free(plaintext); + free(payload); + } +} diff --git a/mobile/ios/ThirdParty/ecec/test/encrypt/aesgcm.c b/mobile/ios/ThirdParty/ecec/test/encrypt/aesgcm.c new file mode 100644 index 0000000000..43fe66866f --- /dev/null +++ b/mobile/ios/ThirdParty/ecec/test/encrypt/aesgcm.c @@ -0,0 +1,219 @@ +#include "test.h" + +#include +#include + +#include + +typedef struct webpush_aesgcm_encrypt_ok_test_s { + const char* desc; + const char* ciphertext; + const char* senderPrivKey; + const char* recvPubKey; + const char* authSecret; + const char* salt; + const char* plaintext; + size_t plaintextLen; + size_t padLen; + size_t maxCiphertextLen; + size_t ciphertextLen; + uint32_t rs; +} webpush_aesgcm_encrypt_ok_test_t; + +static webpush_aesgcm_encrypt_ok_test_t webpush_aesgcm_encrypt_ok_tests[] = { + { + .desc = "Example from draft-ietf-webpush-encryption-04", + .ciphertext = "\xea\x7a\x80\x41\x43\x04\xf2\x13\x6a\xc3\x92\x77\x92\x5f" + "\x1c\xa5\x55\x49\xca\x55\xca\x62\xa6\x4e\x7a\xc7\x99\x1b" + "\xc5\x2e\x78\xaa\x40", + .senderPrivKey = "\x9c\x24\x9c\x7a\x4f\x90\xa4\x48\xe6\x38\xe9\x53\xfa" + "\xb4\x37\xf2\x76\x73\xbd\xd3\xe5\xa9\xad\x34\x67\x2d" + "\x22\xea\x6d\x8e\x26\xf6", + .recvPubKey = "\x04\x21\x24\x06\x3c\xcb\xf1\x9d\xc2\xfa\x88\xb6\x43\xba" + "\x04\xe6\xdd\x8d\xa7\xea\x7b\xa2\xc8\xc6\x2e\x0f\x77\xa9" + "\x43\xf4\xc2\xfa\x91\x4f\x6d\x44\x11\x6c\x9f\xd1\xc4\x03" + "\x41\xc6\xa4\x40\xca\xb3\xe2\x14\x0a\x60\xe4\x37\x8a\x5d" + "\xa7\x35\x97\x2d\xe0\x78\x00\x51\x05", + .authSecret = + "\x47\x6f\x6f\x20\x67\x6f\x6f\x20\x67\x27\x20\x6a\x6f\x6f\x62\x21", + .salt = "\x96\x78\x1a\xad\xbc\x8a\x7c\xca\x22\xf5\x9e\xf9\xc5\x85\xe6\x92", + .plaintext = "I am the walrus", + .plaintextLen = 15, + .padLen = 0, + .maxCiphertextLen = 33, + .ciphertextLen = 33, + .rs = 4096, + }, + { + .desc = "rs = 4, trailer", + .ciphertext = + "\xe8\x7d\x81\x05\x8d\x25\xd1\xdb\xfc\xf3\x1c\x5d\x52\xd4\x39\x51\xb4" + "\x7c\x63\x07\x4d\x6d\xb3\xf0\xaf\x89\xe4\xb3\xae\x64\x3d\x00\xb5\xce" + "\xda\x84\x4c\xf7\x9b\xce\xb7\xf9\x4e\xdc\x95\xa9\xf8\x78\x4d\xcf\xf0" + "\xb7\xeb\xdc\x27\x7e\x9a\x17\xdb\xd4\x62\x78\xaa\x8d\x7e\xe9\x65\x24" + "\x81\x22\xe8\x40\x81\x37\x4c\xab\xd0\x3d\xb8\x3b\x98\x39\xdb\x34\x28" + "\x88\xf9\x68\x58\x35\x26\x58\x34\xbb\x11\xe6\xdb\x1e\xb0\x95\xcd\x17" + "\x5e\x12\xc5\xf8\x3f\x1f\xb6\xed\x3a\x4a\x00\x2b\x05\x7c\x10\xeb\x97" + "\x94\x16\x6a\xc6\x1d\xc0\x00\x74\xf8\xd0\x95\xbb\xb6\x7f\xa8\x18\x35" + "\xac\x1b\xa7\x16\xc8\xde\x94\xda\x49\xfb\xdb\x0a\x74\xf6\xac\x68\x89" + "\x7b\xb3\x8d\x3f\xba\x88\x7a\x8b\xc9\x44\xa4\xc9\xdd\x83\x61\x20\x8b" + "\xc3\x82\x57\x65\xe6\xb8\xc7\x69\xd5\x4d\xa7\xa4\x6f\x26\xf0\x50\xd0" + "\xad\xc9\x5a\x16\xde\xa1\xa7\x50\xb0\xf7\xcf\x4d\x3b\xf4\xf5\x1f\xad" + "\x32\xaf\x7a\x0a\x4a\x6a\x4c\x33\x2b\xa8\x49\xe5\xcf\x78\xa3\xf7\x8b" + "\x38\x98\xdc\x3a\x8c\x95\x3c\xde\xb1\xc9\x79\x9b\x0e\xb4\x6e\xad\x37" + "\xe0\xfa\xf4\xba\xc2\xbe\xd6\x56\x58\xb4\x86\x1c\xf9\x42\xfd\x77\xf2" + "\x36\x33\xc5\xe5\xa1\x2e\xe1\x3c\xba\xd7\x64\xa2\x1a\x92\x5c\x79\x47" + "\xf1\xd3\x18\x34\x9a\x9d\x96\x10\x71\xb8\x74\xc6\xb0\xc1\x06\x66\x62" + "\x34\x97\x27\x84\x36\xa9\x3c\xd1\xa9\xa4\xe0\x32\xa9\x64\x9f\x29\x7a" + "\x4d\x8b\xa4\x59\x44\xf6\x3b\xdb\x0b\xce\x45\xce\x0d\x75\xd3\x7c\x24" + "\xa0\x69\x0f\xa0\x96\x0d\x86\x6e\x99\x2d\x61\x63\x15\xba\x01\x3b\x80" + "\xc6\xe3\xac\xfd\x97\x26\x07\x42\x4b\xec\x10\x88\x8d\xd5\x0a\x15\x13" + "\x42\xf1\x5d\x48\x18\x36\xfd\xf3\x65\x8c\x86\xe8\x58\xac\xc0\xf2\x5f" + "\x3f\xe0\xce\xe5\xc2\x29\x5f\x04\x4e\x1e\x82\x89\xbc\x52\x50\x9d\x4f" + "\x5c\x4e\xb5\x56\x5d\x4e\x62\x0a\xe5\x9c\xdb\x26\xa5\x02\x09\x63\xfc" + "\x6e\x43\xca\x0e\x7c\x45\x29\xb3\x22\x38\xfd\x3a\xf6\x0e\xda\xf6\xc5" + "\x6c\x9f\xb4\x08\x95\xb1\xe6\x01\x4a\xd7\x3b\x69\xb3\xcb\x55\x2c\xe8" + "\xc0\xa8\xbb\x14\xff\xe9\x66\xc7\x4d\x40\xfc\x28\xd2\x05\xab\xee\x5f" + "\x73\xed\x9a\x0d\xb9\x64\xe0\x69\xe4\x86\xb3\x02\xef\x58\xcc\xf6\xa3" + "\x4b\x4e", + .senderPrivKey = "\xf1\xfe\xd7\x17\xb0\xd8\x35\xd2\x9b\x9e\x4e\x4f\x4c" + "\xf8\x7d\x0c\xb9\x85\xe2\xbc\x44\xff\xd6\xd4\x37\xa4" + "\xe7\x4a\x18\x04\x55\x87", + .recvPubKey = "\x04\x09\x22\xf1\x7c\xed\x7d\x09\xd4\x55\x10\x2d\xd9\x0c" + "\x31\x46\xce\x33\x2f\x45\xd2\x34\x96\x46\xfe\x64\xca\xbb" + "\x7f\x41\x23\x03\xbe\x5c\x23\x53\x9a\xff\x5c\x30\x72\xdd" + "\x85\xa9\x0b\x76\x92\xe1\x96\x22\x09\x39\xee\xb7\x3e\x0e" + "\xf2\x47\x59\x32\xb1\xcd\xa9\x16\xc7", + .authSecret = + "\xd3\xf8\x22\xf4\x8c\x74\x74\x19\x5d\xa6\x4d\x78\x5f\x40\x8f\x26", + .salt = "\x77\x10\x67\x4b\x1e\xcd\x0b\xf7\xa7\xd3\x25\x88\xea\xe9\x53\x9c", + .plaintext = "I am the very model of a modern Major-General.", + .plaintextLen = 46, + .padLen = 0, + .maxCiphertextLen = 478, + .ciphertextLen = 478, + .rs = 4, + }, + { + .desc = "rs = 6, pad = 4", + .ciphertext = "\x54\x6d\x13\x0b\x1e\xf0\xc2\x3e\xff\x49\x87\xe5\xc6\x57\xf1" + "\x94\xb1\xb8\xad\x5a\xda\xb7\x02\x32\x3c\xf9\xc0\x7b\xd8\x28" + "\x20\x9e\x5d\xe7\x5e\x8b\xf6\x6a\xa5\xf8\xf6\x3b\x0a\x66\xd3" + "\x99\xeb\x8b\x98\x70\x6c\xfc\xa5\xa5\x3f\x8f\x50\x8c\x26\x56" + "\x5a\x34\xe4", + .senderPrivKey = "\xe3\x31\x8e\xc3\x99\xa9\xc4\x71\x7a\xa9\x4b\xa4\xed\xb3" + "\xaa\x1d\x90\x96\x5c\xe7\x6a\x57\x6b\x52\xa0\x7c\x27\x44" + "\x0c\x6d\x9b\xd1", + .recvPubKey = "\x04\xaa\xec\x79\x05\x22\xad\x2b\x70\x56\x05\x58\x99\xda\xa6" + "\x47\x7d\x55\xab\x3f\x16\x5d\x76\x00\x56\x19\xf3\xdf\xa9\x72" + "\x19\xf5\x59\x02\x5d\x09\xc0\x80\x83\x70\xc6\x06\x8a\x66\x51" + "\x81\xd4\x3d\x74\xac\x3d\x0a\xca\x28\x67\x5e\x81\x55\x9a\x20" + "\x4a\x6d\x44\xb5\x91", + .authSecret = + "\x54\x9d\xe2\xfd\x40\x52\xef\x4e\x05\x28\x45\x13\x3e\x37\x61\x3d", + .salt = "\x3a\x4b\xd3\x72\x75\x09\x5d\x71\xc9\x81\xe0\x9c\xde\x0d\xa6\x2a", + .plaintext = "Hello", + .plaintextLen = 5, + .padLen = 4, + .maxCiphertextLen = 63, + .ciphertextLen = 63, + .rs = 6, + }, +}; + +void +test_webpush_aesgcm_encrypt_ok(void) { + size_t tests = sizeof(webpush_aesgcm_encrypt_ok_tests) / + sizeof(webpush_aesgcm_encrypt_ok_test_t); + for (size_t i = 0; i < tests; i++) { + webpush_aesgcm_encrypt_ok_test_t t = webpush_aesgcm_encrypt_ok_tests[i]; + + const void* senderPrivKey = t.senderPrivKey; + const void* authSecret = t.authSecret; + const void* salt = t.salt; + const void* recvPubKey = t.recvPubKey; + const void* plaintext = t.plaintext; + + size_t ciphertextLen = + ece_aesgcm_ciphertext_max_length(t.rs, t.padLen, t.plaintextLen); + ece_assert(ciphertextLen == t.maxCiphertextLen, + "Got ciphertext max length %zu for `%s`; want %zu", + ciphertextLen, t.desc, t.maxCiphertextLen); + + uint8_t* ciphertext = calloc(ciphertextLen, sizeof(uint8_t)); + + int err = ece_webpush_aesgcm_encrypt_with_keys( + senderPrivKey, ECE_WEBPUSH_PRIVATE_KEY_LENGTH, authSecret, + ECE_WEBPUSH_AUTH_SECRET_LENGTH, salt, ECE_SALT_LENGTH, recvPubKey, + ECE_WEBPUSH_PUBLIC_KEY_LENGTH, t.rs, t.padLen, plaintext, t.plaintextLen, + ciphertext, &ciphertextLen); + ece_assert(!err, "Got %d encrypting ciphertext for `%s`", err, t.desc); + + ece_assert(ciphertextLen == t.ciphertextLen, + "Got actual ciphertext length %zu for `%s`; want %zu", + ciphertextLen, t.desc, t.ciphertextLen); + ece_assert(!memcmp(ciphertext, t.ciphertext, ciphertextLen), + "Wrong ciphertext for `%s`", t.desc); + + free(ciphertext); + } +} + +void +test_webpush_aesgcm_encrypt_pad(void) { + static const uint32_t maxRs = 128; + + const void* senderPrivKey = "\xac\xae\xc1\xc3\x7c\x30\x7c\xb9\x02\x8f\xbb\xd9" + "\xc7\xf3\xc6\x89\x26\x60\x08\x95\x9a\x5e\xd4\x03" + "\x42\x21\xb2\xda\x72\x01\x82\x8f"; + const void* authSecret = + "\x44\x29\x81\x2d\x53\x5f\xbf\xdb\xea\xc8\x6d\xb7\x14\x5c\x6a\xf2"; + const void* salt = + "\x45\x2b\xfb\xea\x8c\xc7\xa7\x57\x14\xd2\x03\xcf\xf1\x02\xe8\x76"; + const void* recvPubKey = + "\x04\x2d\x78\x8d\x3e\x8e\x82\xf2\xd7\xea\xef\xbd\xe3\xa1\xbe\xde\xa2\x1f" + "\x3b\xc9\x60\x33\x15\x73\x22\xa0\x9e\x14\x46\x55\xa3\xdf\x78\xfd\xca\xc8" + "\x10\xe3\x02\x2a\xb5\x6a\x0e\xa9\xb8\xec\x06\x73\x8a\xce\x41\x1f\x49\x54" + "\x7b\xc0\x0d\x1a\x1c\xde\x97\xce\x7b\xdd\x26"; + + for (uint32_t rs = ECE_AESGCM_MIN_RS + 1; rs <= maxRs; rs++) { + // Generate a random plaintext. + size_t plaintextLen = (size_t)(rand() % (int) (maxRs - 1) + 1); + uint8_t* plaintext = calloc(plaintextLen, sizeof(uint8_t)); + int ok = RAND_bytes(plaintext, (int) plaintextLen); + ece_assert(ok == 1, "Got %d generating plaintext for rs = %d", ok, rs); + + size_t maxPadLen = (rs - ECE_AESGCM_MIN_RS) * (plaintextLen + 1); + + // Encrypting with the maximum padding length should succeed. + size_t ciphertextLen = + ece_aesgcm_ciphertext_max_length(rs, maxPadLen, plaintextLen); + uint8_t* ciphertext = calloc(ciphertextLen, sizeof(uint8_t)); + + int err = ece_webpush_aesgcm_encrypt_with_keys( + senderPrivKey, ECE_WEBPUSH_PRIVATE_KEY_LENGTH, authSecret, + ECE_WEBPUSH_AUTH_SECRET_LENGTH, salt, ECE_SALT_LENGTH, recvPubKey, + ECE_WEBPUSH_PUBLIC_KEY_LENGTH, rs, maxPadLen, plaintext, plaintextLen, + ciphertext, &ciphertextLen); + ece_assert(!err, "Got %d encrypting with rs = %d, padLen = %zu", err, rs, + maxPadLen); + + // Adding more padding should fail. + size_t badPadLen = maxPadLen + 1; + ciphertextLen = + ece_aesgcm_ciphertext_max_length(rs, badPadLen, plaintextLen); + ciphertext = realloc(ciphertext, ciphertextLen); + + err = ece_webpush_aesgcm_encrypt_with_keys( + senderPrivKey, ECE_WEBPUSH_PRIVATE_KEY_LENGTH, authSecret, + ECE_WEBPUSH_AUTH_SECRET_LENGTH, salt, ECE_SALT_LENGTH, recvPubKey, + ECE_WEBPUSH_PUBLIC_KEY_LENGTH, rs, badPadLen, plaintext, plaintextLen, + ciphertext, &ciphertextLen); + ece_assert(err == ECE_ERROR_ENCRYPT_PADDING, + "Want error encrypting with rs = %d, padLen = %zu", rs, + badPadLen); + + free(plaintext); + free(ciphertext); + } +} diff --git a/mobile/ios/ThirdParty/ecec/test/params.c b/mobile/ios/ThirdParty/ecec/test/params.c new file mode 100644 index 0000000000..06d7d92a04 --- /dev/null +++ b/mobile/ios/ThirdParty/ecec/test/params.c @@ -0,0 +1,391 @@ +#include "test.h" + +#include +#include + +typedef struct webpush_aesgcm_from_params_test_s { + const char* cryptoKey; + const char* encryption; + const char* salt; + const char* rawSenderPubKey; + size_t cryptoKeyLen; + size_t encryptionLen; + size_t saltLen; + size_t rawSenderPubKeyLen; + uint32_t rs; +} webpush_aesgcm_from_params_test_t; + +static webpush_aesgcm_from_params_test_t webpush_aesgcm_from_params_tests[] = { + { + .cryptoKey = "dh=Iy1Je2Kv11A", + .cryptoKeyLen = 14, + .encryption = "rs=7;salt=upk1yFkp1xI", + .encryptionLen = 21, + .salt = "\xba\x99\x35\xc8\x59\x29\xd7\x12", + .saltLen = 8, + .rawSenderPubKey = "\x23\x2d\x49\x7b\x62\xaf\xd7\x50", + .rawSenderPubKeyLen = 8, + .rs = 7, + }, + { + .cryptoKey = "dh=mwyOULZ4upjVbCpQLRLOeg", + .cryptoKeyLen = 25, + .encryption = "rs=4096;salt=qzqku7FRdW9Vs97w8O8DoA", + .encryptionLen = 35, + .salt = "\xab\x3a\xa4\xbb\xb1\x51\x75\x6f\x55\xb3\xde\xf0\xf0\xef\x03\xa0", + .saltLen = 16, + .rawSenderPubKey = + "\x9b\x0c\x8e\x50\xb6\x78\xba\x98\xd5\x6c\x2a\x50\x2d\x12\xce\x7a", + .rawSenderPubKeyLen = 16, + .rs = 4096, + }, +}; + +typedef struct webpush_aesgcm_extract_params_ok_test_s { + const char* desc; + const char* cryptoKey; + const char* encryption; + const char* salt; + const char* rawSenderPubKey; + uint32_t rs; +} webpush_aesgcm_extract_params_ok_test_t; + +static webpush_aesgcm_extract_params_ok_test_t + webpush_aesgcm_extract_params_ok_tests[] = { + { + .desc = "Multiple keys in Crypto-Key header", + .cryptoKey = "keyid=p256dh;dh=Iy1Je2Kv11A,p256ecdsa=o2M8QfiEKuI", + .encryption = "keyid=p256dh;salt=upk1yFkp1xI", + .salt = "\xba\x99\x35\xc8\x59\x29\xd7\x12", + .rawSenderPubKey = "\x23\x2d\x49\x7b\x62\xaf\xd7\x50", + .rs = 4096, + }, + { + .desc = "Multiple keys in both headers", + .cryptoKey = "keyid=a;dh=bX0VbuZy8HQ,dh=Iy1Je2Kv11A;keyid=p256dh", + .encryption = + "salt=upk1yFkp1xI;rs=48;keyid=p256dh,salt=U0DM1JsdIbU;keyid=a", + .salt = "\xba\x99\x35\xc8\x59\x29\xd7\x12", + .rawSenderPubKey = "\x23\x2d\x49\x7b\x62\xaf\xd7\x50", + .rs = 48, + }, + { + .desc = "Quoted key ID pair value", + .cryptoKey = "dh=\"byfHbUffc-k\"", + .encryption = "salt=C11AvAsp6Gc", + .salt = "\x0b\x5d\x40\xbc\x0b\x29\xe8\x67", + .rawSenderPubKey = "\x6f\x27\xc7\x6d\x47\xdf\x73\xe9", + .rs = 4096, + }, + { + .desc = "Quoted salt pair value and rs = 24", + .cryptoKey = "dh=ybuT4VDz-Bg", + .encryption = "rs=24; salt=\"H7U7wcIoIKs\"", + .salt = "\x1f\xb5\x3b\xc1\xc2\x28\x20\xab", + .rawSenderPubKey = "\xc9\xbb\x93\xe1\x50\xf3\xf8\x18", + .rs = 24, + }, + { + .desc = "Multiple keys, extra whitespace, strange key ID", + .cryptoKey = " dh= \"ujIToeKunCY\" ,keyid = hello ; dh = I7p5M0yyP8A ", + .encryption = "salt=ie_oYLhw7SI; keyid=\"hello\"; rs =6 , salt = " + "6NAh50bfJZc ;keyid=ujIToeKunCY ", + .salt = "\x89\xef\xe8\x60\xb8\x70\xed\x22", + .rawSenderPubKey = "\x23\xba\x79\x33\x4c\xb2\x3f\xc0", + .rs = 6, + }, + { + // Invalid, but we don't check the entire `Crypto-Key` param once we see a + // match. + .desc = "Duplicate key ID in Crypto-Key", + .cryptoKey = "keyid=a;keyid=b;dh=pbmv1QkcEDY", + .encryption = "salt=Esao8aTBfIk;keyid=b", + .salt = "\x12\xc6\xa8\xf1\xa4\xc1\x7c\x89", + .rawSenderPubKey = "\xa5\xb9\xaf\xd5\x09\x1c\x10\x36", + .rs = 4096, + }, +}; + +typedef struct webpush_aesgcm_extract_params_err_test_s { + const char* desc; + const char* cryptoKey; + const char* encryption; + int err; +} webpush_aesgcm_extract_params_err_test_t; + +static webpush_aesgcm_extract_params_err_test_t + webpush_aesgcm_extract_params_err_tests[] = { + { + .desc = "Invalid record size", + .cryptoKey = "dh=pbmv1QkcEDY", + .encryption = "salt=Esao8aTBfIk;rs=bad", + .err = ECE_ERROR_INVALID_RS, + }, + { + .desc = "Blank Crypto-Key header", + .cryptoKey = " \t ", + .encryption = "salt=Esao8aTBfIk", + .err = ECE_ERROR_INVALID_CRYPTO_KEY_HEADER, + }, + { + .desc = "Empty Encryption header", + .cryptoKey = "dh=pbmv1QkcEDY", + .encryption = "", + .err = ECE_ERROR_INVALID_ENCRYPTION_HEADER, + }, + { + .desc = "Crypto-Key missing pair value", + .cryptoKey = "dh=", + .encryption = "salt=Esao8aTBfIk", + .err = ECE_ERROR_INVALID_CRYPTO_KEY_HEADER, + }, + { + .desc = "Encryption missing pair value with trailing whitespace", + .cryptoKey = "dh=pbmv1QkcEDY", + .encryption = "salt= ", + .err = ECE_ERROR_INVALID_ENCRYPTION_HEADER, + }, + { + .desc = "Crypto-Key pair without value", + .cryptoKey = "dh=pbmv1QkcEDY; keyid, dh=rqowftPcCVo", + .encryption = "salt=Esao8aTBfIk", + .err = ECE_ERROR_INVALID_CRYPTO_KEY_HEADER, + }, + { + .desc = "Encryption pair without value", + .cryptoKey = "dh=pbmv1QkcEDY", + .encryption = "rs; salt=Esao8aTBfIk", + .err = ECE_ERROR_INVALID_ENCRYPTION_HEADER, + }, + { + .desc = "Crypto-Key missing pair name", + .cryptoKey = "dh=pbmv1QkcEDY; =rqowftPcCVo", + .encryption = "salt=Esao8aTBfIk", + .err = ECE_ERROR_INVALID_CRYPTO_KEY_HEADER, + }, + { + .desc = "Encryption missing pair name", + .cryptoKey = "dh=pbmv1QkcEDY", + .encryption = "=Esao8aTBfIk", + .err = ECE_ERROR_INVALID_ENCRYPTION_HEADER, + }, + { + .desc = "Whitespace in quoted Crypto-Key pair value", + .cryptoKey = "dh=byfHbUffc-k; param=\" \"", + .encryption = "salt=C11AvAsp6Gc", + .err = ECE_ERROR_INVALID_CRYPTO_KEY_HEADER, + }, + { + .desc = "Empty quoted value in Encryption header", + .cryptoKey = "dh=byfHbUffc-k", + .encryption = "salt=\"\"; rs=6", + .err = ECE_ERROR_INVALID_ENCRYPTION_HEADER, + }, + { + .desc = "Invalid character in Crypto-Key pair value", + .cryptoKey = "dh==byfHbUffc-k", + .encryption = "salt=C11AvAsp6Gc", + .err = ECE_ERROR_INVALID_CRYPTO_KEY_HEADER, + }, + { + .desc = "Invalid character in Encryption pair name", + .cryptoKey = "dh=byfHbUffc-k", + .encryption = "sa!t=C11AvAsp6Gc", + .err = ECE_ERROR_INVALID_ENCRYPTION_HEADER, + }, + { + .desc = "Leading , in Crypto-Key header", + .cryptoKey = ",dh=byfHbUffc-k", + .encryption = "salt=C11AvAsp6Gc", + .err = ECE_ERROR_INVALID_CRYPTO_KEY_HEADER, + }, + { + .desc = "Trailing ; in Crypto-Key header", + .cryptoKey = "dh=byfHbUffc-k;", + .encryption = "salt=C11AvAsp6Gc", + .err = ECE_ERROR_INVALID_CRYPTO_KEY_HEADER, + }, + { + .desc = "Leading ; in Encryption header", + .cryptoKey = "dh=byfHbUffc-k", + .encryption = "; salt=C11AvAsp6Gc", + .err = ECE_ERROR_INVALID_ENCRYPTION_HEADER, + }, + { + .desc = "Trailing , in Encryption header", + .cryptoKey = "dh=byfHbUffc-k", + .encryption = "salt=C11AvAsp6Gc,", + .err = ECE_ERROR_INVALID_ENCRYPTION_HEADER, + }, + { + .desc = "Unterminated quoted value in Encryption header", + .cryptoKey = "dh=byfHbUffc-k", + .encryption = "rs=6; salt=\"C11AvAsp6Gc", + .err = ECE_ERROR_INVALID_ENCRYPTION_HEADER, + }, + { + .desc = "Invalid quoted name in Crypto-Key header", + .cryptoKey = "\"dh\"=\"byfHbUffc-k\"", + .encryption = "salt=C11AvAsp6Gc", + .err = ECE_ERROR_INVALID_CRYPTO_KEY_HEADER, + }, + { + .desc = "Invalid quoted name in Encryption header", + .cryptoKey = "dh=byfHbUffc-k", + .encryption = "\"salt\"=C11AvAsp6Gc", + .err = ECE_ERROR_INVALID_ENCRYPTION_HEADER, + }, + { + .desc = "Mismatched key IDs", + .cryptoKey = "keyid=p256dh;dh=pbmv1QkcEDY", + .encryption = "keyid=different;salt=Esao8aTBfIk", + .err = ECE_ERROR_INVALID_DH, + }, + { + .desc = "Multiple mismatched key IDs", + .cryptoKey = "keyid=a;dh=bX0VbuZy8HQ,dh=Iy1Je2Kv11A;keyid=b", + .encryption = "salt=upk1yFkp1xI;rs=48;keyid=c,salt=U0DM1JsdIbU;keyid=d", + .err = ECE_ERROR_INVALID_DH, + }, + { + .desc = "Key ID with matching pair value, wrong pair name", + .cryptoKey = "p256dh=p256dh;dh=pbmv1QkcEDY", + .encryption = "keyid=p256dh;salt=Esao8aTBfIk", + .err = ECE_ERROR_INVALID_DH, + }, + { + .desc = "Invalid Base64url-encoded salt", + .cryptoKey = "dh=pbmv1QkcEDY", + .encryption = "salt=99999", + .err = ECE_ERROR_INVALID_SALT, + }, + { + .desc = "Invalid Base64url-encoded dh pair value", + .cryptoKey = "dh=zzzzz", + .encryption = "salt=Esao8aTBfIk", + .err = ECE_ERROR_INVALID_DH, + }, + { + .desc = "Invalid character at end of salt pair name", + .cryptoKey = "dh=pbmv1QkcEDY", + .encryption = "salt !=Esao8aTBfIk", + .err = ECE_ERROR_INVALID_ENCRYPTION_HEADER, + }, + { + .desc = "Invalid character at end of salt", + .cryptoKey = "dh=pbmv1QkcEDY", + .encryption = "salt=Esao8aTBfIk!", + .err = ECE_ERROR_INVALID_ENCRYPTION_HEADER, + }, + { + .desc = "Invalid character after quoted dh pair value", + .cryptoKey = "dh=\"pbmv1QkcEDY\"!", + .encryption = "salt=Esao8aTBfIk", + .err = ECE_ERROR_INVALID_CRYPTO_KEY_HEADER, + }, + { + .desc = "Duplicate key ID in Encryption header", + .cryptoKey = "keyid=b;dh=pbmv1QkcEDY", + .encryption = "keyid=a;salt=Esao8aTBfIk;keyid=b", + .err = ECE_ERROR_INVALID_ENCRYPTION_HEADER, + }, + { + .desc = "Duplicate record size in Encryption header", + .cryptoKey = "dh=pbmv1QkcEDY", + .encryption = "rs=5;salt=Esao8aTBfIk;rs=10", + .err = ECE_ERROR_INVALID_ENCRYPTION_HEADER, + }, + { + .desc = "Duplicate salt in Encryption header", + .cryptoKey = "dh=pbmv1QkcEDY", + .encryption = "salt=Esao8aTBfIk; salt=Esao8aTBfIk", + .err = ECE_ERROR_INVALID_ENCRYPTION_HEADER, + }, + { + .desc = "Missing salt in Encryption header", + .cryptoKey = "dh=pbmv1QkcEDY", + .encryption = "rs=5", + .err = ECE_ERROR_INVALID_SALT, + }, +}; + +void +test_webpush_aesgcm_headers_from_params(void) { + size_t length = sizeof(webpush_aesgcm_from_params_tests) / + sizeof(webpush_aesgcm_from_params_test_t); + for (size_t i = 0; i < length; i++) { + webpush_aesgcm_from_params_test_t t = webpush_aesgcm_from_params_tests[i]; + + size_t cryptoKeyLen = 0; + size_t encryptionLen = 0; + int err = ece_webpush_aesgcm_headers_from_params( + t.salt, t.saltLen, t.rawSenderPubKey, t.rawSenderPubKeyLen, t.rs, NULL, + &cryptoKeyLen, NULL, &encryptionLen); + ece_assert(!err, "Got %d determining lengths for (%s, %s)", err, + t.cryptoKey, t.encryption); + ece_assert(cryptoKeyLen == t.cryptoKeyLen, + "Got Crypto-Key length %zu for (%s, %s); want %zu", cryptoKeyLen, + t.cryptoKey, t.encryption, t.cryptoKeyLen); + ece_assert(encryptionLen == t.encryptionLen, + "Got Encryption length %zu for (%s, %s); want %zu", + encryptionLen, t.cryptoKey, t.encryption, t.encryptionLen); + + char* cryptoKey = malloc(cryptoKeyLen + 1); + char* encryption = malloc(encryptionLen + 1); + + err = ece_webpush_aesgcm_headers_from_params( + t.salt, t.saltLen, t.rawSenderPubKey, t.rawSenderPubKeyLen, t.rs, + cryptoKey, &cryptoKeyLen, encryption, &encryptionLen); + ece_assert(!err, "Got %d formatting headers for (%s, %s)", err, t.cryptoKey, + t.encryption); + ece_assert(!memcmp(cryptoKey, t.cryptoKey, t.cryptoKeyLen), + "Wrong Crypto-Key for (%s, %s)", t.cryptoKey, t.encryption); + ece_assert(!memcmp(encryption, t.encryption, t.encryptionLen), + "Wrong Encryption for (%s, %s)", t.cryptoKey, t.encryption); + + free(cryptoKey); + free(encryption); + } +} + +void +test_webpush_aesgcm_headers_extract_params_ok(void) { + size_t length = sizeof(webpush_aesgcm_extract_params_ok_tests) / + sizeof(webpush_aesgcm_extract_params_ok_test_t); + for (size_t i = 0; i < length; i++) { + webpush_aesgcm_extract_params_ok_test_t t = + webpush_aesgcm_extract_params_ok_tests[i]; + + uint8_t salt[8]; + uint32_t rs; + uint8_t rawSenderPubKey[8]; + int err = ece_webpush_aesgcm_headers_extract_params( + t.cryptoKey, t.encryption, salt, 8, rawSenderPubKey, 8, &rs); + + ece_assert(!err, "Got %d extracting params for `%s`", err, t.desc); + ece_assert(!memcmp(salt, t.salt, 8), "Wrong salt for `%s`", t.desc); + ece_assert(rs == t.rs, "Got rs = %" PRIu32 " for `%s`; want %" PRIu32, rs, + t.desc, t.rs); + ece_assert(!memcmp(rawSenderPubKey, t.rawSenderPubKey, 8), + "Wrong public key for `%s`", t.desc); + } +} + +void +test_webpush_aesgcm_headers_extract_params_err(void) { + size_t length = sizeof(webpush_aesgcm_extract_params_err_tests) / + sizeof(webpush_aesgcm_extract_params_err_test_t); + for (size_t i = 0; i < length; i++) { + webpush_aesgcm_extract_params_err_test_t t = + webpush_aesgcm_extract_params_err_tests[i]; + + uint8_t salt[8]; + uint32_t rs; + uint8_t rawSenderPubKey[8]; + int err = ece_webpush_aesgcm_headers_extract_params( + t.cryptoKey, t.encryption, salt, 8, rawSenderPubKey, 8, &rs); + + ece_assert(err == t.err, "Got %d extracting params for `%s`; want %d", err, + t.desc, t.err); + } +} diff --git a/mobile/ios/ThirdParty/ecec/test/test.c b/mobile/ios/ThirdParty/ecec/test/test.c new file mode 100644 index 0000000000..8bd4699aa7 --- /dev/null +++ b/mobile/ios/ThirdParty/ecec/test/test.c @@ -0,0 +1,66 @@ +#include "test.h" + +#include +#include +#include +#include + +int +main() { + test_webpush_aesgcm_headers_from_params(); + test_webpush_aesgcm_headers_extract_params_ok(); + test_webpush_aesgcm_headers_extract_params_err(); + + test_webpush_aesgcm_encrypt_ok(); + test_webpush_aesgcm_encrypt_pad(); + test_webpush_aesgcm_decrypt_ok(); + test_webpush_aesgcm_decrypt_err(); + + test_webpush_aes128gcm_encrypt_ok(); + test_webpush_aes128gcm_encrypt_pad(); + test_webpush_aes128gcm_decrypt_ok(); + test_webpush_aes128gcm_decrypt_err(); + test_aes128gcm_decrypt_ok(); + test_aes128gcm_decrypt_err(); + + test_webpush_aes128gcm_e2e(); + test_webpush_aesgcm_e2e(); + + test_base64url_encode(); + test_base64url_decode(); + + return 0; +} + +void +ece_log(const char* funcName, int line, const char* expr, const char* format, + ...) { + char* message = NULL; + va_list args; + va_start(args, format); + + // Determine the size of the formatted message, then allocate and write to a + // buffer large enough to hold the message. `vsnprintf` mutates its argument + // list, so we make a copy for calculating the size. + va_list sizeArgs; + va_copy(sizeArgs, args); + int size = vsnprintf(NULL, 0, format, sizeArgs); + va_end(sizeArgs); + if (size < 0) { + goto error; + } + message = malloc((size_t) size + 1); + if (!message || vsprintf(message, format, args) != size) { + goto error; + } + message[size] = '\0'; + fprintf(stderr, "[%s:%d] (%s): %s\n", funcName, line, expr, message); + goto end; + +error: + fprintf(stderr, "[%s:%d]: %s\n", funcName, line, expr); + +end: + va_end(args); + free(message); +} diff --git a/mobile/ios/ThirdParty/ecec/test/test.h b/mobile/ios/ThirdParty/ecec/test/test.h new file mode 100644 index 0000000000..762309c483 --- /dev/null +++ b/mobile/ios/ThirdParty/ecec/test/test.h @@ -0,0 +1,70 @@ +#include +#include + +#include + +// This macro is similar to the standard `assert`, but accepts a format string +// with an informative failure message. +#define ece_assert(cond, format, ...) \ + do { \ + if (!(cond)) { \ + ece_log(__func__, __LINE__, #cond, format, __VA_ARGS__); \ + abort(); \ + } \ + } while (0) + +// Logs an assertion failure to standard error. +void +ece_log(const char* funcName, int line, const char* expr, const char* format, + ...); + +void +test_webpush_aesgcm_headers_from_params(void); + +void +test_webpush_aesgcm_headers_extract_params_ok(void); + +void +test_webpush_aesgcm_headers_extract_params_err(void); + +void +test_webpush_aesgcm_encrypt_ok(void); + +void +test_webpush_aesgcm_encrypt_pad(void); + +void +test_webpush_aesgcm_decrypt_ok(void); + +void +test_webpush_aesgcm_decrypt_err(void); + +void +test_webpush_aes128gcm_encrypt_ok(void); + +void +test_webpush_aes128gcm_encrypt_pad(void); + +void +test_aes128gcm_decrypt_ok(void); + +void +test_webpush_aes128gcm_decrypt_ok(void); + +void +test_aes128gcm_decrypt_err(void); + +void +test_webpush_aes128gcm_decrypt_err(void); + +void +test_webpush_aes128gcm_e2e(void); + +void +test_webpush_aesgcm_e2e(void); + +void +test_base64url_encode(void); + +void +test_base64url_decode(void); diff --git a/mobile/ios/ThirdParty/ecec/tool/decrypt.c b/mobile/ios/ThirdParty/ecec/tool/decrypt.c new file mode 100644 index 0000000000..85ac8d3279 --- /dev/null +++ b/mobile/ios/ThirdParty/ecec/tool/decrypt.c @@ -0,0 +1,88 @@ +#include +#include +#include + +#include + +int +main(int argc, char** argv) { + if (argc < 4) { + fprintf(stderr, "Usage: %s ", + argv[0]); + return 2; + } + + int err = 0; + uint8_t* payload = NULL; + uint8_t* plaintext = NULL; + + uint8_t authSecret[ECE_WEBPUSH_AUTH_SECRET_LENGTH]; + if (!ece_base64url_decode(argv[1], strlen(argv[1]), + ECE_BASE64URL_REJECT_PADDING, authSecret, + ECE_WEBPUSH_AUTH_SECRET_LENGTH)) { + fprintf(stderr, "Error: Failed to Base64url-decode auth secret\n"); + goto error; + } + uint8_t rawRecvPrivKey[ECE_WEBPUSH_PRIVATE_KEY_LENGTH]; + if (!ece_base64url_decode(argv[2], strlen(argv[2]), + ECE_BASE64URL_REJECT_PADDING, rawRecvPrivKey, + ECE_WEBPUSH_PRIVATE_KEY_LENGTH)) { + fprintf(stderr, "Error: Failed to Base64url-decode private key\n"); + goto error; + } + size_t payloadBase64Len = strlen(argv[3]); + size_t payloadLen = ece_base64url_decode( + argv[3], payloadBase64Len, ECE_BASE64URL_REJECT_PADDING, NULL, 0); + if (!payloadLen) { + fprintf(stderr, "Error: Empty or invalid Base64url-encoded message\n"); + goto error; + } + payload = calloc(payloadLen, sizeof(uint8_t)); + if (!payload) { + fprintf( + stderr, + "Error: Failed to allocate %zu bytes for Base64url-decoded message\n", + payloadLen); + goto error; + } + payloadLen = + ece_base64url_decode(argv[3], payloadBase64Len, + ECE_BASE64URL_REJECT_PADDING, payload, payloadLen); + if (!payloadLen) { + fprintf(stderr, "Error: Failed to Base64url-decode message\n"); + goto error; + } + + size_t plaintextLen = ece_aes128gcm_plaintext_max_length(payload, payloadLen); + if (!plaintextLen) { + fprintf(stderr, "Error: Encrypted message too short\n"); + goto error; + } + plaintextLen++; + plaintext = calloc(plaintextLen, sizeof(uint8_t)); + if (!plaintext) { + fprintf(stderr, + "Error: Failed to allocate %zu bytes for decrypted message\n", + plaintextLen); + goto error; + } + err = ece_webpush_aes128gcm_decrypt( + rawRecvPrivKey, ECE_WEBPUSH_PRIVATE_KEY_LENGTH, authSecret, + ECE_WEBPUSH_AUTH_SECRET_LENGTH, payload, payloadLen, plaintext, + &plaintextLen); + if (err) { + fprintf(stderr, "Error: Failed to decrypt message: %d\n", err); + goto error; + } + plaintext[plaintextLen] = '\0'; + printf("Decrypted message: %s\n", plaintext); + goto end; + +error: + err = 1; + +end: + free(payload); + free(plaintext); + return err; +} diff --git a/mobile/ios/ThirdParty/ecec/tool/keygen.c b/mobile/ios/ThirdParty/ecec/tool/keygen.c new file mode 100644 index 0000000000..7ff4240620 --- /dev/null +++ b/mobile/ios/ThirdParty/ecec/tool/keygen.c @@ -0,0 +1,63 @@ +#include +#include + +#include + +static const char ece_keygen_hex_alphabet[] = "0123456789abcdef"; + +char* +ece_keygen_hex_encode(const uint8_t* binary, size_t binaryLen) { + if (binaryLen > SIZE_MAX / 2 - 1) { + return NULL; + } + char* encoded = malloc(binaryLen * 2 + 1); + if (!encoded) { + return NULL; + } + char* hex = encoded; + for (size_t i = 0; i < binaryLen; i++) { + *hex++ = ece_keygen_hex_alphabet[(binary[i] >> 4) & 0xf]; + *hex++ = ece_keygen_hex_alphabet[binary[i] & 0xf]; + } + *hex = '\0'; + return encoded; +} + +int +main(int argc, char** argv) { + uint8_t rawRecvPrivKey[ECE_WEBPUSH_PRIVATE_KEY_LENGTH]; + uint8_t rawRecvPubKey[ECE_WEBPUSH_PUBLIC_KEY_LENGTH]; + uint8_t authSecret[ECE_WEBPUSH_AUTH_SECRET_LENGTH]; + + int err = ece_webpush_generate_keys( + rawRecvPrivKey, ECE_WEBPUSH_PRIVATE_KEY_LENGTH, rawRecvPubKey, + ECE_WEBPUSH_PUBLIC_KEY_LENGTH, authSecret, ECE_WEBPUSH_AUTH_SECRET_LENGTH); + if (err) { + fprintf(stderr, "Error: Failed to generate subscription keys: %d\n", err); + return 1; + } + + char* hexRecvPrivKey = + ece_keygen_hex_encode(rawRecvPrivKey, ECE_WEBPUSH_PRIVATE_KEY_LENGTH); + if (hexRecvPrivKey) { + printf("Private key: %s\n", hexRecvPrivKey); + } + + char* hexRecvPubKey = + ece_keygen_hex_encode(rawRecvPubKey, ECE_WEBPUSH_PUBLIC_KEY_LENGTH); + if (hexRecvPubKey) { + printf("Public key: %s\n", hexRecvPubKey); + } + + char* hexAuthSecret = + ece_keygen_hex_encode(authSecret, ECE_WEBPUSH_AUTH_SECRET_LENGTH); + if (hexAuthSecret) { + printf("Authentication secret: %s\n", hexAuthSecret); + } + + free(hexRecvPrivKey); + free(hexRecvPubKey); + free(hexAuthSecret); + + return 0; +} diff --git a/mobile/ios/ThirdParty/sqlcipher/.gitignore b/mobile/ios/ThirdParty/sqlcipher/.gitignore new file mode 100644 index 0000000000..f68644eb92 --- /dev/null +++ b/mobile/ios/ThirdParty/sqlcipher/.gitignore @@ -0,0 +1,40 @@ +*.lo +*.o +*.mode1v3 +*.pbxuser +xcuserdata +xcuserdata/* +*.xcworkspace/* +*.xcworkspace +/Makefile +/.libs +/.target_source +/config.h +/config.log +/config.status +/keywordhash.h +/mkkeywordhash* +/opcodes.c +/opcodes.h +/parse.c +/parse.h +/parse.h.temp +/parse.out +/tsrc +/testfixture* +/lemon* +/libtool +/libsqlite3.la +/libtclsqlite3.la +/sqlite3 +/sqlite3.c +/sqlite3.h +/lempar.c +/parse.y +/shell.c +/sqlcipher +/sqlite3.pc +/sqlcipher.pc +/sqlite3ext.h +/libsqlcipher.la +/.DS_Store diff --git a/mobile/ios/ThirdParty/sqlcipher/CHANGELOG.md b/mobile/ios/ThirdParty/sqlcipher/CHANGELOG.md new file mode 100644 index 0000000000..43d6fdb511 --- /dev/null +++ b/mobile/ios/ThirdParty/sqlcipher/CHANGELOG.md @@ -0,0 +1,78 @@ +# SQLCipher Change Log +All notable changes to this project will be documented in this file. + +## [Unreleased][unreleased] + +## [3.3.0] - 2015-03-25 +### Added +- Added FIPS API calls within the OpenSSL crpyto provider +- `PRAGMA cipher_default_page_size` - support for attaching non-default page sizes + +### Changed +- Merged upstream SQLite 3.8.8.3 + +## [3.2.0] - 2014-09-30 +### Added +- Added PRAGMA cipher_store_pass + +### Changed +- Merged upstream SQLite 3.8.6 +- Renmed README to README.md + +## [3.1.0] - 2014-04-23 +### Added +- Added `PRAGMA cipher_profile` + +### Changed +- Merged upstream SQLite 3.8.4.3 + +## [3.0.1] - 2013-12-06 +### Added +- Add `PRAGMA cipher_add_random` to source external entropy + +### Changed +- Fix `PRAGMA cipher_migrate` to handle passphrases longer than 64 characters & raw keys +- Improvements to the libtomcrypt provider + +## [3.0.0] - 2013-11-05 +### Added +- Added `PRAGMA cipher_migrate` to migrate older database file formats + +### Changed +- Merged upstream SQLite 3.8.0.2 +- Remove usage of VirtualLock/Unlock on WinRT and Windows Phone +- Ignore HMAC read during Btree file copy +- Fix lib naming for pkg-config +- Use _v2 version of sqlite3_key and sqlite3_rekey +- Update xcodeproj file + +### Security +- Change KDF iteration length from 4,000 to 64,000 + +[unreleased]: https://github.com/sqlcipher/sqlcipher/compare/v3.3.0...prerelease +[3.3.0]: https://github.com/sqlcipher/sqlcipher/compare/v3.2.0...v3.3.0 +[3.2.0]: https://github.com/sqlcipher/sqlcipher/compare/v3.1.0...v3.2.0 +[3.1.0]: https://github.com/sqlcipher/sqlcipher/compare/v3.0.1...v3.1.0 +[3.0.1]: https://github.com/sqlcipher/sqlcipher/compare/v3.0.0...v3.0.1 +[3.0.0]: https://github.com/sqlcipher/sqlcipher/compare/v2.2.0...v3.0.0 +[2.2.0]: https://github.com/sqlcipher/sqlcipher/compare/v2.1.1...v2.2.0 +[2.1.1]: https://github.com/sqlcipher/sqlcipher/compare/v2.1.0...v2.1.1 +[2.1.0]: https://github.com/sqlcipher/sqlcipher/compare/v2.0.6...v2.1.0 +[2.0.6]: https://github.com/sqlcipher/sqlcipher/compare/v2.0.5...v2.0.6 +[2.0.5]: https://github.com/sqlcipher/sqlcipher/compare/v2.0.3...v2.0.5 +[2.0.3]: https://github.com/sqlcipher/sqlcipher/compare/v2.0.0...v2.0.3 +[2.0.0]: https://github.com/sqlcipher/sqlcipher/compare/v1.1.10...v2.0.0 +[1.1.10]: https://github.com/sqlcipher/sqlcipher/compare/v1.1.9...v1.1.10 +[1.1.9]: https://github.com/sqlcipher/sqlcipher/compare/v1.1.8...v1.1.9 +[1.1.8]: https://github.com/sqlcipher/sqlcipher/compare/v1.1.7...v1.1.8 +[1.1.7]: https://github.com/sqlcipher/sqlcipher/compare/v1.1.6...v1.1.7 +[1.1.6]: https://github.com/sqlcipher/sqlcipher/compare/v1.1.5...v1.1.6 +[1.1.5]: https://github.com/sqlcipher/sqlcipher/compare/v1.1.4...v1.1.5 +[1.1.4]: https://github.com/sqlcipher/sqlcipher/compare/v1.1.3...v1.1.4 +[1.1.3]: https://github.com/sqlcipher/sqlcipher/compare/v1.1.2...v1.1.3 +[1.1.2]: https://github.com/sqlcipher/sqlcipher/compare/v1.1.1...v1.1.1 +[1.1.1]: https://github.com/sqlcipher/sqlcipher/compare/v1.1.0...v1.1.1 +[1.1.0]: https://github.com/sqlcipher/sqlcipher/compare/617ed01...v1.1.0 + + + diff --git a/mobile/ios/ThirdParty/sqlcipher/LICENSE b/mobile/ios/ThirdParty/sqlcipher/LICENSE new file mode 100644 index 0000000000..bebe1e7eb1 --- /dev/null +++ b/mobile/ios/ThirdParty/sqlcipher/LICENSE @@ -0,0 +1,24 @@ +Copyright (c) 2008, ZETETIC LLC +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * Neither the name of the ZETETIC LLC nor the + names of its contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY ZETETIC LLC ''AS IS'' AND ANY +EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL ZETETIC LLC BE LIABLE FOR ANY +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/mobile/ios/ThirdParty/sqlcipher/README.md b/mobile/ios/ThirdParty/sqlcipher/README.md new file mode 100644 index 0000000000..fc4547f4b0 --- /dev/null +++ b/mobile/ios/ThirdParty/sqlcipher/README.md @@ -0,0 +1,346 @@ +## SQLCipher + +SQLCipher is an SQLite extension that provides transparent 256-bit AES encryption of +database files. Pages are encrypted before being written to disk and are decrypted +when read back. Due to the small footprint and great performance it’s ideal for +protecting embedded application databases and is well suited for mobile development. + +The official SQLCipher software site is http://sqlcipher.net + +SQLCipher was initially developed by Stephen Lombardo at Zetetic LLC +(sjlombardo@zetetic.net) as the encrypted database layer for Strip, +an iPhone data vault and password manager (http://getstrip.com). + +## Features + +- Fast performance with as little as 5-15% overhead for encryption on many operations +- 100% of data in the database file is encrypted +- Good security practices (CBC mode, key derivation) +- Zero-configuration and application level cryptography +- Algorithms provided by the peer reviewed OpenSSL crypto library. +- Configurable crypto providers + +## Contributions + +We welcome contributions, to contribute to SQLCipher, a [contributor agreement](https://www.zetetic.net/contributions/) needs to be submitted. All submissions should be based on the `prerelease` branch. + +## Compiling + +Building SQLCipher is almost the same as compiling a regular version of +SQLite with two small exceptions: + + 1. You *must* define SQLITE_HAS_CODEC and SQLITE_TEMP_STORE=2 when building sqlcipher. + 2. You need to link against a OpenSSL's libcrypto + +Example Static linking (replace /opt/local/lib with the path to libcrypto.a). Note in this +example, --enable-tempstore=yes is setting SQLITE_TEMP_STORE=2 for the build. + + $ ./configure --enable-tempstore=yes CFLAGS="-DSQLITE_HAS_CODEC" \ + LDFLAGS="/opt/local/lib/libcrypto.a" + $ make + +Example Dynamic linking + + $ ./configure --enable-tempstore=yes CFLAGS="-DSQLITE_HAS_CODEC" \ + LDFLAGS="-lcrypto" + $ make + +## Encrypting a database + +To specify an encryption passphrase for the database via the SQL interface you +use a pragma. The passphrase you enter is passed through PBKDF2 key derivation to +obtain the encryption key for the database + + PRAGMA key = 'passphrase'; + +Alternately, you can specify an exact byte sequence using a blob literal. If you +use this method it is your responsibility to ensure that the data you provide a +64 character hex string, which will be converted directly to 32 bytes (256 bits) of +key data without key derivation. + + PRAGMA key = "x'2DD29CA851E7B56E4697B0E1F08507293D761A05CE4D1B628663F411A8086D99'"; + +To encrypt a database programatically you can use the sqlite3_key function. +The data provided in pKey is converted to an encryption key according to the +same rules as PRAGMA key. + + int sqlite3_key(sqlite3 *db, const void *pKey, int nKey); + +PRAGMA key or sqlite3_key should be called as the first operation when a database is open. + +## Changing a database key + +To change the encryption passphrase for an existing database you may use the rekey pragma +after you've supplied the correct database password; + + PRAGMA key = 'passphrase'; -- start with the existing database passphrase + PRAGMA rekey = 'new-passphrase'; -- rekey will reencrypt with the new passphrase + +The hexrekey pragma may be used to rekey to a specific binary value + + PRAGMA rekey = "x'2DD29CA851E7B56E4697B0E1F08507293D761A05CE4D1B628663F411A8086D99'"; + +This can be accomplished programtically by using sqlite3_rekey; + + sqlite3_rekey(sqlite3 *db, const void *pKey, int nKey) + +## Support + +The primary avenue for support and discussions is the SQLCipher users mailing list: + +http://groups.google.com/group/sqlcipher + +Issues or support questions on using SQLCipher should be entered into the +GitHub Issue tracker: + +https://github.com/sqlcipher/sqlcipher/issues + +Please DO NOT post issues, support questions, or other problems to blog +posts about SQLCipher as we do not monitor them frequently. + +If you are using SQLCipher in your own software please let us know at +support@zetetic.net! + +## License + +Copyright (c) 2008, ZETETIC LLC +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * Neither the name of the ZETETIC LLC nor the + names of its contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY ZETETIC LLC ''AS IS'' AND ANY +EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL ZETETIC LLC BE LIABLE FOR ANY +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +## End SQLCipher + +This directory contains source code to + + SQLite: An Embeddable SQL Database Engine + +To compile the project, first create a directory in which to place +the build products. It is recommended, but not required, that the +build directory be separate from the source directory. Cd into the +build directory and then from the build directory run the configure +script found at the root of the source tree. Then run "make". + +For example: + + tar xzf sqlite.tar.gz ;# Unpack the source tree into "sqlite" + mkdir bld ;# Build will occur in a sibling directory + cd bld ;# Change to the build directory + ../sqlite/configure ;# Run the configure script + make ;# Run the makefile. + make install ;# (Optional) Install the build products + +The configure script uses autoconf 2.61 and libtool. If the configure +script does not work out for you, there is a generic makefile named +"Makefile.linux-gcc" in the top directory of the source tree that you +can copy and edit to suit your needs. Comments on the generic makefile +show what changes are needed. + +The linux binaries on the website are created using the generic makefile, +not the configure script. The windows binaries on the website are created +using MinGW32 configured as a cross-compiler running under Linux. For +details, see the ./publish.sh script at the top-level of the source tree. +The developers do not use teh configure script. + +SQLite does not require TCL to run, but a TCL installation is required +by the makefiles. SQLite contains a lot of generated code and TCL is +used to do much of that code generation. The makefile also requires +AWK. + +Contacts: + + http://www.sqlite.org/ +======= + mkdir bld + cd bld + nmake /f Makefile.msc TOP=..\sqlite + nmake /f Makefile.msc sqlite3.c TOP=..\sqlite + nmake /f Makefile.msc sqlite3.dll TOP=..\sqlite + nmake /f Makefile.msc sqlite3.exe TOP=..\sqlite + nmake /f Makefile.msc test TOP=..\sqlite + +There are several build options that can be set via the NMAKE command +line. For example, to build for WinRT, simply add "FOR_WINRT=1" argument +to the "sqlite3.dll" command line above. When debugging into the SQLite +code, adding the "DEBUG=1" argument to one of the above command lines is +recommended. + +SQLite does not require [Tcl](http://www.tcl.tk/) to run, but a Tcl installation +is required by the makefiles (including those for MSVC). SQLite contains +a lot of generated code and Tcl is used to do much of that code generation. +The makefiles also require AWK. + +## Source Code Tour + +Most of the core source files are in the **src/** subdirectory. But +src/ also contains files used to build the "testfixture" test harness; +those file all begin with "test". And src/ contains the "shell.c" file +which is the main program for the "sqlite3.exe" command-line shell and +the "tclsqlite.c" file which implements the bindings to SQLite from the +Tcl programming language. (Historical note: SQLite began as a Tcl +extension and only later escaped to the wild as an independent library.) + +Test scripts and programs are found in the **test/** subdirectory. +There are other test suites for SQLite (see +[How SQLite Is Tested](http://www.sqlite.org/testing.html)) +but those other test suites are +in separate source repositories. + +The **ext/** subdirectory contains code for extensions. The +Full-text search engine is in **ext/fts3**. The R-Tree engine is in +**ext/rtree**. The **ext/misc** subdirectory contains a number of +smaller, single-file extensions, such as a REGEXP operator. + +The **tool/** subdirectory contains various scripts and programs used +for building generated source code files or for testing or for generating +accessory programs such as "sqlite3_analyzer(.exe)". + +### Generated Source Code Files + +Several of the C-language source files used by SQLite are generated from +other sources rather than being typed in manually by a programmer. This +section will summarize those automatically-generated files. To create all +of the automatically-generated files, simply run "make target_source". +The "target_source" make target will create a subdirectory "tsrc/" and +fill it with all the source files needed to build SQLite, both +manually-edited files and automatically-generated files. + +The SQLite interface is defined by the **sqlite3.h** header file, which is +generated from src/sqlite.h.in, ./manifest.uuid, and ./VERSION. The +[Tcl script](http://www.tcl.tk) at tool/mksqlite3h.tcl does the conversion. +The manifest.uuid file contains the SHA1 hash of the particular check-in +and is used to generate the SQLITE\_SOURCE\_ID macro. The VERSION file +contains the current SQLite version number. The sqlite3.h header is really +just a copy of src/sqlite.h.in with the source-id and version number inserted +at just the right spots. Note that comment text in the sqlite3.h file is +used to generate much of the SQLite API documentation. The Tcl scripts +used to generate that documentation are in a separate source repository. + +The SQL language parser is **parse.c** which is generate from a grammar in +the src/parse.y file. The conversion of "parse.y" into "parse.c" is done +by the [lemon](./doc/lemon.html) LALR(1) parser generator. The source code +for lemon is at tool/lemon.c. Lemon uses a +template for generating its parser. A generic template is in tool/lempar.c, +but SQLite uses a slightly modified template found in src/lempar.c. + +Lemon also generates the **parse.h** header file, at the same time it +generates parse.c. But the parse.h header file is +modified further (to add additional symbols) using the ./addopcodes.awk +AWK script. + +The **opcodes.h** header file contains macros that define the numbers +corresponding to opcodes in the "VDBE" virtual machine. The opcodes.h +file is generated by the scanning the src/vdbe.c source file. The +AWK script at ./mkopcodeh.awk does this scan and generates opcodes.h. +A second AWK script, ./mkopcodec.awk, then scans opcodes.h to generate +the **opcodes.c** source file, which contains a reverse mapping from +opcode-number to opcode-name that is used for EXPLAIN output. + +The **keywordhash.h** header file contains the definition of a hash table +that maps SQL language keywords (ex: "CREATE", "SELECT", "INDEX", etc.) into +the numeric codes used by the parse.c parser. The keywordhash.h file is +generated by a C-language program at tool mkkeywordhash.c. + +### The Amalgamation + +All of the individual C source code and header files (both manually-edited +and automatically-generated) can be combined into a single big source file +**sqlite3.c** called "the amalgamation". The amalgamation is the recommended +way of using SQLite in a larger application. Combining all individual +source code files into a single big source code file allows the C compiler +to perform more cross-procedure analysis and generate better code. SQLite +runs about 5% faster when compiled from the amalgamation versus when compiled +from individual source files. + +The amalgamation is generated from the tool/mksqlite3c.tcl Tcl script. +First, all of the individual source files must be gathered into the tsrc/ +subdirectory (using the equivalent of "make target_source") then the +tool/mksqlite3c.tcl script is run to copy them all together in just the +right order while resolving internal "#include" references. + +The amalgamation source file is more than 100K lines long. Some symbolic +debuggers (most notably MSVC) are unable to deal with files longer than 64K +lines. To work around this, a separate Tcl script, tool/split-sqlite3c.tcl, +can be run on the amalgamation to break it up into a single small C file +called **sqlite3-all.c** that does #include on about five other files +named **sqlite3-1.c**, **sqlite3-2.c**, ..., **sqlite3-5.c**. In this way, +all of the source code is contained within a single translation unit so +that the compiler can do extra cross-procedure optimization, but no +individual source file exceeds 32K lines in length. + +## How It All Fits Together + +SQLite is modular in design. +See the [architectural description](http://www.sqlite.org/arch.html) +for details. Other documents that are useful in +(helping to understand how SQLite works include the +[file format](http://www.sqlite.org/fileformat2.html) description, +the [virtual machine](http://www.sqlite.org/vdbe.html) that runs +prepared statements, the description of +[how transactions work](http://www.sqlite.org/atomiccommit.html), and +the [overview of the query planner](http://www.sqlite.org/optoverview.html). + +Unfortunately, years of effort have gone into optimizating SQLite, both +for small size and high performance. And optimizations tend to result in +complex code. So there is a lot of complexity in the SQLite implementation. + +Key files: + + * **sqlite3.h** - This file defines the public interface to the SQLite + library. Readers will need to be familiar with this interface before + trying to understand how the library works internally. + + * **sqliteInt.h** - this header file defines many of the data objects + used internally by SQLite. + + * **parse.y** - This file describes the LALR(1) grammer that SQLite uses + to parse SQL statements, and the actions that are taken at each stop + in the parsing process. + + * **vdbe.c** - This file implements the virtual machine that runs + prepared statements. There are various helper files whose names + begin with "vdbe". The VDBE has access to the vdbeInt.h header file + which defines internal data objects. The rest of SQLite interacts + with the VDBE through an interface defined by vdbe.h. + + * **where.c** - This file analyzes the WHERE clause and generates + virtual machine code to run queries efficiently. This file is + sometimes called the "query optimizer". It has its own private + header file, whereInt.h, that defines data objects used internally. + + * **btree.c** - This file contains the implementation of the B-Tree + storage engine used by SQLite. + + * **pager.c** - This file contains the "pager" implementation, the + module that implements transactions. + + * **os_unix.c** and **os_win.c** - These two files implement the interface + between SQLite and the underlying operating system using the run-time + pluggable VFS interface. + + +## Contacts + +The main SQLite webpage is [http://www.sqlite.org/](http://www.sqlite.org/) +with geographically distributed backup servers at +[http://www2.sqlite.org/](http://www2.sqlite.org) and +[http://www3.sqlite.org/](http://www3.sqlite.org). diff --git a/mobile/ios/ThirdParty/sqlcipher/VERSION b/mobile/ios/ThirdParty/sqlcipher/VERSION new file mode 100644 index 0000000000..67a9eb7bd3 --- /dev/null +++ b/mobile/ios/ThirdParty/sqlcipher/VERSION @@ -0,0 +1 @@ +3.8.8.3 \ No newline at end of file diff --git a/mobile/ios/ThirdParty/sqlcipher/sqlcipher.xcodeproj/project.pbxproj b/mobile/ios/ThirdParty/sqlcipher/sqlcipher.xcodeproj/project.pbxproj new file mode 100644 index 0000000000..d1239c50c4 --- /dev/null +++ b/mobile/ios/ThirdParty/sqlcipher/sqlcipher.xcodeproj/project.pbxproj @@ -0,0 +1,307 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + 28B46E6317CD07A700672510 /* Security.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 28B46E6217CD07A600672510 /* Security.framework */; }; + 9069D0A30FCE1A4D0042E34C /* sqlite3.c in Sources */ = {isa = PBXBuildFile; fileRef = 9069D0A20FCE1A4D0042E34C /* sqlite3.c */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 28B46E6217CD07A600672510 /* Security.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Security.framework; path = System/Library/Frameworks/Security.framework; sourceTree = SDKROOT; }; + 9069D0A20FCE1A4D0042E34C /* sqlite3.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = sqlite3.c; sourceTree = ""; }; + D2AAC046055464E500DB518D /* libsqlcipher.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libsqlcipher.a; sourceTree = BUILT_PRODUCTS_DIR; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + D289987405E68DCB004EDB86 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 28B46E6317CD07A700672510 /* Security.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 08FB7794FE84155DC02AAC07 /* sqlcipher */ = { + isa = PBXGroup; + children = ( + 28B46E6217CD07A600672510 /* Security.framework */, + 08FB7795FE84155DC02AAC07 /* Source */, + C6A0FF2B0290797F04C91782 /* Documentation */, + 1AB674ADFE9D54B511CA2CBB /* Products */, + ); + name = sqlcipher; + sourceTree = ""; + }; + 08FB7795FE84155DC02AAC07 /* Source */ = { + isa = PBXGroup; + children = ( + 9069D0A20FCE1A4D0042E34C /* sqlite3.c */, + ); + name = Source; + sourceTree = ""; + }; + 1AB674ADFE9D54B511CA2CBB /* Products */ = { + isa = PBXGroup; + children = ( + D2AAC046055464E500DB518D /* libsqlcipher.a */, + ); + name = Products; + sourceTree = ""; + }; + C6A0FF2B0290797F04C91782 /* Documentation */ = { + isa = PBXGroup; + children = ( + ); + name = Documentation; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXHeadersBuildPhase section */ + D2AAC043055464E500DB518D /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXHeadersBuildPhase section */ + +/* Begin PBXNativeTarget section */ + D2AAC045055464E500DB518D /* sqlcipher */ = { + isa = PBXNativeTarget; + buildConfigurationList = 1DEB91EB08733DB70010E9CD /* Build configuration list for PBXNativeTarget "sqlcipher" */; + buildPhases = ( + D2AAC043055464E500DB518D /* Headers */, + D2AAC044055464E500DB518D /* Sources */, + D289987405E68DCB004EDB86 /* Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = sqlcipher; + productName = sqlcipher; + productReference = D2AAC046055464E500DB518D /* libsqlcipher.a */; + productType = "com.apple.product-type.library.static"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 08FB7793FE84155DC02AAC07 /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftMigration = 0700; + LastUpgradeCheck = 0800; + }; + buildConfigurationList = 1DEB91EF08733DB70010E9CD /* Build configuration list for PBXProject "sqlcipher" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 1; + knownRegions = ( + English, + Japanese, + French, + German, + ); + mainGroup = 08FB7794FE84155DC02AAC07 /* sqlcipher */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + D2AAC045055464E500DB518D /* sqlcipher */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXSourcesBuildPhase section */ + D2AAC044055464E500DB518D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 9069D0A30FCE1A4D0042E34C /* sqlite3.c in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + 1DEB91EC08733DB70010E9CD /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + "ARCHS[sdk=iphoneos*]" = "$(ARCHS_STANDARD)"; + "ARCHS[sdk=iphonesimulator*]" = "$(ARCHS_STANDARD)"; + "ARCHS[sdk=macosx*]" = "$(ARCHS_STANDARD)"; + COMBINE_HIDPI_IMAGES = YES; + COPY_PHASE_STRIP = NO; + GCC_DYNAMIC_NO_PIC = NO; + GCC_MODEL_TUNING = G5; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_WARN_64_TO_32_BIT_CONVERSION = NO; + GCC_WARN_UNUSED_VARIABLE = NO; + HEADER_SEARCH_PATHS = "$(PROJECT_DIR)"; + INSTALL_PATH = /usr/local/lib; + OTHER_CFLAGS = ( + "-DSQLITE_HAS_CODEC", + "-DSQLITE_TEMP_STORE=2", + "-DSQLITE_THREADSAFE=2", + "-DSQLCIPHER_CRYPTO_CC", + ); + "OTHER_CFLAGS[arch=armv6]" = ( + "-mno-thumb", + "-DSQLITE_HAS_CODEC", + "-DSQLITE_TEMP_STORE=2", + "-DSQLITE_THREADSAFE=2", + "-DSQLCIPHER_CRYPTO_CC", + ); + OTHER_LDFLAGS = ""; + PRODUCT_NAME = sqlcipher; + SUPPORTED_PLATFORMS = "iphonesimulator iphoneos"; + VALID_ARCHS = "arm64 armv7 armv7s"; + }; + name = Debug; + }; + 1DEB91ED08733DB70010E9CD /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + "ARCHS[sdk=iphoneos*]" = "$(ARCHS_STANDARD)"; + "ARCHS[sdk=iphonesimulator*]" = "$(ARCHS_STANDARD)"; + "ARCHS[sdk=macosx*]" = "$(ARCHS_STANDARD)"; + COMBINE_HIDPI_IMAGES = YES; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + GCC_MODEL_TUNING = G5; + GCC_WARN_64_TO_32_BIT_CONVERSION = NO; + GCC_WARN_UNUSED_VARIABLE = NO; + HEADER_SEARCH_PATHS = "$(PROJECT_DIR)"; + INSTALL_PATH = /usr/local/lib; + "IPHONEOS_DEPLOYMENT_TARGET[sdk=iphoneos*]" = 5.1.1; + OTHER_CFLAGS = ( + "-DSQLITE_HAS_CODEC", + "-DNDEBUG", + "-DSQLITE_OS_UNIX=1", + "-DSQLITE_TEMP_STORE=2", + "-DSQLITE_THREADSAFE=2", + "-DSQLCIPHER_CRYPTO_CC", + ); + "OTHER_CFLAGS[arch=armv6]" = ( + "-mno-thumb", + "-DSQLITE_HAS_CODEC", + "-DNDEBUG", + "-DSQLITE_OS_UNIX=1", + "-DSQLITE_TEMP_STORE=2", + "-DSQLITE_THREADSAFE=2", + "-DSQLCIPHER_CRYPTO_CC", + ); + OTHER_LDFLAGS = ""; + PRODUCT_NAME = sqlcipher; + SUPPORTED_PLATFORMS = "iphonesimulator iphoneos"; + VALID_ARCHS = "arm64 armv7 armv7s"; + }; + name = Release; + }; + 1DEB91F008733DB70010E9CD /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + "ARCHS[sdk=iphoneos*]" = "$(ARCHS_STANDARD)"; + "ARCHS[sdk=iphonesimulator*]" = "$(ARCHS_STANDARD)"; + "ARCHS[sdk=macosx*]" = "$(ARCHS_STANDARD)"; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = c99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + "IPHONEOS_DEPLOYMENT_TARGET[arch=arm64]" = 7.0; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = "iphonesimulator iphoneos"; + VALID_ARCHS = "arm64 armv7 armv7s"; + }; + name = Debug; + }; + 1DEB91F108733DB70010E9CD /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + "ARCHS[sdk=iphoneos*]" = "$(ARCHS_STANDARD)"; + "ARCHS[sdk=iphonesimulator*]" = "$(ARCHS_STANDARD)"; + "ARCHS[sdk=macosx*]" = "$(ARCHS_STANDARD)"; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = c99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + "IPHONEOS_DEPLOYMENT_TARGET[arch=arm64]" = 7.0; + "IPHONEOS_DEPLOYMENT_TARGET[sdk=iphoneos*]" = 5.1.1; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = "iphonesimulator iphoneos"; + VALID_ARCHS = "arm64 armv7 armv7s"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 1DEB91EB08733DB70010E9CD /* Build configuration list for PBXNativeTarget "sqlcipher" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 1DEB91EC08733DB70010E9CD /* Debug */, + 1DEB91ED08733DB70010E9CD /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 1DEB91EF08733DB70010E9CD /* Build configuration list for PBXProject "sqlcipher" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 1DEB91F008733DB70010E9CD /* Debug */, + 1DEB91F108733DB70010E9CD /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 08FB7793FE84155DC02AAC07 /* Project object */; +} diff --git a/mobile/ios/UITests/AuthenticationTests.swift b/mobile/ios/UITests/AuthenticationTests.swift new file mode 100644 index 0000000000..99f5b17c68 --- /dev/null +++ b/mobile/ios/UITests/AuthenticationTests.swift @@ -0,0 +1,114 @@ +/* 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 EarlGrey + +class AuthenticationTests: KIFTestCase { + fileprivate var webRoot: String! + + override func setUp() { + super.setUp() + webRoot = SimplePageServer.start() + BrowserUtils.dismissFirstRunUI() + } + + override func tearDown() { + BrowserUtils.resetToAboutHome(tester()) + BrowserUtils.clearPrivateData(tester: tester()) + super.tearDown() + } + + /** + * Tests HTTP authentication credentials and auto-fill. + */ + func testAuthentication() { + loadAuthPage() + + // Make sure that 3 invalid credentials result in authentication failure. + enterCredentials(usernameValue: "Username", passwordValue: "Password", username: "foo", password: "bar") + enterCredentials(usernameValue: "foo", passwordValue: "•••", username: "foo2", password: "bar2") + enterCredentials(usernameValue: "foo2", passwordValue: "••••", username: "foo3", password: "bar3") + + // Use KIFTest framework for checking elements within webView + tester().waitForWebViewElementWithAccessibilityLabel("auth fail") + + // Enter valid credentials and ensure the page loads. + EarlGrey.select(elementWithMatcher: grey_accessibilityLabel("Reload")).perform(grey_tap()) + enterCredentials(usernameValue: "Username", passwordValue: "Password", username: "user", password: "pass") + tester().waitForWebViewElementWithAccessibilityLabel("logged in") + + // Save the credentials. + tester().tapView(withAccessibilityIdentifier: "SaveLoginPrompt.saveLoginButton") + + logOut() + loadAuthPage() + + // Make sure the credentials were saved and auto-filled. + EarlGrey.select(elementWithMatcher: grey_accessibilityLabel("Log in")) + .inRoot(grey_kindOfClass(NSClassFromString("_UIAlertControllerActionView")!)) + .perform(grey_tap()) + tester().waitForWebViewElementWithAccessibilityLabel("logged in") + + // Add a private tab. + if BrowserUtils.iPad() { + EarlGrey.select(elementWithMatcher:grey_accessibilityID("TopTabsViewController.tabsButton")) + .perform(grey_tap()) + } else { + EarlGrey.select(elementWithMatcher:grey_accessibilityID("TabToolbar.tabsButton")) + .perform(grey_tap()) + } + EarlGrey.select(elementWithMatcher:grey_accessibilityID("TabTrayController.maskButton")) + .perform(grey_tap()) + EarlGrey.select(elementWithMatcher:grey_accessibilityID("TabTrayController.addTabButton")) + .perform(grey_tap()) + loadAuthPage() + + // Make sure the auth prompt is shown. + // Note that in the future, we might decide to auto-fill authentication credentials in private browsing mode, + // but that's not currently supported. We assume the username and password fields are empty. + enterCredentials(usernameValue: "Username", passwordValue: "Password", username: "user", password: "pass") + tester().waitForWebViewElementWithAccessibilityLabel("logged in") + + } + + fileprivate func loadAuthPage() { + EarlGrey.select(elementWithMatcher: grey_accessibilityID("url")).perform(grey_tap()) + EarlGrey.select(elementWithMatcher: grey_accessibilityID("address")).perform(grey_replaceText("\(webRoot!)/auth.html")) + EarlGrey.select(elementWithMatcher: grey_accessibilityID("address")).perform(grey_typeText("\n")) + } + + fileprivate func logOut() { + EarlGrey.select(elementWithMatcher: grey_accessibilityID("url")).perform(grey_tap()) + EarlGrey.select(elementWithMatcher: grey_accessibilityID("address")).perform(grey_replaceText("\(webRoot!)/auth.html?logout=1")) + EarlGrey.select(elementWithMatcher: grey_accessibilityID("address")).perform(grey_typeText("\n")) + // Wait until the dialog shows up + let dialogAppeared = GREYCondition(name: "Wait the login dialog to appear", block: { _ in + var errorOrNil: NSError? + let matcher = grey_allOf([grey_accessibilityLabel("Cancel"), + grey_sufficientlyVisible()]) + EarlGrey.select(elementWithMatcher: matcher) + .inRoot(grey_kindOfClass(NSClassFromString("_UIAlertControllerActionView")!)) + .assert(grey_notNil(), error: &errorOrNil) + let success = errorOrNil == nil + return success + }).wait(withTimeout: 20) + + GREYAssertTrue(dialogAppeared, reason: "Failed to display login dialog") + + EarlGrey.select(elementWithMatcher: grey_accessibilityLabel("Cancel")) + .inRoot(grey_kindOfClass(NSClassFromString("_UIAlertControllerActionView")!)) + .perform(grey_tap()) + } + + fileprivate func enterCredentials(usernameValue: String, passwordValue: String, username: String, password: String) { + + // In case of IPad, Earl Grey complains that UI Loop has not been finished for password field, reverting. + let usernameField = tester().waitForViewWithAccessibilityValue(usernameValue) as! UITextField + let passwordField = tester().waitForViewWithAccessibilityValue(passwordValue) as! UITextField + usernameField.text = username + passwordField.text = password + tester().tapView(withAccessibilityLabel: "Log in") + } +} diff --git a/mobile/ios/UITests/BookmarksPanelTests.swift b/mobile/ios/UITests/BookmarksPanelTests.swift new file mode 100644 index 0000000000..a0e72093fb --- /dev/null +++ b/mobile/ios/UITests/BookmarksPanelTests.swift @@ -0,0 +1,218 @@ +/* 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/. */ + +@testable import Client +@testable import Storage +import Shared +import Foundation +import WebKit +import EarlGrey + +class BookmarksPanelTests: KIFTestCase { + + override func setUp() { + super.setUp() + BrowserUtils.dismissFirstRunUI() + } + + override func tearDown() { + super.tearDown() + BrowserUtils.resetToAboutHome(tester()) + } + + private func getAppBookmarkStorage() -> BookmarkBufferStorage? { + let application = UIApplication.shared + + guard let delegate = application.delegate as? TestAppDelegate else { + XCTFail("Couldn't get app delegate.") + return nil + } + + let profile = delegate.getProfile(application) + + guard let bookmarks = profile.bookmarks as? BookmarkBufferStorage else { + XCTFail("Couldn't get buffer storage.") + return nil + } + + return bookmarks + } + + private func createSomeBufferBookmarks() { + // Set up the buffer. + let bufferDate = Date.now() + let changedBufferRecords = [ + BookmarkMirrorItem.folder(BookmarkRoots.ToolbarFolderGUID, dateAdded: Date.now(), modified: bufferDate, hasDupe: false, parentID: BookmarkRoots.RootGUID, parentName: nil, title: "Bookmarks Toolbar", description: nil, children: ["aaa", "bbb"]), + BookmarkMirrorItem.bookmark("aaa", dateAdded: Date.now(), modified: Date.now(), hasDupe: false, parentID: BookmarkRoots.ToolbarFolderGUID, parentName: nil, title: "AAA", description: nil, URI: "http://getfirefox.com", tags: "[]", keyword: nil), + BookmarkMirrorItem.livemark("bbb", dateAdded: Date.now(), modified: Date.now(), hasDupe: false, parentID: BookmarkRoots.ToolbarFolderGUID, parentName: nil, title: "Some Livemark", description: nil, feedURI: "https://www.google.ca", siteURI: "https://www.google.ca") ] + + if let bookmarks = getAppBookmarkStorage() { + XCTAssert(bookmarks.applyRecords(changedBufferRecords).value.isSuccess) + } + } + + func testBookmarkPanelBufferOnly() { + // Insert some data into the buffer. There will be nothing in the mirror, but we can still + // show Desktop Bookmarks. + + createSomeBufferBookmarks() + + EarlGrey.select(elementWithMatcher: grey_accessibilityID("url")).perform(grey_tap()) + EarlGrey.select(elementWithMatcher: grey_accessibilityLabel("Bookmarks")).perform(grey_tap()) + EarlGrey.select(elementWithMatcher: grey_accessibilityLabel("Desktop Bookmarks")).perform(grey_tap()) + EarlGrey.select(elementWithMatcher: grey_accessibilityLabel("Bookmarks Toolbar")).perform(grey_tap()) + EarlGrey.select(elementWithMatcher: grey_accessibilityLabel("AAA")) + .assert(grey_sufficientlyVisible()) + EarlGrey.select(elementWithMatcher: grey_accessibilityLabel("Some Livemark")) + .assert(grey_sufficientlyVisible()) + + // When we tap the livemark, we load the siteURI. + EarlGrey.select(elementWithMatcher: grey_accessibilityLabel("Some Livemark")).perform(grey_tap()) + + // … so we show the truncated URL. + // Strangely, earlgrey cannot find the label in buddybuild, but passes locally. + // Using KIF for this check for now. + EarlGrey.select(elementWithMatcher: grey_accessibilityValue("www.google.ca/")).assert(grey_sufficientlyVisible()) + } + + private func navigateBackInTableView() { + EarlGrey.select(elementWithMatcher: grey_accessibilityLabel("Bookmarks")).inRoot(grey_kindOfClass(UITableView.self)).perform(grey_tap()) + } + + private func navigateFolder(withTitle title: String) { + EarlGrey.select(elementWithMatcher: grey_accessibilityLabel(title)).perform(grey_tap()) + } + + private func makeBookmark(guid: GUID, parentID: GUID, title: String) -> BookmarkMirrorItem { + return BookmarkMirrorItem.bookmark(guid, dateAdded: Date.now(), modified: Date.now(), hasDupe: false, parentID: parentID, parentName: nil, title: title, description: nil, URI: "http://unused.com", tags: "[]", keyword: nil) + } + + private func makeFolder(guid: GUID, parentID: GUID, title: String, childrenGuids: [GUID]) -> BookmarkMirrorItem { + return BookmarkMirrorItem.folder(guid, dateAdded: Date.now(), modified: Date.now(), hasDupe: false, parentID: parentID, parentName: nil, title: title, description: nil, children: childrenGuids) + } + + private func assertRowExists(withTitle title: String) { + EarlGrey.select(elementWithMatcher: grey_accessibilityLabel(title)).inRoot(grey_kindOfClass(UITableView.self)).assert(grey_notNil()) + } + + func testMobileBookmarks() { + verifyRootHasLocalAndBufferBookmarks() + + // Rather than an elaborate setup() step, just continue from the previous test + // as the view state is now setup for testing deletion. + verifyMobileBookmarkDelete() + } + + func verifyRootHasLocalAndBufferBookmarks() { + // Add buffer data, then later in the test verify that the buffer mobile folder is not shown in there anymore. + createSomeBufferBookmarks() + + guard let bookmarks = getAppBookmarkStorage() else { + return + } + + // TEST: Create remote mobile bookmark, and verify the bookmark appears in the root and the remote mobile folder is not shown + var applyResult = bookmarks.applyRecords([ + makeBookmark(guid: "bm-guid0", parentID: BookmarkRoots.MobileFolderGUID, title: "xyz"), + makeFolder(guid: BookmarkRoots.MobileFolderGUID, parentID: BookmarkRoots.RootGUID, title: "", childrenGuids: ["bm-guid0"]) + ]) + XCTAssert(applyResult.value.isSuccess) + EarlGrey.select(elementWithMatcher: grey_accessibilityID("url")).perform(grey_tap()) + EarlGrey.select(elementWithMatcher: grey_accessibilityLabel("Bookmarks")).perform(grey_tap()) + + // is this in the root? + assertRowExists(withTitle: "xyz") + + navigateFolder(withTitle: "Desktop Bookmarks") + // this should be missing, they are shown in the root + EarlGrey.select(elementWithMatcher: grey_accessibilityLabel("Mobile Bookmarks")).assert(grey_nil()) + + // TEST: Add a local bookmark, and then navigate back to the root view (the navigation will refresh the table). + let isAdded = (bookmarks as! MergedSQLiteBookmarks).local.addToMobileBookmarks(URL(string: "http://another-unused")!, title: "123", favicon: nil) + XCTAssert(isAdded.value.isSuccess) + navigateBackInTableView() + // is this in the root? + assertRowExists(withTitle: "123") + + // TEST: Add sub-folder to MobileFolderGUID, ensure it is navigable + applyResult = bookmarks.applyRecords([ + makeBookmark(guid: "bm-guid1", parentID: "folder-guid0", title: "item-in-remote-subfolder"), + makeFolder(guid: "folder-guid0", parentID: BookmarkRoots.MobileFolderGUID, title: "remote-subfolder", childrenGuids: ["bm-guid1"]), + makeFolder(guid: BookmarkRoots.MobileFolderGUID, parentID: BookmarkRoots.RootGUID, title: "", childrenGuids: ["bm-guid0", "folder-guid0"]) + ]) + XCTAssert(applyResult.value.isSuccess) + + // refresh view + navigateFolder(withTitle: "Desktop Bookmarks") + navigateBackInTableView() + + // subfolder should now be in the root + assertRowExists(withTitle: "remote-subfolder") + + // navigate remote mobile subfolder, ensure bookmark is shown in the subfolder + navigateFolder(withTitle: "remote-subfolder") + assertRowExists(withTitle: "item-in-remote-subfolder") + } + + // This is a continuation of the above test + func verifyMobileBookmarkDelete() { + // Assert precondition that this test continues from previous state + assertRowExists(withTitle: "item-in-remote-subfolder") + + let deleteAction = GREYMatchers.matcher(forText: "Remove Bookmark") + + // Test deletion not available + + var row = EarlGrey.select(elementWithMatcher: grey_accessibilityLabel("item-in-remote-subfolder")) + row.perform(grey_longPress()) + // verify the context menu does not have deletion option + EarlGrey.select(elementWithMatcher: deleteAction).assert(grey_nil()) + // close the context menu + EarlGrey.select(elementWithMatcher: GREYMatchers.matcher(forText: "Pin to Top Sites")).perform(grey_tap()) + + navigateBackInTableView() + + // Test successful deletion + + assertRowExists(withTitle: "xyz") + + let tv = tester().waitForView(withAccessibilityIdentifier: "Bookmarks List") as! UITableView + let rowCount = tv.numberOfRows(inSection: 0) + + row = EarlGrey.select(elementWithMatcher: grey_accessibilityLabel("xyz")) + row.perform(grey_longPress()) + EarlGrey.select(elementWithMatcher: deleteAction).perform(grey_tap()) + + XCTAssert(tv.numberOfRows(inSection: 0) == rowCount - 1) + + EarlGrey.select(elementWithMatcher: grey_accessibilityID("goBack")).perform(grey_tap()) + } + + func testRefreshBookmarks() { + createSomeBufferBookmarks() + guard let bookmarks = getAppBookmarkStorage() else { + return + } + let applyResult = bookmarks.applyRecords([ + makeBookmark(guid: "bm-guid0", parentID: BookmarkRoots.MobileFolderGUID, title: "xyz"), + makeFolder(guid: BookmarkRoots.MobileFolderGUID, parentID: BookmarkRoots.RootGUID, title: "", childrenGuids: ["bm-guid0"]) + ]) + XCTAssert(applyResult.value.isSuccess) + EarlGrey.select(elementWithMatcher: grey_accessibilityID("url")).perform(grey_tap()) + EarlGrey.select(elementWithMatcher: grey_accessibilityLabel("Bookmarks")).perform(grey_tap()) + + // Add a bookmark + let isAdded = (bookmarks as! MergedSQLiteBookmarks).local.addToMobileBookmarks(URL(string: "http://new-bookmark")!, title: "NewBookmark", favicon: nil) + XCTAssert(isAdded.value.isSuccess) + + // Pull to refresh + assertRowExists(withTitle: "xyz") + EarlGrey.select(elementWithMatcher: grey_accessibilityLabel("xyz")).inRoot(grey_kindOfClass(NSClassFromString("UITableView")!)).perform(grey_swipeSlowInDirectionWithStartPoint(.down, 0.7, 0.7)) + + // Verify new bookmark exists + assertRowExists(withTitle: "NewBookmark") + + EarlGrey.select(elementWithMatcher: grey_accessibilityID("goBack")).perform(grey_tap()) + } +} diff --git a/mobile/ios/UITests/BrowserTests.swift b/mobile/ios/UITests/BrowserTests.swift new file mode 100644 index 0000000000..958127993a --- /dev/null +++ b/mobile/ios/UITests/BrowserTests.swift @@ -0,0 +1,56 @@ +/* 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 Storage +import EarlGrey +@testable import Client + +class BrowserTests: KIFTestCase { + + private var webRoot: String! + + override func setUp() { + super.setUp() + webRoot = SimplePageServer.start() + BrowserUtils.dismissFirstRunUI() + } + + override func tearDown() { + BrowserUtils.resetToAboutHome(tester()) + BrowserUtils.clearPrivateData(tester: tester()) + super.tearDown() + } + + // Disabled with https://bugzilla.mozilla.org/show_bug.cgi?id=1409851 + func testDisplaySharesheetWhileJSPromptOccurs() { + let url = "\(webRoot!)/JSPrompt.html" + EarlGrey.select(elementWithMatcher: grey_accessibilityID("url")).perform(grey_tap()) + EarlGrey.select(elementWithMatcher: grey_accessibilityID("address")).perform(grey_replaceText(url)) + EarlGrey.select(elementWithMatcher: grey_accessibilityID("address")).perform(grey_typeText("\n")) + tester().waitForWebViewElementWithAccessibilityLabel("JS Prompt") + //EarlGrey.select(elementWithMatcher: grey_kindOfClass(NSClassFromString("_UIAlertControllerInterfaceActionGroupView")!)) + // .assert(grey_sufficientlyVisible()) + + // Show share sheet and wait for the JS prompt to fire + EarlGrey.select(elementWithMatcher: grey_accessibilityLabel("Share")).perform(grey_tap()) + + if BrowserUtils.iPad() { + // iPad does not have cancel btn to close dialog + EarlGrey.select(elementWithMatcher: grey_accessibilityLabel("Share")).perform(grey_tap()) + } else { + let matcher = grey_allOf([grey_accessibilityLabel("Cancel"), + grey_accessibilityTrait(UIAccessibilityTraitButton), + grey_sufficientlyVisible()]) + EarlGrey.select(elementWithMatcher: matcher).perform(grey_tap()) + } + + // Check to see if the JS Prompt is dequeued and showing + tester().waitForView(withAccessibilityLabel: "OK") + EarlGrey.select(elementWithMatcher: grey_accessibilityLabel("OK")) + .inRoot(grey_kindOfClass(NSClassFromString("_UIAlertControllerActionView")!)) + .assert(grey_enabled()) + .perform((grey_tap())) + } +} diff --git a/mobile/ios/UITests/ClearPrivateDataTests.swift b/mobile/ios/UITests/ClearPrivateDataTests.swift new file mode 100644 index 0000000000..258508e5d3 --- /dev/null +++ b/mobile/ios/UITests/ClearPrivateDataTests.swift @@ -0,0 +1,229 @@ +/* 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 WebKit +import UIKit +import EarlGrey +import GCDWebServers + +class ClearPrivateDataTests: KIFTestCase, UITextFieldDelegate { + + fileprivate var webRoot: String! + + override func setUp() { + super.setUp() + webRoot = SimplePageServer.start() + BrowserUtils.dismissFirstRunUI() + } + + override func tearDown() { + BrowserUtils.resetToAboutHome(tester()) + } + + func visitSites(noOfSites: Int) -> [(title: String, domain: String, dispDomain: String, url: String)] { + var urls: [(title: String, domain: String, dispDomain: String, url: String)] = [] + for pageNo in 1...noOfSites { + let url = "\(webRoot!)/numberedPage.html?page=\(pageNo)" + EarlGrey.select(elementWithMatcher: grey_accessibilityID("url")).perform(grey_tap()) + EarlGrey.select(elementWithMatcher: grey_accessibilityID("address")).perform(grey_replaceText(url)) + EarlGrey.select(elementWithMatcher: grey_accessibilityID("address")).perform(grey_typeText("\n")) + + tester().waitForWebViewElementWithAccessibilityLabel("Page \(pageNo)") + let dom = URL(string: url)!.normalizedHost! + let index = dom.index(dom.startIndex, offsetBy: 7) + let dispDom = dom.substring(to: index) // On IPhone, it only displays first 8 chars + let tuple: (title: String, domain: String, dispDomain: String, url: String) + = ("Page \(pageNo)", dom, dispDom, url) + urls.append(tuple) + } + BrowserUtils.resetToAboutHome(tester()) + return urls + } + + func anyDomainsExistOnTopSites(_ domains: Set, fulldomains: Set) { + if checkDomains(domains: domains) == true { + return + } else { + if checkDomains(domains: fulldomains) == true { + return + } + } + XCTFail("Couldn't find any domains in top sites.") + } + + private func checkDomains(domains: Set) -> Bool { + var errorOrNil: NSError? + + for domain in domains { + let withoutDot = domain.replacingOccurrences(of: ".", with: " ") + let matcher = grey_allOf([grey_accessibilityLabel(withoutDot), + grey_kindOfClass(NSClassFromString("Client.TopSiteItemCell")!), + grey_sufficientlyVisible()]) + EarlGrey.select(elementWithMatcher: matcher).assert(grey_notNil(), error: &errorOrNil) + + if errorOrNil == nil { + return true + } + } + return false + } + + func testRemembersToggles() { + BrowserUtils.clearPrivateData([BrowserUtils.Clearable.History], swipe:false, tester: tester()) + BrowserUtils.openClearPrivateDataDialog(false, tester: tester()) + + // Ensure the toggles match our settings. + [ + (BrowserUtils.Clearable.Cache, "0"), + (BrowserUtils.Clearable.Cookies, "0"), + (BrowserUtils.Clearable.OfflineData, "0"), + (BrowserUtils.Clearable.History, "1") + ].forEach { clearable, switchValue in + XCTAssertNotNil(tester() + .waitForView(withAccessibilityLabel: clearable.rawValue, value: switchValue, traits: UIAccessibilityTraitNone)) + } + + BrowserUtils.closeClearPrivateDataDialog(tester()) + } + + func testClearsHistoryPanel() { + let urls = visitSites(noOfSites: 2) + + let url1 = urls[0].url + let url2 = urls[1].url + tester().waitForView(withAccessibilityIdentifier: "HomePanels.History") + tester().tapView(withAccessibilityIdentifier: "HomePanels.History") + tester().waitForView(withAccessibilityLabel: url1) + tester().waitForView(withAccessibilityLabel: url2) + + BrowserUtils.clearPrivateData([BrowserUtils.Clearable.History], swipe: false, tester: tester()) + tester().tapView(withAccessibilityLabel: "Bookmarks") + tester().tapView(withAccessibilityLabel: "History") + tester().waitForAbsenceOfView(withAccessibilityLabel: url1) + tester().waitForAbsenceOfView(withAccessibilityLabel: url2) + } + + func testDisabledHistoryDoesNotClearHistoryPanel() { + let urls = visitSites(noOfSites: 2) + var errorOrNil: NSError? + + let url1 = urls[0].url + let url2 = urls[1].url + EarlGrey.select(elementWithMatcher: grey_accessibilityLabel("History")).perform(grey_tap()) + tester().waitForView(withAccessibilityLabel: url1) + tester().waitForView(withAccessibilityLabel: url2) + BrowserUtils.clearPrivateData(BrowserUtils.AllClearables.subtracting([BrowserUtils.Clearable.History]), swipe: false, tester: tester()) + + EarlGrey.select(elementWithMatcher: grey_accessibilityLabel(url1)) + .assert(grey_notNil(), error: &errorOrNil) + EarlGrey.select(elementWithMatcher: grey_accessibilityLabel(url2)) + .assert(grey_notNil(), error: &errorOrNil) + } + + func testClearsCookies() { + let url = "\(webRoot!)/numberedPage.html?page=1" + + EarlGrey.select(elementWithMatcher: grey_accessibilityID("url")).perform(grey_tap()) + + EarlGrey.select(elementWithMatcher: grey_accessibilityID("address")).perform(grey_replaceText(url)) + EarlGrey.select(elementWithMatcher: grey_accessibilityID("address")).perform(grey_typeText("\n")) + tester().waitForWebViewElementWithAccessibilityLabel("Page 1") + + let webView = tester().waitForView(withAccessibilityLabel: "Web content") as! WKWebView + + // Set and verify a dummy cookie value. + setCookies(webView, cookie: "foo=bar") + var cookies = getCookies(webView) + XCTAssertEqual(cookies.cookie, "foo=bar") + XCTAssertEqual(cookies.localStorage, "foo=bar") + XCTAssertEqual(cookies.sessionStorage, "foo=bar") + + // Verify that cookies are not cleared when Cookies is deselected. + BrowserUtils.clearPrivateData(BrowserUtils.AllClearables.subtracting([BrowserUtils.Clearable.Cookies]), swipe: false, tester: tester()) + cookies = getCookies(webView) + XCTAssertEqual(cookies.cookie, "foo=bar") + XCTAssertEqual(cookies.localStorage, "foo=bar") + XCTAssertEqual(cookies.sessionStorage, "foo=bar") + + // Verify that cookies are cleared when Cookies is selected. + BrowserUtils.clearPrivateData([BrowserUtils.Clearable.Cookies], swipe: false, tester: tester()) + cookies = getCookies(webView) + XCTAssertEqual(cookies.cookie, "") + XCTAssertEqual(cookies.localStorage, "null") + XCTAssertEqual(cookies.sessionStorage, "null") + } + + func testClearsCache() { + let cachedServer = CachedPageServer() + let cacheRoot = cachedServer.start() + let url = "\(cacheRoot)/cachedPage.html" + EarlGrey.select(elementWithMatcher: grey_accessibilityID("url")).perform(grey_tap()) + EarlGrey.select(elementWithMatcher: grey_accessibilityID("address")).perform(grey_replaceText(url)) + EarlGrey.select(elementWithMatcher: grey_accessibilityID("address")).perform(grey_typeText("\n")) + tester().waitForWebViewElementWithAccessibilityLabel("Cache test") + + let webView = tester().waitForView(withAccessibilityLabel: "Web content") as! WKWebView + let requests = cachedServer.requests + + // Verify that clearing non-cache items will keep the page in the cache. + BrowserUtils.clearPrivateData(BrowserUtils.AllClearables.subtracting([BrowserUtils.Clearable.Cache]), swipe: false, tester: tester()) + webView.reload() + XCTAssertEqual(cachedServer.requests, requests) + + // Verify that clearing the cache will fire a new request. + BrowserUtils.clearPrivateData([BrowserUtils.Clearable.Cache], swipe: false, tester: tester()) + webView.reload() + XCTAssertEqual(cachedServer.requests, requests + 1) + } + + fileprivate func setCookies(_ webView: WKWebView, cookie: String) { + let expectation = self.expectation(description: "Set cookie") + webView.evaluateJavaScript("document.cookie = \"\(cookie)\"; localStorage.cookie = \"\(cookie)\"; sessionStorage.cookie = \"\(cookie)\";") { result, _ in + expectation.fulfill() + } + waitForExpectations(timeout: 10, handler: nil) + } + + fileprivate func getCookies(_ webView: WKWebView) -> (cookie: String, localStorage: String?, sessionStorage: String?) { + var cookie: (String, String?, String?)! + var value: String! + let expectation = self.expectation(description: "Got cookie") + + webView.evaluateJavaScript("JSON.stringify([document.cookie, localStorage.cookie, sessionStorage.cookie])") { result, _ in + value = result as! String + expectation.fulfill() + } + + waitForExpectations(timeout: 10, handler: nil) + value = value.replacingOccurrences(of: "[", with: "") + value = value.replacingOccurrences(of: "]", with: "") + value = value.replacingOccurrences(of: "\"", with: "") + let items = value.components(separatedBy: ",") + cookie = (items[0], items[1], items[2]) + return cookie + } +} + +/// Server that keeps track of requests. +private class CachedPageServer { + var requests = 0 + + func start() -> String { + let webServer = GCDWebServer() + webServer?.addHandler(forMethod: "GET", path: "/cachedPage.html", request: GCDWebServerRequest.self) { (request) -> GCDWebServerResponse! in + self.requests += 1 + return GCDWebServerDataResponse(html: "Cached pageCache test") + } + + webServer?.start(withPort: 0, bonjourName: nil) + + // We use 127.0.0.1 explicitly here, rather than localhost, in order to avoid our + // history exclusion code (Bug 1188626). + let port = (webServer?.port)! + let webRoot = "http://127.0.0.1:\(port)" + return webRoot + } +} diff --git a/mobile/ios/UITests/DomainAutocompleteTests.swift b/mobile/ios/UITests/DomainAutocompleteTests.swift new file mode 100644 index 0000000000..4bc78a87f8 --- /dev/null +++ b/mobile/ios/UITests/DomainAutocompleteTests.swift @@ -0,0 +1,40 @@ +/* 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 EarlGrey + +class DomainAutocompleteTests: KIFTestCase { + override func setUp() { + + super.setUp() + BrowserUtils.dismissFirstRunUI(tester()) + } + + func testAutocomplete() { + BrowserUtils.addHistoryEntry("Foo bar baz", url: URL(string: "https://foo.bar.baz.org/dingbat")!) + + tester().tapView(withAccessibilityIdentifier: "url") + let textField = tester().waitForView(withAccessibilityLabel: "Address and Search") as! UITextField + + // Multiple subdomains. + tester().enterText(intoCurrentFirstResponder: "f") + BrowserUtils.ensureAutocompletionResult(tester(), textField: textField, prefix: "f", completion: "oo.bar.baz.org") + tester().clearTextFromFirstResponder() + tester().enterText(intoCurrentFirstResponder: "b") + BrowserUtils.ensureAutocompletionResult(tester(), textField: textField, prefix: "b", completion: "ar.baz.org") + tester().enterText(intoCurrentFirstResponder: "a") + BrowserUtils.ensureAutocompletionResult(tester(), textField: textField, prefix: "ba", completion: "r.baz.org") + tester().enterText(intoCurrentFirstResponder: "z") + BrowserUtils.ensureAutocompletionResult(tester(), textField: textField, prefix: "baz", completion: ".org") + } + + override func tearDown() { + super.tearDown() + EarlGrey.select(elementWithMatcher: grey_accessibilityID("goBack")).perform(grey_tap()) + BrowserUtils.resetToAboutHome(tester()) + BrowserUtils.clearPrivateData(tester: tester()) + } +} diff --git a/mobile/ios/UITests/EarlGrey.swift b/mobile/ios/UITests/EarlGrey.swift new file mode 100644 index 0000000000..4597354609 --- /dev/null +++ b/mobile/ios/UITests/EarlGrey.swift @@ -0,0 +1,151 @@ +// +// Copyright 2016 Google Inc. +// +// 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 EarlGrey +import Foundation + +public func GREYAssert(_ expression: @autoclosure () -> Bool, reason: String) { + GREYAssert(expression, reason, details: "Expected expression to be true") +} + +public func GREYAssertTrue(_ expression: @autoclosure () -> Bool, reason: String) { + GREYAssert(expression(), reason, details: "Expected the boolean expression to be true") +} + +public func GREYAssertFalse(_ expression: @autoclosure () -> Bool, reason: String) { + GREYAssert(!expression(), reason, details: "Expected the boolean expression to be false") +} + +public func GREYAssertNotNil(_ expression: @autoclosure ()-> Any?, reason: String) { + GREYAssert(expression() != nil, reason, details: "Expected expression to be not nil") +} + +public func GREYAssertNil(_ expression: @autoclosure () -> Any?, reason: String) { + GREYAssert(expression() == nil, reason, details: "Expected expression to be nil") +} + +public func GREYAssertEqual(_ left: @autoclosure () -> AnyObject?, + _ right: @autoclosure () -> AnyObject?, reason: String) { + GREYAssert(left() === right(), reason, details: "Expected left term to be equal to right term") +} + +public func GREYAssertNotEqual(_ left: @autoclosure () -> AnyObject?, + _ right: @autoclosure () -> AnyObject?, reason: String) { + GREYAssert(left() !== right(), reason, details: "Expected left term to not equal the right term") +} + +public func GREYAssertEqualObjects( _ left: @autoclosure () -> T?, _ right: @autoclosure () -> T?, reason: String) { + GREYAssert(left() == right(), reason, details: "Expected object of the left term to be equal" + + " to the object of the right term") +} + +public func GREYAssertNotEqualObjects( _ left: @autoclosure () -> T?, _ right: @autoclosure () -> T?, reason: String) { + GREYAssert(left() != right(), reason, details: "Expected object of the left term to not" + + " equal the object of the right term") +} + +public func GREYFail(_ reason: String) { + EarlGrey.handle(exception: GREYFrameworkException(name: kGREYAssertionFailedException, + reason: reason), + details: "") +} + +public func GREYFailWithDetails(_ reason: String, details: String) { + EarlGrey.handle(exception: GREYFrameworkException(name: kGREYAssertionFailedException, + reason: reason), + details: details) +} + +private func GREYAssert(_ expression: @autoclosure () -> Bool, + _ reason: String, details: String) { + GREYSetCurrentAsFailable() + if !expression() { + EarlGrey.handle(exception: GREYFrameworkException(name: kGREYAssertionFailedException, + reason: reason), + details: details) + } +} + +private func GREYSetCurrentAsFailable() { + let greyFailureHandlerSelector = + #selector(GREYFailureHandler.setInvocationFile(_:andInvocationLine:)) + let greyFailureHandler = + Thread.current.threadDictionary.value(forKey: kGREYFailureHandlerKey) as! GREYFailureHandler + if greyFailureHandler.responds(to: greyFailureHandlerSelector) { + greyFailureHandler.setInvocationFile!(#file, andInvocationLine:#line) + } +} + +open class EarlGrey: NSObject { + open class func select(elementWithMatcher matcher: GREYMatcher, + file: StaticString = #file, + line: UInt = #line) -> GREYElementInteraction { + return EarlGreyImpl.invoked(fromFile: file.description, lineNumber: line) + .selectElement(with: matcher) + } + + open class func setFailureHandler(handler: GREYFailureHandler, + file: StaticString = #file, + line: UInt = #line) { + return EarlGreyImpl.invoked(fromFile: file.description, lineNumber: line) + .setFailureHandler(handler) + } + + open class func handle(exception: GREYFrameworkException, + details: String, + file: StaticString = #file, + line: UInt = #line) { + return EarlGreyImpl.invoked(fromFile: file.description, lineNumber: line) + .handle(exception, details: details) + } + + @discardableResult open class func rotateDeviceTo(orientation: UIDeviceOrientation, + errorOrNil: UnsafeMutablePointer!, + file: StaticString = #file, + line: UInt = #line) + -> Bool { + return EarlGreyImpl.invoked(fromFile: file.description, lineNumber: line) + .rotateDevice(to: orientation, + errorOrNil: errorOrNil) + } +} + +extension GREYInteraction { + @discardableResult public func assert(_ matcher: @autoclosure () -> GREYMatcher) -> Self { + return self.assert(with:matcher()) + } + + @discardableResult public func assert(_ matcher: @autoclosure () -> GREYMatcher, + error: UnsafeMutablePointer!) -> Self { + return self.assert(with: matcher(), error: error) + } + + @discardableResult public func using(searchAction: GREYAction, + onElementWithMatcher matcher: GREYMatcher) -> Self { + return self.usingSearch(searchAction, onElementWith: matcher) + } +} + +extension GREYCondition { + open func waitWithTimeout(seconds: CFTimeInterval) -> Bool { + return self.wait(withTimeout: seconds) + } + + open func waitWithTimeout(seconds: CFTimeInterval, pollInterval: CFTimeInterval) + -> Bool { + return self.wait(withTimeout: seconds, pollInterval: pollInterval) + } +} diff --git a/mobile/ios/UITests/Global.swift b/mobile/ios/UITests/Global.swift new file mode 100644 index 0000000000..e8b3bd4a88 --- /dev/null +++ b/mobile/ios/UITests/Global.swift @@ -0,0 +1,670 @@ +/* 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 GCDWebServers +import Storage +import WebKit +import SwiftKeychainWrapper +import Shared +import EarlGrey +@testable import Client + +let LabelAddressAndSearch = "Address and Search" + +extension XCTestCase { + func tester(_ file: String = #file, _ line: Int = #line) -> KIFUITestActor { + return KIFUITestActor(inFile: file, atLine: line, delegate: self) + } + + func system(_ file: String = #file, _ line: Int = #line) -> KIFSystemTestActor { + return KIFSystemTestActor(inFile: file, atLine: line, delegate: self) + } +} + +extension KIFUITestActor { + /// Looks for a view with the given accessibility hint. + func tryFindingViewWithAccessibilityHint(_ hint: String) -> Bool { + let element = UIApplication.shared.accessibilityElement { element in + return element?.accessibilityHint! == hint + } + + return element != nil + } + + func waitForViewWithAccessibilityHint(_ hint: String) -> UIView? { + var view: UIView? = nil + autoreleasepool { + wait(for: nil, view: &view, withElementMatching: NSPredicate(format: "accessibilityHint = %@", hint), tappable: false) + } + return view + } + + func viewExistsWithLabel(_ label: String) -> Bool { + do { + try self.tryFindingView(withAccessibilityLabel: label) + return true + } catch { + return false + } + } + + func viewExistsWithLabelPrefixedBy(_ prefix: String) -> Bool { + let element = UIApplication.shared.accessibilityElement { element in + return element?.accessibilityLabel?.hasPrefix(prefix) ?? false + } + return element != nil + } + + /// Waits for and returns a view with the given accessibility value. + func waitForViewWithAccessibilityValue(_ value: String) -> UIView { + var element: UIAccessibilityElement! + + run { _ in + element = UIApplication.shared.accessibilityElement { element in + return element?.accessibilityValue == value + } + + return (element == nil) ? KIFTestStepResult.wait : KIFTestStepResult.success + } + + return UIAccessibilityElement.viewContaining(element) + } + + /// Wait for and returns a view with the given accessibility label as an + /// attributed string. See the comment in ReadingListPanel.swift about + /// using attributed strings as labels. (It lets us set the pitch) + func waitForViewWithAttributedAccessibilityLabel(_ label: NSAttributedString) -> UIView { + var element: UIAccessibilityElement! + + run { _ in + element = UIApplication.shared.accessibilityElement { element in + if let elementLabel = element?.value(forKey: "accessibilityLabel") as? NSAttributedString { + return elementLabel.isEqual(to: label) + } + return false + } + + return (element == nil) ? KIFTestStepResult.wait : KIFTestStepResult.success + } + + return UIAccessibilityElement.viewContaining(element) + } + + /// There appears to be a KIF bug where waitForViewWithAccessibilityLabel returns the parent + /// UITableView instead of the UITableViewCell with the given label. + /// As a workaround, retry until KIF gives us a cell. + /// Open issue: https://github.com/kif-framework/KIF/issues/336 + func waitForCellWithAccessibilityLabel(_ label: String) -> UITableViewCell { + var cell: UITableViewCell! + + run { _ in + let view = self.waitForView(withAccessibilityLabel: label) + cell = view as? UITableViewCell + return (cell == nil) ? KIFTestStepResult.wait : KIFTestStepResult.success + } + + return cell + } + + /** + * Finding views by accessibility label doesn't currently work with WKWebView: + * https://github.com/kif-framework/KIF/issues/460 + * As a workaround, inject a KIFHelper class that iterates the document and finds + * elements with the given textContent or title. + */ + func waitForWebViewElementWithAccessibilityLabel(_ text: String) { + run { error in + if self.hasWebViewElementWithAccessibilityLabel(text) { + return KIFTestStepResult.success + } + + return KIFTestStepResult.wait + } + } + + /** + * Sets the text for a WKWebView input element with the given name. + */ + func enterText(_ text: String, intoWebViewInputWithName inputName: String) { + let webView = getWebViewWithKIFHelper() + var stepResult = KIFTestStepResult.wait + + let escaped = text.replacingOccurrences(of: "\"", with: "\\\"") + webView.evaluateJavaScript("KIFHelper.enterTextIntoInputWithName(\"\(escaped)\", \"\(inputName)\");") { success, _ in + stepResult = (success as! Bool) ? KIFTestStepResult.success : KIFTestStepResult.failure + } + + run { error in + if stepResult == KIFTestStepResult.failure { + error?.pointee = NSError(domain: "KIFHelper", code: 0, userInfo: [NSLocalizedDescriptionKey: "Input element not found in webview: \(escaped)"]) + } + return stepResult + } + } + + /** + * Clicks a WKWebView element with the given label. + */ + func tapWebViewElementWithAccessibilityLabel(_ text: String) { + let webView = getWebViewWithKIFHelper() + var stepResult = KIFTestStepResult.wait + + let escaped = text.replacingOccurrences(of: "\"", with: "\\\"") + webView.evaluateJavaScript("KIFHelper.tapElementWithAccessibilityLabel(\"\(escaped)\")") { success, _ in + stepResult = (success as! Bool) ? KIFTestStepResult.success : KIFTestStepResult.failure + } + + run { error in + if stepResult == KIFTestStepResult.failure { + error?.pointee = NSError(domain: "KIFHelper", code: 0, userInfo: [NSLocalizedDescriptionKey: "Accessibility label not found in webview: \(escaped)"]) + } + return stepResult + } + } + + /** + * Determines whether an element in the page exists. + */ + func hasWebViewElementWithAccessibilityLabel(_ text: String) -> Bool { + let webView = getWebViewWithKIFHelper() + var stepResult = KIFTestStepResult.wait + var found = false + + let escaped = text.replacingOccurrences(of: "\"", with: "\\\"") + webView.evaluateJavaScript("KIFHelper.hasElementWithAccessibilityLabel(\"\(escaped)\")") { success, _ in + found = success as? Bool ?? false + stepResult = KIFTestStepResult.success + } + + run { _ in return stepResult } + + return found + } + + fileprivate func getWebViewWithKIFHelper() -> WKWebView { + let webView = waitForView(withAccessibilityLabel: "Web content") as! WKWebView + + // Wait for the web view to stop loading. + run { _ in + return webView.isLoading ? KIFTestStepResult.wait : KIFTestStepResult.success + } + + var stepResult = KIFTestStepResult.wait + + webView.evaluateJavaScript("typeof KIFHelper") { result, _ in + if result as! String == "undefined" { + let bundle = Bundle(for: BrowserTests.self) + let path = bundle.path(forResource: "KIFHelper", ofType: "js")! + let source = try! NSString(contentsOfFile: path, encoding: String.Encoding.utf8.rawValue) + webView.evaluateJavaScript(source as String, completionHandler: nil) + } + stepResult = KIFTestStepResult.success + } + + run { _ in return stepResult } + + return webView + } + + public func deleteCharacterFromFirstResponser() { + self.enterText(intoCurrentFirstResponder: "\u{0008}") + } +} + +class BrowserUtils { + // Needs to be in sync with Client Clearables. + enum Clearable: String { + case History = "Browsing History" + case Cache = "Cache" + case OfflineData = "Offline Website Data" + case Cookies = "Cookies" + } + internal static let AllClearables = Set([Clearable.History, Clearable.Cache, Clearable.OfflineData, Clearable.Cookies]) + + /// Close all tabs to restore the browser to startup state. + class func resetToAboutHome(_ tester: KIFUITestActor) { + + do { + try tester.tryFindingTappableView(withAccessibilityLabel: "Cancel") + tester.tapView(withAccessibilityLabel: "Cancel") + } catch _ { + } + do { + try tester.tryFindingTappableView(withAccessibilityLabel: "Show Tabs") + tester.tapView(withAccessibilityLabel: "Show Tabs") + } catch _ { + + } + let tabsView = tester.waitForView(withAccessibilityLabel: "Tabs Tray").subviews.first as! UICollectionView + + // Switch to Private Mode if we're not in it already. + do { + try tester.tryFindingTappableView(withAccessibilityLabel: "Private Mode", value: "Off", traits: UIAccessibilityTraitButton) + tester.tapView(withAccessibilityLabel: "Private Mode") + } catch _ {} + + // Clear all private tabs. + while tabsView.numberOfItems(inSection: 0) > 0 { + let cell = tabsView.cellForItem(at: IndexPath(item: 0, section: 0))! + tester.swipeView(withAccessibilityLabel: cell.accessibilityLabel, in: KIFSwipeDirection.left) + tester.waitForAbsenceOfView(withAccessibilityLabel: cell.accessibilityLabel) + } + tester.tapView(withAccessibilityLabel: "Private Mode") + + while tabsView.numberOfItems(inSection: 0) > 1 { + let oldCount = tabsView.numberOfItems(inSection: 0) + let cell = tabsView.cellForItem(at: IndexPath(item: 0, section: 0))! + tester.swipeView(withAccessibilityLabel: cell.accessibilityLabel, in: KIFSwipeDirection.left) + tester.waitForAnimationsToFinish() + XCTAssertEqual(oldCount-1, tabsView.numberOfItems(inSection: 0)) + } + + // When the last tab is closed, the tabs tray will automatically be closed + // since a new about:home tab will be selected. + if let cell = tabsView.cellForItem(at: IndexPath(item: 0, section: 0)) { + tester.swipeView(withAccessibilityLabel: cell.accessibilityLabel, in: KIFSwipeDirection.left) + tester.waitForTappableView(withAccessibilityLabel: "Show Tabs") + } + } + + //If it is a first run, first run window should be gone + class func dismissFirstRunUI(_ tester: KIFUITestActor) { + do { + try tester.tryFindingView(withAccessibilityLabel: "Intro Tour Carousel") + tester.swipeView(withAccessibilityLabel: "Intro Tour Carousel", in: KIFSwipeDirection.left) + tester.waitForTappableView(withAccessibilityLabel: "Start Browsing") + tester.tapView(withAccessibilityLabel: "Start Browsing") + } catch { + //First run dialog did not appear + } + } + + class func dismissFirstRunUI() { + var error: NSError? + + let matcher = grey_allOf([ + grey_accessibilityID("IntroViewController.scrollView"), grey_sufficientlyVisible()]) + + EarlGrey.select(elementWithMatcher: matcher).assert(grey_notNil(), error: &error) + + if error == nil { + EarlGrey.select(elementWithMatcher: matcher).perform(grey_swipeFastInDirection(GREYDirection.left)) + let buttonMatcher = grey_allOf([ + grey_accessibilityID("IntroViewController.startBrowsingButton"), grey_sufficientlyVisible()]) + + EarlGrey.select(elementWithMatcher: buttonMatcher).assert(grey_notNil(), error: &error) + + if error == nil { + EarlGrey.select(elementWithMatcher: buttonMatcher).perform(grey_tap()) + } + } + } + + class func iPad() -> Bool { + return UIDevice.current.userInterfaceIdiom == .pad + } + + /// Injects a URL and title into the browser's history database. + class func addHistoryEntry(_ title: String, url: URL) { + let notificationCenter = NotificationCenter.default + var info = [AnyHashable: Any]() + info["url"] = url + info["title"] = title + info["visitType"] = VisitType.link.rawValue + notificationCenter.post(name: Notification.Name(rawValue: "OnLocationChange"), object: self, userInfo: info) + } + + fileprivate class func clearHistoryItemAtIndex(_ index: IndexPath, tester: KIFUITestActor) { + if let row = tester.waitForCell(at: index, inTableViewWithAccessibilityIdentifier: "History List") { + tester.swipeView(withAccessibilityLabel: row.accessibilityLabel, value: row.accessibilityValue, in: KIFSwipeDirection.left) + tester.tapView(withAccessibilityLabel: "Remove") + } + } + class func openClearPrivateDataDialog(_ swipe: Bool, tester: KIFUITestActor) { + tester.tapView(withAccessibilityLabel: "Menu") + + // Need this for simulator only + if swipe { + tester.swipeView(withAccessibilityLabel: "Set Homepage", in: KIFSwipeDirection.left) + } + tester.tapView(withAccessibilityLabel: "Settings") + tester.tapView(withAccessibilityLabel: "Clear Private Data") + } + + class func closeClearPrivateDataDialog(_ tester: KIFUITestActor) { + tester.tapView(withAccessibilityLabel: "Settings") + tester.tapView(withAccessibilityLabel: "Done") + } + + fileprivate class func acceptClearPrivateData(_ tester: KIFUITestActor) { + tester.waitForView(withAccessibilityLabel: "OK") + tester.tapView(withAccessibilityLabel: "OK") + tester.waitForView(withAccessibilityLabel: "Clear Private Data") + } + + fileprivate class func cancelClearPrivateData(_ tester: KIFUITestActor) { + tester.waitForView(withAccessibilityLabel: "Clear") + tester.tapView(withAccessibilityLabel: "Cancel") + tester.waitForView(withAccessibilityLabel: "Clear Private Data") + } + + class func clearPrivateData(_ clearables: Set? = AllClearables, swipe: Bool? = false, tester: KIFUITestActor) { + let AllClearables = Set([Clearable.History, Clearable.Cache, Clearable.OfflineData, Clearable.Cookies]) + + openClearPrivateDataDialog(swipe!, tester: tester) + + // Disable all items that we don't want to clear. + for clearable in AllClearables { + // If we don't wait here, setOn:forSwitchWithAccessibilityLabel tries to use the UITableViewCell + // instead of the UISwitch. KIF bug? + tester.waitForView(withAccessibilityLabel: clearable.rawValue) +// tester.setOn(clearables!.contains(clearable), forSwitchWithAccessibilityLabel: clearable.rawValue) + let switchControl = grey_allOf([grey_accessibilityLabel(clearable.rawValue), + grey_kindOfClass(UISwitch.self)]) + EarlGrey.select(elementWithMatcher: switchControl).perform(grey_turnSwitchOn(clearables!.contains(clearable))) + } + + + + tester.waitForView(withAccessibilityLabel: "Clear Private Data") + tester.tapView(withAccessibilityLabel: "Clear Private Data", traits: UIAccessibilityTraitButton) + acceptClearPrivateData(tester) + + closeClearPrivateDataDialog(tester) + } + + class func clearHistoryItems(_ tester: KIFUITestActor, numberOfTests: Int = -1) { + resetToAboutHome(tester) + tester.tapView(withAccessibilityLabel: "History") + + let historyTable = tester.waitForView(withAccessibilityIdentifier: "History List") as! UITableView + var index = 0 + for _ in 0 ..< historyTable.numberOfSections { + for _ in 0 ..< historyTable.numberOfRows(inSection: 0) { + clearHistoryItemAtIndex(IndexPath(row: 0, section: 0), tester: tester) + if numberOfTests > -1 { + index += 1 + if index == numberOfTests { + return + } + } + } + } + tester.tapView(withAccessibilityLabel: "Top sites") + } + + class func ensureAutocompletionResult(_ tester: KIFUITestActor, textField: UITextField, prefix: String, completion: String) { + // searches are async (and debounced), so we have to wait for the results to appear. + tester.waitForViewWithAccessibilityValue(prefix + completion) + + let autocompleteFieldlabel = textField.subviews.filter { $0.accessibilityIdentifier == "autocomplete" }.first as? UILabel + + if completion == "" { + XCTAssertTrue(autocompleteFieldlabel == nil, "The autocomplete was empty but the label still exists.") + return + } + + XCTAssertTrue(autocompleteFieldlabel != nil, "The autocomplete was not found") + XCTAssertEqual(completion, autocompleteFieldlabel!.text, "Expected prefix matches actual prefix") + + } +} + +class SimplePageServer { + class func getPageData(_ name: String, ext: String = "html") -> String { + let pageDataPath = Bundle(for: self).path(forResource: name, ofType: ext)! + return (try! NSString(contentsOfFile: pageDataPath, encoding: String.Encoding.utf8.rawValue)) as String + } + + class func start() -> String { + let webServer: GCDWebServer = GCDWebServer() + + webServer.addHandler(forMethod: "GET", path: "/image.png", request: GCDWebServerRequest.self) { (request) -> GCDWebServerResponse! in + let img = UIImagePNGRepresentation(UIImage(named: "goBack")!) + return GCDWebServerDataResponse(data: img, contentType: "image/png") + } + + for page in ["findPage", "noTitle", "readablePage", "JSPrompt"] { + webServer.addHandler(forMethod: "GET", path: "/\(page).html", request: GCDWebServerRequest.self) { (request) -> GCDWebServerResponse! in + return GCDWebServerDataResponse(html: self.getPageData(page)) + } + } + + // we may create more than one of these but we need to give them uniquie accessibility ids in the tab manager so we'll pass in a page number + webServer.addHandler(forMethod: "GET", path: "/scrollablePage.html", request: GCDWebServerRequest.self) { (request) -> GCDWebServerResponse! in + var pageData = self.getPageData("scrollablePage") + let page = Int((request?.query["page"] as! String))! + pageData = pageData.replacingOccurrences(of: "{page}", with: page.description) + return GCDWebServerDataResponse(html: pageData as String) + } + + webServer.addHandler(forMethod: "GET", path: "/numberedPage.html", request: GCDWebServerRequest.self) { (request) -> GCDWebServerResponse! in + var pageData = self.getPageData("numberedPage") + + let page = Int((request?.query["page"] as! String))! + pageData = pageData.replacingOccurrences(of: "{page}", with: page.description) + + return GCDWebServerDataResponse(html: pageData as String) + } + + webServer.addHandler(forMethod: "GET", path: "/readerContent.html", request: GCDWebServerRequest.self) { (request) -> GCDWebServerResponse! in + return GCDWebServerDataResponse(html: self.getPageData("readerContent")) + } + + webServer.addHandler(forMethod: "GET", path: "/loginForm.html", request: GCDWebServerRequest.self) { _ in + return GCDWebServerDataResponse(html: self.getPageData("loginForm")) + } + + webServer.addHandler(forMethod: "GET", path: "/navigationDelegate.html", request: GCDWebServerRequest.self) { _ in + return GCDWebServerDataResponse(html: self.getPageData("navigationDelegate")) + } + + webServer.addHandler(forMethod: "GET", path: "/localhostLoad.html", request: GCDWebServerRequest.self) { _ in + return GCDWebServerDataResponse(html: self.getPageData("localhostLoad")) + } + + webServer.addHandler(forMethod: "GET", path: "/auth.html", request: GCDWebServerRequest.self) { (request: GCDWebServerRequest?) in + // "user:pass", Base64-encoded. + let expectedAuth = "Basic dXNlcjpwYXNz" + + let response: GCDWebServerDataResponse + if request?.headers["Authorization"] as? String == expectedAuth && request?.query["logout"] == nil { + response = GCDWebServerDataResponse(html: "logged in") + } else { + // Request credentials if the user isn't logged in. + response = GCDWebServerDataResponse(html: "auth fail") + response.statusCode = 401 + response.setValue("Basic realm=\"test\"", forAdditionalHeader: "WWW-Authenticate") + } + + return response + } + + func htmlForImageBlockingTest(imageURL: String) -> String{ + let html = + """ + + TEST IMAGE BLOCKING + """ + return html + } + + // Add tracking protection check page + webServer.addHandler(forMethod: "GET", path: "/tracking-protection-test.html", request: GCDWebServerRequest.self) { (request: GCDWebServerRequest?) in + return GCDWebServerDataResponse(html: htmlForImageBlockingTest(imageURL: "http://ymail.com/favicon.ico")) + } + + // Add image blocking test page + webServer.addHandler(forMethod: "GET", path: "/hide-images-test.html", request: GCDWebServerRequest.self) { (request: GCDWebServerRequest?) in + return GCDWebServerDataResponse(html: htmlForImageBlockingTest(imageURL: "https://www.mozilla.com/favicon.ico")) + } + + + if !webServer.start(withPort: 0, bonjourName: nil) { + XCTFail("Can't start the GCDWebServer") + } + + // We use 127.0.0.1 explicitly here, rather than localhost, in order to avoid our + // history exclusion code (Bug 1188626). + let webRoot = "http://127.0.0.1:\(webServer.port)" + return webRoot + } +} + +class SearchUtils { + static func navigateToSearchSettings(_ tester: KIFUITestActor) { + let engine = SearchUtils.getDefaultEngine().shortName + tester.tapView(withAccessibilityLabel: "Menu") + tester.waitForAnimationsToFinish() + tester.tapView(withAccessibilityLabel: "Settings") + tester.waitForView(withAccessibilityLabel: "Settings") + tester.tapView(withAccessibilityLabel: "Search, \(engine)") + tester.waitForView(withAccessibilityIdentifier: "Search") + } + + static func navigateFromSearchSettings(_ tester: KIFUITestActor) { + tester.tapView(withAccessibilityLabel: "Settings") + tester.tapView(withAccessibilityLabel: "Done") + } + + // Given that we're at the Search Settings sheet, select the named search engine as the default. + // Afterwards, we're still at the Search Settings sheet. + static func selectDefaultSearchEngineName(_ tester: KIFUITestActor, engineName: String) { + tester.tapView(withAccessibilityLabel: "Default Search Engine", traits: UIAccessibilityTraitButton) + tester.waitForView(withAccessibilityLabel: "Default Search Engine") + tester.tapView(withAccessibilityLabel: engineName) + tester.waitForView(withAccessibilityLabel: "Search") + } + + // Given that we're at the Search Settings sheet, return the default search engine's name. + static func getDefaultSearchEngineName(_ tester: KIFUITestActor) -> String { + let view = tester.waitForCellWithAccessibilityLabel("Default Search Engine") + return view.accessibilityValue! + } + + static func getDefaultEngine() -> OpenSearchEngine! { + guard let userProfile = (UIApplication.shared.delegate as? AppDelegate)?.profile else { + XCTFail("Unable to get a reference to the user's profile") + return nil + } + return userProfile.searchEngines.defaultEngine + } +/* + static func youTubeSearchEngine() -> OpenSearchEngine { + return mockSearchEngine("YouTube", template: "https://m.youtube.com/#/results?q={searchTerms}&sm=", icon: "youtube")! + } + + static func mockSearchEngine(_ name: String, template: String, icon: String) -> OpenSearchEngine? { + guard let imagePath = Bundle(for: self).path(forResource: icon, ofType: "ico"), + let imageData = Data(contentsOfFile: imagePath), + let image = UIImage(data: imageData) else { + XCTFail("Unable to load search engine image named \(icon).ico") + return nil + } + + return OpenSearchEngine(engineID: nil, shortName: name, image: image, searchTemplate: template, suggestTemplate: nil, isCustomEngine: true) + } +*/ + static func addCustomSearchEngine(_ engine: OpenSearchEngine) { + guard let userProfile = (UIApplication.shared.delegate as? AppDelegate)?.profile else { + XCTFail("Unable to get a reference to the user's profile") + return + } + + userProfile.searchEngines.addSearchEngine(engine) + } + + static func removeCustomSearchEngine(_ engine: OpenSearchEngine) { + guard let userProfile = (UIApplication.shared.delegate as? AppDelegate)?.profile else { + XCTFail("Unable to get a reference to the user's profile") + return + } + + userProfile.searchEngines.deleteCustomEngine(engine) + } +} + +// From iOS 10, below methods no longer works +class DynamicFontUtils { + // Need to leave time for the notification to propagate + static func bumpDynamicFontSize(_ tester: KIFUITestActor) { + let value = UIContentSizeCategory.accessibilityExtraLarge + UIApplication.shared.setValue(value, forKey: "preferredContentSizeCategory") + tester.wait(forTimeInterval: 0.3) + } + + static func lowerDynamicFontSize(_ tester: KIFUITestActor) { + let value = UIContentSizeCategory.extraSmall + UIApplication.shared.setValue(value, forKey: "preferredContentSizeCategory") + tester.wait(forTimeInterval: 0.3) + } + + static func restoreDynamicFontSize(_ tester: KIFUITestActor) { + let value = UIContentSizeCategory.medium + UIApplication.shared.setValue(value, forKey: "preferredContentSizeCategory") + tester.wait(forTimeInterval: 0.3) + } +} + +class PasscodeUtils { + static func resetPasscode() { + KeychainWrapper.sharedAppContainerKeychain.setAuthenticationInfo(nil) + } + + static func setPasscode(_ code: String, interval: PasscodeInterval) { + let info = AuthenticationKeychainInfo(passcode: code) + info.updateRequiredPasscodeInterval(interval) + KeychainWrapper.sharedAppContainerKeychain.setAuthenticationInfo(info) + } + + static func enterPasscode(_ tester: KIFUITestActor, digits: String) { + tester.tapView(withAccessibilityLabel: String(digits.characters[digits.startIndex])) + tester.tapView(withAccessibilityLabel: String(digits.characters[digits.characters.index(digits.startIndex, offsetBy: 1)])) + tester.tapView(withAccessibilityLabel: String(digits.characters[digits.characters.index(digits.startIndex, offsetBy: 2)])) + tester.tapView(withAccessibilityLabel: String(digits.characters[digits.characters.index(digits.startIndex, offsetBy: 3)])) + } +} + +class HomePageUtils { + static func navigateToHomePageSettings(_ tester: KIFUITestActor) { + tester.waitForAnimationsToFinish() + tester.tapView(withAccessibilityLabel: "Menu") + tester.tapView(withAccessibilityLabel: "Settings") + tester.tapView(withAccessibilityIdentifier: "Homepage") + } + + static func homePageSetting(_ tester: KIFUITestActor) -> String? { + let view = tester.waitForView(withAccessibilityIdentifier: "HomePageSettingTextField") + guard let textField = view as? UITextField else { + XCTFail("View is not a textField: view is \(String(describing: view))") + return nil + } + return textField.text + } + + static func navigateFromHomePageSettings(_ tester: KIFUITestActor) { + tester.tapView(withAccessibilityLabel: "Settings") + tester.tapView(withAccessibilityLabel: "Done") + } +} diff --git a/mobile/ios/UITests/HistoryTests.swift b/mobile/ios/UITests/HistoryTests.swift new file mode 100644 index 0000000000..9e5ee32921 --- /dev/null +++ b/mobile/ios/UITests/HistoryTests.swift @@ -0,0 +1,149 @@ +/* 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 WebKit +import EarlGrey + +class HistoryTests: KIFTestCase { + fileprivate var webRoot: String! + + override func setUp() { + super.setUp() + webRoot = SimplePageServer.start() + BrowserUtils.dismissFirstRunUI() + } + + func addHistoryItemPage(_ pageNo: Int) -> String { + // Load a page + let url = "\(webRoot!)/numberedPage.html?page=\(pageNo)" + EarlGrey.select(elementWithMatcher: grey_accessibilityID("url")).perform(grey_tap()) + + EarlGrey.select(elementWithMatcher: grey_accessibilityID("address")).perform(grey_replaceText(url)) + EarlGrey.select(elementWithMatcher: grey_accessibilityID("address")).perform(grey_typeText("\n")) + tester().waitForWebViewElementWithAccessibilityLabel("Page \(pageNo)") + return url + } + + func addHistoryItems(_ noOfItemsToAdd: Int) -> [String] { + var urls = [String]() + for index in 1...noOfItemsToAdd { + urls.append(addHistoryItemPage(index)) + } + return urls + } + + /** + * Tests for listed history visits + */ + func testAddHistoryUI() { + _ = addHistoryItems(2) + + // Check that both appear in the history home panel + EarlGrey.select(elementWithMatcher: grey_accessibilityID("url")).perform(grey_tap()) + EarlGrey.select(elementWithMatcher: grey_accessibilityLabel("History")).perform(grey_tap()) + + // Wait until the dialog shows up + let listAppeared = GREYCondition(name: "Wait the history list to appear", block: { _ in + var errorOrNil: NSError? + let matcher = grey_allOf([grey_accessibilityLabel("Page 2"), + grey_sufficientlyVisible()]) + EarlGrey.select(elementWithMatcher: matcher) + .inRoot(grey_accessibilityID("History List")) + .assert(grey_notNil(), error: &errorOrNil) + return errorOrNil == nil + }).wait(withTimeout: 20) + GREYAssertTrue(listAppeared, reason: "Failed to display history") + + EarlGrey.select(elementWithMatcher: grey_accessibilityLabel("Page 2")) + .inRoot(grey_accessibilityID("History List")) + .assert(grey_sufficientlyVisible()) + EarlGrey.select(elementWithMatcher: grey_accessibilityLabel("Page 1")) + .inRoot(grey_accessibilityID("History List")) + .assert(grey_sufficientlyVisible()) + EarlGrey.select(elementWithMatcher: grey_accessibilityLabel("\(webRoot!)/numberedPage.html?page=2")) + .inRoot(grey_accessibilityID("History List")) + .assert(grey_sufficientlyVisible()) + EarlGrey.select(elementWithMatcher: grey_accessibilityLabel("\(webRoot!)/numberedPage.html?page=1")) + .inRoot(grey_accessibilityID("History List")) + .assert(grey_sufficientlyVisible()) + + EarlGrey.select(elementWithMatcher: grey_accessibilityID("goBack")).perform(grey_tap()) + } + + func testDeleteHistoryItemFromListWith2Items() { + // add 2 history items + let urls = addHistoryItems(2) + + // Check that both appear in the history home panel + EarlGrey.select(elementWithMatcher: grey_accessibilityID("url")).perform(grey_tap()) + EarlGrey.select(elementWithMatcher: grey_accessibilityLabel("History")).perform(grey_tap()) + EarlGrey.select(elementWithMatcher: grey_accessibilityLabel(urls[0])) + .perform(grey_longPress()) + EarlGrey.select(elementWithMatcher: grey_accessibilityLabel("Delete from History")) + .inRoot(grey_kindOfClass(NSClassFromString("UITableViewCellContentView")!)) + .perform(grey_tap()) + + // The second history entry still exists + EarlGrey.select(elementWithMatcher: grey_accessibilityLabel(urls[1])) + .inRoot(grey_kindOfClass(NSClassFromString("UITableViewCellContentView")!)) + .assert(grey_notNil()) + + // check page 1 does not exist + let historyRemoved = GREYCondition(name: "Check entry is removed", block: { _ in + var errorOrNil: NSError? + let matcher = grey_allOf([grey_accessibilityLabel(urls[0]), + grey_sufficientlyVisible()]) + EarlGrey.select(elementWithMatcher: matcher).assert(grey_notNil(), error: &errorOrNil) + let success = errorOrNil != nil + return success + }).wait(withTimeout: 5) + GREYAssertTrue(historyRemoved, reason: "Failed to remove history") + + EarlGrey.select(elementWithMatcher:grey_accessibilityID("goBack")).perform(grey_tap()) + } + + func testDeleteHistoryItemFromListWithMoreThan100Items() { + EarlGrey.select(elementWithMatcher: grey_accessibilityID("url")).perform(grey_tap()) + EarlGrey.select(elementWithMatcher: grey_accessibilityLabel("Top sites")).perform(grey_tap()) + + for pageNo in 1...102 { + BrowserUtils.addHistoryEntry("Page \(pageNo)", url: URL(string: "\(webRoot!)/numberedPage.html?page=\(pageNo)")!) + } + let urlToDelete = "\(webRoot!)/numberedPage.html?page=\(102)" + let oldestUrl = "\(webRoot!)/numberedPage.html?page=\(101)" + + EarlGrey.select(elementWithMatcher:grey_accessibilityLabel("History")) + .perform(grey_tap()) + EarlGrey.select(elementWithMatcher: grey_accessibilityLabel("Page 102")).inRoot(grey_kindOfClass(NSClassFromString("UITableView")!)).perform(grey_swipeSlowInDirectionWithStartPoint(.left, 0.4, 0.4)) + EarlGrey.select(elementWithMatcher:grey_accessibilityLabel("Delete")) + .inRoot(grey_kindOfClass(NSClassFromString("UISwipeActionStandardButton")!)) + .perform(grey_tap()) + + // The history list still exists + EarlGrey.select(elementWithMatcher: grey_accessibilityID("History List")) + .assert(grey_notNil()) + EarlGrey.select(elementWithMatcher: grey_accessibilityLabel(oldestUrl)) + .assert(grey_notNil()) + + // check page 1 does not exist + let historyRemoved = GREYCondition(name: "Check entry is removed", block: { _ in + var errorOrNil: NSError? + let matcher = grey_allOf([grey_accessibilityLabel(urlToDelete), + grey_sufficientlyVisible()]) + EarlGrey.select(elementWithMatcher:matcher).assert(grey_notNil(), error: &errorOrNil) + let success = errorOrNil != nil + return success + }).wait(withTimeout: 5) + GREYAssertTrue(historyRemoved, reason: "Failed to remove history") + + EarlGrey.select(elementWithMatcher:grey_accessibilityID("goBack")).perform(grey_tap()) + } + + override func tearDown() { + //BrowserUtils.resetToAboutHome(tester()) + BrowserUtils.clearPrivateData(tester: tester()) + super.tearDown() + } +} diff --git a/mobile/ios/UITests/Info.plist b/mobile/ios/UITests/Info.plist new file mode 100644 index 0000000000..9411e4a06e --- /dev/null +++ b/mobile/ios/UITests/Info.plist @@ -0,0 +1,24 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + BNDL + CFBundleShortVersionString + 10.6 + CFBundleSignature + ???? + CFBundleVersion + 1 + + diff --git a/mobile/ios/UITests/JSPrompt.html b/mobile/ios/UITests/JSPrompt.html new file mode 100644 index 0000000000..698c3cf460 --- /dev/null +++ b/mobile/ios/UITests/JSPrompt.html @@ -0,0 +1,25 @@ + + + + JS Prompt + + + + +
JS Prompt
+ +
+ Click the 'Mana' button, sit idle on the auth prompt + + This page will popup alert after 6 seconds + + Click cancel on the auth prompt when focus returns to this page +
+ + + + \ No newline at end of file diff --git a/mobile/ios/UITests/KIFHelper.js b/mobile/ios/UITests/KIFHelper.js new file mode 100644 index 0000000000..f056fca996 --- /dev/null +++ b/mobile/ios/UITests/KIFHelper.js @@ -0,0 +1,60 @@ +/* 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/. */ + +var KIFHelper = { + /** + * Determines whether an element with the given accessibility label exists. + * @return True if an element was found + */ + hasElementWithAccessibilityLabel: function (value) { + return this._getElementWithAccessibilityLabel(document.body, value) !== null; + }, + + _getElementWithAccessibilityLabel: function (el, value) { + if (el.textContent == value || el.title == value || el.value == value) { + return el; + } + + for (var i = 0; i < el.children.length; i++) { + var found = this._getElementWithAccessibilityLabel(el.children[i], value); + if (found) { + return found; + } + } + + return null; + }, + + /** + * Sets the text for an input element with the given name. + * @return True if successful + */ + enterTextIntoInputWithName: function (text, name) { + var inputs = document.getElementsByName(name); + if (inputs.length !== 1) { + return false; + } + + var input = inputs[0]; + if (input.tagName !== "INPUT") { + return false; + } + + input.value = text; + return true; + }, + + /** + * Taps an element with the given accessibility label. + * @return True if successful + */ + tapElementWithAccessibilityLabel: function (label) { + var found = this._getElementWithAccessibilityLabel(document.body, label); + if (found) { + found.click(); + return true; + } + return false; + }, +}; diff --git a/mobile/ios/UITests/LoginInputTests.swift b/mobile/ios/UITests/LoginInputTests.swift new file mode 100644 index 0000000000..df9827389f --- /dev/null +++ b/mobile/ios/UITests/LoginInputTests.swift @@ -0,0 +1,124 @@ +/* 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 EarlGrey +@testable import Client + +class LoginInputTests: KIFTestCase { + fileprivate var webRoot: String! + fileprivate var profile: Profile! + + override func setUp() { + super.setUp() + profile = (UIApplication.shared.delegate as! AppDelegate).profile! + webRoot = SimplePageServer.start() + BrowserUtils.dismissFirstRunUI() + } + + override func tearDown() { + _ = profile.logins.removeAll().value + BrowserUtils.resetToAboutHome(tester()) + BrowserUtils.clearPrivateData(tester: tester()) + super.tearDown() + } + + func enterUrl(url: String) { + EarlGrey.select(elementWithMatcher: grey_accessibilityID("url")).perform(grey_tap()) + EarlGrey.select(elementWithMatcher: grey_accessibilityID("address")).perform(grey_replaceText(url)) + EarlGrey.select(elementWithMatcher: grey_accessibilityID("address")).perform(grey_typeText("\n")) + } + + func waitForLoginDialog(text: String, appears: Bool = true) { + var success = false + var failedReason = "Failed to display dialog" + + if appears == false { + failedReason = "Dialog still displayed" + } + + let saveLoginDialog = GREYCondition(name: "Check login dialog appears", block: { _ in + var errorOrNil: NSError? + let matcher = grey_allOf([grey_accessibilityLabel(text), + grey_sufficientlyVisible()]) + EarlGrey.select(elementWithMatcher: matcher) + .assert(grey_notNil(), error: &errorOrNil) + if appears == true { + success = errorOrNil == nil + } else { + success = errorOrNil != nil + } + return success + }).wait(withTimeout: 10) + + GREYAssertTrue(saveLoginDialog, reason: failedReason) + } + + func testLoginFormDisplaysNewSnackbar() { + let url = "\(webRoot!)/loginForm.html" + let username = "test@user.com" + + enterUrl(url: url) + tester().enterText(username, intoWebViewInputWithName: "username") + tester().enterText("password", intoWebViewInputWithName: "password") + tester().tapWebViewElementWithAccessibilityLabel("submit_btn") + + waitForLoginDialog(text: "Save login \(username) for \(self.webRoot!)?") + EarlGrey.select(elementWithMatcher: grey_accessibilityID("SaveLoginPrompt.dontSaveButton")).perform(grey_tap()) + } + + func testLoginFormDisplaysUpdateSnackbarIfPreviouslySaved() { + let url = "\(webRoot!)/loginForm.html" + let username = "test@user.com" + let password1 = "password1" + let password2 = "password2" + + enterUrl(url: url) + tester().enterText(username, intoWebViewInputWithName: "username") + tester().enterText(password1, intoWebViewInputWithName: "password") + tester().tapWebViewElementWithAccessibilityLabel("submit_btn") + waitForLoginDialog(text: "Save login \(username) for \(self.webRoot!)?") + EarlGrey.select(elementWithMatcher: grey_accessibilityID("SaveLoginPrompt.saveLoginButton")) + .perform(grey_tap()) + + tester().enterText(username, intoWebViewInputWithName: "username") + tester().enterText(password2, intoWebViewInputWithName: "password") + tester().tapWebViewElementWithAccessibilityLabel("submit_btn") + waitForLoginDialog(text: "Update login \(username) for \(self.webRoot!)?") + EarlGrey.select(elementWithMatcher: grey_accessibilityID("UpdateLoginPrompt.updateButton")) + .perform(grey_tap()) + } + + func testLoginFormDoesntOfferSaveWhenEmptyPassword() { + let url = "\(webRoot!)/loginForm.html" + let username = "test@user.com" + + enterUrl(url: url) + tester().enterText(username, intoWebViewInputWithName: "username") + tester().enterText("", intoWebViewInputWithName: "password") + tester().tapWebViewElementWithAccessibilityLabel("submit_btn") + + waitForLoginDialog(text: "Save login \(username) for \(self.webRoot!)?", appears: false) + } + + func testLoginFormDoesntOfferUpdateWhenEmptyPassword() { + let url = "\(webRoot!)/loginForm.html" + let username = "test@user.com" + let password1 = "password1" + let password2 = "" + + enterUrl(url: url) + tester().enterText(username, intoWebViewInputWithName: "username") + tester().enterText(password1, intoWebViewInputWithName: "password") + tester().tapWebViewElementWithAccessibilityLabel("submit_btn") + waitForLoginDialog(text: "Save login \(username) for \(self.webRoot!)?") + EarlGrey.select(elementWithMatcher: grey_accessibilityID("SaveLoginPrompt.saveLoginButton")) + .perform(grey_tap()) + + tester().enterText(username, intoWebViewInputWithName: "username") + tester().enterText(password2, intoWebViewInputWithName: "password") + tester().tapWebViewElementWithAccessibilityLabel("submit_btn") + waitForLoginDialog(text: "Save login \(username) for \(self.webRoot!)?", appears: false) + } +} diff --git a/mobile/ios/UITests/LoginManagerTests.swift b/mobile/ios/UITests/LoginManagerTests.swift new file mode 100644 index 0000000000..917deda0a5 --- /dev/null +++ b/mobile/ios/UITests/LoginManagerTests.swift @@ -0,0 +1,706 @@ +/* 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 Storage +import EarlGrey +@testable import Client + +class LoginManagerTests: KIFTestCase { + + fileprivate var webRoot: String! + + override func setUp() { + super.setUp() + PasscodeUtils.resetPasscode() + webRoot = SimplePageServer.start() + generateLogins() + BrowserUtils.dismissFirstRunUI() + } + + override func tearDown() { + super.tearDown() + clearLogins() + PasscodeUtils.resetPasscode() + BrowserUtils.resetToAboutHome(tester()) + } + + fileprivate func openLoginManager() { + // Wait until the dialog shows up + let menuAppeared = GREYCondition(name: "Wait the Settings dialog to appear", block: { () -> Bool in + var errorOrNil: NSError? + EarlGrey.select(elementWithMatcher: grey_accessibilityLabel("Logins")).assert(grey_notNil(), error: &errorOrNil) + let success = errorOrNil == nil + return success + }) + + if BrowserUtils.iPad() { + EarlGrey.select(elementWithMatcher: grey_accessibilityLabel("Menu")).perform(grey_tap()) + let settings_button = grey_allOf([grey_accessibilityLabel("Settings"), + grey_accessibilityID("menu-Settings")]) + EarlGrey.select(elementWithMatcher: settings_button).perform(grey_tap()) + } else { + let menu_button = grey_allOf([grey_accessibilityLabel("Menu"), + grey_accessibilityID("TabToolbar.menuButton")]) + EarlGrey.select(elementWithMatcher: menu_button).perform(grey_tap()) + EarlGrey.select(elementWithMatcher: grey_text("Settings")).perform(grey_tap()) + } + + let success = menuAppeared?.wait(withTimeout: 20) + GREYAssertTrue(success!, reason: "Failed to display settings dialog") + + EarlGrey.select(elementWithMatcher: grey_accessibilityLabel("Logins")).perform(grey_tap()) + } + + fileprivate func closeLoginManager() { + tester().tapView(withAccessibilityLabel: "Settings") + tester().tapView(withAccessibilityLabel: "Done") + } + + fileprivate func generateLogins() { + let profile = (UIApplication.shared.delegate as! AppDelegate).profile! + + let prefixes = "abcdefghijk" + let numRange = (0..<20) + + let passwords = generateStringListWithFormat("password%@%d", numRange: numRange, prefixes: prefixes) + let hostnames = generateStringListWithFormat("http://%@%d.com", numRange: numRange, prefixes: prefixes) + let usernames = generateStringListWithFormat("%@%d@email.com", numRange: numRange, prefixes: prefixes) + + (0..<(numRange.count * prefixes.characters.count)).forEach { index in + let login = Login(guid: "\(index)", hostname: hostnames[index], username: usernames[index], password: passwords[index]) + login.formSubmitURL = hostnames[index] + profile.logins.addLogin(login).value + } + } + + func waitForMatcher(name: String) { + let matcher = grey_allOf([grey_accessibilityLabel(name), + grey_kindOfClass(NSClassFromString("UICalloutBarButton")!), + grey_sufficientlyVisible()]) + + let menuShown = GREYCondition(name: "Wait for " + name) { + var errorOrNil: NSError? + + EarlGrey.select(elementWithMatcher: matcher).assert(grey_notNil(), error: &errorOrNil) + let success = errorOrNil == nil + return success + } + let success = menuShown?.wait(withTimeout: 10) + GREYAssertTrue(success!, reason: name + " Menu not shown") + EarlGrey.select(elementWithMatcher: matcher).perform(grey_tap()) + } + + fileprivate func generateStringListWithFormat(_ format: String, numRange: CountableRange, prefixes: String) -> [String] { + return prefixes.characters.map { char in + + return numRange.map { num in + return String(format: format, "\(char)", num) + } + } .flatMap { $0 } + } + + fileprivate func clearLogins() { + let profile = (UIApplication.shared.delegate as! AppDelegate).profile! + profile.logins.removeAll().value + } + + func testListFiltering() { + openLoginManager() + + var list = tester().waitForView(withAccessibilityIdentifier: "Login List") as! UITableView + + // Filter by username + tester().waitForView(withAccessibilityLabel: "a0@email.com, http://a0.com") + tester().tapView(withAccessibilityLabel: "Enter Search Mode") + tester().waitForAnimationsToFinish() + + // In simulator, the typing is too fast for the screen to be updated properly + // pausing after 'password' (which all login password contains) to update the screen seems to make the test reliable + tester().enterText(intoCurrentFirstResponder: "k10") + tester().wait(forTimeInterval: 3) // Wait until the table is updated + tester().waitForAnimationsToFinish() + tester().enterText(intoCurrentFirstResponder: "@email.com") + tester().wait(forTimeInterval: 3) // Wait until the table is updated + tester().waitForAnimationsToFinish() + list = tester().waitForView(withAccessibilityIdentifier: "Login List") as! UITableView + tester().waitForView(withAccessibilityLabel: "k10@email.com") + + XCTAssertEqual(list.numberOfRows(inSection: 0), 1) + + tester().tapView(withAccessibilityLabel: "Clear Search") + // Filter by hostname + tester().waitForView(withAccessibilityLabel: "a0@email.com, http://a0.com") + tester().tapView(withAccessibilityLabel: "Enter Search Mode") + tester().waitForAnimationsToFinish() + tester().enterText(intoCurrentFirstResponder: "http://k10") + tester().waitForAnimationsToFinish() + tester().wait(forTimeInterval: 3) // Wait until the table is updated + tester().enterText(intoCurrentFirstResponder: ".com") + tester().wait(forTimeInterval: 3) // Wait until the table is updated + list = tester().waitForView(withAccessibilityIdentifier: "Login List") as! UITableView + tester().waitForView(withAccessibilityLabel: "k10@email.com") + XCTAssertEqual(list.numberOfRows(inSection: 0), 1) + + tester().tapView(withAccessibilityLabel: "Clear Search") + // Filter by password + tester().waitForView(withAccessibilityLabel: "a0@email.com, http://a0.com") + tester().tapView(withAccessibilityLabel: "Enter Search Mode") + tester().waitForAnimationsToFinish() + tester().enterText(intoCurrentFirstResponder: "password") + tester().waitForAnimationsToFinish() + tester().wait(forTimeInterval: 3) // Wait until the table is updated + tester().enterText(intoCurrentFirstResponder: "d9") + tester().waitForAnimationsToFinish() + list = tester().waitForView(withAccessibilityIdentifier: "Login List") as! UITableView + tester().waitForView(withAccessibilityLabel: "d9@email.com") + tester().wait(forTimeInterval: 3) // Wait until the table is updated + XCTAssertEqual(list.numberOfRows(inSection: 0), 1) + + tester().tapView(withAccessibilityLabel: "Clear Search") + // Filter by something that doesn't match anything + tester().waitForView(withAccessibilityLabel: "a0@email.com, http://a0.com") + tester().tapView(withAccessibilityLabel: "Enter Search Mode") + tester().enterText(intoCurrentFirstResponder: "thisdoesntmatch") + tester().waitForView(withAccessibilityIdentifier: "Login List") + + // KIFTest has a bug where waitForViewWithAccessibilityLabel causes the lists to appear again on device, + // so checking the number of rows instead + tester().waitForView(withAccessibilityLabel: "No logins found") + let loginCount = countOfRowsInTableView(list) + XCTAssertEqual(loginCount, 0) + + closeLoginManager() + } + + func testListIndexView() { + openLoginManager() + + // Swipe the index view to navigate to bottom section + tester().waitForAnimationsToFinish() + tester().waitForView(withAccessibilityLabel: "a0@email.com, http://a0.com") + tester().swipeView(withAccessibilityLabel: "table index", in: KIFSwipeDirection.down) + tester().waitForView(withAccessibilityLabel: "k0@email.com, http://k0.com") + closeLoginManager() + } + + + func testDetailPasswordMenuOptions() { + openLoginManager() + + tester().waitForView(withAccessibilityLabel: "a0@email.com, http://a0.com") + tester().tapView(withAccessibilityLabel: "a0@email.com, http://a0.com") + + tester().waitForView(withAccessibilityLabel: "password") + + var passwordField = tester().waitForView(withAccessibilityIdentifier: "passwordField") as! UITextField + XCTAssertTrue(passwordField.isSecureTextEntry) + + // Tap the ‘Reveal’ menu option + EarlGrey.select(elementWithMatcher: grey_accessibilityID("passwordField")).perform(grey_tap()) + waitForMatcher(name: "Reveal") + passwordField = tester().waitForView(withAccessibilityIdentifier: "passwordField") as! UITextField + XCTAssertFalse(passwordField.isSecureTextEntry) + + // Tap the ‘Hide’ menu option + EarlGrey.select(elementWithMatcher: grey_accessibilityID("passwordField")).perform(grey_tap()) + waitForMatcher(name: "Hide") + passwordField = tester().waitForView(withAccessibilityIdentifier: "passwordField") as! UITextField + XCTAssertTrue(passwordField.isSecureTextEntry) + + // Tap the ‘Copy’ menu option + EarlGrey.select(elementWithMatcher: grey_accessibilityID("passwordField")).perform(grey_tap()) + waitForMatcher(name: "Copy") + + tester().tapView(withAccessibilityLabel: "Logins") + closeLoginManager() + XCTAssertEqual(UIPasteboard.general.string, "passworda0") + } + + func testDetailWebsiteMenuCopy() { + openLoginManager() + + tester().waitForView(withAccessibilityLabel: "a0@email.com, http://a0.com") + tester().tapView(withAccessibilityLabel: "a0@email.com, http://a0.com") + + tester().waitForView(withAccessibilityLabel: "password") + + EarlGrey.select(elementWithMatcher: grey_accessibilityID("websiteField")).perform(grey_tap()) + waitForMatcher(name: "Copy") + + // Tap the 'Open & Fill' menu option just checks to make sure we navigate to the web page + EarlGrey.select(elementWithMatcher: grey_accessibilityID("websiteField")).perform(grey_tap()) + waitForMatcher(name: "Open & Fill") + + tester().wait(forTimeInterval: 2) + tester().waitForViewWithAccessibilityValue("a0.com/") + XCTAssertEqual(UIPasteboard.general.string, "http://a0.com") + } + + func testOpenAndFillFromNormalContext() { + openLoginManager() + + tester().waitForView(withAccessibilityLabel: "a0@email.com, http://a0.com") + tester().tapView(withAccessibilityLabel: "a0@email.com, http://a0.com") + + tester().waitForView(withAccessibilityLabel: "password") + + // Tap the 'Open & Fill' menu option just checks to make sure we navigate to the web page + EarlGrey.select(elementWithMatcher: grey_accessibilityID("websiteField")).perform(grey_tap()) + waitForMatcher(name: "Open & Fill") + + tester().wait(forTimeInterval: 5) + tester().waitForViewWithAccessibilityValue("a0.com/") + } + + func testOpenAndFillFromPrivateContext() { + if BrowserUtils.iPad() { + EarlGrey.select(elementWithMatcher: grey_accessibilityID("TopTabsViewController.tabsButton")) + .perform(grey_tap()) + } else { + EarlGrey.select(elementWithMatcher: grey_accessibilityID("TabToolbar.tabsButton")).perform(grey_tap()) + } + EarlGrey.select(elementWithMatcher: grey_accessibilityLabel("Private Mode")).perform(grey_tap()) + EarlGrey.select(elementWithMatcher: grey_accessibilityLabel("Add Tab")).perform(grey_tap()) + EarlGrey.select(elementWithMatcher: grey_accessibilityLabel("Menu")).perform(grey_tap()) + if BrowserUtils.iPad() { + let settings_button = grey_allOf([grey_accessibilityLabel("Settings"), + grey_accessibilityID("menu-Settings")]) + EarlGrey.select(elementWithMatcher: settings_button).perform(grey_tap()) + } else { + EarlGrey.select(elementWithMatcher: grey_text("Settings")).perform(grey_tap()) + } + EarlGrey.select(elementWithMatcher: grey_accessibilityLabel("Logins")).perform(grey_tap()) + + tester().waitForView(withAccessibilityLabel: "a0@email.com, http://a0.com") + tester().tapView(withAccessibilityLabel: "a0@email.com, http://a0.com") + + tester().waitForView(withAccessibilityLabel: "password") + + // Tap the 'Open & Fill' menu option just checks to make sure we navigate to the web page + EarlGrey.select(elementWithMatcher: grey_accessibilityID("websiteField")).perform(grey_tap()) + waitForMatcher(name: "Open & Fill") + + tester().wait(forTimeInterval: 2) + tester().waitForViewWithAccessibilityValue("a0.com/") + } + + func testDetailUsernameMenuOptions() { + openLoginManager() + + tester().waitForView(withAccessibilityLabel: "a0@email.com, http://a0.com") + tester().tapView(withAccessibilityLabel: "a0@email.com, http://a0.com") + + tester().waitForView(withAccessibilityLabel: "password") + + // Tap the 'Open & Fill' menu option just checks to make sure we navigate to the web page + EarlGrey.select(elementWithMatcher: grey_accessibilityID("usernameField")).perform(grey_tap()) + waitForMatcher(name: "Copy") + + tester().tapView(withAccessibilityLabel: "Logins") + closeLoginManager() + XCTAssertEqual(UIPasteboard.general.string!, "a0@email.com") + } + + func testListSelection() { + openLoginManager() + + tester().waitForAnimationsToFinish() + tester().tapView(withAccessibilityLabel: "Edit") + tester().waitForAnimationsToFinish() + + // Select one entry + let firstIndexPath = IndexPath(row: 0, section: 0) + tester().tapRow(at: firstIndexPath, inTableViewWithAccessibilityIdentifier: "Login List") + tester().waitForView(withAccessibilityLabel: "Delete") + + let list = tester().waitForView(withAccessibilityIdentifier: "Login List") as! UITableView + let firstCell = list.cellForRow(at: firstIndexPath)! + XCTAssertTrue(firstCell.isSelected) + + // Deselect first row + tester().tapRow(at: firstIndexPath, inTableViewWithAccessibilityIdentifier: "Login List") + XCTAssertFalse(firstCell.isSelected) + + // Cancel + tester().tapView(withAccessibilityLabel: "Cancel") + tester().waitForView(withAccessibilityLabel: "Edit") + + // Select multiple logins + tester().tapView(withAccessibilityLabel: "Edit") + tester().waitForAnimationsToFinish() + + let pathsToSelect = (0..<3).map { IndexPath(row: $0, section: 0) } + pathsToSelect.forEach { path in + tester().tapRow(at: path, inTableViewWithAccessibilityIdentifier: "Login List") + } + tester().waitForView(withAccessibilityLabel: "Delete") + + pathsToSelect.forEach { path in + XCTAssertTrue(list.cellForRow(at: path)!.isSelected) + } + + // Deselect only first row + tester().tapRow(at: firstIndexPath, inTableViewWithAccessibilityIdentifier: "Login List") + XCTAssertFalse(firstCell.isSelected) + + // Make sure delete is still showing + tester().waitForView(withAccessibilityLabel: "Delete") + + // Deselect the rest + let pathsWithoutFirst = pathsToSelect[1.. Int { + var count = 0 + (0.. +#import +#import +#import +#import + +#endif diff --git a/mobile/ios/UITests/ViewMemoryLeakTests.swift b/mobile/ios/UITests/ViewMemoryLeakTests.swift new file mode 100644 index 0000000000..33838618ce --- /dev/null +++ b/mobile/ios/UITests/ViewMemoryLeakTests.swift @@ -0,0 +1,124 @@ +/* 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 WebKit + +/// This test should be disabled since getTopViewController() crashes with iOS 10 +/// ------------------------------------------------------------------------------ +/// Set of tests that wait for weak references to views to be cleared. Technically, this is +/// non-deterministic and there are no guarantees the references will be set to nil. In practice, +/// though, the references are consistently cleared, which should be good enough for testing. +class ViewMemoryLeakTests: KIFTestCase, UITextFieldDelegate { + fileprivate var webRoot: String! + + override func setUp() { + webRoot = SimplePageServer.start() + } + + override func tearDown() { + do { + try tester().tryFindingTappableView(withAccessibilityLabel: "home") + tester().tapView(withAccessibilityLabel: "home") + } catch _ { + } + BrowserUtils.resetToAboutHome(tester()) + } + /* + func testAboutHomeDisposed() { + // about:home is already active on startup; grab a reference to it. + let browserViewController = getTopViewController() + weak var aboutHomeController = getChildViewController(browserViewController, childClass: "HomePanelViewController") + + // Change the page to make about:home go away. + tester().tapViewWithAccessibilityIdentifier("url") + let url = "\(webRoot)/numberedPage.html?page=1" + + // Work around potential KIF bug. The nillification does not seem to propagate unless we use and explicit autorelease pool. + // https://github.com/kif-framework/KIF/issues/739 + autoreleasepool { + tester().clearTextFromAndThenEnterTextIntoCurrentFirstResponder("\(url)\n") + } + + tester().waitForWebViewElementWithAccessibilityLabel("Page 1") + + + tester().runBlock { _ in + return (aboutHomeController == nil) ? KIFTestStepResult.Success : KIFTestStepResult.Wait + } + XCTAssertNil(aboutHomeController, "about:home controller disposed") + } + + func testSearchViewControllerDisposed() { + // Type the URL to make the search controller appear. + tester().tapViewWithAccessibilityIdentifier("url") + tester().clearTextFromAndThenEnterTextIntoCurrentFirstResponder("foobar") + tester().waitForTimeInterval(0.1) // Wait to make sure that input has propagated through delegate methods + let browserViewController = getTopViewController() + weak var searchViewController = getChildViewController(browserViewController, childClass: "SearchViewController") + XCTAssertNotNil(searchViewController, "Got search controller reference") + + // Submit to close about:home and the search controller. + tester().enterTextIntoCurrentFirstResponder("\n") + + tester().runBlock { _ in + return (searchViewController == nil) ? KIFTestStepResult.Success : KIFTestStepResult.Wait + } + XCTAssertNil(searchViewController, "Search controller disposed") + } + + func testTabTrayDisposed() { + // Enter the tab tray. + tester().tapViewWithAccessibilityLabel("Show Tabs") + tester().waitForViewWithAccessibilityLabel("Tabs Tray") + weak var tabTrayController = getTopViewController() + weak var tabCell = tester().waitForTappableViewWithAccessibilityLabel("home") + XCTAssertNotNil(tabTrayController, "Got tab tray reference") + XCTAssertNotNil(tabCell, "Got tab cell reference") + + // Leave the tab tray. + tester().tapViewWithAccessibilityLabel("home") + + tester().runBlock { _ in + return (tabTrayController == nil) ? KIFTestStepResult.Success : KIFTestStepResult.Wait + } + XCTAssertNil(tabTrayController, "Tab tray controller disposed") + + tester().runBlock { _ in + return (tabCell == nil) ? KIFTestStepResult.Success : KIFTestStepResult.Wait + } + XCTAssertNil(tabCell, "Tab tray cell disposed") + } + + func testWebViewDisposed() { + weak var webView = tester().waitForViewWithAccessibilityLabel("Web content") + XCTAssertNotNil(webView, "webView found") + + tester().tapViewWithAccessibilityLabel("Show Tabs") + let tabsView = tester().waitForViewWithAccessibilityLabel("Tabs Tray").subviews.first as! UICollectionView + let cell = tabsView.cellForItemAtIndexPath(NSIndexPath(forItem: 0, inSection: 0))! + tester().swipeViewWithAccessibilityLabel(cell.accessibilityLabel, inDirection: KIFSwipeDirection.Left) + tester().waitForTappableViewWithAccessibilityLabel("Show Tabs") + + tester().runBlock { _ in + return (webView == nil) ? KIFTestStepResult.Success : KIFTestStepResult.Wait + } + XCTAssertNil(webView, "webView disposed") + } + + private func getTopViewController() -> UIViewController { + //return (UIApplication.sharedApplication().keyWindow!.rootViewController as! UINavigationController).topViewController! + return UIApplication.sharedApplication().keyWindow!.rootViewController! + } + + private func getChildViewController(parent: UIViewController, childClass: String) -> UIViewController { + let childControllers = parent.childViewControllers.filter { child in + let description = NSString(string: child.description) + return description.containsString(childClass) + } + XCTAssertEqual(childControllers.count, 0, "Found 1 child controller of type: \(childClass)") + return childControllers.first! + } + */ +} diff --git a/mobile/ios/UITests/findPage.html b/mobile/ios/UITests/findPage.html new file mode 100644 index 0000000000..e9ed349d48 --- /dev/null +++ b/mobile/ios/UITests/findPage.html @@ -0,0 +1,16 @@ + + + Find Page + + + +
nullam
+ +
Find Page
+

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut ullamcorper dolor nisi, id faucibus enim tincidunt vitae. Aenean felis ex, dignissim sagittis consectetur nec, vehicula ut sem. Aliquam pulvinar egestas dolor, non sagittis sem ultricies lacinia. Praesent vulputate ullamcorper nisl, eu egestas massa laoreet in. Integer consequat, sem eget dignissim suscipit, augue odio convallis eros, in ultricies urna elit egestas purus. Etiam tempor leo justo, at laoreet sem vestibulum ac. Nullam convallis mollis elit, bibendum ultrices ipsum consequat at.

+

Vivamus ornare eros lectus, vel vulputate lorem blandit et. Fusce quis bibendum erat. Quisque pretium mollis lectus pulvinar aliquet. Vivamus ut mi tincidunt, bibendum neque eget, maximus dolor. Cras dictum elementum magna, luctus rutrum est accumsan ac. Quisque aliquam leo fermentum arcu consectetur sagittis. In arcu sapien, ornare in mi sit amet, porta volutpat turpis.

+

Suspendisse blandit enim nec justo tempus pharetra. Donec sagittis dolor malesuada lorem vulputate sagittis. Vivamus eu facilisis ligula. Integer gravida nibh sapien, ut scelerisque lectus faucibus id. Ut gravida nec lacus fringilla interdum. Proin vehicula maximus odio, id dapibus ex faucibus a. Nulla aliquam augue augue, nec lacinia tortor convallis sit amet. Donec id lacus euismod, rutrum mauris sit amet, tempus nisi. Integer a aliquet magna. Quisque non ultrices lacus. Pellentesque quis eros dictum, mollis leo ut, auctor magna. Pellentesque ex erat, eleifend ac nibh et, consequat tempor risus. Proin ornare eros sit amet magna ultricies vulputate. Sed non libero sit amet ante semper ornare. Vivamus vestibulum, mauris ac porttitor rutrum, turpis risus vestibulum mi, vel luctus arcu arcu vitae sapien. Vivamus nibh mi, porttitor vel dui eget, suscipit vulputate mi.

+

Donec nec pulvinar nunc. Sed tristique bibendum sagittis. Donec eget nisi magna. Phasellus lacinia blandit feugiat. Phasellus placerat aliquam odio, a tristique eros. Nulla at neque lectus. Mauris vel commodo odio, eu elementum nulla. Morbi euismod, lacus quis viverra volutpat, eros lacus suscipit dui, vel pretium orci urna nec enim. Integer tincidunt dui sodales consectetur euismod. Vivamus turpis eros, hendrerit sed hendrerit a, tincidunt nec nunc. Nullam quis dui ac ligula faucibus vulputate. Nullam nec dolor ut orci malesuada malesuada. Sed eros lectus, tempor non mauris id, euismod placerat lorem.

+

Quisque eu tincidunt urna. Ut et mattis est. Donec quis elementum orci, et gravida tortor. In eu sagittis mi. Sed in dapibus diam. Integer bibendum, quam in efficitur posuere, leo dui pharetra mauris, nec ultrices arcu nulla nec lorem. Aliquam fringilla leo in luctus blandit. Phasellus lacinia suscipit varius. Sed vel ultrices metus. Sed lacinia malesuada dui, imperdiet ullamcorper libero suscipit eget. Quisque a velit ut orci fringilla eleifend eu at urna. Aenean venenatis enim erat. Aliquam molestie urna non velit consequat volutpat. Sed molestie, risus vitae sodales gravida, purus lacus bibendum libero, id fermentum sem nibh ut magna. Donec facilisis vel tellus in faucibus. Cras ac rhoncus metus.

+ + diff --git a/mobile/ios/UITests/localhostLoad.html b/mobile/ios/UITests/localhostLoad.html new file mode 100644 index 0000000000..6f047dd6d7 --- /dev/null +++ b/mobile/ios/UITests/localhostLoad.html @@ -0,0 +1,23 @@ + + + + + + + + + + + + diff --git a/mobile/ios/UITests/loginForm.html b/mobile/ios/UITests/loginForm.html new file mode 100644 index 0000000000..3b47a88b7d --- /dev/null +++ b/mobile/ios/UITests/loginForm.html @@ -0,0 +1,12 @@ + + + Login form + + +
+ + + +
+ + diff --git a/mobile/ios/UITests/navigationDelegate.html b/mobile/ios/UITests/navigationDelegate.html new file mode 100644 index 0000000000..9a55475641 --- /dev/null +++ b/mobile/ios/UITests/navigationDelegate.html @@ -0,0 +1,8 @@ + + + Test navigation delegate + + +
link + + diff --git a/mobile/ios/UITests/noTitle.html b/mobile/ios/UITests/noTitle.html new file mode 100644 index 0000000000..e08f0a9425 --- /dev/null +++ b/mobile/ios/UITests/noTitle.html @@ -0,0 +1,5 @@ + + +
This page has no title
+ + diff --git a/mobile/ios/UITests/numberedPage.html b/mobile/ios/UITests/numberedPage.html new file mode 100644 index 0000000000..0f6d329716 --- /dev/null +++ b/mobile/ios/UITests/numberedPage.html @@ -0,0 +1,9 @@ + + + + Page {page} + + +
Page {page}
+ + diff --git a/mobile/ios/UITests/readablePage.html b/mobile/ios/UITests/readablePage.html new file mode 100644 index 0000000000..5e12fb2841 --- /dev/null +++ b/mobile/ios/UITests/readablePage.html @@ -0,0 +1,14 @@ + + + + Readable page + + +
Readable Page
+

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut ullamcorper dolor nisi, id faucibus enim tincidunt vitae. Aenean felis ex, dignissim sagittis consectetur nec, vehicula ut sem. Aliquam pulvinar egestas dolor, non sagittis sem ultricies lacinia. Praesent vulputate ullamcorper nisl, eu egestas massa laoreet in. Integer consequat, sem eget dignissim suscipit, augue odio convallis eros, in ultricies urna elit egestas purus. Etiam tempor leo justo, at laoreet sem vestibulum ac. Nullam convallis mollis elit, bibendum ultrices ipsum consequat at.

+

Vivamus ornare eros lectus, vel vulputate lorem blandit et. Fusce quis bibendum erat. Quisque pretium mollis lectus pulvinar aliquet. Vivamus ut mi tincidunt, bibendum neque eget, maximus dolor. Cras dictum elementum magna, luctus rutrum est accumsan ac. Quisque aliquam leo fermentum arcu consectetur sagittis. In arcu sapien, ornare in mi sit amet, porta volutpat turpis.

+

Suspendisse blandit enim nec justo tempus pharetra. Donec sagittis dolor malesuada lorem vulputate sagittis. Vivamus eu facilisis ligula. Integer gravida nibh sapien, ut scelerisque lectus faucibus id. Ut gravida nec lacus fringilla interdum. Proin vehicula maximus odio, id dapibus ex faucibus a. Nulla aliquam augue augue, nec lacinia tortor convallis sit amet. Donec id lacus euismod, rutrum mauris sit amet, tempus nisi. Integer a aliquet magna. Quisque non ultrices lacus. Pellentesque quis eros dictum, mollis leo ut, auctor magna. Pellentesque ex erat, eleifend ac nibh et, consequat tempor risus. Proin ornare eros sit amet magna ultricies vulputate. Sed non libero sit amet ante semper ornare. Vivamus vestibulum, mauris ac porttitor rutrum, turpis risus vestibulum mi, vel luctus arcu arcu vitae sapien. Vivamus nibh mi, porttitor vel dui eget, suscipit vulputate mi.

+

Donec nec pulvinar nunc. Sed tristique bibendum sagittis. Donec eget nisi magna. Phasellus lacinia blandit feugiat. Phasellus placerat aliquam odio, a tristique eros. Nulla at neque lectus. Mauris vel commodo odio, eu elementum nulla. Morbi euismod, lacus quis viverra volutpat, eros lacus suscipit dui, vel pretium orci urna nec enim. Integer tincidunt dui sodales consectetur euismod. Vivamus turpis eros, hendrerit sed hendrerit a, tincidunt nec nunc. Nullam quis dui ac ligula faucibus vulputate. Nullam nec dolor ut orci malesuada malesuada. Sed eros lectus, tempor non mauris id, euismod placerat lorem.

+

Quisque eu tincidunt urna. Ut et mattis est. Donec quis elementum orci, et gravida tortor. In eu sagittis mi. Sed in dapibus diam. Integer bibendum, quam in efficitur posuere, leo dui pharetra mauris, nec ultrices arcu nulla nec lorem. Aliquam fringilla leo in luctus blandit. Phasellus lacinia suscipit varius. Sed vel ultrices metus. Sed lacinia malesuada dui, imperdiet ullamcorper libero suscipit eget. Quisque a velit ut orci fringilla eleifend eu at urna. Aenean venenatis enim erat. Aliquam molestie urna non velit consequat volutpat. Sed molestie, risus vitae sodales gravida, purus lacus bibendum libero, id fermentum sem nibh ut magna. Donec facilisis vel tellus in faucibus. Cras ac rhoncus metus.

+ + diff --git a/mobile/ios/UITests/readerContent.html b/mobile/ios/UITests/readerContent.html new file mode 100644 index 0000000000..ca2623cfe1 --- /dev/null +++ b/mobile/ios/UITests/readerContent.html @@ -0,0 +1,31 @@ + + + + + Reader View Test + + +
+

Lorem

+
+ Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod + tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, + quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo + consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse + cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non + proident, sunt in culpa qui officia deserunt mollit anim id est laborum. +
+

Links

+

link

+

link

+

Foo

+
+ Tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, + quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo + consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse + cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non + proident, sunt in culpa qui officia deserunt mollit anim id est laborum. +
+
+ + \ No newline at end of file diff --git a/mobile/ios/UITests/scrollablePage.html b/mobile/ios/UITests/scrollablePage.html new file mode 100644 index 0000000000..50ad126d10 --- /dev/null +++ b/mobile/ios/UITests/scrollablePage.html @@ -0,0 +1,68 @@ + + + + Scrollable Page {page} + + +
Top
+
    +
  • middle
  • +
  • middle
  • +
  • middle
  • +
  • middle
  • +
  • middle
  • +
  • middle
  • +
  • middle
  • +
  • middle
  • +
  • middle
  • +
  • middle
  • +
  • middle
  • +
  • middle
  • +
  • middle
  • +
  • middle
  • +
  • middle
  • +
  • middle
  • +
  • middle
  • +
  • middle
  • +
  • middle
  • +
  • middle
  • +
  • middle
  • +
  • middle
  • +
  • middle
  • +
  • middle
  • +
  • middle
  • +
  • middle
  • +
  • middle
  • +
  • middle
  • +
  • middle
  • +
  • middle
  • +
  • middle
  • +
  • middle
  • +
  • middle
  • +
  • middle
  • +
  • middle
  • +
  • middle
  • +
  • middle
  • +
  • middle
  • +
  • middle
  • +
  • middle
  • +
  • middle
  • +
  • middle
  • +
  • middle
  • +
  • middle
  • +
  • middle
  • +
  • middle
  • +
  • middle
  • +
  • middle
  • +
  • middle
  • +
  • middle
  • +
  • middle
  • +
  • middle
  • +
  • middle
  • +
  • middle
  • +
  • middle
  • +
  • middle
  • +
+
Bottom
+ + diff --git a/mobile/ios/UITests/youtube.ico b/mobile/ios/UITests/youtube.ico new file mode 100644 index 0000000000..aa914c3301 Binary files /dev/null and b/mobile/ios/UITests/youtube.ico differ diff --git a/mobile/ios/XCUITests/ActivityStreamTest.swift b/mobile/ios/XCUITests/ActivityStreamTest.swift new file mode 100644 index 0000000000..b65073cdc9 --- /dev/null +++ b/mobile/ios/XCUITests/ActivityStreamTest.swift @@ -0,0 +1,378 @@ +/* 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 XCTest + +let defaultTopSite = ["topSiteLabel": "wikipedia", "bookmarkLabel": "Wikipedia"] +let newTopSite = ["url": "www.mozilla.org", "topSiteLabel": "mozilla", "bookmarkLabel": "Internet for people, not profit — Mozilla"] +let allDefaultTopSites = ["facebook", "youtube", "amazon", "wikipedia", "twitter"] + +class ActivityStreamTest: BaseTestCase { + var navigator: Navigator! + var app: XCUIApplication! + + let TopSiteCellgroup = XCUIApplication().collectionViews.cells["TopSitesCell"] + + override func setUp() { + super.setUp() + app = XCUIApplication() + navigator = createScreenGraph(app).navigator(self) + dismissFirstRunUI() + } + + override func tearDown() { + super.tearDown() + } + + func testDefaultSites() { + // There should be 5 top sites by default + checkNumberOfExpectedTopSites(numberOfExpectedTopSites: 5) + } + + func testTopSitesAdd() { + checkNumberOfExpectedTopSites(numberOfExpectedTopSites: 5) + + loadWebPage("http://example.com") + if iPad() { + app.buttons["URLBarView.backButton"].tap() + } else { + app.buttons["TabToolbar.backButton"].tap() + } + navigator.goto(URLBarOpen) + checkNumberOfExpectedTopSites(numberOfExpectedTopSites: 6) + } + + func testTopSitesRemove() { + loadWebPage("http://example.com") + if iPad() { + app.buttons["URLBarView.backButton"].tap() + } else { + app.buttons["TabToolbar.backButton"].tap() + } + navigator.goto(URLBarOpen) + // A new site has been added to the top sites + checkNumberOfExpectedTopSites(numberOfExpectedTopSites: 6) + + TopSiteCellgroup.cells["example"].press(forDuration: 1) //example is the name of the domain. (example.com) + app.tables["Context Menu"].cells["Remove"].tap() + // A top site has been removed + checkNumberOfExpectedTopSites(numberOfExpectedTopSites: 5) + } + + func testTopSitesRemoveDefaultTopSite() { + TopSiteCellgroup.cells[defaultTopSite["topSiteLabel"]!].press(forDuration: 1) + + // Tap on Remove and check that now there should be only 4 default top sites + selectOptionFromContextMenu(option: "Remove") + checkNumberOfExpectedTopSites(numberOfExpectedTopSites: 4) + } + + func testTopSitesRemoveAllDefaultTopSitesAddNewOne() { + // Remove all default Top Sites + for i in allDefaultTopSites { + TopSiteCellgroup.cells[i].press(forDuration: 1) + selectOptionFromContextMenu(option: "Remove") + } + + let numberOfTopSites = TopSiteCellgroup.cells.matching(identifier: "TopSite").count + XCTAssertEqual(numberOfTopSites, 0, "All top sites should have been removed") + + // Open a new page and wait for the completion + navigator.nowAt(HomePanelsScreen) + navigator.goto(URLBarOpen) + app.textFields["address"].typeText(newTopSite["url"]!) + app.textFields["address"].typeText("\r") + waitUntilPageLoad() + + if iPad() { + app.buttons["URLBarView.backButton"].tap() + } else { + app.buttons["TabToolbar.backButton"].tap() + } + waitforExistence(TopSiteCellgroup.cells[newTopSite["topSiteLabel"]!]) + checkNumberOfExpectedTopSites(numberOfExpectedTopSites: 1) + } + + func testTopSitesShiftAfterRemovingOne() { + + // Check top site in first and second cell + let topSiteFirstCell = app.collectionViews.cells.collectionViews.cells.element(boundBy: 0).label + let topSiteSecondCell = app.collectionViews.cells.collectionViews.cells.element(boundBy: 1).label + + XCTAssertTrue(topSiteFirstCell == allDefaultTopSites[0]) + XCTAssertTrue(topSiteSecondCell == allDefaultTopSites[1]) + + // Remove it + let topSiteCells = TopSiteCellgroup.cells + topSiteCells[allDefaultTopSites[0]].press(forDuration: 1) + selectOptionFromContextMenu(option: "Remove") + + // Check top site in first cell now + let topSiteFirstCellAfter = app.collectionViews.cells.collectionViews.cells.element(boundBy: 0).label + XCTAssertTrue(topSiteFirstCellAfter == allDefaultTopSites[1]) + } + + func testTopSitesOpenInNewTab() { + loadWebPage("http://example.com") + if iPad() { + app.buttons["URLBarView.backButton"].tap() + } else { + app.buttons["TabToolbar.backButton"].tap() + } + navigator.goto(URLBarOpen) + TopSiteCellgroup.cells["example"].press(forDuration: 1) + app.tables["Context Menu"].cells["Open in New Tab"].tap() + XCTAssert(TopSiteCellgroup.exists) + XCTAssertFalse(app.staticTexts["example"].exists) + + //URLBarview goBack button + let goBackButton = app.children(matching: .window).element(boundBy: 0).children(matching: .other).element.children(matching: .other).element.children(matching: .other).element.children(matching: .other).element.children(matching: .other).element(boundBy: 1).children(matching: .other).element.children(matching: .button).element(boundBy: 0) + goBackButton.tap() + + if iPad() { + app.buttons["TopTabsViewController.tabsButton"].tap() + } else { + app.buttons["TabToolbar.tabsButton"].tap() + } + + app.cells.element(boundBy: 0).tap() //"Example Domain" + XCTAssertFalse(app.tables["Top sites"].exists) + + let staticTextsQuery = self.app.staticTexts.matching(identifier: "Example Domain") + if staticTextsQuery.count > 0 { + let firstText = staticTextsQuery.element(boundBy: 0) + XCTAssert(firstText.exists) + } + } + + func testTopSitesOpenInNewTabDefaultTopSite() { + // Open one of the sites from Topsites and wait until page is loaded + TopSiteCellgroup.cells[defaultTopSite["topSiteLabel"]!] + .press(forDuration: 1) + selectOptionFromContextMenu(option: "Open in New Tab") + waitUntilPageLoad() + + // Check that two tabs are open and one of them is the default top site one + navigator.goto(TabTray) + waitforExistence(app.collectionViews.cells[defaultTopSite["bookmarkLabel"]!]) + let numTabsOpen = app.collectionViews.cells.count + XCTAssertEqual(numTabsOpen, 2, "New tab not open") + } + + func testTopSitesOpenInNewPrivateTab() { + loadWebPage("http://example.com") + if iPad() { + app.buttons["URLBarView.backButton"].tap() + } else { + app.buttons["TabToolbar.backButton"].tap() + } + navigator.goto(URLBarOpen) + app.collectionViews.cells["TopSitesCell"].cells["example"].press(forDuration: 1) + app.tables["Context Menu"].cells["Open in New Private Tab"].tap() + + XCTAssert(TopSiteCellgroup.exists) + XCTAssertFalse(app.staticTexts["example"].exists) + + //URLBarview goBack button + let goBackButton = app.children(matching: .window).element(boundBy: 0).children(matching: .other).element.children(matching: .other).element.children(matching: .other).element.children(matching: .other).element.children(matching: .other).element(boundBy: 1).children(matching: .other).element.children(matching: .button).element(boundBy: 0) + goBackButton.tap() + if iPad() { + app.buttons["TopTabsViewController.tabsButton"].tap() + } else { + app.buttons["TabToolbar.tabsButton"].tap() + } + + app.buttons["TabTrayController.maskButton"].tap() + app.cells["Example Domain"].tap() + + XCTAssertFalse(TopSiteCellgroup.exists) + XCTAssert(app.staticTexts["Example Domain"].exists) + } + + func testTopSitesOpenInNewPrivateTabDefaultTopSite() { + // Open one of the sites from Topsites and wait until page is loaded + TopSiteCellgroup.cells[defaultTopSite["topSiteLabel"]!] + .press(forDuration: 1) + selectOptionFromContextMenu(option: "Open in New Private Tab") + + // Check that two tabs are open and one of them is the default top site one + navigator.goto(PrivateTabTray) + waitforExistence(app.collectionViews.cells[defaultTopSite["bookmarkLabel"]!]) + let numTabsOpen = app.collectionViews.cells.count + XCTAssertEqual(numTabsOpen, 1, "New tab not open") + } + + func testTopSitesBookmarkDefaultTopSite() { + // Bookmark a default TopSite + TopSiteCellgroup.cells[defaultTopSite["topSiteLabel"]!] + .press(forDuration: 1) + selectOptionFromContextMenu(option: "Bookmark") + + // Check that it appears under Bookmarks menu + navigator.goto(HomePanel_Bookmarks) + XCTAssertTrue(app.tables["Bookmarks List"].staticTexts[defaultTopSite["bookmarkLabel"]!].exists) + + // Check that longtapping on the TopSite gives the option to remove it + navigator.goto(HomePanel_TopSites) + TopSiteCellgroup.cells[defaultTopSite["topSiteLabel"]!] + .press(forDuration: 2) + XCTAssertTrue(app.tables["Context Menu"].cells["Remove Bookmark"].exists) + + // Unbookmark it + selectOptionFromContextMenu(option: "Remove Bookmark") + navigator.goto(HomePanel_Bookmarks) + XCTAssertFalse(app.tables["Bookmarks List"].staticTexts[defaultTopSite["bookmarkLabel"]!].exists) + } + + func testTopSitesBookmarkNewTopSite () { + // Bookmark a new TopSite + navigator.openURL(urlString: newTopSite["url"]!) + waitUntilPageLoad() + if iPad() { + app.buttons["URLBarView.backButton"].tap() + } else { + app.buttons["TabToolbar.backButton"].tap() + } + let topSiteCells = TopSiteCellgroup.cells + waitforExistence(topSiteCells[newTopSite["topSiteLabel"]!]) + topSiteCells[newTopSite["topSiteLabel"]!].press(forDuration: 1) + selectOptionFromContextMenu(option: "Bookmark") + + // Check that it appears under Bookmarks menu + navigator.nowAt(HomePanelsScreen) + navigator.goto(HomePanel_Bookmarks) + XCTAssertTrue(app.tables["Bookmarks List"].staticTexts[newTopSite["bookmarkLabel"]!].exists) + + // Check that longtapping on the TopSite gives the option to remove it + navigator.goto(HomePanel_TopSites) + TopSiteCellgroup.cells[newTopSite["topSiteLabel"]!].press(forDuration: 1) + + // Unbookmark it + selectOptionFromContextMenu(option: "Remove Bookmark") + navigator.goto(HomePanel_Bookmarks) + XCTAssertFalse(app.tables["Bookmarks List"].staticTexts[newTopSite["bookmarkLabel"]!].exists) + } + + func testTopSitesShareDefaultTopSite () { + TopSiteCellgroup.cells[defaultTopSite["topSiteLabel"]!] + .press(forDuration: 1) + + // Tap on Share option and verify that the menu is shown and it is possible to cancel it + selectOptionFromContextMenu(option: "Share") + if !iPad() { + app.buttons["Cancel"].tap() + } + } + + func testTopSitesShareNewTopSite () { + navigator.openURL(urlString: newTopSite["url"]!) + waitUntilPageLoad() + navigator.goto(TabTray) + navigator.goto(HomePanelsScreen) + let topSiteCells = TopSiteCellgroup.cells + waitforExistence(topSiteCells[newTopSite["topSiteLabel"]!]) + topSiteCells[newTopSite["topSiteLabel"]!].press(forDuration: 1) + + // Tap on Share option and verify that the menu is shown and it is possible to cancel it.... + selectOptionFromContextMenu(option: "Share") + if !iPad() { + app.buttons["Cancel"].tap() + } + } + + private func selectOptionFromContextMenu(option: String) { + XCTAssertTrue(app.tables["Context Menu"].cells[option].exists) + app.tables["Context Menu"].cells[option].tap() + } + + private func checkNumberOfExpectedTopSites(numberOfExpectedTopSites: Int) { + waitforExistence(app.cells["TopSitesCell"]) + XCTAssertTrue(app.cells["TopSitesCell"].exists) + let numberOfTopSites = TopSiteCellgroup.cells.matching(identifier: "TopSite").count + XCTAssertEqual(numberOfTopSites, UInt(numberOfExpectedTopSites), "The number of Top Sites is not correct") + } + + func testOpenTopSitesFromContextMenu () { + // Top Sites is shown by default + waitforExistence(app.cells["TopSitesCell"]) + checkNumberOfExpectedTopSites(numberOfExpectedTopSites: 5) + + // Go to a website + navigator.openURL(urlString: newTopSite["url"]!) + waitUntilPageLoad() + + // Go back to Top Sites from context menu + navigator.browserPerformAction(.openTopSitesOption) + checkNumberOfExpectedTopSites(numberOfExpectedTopSites: 6) + } + + func testActivityStreamPages() { + let pagecontrolButton = TopSiteCellgroup.buttons["Next Page"] + XCTAssertFalse(pagecontrolButton.exists, "The Page Control button must not exist. Only 5 elements should be on the page") + + navigator.openURL(urlString: "http://example.com") + waitUntilPageLoad() + waitForValueContains(app.textFields["url"], value: "example.com") + navigator.openURL(urlString: "http://mozilla.org") + waitUntilPageLoad() + waitForValueContains(app.textFields["url"], value: "mozilla.org") + navigator.openURL(urlString: "http://apple.com") + waitUntilPageLoad() + waitForValueContains(app.textFields["url"], value: "apple.com") + navigator.openURL(urlString: "http://slack.com") + waitUntilPageLoad() + waitForValueContains(app.textFields["url"], value: "slack.com") + + if iPad() { + // Test timeout on BB when loading these pages + navigator.openURL(urlString: "http://cvs.com") + waitUntilPageLoad() + waitForValueContains(app.textFields["url"], value: "cvs.com") + navigator.openURL(urlString: "http://linkedin.com") + waitUntilPageLoad() + waitForValueContains(app.textFields["url"], value: "linkedin.com") + navigator.openURL(urlString: "http://zara.com") + waitUntilPageLoad() + waitForValueContains(app.textFields["url"], value: "zara.com") + navigator.openURL(urlString: "http://twitter.com") + waitUntilPageLoad() + waitForValueContains(app.textFields["url"], value: "twitter.com") + navigator.openURL(urlString: "http://instagram.com") + waitUntilPageLoad() + waitForValueContains(app.textFields["url"], value: "instagram.com") + } + navigator.goto(URLBarOpen) + waitforExistence(pagecontrolButton) + XCTAssert(pagecontrolButton.exists, "The Page Control button must exist") + pagecontrolButton.tap() + pagecontrolButton.tap() + let topSiteCells = TopSiteCellgroup.cells + waitforExistence(topSiteCells["example"]) + topSiteCells["example"].press(forDuration: 1) + app.tables["Context Menu"].cells["Remove"].tap() + waitforNoExistence(pagecontrolButton) + XCTAssertFalse(pagecontrolButton.exists, "The Page Control button should disappear after an item is deleted.") + } + + func testContextMenuInLandscape() { + XCUIDevice.shared().orientation = .landscapeLeft + + navigator.openURL(urlString: "http://example.com") + waitUntilPageLoad() + if app.buttons["URLBarView.backButton"].isEnabled { + app.buttons["URLBarView.backButton"].tap() + } else { + app.textFields["url"].tap() + } + TopSiteCellgroup.cells["example"].press(forDuration: 1) + + let contextMenuHeight = app.tables["Context Menu"].frame.size.height + let parentViewHeight = app.otherElements["Action Sheet"].frame.size.height + + XCTAssertLessThanOrEqual(contextMenuHeight, parentViewHeight) + + // Go back to portrait mode + XCUIDevice.shared().orientation = .portrait + } +} diff --git a/mobile/ios/XCUITests/AuthenticationTest.swift b/mobile/ios/XCUITests/AuthenticationTest.swift new file mode 100644 index 0000000000..ed64dd07bb --- /dev/null +++ b/mobile/ios/XCUITests/AuthenticationTest.swift @@ -0,0 +1,347 @@ +/* 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 XCTest + +extension String { + + subscript (i: Int) -> Character { + return self[self.characters.index(self.startIndex, offsetBy: i)] + } + + subscript (i: Int) -> String { + return String(self[i] as Character) + } + + subscript (r: Range) -> String { + let start = characters.index(startIndex, offsetBy: r.lowerBound) + let end = self.index(start, offsetBy: r.upperBound - r.lowerBound) + return self[Range(start ..< end)] + } +} + +class AuthenticationTest: BaseTestCase { + var navigator: Navigator! + var app: XCUIApplication! + + override func setUp() { + super.setUp() + app = XCUIApplication() + navigator = createScreenGraph(app).navigator(self) + continueAfterFailure = false + } + + override func tearDown() { + // Put teardown code here. This method is called after the invocation of each test method in the class. + closeAuthenticationManager() + super.tearDown() + } + + fileprivate func typePasscode(_ passCode: String) { + app.keys[passCode[0]].tap() + app.keys[passCode[1]].tap() + app.keys[passCode[2]].tap() + app.keys[passCode[3]].tap() + app.keys[passCode[4]].tap() + app.keys[passCode[5]].tap() + } + + fileprivate func closeAuthenticationManager() { + navigator.goto(BrowserTab) + } + + fileprivate func disablePasscode(_ passCode: String) { + navigator.goto(PasscodeSettings) + + app.tables["AuthenticationManager.settingsTableView"].staticTexts["Turn Passcode Off"].tap() + waitforExistence(app.staticTexts["Enter passcode"]) + typePasscode(passCode) + waitforExistence(app.tables["AuthenticationManager.settingsTableView"].staticTexts["Turn Passcode On"]) + } + + fileprivate func enablePasscode(_ passCode: String, interval: String = "Immediately") { + let authenticationmanagerSettingstableviewTable = app.tables["AuthenticationManager.settingsTableView"] + + navigator.goto(PasscodeSettings) + app.tables["AuthenticationManager.settingsTableView"].staticTexts["Turn Passcode On"].tap() + waitforExistence(app.staticTexts["Enter a passcode"]) + typePasscode(passCode) + waitforExistence(app.staticTexts["Re-enter passcode"]) + typePasscode(passCode) + waitforExistence(app.tables["AuthenticationManager.settingsTableView"].staticTexts["Turn Passcode Off"]) + navigator.goto(PasscodeIntervalSettings) + typePasscode(passCode) + app.staticTexts[interval].tap() + navigator.goto(PasscodeSettings) + waitforExistence(authenticationmanagerSettingstableviewTable.staticTexts[interval]) + } + + // Sets the passcode and interval (set to immediately) + func testTurnOnOff() { + enablePasscode("133777") + XCTAssertTrue(app.staticTexts["Immediately"].exists) + disablePasscode("133777") + } + + func testChangePassCode() { + enablePasscode("133777") + app.staticTexts["Change Passcode"].tap() + waitforExistence(app.staticTexts["Enter passcode"]) + typePasscode("133777") + waitforExistence(app.staticTexts["Enter a new passcode"]) + typePasscode("233777") + waitforExistence(app.staticTexts["Re-enter passcode"]) + typePasscode("233777") + waitforExistence(app.tables["AuthenticationManager.settingsTableView"].staticTexts["Turn Passcode Off"]) + disablePasscode("233777") + } + + func testPromptPassCodeUponReentry() { + enablePasscode("133777") + navigator.goto(LoginsSettings) + waitforExistence(app.navigationBars["Enter Passcode"]) + typePasscode("133777") + waitforExistence(app.tables["Login List"]) + + //send app to background, and re-enter + XCUIDevice.shared().press(.home) + app.activate() + let contentView = app.navigationBars["Client.FxAContentView"] + if contentView.exists { + app.navigationBars["Client.FxAContentView"].buttons["Settings"].tap() + } + navigator.nowAt("SettingsScreen") + navigator.goto(LoginsSettings) + waitforExistence(app.navigationBars["Enter Passcode"]) + } + + func testPromptPassCodeUponReentryWithDelay() { + enablePasscode("133777", interval: "After 5 minutes") + navigator.goto(LoginsSettings) + waitforExistence(app.navigationBars["Enter Passcode"]) + typePasscode("133777") + waitforExistence(app.tables["Login List"]) + + //send app to background, and re-enter + XCUIDevice.shared().press(.home) + app.activate() + let contentView = app.navigationBars["Client.FxAContentView"] + if contentView.exists { + app.navigationBars["Client.FxAContentView"].buttons["Settings"].tap() + } + + navigator.nowAt("SettingsScreen") + navigator.goto(LoginsSettings) + waitforExistence(app.tables["Login List"]) + } + + func testChangePasscodeShowsErrorStates() { + enablePasscode("133777") + app.staticTexts["Change Passcode"].tap() + waitforExistence(app.staticTexts["Enter passcode"]) + typePasscode("233777") + waitforExistence(app.staticTexts["Incorrect passcode. Try again (Attempts remaining: 2)."]) + typePasscode("333777") + waitforExistence(app.staticTexts["Incorrect passcode. Try again (Attempts remaining: 1)."]) + typePasscode("133777") + waitforExistence(app.staticTexts["Enter a new passcode"]) + + // Enter same passcode as new one + typePasscode("133777") + waitforExistence(app.staticTexts["New passcode must be different than existing code."]) + + // Enter mismatched passcode + typePasscode("233777") + waitforExistence(app.staticTexts["Re-enter passcode"]) + typePasscode("333777") + waitforExistence(app.staticTexts["Passcodes didn’t match. Try again."]) + + // Put proper password + XCTAssertTrue(app.staticTexts["Enter a new passcode"].exists) + typePasscode("233777") + waitforExistence(app.staticTexts["Re-enter passcode"]) + typePasscode("233777") + waitforExistence(app.tables["AuthenticationManager.settingsTableView"].staticTexts["Turn Passcode Off"]) + + disablePasscode("233777") + } + + func testChangeRequirePasscodeInterval() { + enablePasscode("133777") + + let authenticationmanagerSettingstableviewTable = app.tables["AuthenticationManager.settingsTableView"] + navigator.goto(PasscodeIntervalSettings) + waitforExistence(app.navigationBars["Enter Passcode"]) + typePasscode("133777") + waitforExistence(app.staticTexts["Immediately"]) + XCTAssertTrue(app.staticTexts["After 1 minute"].exists) + XCTAssertTrue(app.staticTexts["After 5 minutes"].exists) + XCTAssertTrue(app.staticTexts["After 10 minutes"].exists) + XCTAssertTrue(app.staticTexts["After 15 minutes"].exists) + XCTAssertTrue(app.staticTexts["After 1 hour"].exists) + + app.staticTexts["After 15 minutes"].tap() + navigator.goto(PasscodeSettings) + waitforExistence(authenticationmanagerSettingstableviewTable.staticTexts["After 15 minutes"]) + + // Since we set to 15 min, it shouldn't ask for password again, but it skips verification + // only when timing isn't changed. (could be due to timer reset?) + // For clarification, raised Bug 1325439 + navigator.goto(PasscodeIntervalSettings) + waitforExistence(app.navigationBars["Enter Passcode"]) + typePasscode("133777") + navigator.goto(PasscodeSettings) + waitforExistence(authenticationmanagerSettingstableviewTable.staticTexts["After 15 minutes"]) + disablePasscode("133777") + } + + func testEnteringLoginsUsingPasscode() { + enablePasscode("133777") + + // Enter login + navigator.goto(LoginsSettings) + waitforExistence(app.navigationBars["Enter Passcode"]) + typePasscode("133777") + waitforExistence(app.tables["Login List"]) + navigator.goto(SettingsScreen) + + // Trying again should display passcode screen since we've set the interval to be immediately. + navigator.goto(LoginsSettings) + waitforExistence(app.navigationBars["Enter Passcode"]) + typePasscode("133777") + waitforExistence(app.tables["Login List"]) + disablePasscode("133777") + } + + func testEnteringLoginsUsingPasscodeWithFiveMinutesInterval() { + enablePasscode("133777", interval: "After 5 minutes") + + // now we've changed the timeout, we should prompt next time for passcode. + navigator.goto(LoginsSettings) + waitforExistence(app.navigationBars["Enter Passcode"]) + typePasscode("133777") + waitforExistence(app.tables["Login List"]) + + // Trying again should not display the passcode screen since the interval is 5 minutes + navigator.goto(SettingsScreen) + navigator.goto(LoginsSettings) + waitforExistence(app.tables["Login List"]) + + navigator.goto(PasscodeIntervalSettings) + waitforExistence(app.staticTexts["After 5 minutes"]) + + disablePasscode("133777") + } + + func testEnteringLoginsWithNoPasscode() { + // it is disabled + navigator.goto(PasscodeSettings) + waitforExistence(app.tables["AuthenticationManager.settingsTableView"].staticTexts["Turn Passcode On"]) + + navigator.goto(LoginsSettings) + waitforExistence(app.tables["Login List"]) + } + + func testWrongPasscodeDisplaysAttemptsAndMaxError() { + enablePasscode("133777") + navigator.goto(PasscodeIntervalSettings) + waitforExistence(app.navigationBars["Enter Passcode"]) + typePasscode("133777") + waitforExistence(app.staticTexts["After 5 minutes"]) + app.staticTexts["After 5 minutes"].tap() + navigator.goto(PasscodeSettings) + waitforExistence(app.tables["AuthenticationManager.settingsTableView"].staticTexts["After 5 minutes"]) + + // Enter wrong passcode + navigator.goto(LoginsSettings) + waitforExistence(app.navigationBars["Enter Passcode"]) + typePasscode("233777") + waitforExistence(app.staticTexts["Incorrect passcode. Try again (Attempts remaining: 2)."]) + typePasscode("333777") + waitforExistence(app.staticTexts["Incorrect passcode. Try again (Attempts remaining: 1)."]) + typePasscode("333777") + waitforExistence(app.staticTexts["Maximum attempts reached. Please try again later."]) + } + + func testWrongPasscodeAttemptsPersistAcrossEntryAndConfirmation() { + enablePasscode("133777") + + // Enter wrong passcode on Logins + navigator.goto(LoginsSettings) + waitforExistence(app.navigationBars["Enter Passcode"]) + typePasscode("233777") + waitforExistence(app.staticTexts["Incorrect passcode. Try again (Attempts remaining: 2)."]) + + // Go back to Passcode, and enter a wrong passcode, notice the error count + navigator.goto(PasscodeSettings) + app.staticTexts["Change Passcode"].tap() + waitforExistence(app.staticTexts["Enter passcode"]) + typePasscode("233777") + waitforExistence(app.staticTexts["Incorrect passcode. Try again (Attempts remaining: 1)."]) + app.buttons["Cancel"].tap() + + disablePasscode("133777") + } + + func testChangedPasswordMustBeNew() { + enablePasscode("133777") + app.staticTexts["Change Passcode"].tap() + waitforExistence(app.staticTexts["Enter passcode"]) + typePasscode("133777") + waitforExistence(app.staticTexts["Enter a new passcode"]) + typePasscode("133777") + waitforExistence(app.staticTexts["New passcode must be different than existing code."]) + app.navigationBars["Change Passcode"].buttons["Cancel"].tap() + + disablePasscode("133777") + } + + func testPasscodesMustMatchWhenCreating() { + navigator.goto(PasscodeSettings) + app.tables["AuthenticationManager.settingsTableView"].staticTexts["Turn Passcode On"].tap() + waitforExistence(app.staticTexts["Enter a passcode"]) + typePasscode("133777") + waitforExistence(app.staticTexts["Re-enter passcode"]) + typePasscode("233777") + waitforExistence(app.staticTexts["Passcodes didn’t match. Try again."]) + waitforExistence(app.staticTexts["Enter a passcode"]) + app.buttons["Cancel"].tap() + } + + func testPasscodeMustBeCorrectWhenRemoving() { + enablePasscode("133777") + XCTAssertTrue(app.staticTexts["Immediately"].exists) + app.tables["AuthenticationManager.settingsTableView"].staticTexts["Turn Passcode Off"].tap() + waitforExistence(app.staticTexts["Enter passcode"]) + typePasscode("233777") + + waitforExistence(app.staticTexts["Incorrect passcode. Try again (Attempts remaining: 2)."]) + typePasscode("133777") + waitforExistence(app.tables["AuthenticationManager.settingsTableView"].staticTexts["Turn Passcode On"]) + } + + func testChangingIntervalResetsValidationTimer() { + enablePasscode("133777") + + // Enter login, since the default is 'set immediately,' it will ask for passcode + navigator.goto(LoginsSettings) + waitforExistence(app.navigationBars["Enter Passcode"]) + typePasscode("133777") + waitforExistence(app.tables["Login List"]) + + navigator.goto(PasscodeIntervalSettings) + waitforExistence(app.navigationBars["Enter Passcode"]) + typePasscode("133777") + waitforExistence(app.staticTexts["Immediately"]) + app.staticTexts["After 15 minutes"].tap() + + navigator.goto(PasscodeSettings) + waitforExistence(app.tables["AuthenticationManager.settingsTableView"].staticTexts["After 15 minutes"]) + + // Enter login, since the interval is reset, it will ask for password again + navigator.goto(LoginsSettings) + waitforExistence(app.navigationBars["Enter Passcode"]) + disablePasscode("133777") + } +} diff --git a/mobile/ios/XCUITests/BaseTestCase.swift b/mobile/ios/XCUITests/BaseTestCase.swift new file mode 100644 index 0000000000..4889cf202d --- /dev/null +++ b/mobile/ios/XCUITests/BaseTestCase.swift @@ -0,0 +1,112 @@ +/* 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 XCTest + +class BaseTestCase: XCTestCase { + + override func setUp() { + super.setUp() + continueAfterFailure = false + let app = XCUIApplication() + app.terminate() + restart(app, args: [LaunchArguments.ClearProfile, LaunchArguments.SkipIntro, LaunchArguments.SkipWhatsNew]) + } + + override func tearDown() { + XCUIApplication().terminate() + super.tearDown() + } + + func restart(_ app: XCUIApplication, args: [String] = []) { + XCUIDevice.shared().press(.home) + var launchArguments = [LaunchArguments.Test] + args.forEach { arg in + launchArguments.append(arg) + } + app.launchArguments = launchArguments + app.activate() + } + + //If it is a first run, first run window should be gone + func dismissFirstRunUI() { + let firstRunUI = XCUIApplication().scrollViews["IntroViewController.scrollView"] + + if firstRunUI.exists { + firstRunUI.swipeLeft() + XCUIApplication().buttons["Start Browsing"].tap() + } + } + + func waitforExistence(_ element: XCUIElement, file: String = #file, line: UInt = #line) { + waitFor(element, with: "exists == true", file: file, line: line) + } + + func waitforNoExistence(_ element: XCUIElement, timeoutValue: TimeInterval = 5.0, file: String = #file, line: UInt = #line) { + waitFor(element, with: "exists != true", timeout: timeoutValue, file: file, line: line) + } + + func waitForValueContains(_ element: XCUIElement, value: String, file: String = #file, line: UInt = #line) { + waitFor(element, with: "value CONTAINS '\(value)'", file: file, line: line) + } + + private func waitFor(_ element: XCUIElement, with predicateString: String, description: String? = nil, timeout: TimeInterval = 5.0, file: String, line: UInt) { + let predicate = NSPredicate(format: predicateString) + let expectation = XCTNSPredicateExpectation(predicate: predicate, object: element) + let result = XCTWaiter().wait(for: [expectation], timeout: timeout) + if result != .completed { + let message = description ?? "Expect predicate \(predicateString) for \(element.description)" + self.recordFailure(withDescription: message, inFile: file, atLine: line, expected: false) + } + } + + func loadWebPage(_ url: String, waitForLoadToFinish: Bool = true, file: String = #file, line: UInt = #line) { + let app = XCUIApplication() + UIPasteboard.general.string = url + app.textFields["url"].press(forDuration: 2.0) + app.sheets.element(boundBy: 0).buttons.element(boundBy: 0).tap() + + if waitForLoadToFinish { + let finishLoadingTimeout: TimeInterval = 30 + let progressIndicator = app.progressIndicators.element(boundBy: 0) + waitFor(progressIndicator, + with: "exists != true", + description: "Problem loading \(url)", + timeout: finishLoadingTimeout, + file: file, line: line) + } + } + + func iPad() -> Bool { + if UIDevice.current.userInterfaceIdiom == .pad { + return true + } + return false + } + + func waitUntilPageLoad() { + let app = XCUIApplication() + let progressIndicator = app.progressIndicators.element(boundBy: 0) + + waitforNoExistence(progressIndicator, timeoutValue: 20.0) + } +} + +extension BaseTestCase { + func tabTrayButton(forApp app: XCUIApplication) -> XCUIElement { + return app.buttons["TopTabsViewController.tabsButton"].exists ? app.buttons["TopTabsViewController.tabsButton"] : app.buttons["TabToolbar.tabsButton"] + } +} + +extension XCUIElement { + func tap(force: Bool) { + // There appears to be a bug with tapping elements sometimes, despite them being on-screen and tappable, due to hittable being false. + // See: http://stackoverflow.com/a/33534187/1248491 + if isHittable { + tap() + } else if force { + coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.5)).tap() + } + } +} diff --git a/mobile/ios/XCUITests/BookmarkingTests.swift b/mobile/ios/XCUITests/BookmarkingTests.swift new file mode 100644 index 0000000000..91ec9cec5f --- /dev/null +++ b/mobile/ios/XCUITests/BookmarkingTests.swift @@ -0,0 +1,112 @@ +/* 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 XCTest + +let url_1 = "www.google.com" +let url_2 = ["url": "www.mozilla.org", "bookmarkLabel": "Internet for people, not profit — Mozilla"] + +class BookmarkingTests: BaseTestCase { + var navigator: Navigator! + var app: XCUIApplication! + + override func setUp() { + super.setUp() + app = XCUIApplication() + navigator = createScreenGraph(app).navigator(self) + } + + override func tearDown() { + super.tearDown() + } + + private func bookmark() { + navigator.goto(PageOptionsMenu) + waitforExistence(app.tables.cells["Bookmark This Page"]) + app.tables.cells["Bookmark This Page"].tap() + navigator.nowAt(BrowserTab) + } + + private func unbookmark() { + navigator.goto(PageOptionsMenu) + waitforExistence(app.tables.cells["Remove Bookmark"]) + app.cells["Remove Bookmark"].tap() + navigator.nowAt(BrowserTab) + } + + private func checkBookmarked() { + navigator.goto(PageOptionsMenu) + waitforExistence(app.tables.cells["Remove Bookmark"]) + if iPad() { + app.otherElements["PopoverDismissRegion"].tap() + navigator.nowAt(BrowserTab) + } else { + navigator.goto(BrowserTab) + } + } + + private func checkUnbookmarked() { + navigator.goto(PageOptionsMenu) + waitforExistence(app.tables.cells["Bookmark This Page"]) + if iPad() { + app.otherElements["PopoverDismissRegion"].tap() + navigator.nowAt(BrowserTab) + } else { + navigator.goto(BrowserTab) + } + } + + func testBookmarkingUI() { + // Go to a webpage, and add to bookmarks, check it's added + navigator.createNewTab() + loadWebPage(url_1) + navigator.nowAt(BrowserTab) + bookmark() + checkBookmarked() + + // Load a different page on a new tab, check it's not bookmarked + navigator.createNewTab() + loadWebPage(url_2["url"]!) + navigator.nowAt(BrowserTab) + checkUnbookmarked() + + // Go back, check it's still bookmarked, check it's on bookmarks home panel + navigator.goto(TabTray) + app.collectionViews.cells["Google"].tap() + navigator.nowAt(BrowserTab) + checkBookmarked() + + // Open it, then unbookmark it, and check it's no longer on bookmarks home panel + unbookmark() + checkUnbookmarked() + } + + private func checkEmptyBookmarkList() { + let list = app.tables["Bookmarks List"].cells.count + XCTAssertEqual(list, 0, "There should not be any entry in the bookmarks list") + } + + private func checkItemInBookmarkList() { + let list = app.tables["Bookmarks List"].cells.count + XCTAssertEqual(list, 1, "There should be an entry in the bookmarks list") + XCTAssertTrue(app.tables["Bookmarks List"].staticTexts[url_2["bookmarkLabel"]!].exists) + } + + func testAccessBookmarksFromContextMenu() { + //First time there is not any bookmark + navigator.browserPerformAction(.openBookMarksOption) + checkEmptyBookmarkList() + navigator.nowAt(BrowserTab) + + //Add a bookmark + navigator.createNewTab() + loadWebPage(url_2["url"]!) + navigator.nowAt(BrowserTab) + bookmark() + + //There should be a bookmark + navigator.browserPerformAction(.openBookMarksOption) + checkItemInBookmarkList() + } +} diff --git a/mobile/ios/XCUITests/ClipBoardTests.swift b/mobile/ios/XCUITests/ClipBoardTests.swift new file mode 100644 index 0000000000..7e7d4ab0d4 --- /dev/null +++ b/mobile/ios/XCUITests/ClipBoardTests.swift @@ -0,0 +1,68 @@ +/* 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 XCTest + +class ClipBoardTests: BaseTestCase { + let url = "www.example.com" + var navigator: Navigator! + var app: XCUIApplication! + + override func setUp() { + super.setUp() + app = XCUIApplication() + navigator = createScreenGraph(app).navigator(self) + } + + override func tearDown() { + navigator = nil + app = nil + super.tearDown() + } + + //Check for test url in the browser + func checkUrl() { + let urlTextField = app.textFields["url"] + waitForValueContains(urlTextField, value: "www.example") + } + + //Copy url from the browser + func copyUrl() { + navigator.goto(URLBarOpen) + app.textFields["address"].press(forDuration: 3) + waitforExistence(app.menuItems["Select All"]) + app.menuItems["Select All"].tap() + waitforExistence(app.menuItems["Copy"]) + app.menuItems["Copy"].tap() + app.typeText("\r") + navigator.nowAt(BrowserTab) + } + + //Check copied url is same as in browser + func checkCopiedUrl() { + if let myString = UIPasteboard.general.string { + var value = app.textFields["url"].value as! String + if value.hasPrefix("http") == false { + value = "http://\(value)" + } + XCTAssertNotNil(myString) + XCTAssertEqual(myString, value, "Url matches with the UIPasteboard") + } + } + + // This test is disabled in release, but can still run on master + func testClipboard() { + navigator.openURL(urlString: url) + waitUntilPageLoad() + checkUrl() + copyUrl() + checkCopiedUrl() + + navigator.createNewTab() + navigator.goto(URLBarOpen) + app.textFields["address"].press(forDuration: 3) + app.menuItems["Paste"].tap() + waitForValueContains(app.textFields["address"], value: "www.example.com") + } +} diff --git a/mobile/ios/XCUITests/CopiedLinksTests.swift b/mobile/ios/XCUITests/CopiedLinksTests.swift new file mode 100644 index 0000000000..65a155e8e8 --- /dev/null +++ b/mobile/ios/XCUITests/CopiedLinksTests.swift @@ -0,0 +1,48 @@ +/* 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 XCTest + +class CopiedLinksTests: BaseTestCase { + var navigator: Navigator! + var app: XCUIApplication! + + override func setUp() { + super.setUp() + app = XCUIApplication() + navigator = createScreenGraph(app).navigator(self) + } + + override func tearDown() { + navigator = nil + app = nil + super.tearDown() + } + + // This test is enable Offer to open copied links, when opening firefox + func testCopiedLinks() { + navigator.goto(SettingsScreen) + + //Check Offer to open copied links, when opening firefox is off + let value = app.tables.cells.switches["Offer to Open Copied Links, When Opening Firefox"].value + XCTAssertEqual(value as? String, "0") + + //Switch on, Offer to open copied links, when opening firefox + app.tables.cells.switches["Offer to Open Copied Links, When Opening Firefox"].tap() + + //Check Offer to open copied links, when opening firefox is on + let value2 = app.tables.cells.switches["Offer to Open Copied Links, When Opening Firefox"].value + XCTAssertEqual(value2 as? String, "1") + + app.navigationBars["Settings"]/*@START_MENU_TOKEN@*/.buttons["Done"]/*[[".buttons[\"Done\"]",".buttons[\"AppSettingsTableViewController.navigationItem.leftBarButtonItem\"]"],[[[-1,1],[-1,0]]],[1]]@END_MENU_TOKEN@*/.tap() + + app/*@START_MENU_TOKEN@*/.buttons["TabToolbar.menuButton"]/*[[".buttons[\"Menu\"]",".buttons[\"TabToolbar.menuButton\"]"],[[[-1,1],[-1,0]]],[0]]@END_MENU_TOKEN@*/.tap() + let settingsmenuitemCell = app.tables.cells["Settings"] + settingsmenuitemCell.tap() + + //Check Offer to open copied links, when opening firefox is on + let value3 = app.tables.cells.switches["Offer to Open Copied Links, When Opening Firefox"].value + XCTAssertEqual(value3 as? String, "1") + } +} diff --git a/mobile/ios/XCUITests/DomainAutocompleteTest.swift b/mobile/ios/XCUITests/DomainAutocompleteTest.swift new file mode 100644 index 0000000000..67590df0a5 --- /dev/null +++ b/mobile/ios/XCUITests/DomainAutocompleteTest.swift @@ -0,0 +1,174 @@ +/* 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 XCTest + +let website = ["url": "www.mozilla.org", "value": "www.mozilla.org", "subDomain": "https://www.mozilla.org/en-US/firefox/products"] + +class DomainAutocompleteTest: BaseTestCase { + var navigator: Navigator! + var app: XCUIApplication! + + override func setUp() { + super.setUp() + app = XCUIApplication() + navigator = createScreenGraph(app).navigator(self) + } + + override func tearDown() { + super.tearDown() + } + + func testAutocomplete() { + navigator.openURL(urlString: website["url"]!) + + // Basic autocompletion cases + navigator.goto(URLBarOpen) + app.textFields["address"].typeText("w") + + waitForValueContains(app.textFields["address"], value: website["value"]!) + let value = app.textFields["address"].value + XCTAssertEqual(value as? String, website["value"]!, "Wrong autocompletion") + + // Enter the complete website and check that there is not more text added, just what user typed + app.buttons["Clear text"].tap() + app.textFields["address"].typeText(website["value"]!) + waitForValueContains(app.textFields["address"], value: website["value"]!) + let value2 = app.textFields["address"].value + XCTAssertEqual(value2 as? String, website["value"]!, "Wrong autocompletion") + } + // Test that deleting characters works correctly with autocomplete + func testAutocompleteDeletingChars() { + navigator.openURL(urlString: website["url"]!) + navigator.goto(URLBarOpen) + app.textFields["address"].typeText("www.moz") + + // First delete the autocompleted part + app.textFields["address"].typeText("\u{0008}") + // Then remove an extra char and check that the autocompletion stops working + app.textFields["address"].typeText("\u{0008}") + waitForValueContains(app.textFields["address"], value: "mo") + // Then write another letter and the autocompletion works again + app.textFields["address"].typeText("z") + waitForValueContains(app.textFields["address"], value: "moz") + + let value = app.textFields["address"].value + XCTAssertEqual(value as? String, website["value"]!, "Wrong autocompletion") + } + // Delete the entire string and verify that the home panels are shown again. + func testDeleteEntireString() { + navigator.openURL(urlString: website["url"]!) + navigator.goto(URLBarOpen) + app.textFields["address"].typeText("www.moz") + waitforExistence(app.buttons["Clear text"]) + app.buttons["Clear text"].tap() + + // Check that the address field is empty and that the home panels are shown + let value = app.textFields["address"].value + XCTAssertEqual(value as? String, "", "The url has not been removed correctly") + + waitforExistence(app.buttons["HomePanels.TopSites"]) + XCTAssertFalse(app.buttons["HomePanels.TopSites"].isEnabled) + XCTAssertTrue(app.buttons["HomePanels.Bookmarks"].isEnabled) + } + // Ensure that the scheme is included in the autocompletion. + func testEnsureSchemeIncludedAutocompletion() { + navigator.openURL(urlString: website["url"]!) + waitUntilPageLoad() + navigator.goto(URLBarOpen) + app.textFields["address"].typeText("https") + waitForValueContains(app.textFields["address"], value: "mozilla") + let value = app.textFields["address"].value + XCTAssertEqual(value as? String, "https://www.mozilla.org", "Wrong autocompletion") + } + // Non-matches. + func testNoMatches() { + navigator.openURL(urlString: website["url"]!) + navigator.openURL(urlString: website["subDomain"]!) + navigator.goto(URLBarOpen) + app.textFields["address"].typeText("baz") + let value = app.textFields["address"].value + // Check there is not more text added, just what user typed + XCTAssertEqual(value as? String, "baz", "Wrong autocompletion") + + // Ensure we don't match against TLDs. + app.buttons["Clear text"].tap() + app.textFields["address"].typeText("org") + let value2 = app.textFields["address"].value + // Check there is not more text added, just what user typed + XCTAssertEqual(value2 as? String, "org", "Wrong autocompletion") + + // Ensure we don't match other characters ie: ., :, / + app.buttons["Clear text"].tap() + app.textFields["address"].typeText(".") + let value3 = app.textFields["address"].value + XCTAssertEqual(value3 as? String, ".", "Wrong autocompletion") + + app.buttons["Clear text"].tap() + app.textFields["address"].typeText(":") + let value4 = app.textFields["address"].value + XCTAssertEqual(value4 as? String, ":", "Wrong autocompletion") + + app.buttons["Clear text"].tap() + app.textFields["address"].typeText("/") + let value5 = app.textFields["address"].value + XCTAssertEqual(value5 as? String, "/", "Wrong autocompletion") + + // Ensure we don't match strings that don't start a word. + app.buttons["Clear text"].tap() + app.textFields["address"].typeText("ozilla") + let value6 = app.textFields["address"].value + XCTAssertEqual(value6 as? String, "ozilla", "Wrong autocompletion") + + // Ensure we don't match words outside of the domain + app.buttons["Clear text"].tap() + app.textFields["address"].typeText("products") + let value7 = app.textFields["address"].value + XCTAssertEqual(value7 as? String, "products", "Wrong autocompletion") + } + // Test default domains. + func testDefaultDomains() { + navigator.goto(URLBarOpen) + app.textFields["address"].typeText("a") + waitForValueContains(app.textFields["address"], value: ".com") + let value = app.textFields["address"].value + XCTAssertEqual(value as? String, "amazon.com", "Wrong autocompletion") + + app.buttons["Clear text"].tap() + app.textFields["address"].typeText("an") + waitForValueContains(app.textFields["address"], value: ".com") + let value2 = app.textFields["address"].value + XCTAssertEqual(value2 as? String, "answers.com", "Wrong autocompletion") + + app.buttons["Clear text"].tap() + app.textFields["address"].typeText("anc") + waitForValueContains(app.textFields["address"], value: ".com") + let value3 = app.textFields["address"].value + XCTAssertEqual(value3 as? String, "ancestry.com", "Wrong autocompletion") + } + // Test mixed case autocompletion. + func testMixedCaseAutocompletion() { + navigator.openURL(urlString: website1["url"]!) + navigator.goto(URLBarOpen) + app.textFields["address"].typeText("MoZ") + waitForValueContains(app.textFields["address"], value: ".org") + let value = app.textFields["address"].value + XCTAssertEqual(value as? String, "MoZilla.org", "Wrong autocompletion") + + // Test that leading spaces still show suggestions. + app.buttons["Clear text"].tap() + app.textFields["address"].typeText(" moz") + waitForValueContains(app.textFields["address"], value: ".org") + let value2 = app.textFields["address"].value + XCTAssertEqual(value2 as? String, " mozilla.org", "Wrong autocompletion") + + // Test that trailing spaces do *not* show suggestions. + app.buttons["Clear text"].tap() + app.textFields["address"].typeText(" moz ") + waitForValueContains(app.textFields["address"], value: "moz") + let value3 = app.textFields["address"].value + // No autocompletion, just what user typed + XCTAssertEqual(value3 as? String, " moz ", "Wrong autocompletion") + } +} diff --git a/mobile/ios/XCUITests/FindInPageTest.swift b/mobile/ios/XCUITests/FindInPageTest.swift new file mode 100644 index 0000000000..15d01121cd --- /dev/null +++ b/mobile/ios/XCUITests/FindInPageTest.swift @@ -0,0 +1,123 @@ +/* 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 XCTest + +class FindInPageTests: BaseTestCase { + var navigator: Navigator! + var app: XCUIApplication! + + override func setUp() { + super.setUp() + app = XCUIApplication() + navigator = createScreenGraph(app).navigator(self) + } + + override func tearDown() { + super.tearDown() + } + + private func openFindInPageFromMenu() { + navigator.goto(FindInPage) + + waitforExistence(app.buttons["Next in-page result"]) + waitforExistence(app.buttons["Previous in-page result"]) + XCTAssertTrue(app.textFields[""].exists) + } + + func testFindFromMenu() { + + openFindInPageFromMenu() + + // Enter some text to start finding + app.textFields[""].typeText("Book") + + // Once there are matches, test previous/next buttons + waitforExistence(app.staticTexts["1/6"]) + XCTAssertTrue(app.staticTexts["1/6"].exists) + + let nextInPageResultButton = app.buttons["Next in-page result"] + nextInPageResultButton.tap() + waitforExistence(app.staticTexts["2/6"]) + XCTAssertTrue(app.staticTexts["2/6"].exists) + + nextInPageResultButton.tap() + waitforExistence(app.staticTexts["3/6"]) + XCTAssertTrue(app.staticTexts["3/6"].exists) + + let previousInPageResultButton = app.buttons["Previous in-page result"] + previousInPageResultButton.tap() + + waitforExistence(app.staticTexts["2/6"]) + XCTAssertTrue(app.staticTexts["2/6"].exists) + + previousInPageResultButton.tap() + waitforExistence(app.staticTexts["1/6"]) + XCTAssertTrue(app.staticTexts["1/6"].exists) + + // Tapping on close dismisses the search bar + //app.buttons["Done"].tap() + navigator.goto(BrowserTab) + waitforNoExistence(app.textFields["Book"]) + } + + func testQueryWithNoMatches() { + openFindInPageFromMenu() + + // Try to find text which does not match and check that there are not results + app.textFields["FindInPage.searchField"].typeText("foo") + waitforExistence(app.staticTexts["0/0"]) + XCTAssertTrue(app.staticTexts["0/0"].exists, "There should not be any matches") + } + + func testBarDissapearsWhenReloading() { + openFindInPageFromMenu() + + // Before reloading, it is necessary to hide the keyboard + app.textFields["url"].tap() + app.textFields["address"].typeText("\n") + + // Once the page is reloaded the search bar should not appear + waitforNoExistence(app.textFields[""]) + XCTAssertFalse(app.textFields[""].exists) + } + + func testBarDissapearsWhenOpeningTabsTray() { + openFindInPageFromMenu() + + // Dismiss keyboard + app.otherElements["contentView"].tap() + navigator.nowAt(BrowserTab) + + // Going to tab tray and back to the website hides the search field. + navigator.goto(TabTray) + + waitforExistence(app.collectionViews.cells["The Book of Mozilla"]) + app.collectionViews.cells["The Book of Mozilla"].tap() + XCTAssertFalse(app.textFields[""].exists) + XCTAssertFalse(app.buttons["Next in-page result"].exists) + XCTAssertFalse(app.buttons["Previous in-page result"].exists) + } + + func testFindFromSelection() { + navigator.goto(BrowserTab) + let textToFind = "from" + + // Long press on the word to be found + waitUntilPageLoad() + waitforExistence(app.webViews.staticTexts[textToFind]) + let stringToFind = app.webViews.staticTexts.matching(identifier: textToFind) + let firstStringToFind = stringToFind.element(boundBy: 0) + firstStringToFind.press(forDuration: 3) + + // Find in page is correctly launched, bar with text pre-filled and + // the buttons to find next and previous + waitforExistence(app.menuItems["Find in Page"]) + app.menuItems["Find in Page"].tap() + waitforExistence(app.textFields[textToFind]) + XCTAssertTrue(app.textFields[textToFind].exists, "The bar does not appear with the text selected to be found") + XCTAssertTrue(app.buttons["Previous in-page result"].exists, "Find previus button exists") + XCTAssertTrue(app.buttons["Next in-page result"].exists, "Find next button exists") + } +} diff --git a/mobile/ios/XCUITests/FxScreenGraph.swift b/mobile/ios/XCUITests/FxScreenGraph.swift new file mode 100644 index 0000000000..460ca6d8cb --- /dev/null +++ b/mobile/ios/XCUITests/FxScreenGraph.swift @@ -0,0 +1,476 @@ +/* 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 XCTest + +let FirstRun = "OptionalFirstRun" +let TabTray = "TabTray" +let PrivateTabTray = "PrivateTabTray" +let URLBarOpen = "URLBarOpen" +let PrivateURLBarOpen = "PrivateURLBarOpen" +let BrowserTab = "BrowserTab" +let PrivateBrowserTab = "PrivateBrowserTab" +let BrowserTabMenu = "BrowserTabMenu" +let PageOptionsMenu = "PageOptionsMenu" +let FindInPage = "FindInPage" +let SettingsScreen = "SettingsScreen" +let HomePageSettings = "HomePageSettings" +let PasscodeSettings = "PasscodeSettings" +let PasscodeIntervalSettings = "PasscodeIntervalSettings" +let SearchSettings = "SearchSettings" +let NewTabSettings = "NewTabSettings" +let ClearPrivateDataSettings = "ClearPrivateDataSettings" +let LoginsSettings = "LoginsSettings" +let OpenWithSettings = "OpenWithSettings" +let ShowTourInSettings = "ShowTourInSettings" +let Intro_FxASignin = "Intro_FxASignin" + +let allSettingsScreens = [ + SettingsScreen, + HomePageSettings, + PasscodeSettings, + SearchSettings, + NewTabSettings, + ClearPrivateDataSettings, + LoginsSettings, + OpenWithSettings +] + +let Intro_Welcome = "Intro.Welcome" +let Intro_Search = "Intro.Search" +let Intro_Private = "Intro.Private" +let Intro_Mail = "Intro.Mail" +let Intro_Sync = "Intro.Sync" + +let allIntroPages = [ + Intro_Welcome, + Intro_Search, + Intro_Private, + Intro_Mail, + Intro_Sync +] + +let HomePanelsScreen = "HomePanels" +let PrivateHomePanelsScreen = "PrivateHomePanels" +let HomePanel_TopSites = "HomePanel.TopSites.0" +let HomePanel_Bookmarks = "HomePanel.Bookmarks.1" +let HomePanel_History = "HomePanel.History.2" +let HomePanel_ReadingList = "HomePanel.ReadingList.3" +let P_HomePanel_TopSites = "P_HomePanel.TopSites.0" +let P_HomePanel_Bookmarks = "P_HomePanel.Bookmarks.1" +let P_HomePanel_History = "P_HomePanel.History.2" +let P_HomePanel_ReadingList = "P_HomePanel.ReadingList.3" + +let allHomePanels = [ + HomePanel_Bookmarks, + HomePanel_TopSites, + HomePanel_History, + HomePanel_ReadingList +] +let allPrivateHomePanels = [ + P_HomePanel_Bookmarks, + P_HomePanel_TopSites, + P_HomePanel_History, + P_HomePanel_ReadingList +] + +let ContextMenu_ReloadButton = "ContextMenu_ReloadButton" + +func createScreenGraph(_ app: XCUIApplication, url: String = "https://www.mozilla.org/en-US/book/") -> ScreenGraph { + let map = ScreenGraph() + + let startBrowsingButton = app.buttons["IntroViewController.startBrowsingButton"] + let introScrollView = app.scrollViews["IntroViewController.scrollView"] + map.createScene(FirstRun) { scene in + // We don't yet have conditional edges, so we declare an edge from this node + // to BrowserTab, and then just make it work. + if introScrollView.exists { + + scene.gesture(to: BrowserTab) { + // go find the startBrowsing button on the second page of the intro. + introScrollView.swipeLeft() + startBrowsingButton.tap() + } + + scene.noop(to: allIntroPages[0]) + } else { + scene.noop(to: HomePanelsScreen) + } + } + + // Add the intro screens. + var i = 0 + let introLast = allIntroPages.count - 1 + let introPager = app.scrollViews["IntroViewController.scrollView"] + for intro in allIntroPages { + let prev = i == 0 ? nil : allIntroPages[i - 1] + let next = i == introLast ? nil : allIntroPages[i + 1] + + map.createScene(intro) { scene in + if let prev = prev { + scene.swipeRight(introPager, to: prev) + } + + if let next = next { + scene.swipeLeft(introPager, to: next) + } + + if i > 0 { + scene.tap(startBrowsingButton, to: BrowserTab) + } + } + + i += 1 + } + + map.createScene(URLBarOpen) { scene in + // This is used for opening BrowserTab with default mozilla URL + // For custom URL, should use Navigator.openNewURL or Navigator.openURL. + scene.typeText(url + "\r", into: app.textFields["address"], to: BrowserTab) + scene.tap( app.textFields["url"], to: HomePanelsScreen) + /* + scene.backAction = { + app.buttons["goBack"].tap() + } + */ + } + + map.createScene(PrivateURLBarOpen) { scene in + // This is used for opening BrowserTab with default mozilla URL + // For custom URL, should use Navigator.openNewURL or Navigator.openURL. + scene.typeText(url + "\r", into: app.textFields["address"], to:PrivateBrowserTab) + scene.tap( app.textFields["address"], to: PrivateHomePanelsScreen) + } + + let noopAction = {} + map.createScene(HomePanelsScreen) { scene in + scene.tap(app.buttons["HomePanels.TopSites"], to: HomePanel_TopSites) + scene.tap(app.buttons["HomePanels.Bookmarks"], to: HomePanel_Bookmarks) + scene.tap(app.buttons["HomePanels.History"], to: HomePanel_History) + scene.tap(app.buttons["HomePanels.ReadingList"], to: HomePanel_ReadingList) + + scene.tap(app.textFields["url"], to: URLBarOpen) + scene.tap(app.buttons["TabToolbar.menuButton"], to: BrowserTabMenu) + if map.isiPad() { + scene.tap(app.buttons["TopTabsViewController.tabsButton"], to: TabTray) + } else { + scene.gesture(to: TabTray) { + if (app.buttons["TabToolbar.tabsButton"].exists) { + app.buttons["TabToolbar.tabsButton"].tap() + } else { + app.buttons["URLBarView.tabsButton"].tap() + } + } + } + } + + map.createScene(PrivateHomePanelsScreen) { scene in + scene.tap(app.buttons["HomePanels.TopSites"], to: P_HomePanel_TopSites) + scene.tap(app.buttons["HomePanels.Bookmarks"], to: P_HomePanel_Bookmarks) + scene.tap(app.buttons["HomePanels.History"], to: P_HomePanel_History) + scene.tap(app.buttons["HomePanels.ReadingList"], to: P_HomePanel_ReadingList) + + scene.tap(app.textFields["url"], to: PrivateURLBarOpen) + scene.tap(app.buttons["TabToolbar.menuButton"], to: BrowserTabMenu) + if map.isiPad() { + scene.tap(app.buttons["TopTabsViewController.tabsButton"], to: PrivateTabTray) + } else { + scene.gesture(to: PrivateTabTray) { + if (app.buttons["TabToolbar.tabsButton"].exists) { + app.buttons["TabToolbar.tabsButton"].tap() + } else { + app.buttons["URLBarView.tabsButton"].tap() + } + } + } + } + + allHomePanels.forEach { name in + // Tab panel means that all the home panels are available all the time, so we can + // fake this out by a noop back to the HomePanelsScreen which can get to every other panel. + map.createScene(name) { scene in + scene.backAction = noopAction + } + } + + allPrivateHomePanels.forEach { name in + // Tab panel means that all the home panels are available all the time, so we can + // fake this out by a noop back to the HomePanelsScreen which can get to every other panel. + map.createScene(name) { scene in + scene.backAction = noopAction + } + } + + let closeMenuAction = { + app.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.25)).tap() + } + + let navigationControllerBackAction = { + app.navigationBars.element(boundBy: 0).buttons.element(boundBy: 0).tap() + } + + let cancelBackAction = { + if map.isiPad() { + // There is not Cancel option in iPad this way it is closed + app/*@START_MENU_TOKEN@*/.otherElements["PopoverDismissRegion"]/*[[".otherElements[\"dismiss popup\"]",".otherElements[\"PopoverDismissRegion\"]"],[[[-1,1],[-1,0]]],[0]]@END_MENU_TOKEN@*/.tap() + } else { + app.buttons["PhotonMenu.cancel"].tap() + } + } + + let backBtnBackAction = { + if map.isiPad() { + app.buttons["URLBarView.backButton"].tap() + } + else { + app.buttons["TabToolbar.backButton"].tap() + } + } + + map.createScene(SettingsScreen) { scene in + let table = app.tables["AppSettingsTableViewController.tableView"] + + scene.tap(table.cells["Search"], to: SearchSettings) + scene.tap(table.cells["NewTab"], to: NewTabSettings) + scene.tap(table.cells["Homepage"], to: HomePageSettings) + scene.tap(table.cells["TouchIDPasscode"], to: PasscodeSettings) + scene.tap(table.cells["Logins"], to: LoginsSettings) + scene.tap(table.cells["ClearPrivateData"], to: ClearPrivateDataSettings) + scene.tap(table.cells["OpenWith.Setting"], to: OpenWithSettings) + scene.tap(table.cells["ShowTour"], to: ShowTourInSettings) + + scene.backAction = navigationControllerBackAction + } + + map.createScene(SearchSettings) { scene in + scene.backAction = navigationControllerBackAction + } + + map.createScene(NewTabSettings) { scene in + scene.backAction = navigationControllerBackAction + } + + map.createScene(HomePageSettings) { scene in + scene.backAction = navigationControllerBackAction + } + + map.createScene(PasscodeSettings) { scene in + scene.backAction = navigationControllerBackAction + + scene.tap(app.tables["AuthenticationManager.settingsTableView"].staticTexts["Require Passcode"], to: PasscodeIntervalSettings) + } + + map.createScene(PasscodeIntervalSettings) { scene in + // The test is likely to know what it needs to do here. + // This screen is protected by a passcode and is essentially modal. + scene.gesture(to: PasscodeSettings) { + if app.navigationBars["Require Passcode"].exists { + // Go back, accepting modifications + app.navigationBars["Require Passcode"].buttons["Passcode For Logins"].tap() + } else { + // Cancel + app.navigationBars["Enter Passcode"].buttons["Cancel"].tap() + } + } + } + + map.createScene(LoginsSettings) { scene in + scene.gesture(to: SettingsScreen) { + let loginList = app.tables["Login List"] + if loginList.exists { + app.navigationBars["Logins"].buttons["Settings"].tap() + } else { + app.navigationBars["Enter Passcode"].buttons["Cancel"].tap() + } + } + } + + map.createScene(ClearPrivateDataSettings) { scene in + scene.backAction = navigationControllerBackAction + } + + map.createScene(OpenWithSettings) { scene in + scene.backAction = navigationControllerBackAction + } + + map.createScene(ShowTourInSettings) { scene in + scene.gesture(to: Intro_FxASignin) { + let introScrollView = app.scrollViews["IntroViewController.scrollView"] + for _ in 1...4 { + introScrollView.swipeLeft() + } + app.buttons["Sign in to Firefox"].tap() + } + scene.backAction = { + introScrollView.swipeLeft() + startBrowsingButton.tap() + } + + } + + map.createScene(Intro_FxASignin) { scene in + scene.tap(app.navigationBars["Client.FxAContentView"].buttons.element(boundBy: 0), to: HomePanelsScreen) + } + + map.createScene(PrivateTabTray) { scene in + scene.tap(app.buttons["TabTrayController.addTabButton"], to: PrivateHomePanelsScreen) + scene.tap(app.buttons["TabTrayController.maskButton"], to: TabTray) + } + + map.createScene(TabTray) { scene in + scene.tap(app.buttons["TabTrayController.addTabButton"], to: HomePanelsScreen) + scene.tap(app.buttons["TabTrayController.maskButton"], to: PrivateTabTray) + } + + map.createScene(BrowserTab) { scene in + scene.tap(app.textFields["url"], to: URLBarOpen) + scene.tap(app.buttons["TabToolbar.menuButton"], to: BrowserTabMenu) + if map.isiPad() { + scene.tap(app.buttons["TopTabsViewController.tabsButton"], to: TabTray) + } else { + scene.gesture(to: TabTray) { + if (app.buttons["TabToolbar.tabsButton"].exists) { + app.buttons["TabToolbar.tabsButton"].tap() + } else { + app.buttons["URLBarView.tabsButton"].tap() + } + } + } + scene.tap(app.buttons["TabLocationView.pageOptionsButton"], to: PageOptionsMenu) + + scene.backAction = backBtnBackAction + } + + // make sure after the menu action, navigator.nowAt() is used to set the current state + map.createScene(PageOptionsMenu) {scene in + scene.tap(app.tables["Context Menu"].cells["Find in Page"], to: FindInPage) + scene.backAction = cancelBackAction + scene.dismissOnUse = true + } + + map.createScene(FindInPage) {scene in + scene.tap(app.buttons["FindInPage.close"], to: BrowserTab) + } + + map.createScene(PrivateBrowserTab) { scene in + scene.tap(app.textFields["url"], to: URLBarOpen) + scene.tap(app.buttons["TabToolbar.menuButton"], to: BrowserTabMenu) + if map.isiPad() { + scene.tap(app.buttons["TopTabsViewController.tabsButton"], to: PrivateTabTray) + } else { + scene.gesture(to: PrivateTabTray) { + if (app.buttons["TabToolbar.tabsButton"].exists) { + app.buttons["TabToolbar.tabsButton"].tap() + } else { + app.buttons["URLBarView.tabsButton"].tap() + } + } + } + scene.tap(app.buttons["TabLocationView.pageOptionsButton"], to: PageOptionsMenu) + + scene.backAction = backBtnBackAction + } + + map.createScene(BrowserTabMenu) { scene in + scene.backAction = cancelBackAction + scene.tap(app.tables.cells["Settings"], to: SettingsScreen) + + + scene.dismissOnUse = true + } + + map.initialSceneName = FirstRun + + return map +} +extension ScreenGraph { + + // Checks whether the current device is iPad or non-iPad + func isiPad() -> Bool { + if UIDevice.current.userInterfaceIdiom == .pad { + return true + } + return false + } +} + +extension Navigator { + + // Open a URL. Will use/re-use the first BrowserTab or NewTabScreen it comes to. + func openURL(urlString: String) { + self.goto(URLBarOpen) + let app = XCUIApplication() + app.textFields["address"].typeText(urlString + "\r") + + self.nowAt(BrowserTab) + } + + // Opens a URL in a new tab. + func openNewURL(urlString: String) { + self.goto(TabTray) + createNewTab() + self.openURL(urlString: urlString) + } + + // Closes all Tabs from the option in TabTrayMenu + func closeAllTabs() { + let app = XCUIApplication() + app.buttons["TabTrayController.removeTabsButton"].tap() + app.sheets.buttons["Close All Tabs"].tap() + self.nowAt(HomePanelsScreen) + } + + // Add a new Tab from the New Tab option in Browser Tab Menu + func createNewTab() { + let app = XCUIApplication() + self.goto(TabTray) + app.buttons["TabTrayController.addTabButton"].tap() + self.nowAt(HomePanelsScreen) + } + + // Add Tab(s) from the Tab Tray + func createSeveralTabsFromTabTray(numberTabs: Int) { + for _ in 1...numberTabs { + self.goto(TabTray) + self.goto(HomePanelsScreen) + + } + } + + func browserPerformAction(_ view: BrowserPerformAction) { + let PageMenuOptions = [.toggleBookmarkOption, .addReadingListOption, .copyURLOption, .findInPageOption, .toggleDesktopOption, .pinToTopSitesOption, .sendToDeviceOption, BrowserPerformAction.shareOption] + let BrowserMenuOptions = [.openTopSitesOption, .openBookMarksOption, .openHistoryOption, .openReadingListOption, .toggleHideImages, .toggleNightMode, BrowserPerformAction.openSettingsOption] + + let app = XCUIApplication() + + if PageMenuOptions.contains(view) { + self.goto(PageOptionsMenu) + app.tables["Context Menu"].cells[view.rawValue].tap() + } else if BrowserMenuOptions.contains(view) { + self.goto(BrowserTabMenu) + app.tables["Context Menu"].cells[view.rawValue].tap() + } + } +} +enum BrowserPerformAction: String { + // Page Menu + case toggleBookmarkOption = "menu-Bookmark" + case addReadingListOption = "addToReadingList" + case copyURLOption = "menu-Copy-Link" + case findInPageOption = "menu-FindInPage" + case toggleDesktopOption = "menu-RequestDesktopSite" + case pinToTopSitesOption = "action_pin" + case sendToDeviceOption = "menu-Send-to-Device" + case shareOption = "action_share" + + // Tab Menu + case openTopSitesOption = "menu-panel-TopSites" + case openBookMarksOption = "menu-panel-Bookmarks" + case openHistoryOption = "menu-panel-History" + case openReadingListOption = "menu-panel-ReadingList" + case toggleHideImages = "menu-NoImageMode" + case toggleNightMode = "menu-NightMode" + case openSettingsOption = "menu-Settings" +} diff --git a/mobile/ios/XCUITests/HistoryTests.swift b/mobile/ios/XCUITests/HistoryTests.swift new file mode 100644 index 0000000000..7cab65bc81 --- /dev/null +++ b/mobile/ios/XCUITests/HistoryTests.swift @@ -0,0 +1,44 @@ +/* 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 XCTest + +let webpage = ["url": "www.mozilla.org", "label": "Internet for people, not profit — Mozilla", "value": "mozilla.org"] + +class HistoryTests: BaseTestCase { + var navigator: Navigator! + var app: XCUIApplication! + + override func setUp() { + super.setUp() + app = XCUIApplication() + navigator = createScreenGraph(app).navigator(self) + } + + func testEmptyHistoryListFirstTime() { + // Go to History List from Top Sites and check it is empty + navigator.goto(HomePanel_History) + waitforExistence(app.tables.cells["HistoryPanel.recentlyClosedCell"]) + XCTAssertTrue(app.tables.cells["HistoryPanel.recentlyClosedCell"].exists) + XCTAssertTrue(app.tables.cells["HistoryPanel.syncedDevicesCell"].exists) + XCTAssertFalse(app.tables.otherElements.staticTexts["Today"].exists) + } + + func testOpenHistoryFromBrowserContextMenuOptions() { + navigator.openURL(urlString: webpage["url"]!) + navigator.browserPerformAction(.openHistoryOption) + + // Go to History List from Browser context menu and there should be one entry + waitforExistence(app.tables.cells["HistoryPanel.recentlyClosedCell"]) + XCTAssertTrue(app.tables.otherElements.staticTexts["Today"].exists) + XCTAssertTrue(app.tables.cells.staticTexts[webpage["label"]!].exists) + } + + func testOpenSyncDevices() { + navigator.goto(HomePanel_History) + app.tables.cells["HistoryPanel.syncedDevicesCell"].tap() + waitforExistence(app.tables.cells.staticTexts["Firefox Sync"]) + XCTAssertTrue(app.tables/*@START_MENU_TOKEN@*/.buttons["Sign in"]/*[[".cells.buttons[\"Sign in\"]",".buttons[\"Sign in\"]"],[[[-1,1],[-1,0]]],[0]]@END_MENU_TOKEN@*/.exists) + } +} diff --git a/mobile/ios/XCUITests/HomePageSettingsTest.swift b/mobile/ios/XCUITests/HomePageSettingsTest.swift new file mode 100644 index 0000000000..4cfa8a8e4b --- /dev/null +++ b/mobile/ios/XCUITests/HomePageSettingsTest.swift @@ -0,0 +1,75 @@ +/* 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 XCTest + +class HomePageSettingsTest: BaseTestCase { + var navigator: Navigator! + var app: XCUIApplication! + + override func setUp() { + super.setUp() + app = XCUIApplication() + navigator = createScreenGraph(app).navigator(self) + } + + override func tearDown() { + super.tearDown() + } + + // This test has moved from KIF UITest - accesses google.com instead of local webserver instance + func testCurrentPage() { + navigator.goto(BrowserTab) + + // Accessing https means that it is routed + var currentURL = app.textFields["url"].value as! String + + // Go via the menu, becuase if we go via the TabTray, then we + // won't have a current tab. + navigator.goto(HomePageSettings) + app.tables.staticTexts["Use Current Page"].tap() + + // check the value of the current homepage + let homepageURL = app.tables.textFields["HomePageSettingTextField"].value as? String + + if homepageURL?.hasPrefix("https://") == true { + currentURL = "https://\(currentURL)" + } + XCTAssertEqual(currentURL, homepageURL) + app.tables.staticTexts["Clear"].tap() + + let urlString = app.tables.textFields["HomePageSettingTextField"].value ?? "" + + if iPad() { + XCTAssertEqual("", urlString as! String) + } else { + XCTAssertEqual("Enter a webpage", urlString as! String) + } + } + + /* Disabled due to new Photon UI + // Check whether the toolbar/menu shows homepage icon + func testShowHomePageIcon() { + // Check the Homepage icon is present in menu by default + navigator.goto(BrowserTab) + var currentURL = app.textFields["url"].value as! String + navigator.goto(PageOptionsMenu) + waitforExistence(app.tables.staticTexts["Set as Homepage"]) + app.tables.staticTexts["Set as Homepage"].tap() + app.buttons["Set Homepage"].tap() + + navigator.nowAt(BrowserTab) + navigator.goto(HomePageSettings) + + if currentURL.hasPrefix("https://") == false { + currentURL = "https://\(currentURL)" + } + + // Go to settings, and check the url are matching or not + let value = app.textFields["HomePageSettingTextField"].value as! String + XCTAssertEqual(value, currentURL, + "The webpage typed does not match with the one saved") + } + */ +} diff --git a/mobile/ios/XCUITests/HomePageSettingsUITest.swift b/mobile/ios/XCUITests/HomePageSettingsUITest.swift new file mode 100644 index 0000000000..aa31ded5c6 --- /dev/null +++ b/mobile/ios/XCUITests/HomePageSettingsUITest.swift @@ -0,0 +1,107 @@ +/* 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 XCTest + +let websiteUrl1 = "www.mozilla.org" +let websiteUrl2 = "developer.mozilla.org" +let invalidUrl = "1-2-3" + +class HomePageSettingsUITests: BaseTestCase { + var navigator: Navigator! + var app: XCUIApplication! + + override func setUp() { + super.setUp() + app = XCUIApplication() + navigator = createScreenGraph(app).navigator(self) + } + + override func tearDown() { + super.tearDown() + } + + private func enterWebPageAsHomepage(text: String) { + app.textFields["HomePageSettingTextField"].tap() + app.textFields["HomePageSettingTextField"].typeText(text) + let value = app.textFields["HomePageSettingTextField"].value + XCTAssertEqual(value as? String, text, "The webpage typed does not match with the one saved") + } + + func testTyping() { + navigator.goto(HomePageSettings) + // Enter a webpage + enterWebPageAsHomepage(text: websiteUrl1) + + // Check if it is saved going back and then again to home settings menu + navigator.goto(HomePageSettings) + let valueAfter = app.textFields["HomePageSettingTextField"].value + XCTAssertEqual(valueAfter as? String, websiteUrl1) + + // Check that it is actually set by opening a different website and going to Home + navigator.openURL(urlString: websiteUrl2) + navigator.goto(BrowserTabMenu) + + //Now check open home page should load the previously saved home page + let homePageMenuItem = app.tables["Context Menu"].cells["Open Homepage"] + waitforExistence(homePageMenuItem) + homePageMenuItem.tap() + waitForValueContains(app.textFields["url"], value: websiteUrl1) + } + + func testTypingBadURL() { + navigator.goto(HomePageSettings) + // Enter an invalid Url + enterWebPageAsHomepage(text: invalidUrl) + navigator.goto(SettingsScreen) + // Check that it is not saved + navigator.goto(HomePageSettings) + let valueAfter = app.textFields["HomePageSettingTextField"].value + XCTAssertEqual("Enter a webpage", valueAfter as! String) + + // There is no option to go to Home, instead the website open has the option to be set as HomePageSettings + navigator.openURL(urlString: websiteUrl1) + navigator.goto(BrowserTabMenu) + let homePageMenuItem = app.tables["Context Menu"].cells["Open Homepage"] + XCTAssertFalse(homePageMenuItem.exists) + } + + func testClipboard() { + // Go to a website and copy the url + navigator.openURL(urlString: websiteUrl1) + app.textFields["url"].press(forDuration: 5) + app.buttons["Copy Address"].tap() + + // Go to HomePage settings and paste it using the option Used Copied Link + navigator.goto(HomePageSettings) + XCTAssertTrue(app.cells["Use Copied Link"].isEnabled) + app.cells["Use Copied Link"].tap() + + // Check that the webpage has been correclty copied into the correct field + let value = app.textFields["HomePageSettingTextField"].value + XCTAssertEqual(value as? String, "https://\(websiteUrl1)/en-US/", + "The webpage typed does not match with the one saved") + } + + func testDisabledClipboard() { + // Type an incorrect URL and copy it + navigator.goto(URLBarOpen) + app.textFields["address"].typeText(invalidUrl) + app.textFields["address"].press(forDuration: 5) + app.menuItems["Select All"].tap() + app.menuItems["Copy"].tap() + app.buttons["goBack"].tap() + + // Go to HomePage settings and check that it is not possible to copy it into the set webpage field + navigator.nowAt(BrowserTab) + navigator.goto(HomePageSettings) + waitforExistence(app.staticTexts["Use Copied Link"]) + + // Check that nothing is copied in the Set webpage field + app.cells["Use Copied Link"].tap() + let value = app.textFields["HomePageSettingTextField"].value + + XCTAssertEqual("Enter a webpage", value as! String) + } +} diff --git a/mobile/ios/XCUITests/HomePageUITest.swift b/mobile/ios/XCUITests/HomePageUITest.swift new file mode 100644 index 0000000000..3e86e083f3 --- /dev/null +++ b/mobile/ios/XCUITests/HomePageUITest.swift @@ -0,0 +1,31 @@ +/* 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 XCTest + +class HomePageUITest: BaseTestCase { + var navigator: Navigator! + var app: XCUIApplication! + + override func setUp() { + super.setUp() + app = XCUIApplication() + navigator = createScreenGraph(app).navigator(self) + dismissFirstRunUI() + } + + override func tearDown() { + super.tearDown() + } + + func testSetInternalURLAsHomepage() { + loadWebPage("http://en.m.wikipedia.org/wiki/Main_Page") + app.buttons["Reader View"].tap() + navigator.goto(PageOptionsMenu) + app.cells["Set as Homepage"].tap() + app.buttons["Set Homepage"].tap() + navigator.nowAt(BrowserTab) + XCTAssertTrue(app.alerts.count == 0) + } +} diff --git a/mobile/ios/XCUITests/Info.plist b/mobile/ios/XCUITests/Info.plist new file mode 100644 index 0000000000..9411e4a06e --- /dev/null +++ b/mobile/ios/XCUITests/Info.plist @@ -0,0 +1,24 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + BNDL + CFBundleShortVersionString + 10.6 + CFBundleSignature + ???? + CFBundleVersion + 1 + + diff --git a/mobile/ios/XCUITests/NavigationTest.swift b/mobile/ios/XCUITests/NavigationTest.swift new file mode 100644 index 0000000000..b841f7944d --- /dev/null +++ b/mobile/ios/XCUITests/NavigationTest.swift @@ -0,0 +1,299 @@ +/* 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 XCTest + +let website_1 = ["url": "www.mozilla.org", "label": "Internet for people, not profit — Mozilla", "value": "mozilla.org"] +let website_2 = ["url": "www.example.com", "label": "Example", "value": "example"] + +let urlAddOns = "addons.mozilla.org" + +let requestMobileSiteLabel = "Request Mobile Site" +let requestDesktopSiteLabel = "Request Desktop Site" + +class NavigationTest: BaseTestCase { + var navigator: Navigator! + var app: XCUIApplication! + + override func setUp() { + super.setUp() + app = XCUIApplication() + navigator = createScreenGraph(app).navigator(self) + } + + override func tearDown() { + super.tearDown() + } + + func testNavigation() { + navigator.goto(URLBarOpen) + let urlPlaceholder = "Search or enter address" + XCTAssert(app.textFields["url"].exists) + let defaultValuePlaceholder = app.textFields["url"].placeholderValue! + + // Check the url placeholder text and that the back and forward buttons are disabled + XCTAssert(urlPlaceholder == defaultValuePlaceholder) + if iPad() { + app.buttons["goBack"].tap() + XCTAssertFalse(app.buttons["URLBarView.backButton"].isEnabled) + XCTAssertFalse(app.buttons["Forward"].isEnabled) + app.textFields["url"].tap() + } else { + XCTAssertFalse(app.buttons["TabToolbar.backButton"].isEnabled) + XCTAssertFalse(app.buttons["TabToolbar.forwardButton"].isEnabled) + } + + // Once an url has been open, the back button is enabled but not the forward button + navigator.openURL(urlString: website_1["url"]!) + waitUntilPageLoad() + waitForValueContains(app.textFields["url"], value: website_1["value"]!) + if iPad() { + XCTAssertTrue(app.buttons["URLBarView.backButton"].isEnabled) + XCTAssertFalse(app.buttons["Forward"].isEnabled) + } else { + XCTAssertTrue(app.buttons["TabToolbar.backButton"].isEnabled) + XCTAssertFalse(app.buttons["TabToolbar.forwardButton"].isEnabled) + } + + // Once a second url is open, back button is enabled but not the forward one till we go back to url_1 + navigator.openURL(urlString: website_2["url"]!) + waitUntilPageLoad() + waitForValueContains(app.textFields["url"], value: website_2["value"]!) + if iPad() { + XCTAssertTrue(app.buttons["URLBarView.backButton"].isEnabled) + XCTAssertFalse(app.buttons["Forward"].isEnabled) + // Go back to previous visited web site + app.buttons["URLBarView.backButton"].tap() + } else { + XCTAssertTrue(app.buttons["TabToolbar.backButton"].isEnabled) + XCTAssertFalse(app.buttons["TabToolbar.forwardButton"].isEnabled) + // Go back to previous visited web site + app.buttons["TabToolbar.backButton"].tap() + } + waitUntilPageLoad() + waitForValueContains(app.textFields["url"], value: website_1["value"]!) + + if iPad() { + app.buttons["Forward"].tap() + } else { + // Go forward to next visited web site + app.buttons["TabToolbar.forwardButton"].tap() + } + waitUntilPageLoad() + waitForValueContains(app.textFields["url"], value: website_2["value"]!) + } + + func testTapSignInShowsFxAFromTour() { + // Open FxAccount from tour option in settings menu and go throughout all the screens there + navigator.goto(Intro_FxASignin) + checkFirefoxSyncScreenShown() + + // Go back to NewTabScreen + navigator.goto(HomePanelsScreen) + waitforExistence(app.buttons["HomePanels.TopSites"]) + } + + func testTapSigninShowsFxAFromSettings() { + navigator.goto(SettingsScreen) + // Open FxAccount from settings menu and check the Sign in to Firefox scren + let signInToFirefoxStaticText = app.tables["AppSettingsTableViewController.tableView"].staticTexts["Sign in to Sync"] + signInToFirefoxStaticText.tap() + checkFirefoxSyncScreenShown() + + // After that it is possible to go back to Settings + let settingsButton = app.navigationBars["Client.FxAContentView"].buttons["Settings"] + settingsButton.tap() + } + + func testTapSignInShowsFxAFromRemoteTabPanel() { + navigator.goto(HomePanel_TopSites) + // Open FxAccount from remote tab panel and check the Sign in to Firefox scren + navigator.goto(HomePanel_History) + XCTAssertTrue(app.tables["History List"].staticTexts["Synced Devices"].isEnabled) + app.tables["History List"].staticTexts["Synced Devices"].tap() + app.tables.buttons["Sign in"].tap() + checkFirefoxSyncScreenShown() + app.navigationBars["Client.FxAContentView"].buttons["Done"].tap() + navigator.nowAt(HomePanel_History) + } + + private func checkFirefoxSyncScreenShown() { + waitforExistence(app.webViews.staticTexts["Sign in"]) + XCTAssertTrue(app.webViews.textFields["Email"].exists) + XCTAssertTrue(app.webViews.secureTextFields["Password"].exists) + XCTAssertTrue(app.webViews.buttons["Sign in"].exists) + } + + func testScrollsToTopWithMultipleTabs() { + navigator.goto(TabTray) + navigator.openURL(urlString: website_1["url"]!) + waitForValueContains(app.textFields["url"], value: website_1["value"]!) + + // Element at the TOP. TBChanged once the web page is correclty shown + let topElement = app.webViews.staticTexts["Internet for people, "] + + // Element at the BOTTOM + let bottomElement = app.webViews.links.staticTexts["Contact Us"] + + // Scroll to bottom + bottomElement.tap() + waitUntilPageLoad() + if iPad() { + app.buttons["URLBarView.backButton"].tap() + } else { + app.buttons["TabToolbar.backButton"].tap() + } + waitUntilPageLoad() + + // Scroll to top + topElement.tap() + waitforExistence(topElement) + } + + private func checkMobileView() { + let mobileViewElement = app.webViews.links.staticTexts["View classic desktop site"] + waitforExistence(mobileViewElement) + XCTAssertTrue (mobileViewElement.exists, "Mobile view is not available") + } + + private func checkDesktopView() { + let desktopViewElement = app.webViews.links.staticTexts["View Mobile Site"] + waitforExistence(desktopViewElement) + XCTAssertTrue (desktopViewElement.exists, "Desktop view is not available") + } + + private func clearData() { + navigator.goto(ClearPrivateDataSettings) + app.tables.staticTexts["Clear Private Data"].tap() + app.alerts.buttons["OK"].tap() + navigator.goto(HomePanel_TopSites) + } + + func testToggleBetweenMobileAndDesktopSiteFromSite() { + clearData() + let goToDesktopFromMobile = app.webViews.links.staticTexts["View classic desktop site"] + // Open URL by default in mobile view + navigator.openURL(urlString: urlAddOns) + waitUntilPageLoad() + waitForValueContains(app.textFields["url"], value: urlAddOns) + waitforExistence(goToDesktopFromMobile) + + // From the website go to Desktop view + goToDesktopFromMobile.tap() + waitUntilPageLoad() + checkDesktopView() + + // From the website go back to Mobile view + app.webViews.links.staticTexts["View Mobile Site"].tap() + waitUntilPageLoad() + checkMobileView() + } + + func testToggleBetweenMobileAndDesktopSiteFromMenu() { + clearData() + navigator.openURL(urlString: urlAddOns) + waitUntilPageLoad() + waitForValueContains(app.textFields["url"], value: urlAddOns) + + // Mobile view by default, desktop view should be available + navigator.browserPerformAction(.toggleDesktopOption) + checkDesktopSite() + + // From desktop view it is posible to change to mobile view again + navigator.nowAt(BrowserTab) + navigator.browserPerformAction(.toggleDesktopOption) + checkMobileSite() + } + + private func checkMobileSite() { + app.buttons["TabLocationView.pageOptionsButton"].tap() + waitforExistence(app.tables.cells["menu-RequestDesktopSite"].staticTexts[requestDesktopSiteLabel]) + navigator.goto(BrowserTab) + } + + private func checkDesktopSite() { + app.buttons["TabLocationView.pageOptionsButton"].tap() + waitforExistence(app.tables.cells["menu-RequestDesktopSite"].staticTexts[requestMobileSiteLabel]) + navigator.goto(BrowserTab) + } + + func testNavigationPreservesDesktopSiteOnSameHost() { + clearData() + navigator.openURL(urlString: urlAddOns) + waitUntilPageLoad() + + // Mobile view by default, desktop view should be available + navigator.browserPerformAction(.toggleDesktopOption) + waitforNoExistence(app.tables["Context Menu"]) + checkDesktopView() + + // Select any link to navigate to another site and check if the view is kept in desktop view + waitforExistence(app.webViews.links["Featured ›"]) + app.webViews.links["Featured ›"].tap() + waitUntilPageLoad() + checkDesktopView() + } + + func testReloadPreservesMobileOrDesktopSite() { + clearData() + navigator.openURL(urlString: urlAddOns) + waitUntilPageLoad() + + // Mobile view by default, desktop view should be available + navigator.browserPerformAction(.toggleDesktopOption) + + // After reloading a website the desktop view should be kept + if iPad() { + app.buttons["Reload"].tap() + } else { + app.buttons["TabToolbar.stopReloadButton"].tap() + } + waitForValueContains(app.textFields["url"], value: urlAddOns) + checkDesktopView() + + // From desktop view it is posible to change to mobile view again + navigator.nowAt(BrowserTab) + navigator.browserPerformAction(.toggleDesktopOption) + waitUntilPageLoad() + waitForValueContains(app.textFields["url"], value: urlAddOns) + + // After reloading a website the mobile view should be kept + if iPad() { + app.buttons["Reload"].tap() + } else { + app.buttons["TabToolbar.stopReloadButton"].tap() + } + checkMobileView() + } + + /* Disable test due to bug 1346157, the desktop view is not kept after going back and forward + func testBackForwardNavigationRestoresMobileOrDesktopSite() { + clearData() + let desktopViewElement = app.webViews.links.staticTexts["Mobile"] + + // Open first url and keep it in mobile view + navigator.openURL(urlString: urlAddOns) + waitForValueContains(app.textFields["url"], value: urlAddOns) + checkMobileView() + // Open a second url and change it to desktop view + navigator.openURL(urlString: "www.linkedin.com") + navigator.goto(PageOptionsMenu) + waitforExistence(app.tables.cells["menu-RequestDesktopSite"].staticTexts[requestDesktopSiteLabel]) + app.tables.cells["menu-RequestDesktopSite"].tap() + waitforExistence(desktopViewElement) + XCTAssertTrue (desktopViewElement.exists, "Desktop view is not available") + + // Go back to first url and check that the view is still mobile view + app.buttons["TabToolbar.backButton"].tap() + waitForValueContains(app.textFields["url"], value: urlAddOns) + checkMobileView() + + // Go forward to second url and check that the view is still desktop view + app.buttons["TabToolbar.forwardButton"].tap() + waitForValueContains(app.textFields["url"], value: "www.linkedin.com") + waitforExistence(desktopViewElement) + XCTAssertTrue (desktopViewElement.exists, "Desktop view is not available after coming from another site in mobile view") + } + */ + } diff --git a/mobile/ios/XCUITests/NightModeTests.swift b/mobile/ios/XCUITests/NightModeTests.swift new file mode 100644 index 0000000000..e485a475d2 --- /dev/null +++ b/mobile/ios/XCUITests/NightModeTests.swift @@ -0,0 +1,65 @@ +/* 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 XCTest +class NightModeTests: BaseTestCase { + + var navigator: Navigator! + var app: XCUIApplication! + + override func setUp() { + super.setUp() + app = XCUIApplication() + navigator = createScreenGraph(app).navigator(self) + } + + override func tearDown() { + navigator = nil + app = nil + super.tearDown() + } + + private func nightModeOff() { + navigator.goto(BrowserTabMenu) + app.tables.cells["Disable Night Mode"].tap() + navigator.nowAt(BrowserTab) + } + + private func nightModeOn() { + navigator.goto(BrowserTabMenu) + app.tables.cells["Enable Night Mode"].tap() + navigator.nowAt(BrowserTab) + } + + private func checkNightModeOn() { + navigator.goto(BrowserTabMenu) + waitforExistence(app.tables.cells["Disable Night Mode"]) + navigator.goto(BrowserTab) + } + + private func checkNightModeOff() { + navigator.goto(BrowserTabMenu) + waitforExistence(app.tables.cells["Enable Night Mode"]) + navigator.goto(BrowserTab) + } + + func testNightModeUI() { + let url1 = "www.google.com" + + // Go to a webpage, and select night mode on and off, check it's applied or not + navigator.openNewURL(urlString: url1) + + //turn on the night mode + nightModeOn() + + //checking night mode on or off + checkNightModeOn() + + //turn off the night mode + nightModeOff() + + //checking night mode on or off + checkNightModeOff() + } +} diff --git a/mobile/ios/XCUITests/NoImageTests.swift b/mobile/ios/XCUITests/NoImageTests.swift new file mode 100644 index 0000000000..ae14b2a51d --- /dev/null +++ b/mobile/ios/XCUITests/NoImageTests.swift @@ -0,0 +1,65 @@ +/* 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 XCTest + +class NoImageTests: BaseTestCase { + + var navigator: Navigator! + var app: XCUIApplication! + static let NoImageButtonIdentifier = "menu-NoImageMode" + + override func setUp() { + super.setUp() + app = XCUIApplication() + navigator = createScreenGraph(app).navigator(self) + } + + override func tearDown() { + navigator = nil + app = nil + super.tearDown() + } + + private func showImages() { + navigator.goto(BrowserTabMenu) + app.tables["Context Menu"].cells["menu-NoImageMode"].tap() + navigator.nowAt(BrowserTab) + } + + private func hideImages() { + navigator.goto(BrowserTabMenu) + app.tables["Context Menu"].cells["menu-NoImageMode"].tap() + navigator.nowAt(BrowserTab) + } + + private func checkShowImages() { + navigator.goto(BrowserTabMenu) + waitforExistence(app.tables["Context Menu"].cells["menu-NoImageMode"]) + XCTAssertTrue(app.tables["Context Menu"].cells["menu-NoImageMode"].staticTexts["Show Images"].exists) + navigator.goto(BrowserTab) + } + + private func checkHideImages() { + navigator.goto(BrowserTabMenu) + waitforExistence(app.tables["Context Menu"].cells["menu-NoImageMode"]) + XCTAssertTrue(app.tables["Context Menu"].cells["menu-NoImageMode"].staticTexts["Hide Images"].exists) + navigator.goto(BrowserTab) + } + + // Functionality is tested by UITests/NoImageModeTests, here only the UI is updated properly + func testImageOnOff() { + // Go to a webpage, and select no images or hide images, check it's hidden or not + navigator.openNewURL(urlString: "www.google.com") + waitUntilPageLoad() + + // Select hide images, and check the UI is updated + hideImages() + checkShowImages() + + // Select show images, and check the UI is updated + showImages() + checkHideImages() + } +} diff --git a/mobile/ios/XCUITests/PhotonActionSheetTest.swift b/mobile/ios/XCUITests/PhotonActionSheetTest.swift new file mode 100644 index 0000000000..1e7e07bd67 --- /dev/null +++ b/mobile/ios/XCUITests/PhotonActionSheetTest.swift @@ -0,0 +1,87 @@ +/* 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 XCTest + +class PhotonActionSheetTest: BaseTestCase { + var navigator: Navigator! + var app: XCUIApplication! + + override func setUp() { + super.setUp() + app = XCUIApplication() + navigator = createScreenGraph(app).navigator(self) + } + + func testPinToTop() { + navigator.openURL(urlString: "http://example.com") + waitUntilPageLoad() + // Open Action Sheet + app.buttons["TabLocationView.pageOptionsButton"].tap() + + // Pin the site + app.cells["Pin to Top Sites"].tap() + + // Verify that the site has been pinned + + // Open menu + app.buttons["Menu"].tap() + + // Navigate to top sites + app.cells["Top Sites"].tap() + + waitforExistence(app.cells["TopSite"].firstMatch) + + // Verify that the site is pinned to top + let cell = app.cells["TopSite"].firstMatch + XCTAssertEqual(cell.label, "example") + + // Remove pin + cell.press(forDuration: 2) + app.cells["Remove"].tap() + } + + func testShareOptionIsShown() { + navigator.browserPerformAction(.shareOption) + + // Wait to see the Share options sheet + waitforExistence(app.buttons["Copy"]) + } + + func testShareOptionIsShownFromShortCut() { + navigator.goto(BrowserTab) + waitUntilPageLoad() + app.buttons["TabLocationView.pageOptionsButton"].press(forDuration: 1) + // Wait to see the Share options sheet + waitforExistence(app.buttons["Copy"]) + } + + func testSendToDeviceFromPageOptionsMenu() { + // User not logged in + navigator.browserPerformAction(.sendToDeviceOption) + waitforExistence(app.images["emptySync"]) + XCTAssertTrue(app.staticTexts["You are not signed in to your Firefox Account."].exists) + } + + func testSendToDeviceFromShareOption() { + // Open and Wait to see the Share options sheet + navigator.browserPerformAction(.shareOption) + waitforExistence(app.buttons["More"]) + waitforNoExistence(app.buttons["Send Tab"]) + app.collectionViews.cells/*@START_MENU_TOKEN@*/.collectionViews.containing(.button, identifier:"Copy")/*[[".collectionViews.containing(.button, identifier:\"Create PDF\")",".collectionViews.containing(.button, identifier:\"Print\")",".collectionViews.containing(.button, identifier:\"Copy\")"],[[[-1,2],[-1,1],[-1,0]]],[0]]@END_MENU_TOKEN@*/.buttons["More"].tap() + + // Enable Send Tab + let sendTabButton = app.tables.cells.switches["Send Tab"] + sendTabButton.tap() + app.navigationBars["Activities"].buttons["Done"].tap() + + // Send Tab option appears on the Share options sheet + waitforExistence(app.buttons["Send Tab"]) + app.buttons["Send Tab"].tap() + + // User not logged in + waitforExistence(app.images["emptySync"]) + XCTAssertTrue(app.staticTexts["You are not signed in to your Firefox Account."].exists) + } +} diff --git a/mobile/ios/XCUITests/PrivateBrowsingTest.swift b/mobile/ios/XCUITests/PrivateBrowsingTest.swift new file mode 100644 index 0000000000..b86509f149 --- /dev/null +++ b/mobile/ios/XCUITests/PrivateBrowsingTest.swift @@ -0,0 +1,166 @@ +/* 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 XCTest + +let url1 = "www.mozilla.org" +let url2 = "www.facebook.com" + +let url1Label = "Internet for people, not profit — Mozilla" +let url2Label = "Facebook - Log In or Sign Up" + +class PrivateBrowsingTest: BaseTestCase { + + var navigator: Navigator! + var app: XCUIApplication! + + override func setUp() { + super.setUp() + app = XCUIApplication() + navigator = createScreenGraph(app).navigator(self) + } + + override func tearDown() { + super.tearDown() + } + + func testPrivateTabDoesNotTrackHistory() { + navigator.openURL(urlString: url1) + navigator.goto(BrowserTabMenu) + // Go to History screen + waitforExistence(app.tables.cells["History"]) + app.tables.cells["History"].tap() + navigator.nowAt(BrowserTab) + waitforExistence(app.tables["History List"]) + + XCTAssertTrue(app.tables["History List"].staticTexts[url1Label].exists) + // History without counting Recently Closed and Synced devices + let history = app.tables["History List"].cells.count - 2 + + XCTAssertEqual(history, 1, "History entries in regular browsing do not match") + + // Go to Private browsing to open a website and check if it appears on History + navigator.goto(PrivateTabTray) + navigator.openURL(urlString: url2) + navigator.nowAt(PrivateBrowserTab) + waitForValueContains(app.textFields["url"], value: "facebook") + navigator.goto(BrowserTabMenu) + waitforExistence(app.tables.cells["History"]) + app.tables.cells["History"].tap() + waitforExistence(app.tables["History List"]) + XCTAssertTrue(app.tables["History List"].staticTexts[url1Label].exists) + XCTAssertFalse(app.tables["History List"].staticTexts[url2Label].exists) + + // Open one tab in private browsing and check the total number of tabs + let privateHistory = app.tables["History List"].cells.count - 2 + XCTAssertEqual(privateHistory, 1, "History entries in private browsing do not match") + } + + func testTabCountShowsOnlyNormalOrPrivateTabCount() { + // Open two tabs in normal browsing and check the number of tabs open + navigator.openNewURL(urlString: url1) + waitUntilPageLoad() + navigator.goto(TabTray) + navigator.goto(BrowserTab) + navigator.goto(TabTray) + + waitforExistence(app.collectionViews.cells[url1Label]) + let numTabs = app.collectionViews.cells.count + XCTAssertEqual(numTabs, 3, "The number of regular tabs is not correct") + + // Open one tab in private browsing and check the total number of tabs + navigator.goto(PrivateTabTray) + navigator.goto(URLBarOpen) + waitUntilPageLoad() + navigator.openURL(urlString: url2) + waitUntilPageLoad() + navigator.nowAt(PrivateBrowserTab) + waitForValueContains(app.textFields["url"], value: "facebook") + + navigator.goto(PrivateTabTray) + + waitforExistence(app.collectionViews.cells[url2Label]) + let numPrivTabs = app.collectionViews.cells.count + XCTAssertEqual(numPrivTabs, 1, "The number of private tabs is not correct") + + // Go back to regular mode and check the total number of tabs + navigator.goto(TabTray) + waitforExistence(app.collectionViews.cells[url1Label]) + waitforNoExistence(app.collectionViews.cells[url2Label]) + let numRegularTabs = app.collectionViews.cells.count + XCTAssertEqual(numRegularTabs, 3, "The number of regular tabs is not correct") + } + + func testClosePrivateTabsOptionClosesPrivateTabs() { + // Check that Close Private Tabs when closing the Private Browsing Button is off by default + navigator.goto(SettingsScreen) + let settingsTableView = app.tables["AppSettingsTableViewController.tableView"] + + while settingsTableView.staticTexts["Close Private Tabs"].exists == false { + settingsTableView.swipeUp() + } + + let closePrivateTabsSwitch = settingsTableView.switches["Close Private Tabs, When Leaving Private Browsing"] + + XCTAssertFalse(closePrivateTabsSwitch.isSelected) + + // Open a Private tab + navigator.goto(PrivateTabTray) + navigator.openURL(urlString: url1) + //Wait until the page loads + waitUntilPageLoad() + navigator.nowAt(PrivateBrowserTab) + navigator.goto(PrivateTabTray) + + // Go back to regular browser + navigator.goto(TabTray) + + // Go back to private browsing and check that the tab has not been closed + navigator.goto(PrivateTabTray) + waitforExistence(app.collectionViews.cells[url1Label]) + let numPrivTabs = app.collectionViews.cells.count + XCTAssertEqual(numPrivTabs, 1, "The number of tabs is not correct, the private tab should not have been closed") + + app.collectionViews.cells[url1Label].tap() + navigator.nowAt(PrivateBrowserTab) + // Now the enable the Close Private Tabs when closing the Private Browsing Button + navigator.goto(SettingsScreen) + closePrivateTabsSwitch.tap() + navigator.goto(PrivateBrowserTab) + // Go back to regular browsing and check that the private tab has been closed and that the initial Private Browsing message appears when going back to Private Browsing + navigator.goto(PrivateTabTray) + navigator.goto(TabTray) + navigator.goto(PrivateTabTray) + waitforNoExistence(app.collectionViews.cells[url1Label]) + let numPrivTabsAfterClosing = app.collectionViews.cells.count + XCTAssertEqual(numPrivTabsAfterClosing, 0, "The number of tabs is not correct, the private tab should have been closed") + XCTAssertTrue(app.staticTexts["Private Browsing"].exists, "Private Browsing screen is not shown") + } + + func testPrivateBrowserPanelView() { + // If no private tabs are open, there should be a initial screen with label Private Browsing + navigator.goto(PrivateTabTray) + + XCTAssertTrue(app.staticTexts["Private Browsing"].exists, "Private Browsing screen is not shown") + let numPrivTabsFirstTime = app.collectionViews.cells.count + XCTAssertEqual(numPrivTabsFirstTime, 0, "The number of tabs is not correct, there should not be any private tab yet") + + // If a private tab is open Private Browsing screen is not shown anymore + navigator.goto(PrivateBrowserTab) + //Wait until the page loads + waitUntilPageLoad() + navigator.goto(PrivateTabTray) + + // Go to regular browsing + navigator.goto(TabTray) + + // Go back to private brosing + navigator.goto(PrivateTabTray) + + waitforNoExistence(app.staticTexts["Private Browsing"]) + XCTAssertFalse(app.staticTexts["Private Browsing"].exists, "Private Browsing screen is shown") + let numPrivTabsOpen = app.collectionViews.cells.count + XCTAssertEqual(numPrivTabsOpen, 1, "The number of tabs is not correct, there should be one private tab") + } +} diff --git a/mobile/ios/XCUITests/ReaderViewUITest.swift b/mobile/ios/XCUITests/ReaderViewUITest.swift new file mode 100644 index 0000000000..bee8eab6c3 --- /dev/null +++ b/mobile/ios/XCUITests/ReaderViewUITest.swift @@ -0,0 +1,143 @@ +/* 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 XCTest + +class ReaderViewTest: BaseTestCase { + var navigator: Navigator! + var app: XCUIApplication! + + override func setUp() { + super.setUp() + app = XCUIApplication() + navigator = createScreenGraph(app).navigator(self) + } + + override func tearDown() { + super.tearDown() + } + + func testLoadReaderContent() { + navigator.goto(BrowserTab) + app.buttons["Reader View"].tap() + // The settings of reader view are shown as well as the content of the web site + waitforExistence(app.buttons["Display Settings"]) + XCTAssertTrue(app.webViews.staticTexts["The Book of Mozilla"].exists) + } + + private func addContentToReaderView() { + navigator.goto(BrowserTab) + waitUntilPageLoad() + app.buttons["Reader View"].tap() + waitforExistence(app.buttons["Add to Reading List"]) + app.buttons["Add to Reading List"].tap() + } + + private func checkReadingListNumberOfItems(items: Int) { + waitforExistence(app.tables["ReadingTable"]) + let list = app.tables["ReadingTable"].cells.count + XCTAssertEqual(list, UInt(items), "The number of items in the reading table is not correct") + } + + func testAddToReadingList() { + // Initially reading list is empty + navigator.goto(HomePanel_ReadingList) + + // Check the button is selected (is disabled and the rest bookmarks and so are enabled) + XCTAssertFalse(app.buttons["HomePanels.ReadingList"].isEnabled) + XCTAssertTrue(app.buttons["HomePanels.Bookmarks"].isEnabled) + + checkReadingListNumberOfItems(items: 0) + + // Add item to reading list and check that it appears + addContentToReaderView() + navigator.goto(HomePanel_ReadingList) + waitforExistence(app.buttons["HomePanels.ReadingList"]) + + // Check that there is one item + let savedToReadingList = app.tables["ReadingTable"].cells.staticTexts["The Book of Mozilla"] + XCTAssertTrue(savedToReadingList.exists) + checkReadingListNumberOfItems(items: 1) + } + + func testMarkAsReadAndUreadFromReaderView() { + addContentToReaderView() + + // Mark the content as read, so the mark as unread buttons appear + app.buttons["Mark as Read"].tap() + waitforExistence(app.buttons["Mark as Unread"]) + + // Mark the content as unread, so the mark as read button appear + app.buttons["Mark as Unread"].tap() + waitforExistence(app.buttons["Mark as Read"]) + } + + func testRemoveFromReadingView() { + addContentToReaderView() + // Once the content has been added, remove it + waitforExistence(app.buttons["Remove from Reading List"]) + app.buttons["Remove from Reading List"].tap() + + // Check that instead of the remove icon now it is shown the add to read list + waitforExistence(app.buttons["Add to Reading List"]) + + // Go to reader list view to check that there is not any item there + navigator.goto(HomePanel_ReadingList) + waitforExistence(app.buttons["HomePanels.ReadingList"]) + navigator.goto(HomePanel_ReadingList) + checkReadingListNumberOfItems(items: 0) + } + + func testMarkAsReadAndUnreadFromReadingList() { + addContentToReaderView() + navigator.goto(HomePanel_ReadingList) + waitforExistence(app.buttons["HomePanels.ReadingList"]) + navigator.goto(HomePanel_ReadingList) + + // Check that there is one item + let savedToReadingList = app.tables["ReadingTable"].cells.staticTexts["The Book of Mozilla"] + XCTAssertTrue(savedToReadingList.exists) + + // Mark it as read/unread + savedToReadingList.swipeLeft() + waitforExistence(app.buttons["Mark as Read"]) + app.buttons["Mark as Read"].tap() + savedToReadingList.swipeLeft() + waitforExistence(app.buttons["Mark as Unread"]) + } + + func testRemoveFromReadingList() { + addContentToReaderView() + navigator.goto(HomePanel_ReadingList) + waitforExistence(app.buttons["HomePanels.ReadingList"]) + navigator.goto(HomePanel_ReadingList) + + let savedToReadingList = app.tables["ReadingTable"].cells.staticTexts["The Book of Mozilla"] + savedToReadingList.swipeLeft() + waitforExistence(app.buttons["Remove"]) + + // Remove the item from reading list + app.buttons["Remove"].tap() + XCTAssertFalse(savedToReadingList.exists) + + // Reader list view should be empty + checkReadingListNumberOfItems(items: 0) + } + + func testAddToReadingListFromPageOptionsMenu() { + // First time Reading list is empty + navigator.goto(HomePanel_ReadingList) + checkReadingListNumberOfItems(items: 0) + + // Add item to Reading List from Page Options Menu + navigator.goto(BrowserTab) + waitUntilPageLoad() + navigator.browserPerformAction(.addReadingListOption) + + // Now there should be an item on the list + navigator.nowAt(BrowserTab) + navigator.browserPerformAction(.openReadingListOption) + checkReadingListNumberOfItems(items: 1) + } +} diff --git a/mobile/ios/XCUITests/ScreenGraph.swift b/mobile/ios/XCUITests/ScreenGraph.swift new file mode 100644 index 0000000000..6ff28b8954 --- /dev/null +++ b/mobile/ios/XCUITests/ScreenGraph.swift @@ -0,0 +1,388 @@ +/* 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/. */ + +/** + * ScreenGraph helps you get rid of the navigation boiler plate found in a lot of whole-application UI testing. + * + * You create a shared graph of UI 'screens' or 'scenes' for your app, and use it for every test. + * + * In your tests, you use a navigator which does the job of getting your tests from place to place in your application, + * leaving you to concentrate on testing, rather than maintaining brittle and duplicated navigation code. + * + * The shared graph may also have other uses, such as generating screen shots for the App Store or L10n translators. + * + * Under the hood, the ScreenGraph is using GameplayKit's path finding to do the heavy lifting. + */ + +import Foundation +import GameplayKit +import XCTest + +typealias Edge = (XCTestCase, String, UInt) -> Void +typealias SceneBuilder = (ScreenGraphNode) -> Void +typealias NodeVisitor = (String) -> Void + +/** + * ScreenGraph + * This is the main interface to building a graph of screens/app states and how to navigate between them. + * The ScreenGraph will be used as a map to navigate the test agent around the app. + */ +open class ScreenGraph { + var initialSceneName: String? + + var namedScenes: [String: ScreenGraphNode] = [:] + var nodedScenes: [GKGraphNode: ScreenGraphNode] = [:] + + var isReady: Bool = false + + let gkGraph: GKGraph + + init() { + self.gkGraph = GKGraph() + } +} + +extension ScreenGraph { + /** + * Method for creating a ScreenGraphNode in the graph. The node should be accompanied by a closure + * used to document the exits out of this node to other nodes. + */ + func createScene(_ name: String, file: String = #file, line: UInt = #line, builder: @escaping (ScreenGraphNode) -> Void) { + let scene = ScreenGraphNode(map: self, name: name, builder: builder) + scene.file = file + scene.line = line + namedScenes[name] = scene + nodedScenes[scene.gkNode] = scene + } +} + +extension ScreenGraph { + /** + * Create a new navigator object. Navigator objects are the main way of getting around the app. + * Typically, you'll do this in `TestCase.setUp()` + */ + func navigator(_ xcTest: XCTestCase, startingAt: String? = nil, file: String = #file, line: UInt = #line) -> Navigator { + buildGkGraph() + var current: ScreenGraphNode? + if let name = startingAt ?? initialSceneName { + current = namedScenes[name] + } + + if current == nil { + xcTest.recordFailure(withDescription: "The app's initial state couldn't be established.", + inFile: file, atLine: line, expected: false) + } + return Navigator(self, xcTest: xcTest, initialScene: current!) + } + + fileprivate func buildGkGraph() { + if isReady { + return + } + + isReady = true + + // Construct all the GKGraphNodes, and add them to the GKGraph. + let scenes = namedScenes.values + gkGraph.add(scenes.map { $0.gkNode }) + + // Now, use the scene builders to collect edge actions and destinations. + scenes.forEach { scene in + scene.builder(scene) + } + + scenes.forEach { scene in + let gkNodes = scene.edges.keys.flatMap { self.namedScenes[$0]?.gkNode } as [GKGraphNode] + scene.gkNode.addConnections(to: gkNodes, bidirectional: false) + } + } +} + +typealias Gesture = () -> Void + +/** + * The ScreenGraph is made up of nodes. It is not possible to init these directly, only by creating + * screen nodes from the ScreenGraph object. + * + * The ScreenGraphNode has all the methods needed to define edges from this node to another node, using the usual + * XCUIElement method of moving about. + */ +class ScreenGraphNode { + let name: String + fileprivate let builder: SceneBuilder + fileprivate let gkNode: GKGraphNode + fileprivate var edges: [String: Edge] = [:] + + fileprivate weak var map: ScreenGraph? + + // Iff this node has a backAction, this store temporarily stores + // the node we were at before we got to this one. This becomes the node we return to when the backAction is + // invoked. + fileprivate weak var returnNode: ScreenGraphNode? + + fileprivate var hasBack: Bool { + return backAction != nil + } + + /** + * This is an action that will cause us to go back from where we came from. + * This is most useful when the same screen is accessible from multiple places, + * and we have a back button to return to where we came from. + */ + var backAction: Gesture? + + /** + * This flag indicates that once we've moved on from this node, we can't come back to + * it via `backAction`. This is especially useful for Menus, and dialogs. + */ + var dismissOnUse: Bool = false + + var existsWhen: XCUIElement? + + fileprivate var line: UInt! + + fileprivate var file: String! + + fileprivate init(map: ScreenGraph, name: String, builder: @escaping SceneBuilder) { + self.map = map + self.name = name + self.gkNode = GKGraphNode() + self.builder = builder + } + + fileprivate func addEdge(_ dest: String, by edge: @escaping Edge) { + edges[dest] = edge + // by this time, we should've added all nodes in to the gkGraph. + + assert(map?.namedScenes[dest] != nil, "Destination scene '\(dest)' has not been created anywhere") + } +} + +private let existsPredicate = NSPredicate(format: "exists == true") +private let enabledPredicate = NSPredicate(format: "enabled == true") +private let hittablePredicate = NSPredicate(format: "hittable == true") +private let noopNodeVisitor: NodeVisitor = { _ in } + +extension ScreenGraphNode { + fileprivate func waitForElement(_ element: XCUIElement, withTest xcTest: XCTestCase, notFoundHandler: () -> Void) { + // Wait for 5 seconds for the element to exist. + let expectation = XCTNSPredicateExpectation(predicate: existsPredicate, object: element) + let result = XCTWaiter().wait(for: [expectation], timeout: 5) + if result != .completed { + notFoundHandler() + } + } +} + +// Public methods for defining edges out of this node. +extension ScreenGraphNode { + /** + * Declare that by performing the given action/gesture, then we can navigate from this node to the next. + * + * @param withElement – optional, but if provided will attempt to verify it is there before performing the action. + * @param to – the destination node. + */ + func gesture(withElement element: XCUIElement? = nil, to nodeName: String, file declFile: String = #file, line declLine: UInt = #line, g: @escaping () -> Void) { + addEdge(nodeName) { xcTest, file, line in + if let el = element { + self.waitForElement(el, withTest: xcTest) { _ in + xcTest.recordFailure(withDescription: "Cannot find \(el)", inFile: declFile, atLine: declLine, expected: false) + xcTest.recordFailure(withDescription: "Cannot get from \(self.name) to \(nodeName). See \(declFile)", inFile: file, atLine: line, expected: false) + } + } + g() + } + } + + func noop(to nodeName: String, file: String = #file, line: UInt = #line) { + self.gesture(to: nodeName, file: file, line: line) { + // NOOP. + } + } + + /** + * Declare that by tapping a given element, we should be able to navigate from this node to another. + * + * @param element - the element to tap + * @param to – the destination node. + */ + func tap(_ element: XCUIElement, to nodeName: String, file: String = #file, line: UInt = #line) { + self.gesture(withElement: element, to: nodeName, file: file, line: line) { + element.tap() + } + } + + func doubleTap(_ element: XCUIElement, to nodeName: String, file: String = #file, line: UInt = #line) { + self.gesture(withElement: element, to: nodeName, file: file, line: line) { + element.doubleTap() + } + } + + func typeText(_ text: String, into element: XCUIElement, to nodeName: String, file: String = #file, line: UInt = #line) { + self.gesture(withElement: element, to: nodeName, file: file, line: line) { + element.typeText(text) + } + } + + func swipeLeft(_ element: XCUIElement, to nodeName: String, file: String = #file, line: UInt = #line) { + self.gesture(withElement: element, to: nodeName, file: file, line: line) { + element.swipeLeft() + } + } + + func swipeRight(_ element: XCUIElement, to nodeName: String, file: String = #file, line: UInt = #line) { + self.gesture(withElement: element, to: nodeName, file: file, line: line) { + element.swipeRight() + } + } + + func swipeUp(_ element: XCUIElement, to nodeName: String, file: String = #file, line: UInt = #line) { + self.gesture(withElement: element, to: nodeName, file: file, line: line) { + element.swipeUp() + } + } + + func swipeDown(_ element: XCUIElement, to nodeName: String, file: String = #file, line: UInt = #line) { + self.gesture(withElement: element, to: nodeName, file: file, line: line) { + element.swipeDown() + } + } +} + +/** + * The Navigator provides a set of methods to navigate around the app. You can `goto` nodes, `visit` multiple nodes, + * or visit all nodes, but mostly you just goto. If you take actions that move around the app outside of the + * navigator, you can re-sync app with navigator my telling it which node it is now at, using the `nowAt` method. + */ +class Navigator { + fileprivate let map: ScreenGraph + fileprivate var currentScene: ScreenGraphNode + fileprivate var returnToRecentScene: ScreenGraphNode + fileprivate let xcTest: XCTestCase + + fileprivate init(_ map: ScreenGraph, xcTest: XCTestCase, initialScene: ScreenGraphNode) { + self.map = map + self.xcTest = xcTest + self.currentScene = initialScene + self.returnToRecentScene = initialScene + } + + /** + * Move the application to the named node. + */ + func goto(_ nodeName: String, file: String = #file, line: UInt = #line) { + goto(nodeName, file: file, line: line, visitWith: noopNodeVisitor) + } + + /** + * Move the application to the named node, wth an optional node visitor closure, which is called each time the + * node changes. + */ + func goto(_ nodeName: String, file: String = #file, line: UInt = #line, visitWith nodeVisitor: NodeVisitor) { + let gkSrc = currentScene.gkNode + guard let gkDest = map.namedScenes[nodeName]?.gkNode else { + xcTest.recordFailure(withDescription: "Cannot route to \(nodeName), because it doesn't exist", inFile: file, atLine: line, expected: false) + return + } + + var gkPath = map.gkGraph.findPath(from: gkSrc, to: gkDest) + guard gkPath.count > 0 else { + xcTest.recordFailure(withDescription: "Cannot route from \(currentScene.name) to \(nodeName)", inFile: file, atLine: line, expected: false) + return + } + + gkPath.removeFirst() + gkPath.forEach { gkNext in + if !currentScene.dismissOnUse { + returnToRecentScene = currentScene + } + + let nextScene = map.nodedScenes[gkNext]! + let action = currentScene.edges[nextScene.name]! + + // We definitely have an action, so it's save to unbox. + action(xcTest, file, line) + + if let testElement = nextScene.existsWhen { + nextScene.waitForElement(testElement, withTest: xcTest) { _ in + // TODO report error in the correct place in the graph. + self.xcTest.recordFailure(withDescription: "Cannot find \(testElement) in \(nextScene.name)", + inFile: nextScene.file, + atLine: nextScene.line, + expected: false) + } + } + + if nextScene.hasBack { + if nextScene.returnNode == nil { + nextScene.returnNode = returnToRecentScene + nextScene.gkNode.addConnections(to: [ returnToRecentScene.gkNode ], bidirectional: false) + nextScene.gesture(to: returnToRecentScene.name, g: nextScene.backAction!) + } + } + + if currentScene.hasBack { + if nextScene.name == currentScene.returnNode?.name { + currentScene.returnNode = nil + currentScene.gkNode.removeConnections(to: [ nextScene.gkNode ], bidirectional: false) + } + } + nodeVisitor(currentScene.name) + currentScene = nextScene + } + } + + /** + * Helper method when the navigator gets out of sync with the actual app. + * This should not be used too often, as it indicates you should probably have another node in your graph, + * or you should be using `scene.dismissOnUse = true`. + * Also useful if you're using XCUIElement taps directly to navigate from one node to another. + */ + func nowAt(_ nodeName: String, file: String = #file, line: UInt = #line) { + guard let newScene = map.namedScenes[nodeName] else { + xcTest.recordFailure(withDescription: "Cannot force to unknown \(nodeName). Currently at \(currentScene.name)", inFile: file, atLine: line, expected: false) + return + } + currentScene = newScene + } + + /** + * Visit the named nodes, calling the NodeVisitor the first time it is encountered. + */ + func visitNodes(_ nodes: [String], file: String = #file, line: UInt = #line, f: NodeVisitor) { + var visitedNodes = Set() + let desiredNodes = Set(nodes) + nodes.forEach { node in + if visitedNodes.contains(node) { + return + } + self.goto(node, file: file, line: line) { visitedNode in + if desiredNodes.contains(visitedNode) && !visitedNodes.contains(visitedNode) { + f(visitedNode) + } + visitedNodes.insert(visitedNode) + } + } + } + + /** + * Visit all nodes, calling the NodeVisitor the first time it is encountered. + * + * Some nodes may not be immediately available, depending on the state of the app. + */ + func visitAll(_ file: String = #file, line: UInt = #line, f: NodeVisitor) { + let nodes: [String] = self.map.namedScenes.keys.map { $0 } // keys can't be coerced into a [String] + self.visitNodes(nodes, file: file, line: line, f: f) + } + + /** + * Move the app back to its initial state. + * This may not be possible. + */ + func revert(_ file: String = #file, line: UInt = #line) { + if let initial = self.map.initialSceneName { + self.goto(initial, file: file, line: line) + } + } +} diff --git a/mobile/ios/XCUITests/SearchSettingsUITest.swift b/mobile/ios/XCUITests/SearchSettingsUITest.swift new file mode 100644 index 0000000000..30f6235f68 --- /dev/null +++ b/mobile/ios/XCUITests/SearchSettingsUITest.swift @@ -0,0 +1,123 @@ +/* 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 XCTest + +let defaultSearchEngine1 = "Google" +let defaultSearchEngine2 = "Amazon.com" +let customSearchEngine = ["name": "youtube", "url": "http://youtube.com/search?q=%s"] + +class SearchSettingsUITests: BaseTestCase { + var navigator: Navigator! + var app: XCUIApplication! + + override func setUp() { + super.setUp() + app = XCUIApplication() + navigator = createScreenGraph(app).navigator(self) + } + + override func tearDown() { + super.tearDown() + } + + func testDefaultSearchEngine() { + navigator.goto(SearchSettings) + // Check the default browser + let defaultSearchEngine = app.tables.cells.element(boundBy: 0) + waitforExistence(app.tables.cells.staticTexts[defaultSearchEngine1]) + + // Change to another browser and check it is set as default + defaultSearchEngine.tap() + let listOfEngines = app.tables + listOfEngines.staticTexts[defaultSearchEngine2].tap() + waitforExistence(app.tables.cells.staticTexts[defaultSearchEngine2]) + } + + func testCustomSearchEngineIsEditable() { + navigator.goto(SearchSettings) + // Add a custom search engine + addCustomSearchEngine() + // Check that the custom search appears on the list + waitforExistence(app.tables.cells.staticTexts[customSearchEngine["name"]!]) + + // Check that it can be edited + XCTAssertTrue(app.buttons["Edit"].isEnabled) + app.buttons["Edit"].tap() + + waitforExistence(app.tables.buttons["Delete \(customSearchEngine["name"]!)"]) + } + + private func addCustomSearchEngine() { + waitforExistence(app.tables.cells["customEngineViewButton"]) + app.tables.cells["customEngineViewButton"].tap() + waitforExistence(app.tables.cells.staticTexts["Search Engine"]) + app.tables.cells.textViews["customEngineTitle"].tap() + app.tables.cells.textViews["customEngineTitle"].typeText(customSearchEngine["name"]!) + + app.tables.cells.textViews["customEngineUrl"].tap() + app.tables.cells.textViews["customEngineUrl"].typeText(customSearchEngine["url"]!) + + app.buttons["Save"].tap() + // Check that custom engine has been added successfully + waitforExistence(app.tables.cells.staticTexts[customSearchEngine["name"]!]) + } + + func testCustomSearchEngineAsDefaultIsNotEditable() { + navigator.goto(SearchSettings) + // Edit is disabled + XCTAssertFalse(app.buttons["Edit"].isEnabled) + + addCustomSearchEngine() + // Edit is enabled + XCTAssertTrue(app.buttons["Edit"].isEnabled) + + // Select the custom engine as the default one + let defaultSearchEngine = app.tables.cells.element(boundBy: 0) + defaultSearchEngine.tap() + let listOfEngines = app.tables + listOfEngines.staticTexts[customSearchEngine["name"]!].tap() + // Edit is disabled + XCTAssertFalse(app.buttons["Edit"].isEnabled) + } + + func testNavigateToSearchPickerTurnsOffEditing() { + navigator.goto(SearchSettings) + // Edit is disabled + XCTAssertFalse(app.buttons["Edit"].isEnabled) + + addCustomSearchEngine() + // Edit is enabled + XCTAssertTrue(app.buttons["Edit"].isEnabled) + app.buttons["Edit"].tap() + XCTAssertTrue(app.buttons["Done"].isEnabled) + + // Navigate to the search engine picker and back + let defaultSearchEngine = app.tables.cells.element(boundBy: 0) + defaultSearchEngine.tap() + app.buttons["Cancel"].tap() + + // Check to see we're not in editing state, edit is enable and done does not appear + XCTAssertTrue(app.buttons["Edit"].isEnabled) + waitforNoExistence(app.buttons["Done"]) + } + + func testDeletingLastCustomEngineExitsEditing() { + navigator.goto(SearchSettings) + // Edit is disabled + XCTAssertFalse(app.buttons["Edit"].isEnabled) + // Add a custom search engine + addCustomSearchEngine() + XCTAssertTrue(app.buttons["Edit"].isEnabled) + + app.buttons["Edit"].tap() + XCTAssertTrue(app.buttons["Done"].isEnabled) + // Remove the custom search engine and check that edit is disabled + let tablesQuery = app.tables + tablesQuery.buttons["Delete \(customSearchEngine["name"]!)"].tap() + tablesQuery.buttons["Delete"].tap() + + XCTAssertFalse(app.buttons["Edit"].isEnabled) + } +} diff --git a/mobile/ios/XCUITests/SearchTest.swift b/mobile/ios/XCUITests/SearchTest.swift new file mode 100644 index 0000000000..fe02a87b43 --- /dev/null +++ b/mobile/ios/XCUITests/SearchTest.swift @@ -0,0 +1,178 @@ +/* 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 XCTest + +private let LabelPrompt: String = "Turn on search suggestions?" +private let SuggestedSite: String = "foobar meaning" + +class SearchTests: BaseTestCase { + var navigator: Navigator! + var app: XCUIApplication! + + override func setUp() { + super.setUp() + app = XCUIApplication() + navigator = createScreenGraph(app).navigator(self) + } + + override func tearDown() { + super.tearDown() + } + + private func typeOnSearchBar(text: String) { + waitforExistence(app.textFields["address"]) + app.textFields["address"].typeText(text) + } + + private func suggestionsOnOff() { + navigator.goto(SearchSettings) + app.tables.switches["Show Search Suggestions"].tap() + app.navigationBars["Search"].buttons["Settings"].tap() + app.navigationBars["Settings"].buttons["AppSettingsTableViewController.navigationItem.leftBarButtonItem"].tap() + } + + func testPromptPresence() { + // Suggestion is on by default (starting on Oct 24th 2017), so the prompt should not appear + navigator.goto(URLBarOpen) + typeOnSearchBar(text: "foobar") + waitforNoExistence(app.staticTexts[LabelPrompt]) + + // Suggestions should be shown + waitforExistence(app.tables["SiteTable"].buttons[SuggestedSite]) + + // Disable Search suggestion + app.buttons["goBack"].tap() + navigator.nowAt(HomePanelsScreen) + suggestionsOnOff() + + // Suggestions should not be shown + waitforNoExistence(app.tables["SiteTable"].buttons[SuggestedSite]) + navigator.nowAt(BrowserTab) + navigator.goto(URLBarOpen) + typeOnSearchBar(text: "foobar") + waitforNoExistence(app.tables["SiteTable"].buttons[SuggestedSite]) + + // Verify that previous choice is remembered + app.buttons["goBack"].tap() + navigator.nowAt(HomePanelsScreen) + navigator.goto(URLBarOpen) + typeOnSearchBar(text: "foobar") + waitforNoExistence(app.tables["SiteTable"].buttons[SuggestedSite]) + app.buttons["goBack"].tap() + navigator.nowAt(HomePanelsScreen) + + // Reset suggestion button, set it to on + suggestionsOnOff() + navigator.nowAt(HomePanelsScreen) + navigator.goto(URLBarOpen) + + // Suggestions prompt should appear + typeOnSearchBar(text: "foobar") + waitforExistence(app.tables["SiteTable"].buttons[SuggestedSite]) + } + + // Promt does not appear once Search has been enabled by default, see bug: 1411184 + func testDismissPromptPresence() { + navigator.goto(URLBarOpen) + typeOnSearchBar(text: "foobar") + waitforExistence(app.staticTexts[LabelPrompt]) + + app.buttons["No"].tap() + waitforNoExistence(app.tables["SiteTable"].buttons[SuggestedSite]) + app.buttons["Go"].tap() + navigator.nowAt(BrowserTab) + // Verify that it is possible to enable suggestions after selecting No + suggestionsOnOff() + navigator.nowAt(BrowserTab) + navigator.goto(URLBarOpen) + typeOnSearchBar(text: "foobar") + waitforExistence(app.tables["SiteTable"].buttons[SuggestedSite]) + } + + func testDoNotShowSuggestionsWhenEnteringURL() { + // According to bug 1192155 if a string contains /, do not show suggestions, if there a space an a string, + // the suggestions are shown again + navigator.goto(URLBarOpen) + typeOnSearchBar(text: "foobar") + waitforNoExistence(app.staticTexts[LabelPrompt]) + + // Suggestions should be shown + waitforExistence(app.tables["SiteTable"].buttons[SuggestedSite]) + + // Typing / should stop showing suggestions + app.textFields["address"].typeText("/") + waitforNoExistence(app.tables["SiteTable"].buttons[SuggestedSite]) + + // Typing space and char after / should show suggestions again + app.textFields["address"].typeText(" b") + waitforExistence(app.tables["SiteTable"].buttons["foobar burn cd"]) + } + + func testCopyPasteComplete() { + // Copy, Paste and Go to url + navigator.goto(URLBarOpen) + typeOnSearchBar(text: "www.mozilla.org") + app.textFields["address"].press(forDuration: 5) + app.menuItems["Select All"].tap() + app.menuItems["Copy"].tap() + app.buttons["goBack"].tap() + navigator.nowAt(HomePanelsScreen) + navigator.goto(URLBarOpen) + app.textFields["address"].tap() + waitforExistence(app.menuItems["Paste"]) + app.menuItems["Paste"].tap() + + // Verify that the Paste shows the search controller with prompt + waitforNoExistence(app.staticTexts[LabelPrompt]) + app.typeText("\r") + + // Check that the website is loaded + waitForValueContains(app.textFields["url"], value: "www.mozilla.org") + + // Go back, write part of moz, check the autocompletion + if iPad() { + app.buttons["URLBarView.backButton"].tap() + } else { + app.buttons["TabToolbar.backButton"].tap() + } + navigator.nowAt(HomePanelsScreen) + navigator.goto(URLBarOpen) + typeOnSearchBar(text: "moz") + waitForValueContains(app.textFields["address"], value: "mozilla.org") + let value = app.textFields["address"].value + XCTAssertEqual(value as? String, "mozilla.org") + } + + private func changeSearchEngine(searchEngine: String) { + navigator.goto(SearchSettings) + // Open the list of default search engines and select the desired + app.tables.cells.element(boundBy: 0).tap() + let tablesQuery2 = app.tables + tablesQuery2.staticTexts[searchEngine].tap() + + navigator.openURL(urlString: "foo") + waitUntilPageLoad() + waitForValueContains(app.textFields["url"], value: searchEngine.lowercased()) + + // Go here so that next time it is possible to access settings + navigator.goto(BrowserTabMenu) + } + + func testSearchEngine() { + // Change to the each search engine and verify the search uses it + changeSearchEngine(searchEngine: "Bing") + changeSearchEngine(searchEngine: "DuckDuckGo") + changeSearchEngine(searchEngine: "Google") + changeSearchEngine(searchEngine: "Twitter") + changeSearchEngine(searchEngine: "Wikipedia") + changeSearchEngine(searchEngine: "Amazon.com") + changeSearchEngine(searchEngine: "Yahoo") + } + + func testDefaultSearchEngine() { + navigator.goto(SearchSettings) + XCTAssert(app.tables.staticTexts["Google"].exists) + } +} diff --git a/mobile/ios/XCUITests/SettingsTest.swift b/mobile/ios/XCUITests/SettingsTest.swift new file mode 100644 index 0000000000..4d44537d91 --- /dev/null +++ b/mobile/ios/XCUITests/SettingsTest.swift @@ -0,0 +1,39 @@ +/* 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 XCTest + +class SettingsTest: BaseTestCase { + var navigator: Navigator! + var app: XCUIApplication! + + override func setUp() { + super.setUp() + app = XCUIApplication() + navigator = createScreenGraph(app).navigator(self) + } + + override func tearDown() { + super.tearDown() + } + + func testHelpOpensSUMOInTab() { + navigator.goto(SettingsScreen) + let settingsTableView = app.tables["AppSettingsTableViewController.tableView"] + + while settingsTableView.staticTexts["Help"].exists == false { + settingsTableView.swipeUp() + } + let helpMenu = settingsTableView.cells["Help"] + XCTAssertTrue(helpMenu.isEnabled) + helpMenu.tap() + + waitUntilPageLoad() + waitForValueContains(app.textFields["url"], value: "support.mozilla.org") + waitforExistence(app.webViews.staticTexts["Firefox for iOS"]) + XCTAssertTrue(app.webViews.staticTexts["Firefox for iOS"].exists) + let numTabs = app.buttons["Show Tabs"].value + XCTAssertEqual("2", numTabs as? String, "Sume should be open in a different tab") + } +} diff --git a/mobile/ios/XCUITests/SiteLoadTest.swift b/mobile/ios/XCUITests/SiteLoadTest.swift new file mode 100644 index 0000000000..60c9d182dc --- /dev/null +++ b/mobile/ios/XCUITests/SiteLoadTest.swift @@ -0,0 +1,52 @@ +/* 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 XCTest + +// NOTE: This test should be DISABLED on XCUITEST/UITEST scheme. This test should be run in conjunction with +// XCode Profiler to measure the memory/CPU load +class SiteLoadTest: BaseTestCase { + + var navigator: Navigator! + var app: XCUIApplication! + + override func setUp() { + super.setUp() + app = XCUIApplication() + navigator = createScreenGraph(app).navigator(self) + } + + override func tearDown() { + super.tearDown() + } + + func testLoadSite() { + + let site = ["http://www.forbes.com", "http://www.amazon.com", "http://theguardian.com", "http://www.reddit.com", "http://cnn.com"] + let durationInHrs = 1 + let futureDate = Date().addingTimeInterval(TimeInterval(60 * 60 * durationInHrs)) + var counter = 0 + while Date() < futureDate { + navigator.goto(URLBarOpen) + navigator.openURL(urlString: site[counter % 5]) + sleep(5) + + navigator.nowAt(BrowserTab) + navigator.goto(TabTray) + + navigator.closeAllTabs() + navigator.goto(BrowserTab) + waitforNoExistence(app.staticTexts["1 tab(s) closed"]) + + // clear the cache + navigator.goto(ClearPrivateDataSettings) + app.tables.staticTexts["Clear Private Data"].tap() + waitforExistence(app.alerts.buttons["OK"]) + app.alerts.buttons["OK"].tap() + navigator.goto(BrowserTab) + waitforExistence(app.textFields["url"]) + counter += 1 + } + } +} diff --git a/mobile/ios/XCUITests/ThirdPartySearchTest.swift b/mobile/ios/XCUITests/ThirdPartySearchTest.swift new file mode 100644 index 0000000000..782c4acb81 --- /dev/null +++ b/mobile/ios/XCUITests/ThirdPartySearchTest.swift @@ -0,0 +1,178 @@ +/* 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 XCTest + +class ThirdPartySearchTest: BaseTestCase { + var navigator: Navigator! + var app: XCUIApplication! + + override func setUp() { + super.setUp() + app = XCUIApplication() + navigator = createScreenGraph(app).navigator(self) + dismissFirstRunUI() + } + + override func tearDown() { + super.tearDown() + } + + fileprivate func dismissKeyboardAssistant(forApp app: XCUIApplication) { + if iPad() { + let searchTheDocsSearchField = app.webViews.searchFields["Search the docs"] + searchTheDocsSearchField.typeText("\r") + } else { + app.buttons["Done"].tap() + } + } + + func testCustomSearchEngines() { + // Visit MDN to add a custom search engine + loadWebPage("https://developer.mozilla.org/en-US/search", waitForLoadToFinish: true) + if iPad() { + let searchTheDocsSearchField = app.webViews.searchFields["Search the docs"] + searchTheDocsSearchField.tap() + app.keyboards.buttons["Search"].tap() + searchTheDocsSearchField.tap() + } else { + app.webViews.searchFields.element(boundBy: 0).tap() + } + app.buttons["AddSearch"].tap() + app.alerts["Add Search Provider?"].buttons["OK"].tap() + XCTAssertFalse(app.buttons["AddSearch"].isEnabled) + dismissKeyboardAssistant(forApp: app) + + // Perform a search using a custom search engine + tabTrayButton(forApp: app).tap() + app.buttons["TabTrayController.addTabButton"].tap() + app.textFields["url"].tap() + app.typeText("window") + app.scrollViews.otherElements.buttons["developer.mozilla.org search"].tap() + + var url = app.textFields["url"].value as! String + if url.hasPrefix("https://") == false { + url = "https://\(url)" + } + XCTAssert(url.hasPrefix("https://developer.mozilla.org/en-US/search"), "The URL should indicate that the search was performed on MDN and not the default") + } + + func testCustomSearchEngineAsDefault() { + // Visit MDN to add a custom search engine + loadWebPage("https://developer.mozilla.org/en-US/search", waitForLoadToFinish: true) + + if iPad() { + let searchTheDocsSearchField = app.webViews.searchFields["Search the docs"] + searchTheDocsSearchField.tap() + app.keyboards.buttons["Search"].tap() + searchTheDocsSearchField.tap() + } else { + app.webViews.searchFields.element(boundBy: 0).tap() + } + app.buttons["AddSearch"].tap() + app.alerts["Add Search Provider?"].buttons["OK"].tap() + XCTAssertFalse(app.buttons["AddSearch"].isEnabled) + dismissKeyboardAssistant(forApp: app) + + // Go to settings and set MDN as the default + navigator.goto(SearchSettings) + app.tables.cells.element(boundBy: 0).tap() + app.tables.staticTexts["developer.mozilla.org"].tap() + navigator.goto(BrowserTab) + + // Perform a search to check + navigator.openNewURL(urlString:"window") + + // Ensure that the default search is MDN + var url = app.textFields["url"].value as! String + if url.hasPrefix("https://") == false { + url = "https://\(url)" + } + XCTAssert(url.hasPrefix("https://developer.mozilla.org/en-US/search"), "The URL should indicate that the search was performed on MDN and not the default") + } + + func testCustomSearchEngineDeletion() { + // Visit MDN to add a custom search engine + loadWebPage("https://developer.mozilla.org/en-US/search", waitForLoadToFinish: true) + + if iPad() { + let searchTheDocsSearchField = app.webViews.searchFields["Search the docs"] + searchTheDocsSearchField.tap() + app.keyboards.buttons["Search"].tap() + searchTheDocsSearchField.tap() + } else { + app.webViews.searchFields.element(boundBy: 0).tap() + } + app.buttons["AddSearch"].tap() + app.alerts["Add Search Provider?"].buttons["OK"].tap() + XCTAssertFalse(app.buttons["AddSearch"].isEnabled) + dismissKeyboardAssistant(forApp: app) + + let tabTrayButton = self.tabTrayButton(forApp: app) + tabTrayButton.tap() + app.buttons["TabTrayController.addTabButton"].tap() + app.textFields["url"].tap() + app.typeText("window") + + // For timing issue, we need a wait statement + waitforExistence(app.scrollViews.otherElements.buttons["developer.mozilla.org search"]) + XCTAssert(app.scrollViews.otherElements.buttons["developer.mozilla.org search"].exists) + app.typeText("\r") + + // Go to settings and set MDN as the default + navigator.goto(SearchSettings) + + app.navigationBars["Search"].buttons["Edit"].tap() + app.tables.buttons["Delete developer.mozilla.org"].tap() + app.tables.buttons["Delete"].tap() + + navigator.goto(BrowserTab) + + // Perform a search to check + tabTrayButton.tap(force: true) + app.buttons["TabTrayController.addTabButton"].tap() + + app.textFields["url"].tap() + app.typeText("window") + waitforNoExistence(app.scrollViews.otherElements.buttons["developer.mozilla.org search"]) + XCTAssertFalse(app.scrollViews.otherElements.buttons["developer.mozilla.org search"].exists) + + } + + func testCustomEngineFromCorrectTemplate() { + navigator.goto(SearchSettings) + app.tables.cells["customEngineViewButton"].tap() + + app.textViews["customEngineTitle"].tap() + app.typeText("Feeling Lucky") + app.textViews["customEngineUrl"].tap() + app.typeText("http://www.google.com/search?q=%s&btnI") + + app.navigationBars.buttons["customEngineSaveButton"].tap() + + // Perform a search using a custom search engine + navigator.goto(HomePanelsScreen) + app.textFields["url"].tap() + app.typeText("strange charm") + app.scrollViews.otherElements.buttons["Feeling Lucky search"].tap() + + // Ensure that correct search is done + let url = app.textFields["url"].value as! String + XCTAssert(url.hasSuffix("&btnI"), "The URL should indicate that the search was performed using IFL") + } + + func testCustomEngineFromIncorrectTemplate() { + navigator.goto(SearchSettings) + app.tables.cells["customEngineViewButton"].tap() + app.textViews["customEngineTitle"].tap() + app.typeText("Feeling Lucky") + app.textViews["customEngineUrl"].tap() + app.typeText("http://www.google.com/search?q=&btnI") //Occurunces of %s != 1 + + app.navigationBars.buttons["customEngineSaveButton"].tap() + + waitforExistence(app.alerts.element(boundBy: 0)) + XCTAssert(app.alerts.element(boundBy: 0).label == "Failed") + } +} diff --git a/mobile/ios/XCUITests/ToolbarTest.swift b/mobile/ios/XCUITests/ToolbarTest.swift new file mode 100644 index 0000000000..39c609ee12 --- /dev/null +++ b/mobile/ios/XCUITests/ToolbarTest.swift @@ -0,0 +1,94 @@ +/* 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 XCTest + +let website1: [String: String] = ["url": "www.mozilla.org", "label": "Internet for people, not profit — Mozilla", "value": "mozilla.org"] +let website2 = "yahoo.com" + +class ToolbarTests: BaseTestCase { + var navigator: Navigator! + var app: XCUIApplication! + + override func setUp() { + super.setUp() + app = XCUIApplication() + XCUIDevice.shared().orientation = UIDeviceOrientation.landscapeLeft + navigator = createScreenGraph(app).navigator(self) + } + + override func tearDown() { + XCUIDevice.shared().orientation = UIDeviceOrientation.portrait + super.tearDown() + } + + /** + * Tests landscape page navigation enablement with the URL bar with tab switching. + */ + func testLandscapeNavigationWithTabSwitch() { + let urlPlaceholder = "Search or enter address" + XCTAssert(app.textFields["url"].exists) + let defaultValuePlaceholder = app.textFields["url"].placeholderValue! + + // Check the url placeholder text and that the back and forward buttons are disabled + XCTAssertTrue(urlPlaceholder == defaultValuePlaceholder, "The placeholder does not show the correct value") + XCTAssertFalse(app.buttons["URLBarView.backButton"].isEnabled) + XCTAssertFalse(app.buttons["Forward"].isEnabled) + XCTAssertFalse(app.buttons["Reload"].isEnabled) + + // Navigate to two pages and press back once so that all buttons are enabled in landscape mode. + navigator.openURL(urlString: website1["url"]!) + waitForValueContains(app.textFields["url"], value: website1["value"]!) + + XCTAssertTrue(app.buttons["URLBarView.backButton"].isEnabled) + XCTAssertFalse(app.buttons["Forward"].isEnabled) + XCTAssertTrue(app.buttons["Reload"].isEnabled) + + navigator.openURL(urlString: website2) + waitUntilPageLoad() + waitForValueContains(app.textFields["url"], value: website2) + XCTAssertTrue(app.buttons["URLBarView.backButton"].isEnabled) + XCTAssertFalse(app.buttons["Forward"].isEnabled) + + app.buttons["URLBarView.backButton"].tap() + waitForValueContains(app.textFields["url"], value: website1["value"]!) + waitUntilPageLoad() + XCTAssertTrue(app.buttons["URLBarView.backButton"].isEnabled) + XCTAssertTrue(app.buttons["Forward"].isEnabled) + + // Open new tab and then go back to previous tab to test navigation buttons. + navigator.goto(TabTray) + waitforExistence(app.collectionViews.cells[website1["label"]!]) + app.collectionViews.cells[website1["label"]!].tap() + waitForValueContains(app.textFields["url"], value: website1["value"]!) + + // Test to see if all the buttons are enabled then close tab. + waitUntilPageLoad() + XCTAssertTrue(app.buttons["URLBarView.backButton"].isEnabled) + XCTAssertTrue(app.buttons["Forward"].isEnabled) + + navigator.nowAt(BrowserTab) + navigator.goto(TabTray) + + waitforExistence(app.collectionViews.cells[website1["label"]!]) + app.collectionViews.cells[website1["label"]!].swipeRight() + + // Go Back to other tab to see if all buttons are disabled. + navigator.nowAt(BrowserTab) + XCTAssertFalse(app.buttons["URLBarView.backButton"].isEnabled) + XCTAssertFalse(app.buttons["Forward"].isEnabled) + } + + func testClearURLTextUsingBackspace() { + navigator.openURL(urlString: website1["url"]!) + waitForValueContains(app.textFields["url"], value: website1["value"]!) + + // Simulate pressing on backspace key should remove the text + app.textFields["url"].tap() + app.textFields["address"].typeText("\u{8}") + + let value = app.textFields["address"].value + XCTAssertEqual(value as? String, "", "The url has not been removed correctly") + } +} diff --git a/mobile/ios/XCUITests/TopTabsTest.swift b/mobile/ios/XCUITests/TopTabsTest.swift new file mode 100644 index 0000000000..fdfc6f97b6 --- /dev/null +++ b/mobile/ios/XCUITests/TopTabsTest.swift @@ -0,0 +1,178 @@ +/* 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 XCTest + +let url = "www.mozilla.org" +let urlLabel = "Internet for people, not profit — Mozilla" +let urlValue = "mozilla.org" + +let urlYah = "www.yahoo.com" +let urlLabelYah = "Yahoo" +let urlValueYah = "yahoo" + +let urlExample = "example.com" + +class TopTabsTest: BaseTestCase { + var navigator: Navigator! + var app: XCUIApplication! + + override func setUp() { + super.setUp() + app = XCUIApplication() + navigator = createScreenGraph(app).navigator(self) + } + + override func tearDown() { + super.tearDown() + } + + func testAddTabFromSettings() { + navigator.createNewTab() + navigator.openURL(urlString: url) + waitForValueContains(app.textFields["url"], value: urlValue) + waitforExistence(app.buttons["Show Tabs"]) + let numTab = app.buttons["Show Tabs"].value as? String + + XCTAssertEqual("2", numTab) + } + + func testAddTabFromTabTray() { + navigator.goto(TabTray) + navigator.openURL(urlString: url) + waitUntilPageLoad() + waitForValueContains(app.textFields["url"], value: urlValue) + // The tabs counter shows the correct number + let tabsOpen = app.buttons["Show Tabs"].value + XCTAssertEqual("2", tabsOpen as? String) + + // The tab tray shows the correct tabs + navigator.goto(TabTray) + waitforExistence(app.collectionViews.cells[urlLabel]) + } + + private func checkNumberOfTabsExpectedToBeOpen(expectedNumberOfTabsOpen: Int) { + let numTabsOpen = app.collectionViews.cells.count + XCTAssertEqual(numTabsOpen, UInt(expectedNumberOfTabsOpen), "The number of tabs open is not correct") + } + + func testAddTabFromContext() { + navigator.openURL(urlString: urlExample) + // Initially there is only one tab open + let tabsOpenInitially = app.buttons["Show Tabs"].value + XCTAssertEqual("1", tabsOpenInitially as? String) + + // Open link in a different tab and switch to it + waitforExistence(app.webViews.links.staticTexts["More information..."]) + app.webViews.links.staticTexts["More information..."].press(forDuration: 5) + app.buttons["Open in New Tab"].tap() + waitUntilPageLoad() + + // Open tab tray to check that both tabs are there + navigator.goto(TabTray) + waitforExistence(app.collectionViews.cells["Example Domain"]) + waitforExistence(app.collectionViews.cells["IANA — IANA-managed Reserved Domains"]) + + checkNumberOfTabsExpectedToBeOpen(expectedNumberOfTabsOpen: 2) + } + + // This test only runs for iPhone see bug 1409750 + func testAddTabByLongPressTabsButton() { + navigator.goto(BrowserTab) + app.buttons["TabToolbar.tabsButton"].press(forDuration: 1) + + waitforExistence(app.buttons["New Tab"]) + app.buttons["New Tab"].tap() + navigator.goto(TabTray) + + checkNumberOfTabsExpectedToBeOpen(expectedNumberOfTabsOpen: 2) + } + + // This test only runs for iPhone see bug 1409750 + func testAddPrivateTabByLongPressTabsButton() { + navigator.goto(BrowserTab) + app.buttons["TabToolbar.tabsButton"].press(forDuration: 1) + + waitforExistence(app.buttons["New Private Tab"]) + app.buttons["New Private Tab"].tap() + navigator.goto(TabTray) + + waitforExistence(app.buttons["TabTrayController.maskButton"]) + XCTAssertTrue(app.buttons["TabTrayController.maskButton"].isEnabled) + checkNumberOfTabsExpectedToBeOpen(expectedNumberOfTabsOpen: 1) + } + + func testSwitchBetweenTabs() { + // Open two urls from tab tray and switch between them + navigator.openURL(urlString: url) + navigator.goto(TabTray) + navigator.openURL(urlString: urlYah) + navigator.goto(TabTray) + + waitforExistence(app.collectionViews.cells[urlLabel]) + app.collectionViews.cells[urlLabel].tap() + waitForValueContains(app.textFields["url"], value: urlValue) + + navigator.nowAt(BrowserTab) + navigator.goto(TabTray) + + waitforExistence(app.collectionViews.cells[urlLabelYah]) + app.collectionViews.cells[urlLabelYah].tap() + waitForValueContains(app.textFields["url"], value: urlValueYah) + } + + func testCloseOneTab() { + navigator.openURL(urlString: url) + waitUntilPageLoad() + navigator.goto(TabTray) + + waitforExistence(app.collectionViews.cells[urlLabel]) + + // 'x' button to close the tab is not visible, so closing by swiping the tab + app.collectionViews.cells[urlLabel].swipeRight() + + // After removing only one tab it automatically goes to HomepanelView + waitforExistence(app.collectionViews.cells["TopSitesCell"]) + XCTAssert(app.buttons["HomePanels.TopSites"].exists) + } + + func testCloseAllTabsUndo() { + // A different tab than home is open to do the proper checks + navigator.openURL(urlString: url) + waitUntilPageLoad() + navigator.createSeveralTabsFromTabTray (numberTabs: 3) + navigator.goto(TabTray) + + waitforExistence(app.collectionViews.cells[urlLabel]) + checkNumberOfTabsExpectedToBeOpen(expectedNumberOfTabsOpen: 4) + + // Close all tabs, undo it and check that the number of tabs is correct + navigator.closeAllTabs() + app.buttons["Undo"].tap() + navigator.nowAt(BrowserTab) + navigator.goto(TabTray) + + waitforExistence(app.collectionViews.cells[urlLabel]) + checkNumberOfTabsExpectedToBeOpen(expectedNumberOfTabsOpen: 4) + } + + func testCloseAllTabs() { + // A different tab than home is open to do the proper checks + navigator.openURL(urlString: url) + waitUntilPageLoad() + // Add several tabs from tab tray menu and check that the number is correct before closing all + navigator.createSeveralTabsFromTabTray (numberTabs: 3) + navigator.goto(TabTray) + + waitforExistence(app.collectionViews.cells[urlLabel]) + checkNumberOfTabsExpectedToBeOpen(expectedNumberOfTabsOpen: 4) + + // Close all tabs and check that the number of tabs is correct + navigator.closeAllTabs() + navigator.goto(TabTray) + + waitforNoExistence(app.collectionViews.cells[urlLabel]) + checkNumberOfTabsExpectedToBeOpen(expectedNumberOfTabsOpen: 1) + } +} diff --git a/mobile/ios/XCUITests/TrackingProtectionTests.swift b/mobile/ios/XCUITests/TrackingProtectionTests.swift new file mode 100644 index 0000000000..b34525f4de --- /dev/null +++ b/mobile/ios/XCUITests/TrackingProtectionTests.swift @@ -0,0 +1,65 @@ +/* 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 XCTest + +class TrackingProtectionTests: BaseTestCase { + var navigator: Navigator! + var app: XCUIApplication! + + override func setUp() { + super.setUp() + app = XCUIApplication() + navigator = createScreenGraph(app).navigator(self) + } + + override func tearDown() { + navigator = nil + app = nil + super.tearDown() + } + + // This test is to change the tracking protection to block known blockers + func testTrackingProtection() { + navigator.goto(SettingsScreen) + let appSettingsTableView = app.tables["AppSettingsTableViewController.tableView"] + //Scroll the table view until Tracking Proection cell is visible + while !app.staticTexts["Tracking Protection"].exists { + appSettingsTableView.swipeUp() + } + appSettingsTableView.staticTexts["Tracking Protection"].tap() + + //Check "Private Browsing Mode Only" is selected and other items are not selected + XCTAssertFalse(app.tables.cells["Always On"].isSelected) + XCTAssertTrue(app.tables.cells["Private Browsing Mode Only"].isSelected) + XCTAssertFalse(app.tables.cells["Never"].isSelected) + + //Select "Always On" + app.tables.cells["Always On"].tap() + XCTAssertTrue(app.tables.cells["Always On"].isSelected) + + app.navigationBars["Tracking Protection"].buttons["Settings"].tap() + + waitforExistence(app.navigationBars["Settings"].buttons["Done"]) + app.navigationBars["Settings"]/*@START_MENU_TOKEN@*/.buttons["Done"]/*[[".buttons[\"Done\"]",".buttons[\"AppSettingsTableViewController.navigationItem.leftBarButtonItem\"]"],[[[-1,1],[-1,0]]],[1]]@END_MENU_TOKEN@*/.tap() + + app/*@START_MENU_TOKEN@*/.buttons["TabToolbar.menuButton"]/*[[".buttons[\"Menu\"]",".buttons[\"TabToolbar.menuButton\"]"],[[[-1,1],[-1,0]]],[0]]@END_MENU_TOKEN@*/.tap() + let settingsmenuitemCell = app.tables.cells["Settings"] + settingsmenuitemCell.tap() + + let appSettingsTableView1 = app.tables["AppSettingsTableViewController.tableView"] + //Scroll the table view until Tracking Proection cell is visible + while !app.staticTexts["Tracking Protection"].exists { + appSettingsTableView1.swipeUp() + } + + appSettingsTableView1.staticTexts["Tracking Protection"].tap() + waitforExistence(app.tables.cells["Always On"]) + + //Check "Always On" is selected and other items are not selected + XCTAssertTrue(app.tables.cells["Always On"].isSelected) + XCTAssertFalse(app.tables.cells["Private Browsing Mode Only"].isSelected) + XCTAssertFalse(app.tables.cells["Never"].isSelected) + } +} diff --git a/mobile/ios/XCUITests/XCUITests-Bridging-Header.h b/mobile/ios/XCUITests/XCUITests-Bridging-Header.h new file mode 100644 index 0000000000..a7c30cbdb2 --- /dev/null +++ b/mobile/ios/XCUITests/XCUITests-Bridging-Header.h @@ -0,0 +1,14 @@ +/* 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/. */ + +#ifndef XCUITests_Bridging_Header_h +#define XCUITests_Bridging_Header_h + +#import "XCTest/XCUIApplication.h" +#import "XCTest/XCUIElement.h" +@interface XCUIApplication (Private) +- (id)initPrivateWithPath:(NSString *)path bundleID:(NSString *)bundleID; +@end + +#endif diff --git a/mobile/ios/bootstrap.sh b/mobile/ios/bootstrap.sh new file mode 100644 index 0000000000..07578f168b --- /dev/null +++ b/mobile/ios/bootstrap.sh @@ -0,0 +1,28 @@ +#!/bin/sh + +# +# 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/. */ +# +# Bootstrap the Carthage dependencies. If the Carthage directory +# already exists then nothing is done. This speeds up builds on +# CI services where the Carthage directory can be cached. +# +# Use the --force option to force a rebuild of the dependencies. +# + +if [ "$1" == "--force" ]; then + rm -rf Carthage/* + rm -rf ~/Library/Caches/org.carthage.CarthageKit +fi + +# Only enable this on the Xcode Server because it times out if it does not +# get any output for some time while building the dependencies. + +CARTHAGE_VERBOSE="" +if [ ! -z "$XCS_BOT_ID" ]; then + CARTHAGE_VERBOSE="--verbose" +fi + +carthage bootstrap $CARTHAGE_VERBOSE --platform ios --color auto --cache-builds diff --git a/mobile/ios/buddybuild_carthage_command.sh b/mobile/ios/buddybuild_carthage_command.sh new file mode 100644 index 0000000000..0fcaece8a1 --- /dev/null +++ b/mobile/ios/buddybuild_carthage_command.sh @@ -0,0 +1,9 @@ +#!/bin/bash + +# This is needed for Leanplum. Our bootstrap.sh is not run on +# BuddyBuild and the carthage command that BuddyBuild runs is +# not compatible with Leanplum for some reason. (It seems to +# have a problem with the --toolchain argument). + +carthage bootstrap --platform ios + diff --git a/mobile/ios/buddybuild_postbuild.sh b/mobile/ios/buddybuild_postbuild.sh new file mode 100644 index 0000000000..3fcf233563 --- /dev/null +++ b/mobile/ios/buddybuild_postbuild.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env bash +chruby 2.3.1 +bundle install +bundle exec danger --fail-on-errors=false + +bash <(curl -s https://codecov.io/bash) diff --git a/mobile/ios/buddybuild_postclone.sh b/mobile/ios/buddybuild_postclone.sh new file mode 100644 index 0000000000..2ca4215d06 --- /dev/null +++ b/mobile/ios/buddybuild_postclone.sh @@ -0,0 +1,26 @@ +#!/usr/bin/env bash + +# Only setup virtualenv if we intend on localizing the app. +function setup_virtualenv { + # Install Python tooling for localizations scripts + echo password | sudo easy_install pip + echo password | sudo -S pip install --upgrade pip + echo password | sudo -S pip install virtualenv +} + +# +# Import only the shipping locales (from shipping_locales.txt) on our Beta and +# Release builds. Import all locales on Fennec_Enterprise, except for pull requests. +# + +git clone https://github.com/mozilla-mobile/ios-l10n-scripts.git || exit 1 + +if [ "$BUDDYBUILD_SCHEME" = "Firefox" ] || [ "$BUDDYBUILD_SCHEME" = "FirefoxBeta" ]; then + setup_virtualenv + ./ios-l10n-scripts/import-locales-firefox.sh --release +fi + +if [ "$BUDDYBUILD_SCHEME" = "Fennec_Enterprise" ] && [ "$BUDDYBUILD_PULL_REQUEST" = "" ]; then + setup_virtualenv + ./ios-l10n-scripts/import-locales-firefox.sh +fi diff --git a/mobile/ios/buddybuild_prebuild.sh b/mobile/ios/buddybuild_prebuild.sh new file mode 100644 index 0000000000..51de61047c --- /dev/null +++ b/mobile/ios/buddybuild_prebuild.sh @@ -0,0 +1,61 @@ +#!/usr/bin/env bash + +# +# Add our Adjust keys to the build depending on the scheme. We use the sandbox for beta so +# that we have some insight in beta usage. +# + +if [ "$BUDDYBUILD_SCHEME" == FirefoxBeta ]; then + echo "Setting Adjust environment to SANDBOX for $BUDDYBUILD_SCHEME" + /usr/libexec/PlistBuddy -c "Set AdjustAppToken $ADJUST_KEY_BETA" "Client/Info.plist" + /usr/libexec/PlistBuddy -c "Set AdjustEnvironment production" "Client/Info.plist" +elif [ "$BUDDYBUILD_SCHEME" == Firefox ]; then + echo "Setting Adjust environment to PRODUCTION for $BUDDYBUILD_SCHEME" + /usr/libexec/PlistBuddy -c "Set AdjustAppToken $ADJUST_KEY_PRODUCTION" "Client/Info.plist" + /usr/libexec/PlistBuddy -c "Set AdjustEnvironment production" "Client/Info.plist" +fi + +# +# Enable File Sharing on all builds except release +# + +if [ "$BUDDYBUILD_SCHEME" != "Firefox" ]; then + /usr/libexec/PlistBuddy -c "Add UIFileSharingEnabled bool true" "Client/Info.plist" +fi + +# +# Leanplum is included only for the Firefox and FirefoxBeta builds. +# + +if [ "$BUDDYBUILD_SCHEME" = "Firefox" ] || [ "$BUDDYBUILD_SCHEME" = "FirefoxBeta" ]; then + echo "Setting Leanplum environment to PRODUCTION for $BUDDYBUILD_SCHEME" + /usr/libexec/PlistBuddy -c "Set LeanplumAppId $LEANPLUM_APP_ID" "Client/Info.plist" + /usr/libexec/PlistBuddy -c "Set LeanplumProductionKey $LEANPLUM_KEY_PRODUCTION" "Client/Info.plist" + /usr/libexec/PlistBuddy -c "Set LeanplumDevelopmentKey $LEANPLUM_KEY_DEVELOPMENT" "Client/Info.plist" +fi + +echo "Setting up Pocket Stories API Key" +if [ "$BUDDYBUILD_SCHEME" == Firefox ]; then + /usr/libexec/PlistBuddy -c "Set PocketEnvironmentAPIKey $POCKET_PRODUCTION_API_KEY" "Client/Info.plist" +else + /usr/libexec/PlistBuddy -c "Set PocketEnvironmentAPIKey $POCKET_STAGING_API_KEY" "Client/Info.plist" +fi + +# +# Setup Sentry. We have different DSNs for Beta and Production. +# + +if [ "$BUDDYBUILD_SCHEME" == FirefoxBeta ]; then + echo "Setting SentryDSN to $SENTRY_DSN_BETA_105" + /usr/libexec/PlistBuddy -c "Set SentryDSN $SENTRY_DSN_BETA_105" "Client/Info.plist" +elif [ "$BUDDYBUILD_SCHEME" == Firefox ]; then + echo "Setting SentryDSN to $SENTRY_DSN_RELEASE_105" + /usr/libexec/PlistBuddy -c "Set SentryDSN $SENTRY_DSN_RELEASE_105" "Client/Info.plist" +fi + +# +# Set the build number to match the Buddybuild number +# + +agvtool new-version -all "$BUDDYBUILD_BUILD_NUMBER" + diff --git a/mobile/ios/codecov.yml b/mobile/ios/codecov.yml new file mode 100644 index 0000000000..70323c2c8e --- /dev/null +++ b/mobile/ios/codecov.yml @@ -0,0 +1,23 @@ +coverage: + precision: 0 + round: down + status: + project: + default: + threshold: 100 + if_no_uploads: success + if_not_found: success + if_ci_failed: success + patch: + default: + enabled: no + threshold: 30 + if_no_uploads: success + if_not_found: success + if_ci_failed: success + changes: no +comment: false +ignore: +- "UITests/.*" +- "XCUITests/.*" +- "ThirdParty/.*" diff --git a/mobile/ios/fastlane/Fastfile b/mobile/ios/fastlane/Fastfile new file mode 100644 index 0000000000..6da5d6c7f6 --- /dev/null +++ b/mobile/ios/fastlane/Fastfile @@ -0,0 +1,26 @@ +# 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/. + +fastlane_version "1.39.0" + +default_platform :ios + + +import_build_tools( + url: 'git@github.com:mozilla/firefox-ios-build-tools.git', + clone_folder: "../build-tools", +) + +#move all the build tools to the right places +clone_folder = "../../build-tools" +sh("rsync -a #{clone_folder}/scripts ../") +sh("rsync -a #{clone_folder}/fastlane/Appfile Appfile") +sh("rsync -a #{clone_folder}/fastlane/Snapfile Snapfile") +sh("rsync -a #{clone_folder}/fastlane/SnapshotHelper.swift SnapshotHelper.swift") +sh("rsync -a #{clone_folder}/fastlane/scripts .") +sh("rsync -a #{clone_folder}/fastlane/frames .") +sh("rsync -a #{clone_folder}/fastlane/templates .") + +#import the fastfile +import "#{clone_folder}/fastlane/BaseFastfile" diff --git a/mobile/ios/fastlane/actions/import_build_tools.rb b/mobile/ios/fastlane/actions/import_build_tools.rb new file mode 100644 index 0000000000..91571ca55b --- /dev/null +++ b/mobile/ios/fastlane/actions/import_build_tools.rb @@ -0,0 +1,79 @@ +module Fastlane + module Actions + + class ImportBuildToolsAction < Action + def self.run(params) + # fastlane will take care of reading in the parameter and fetching the environment variable: + Helper.log.info "Parameter URL: #{params[:url]}" + Helper.log.info "Parameter Clone Folder: #{params[:clone_folder]}" + Helper.log.info "Parameter Branch: #{params[:branch]}" + directory = params[:clone_folder] + + git_command = "" + if File.directory?(directory) + Helper.log.info("Fetching latest version of build tools from #{directory}") + branch_option = "" + branch_option = "git checkout #{params[:branch]}\n" if params[:branch] != 'HEAD' + git_command = "cd #{directory}\n \ + git checkout master\n \ + git fetch\n \ + #{branch_option} \ + git pull" + else + Helper.log.info("Cloning build tools repository") + #import from git into subdir + branch_option = "" + branch_option = "--branch #{params[:branch]}" if params[:branch] != 'HEAD' + + git_command = "git clone '#{params[:url]}' '#{directory}' #{branch_option}" + end + + Helper.log.info("Excuting #{git_command}") + Actions.sh(git_command) + end + + ##################################################### + # @!group Documentation + ##################################################### + + def self.description + "Downloads an Git repo to a given location" + end + + def self.details + "Downloads a Git repo <:url> to a given location <:clone_folder> and checks out a specific branch if <:branch> is provided" + end + + def self.available_options + [ + FastlaneCore::ConfigItem.new(key: :url, + env_name: "FL_IMPORT_BUILD_TOOLS_URL", # The name of the environment variable + description: "URL of github repository that contains build tools", # a short description of this parameter + verify_block: proc do |value| + raise "No URL for ImportBuildToolsAction given, pass using `url: 'value'`".red unless (value and not value.empty?) + # raise "Couldn't find file at path '#{value}'".red unless File.exist?(value) + end), + FastlaneCore::ConfigItem.new(key: :clone_folder, + env_name: "FL_IMPORT_BUILD_TOOLS_CLONE_FOLDER", # The name of the environment variable + description: "path to import build tools to", # a short description of this parameter + verify_block: proc do |value| + raise "No Clone folder for ImportBuildToolsAction given, pass using `clone_folder: 'path'`".red unless (value and not value.empty?) + # raise "Couldn't find file at path '#{value}'".red unless File.exist?(value) + end), + FastlaneCore::ConfigItem.new(key: :branch, + env_name: "FL_IMPORT_BUILD_TOOLS_BRANCH", + description: "Branch of build tools to import", + default_value: "HEAD") # the default value if the user didn't provide one + ] + end + + def self.authors + ["Mozilla"] + end + + def self.is_supported?(platform) + platform == :ios + end + end + end +end diff --git a/mobile/ios/l10n-screenshots.sh b/mobile/ios/l10n-screenshots.sh new file mode 100644 index 0000000000..82a9160712 --- /dev/null +++ b/mobile/ios/l10n-screenshots.sh @@ -0,0 +1,29 @@ +#!/bin/bash + +if [ -d l10n-screenshots ]; then + echo "The l10n-screenshots directory already exists. You decide." + exit 1 +fi + +if [ ! -d firefoxios-l10n ]; then + echo "Did not find a firefox-ios-l10n checkout. Are you running this on a localized build?" + exit 1 +fi + +mkdir l10n-screenshots + +LOCALES=$* +if [ $# -eq 0 ]; then + LOCALES="af ar ast az bg bn br bs ca cs cy da de dsb el en-GB en-US eo es es-CL es-MX eu fa fr ga-IE gd gl he hi-IN hsb hu hy-AM id is it ja kab kk km kn ko lo lt lv ml ms my nb-NO ne-NP nl nn-NO or pa-IN pl pt-BR pt-PT rm ro ru ses si sk sl sq sv-SE te th tl tn tr uk ur uz zh-CN zh-TW" +fi + +for lang in $LOCALES; do + echo "$(date) Snapshotting $lang" + mkdir "l10n-screenshots/$lang" + fastlane snapshot --project Client.xcodeproj --scheme L10nSnapshotTests \ + --skip_open_summary \ + --derived_data_path l10n-screenshots-dd \ + --erase_simulator --localize_simulator \ + --devices "iPhone 5s" --languages "$lang" \ + --output_directory "l10n-screenshots/$lang" > "l10n-screenshots/$lang/snapshot.log" 2>&1 +done diff --git a/mobile/ios/setup_build_tools.sh b/mobile/ios/setup_build_tools.sh new file mode 100644 index 0000000000..967c3079cd --- /dev/null +++ b/mobile/ios/setup_build_tools.sh @@ -0,0 +1,88 @@ +#!/bin/sh + +# +# Install all the required dependencies for building and deploying Firefox for iOS +# Assumes you already have git otherwise you wouldn't have this setup script +# +# run ./setup.sh from the command line to run +# + +# +# Check if XCode Command Line Tools are installed +# +which -s xcode-select +if [[ $? != 0 ]] ; then + echo "Installing XCode Command Line Tools" + # Install XCode Command Line Tools + xcode-select --install +else + echo "XCode Command Line Tools already installed" +fi + +# +# Check if Homebrew is installed +# +which -s brew +if [[ $? != 0 ]] ; then + # Install Homebrew + echo "Installing Homebrew" + ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)" +else + echo "Homebrew already installed" +fi + +# +# Check if python is installed +# +if [ ! -e $(python -c 'from distutils.sysconfig import get_makefile_filename as m; print m()') ]; then + # Install python + echo "Installing python" + brew install python +else + echo "python already installed" +fi + +# +# Check if virtualenv is installed +# +which -s virtualenv +if [[ $? != 0 ]] ; then + # Install virtualenv + echo "Installing vitualenv" + pip install virtualenv +else + echo "virtualenv already installed" +fi + +if [ ! -d /usr/local/Cellar/imagemagick ] ; then + echo "installing imagemagick" + brew install imagemagick +else + echo "imagemagick already installed" +fi + +# +# Check is Carthage is installed +# +which -s carthage +if [[ $? != 0 ]] ; then + # Install Carthage + echo "Installing Carthage" + brew install carthage +else + echo "Carthage already installed" +fi + +# +# Check if fastlane is installed +# +which -s fastlane +if [[ $? != 0 ]] ; then + # Install fastlane + echo "Installing fastlane." + sudo gem install fastlane + fastlane init +else + echo "fastlane already installed" +fi + diff --git a/mobile/ios/shipping_locales.txt b/mobile/ios/shipping_locales.txt new file mode 100644 index 0000000000..5247ff84e1 --- /dev/null +++ b/mobile/ios/shipping_locales.txt @@ -0,0 +1,73 @@ +af +ast +az +bg +bn +br +bs +ca +cs +cy +da +de +dsb +el +en-GB +en-US +eo +es +es-AR +es-CL +es-MX +eu +fr +ga-IE +gd +gl +hi-IN +hsb +hu +hy-AM +ia +id +is +it +ja +kab +kk +km +kn +ko +lo +lt +lv +ml +ms +my +nb-NO +ne-NP +nl +nn-NO +or +pa-IN +pl +pt-BR +pt-PT +rm +ro +ru +ses +si +sk +sl +sq +sv-SE +ta +te +th +tl +tr +uk +uz +zh-CN +zh-TW